سلام! نام من مایکل چانگ است و با تیم Data Arts در Google کار می کنم. اخیراً 100000 ستاره را تکمیل کردیم، یک آزمایش Chrome که ستارههای اطراف را تجسم میکند. این پروژه با THREE.js و CSS3D ساخته شده است. در این مطالعه موردی، من روند کشف را تشریح میکنم، برخی از تکنیکهای برنامهنویسی را به اشتراک میگذارم، و با برخی از افکار برای بهبود آینده پایان میدهم.
موضوعات مورد بحث در اینجا نسبتاً گسترده خواهد بود و به دانش THREE.js نیاز دارد، هرچند امیدوارم که همچنان بتوانید از این به عنوان یک پس از مرگ فنی لذت ببرید. با استفاده از دکمه فهرست مطالب در سمت راست، به راحتی به منطقه مورد علاقه خود بروید. ابتدا بخش رندر پروژه و سپس مدیریت سایه زن و در نهایت نحوه استفاده از برچسب های متنی CSS در ترکیب با WebGL را نشان خواهم داد.
کشف فضا
مدت کوتاهی پس از اینکه Small Arms Globe را به پایان رساندیم، من در حال آزمایش یک نسخه نمایشی ذرات THREE.js با عمق میدان بودم. متوجه شدم که میتوانم «مقیاس» تفسیر شده صحنه را با تنظیم میزان افکت اعمال شده تغییر دهم. هنگامی که اثر عمق میدان واقعاً شدید بود، اجسام دور واقعاً تار میشدند، شبیه به روشی که عکاسی شیبدار برای ایجاد توهم نگاه کردن به یک صحنه میکروسکوپی عمل میکند. برعکس، کم کردن جلوه باعث شد به نظر برسد که به فضای عمیق خیره شده اید.
من شروع به جستجوی دادهای کردم که بتوانم از آن برای تزریق موقعیتهای ذرات استفاده کنم، مسیری که مرا به پایگاه داده HYG astronexus.com هدایت میکند، مجموعهای از سه منبع داده (Hipparcos، کاتالوگ ستاره درخشان ییل، و کاتالوگ Gliese/Jahreiss) توسط مختصات دکارتی xyz از پیش محاسبه شده. بیایید شروع کنیم!
هک کردن چیزی که داده های ستاره را در فضای سه بعدی قرار می داد حدود یک ساعت طول کشید. دقیقاً 119617 ستاره در مجموعه داده وجود دارد، بنابراین نشان دادن هر ستاره با یک ذره مشکلی برای یک GPU مدرن نیست. همچنین 87 ستاره به صورت جداگانه شناسایی شدهاند، بنابراین من یک پوشش نشانگر CSS با استفاده از همان تکنیکی که در Small Arms Globe توضیح دادم ایجاد کردم.
در این مدت من به تازگی مجموعه Mass Effect را تمام کرده بودم. در بازی از بازیکن دعوت میشود که کهکشان را کاوش کند و سیارات مختلف را اسکن کند و در مورد تاریخچه کاملاً تخیلی و ویکیپدیایی آنها بخواند : چه گونههایی در این سیاره رشد کردهاند، تاریخ زمینشناسی آن و غیره.
با دانستن انبوهی از داده های واقعی که در مورد ستارگان وجود دارد، می توان اطلاعات واقعی را در مورد کهکشان به همان شیوه ارائه کرد. هدف نهایی این پروژه زنده کردن این دادهها، اجازه دادن به بیننده برای کشف کهکشان à la Mass Effect، یادگیری در مورد ستارهها و توزیع آنها و القای حس هیبت و شگفتی در مورد فضا است. اوه!
احتمالاً باید پیشگفتار بقیه این مطالعه موردی را با گفتن اینکه من به هیچ وجه منجم نیستم و این کار تحقیقاتی آماتوری است که با توصیههای متخصصان خارجی پشتیبانی میشود، مقدمه کنم. این پروژه قطعاً باید به عنوان یک تفسیر هنرمندانه از فضا تعبیر شود.
ساخت کهکشان
برنامه من این بود که به صورت رویهای مدلی از کهکشان تولید کنم که بتواند دادههای ستاره را در متن قرار دهد - و امیدوارم نمای عالی از مکان ما در کهکشان راه شیری ارائه دهد.
برای تولید کهکشان راه شیری، من 100000 ذره را تولید کردم و با تقلید از نحوه تشکیل بازوهای کهکشانی آنها را در یک مارپیچ قرار دادم. من خیلی نگران خصوصیات تشکیل بازوی مارپیچی نبودم زیرا این یک مدل نمایشی خواهد بود تا یک مدل ریاضی. با این حال، سعی کردم تعداد بازوهای مارپیچی را کم و بیش درست به دست بیاورم و در «جهت درست» بچرخند.
در نسخههای بعدی مدل راه شیری، من بر استفاده از ذرات به نفع تصویری مسطح از یک کهکشان برای همراهی با ذرات تأکید نکردم، امیدواریم که ظاهر عکاسی بیشتری به آن بدهم. تصویر واقعی از کهکشان مارپیچی NGC 1232 است که تقریباً 70 میلیون سال نوری از ما فاصله دارد و با تصویر دستکاری شده تا شبیه کهکشان راه شیری باشد.
من در اوایل تصمیم گرفتم که یک واحد GL، اساساً یک پیکسل به صورت سه بعدی را به عنوان یک سال نوری نشان دهم -- قراردادی که مکان را برای هر چیزی که تجسم می شود یکپارچه می کند، و متأسفانه بعداً مشکلات جدی را به من داد.
قرارداد دیگری که من تصمیم گرفتم این بود که به جای حرکت دوربین، کل صحنه را بچرخانم، کاری که در چند پروژه دیگر انجام داده ام. یکی از مزیتها این است که همه چیز روی یک صفحه گردان قرار میگیرد به طوری که با کشیدن ماوس به چپ و راست شی مورد نظر را میچرخاند، اما بزرگنمایی فقط به تغییر camera.position.z بستگی دارد.
میدان دید (یا FOV) برای دوربین نیز پویا است. زمانی که فرد به سمت بیرون میکشد، میدان دید گستردهتر میشود و تعداد بیشتری از کهکشان را به خود اختصاص میدهد. برعکس وقتی که به سمت داخل به سمت یک ستاره حرکت می کنیم، میدان دید باریک می شود. این به دوربین اجازه می دهد تا چیزهایی را که بینهایت کوچک هستند (در مقایسه با کهکشان) با فشار دادن FOV به چیزی شبیه ذره بین خدایی بدون نیاز به برخورد با مسائل برش نزدیک به صفحه ببیند.
از اینجا توانستم خورشید را در تعدادی واحد دورتر از هسته کهکشانی "قرار دهم". من همچنین توانستم اندازه نسبی منظومه شمسی را با ترسیم شعاع صخره کویپر تجسم کنم (در نهایت تصمیم گرفتم ابر اورت را تجسم کنم). در این مدل منظومه شمسی، من همچنین میتوانم یک مدار ساده شده از زمین و شعاع واقعی خورشید را در مقایسه با آن تجسم کنم.
نمایش خورشید دشوار بود. من مجبور شدم با تکنیکهای گرافیکی بیدرنگ که میدانم تقلب کنم. سطح خورشید یک کف گرم از پلاسما است و باید در طول زمان ضربان داشته باشد و تغییر کند. این از طریق یک بافت بیت مپ از یک تصویر مادون قرمز از سطح خورشید شبیه سازی شد. سایه بان سطح بر اساس مقیاس خاکستری این بافت ظاهری رنگی ایجاد می کند و در یک رمپ رنگی جداگانه نگاهی را انجام می دهد. هنگامی که این نگاه به مرور زمان تغییر می کند، این اعوجاج گدازه مانند را ایجاد می کند.
تکنیک مشابهی برای تاج خورشید استفاده شد، با این تفاوت که این یک کارت اسپرایت تخت است که همیشه با استفاده از https://github.com/mrdoob/three.js/blob/master/src/extras/core رو به دوربین است. /Gyroscope.js .
شعله های خورشیدی از طریق راس و سایه زن های قطعه ای که روی یک چنبره اعمال می شود، ایجاد می شوند، که درست در اطراف لبه سطح خورشید می چرخند. سایه زن رأس دارای عملکرد نویز است که باعث می شود به شکل لکه مانند بافته شود.
در اینجا بود که من شروع به تجربه برخی از مشکلات z-fighting به دلیل دقت GL کردم. همه متغیرهای دقت در THREE.js از قبل تعریف شده بودند، بنابراین من نمیتوانستم بدون حجم زیادی از کار، دقت را به طور واقع بینانه افزایش دهم. مسائل مربوط به دقت در نزدیکی مبدا بد نبود. با این حال، زمانی که شروع به مدلسازی سیستمهای ستارهای دیگر کردم، این موضوع به یک مسئله تبدیل شد.
چند هک وجود داشت که من برای کاهش z-fighting استفاده کردم. THREE's Material.polygonoffset خاصیتی است که به چند ضلعی ها اجازه می دهد در یک مکان درک شده متفاوت ارائه شوند (تا جایی که من متوجه شدم). این برای وادار کردن هواپیمای تاج به استفاده همیشه در بالای سطح خورشید استفاده شد. در زیر این، یک "هاله" خورشید برای ایجاد پرتوهای نور تیز در حال دور شدن از کره ارائه شده است.
یک مشکل متفاوت مربوط به دقت این بود که با بزرگنمایی صحنه، مدلهای ستاره شروع به لرزیدن میکردند. برای رفع این مشکل، باید چرخش صحنه را صفر میکردم و مدل ستاره و نقشه محیط را جداگانه میچرخانم تا این توهم را ایجاد کنم که شما در حال چرخش هستید. ستاره
ایجاد Lensflare
تجسم فضایی جایی است که من احساس می کنم می توانم با استفاده بیش از حد از lensflare فرار کنم. THREE.LensFlare این اهداف را دنبال می کند، تنها کاری که من باید انجام می دادم این بود که تعدادی شش ضلعی آنامورفیک و یک خط تیره جی جی آبرامز بیندازم. قطعه زیر نشان می دهد که چگونه آنها را در صحنه خود بسازید.
// 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;
}
}
یک راه آسان برای انجام اسکرول بافت
برای "صفحه جهت گیری فضایی"، یک THREE.CylinderGeometry() غول پیکر ایجاد شد و بر روی خورشید متمرکز شد. برای ایجاد "موج نور" که به بیرون باد می کند، من تغییر بافت آن را در طول زمان به صورت زیر تغییر دادم:
mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}
map
بافت متعلق به متریال است که یک تابع onUpdate دریافت می کند که می توانید آن را بیش از حد بنویسید. تنظیم افست آن باعث می شود که بافت در امتداد آن محور "پیمایش" شود و ارسال هرزنامه به روز رسانی = true این رفتار را مجبور به حلقه زدن می کند.
استفاده از رمپ های رنگی
هر ستاره بر اساس "شاخص رنگ" که ستاره شناسان به آنها اختصاص داده اند، رنگ متفاوتی دارد. به طور کلی، ستاره های قرمز سردتر و ستاره های آبی/بنفش داغ تر هستند. نواری از رنگ های سفید و نارنجی متوسط در این گرادیان وجود دارد.
هنگام رندر کردن ستارگان می خواستم بر اساس این داده ها به هر ذره رنگ خاص خود را بدهم. روش انجام این کار با «ویژگیهایی» بود که به ماده سایه زن اعمال شده روی ذرات داده شد.
var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};
پر کردن آرایه colorIndex به هر ذره رنگ منحصر به فرد خود را در سایه زن می دهد. به طور معمول، یکی از رنگهای vec3 عبور میکند، اما در این مثال، من از یک شناور عبور میکنم تا در نهایت سطح شیبدار رنگی را مشاهده کنم.
سطح شیب دار رنگی به این شکل بود، با این حال من باید به داده های رنگ بیت مپ آن از جاوا اسکریپت دسترسی پیدا کنم. روشی که من این کار را انجام دادم این بود که ابتدا تصویر را روی DOM بارگذاری کردم، آن را در یک عنصر بوم ترسیم کردم، سپس به bitmap بوم دسترسی پیدا کردم.
// 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;
}
سپس از همین روش برای رنگ آمیزی تک ستاره ها در نمای مدل ستاره استفاده می شود.
مشاجره سایه بان
در طول پروژه متوجه شدم که برای انجام تمام جلوه های بصری نیاز به نوشتن سایه بان های بیشتری دارم. برای این منظور یک شیدر لودر سفارشی نوشتم چون از داشتن سایه بان های زنده در 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') );
}
}
تابع loadShaders () لیستی از نام فایل های سایه بان را می گیرد (منتظر fsh. برای قطعه و vsh. برای سایه زن های راس)، تلاش می کند داده های آنها را بارگذاری کند، و سپس فقط لیست را با اشیا جایگزین می کند. نتیجه نهایی این است که در یونیفرم های THREE.js می توانید سایه بان ها را به این شکل به آن منتقل کنید:
var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});
من احتمالاً می توانستم از require.js استفاده کنم، هرچند که فقط برای این منظور به جمع آوری مجدد کد نیاز داشت. این راه حل، در حالی که بسیار ساده تر است، می تواند بر اساس من، حتی به عنوان یک پسوند THREE.js، بهبود یابد. اگر پیشنهاد یا روشی برای انجام بهتر این کار دارید، لطفاً به من اطلاع دهید!
برچسبهای متنی CSS در بالای THREE.js
در آخرین پروژه ما، Small Arms Globe، من با ساختن برچسب های متنی در بالای صحنه THREE.js بازی کردم. روشی که من استفاده می کردم موقعیت مدل مطلق جایی که می خواهم متن ظاهر شود را محاسبه می کند، سپس موقعیت صفحه را با استفاده از THREE.Projector() تعیین می کند و در نهایت از CSS "top" و "left" برای قرار دادن عناصر CSS در قسمت مورد نظر استفاده می کند. موقعیت
تکرارهای اولیه در این پروژه از همین تکنیک استفاده میکردند، با این حال من از امتحان این روش دیگر که توسط لوئیس کروز توضیح داده شده بود، خسته شدهام.
ایده اصلی: تبدیل ماتریس CSS3D را با دوربین و صحنه THREE مطابقت دهید، و می توانید عناصر CSS را در سه بعدی "قرار دهید" طوری که گویی در بالای صحنه THREE هستند. هر چند محدودیت هایی برای این وجود دارد، برای مثال شما نمی توانید متنی را زیر یک شی THREE.js قرار دهید. این هنوز هم بسیار سریعتر از تلاش برای اجرای طرح بندی با استفاده از ویژگی های CSS "بالا" و "چپ" است.
شما می توانید نسخه ی نمایشی (و کد در منبع مشاهده) را برای این کار در اینجا پیدا کنید. با این حال متوجه شدم که ترتیب ماتریس از آن زمان برای THREE.js تغییر کرده است. تابعی که من به روز کرده ام:
/_ 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(",") + ")";
}
از آنجایی که همه چیز دگرگون شده است، متن دیگر رو به دوربین نیست. راه حل استفاده از THREE.Gyroscope () بود که یک Object3D را مجبور می کند جهت گیری ارثی خود را از صحنه "از دست بدهد". این تکنیک "بیلبورد" نامیده می شود و ژیروسکوپ برای انجام این کار عالی است.
چیزی که واقعاً خوب است این است که همه DOM و CSS معمولی همچنان با هم پخش میشوند، مانند اینکه میتوانید ماوس را روی یک برچسب متنی سهبعدی قرار دهید و آن را با سایههای رها کنید.
هنگام بزرگنمایی متوجه شدم که مقیاس بندی تایپوگرافی باعث ایجاد مشکلاتی در موقعیت یابی می شود. شاید این به دلیل کرنینگ و پر کردن متن باشد؟ مشکل دیگر این بود که متن با بزرگنمایی پیکسلی شد زیرا رندر DOM متن رندر شده را به عنوان یک چهارتایی بافت دار در نظر می گیرد، چیزی که هنگام استفاده از این روش باید از آن آگاه بود. در نگاهی به گذشته، میتوانستم از متنی با اندازه فونت غولپیکر استفاده کنم، و شاید این چیزی برای کاوش در آینده باشد. در این پروژه من همچنین از برچسبهای متنی قرارگیری CSS "بالا/چپ" که قبلا توضیح داده شد، برای عناصر بسیار کوچکی که سیارات منظومه شمسی را همراهی میکنند، استفاده کردم.
پخش موسیقی و حلقه زدن
قطعه موسیقی پخش شده در طول "نقشه کهکشانی" Mass Effect توسط آهنگسازان Bioware سام هالیک و جک وال بود و احساسی داشت که من می خواستم بازدیدکننده تجربه کند. ما میخواستیم در پروژهمان مقداری موسیقی داشته باشیم، زیرا احساس میکردیم که بخش مهمی از فضا است و به ایجاد حس هیبت و شگفتی کمک میکند که سعی میکردیم آن را هدف قرار دهیم.
تهیهکننده ما، والدین کلمپ، با سمی که مجموعهای از موزیکهای «زمین برش» از Mass Effect داشت، تماس گرفت که او با مهربانی به ما اجازه استفاده از آنها را داد. این آهنگ با عنوان "در سرزمین غریب" است.
من از تگ صوتی برای پخش موسیقی استفاده کردم، با این حال حتی در کروم نیز مشخصه "حلقه" غیرقابل اعتماد بود -- گاهی اوقات فقط حلقه نمی شود. در پایان از این هک تگ صوتی دوگانه برای بررسی پایان پخش و دوچرخه سواری به تگ دیگر برای پخش استفاده شد. چیزی که ناامیدکننده بود این بود که این هنوز هم همیشه به طور کامل در حال چرخش نبود، افسوس که احساس می کنم این بهترین کاری بود که می توانستم انجام دهم.
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();
فضایی برای بهبود
پس از مدتی کار با THREE.js، احساس میکنم به نقطهای رسیدهام که دادههایم بیش از حد با کدم مخلوط میشوند. برای مثال، هنگام تعریف مواد، بافتها و دستورالعملهای هندسه در خط، من اساساً «مدلسازی سهبعدی با کد» بودم. این بسیار بد احساس میشود و زمینهای است که در آن تلاشهای آینده با THREE.js میتواند تا حد زیادی بهبود یابد، برای مثال تعریف دادههای مواد در یک فایل جداگانه، ترجیحاً قابل مشاهده و تغییر در برخی زمینهها، و میتوان آن را به پروژه اصلی بازگرداند.
همکار ما Ray McClure نیز مدتی را صرف ایجاد "صداهای فضایی" فوقالعاده مولد کرد که به دلیل ناپایدار بودن API صوتی وب، هر چند وقت یکبار کروم را خراب میکرد. مایه تاسف است... اما قطعاً ما را به فکر بیشتر در فضای صدا برای کارهای آینده واداشت. از زمان نوشتن این مقاله مطلع شدم که Web Audio API وصله شده است، بنابراین ممکن است این مورد در حال حاضر کار کند، چیزی که در آینده باید به آن توجه کرد.
عناصر تایپوگرافی جفت شده با WebGL همچنان یک چالش باقی مانده است، و من 100% مطمئن نیستم کاری که ما در اینجا انجام می دهیم راه درستی باشد. هنوز هم مثل یک هک است. شاید بتوان از نسخههای آینده THREE با CSS Renderer در آینده برای پیوستن بهتر به این دو جهان استفاده کرد.
اعتبارات
با تشکر از آرون کوبلین که به من اجازه داد با این پروژه به شهر بروم. Jono Brandel برای طراحی عالی رابط کاربری + پیاده سازی، نوع درمان و اجرای تور. Valdean Klump به خاطر دادن نام پروژه و تمام نسخه. صباح احمد برای پاک کردن حجم متریک از حقوق استفاده برای منابع داده و تصویر. کلم رایت برای تماس با افراد مناسب برای انتشار. داگ فریتز برای برتری فنی. جورج بروور برای آموزش JS و CSS به من. و البته آقای Doob برای THREE.js.