كسب 100,000 نجمة

مرحبًا اسمي مايكل تشانغ وأعمل مع فريق Data Arts في Google. أكملنا مؤخرًا 100,000 نجم، وهي تجربة Chrome تعرض النجوم القريبة. تم إنشاء المشروع باستخدام THREE.js وCSS3D. في دراسة الحالة هذه، سأوضّح عملية الاستكشاف، وأشارك بعض تقنيات البرمجة، وأختتم ببعض الأفكار لتحسين الأداء في المستقبل.

ستكون المواضيع التي تمت مناقشتها هنا عامة إلى حدّ ما، وستتطلّب بعض المعرفة بـ THREE.js، ولكن آمل أن تستمتع بهذا المحتوى كتحليل فني بعد انتهاء المشروع. يمكنك الانتقال إلى أي قسم يهمّك باستخدام زر جدول المحتويات على اليسار. سأعرض أولاً جزء العرض من المشروع، ثم إدارة التظليل، وأخيرًا كيفية استخدام تصنيفات النصوص CSS مع WebGL.

‫100,000 Stars، تجربة Chrome من فريق Data Arts
تستخدم تجربة "100,000 نجمة" مكتبة THREE.js لعرض النجوم القريبة في مجرّة درب التبانة

استكشاف "المساحة الإبداعية"

بعد الانتهاء من Small Arms Globe بفترة قصيرة، كنت أجرّب عرضًا توضيحيًا للجسيمات باستخدام THREE.js مع عمق المجال. لاحظتُ أنّه يمكنني تغيير "مقياس" المشهد الذي تمّت ترجمته من خلال تعديل مقدار التأثير الذي تمّ تطبيقه. عندما كان تأثير عمق المجال شديدًا، أصبحت الأجسام البعيدة غير واضحة تمامًا، على غرار طريقة عمل التصوير بتقنية الإمالة والتغيير التي تمنح الشخص وهمًا بالنظر إلى مشهد مجهري. في المقابل، يؤدي خفض مستوى التأثير إلى ظهور صورة وكأنّك تتأمل الفضاء العميق.

بدأتُ البحث عن بيانات يمكنني استخدامها لإدخال مواضع الجسيمات، وهو مسار قادني إلى قاعدة بيانات HYG على astronexus.com، وهي مجموعة من مصادر البيانات الثلاثة (Hipparcos وYale Bright Star Catalog وGliese/Jahreiss Catalog) مصحوبة بإحداثيات ديكارتية xyz محسوبة مسبقًا. لنبدأ.

رسم بيانات النجوم
الخطوة الأولى هي تمثيل كل نجمة في الفهرس كجسيم واحد.
النجوم المسماة
بعض النجوم في الكتالوج لديهم أسماء مناسبة، ومصنّفون هنا.

استغرق الأمر حوالي ساعة لتجميع شيء يضع بيانات النجوم في الفضاء الثلاثي الأبعاد. يتضمّن مجموعة البيانات 119,617 نجمًا بالضبط، لذا لا يمثّل عرض كل نجم باستخدام جسيم مشكلة بالنسبة إلى وحدة معالجة الرسومات الحديثة. هناك أيضًا 87 نجمًا تم تحديدها بشكل فردي، لذا أنشأتُ طبقة تراكب لعلامة CSS باستخدام الأسلوب نفسه الذي وصفته في Small Arms Globe.

في ذلك الوقت، كنت قد انتهيتُ للتو من سلسلة ألعاب Mass Effect. في اللعبة، يُدعى اللاعب إلى استكشاف المجرة ومسح الكواكب المختلفة ضوئيًا والاطّلاع على تاريخها الخيالي بالكامل والذي يشبه محتوى ويكيبيديا، مثل أنواع الكائنات الحية التي ازدهرت على الكوكب وتاريخه الجيولوجي وما إلى ذلك.

مع العلم بكمية البيانات الفعلية المتوفّرة حول النجوم، يمكن للمرء أن يعرض معلومات حقيقية حول المجرة بالطريقة نفسها. الهدف النهائي من هذا المشروع هو إحياء هذه البيانات، والسماح للمشاهد باستكشاف المجرة على طريقة لعبة Mass Effect، والتعرّف على النجوم وتوزيعها، ونأمل أن يثير ذلك شعورًا بالرهبة والدهشة بشأن الفضاء. أخيرًا!

قبل أن أتابع بقية دراسة الحالة هذه، يجب أن أقول إنّني لست عالم فلك بأي شكل من الأشكال، وأنّ هذا العمل هو نتيجة بحث هواة مدعوم ببعض النصائح من خبراء خارجيين. يجب بالتأكيد تفسير هذا المشروع على أنّه تفسير فني للفضاء.

بناء مجرّة

كانت خطتي هي إنشاء نموذج إجرائي للمجرة يمكنه وضع بيانات النجوم في سياقها، وآمل أن يقدّم هذا النموذج عرضًا رائعًا لموقعنا في درب التبانة.

نموذج أوّلي مبكر للمجرة
نموذج أوّلي لنظام الجسيمات في مجرّة درب التبانة

لإنشاء مجرّة درب التبانة، أنشأتُ 100,000 جسيم ووضعتها في شكل حلزوني من خلال محاكاة طريقة تكوّن الأذرع المجرّية. لم أكن قلقًا بشأن تفاصيل تكوين الأذرع الحلزونية لأنّ هذا سيكون نموذجًا تمثيليًا وليس رياضيًا. ومع ذلك، حاولت أن يكون عدد الأذرع الحلزونية صحيحًا إلى حد ما، وأن تدور في "الاتجاه الصحيح".

في الإصدارات اللاحقة من نموذج درب التبانة، قلّلتُ من استخدام الجزيئات لصالح صورة مسطّحة لمجرة ترافق الجزيئات، على أمل أن يمنحها ذلك مظهرًا أقرب إلى الصورة الفوتوغرافية. الصورة الفعلية هي للمجرة الحلزونية NGC 1232 التي تبعد عنا حوالي 70 مليون سنة ضوئية، وقد تم التلاعب بالصورة لتبدو مثل مجرة درب التبانة.

تحديد حجم المجرّة
كل وحدة GL هي سنة ضوئية. في هذه الحالة، يبلغ عرض الكرة 110,000 سنة ضوئية، وتشمل نظام الجسيمات.

قرّرتُ في البداية تمثيل وحدة GL واحدة، أي بكسل ثلاثي الأبعاد، بسنة ضوئية واحدة، وهو اصطلاح وحّد موضع كل ما تم تصويره، ولكنّه تسبّب لي في مشاكل خطيرة في الدقة لاحقًا.

اتّبعتُ أسلوبًا آخر وهو تدوير المشهد بأكمله بدلاً من تحريك الكاميرا، وهو أسلوب استخدمته في بعض المشاريع الأخرى. إحدى المزايا هي أنّ كل شيء يتم وضعه على "قرص دوّار"، وبالتالي يؤدي سحب الماوس إلى اليمين واليسار إلى تدوير العنصر المعنيّ، ولكن تكبير الصورة لا يتطلّب سوى تغيير camera.position.z.

مجال الرؤية (FOV) للكاميرا ديناميكي أيضًا. كلما ابتعدنا، اتّسع مجال الرؤية، ما يتيح لنا رؤية المزيد والمزيد من المجرّة. ويحدث العكس عند التحرك للداخل نحو نجم، حيث يضيق مجال الرؤية. يتيح ذلك للكاميرا عرض أشياء متناهية الصغر (بالمقارنة مع المجرة) من خلال تقليل مجال الرؤية إلى ما يشبه العدسة المكبرة الإلهية بدون الحاجة إلى التعامل مع مشاكل قص المستوى القريب.

طرق مختلفة لعرض مجرّة
(أعلاه) مجرّة جسيمات مبكرة (أدناه) جزيئات مصحوبة بمستوى صورة

من هنا، تمكّنتُ من "تحديد موقع" الشمس على مسافة معيّنة من مركز المجرّة. تمكّنت أيضًا من عرض الحجم النسبي للمجموعة الشمسية من خلال تحديد نصف قطر منحدر كويبر (اخترت في النهاية عرض سحابة أورط بدلاً من ذلك). ضمن هذا النظام الشمسي النموذجي، يمكنني أيضًا عرض مدار مبسط للأرض، ونصف قطر الشمس الفعلي مقارنةً به.

النظام الشمسي
الشمس التي تدور حولها الكواكب وكرة تمثّل حزام كايبر

كان من الصعب عرض الشمس. كان عليّ استخدام أكبر عدد ممكن من تقنيات الرسومات في الوقت الفعلي. سطح الشمس عبارة عن رغوة ساخنة من البلازما، ويجب أن ينبض ويتغيّر بمرور الوقت. تمت محاكاة ذلك من خلال نسيج نقطي لصورة بالأشعة تحت الحمراء لسطح الشمس. تُجري أداة تظليل السطح عملية بحث عن اللون استنادًا إلى تدرّج الرمادي في هذه الصورة، وتُجري عملية بحث في منحدر ألوان منفصل. وعندما يتم تغيير موضع البحث هذا بمرور الوقت، يحدث هذا التشويش الشبيه بالحمم البركانية.

تم استخدام أسلوب مشابه مع هالة الشمس، إلا أنّه كان عبارة عن بطاقة sprite مسطّحة تواجه الكاميرا دائمًا باستخدام https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

عرض Sol.
إصدار مبكر من الشمس

تم إنشاء التوهجات الشمسية باستخدام برامج تظليل الرؤوس والقطع التي تم تطبيقها على شكل كعكة دائرية، وتدور حول حافة سطح الشمس. يحتوي برنامج تظليل الرؤوس على دالة تشويش تجعله ينسج بطريقة تشبه النقطة.

في هذه المرحلة، بدأت أواجه بعض المشاكل المتعلّقة بتداخل الأسطح بسبب دقة GL. تم تحديد جميع متغيرات الدقة مسبقًا في THREE.js، لذا لم يكن بإمكاني زيادة الدقة بشكل واقعي بدون بذل مجهود كبير. لم تكن مشاكل الدقة سيئة بالقرب من نقطة الأصل. ولكن عندما بدأت تصميم نماذج لأنظمة نجمية أخرى، أصبحت هذه المشكلة تؤثر في عملي.

نموذج النجمة
تمّ تعميم الرمز البرمجي لعرض الشمس لاحقًا من أجل عرض نجوم أخرى.

استخدمتُ بعض الحيل للتخفيف من مشكلة التداخل. ‫Material.polygonoffset في THREE هي سمة تتيح عرض المضلّعات في موقع مختلف (حسب فهمي). تم استخدام هذا الخيار لإجبار مستوى الهالة على العرض دائمًا فوق سطح الشمس. أسفل ذلك، تم عرض "هالة" الشمس لإظهار أشعة ضوئية حادة تتحرك بعيدًا عن الكرة.

كانت هناك مشكلة أخرى متعلقة بالدقة، وهي أنّ نماذج النجوم كانت تبدأ بالاهتزاز عند تكبير المشهد. لحلّ هذه المشكلة، كان عليّ "إعادة ضبط" دوران المشهد وتدوير نموذج النجم وخريطة البيئة بشكل منفصل لإيهام المستخدم بأنّه يدور حول النجم.

إنشاء تأثير عدسة الكاميرا

مع زيادة القدرات تزداد المسؤولية.
مع زيادة القدرات تزداد المسؤولية.

أعتقد أنّ التصاميم الفضائية هي المكان المناسب للإفراط في استخدام تأثيرات توهّج العدسة. تؤدي THREE.LensFlare هذه الأغراض، وكل ما كان عليّ فعله هو إضافة بعض الأشكال السداسية المشوّهة قليلاً ولمسة من JJ Abrams. يوضّح المقتطف أدناه كيفية إنشائها في المشهد.

// 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;

}
}

طريقة سهلة لتمرير النسيج

مستوحاة من Homeworld
مستوى ديكارتي للمساعدة في تحديد الاتجاهات المكانية في الفضاء

بالنسبة إلى "مستوى التوجيه المكاني"، تم إنشاء THREE.CylinderGeometry() ضخم وتوسيطه على الشمس. لإنشاء تأثير "موجة الضوء" المتوسّعة إلى الخارج، عدّلتُ إزاحة الملمس بمرور الوقت على النحو التالي:

mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}

map هي المادة التي تنتمي إلى المادة، والتي تحصل على دالة onUpdate يمكنك استبدالها. يؤدي ضبط الإزاحة إلى "تمرير" النسيج على طول هذا المحور، وسيؤدي إرسال قيمة needsUpdate = true بشكل متكرر إلى فرض تكرار هذا السلوك.

استخدام منحدرات الألوان

لكل نجم لون مختلف استنادًا إلى "مؤشر الألوان" الذي خصّصه علماء الفلك لها. بشكل عام، تكون النجوم الحمراء أكثر برودةً والنجوم الزرقاء أو الأرجوانية أكثر سخونةً. يتضمّن هذا التدرّج اللوني شريطًا من اللون الأبيض والبرتقالي المتوسط.

عند عرض النجوم، أردت أن أعطي كل جسيم لونه الخاص استنادًا إلى هذه البيانات. وكانت الطريقة المتاحة لذلك هي استخدام "السمات" الممنوحة لمادة التظليل المطبَّقة على الجسيمات.

var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};

سيؤدي ملء مصفوفة colorIndex إلى منح كل جسيم لونه الفريد في برنامج التظليل. عادةً ما يتم إدخال vec3 للألوان، ولكن في هذه الحالة، أدخلتُ قيمة عددية عائمة للبحث عن تدرّج الألوان النهائي.

تدرّج الألوان
مخطط تدرّج الألوان المستخدَم للبحث عن اللون المرئي من فهرس ألوان النجم

كانت منحدرات الألوان تبدو على هذا النحو، ولكن كان عليّ الوصول إلى بيانات ألوان الصورة النقطية من JavaScript. ولإجراء ذلك، حمّلتُ الصورة أولاً على نموذج المستند (DOM)، ثم رسمتها في عنصر لوحة الرسم، ثم وصلتُ إلى صورة نقطية للوحة الرسم.

// 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;
}

يتم بعد ذلك استخدام الطريقة نفسها لتلوين النجوم الفردية في طريقة عرض نموذج النجوم.

عيناي!
يتم استخدام الأسلوب نفسه للبحث عن لون الفئة الطيفية لنجم.

Shader wrangling

خلال المشروع، اكتشفت أنّني بحاجة إلى كتابة المزيد والمزيد من برامج التظليل لتحقيق جميع المؤثرات المرئية. لقد كتبتُ برنامج تحميل مخصّصًا لتظليل الصور لهذا الغرض لأنّني سئمت من تخزين تظليل الصور في ملف 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 Text Labels on top of THREE.js

في مشروعنا الأخير، Small Arms Globe، جرّبتُ جعل التصنيفات النصية تظهر فوق مشهد THREE.js. تحسب الطريقة التي كنت أستخدمها موضع النموذج المطلق للمكان الذي أريد أن يظهر فيه النص، ثم تحدد موضع الشاشة باستخدام THREE.Projector()‎، وأخيرًا تستخدم CSS "أعلى" و "يسار" لوضع عناصر CSS في الموضع المطلوب.

استخدمتُ هذه التقنية نفسها في الإصدارات الأولى من هذا المشروع، ولكنّني كنتُ أتوق إلى تجربة هذه الطريقة الأخرى التي وصفها "لويس كروز".

الفكرة الأساسية هي مطابقة تحويل المصفوفة CSS3D مع كاميرا ومشهد THREE، ويمكنك "وضع" عناصر CSS في 3D كما لو كانت في أعلى مشهد THREE. ومع ذلك، هناك قيود على ذلك، مثلاً لن تتمكّن من وضع نص أسفل عنصر THREE.js. يظلّ هذا أسرع بكثير من محاولة تنفيذ التنسيق باستخدام سمات CSS "الأعلى" و "اليسار".

تصنيفات نصية
استخدام عمليات تحويل CSS3D لوضع تصنيفات نصية فوق WebGL

يمكنك العثور على العرض التوضيحي (والرمز في عرض المصدر) لهذا المثال هنا. ومع ذلك، تبيّن لي أنّ ترتيب المصفوفة قد تغيّر منذ ذلك الحين في 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 على "فقدان" اتجاهه الموروث من المشهد. تُعرف هذه التقنية باسم "اللوحات الإعلانية"، وتطبيق Gyroscope مثالي لتنفيذها.

والأمر الرائع حقًا هو أنّ جميع عناصر DOM وCSS العادية كانت تعمل بشكل جيد، مثل إمكانية تمرير مؤشر الماوس فوق تصنيف نصي ثلاثي الأبعاد وجعله يتوهج مع ظلال متساقطة.

تصنيفات نصية
جعل تصنيفات النصوص تواجه الكاميرا دائمًا من خلال ربطها بـ THREE.Gyroscope().

عند التكبير، تبيّن لي أنّ تغيير حجم الخط كان يتسبّب في مشاكل في تحديد المواضع. هل يرجع ذلك إلى تباعد الأحرف وحشو النص؟ المشكلة الأخرى هي أنّ النص يصبح منقّطًا عند التكبير لأنّ أداة العرض في نموذج المستند (DOM) تتعامل مع النص المعروض على أنّه شكل رباعي الأضلاع مزخرف، لذا يجب الانتباه إلى ذلك عند استخدام هذه الطريقة. بالنظر إلى الماضي، كان بإمكاني استخدام نص بحجم خط كبير جدًا، وربما يكون هذا شيئًا يمكن استكشافه في المستقبل. في هذا المشروع، استخدمتُ أيضًا تصنيفات نصية لتحديد موضع العناصر في CSS، كما سبق أن شرحتُ، وذلك للعناصر الصغيرة جدًا التي ترافق الكواكب في النظام الشمسي.

تشغيل الموسيقى بشكل متكرّر

كانت المقطوعة الموسيقية التي تم تشغيلها خلال "الخريطة الكونية" في لعبة Mass Effect من تأليف "سام هوليك" و"جاك وول"، وهما مؤلفان موسيقيان في شركة Bioware، وقد كانت تحمل نوع العاطفة التي أردت أن يشعر بها الزائر. أردنا إضافة بعض الموسيقى إلى مشروعنا لأنّنا شعرنا أنّها جزء مهم من الأجواء، فهي تساعد في إضفاء الشعور بالرهبة والدهشة الذي كنا نسعى إلى تحقيقه.

تواصل المنتج "فالدين كلومب" مع "سام" الذي كان لديه مجموعة من المقاطع الموسيقية التي لم تُستخدم في لعبة Mass Effect، وقد سمح لنا مشكورًا باستخدامها. عنوان الأغنية هو "In a Strange Land".

استخدمتُ علامة الصوت لتشغيل الموسيقى، ولكن حتى في Chrome كانت السمة "loop" غير موثوقة، ففي بعض الأحيان لا يتم تكرار التشغيل. في النهاية، تم استخدام هذه الطريقة المبتكرة لعلامة الصوت المزدوجة للتحقّق من نهاية التشغيل والانتقال إلى العلامة الأخرى لتشغيل الصوت. ما كان مخيّبًا للآمال هو أنّ هذا المشهد الثابت لم يكن يتكرّر بشكل مثالي طوال الوقت، ولكنّني أعتقد أنّ هذا هو أفضل ما يمكنني فعله.

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 بشكل كبير، على سبيل المثال تحديد بيانات المواد في ملف منفصل، ويفضّل أن يكون قابلاً للعرض والتعديل في سياق معيّن، ويمكن إعادته إلى المشروع الرئيسي.

قضى زميلنا "راي مكلور" بعض الوقت في إنشاء بعض "ضوضاء الفضاء" الرائعة التي تم حذفها بسبب عدم استقرار واجهة برمجة تطبيقات الصوت على الويب، ما أدى إلى تعطُّل Chrome من حين لآخر. هذا أمر مؤسف، ولكنّه حفّزنا على التفكير أكثر في مجال الصوت في أعمالنا المستقبلية. في وقت كتابة هذا الرد، تم إبلاغي بأنّه تم إصلاح Web Audio API، لذا من المحتمل أن يعمل هذا الإجراء الآن، وهو أمر يجب الانتباه إليه في المستقبل.

لا يزال من الصعب دمج العناصر الطباعية مع WebGL، ولستُ متأكدًا بنسبة% 100 مما إذا كانت الطريقة التي نتبعها هنا هي الطريقة الصحيحة. لا يزال يبدو وكأنه حلّ غير تقليدي. ربما يمكن استخدام إصدارات مستقبلية من THREE، مع أداة العرض CSS Renderer القادمة، لدمج هذين العالمين بشكل أفضل.

الساعات المعتمَدة

أشكر "آرون كوبلين" على السماح لي بتنفيذ هذا المشروع. أودّ أن أشكر "جونو براندل" على تصميم واجهة المستخدم وتنفيذها بشكل ممتاز، وعلى معالجة الخطوط، وتنفيذ الجولة. أودّ أن أشكر Valdean Klump على تسمية المشروع وتقديم جميع النصوص. نشكر صباح أحمد على توضيح حقوق الاستخدام لمصادر البيانات والصور. أودّ أن أشكر "كليم رايت" على التواصل مع الأشخاص المناسبين لنشر هذا الكتاب. "دوغ فريتز" للتفوّق التقني أشكر "جورج براور" على تعليمه لي لغتي JS وCSS. وأودّ أيضًا شكر Mr. Doob على THREE.js.

المراجع