Xin chào! Tôi là Michael Chang, làm việc tại Nhóm Nghệ thuật dữ liệu của Google. Gần đây, chúng tôi đã hoàn thành 100.000 ngôi sao, một Thử nghiệm trên Chrome giúp hình dung các ngôi sao ở gần. Dự án này được xây dựng bằng THREE.js và CSS3D. Trong nghiên cứu điển hình này, tôi sẽ trình bày quy trình khám phá, chia sẻ một số kỹ thuật lập trình và kết thúc bằng một số ý tưởng để cải thiện trong tương lai.
Các chủ đề được thảo luận ở đây sẽ khá rộng và đòi hỏi bạn phải có một số kiến thức về THREE.js. Tuy nhiên, tôi hy vọng bạn vẫn có thể thưởng thức bài viết này như một bài phân tích kỹ thuật sau sự cố. Bạn có thể chuyển đến một phần mà mình quan tâm bằng cách sử dụng nút mục lục ở bên phải. Trước tiên, tôi sẽ trình bày phần kết xuất của dự án, sau đó là quản lý chương trình đổ bóng và cuối cùng là cách sử dụng nhãn văn bản CSS kết hợp với WebGL.

Khám phá Space
Ngay sau khi hoàn thành Small Arms Globe, tôi đã thử nghiệm bản minh hoạ hạt THREE.js với độ sâu trường ảnh. Tôi nhận thấy mình có thể thay đổi "tỷ lệ" được diễn giải của cảnh bằng cách điều chỉnh mức độ áp dụng hiệu ứng. Khi hiệu ứng độ sâu trường ảnh ở mức cực đoan, các vật thể ở xa trở nên rất mờ, tương tự như cách chụp ảnh dịch chuyển tiêu điểm tạo cho người xem ảo giác như đang nhìn vào một cảnh siêu nhỏ. Ngược lại, việc giảm hiệu ứng này khiến bạn có cảm giác như đang nhìn vào không gian sâu thẳm.
Tôi bắt đầu tìm kiếm dữ liệu mà tôi có thể dùng để chèn vị trí hạt, một con đường dẫn tôi đến cơ sở dữ liệu HYG của astronexus.com, một bản tổng hợp của 3 nguồn dữ liệu (Hipparcos, Yale Bright Star Catalog và Gliese/Jahreiss Catalog) kèm theo toạ độ Đề các xyz được tính toán trước. Bắt đầu nào!


Tôi mất khoảng một giờ để ghép nối một thứ gì đó đặt dữ liệu về các ngôi sao vào không gian 3D. Có chính xác 119.617 ngôi sao trong tập dữ liệu,vì vậy, việc biểu thị mỗi ngôi sao bằng một hạt không phải là vấn đề đối với GPU hiện đại. Ngoài ra, còn có 87 ngôi sao được xác định riêng lẻ, vì vậy, tôi đã tạo một lớp phủ điểm đánh dấu CSS bằng cách sử dụng cùng một kỹ thuật mà tôi đã mô tả trong Small Arms Globe.
Trong thời gian này, tôi vừa hoàn thành loạt game Mass Effect. Trong trò chơi này, người chơi được mời khám phá thiên hà và quét nhiều hành tinh cũng như đọc về lịch sử hoàn toàn hư cấu của chúng (nghe như trên Wikipedia): những loài nào đã phát triển mạnh trên hành tinh, lịch sử địa chất của hành tinh, v.v.
Với lượng dữ liệu thực tế phong phú về các ngôi sao, người ta có thể trình bày thông tin thực về thiên hà theo cách tương tự. Mục tiêu cuối cùng của dự án này là hiện thực hoá dữ liệu này, cho phép người xem khám phá thiên hà theo cách của Mass Effect, tìm hiểu về các ngôi sao và sự phân bố của chúng, đồng thời hy vọng sẽ truyền cảm hứng và khơi gợi sự kinh ngạc về vũ trụ. Chà!
Có lẽ tôi nên nói rõ trước khi trình bày phần còn lại của nghiên cứu trường hợp này rằng tôi hoàn toàn không phải là nhà thiên văn học và đây là công trình nghiên cứu nghiệp dư được hỗ trợ bởi một số lời khuyên của các chuyên gia bên ngoài. Dự án này chắc chắn nên được hiểu là một cách diễn giải không gian của nghệ sĩ.
Xây dựng một thiên hà
Kế hoạch của tôi là tạo một mô hình của thiên hà theo quy trình, có thể đặt dữ liệu về các ngôi sao trong bối cảnh và hy vọng sẽ mang đến một khung cảnh tuyệt vời về vị trí của chúng ta trong Dải Ngân hà.

Để tạo ra Dải Ngân hà, tôi đã tạo 100.000 hạt và đặt chúng theo hình xoắn ốc bằng cách mô phỏng cách hình thành các nhánh thiên hà. Tôi không quá lo lắng về các đặc điểm cụ thể của quá trình hình thành nhánh xoắn ốc vì đây sẽ là một mô hình mang tính biểu trưng chứ không phải mô hình toán học. Tuy nhiên, tôi đã cố gắng vẽ số lượng nhánh xoắn ốc tương đối chính xác và xoay theo "hướng phù hợp".
Trong các phiên bản sau của mô hình Dải Ngân hà, tôi đã giảm bớt việc sử dụng các hạt để chuyển sang sử dụng hình ảnh phẳng của một thiên hà để đi kèm với các hạt, hy vọng sẽ mang lại cho nó vẻ ngoài giống như ảnh chụp hơn. Hình ảnh thực tế là của thiên hà xoắn ốc NGC 1232 cách chúng ta khoảng 70 triệu năm ánh sáng, được chỉnh sửa để trông giống như Dải Ngân hà.

Ngay từ đầu, tôi đã quyết định biểu thị một đơn vị GL (về cơ bản là một pixel ở chế độ 3D) là một năm ánh sáng. Đây là một quy ước thống nhất vị trí cho mọi thứ được hình dung và không may là quy ước này đã gây ra cho tôi những vấn đề nghiêm trọng về độ chính xác sau này.
Một quy ước khác mà tôi quyết định áp dụng là xoay toàn bộ cảnh thay vì di chuyển camera. Đây là điều mà tôi đã làm trong một số dự án khác. Một lợi thế là mọi thứ đều được đặt trên một "bàn xoay" để việc kéo chuột sang trái và phải sẽ xoay đối tượng được đề cập, nhưng việc thu phóng chỉ là vấn đề thay đổi camera.position.z.
Trường nhìn (FOV) của camera cũng là động. Khi kéo ra ngoài, trường nhìn sẽ mở rộng, thu vào ngày càng nhiều phần của thiên hà. Điều ngược lại sẽ xảy ra khi di chuyển vào phía trong của một ngôi sao, trường nhìn sẽ thu hẹp lại. Điều này cho phép camera xem những vật thể cực nhỏ (so với thiên hà) bằng cách thu hẹp trường nhìn xuống mức tương đương một chiếc kính lúp siêu hạng mà không gặp phải vấn đề về việc cắt gần.

Từ đây, tôi có thể "đặt" Mặt Trời ở một khoảng cách nhất định so với tâm thiên hà. Tôi cũng có thể hình dung kích thước tương đối của hệ Mặt Trời bằng cách lập bản đồ bán kính của Vành đai Kuiper (cuối cùng tôi chọn hình dung Đám mây Oort). Trong mô hình hệ Mặt Trời này, tôi cũng có thể hình dung quỹ đạo đơn giản của Trái Đất và bán kính thực tế của Mặt Trời để so sánh.

Khó hiển thị Mặt Trời. Tôi phải sử dụng nhiều kỹ thuật đồ hoạ theo thời gian thực mà tôi biết. Bề mặt của Mặt Trời là một lớp bọt plasma nóng và cần phải phát ra xung cũng như thay đổi theo thời gian. Điều này được mô phỏng thông qua một hoạ tiết bitmap của hình ảnh hồng ngoại về bề mặt mặt trời. Trình đổ bóng bề mặt sẽ tra cứu màu dựa trên thang độ xám của hoạ tiết này và thực hiện tra cứu trong một dải màu riêng biệt. Khi bảng tra cứu này thay đổi theo thời gian, nó sẽ tạo ra hiệu ứng biến dạng giống như dung nham.
Một kỹ thuật tương tự đã được sử dụng cho vành nhật hoa của Mặt Trời, ngoại trừ việc đó sẽ là một thẻ sprite phẳng luôn hướng về camera bằng cách sử dụng https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

Các vết nổ mặt trời được tạo ra thông qua các chương trình đổ bóng đỉnh và mảnh áp dụng cho một hình xuyến, quay ngay xung quanh rìa bề mặt mặt trời. Chương trình đổ bóng đỉnh có một hàm nhiễu khiến chương trình này dệt theo kiểu blob.
Chính tại đây, tôi bắt đầu gặp phải một số vấn đề về z-fighting do độ chính xác của GL. Tất cả các biến cho độ chính xác đều được xác định trước trong THREE.js, vì vậy, tôi không thể tăng độ chính xác một cách thực tế mà không tốn nhiều công sức. Vấn đề về độ chính xác không nghiêm trọng như gần điểm gốc. Tuy nhiên, khi tôi bắt đầu mô hình hoá các hệ thống sao khác, vấn đề này đã xảy ra.

Tôi đã sử dụng một số mẹo để giảm hiện tượng xung đột z. Material.polygonoffset của THREE là một thuộc tính cho phép hiển thị các đa giác ở một vị trí khác (theo những gì tôi hiểu). Điều này được dùng để buộc mặt phẳng vành nhật hoa luôn hiển thị ở trên cùng của bề mặt Mặt Trời. Bên dưới, một "quầng sáng" của Mặt Trời được kết xuất để tạo ra những tia sáng sắc nét di chuyển ra xa quả cầu.
Một vấn đề khác liên quan đến độ chính xác là các mô hình ngôi sao sẽ bắt đầu rung khi cảnh được phóng to. Để khắc phục vấn đề này, tôi phải "đặt lại" chế độ xoay cảnh và xoay riêng mô hình ngôi sao cũng như bản đồ môi trường để tạo ảo giác rằng bạn đang xoay quanh ngôi sao.
Tạo hiệu ứng loé sáng

Hình ảnh không gian là nơi tôi cảm thấy có thể sử dụng hiệu ứng loá sáng quá mức. THREE.LensFlare phục vụ mục đích này, tất cả những gì tôi cần làm là thêm vào một số hình lục giác biến dạng và một chút phong cách của JJ Abrams. Đoạn mã bên dưới cho biết cách tạo các đối tượng này trong cảnh của bạn.
// This function returns a lesnflare THREE object to be .add()ed to the scene graph
function addLensFlare(x,y,z, size, overrideImage){
var flareColor = new THREE.Color( 0xffffff );
lensFlare = new THREE.LensFlare( overrideImage, 700, 0.0, THREE.AdditiveBlending, flareColor );
// we're going to be using multiple sub-lens-flare artifacts, each with a different size
lensFlare.add( textureFlare1, 4096, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
// and run each through a function below
lensFlare.customUpdateCallback = lensFlareUpdateCallback;
lensFlare.position = new THREE.Vector3(x,y,z);
lensFlare.size = size ? size : 16000 ;
return lensFlare;
}
// this function will operate over each lensflare artifact, moving them around the screen
function lensFlareUpdateCallback( object ) {
var f, fl = this.lensFlares.length;
var flare;
var vecX = -this.positionScreen.x _ 2;
var vecY = -this.positionScreen.y _ 2;
var size = object.size ? object.size : 16000;
var camDistance = camera.position.length();
for( f = 0; f < fl; f ++ ) {
flare = this.lensFlares[ f ];
flare.x = this.positionScreen.x + vecX * flare.distance;
flare.y = this.positionScreen.y + vecY * flare.distance;
flare.scale = size / camDistance;
flare.rotation = 0;
}
}
Cách dễ dàng để thực hiện thao tác cuộn kết cấu

Đối với "mặt phẳng định hướng không gian", một THREE.CylinderGeometry() khổng lồ đã được tạo và đặt ở trung tâm Mặt Trời. Để tạo ra "làn sóng ánh sáng" lan toả ra ngoài, tôi đã sửa đổi độ lệch kết cấu theo thời gian như sau:
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
là hoạ tiết thuộc về vật liệu, nhận được một hàm onUpdate mà bạn có thể ghi đè. Việc đặt độ lệch của kết cấu sẽ khiến kết cấu được "cuộn" dọc theo trục đó và việc spam needsUpdate = true sẽ buộc hành vi này lặp lại.
Sử dụng thang màu
Mỗi ngôi sao có một màu sắc riêng dựa trên "chỉ số màu" mà các nhà thiên văn học đã chỉ định cho chúng. Nhìn chung, các ngôi sao màu đỏ có nhiệt độ thấp hơn và các ngôi sao màu xanh dương/tím có nhiệt độ cao hơn. Dải chuyển đổi này có một dải màu trắng và các màu cam trung gian.
Khi kết xuất các ngôi sao, tôi muốn mỗi hạt có màu riêng dựa trên dữ liệu này. Cách thực hiện việc này là dùng "thuộc tính" được chỉ định cho vật liệu đổ bóng áp dụng cho các hạt.
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};
Việc điền vào mảng colorIndex sẽ giúp mỗi hạt có màu riêng trong chương trình đổ bóng. Thông thường, người ta sẽ truyền vào một vec3 màu, nhưng trong trường hợp này, tôi đang truyền vào một số thực để tra cứu thang màu cuối cùng.

Dải màu có dạng như thế này, tuy nhiên tôi cần truy cập vào dữ liệu màu bitmap của dải màu đó từ JavaScript. Cách tôi thực hiện việc này là trước tiên tải hình ảnh lên DOM, vẽ hình ảnh đó vào một phần tử canvas, sau đó truy cập vào bitmap canvas.
// make a blank canvas, sized to the image, in this case gradientImage is a dom image element
gradientCanvas = document.createElement('canvas');
gradientCanvas.width = gradientImage.width;
gradientCanvas.height = gradientImage.height;
// draw the image
gradientCanvas.getContext('2d').drawImage( gradientImage, 0, 0, gradientImage.width, gradientImage.height );
// a function to grab the pixel color based on a normalized percentage value
gradientCanvas.getColor = function( percentage ){
return this.getContext('2d').getImageData(percentage \* gradientImage.width,0, 1, 1).data;
}
Sau đó, phương thức này cũng được dùng để tô màu từng ngôi sao trong chế độ xem mô hình ngôi sao.

Xử lý trình đổ bóng
Trong suốt dự án, tôi nhận thấy mình cần viết ngày càng nhiều chương trình đổ bóng để hoàn thành tất cả hiệu ứng hình ảnh. Tôi đã viết một trình tải chương trình đổ bóng tuỳ chỉnh cho mục đích này vì tôi cảm thấy mệt mỏi khi các chương trình đổ bóng nằm trong index.html.
// list of shaders we'll load
var shaderList = ['shaders/starsurface', 'shaders/starhalo', 'shaders/starflare', 'shaders/galacticstars', /*...etc...*/];
// a small util to pre-fetch all shaders and put them in a data structure (replacing the list above)
function loadShaders( list, callback ){
var shaders = {};
var expectedFiles = list.length \* 2;
var loadedFiles = 0;
function makeCallback( name, type ){
return function(data){
if( shaders[name] === undefined ){
shaders[name] = {};
}
shaders[name][type] = data;
// check if done
loadedFiles++;
if( loadedFiles == expectedFiles ){
callback( shaders );
}
};
}
for( var i=0; i<list.length; i++ ){
var vertexShaderFile = list[i] + '.vsh';
var fragmentShaderFile = list[i] + '.fsh';
// find the filename, use it as the identifier
var splitted = list[i].split('/');
var shaderName = splitted[splitted.length-1];
$(document).load( vertexShaderFile, makeCallback(shaderName, 'vertex') );
$(document).load( fragmentShaderFile, makeCallback(shaderName, 'fragment') );
}
}
Hàm loadShaders() lấy một danh sách tên tệp chương trình đổ bóng (dự kiến là .fsh cho mảnh và .vsh cho chương trình đổ bóng đỉnh), cố gắng tải dữ liệu của các tệp đó, rồi chỉ cần thay thế danh sách bằng các đối tượng. Kết quả cuối cùng nằm trong các uniform THREE.js mà bạn có thể truyền các chương trình đổ bóng vào đó như sau:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
Tôi có thể đã sử dụng require.js mặc dù điều đó sẽ cần một số mã lắp ráp lại chỉ cho mục đích này. Mặc dù dễ dàng hơn nhiều, nhưng tôi nghĩ giải pháp này có thể được cải thiện, thậm chí có thể là một tiện ích THREE.js. Nếu bạn có đề xuất hoặc cách làm hiệu quả hơn, vui lòng cho tôi biết!
Nhãn văn bản CSS trên THREE.js
Trong dự án gần đây nhất của chúng tôi, Small Arms Globe, tôi đã thử nghiệm việc tạo nhãn văn bản xuất hiện trên đầu cảnh THREE.js. Phương thức mà tôi đang sử dụng sẽ tính toán vị trí mô hình tuyệt đối mà tôi muốn văn bản xuất hiện, sau đó phân giải vị trí màn hình bằng THREE.Projector() và cuối cùng sử dụng "top" và "left" của CSS để đặt các phần tử CSS ở vị trí mong muốn.
Các lần lặp lại ban đầu của dự án này đều sử dụng kỹ thuật tương tự, tuy nhiên, tôi rất muốn thử phương pháp khác này do Luis Cruz mô tả.
Ý tưởng cơ bản: khớp ma trận biến đổi của CSS3D với camera và cảnh của THREE.js, sau đó bạn có thể "đặt" các phần tử CSS ở chế độ 3D như thể phần tử đó nằm trên cảnh của THREE.js. Tuy nhiên, có một số hạn chế đối với việc này, chẳng hạn như bạn sẽ không thể đặt văn bản bên dưới một đối tượng THREE.js. Điều này vẫn nhanh hơn nhiều so với việc cố gắng thực hiện bố cục bằng cách sử dụng các thuộc tính CSS "top" và "left".

Bạn có thể tìm thấy bản minh hoạ (và mã trong nguồn xem) cho việc này tại đây. Tuy nhiên, tôi nhận thấy thứ tự ma trận đã thay đổi đối với THREE.js. Hàm mà tôi đã cập nhật:
/_ Fixes the difference between WebGL coordinates to CSS coordinates _/
function toCSSMatrix(threeMat4, b) {
var a = threeMat4, f;
if (b) {
f = [
a.elements[0], -a.elements[1], a.elements[2], a.elements[3],
a.elements[4], -a.elements[5], a.elements[6], a.elements[7],
a.elements[8], -a.elements[9], a.elements[10], a.elements[11],
a.elements[12], -a.elements[13], a.elements[14], a.elements[15]
];
} else {
f = [
a.elements[0], a.elements[1], a.elements[2], a.elements[3],
a.elements[4], a.elements[5], a.elements[6], a.elements[7],
a.elements[8], a.elements[9], a.elements[10], a.elements[11],
a.elements[12], a.elements[13], a.elements[14], a.elements[15]
];
}
for (var e in f) {
f[e] = epsilon(f[e]);
}
return "matrix3d(" + f.join(",") + ")";
}
Vì mọi thứ đều được biến đổi, nên văn bản không còn hướng về phía camera nữa. Giải pháp là sử dụng THREE.Gyroscope() để buộc Object3D "mất" hướng kế thừa từ cảnh. Kỹ thuật này được gọi là "billboarding" (tạo bảng quảng cáo) và Gyroscope là công cụ hoàn hảo để thực hiện kỹ thuật này.
Điều thực sự thú vị là tất cả DOM và CSS thông thường vẫn hoạt động, chẳng hạn như bạn có thể di chuột lên nhãn văn bản 3D và làm cho nhãn đó phát sáng bằng bóng đổ.

Khi phóng to, tôi nhận thấy việc điều chỉnh tỷ lệ kiểu chữ gây ra vấn đề về vị trí. Có lẽ là do khoảng cách giữa các chữ cái và khoảng đệm của văn bản? Một vấn đề khác là văn bản bị vỡ hạt khi phóng to vì trình kết xuất DOM coi văn bản được kết xuất là một tứ giác có kết cấu. Bạn cần lưu ý điều này khi sử dụng phương thức này. Nhìn lại, tôi có thể chỉ cần dùng văn bản có cỡ chữ khổng lồ và có lẽ đây là điều tôi nên khám phá trong tương lai. Trong dự án này, tôi cũng sử dụng nhãn văn bản vị trí CSS "top/left" (trên cùng/bên trái) như đã mô tả trước đó cho những phần tử thực sự nhỏ đi kèm với các hành tinh trong hệ Mặt Trời.
Phát nhạc và phát lặp lại
Bản nhạc được phát trong phần "Bản đồ thiên hà" của Mass Effect là của nhà soạn nhạc Sam Hulick và Jack Wall của Bioware. Bản nhạc đó mang đến cảm xúc mà tôi muốn khách truy cập trải nghiệm. Chúng tôi muốn có nhạc trong dự án của mình vì cho rằng đó là một phần quan trọng của bầu không khí, giúp tạo ra cảm giác kinh ngạc và kỳ diệu mà chúng tôi đang cố gắng hướng đến.
Nhà sản xuất Valdean Klump của chúng tôi đã liên hệ với Sam. Sam có rất nhiều bản nhạc "bị cắt" trong Mass Effect và anh ấy đã rất hào phóng khi cho phép chúng tôi sử dụng những bản nhạc đó. Bản nhạc này có tên là "In a Strange Land".
Tôi đã sử dụng thẻ âm thanh để phát nhạc, tuy nhiên, ngay cả trong Chrome, thuộc tính "loop" cũng không đáng tin cậy – đôi khi thuộc tính này không lặp lại. Cuối cùng, thủ thuật gắn thẻ âm thanh kép này được dùng để kiểm tra xem quá trình phát đã kết thúc hay chưa và chuyển sang thẻ khác để phát. Điều đáng thất vọng là ảnh tĩnh này không phải lúc nào cũng lặp lại một cách hoàn hảo, nhưng tôi cảm thấy đây là điều tốt nhất mà tôi có thể làm.
var musicA = document.getElementById('bgmusicA');
var musicB = document.getElementById('bgmusicB');
musicA.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playB = function(){
musicB.play();
}
// make it wait 15 seconds before playing again
setTimeout( playB, 15000 );
}, false);
musicB.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playA = function(){
musicA.play();
}
// otherwise the music will drive you insane
setTimeout( playA, 15000 );
}, false);
// okay so there's a bit of code redundancy, I admit it
musicA.play();
Khả năng cải thiện
Sau một thời gian làm việc với THREE.js, tôi cảm thấy như mình đã đến mức dữ liệu của tôi trộn lẫn quá nhiều với mã của tôi. Ví dụ: khi xác định các chỉ dẫn về vật liệu, hoạ tiết và hình học nội tuyến, về cơ bản, tôi đã "mô hình hoá 3D bằng mã". Điều này gây ra cảm giác rất khó chịu và là một khía cạnh mà các nỗ lực trong tương lai với THREE.js có thể cải thiện đáng kể, chẳng hạn như xác định dữ liệu về chất liệu trong một tệp riêng biệt, tốt nhất là có thể xem và điều chỉnh trong một số ngữ cảnh, đồng thời có thể đưa trở lại dự án chính.
Đồng nghiệp Ray McClure của chúng tôi cũng đã dành thời gian tạo ra một số "tiếng ồn không gian" tuyệt vời được tạo sinh, nhưng phải cắt bỏ do API Âm thanh trên web không ổn định, thỉnh thoảng làm Chrome gặp sự cố. Thật đáng tiếc… nhưng điều đó chắc chắn đã khiến chúng tôi suy nghĩ nhiều hơn về không gian âm thanh cho công việc trong tương lai. Tại thời điểm viết bài này, tôi được biết Web Audio API đã được vá nên có thể hiện tại API này đang hoạt động. Đây là một điều cần lưu ý trong tương lai.
Các phần tử kiểu chữ kết hợp với WebGL vẫn là một thách thức và tôi không chắc chắn 100% rằng những gì chúng ta đang làm ở đây là cách chính xác. Nó vẫn có cảm giác như một giải pháp tạm thời. Có lẽ các phiên bản THREE trong tương lai, với CSS Renderer sắp ra mắt, có thể được dùng để kết hợp hai thế giới này một cách hiệu quả hơn.
Ghi công
Cảm ơn Aaron Koblin đã cho phép tôi thực hiện dự án này. Jono Brandel vì thiết kế + triển khai giao diện người dùng, xử lý kiểu chữ và triển khai hướng dẫn tuyệt vời. Valdean Klump vì đã đặt tên cho dự án và viết toàn bộ nội dung. Sabah Ahmed vì đã giải quyết vấn đề về quyền sử dụng cho các nguồn dữ liệu và hình ảnh. Cảm ơn Clem Wright đã liên hệ với những người phù hợp để xuất bản. Doug Fritz vì sự xuất sắc về kỹ thuật. George Brower vì đã dạy tôi về JS và CSS. Và tất nhiên là cả Mr. Doob cho THREE.js.