دراسة حالة - Find Your Way to Oz

مقدمة

"Find Your Way to Oz" هي تجربة جديدة في Google Chrome تقدّمها شركة Disney على الويب. تتيح لك هذه الرحلة التفاعلية زيارة سيرك في كانساس، ثم الانتقال إلى أرض أوز بعد أن تُجرف بعيدًا بسبب عاصفة ضخمة.

كان هدفنا الجمع بين روعة السينما والإمكانات الفنية للمتصفّح لتوفير تجربة ممتعة وغامرة يمكن للمستخدمين التفاعل معها بشكل كبير.

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

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

نظرة من الداخل

يقدّم تطبيق Find Your Way to Oz على أجهزة الكمبيوتر المكتبي عالمًا غامرًا وغنيًا. نستخدم التأثيرات ثلاثية الأبعاد والعديد من الطبقات المستوحاة من الأفلام التقليدية التي تجتمع معًا لإنشاء مشهد شبه واقعي. وتشمل التقنيات الأكثر بروزًا WebGL مع Three.js وتأثيرات مخصّصة وعناصر متحركة في DOM باستخدام ميزات CSS3. بالإضافة إلى ذلك، تتوفّر واجهة برمجة التطبيقات getUserMedia API (WebRTC) للتجارب التفاعلية التي تسمح للمستخدم بإضافة صورته مباشرةً من كاميرا الويب وWebAudio للحصول على صوت ثلاثي الأبعاد.

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

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

قبل مشاركة سرّنا، نريد تحذيرك من أنّه قد يتعطل، تمامًا كما لو كنت تبحث داخل محرك سيارة. تأكَّد من عدم تشغيل أيّ محتوى مهمّ وانتقِل إلى عنوان URL الرئيسي للموقع الإلكتروني وأضِف ?debug=on إلى العنوان. انتظِر تحميل الموقع الإلكتروني وبعد الدخول إليه (اضغط على؟) المفتاح Ctrl-I وسيظهر لك قائمة منسدلة على الجانب الأيمن. في حال إلغاء تحديد خيار "الخروج من مسار الكاميرا"، يمكنك استخدام المفاتيح A وW وS وD والماوس للتنقّل بحرية في المساحة.

مسار الكاميرا

لن نتناول جميع الإعدادات هنا، ولكن ننصحك بالتجربة: تكشف المفاتيح عن إعدادات مختلفة في مشاهد مختلفة. في تسلسل العاصفة النهائي، يتوفّر مفتاح إضافي: Ctrl-A يمكنك من خلاله إيقاف تشغيل الصور المتحركة أو تشغيلها والتحليق حولها. في هذا المشهد، إذا ضغطت على Esc (للخروج من وظيفة قفل الماوس) ثم ضغطت مرة أخرى على Ctrl-I، يمكنك الوصول إلى الإعدادات الخاصة بمشهد العاصفة. يمكنك الاطّلاع على بعض المناظر الجميلة التي تصلح لتكون بطاقة بريدية، مثل الصورة أدناه.

منظر عاصفة

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

يشبه إلى حدٍ ما اللوحة المموّهة

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

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

تم إنشاء الطبقة العليا من الواجهة باستخدام DOM وCSS 3، ما يعني أنّه يمكن تعديل التفاعلات بعدة طرق بغض النظر عن تجربة العرض الثلاثي الأبعاد مع التواصل بين الاثنين وفقًا لقائمة محدّدة من الأحداث. يستخدم هذا الإجراء Backbone Router + حدث onHashChange في HTML5 الذي يتحكّم في المنطقة التي يجب أن تظهر فيها الصور المتحركة أو تختفي. (مصدر المشروع: ‎/develop/coffee/router/Router.coffee).

دليل تعليمي: أوراق Sprite ودعم شاشات Retina

من بين تقنيات التحسين الممتعة التي اعتمدنا عليها في الواجهة، كان دمج العديد من الصور التي تظهر على الواجهة في صورة PNG واحدة لتقليل طلبات الخادم. في هذا المشروع، تم إنشاء الواجهة من أكثر من 70 صورة (باستثناء الأشكال الثلاثية الأبعاد) تم تحميلها مسبقًا لتقليل وقت الاستجابة للموقع الإلكتروني. يمكنك الاطّلاع على جدول صور متحركة للعناصر الحية هنا:

الشاشة العادية: http://findyourwaytooz.com/img/home/interface_1x.png شاشة Retina: http://findyourwaytooz.com/img/home/interface_2x.png

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

إنشاء أوراق صور متحركة

لإنشاء أوراق SpriteSheets، استخدمنا TexturePacker الذي يُخرج الملفات بأي تنسيق تحتاجه. في هذه الحالة، تم التصدير بتنسيق EaselJS الذي يتميز بتصميمه البسيط، ويمكن استخدامه أيضًا لإنشاء صور متحركة.

استخدام جدول Sprite Sheet الذي تم إنشاؤه

بعد إنشاء لوحة رموز متحركة، من المفترض أن يظهر لك ملف JSON على النحو التالي:

{
   "images": ["interface_2x.png"],
   "frames": [
       [2, 1837, 88, 130],
       [2, 2, 1472, 112],
       [1008, 774, 70, 68],
       [562, 1960, 86, 86],
       [473, 1960, 86, 86]
   ],

   "animations": {
       "allow_web":[0],
       "bottomheader":[1],
       "button_close":[2],
       "button_facebook":[3],
       "button_google":[4]
   },
}

المكان:

  • تشير السمة image إلى عنوان URL لجدول أجزاء الصور المتحركة.
  • الإطارات هي إحداثيات كل عنصر من عناصر واجهة المستخدم [x وy وwidth وheight]
  • الصور المتحركة هي أسماء كل مادة عرض.

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

الخلاصة

بعد أن أصبحنا جاهزين، ما علينا سوى استخدام مقتطف JavaScript.

var SSAsset = function (asset, div) {
  var css, x, y, w, h;

  // Divide the coordinates by 2 as retina devices have 2x density
  x = Math.round(asset.x / 2);
  y = Math.round(asset.y / 2);
  w = Math.round(asset.width / 2);
  h = Math.round(asset.height / 2);

  // Create an Object to store CSS attributes
  css = {
    width                : w,
    height               : h,
    'background-image'   : "url(" + asset.image_1x_url + ")",
    'background-size'    : "" + asset.fullSize[0] + "px " + asset.fullSize[1] + "px",
    'background-position': "-" + x + "px -" + y + "px"
  };

  // If retina devices

  if (window.devicePixelRatio === 2) {

    /*
    set -webkit-image-set
    for 1x and 2x
    All the calculations of X, Y, WIDTH and HEIGHT is taken care by the browser
    */

    css['background-image'] = "-webkit-image-set(url(" + asset.image_1x_url + ") 1x,";
    css['background-image'] += "url(" + asset.image_2x_url + ") 2x)";

  }

  // Set the CSS to the DIV
  div.css(css);
};

في ما يلي كيفية استخدامها:

logo = new SSAsset(
{
  fullSize     : [1024, 1024],               // image 1x dimensions Array [x,y]
  x            : 1790,                       // asset x coordinate on SpriteSheet         
  y            : 603,                        // asset y coordinate on SpriteSheet
  width        : 122,                        // asset width
  height       : 150,                        // asset height
  image_1x_url : 'img/spritesheet_1x.png',   // background image 1x URL
  image_2x_url : 'img/spritesheet_2x.png'    // background image 2x URL
},$('#logo'));

لفهم المزيد من المعلومات عن كثافة البكسل المتغيرة، يمكنك قراءة هذه المقالة التي كتبها "بوريس سموس".

مسار المحتوى الثلاثي الأبعاد

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

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

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

كانت هذه الأداة متوفّرة في السابق لاستخدامها مع Flash، وكانت تتيح لك جلب مشهد Maya كبير كملف واحد مضغوط تم تحسينه لوقت فك الحزمة. وكان السبب في أنّه الأمثل هو أنّه يُحزم المشهد بفعالية في بنية البيانات نفسها التي يتم التلاعب بها أثناء العرض والحركة. لا يتطلّب الملف الكثير من التحليل عند تحميله. كان فك الحزمة في Flash سريعًا جدًا لأنّ الملف كان بتنسيق AMF، وهو تنسيق يمكن لبرنامج Flash فك تشفيره بشكل أصلي. ويتطلّب استخدام التنسيق نفسه في WebGL مزيدًا من العمل على وحدة المعالجة المركزية. في الواقع، كان علينا إعادة إنشاء طبقة رمز JavaScript لفك ترميز البيانات، والتي من شأنها فك ضغط هذه الملفات وإعادة إنشاء هياكل البيانات اللازمة لعمل WebGL. إنّ فك ترميز المشهد الثلاثي الأبعاد بالكامل هو عملية تتطلّب قدرًا معتدلاً من طاقة وحدة المعالجة المركزية: يستغرق فك ترميز المشهد 1 في Find Your Way To Oz حوالي ثانيتَين على جهاز متوسط أو عالي المستوى. ولذلك، يتم إجراء ذلك باستخدام تكنولوجيا Web Workers في وقت "إعداد المشهد" (قبل إطلاق المشهد فعليًا)، وذلك لتجنّب تعليق التجربة للمستخدم.

يمكن لهذه الأداة المفيدة استيراد معظم المشهد الثلاثي الأبعاد: النماذج والقوام والرسوم المتحركة للعظام. يمكنك إنشاء ملف مكتبة واحد يمكن بعد ذلك تحميله بواسطة المحرّك الثلاثي الأبعاد. يمكنك إضافة كل النماذج التي تحتاجها في المشهد ضمن هذه المكتبة، وvoila، يمكنك إنشاؤها في المشهد.

واجهتنا مشكلة في أنّنا نتعامل الآن مع WebGL، وهو تنسيق جديد. لقد كان هذا الطفل صعبًا جدًا، فقد كان يحدّد معايير التجارب الثلاثية الأبعاد المستندة إلى المتصفّح. لذلك، أنشأنا طبقة JavaScript مخصّصة تأخذ ملفات المشهد الثلاثي الأبعاد المضغوطة من 3D Librarian وتترجمها بشكل صحيح إلى تنسيق يمكن لـ WebGL فهمه.

دليل توجيهي: إضافة رياح

كان موضوع الرياح مكرّرًا في أغنية "Find Your Way To Oz". تم تصميم سلسلة من أحداث القصة لتكون تصاعدًا في قوة الرياح.

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

لذلك، كان من المهم توفير تأثير ريح غامرة.

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

قطعة قماش ناعمة

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

في WebGL أو JavaScript، لا تتوفّر لنا (حتى الآن) إمكانية تنفيذ محاكاة فيزيائية كاملة. لذلك، كان علينا في أستراليا إيجاد طريقة لإنشاء تأثير الرياح بدون تقليدها.

لقد أدرجنا معلومات "حساسية الرياح" لكل جسم في النموذج الثلاثي الأبعاد نفسه. كان لكلّ رأس من رؤوس النموذج الثلاثي الأبعاد "سمة الرياح" التي حدّدت مقدار تأثُّر هذا الرأس بالرياح. وبالتالي، فإنّ هذه القيمة المحدّدة هي حساسية الرياح للأجسام الثلاثية الأبعاد. بعد ذلك، كان علينا إنشاء الرياح نفسها.

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

لإنتاج تأثير الرياح، يتم نقل الصورة بمرور الوقت بسرعة ثابتة في اتجاه محدّد، وهو اتجاه الرياح. ولضمان عدم تأثُّر كلّ ما في المشهد بـ "المنطقة التي تهبّ فيها الرياح"، نلفّ صورة الرياح حول الحواف، ونحصرها في منطقة التأثير.

A Simple 3D Wind Tutorial

لننشئ الآن تأثير الرياح في مشهد ثلاثي الأبعاد بسيط في Three.js.

سننشئ رياحًا في "حقل عشبي آلي" بسيط.

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

أرض مليئة بالعشب
تضاريس مليئة بالعشب

في ما يلي كيفية إنشاء هذا المشهد البسيط في Three.js باستخدام CoffeeScript.

أولاً، سنبدأ بإعداد Three.js، وربطه بالكاميرا ووحدة تحكّم بالماوس وبعض الإضاءة، على النحو التالي:

constructor: ->

   @clock =  new THREE.Clock()

   @container = document.createElement( 'div' );
   document.body.appendChild( @container );

   @renderer = new THREE.WebGLRenderer();
   @renderer.setSize( window.innerWidth, window.innerHeight );
   @renderer.setClearColorHex( 0x808080, 1 )
   @container.appendChild(@renderer.domElement);

   @camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 5000 );
   @camera.position.x = 5;
   @camera.position.y = 10;
   @camera.position.z = 40;

   @controls = new THREE.OrbitControls( @camera, @renderer.domElement );
   @controls.enabled = true

   @scene = new THREE.Scene();
   @scene.add( new THREE.AmbientLight 0xFFFFFF )

   directional = new THREE.DirectionalLight 0xFFFFFF
   directional.position.set( 10,10,10)
   @scene.add( directional )

   # Demo data
   @grassTex = THREE.ImageUtils.loadTexture("textures/grass.png");
   @initGrass()
   @initTerrain()

   # Stats
   @stats = new Stats();
   @stats.domElement.style.position = 'absolute';
   @stats.domElement.style.top = '0px';
   @container.appendChild( @stats.domElement );
   window.addEventListener( 'resize', @onWindowResize, false );
   @animate()

تؤدي طلبات الدالتَين initGrass وinitTerrain إلى ملء المشهد بالعشب والتضاريس على التوالي:

initGrass:->
   mat = new THREE.MeshPhongMaterial( { map: @grassTex } )
   NUM = 15
   for i in [0..NUM] by 1
       for j in [0..NUM] by 1
           x = ((i/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
           y = ((j/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
           @scene.add( @instanceGrass( x, 2.5, y, 5.0, mat ) )

instanceGrass:(x,y,z,height,mat)->
   geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )
   mesh = new THREE.Mesh( geometry, mat )
   mesh.position.set( x, y, z )
   return mesh

سننشئ هنا شبكة من 15 قطعة عشب بحجم 15 قطعة. نضيف بعض العشوائية إلى كل موضع عشب، حتى لا يتم صفّها مثل الجنود، ما سيبدو غريبًا.

هذه التضاريس هي مجرد مستوى أفقي، تم وضعه في قاعدة قطع العشب (y = 2.5).

initTerrain:->
  @plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial({ map: @grassTex }))
  @plane.rotation.x = -Math.PI/2
  @scene.add( @plane )

ما فعلناه حتى الآن هو إنشاء مشهد Three.js وإضافة بعض قطع العشب، المصنوعة من مخاريط مقلوبة يتم إنشاؤها بشكل آلي، وتضاريس بسيطة.

ما مِن أخبار رائعة حتى الآن.

حان الوقت الآن لبدء إضافة الرياح. أولاً، نريد تضمين معلومات حساسية الرياح في النموذج الثلاثي الأبعاد للعشب.

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

في ما يلي كيفية إعادة ترميز الدالة instanceGrass لإضافة حساسية الرياح كسمة مخصّصة للنموذج الثلاثي الأبعاد للعشب.

instanceGrass:(x,y,z,height)->

  geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )

  for i in [0..geometry.vertices.length-1] by 1
      v = geometry.vertices[i]
      r = (v.y / height) + 0.5
      @windMaterial.attributes.windFactor.value[i] = r * r * r

  # Create mesh
  mesh = new THREE.Mesh( geometry, @windMaterial )
  mesh.position.set( x, y, z )
  return mesh

نستخدم الآن مادة مخصّصة، وهي windMaterial، بدلاً من MeshPhongMaterial التي استخدمناها سابقًا. يُغلِف WindMaterial WindMeshShader الذي سنراه بعد دقيقة.

وبالتالي، يتضمّن الرمز في instanceGrass حلقة تتكرّر في جميع رؤوس نموذج العشب، وتضيف لكل رأس سمة رأس مخصّصة تُسمى windFactor. يتم ضبط متغيّر windFactor على 0 للجزء السفلي من نموذج العشب (حيث من المفترض أن يلامس التضاريس)، ويتم ضبطه على القيمة 1 للجزء العلوي من نموذج العشب.

العنصر الآخر الذي نحتاجه هو إضافة الرياح الفعلية إلى المشهد. كما ناقشنا، سنستخدم ضوضاء Perlin لهذا الغرض. سننشئ بشكلٍ آلي نسيجًا للضوضاء من النوع Perlin.

من أجل الوضوح، سنحدّد هذا الملمس للتضاريس نفسها بدلاً من الملمس الأخضر السابق. سيسهّل ذلك التعرّف على حالة الرياح.

وبالتالي، سيغطي نسيج الضوضاء Perlin هذا مساحة تضاريسنا، وسيحدّد كل بكسل من النسيج شدة الرياح في منطقة التضاريس التي يقع فيها هذا البكسل. سيكون مستطيل التضاريس هو "منطقة الرياح".

يتم إنشاء الضوضاء وفقًا لأسلوب معيّن من خلال برنامج تشويش يُسمى NoiseShader. يستخدم هذا المخطِّط اللوني خوارزميات الضوضاء البسيطة الثلاثية الأبعاد من: https://github.com/ashima/webgl-noise . تم أخذ إصدار WebGL من هذا النص حرفيًا من إحدى نماذج Three.js من MrDoob، على الرابط: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html.

يأخذ NoiseShader مجموعة من المَعلمات، مثل الوقت، والمقياس، والقيمة المُعدَّلة، كقيم ثابتة، ويُخرج توزيعًا ثنائي الأبعاد جميلاً للضوضاء العشوائية.

class NoiseShader

  uniforms:     
    "fTime"  : { type: "f", value: 1 }
    "vScale"  : { type: "v2", value: new THREE.Vector2(1,1) }
    "vOffset"  : { type: "v2", value: new THREE.Vector2(1,1) }

...

سنستخدم هذا المخطِّط لتضمين تأثير Perlin Noise في نسيج. ويتم ذلك في دالة initNoiseShader.

initNoiseShader:->
  @noiseMap  = new THREE.WebGLRenderTarget( 256, 256, { minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat } );
  @noiseShader = new NoiseShader()
  @noiseShader.uniforms.vScale.value.set(0.3,0.3)
  @noiseScene = new THREE.Scene()
  @noiseCameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2,  window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 );
  @noiseCameraOrtho.position.z = 100
  @noiseScene.add( @noiseCameraOrtho )

  @noiseMaterial = new THREE.ShaderMaterial
      fragmentShader: @noiseShader.fragmentShader
      vertexShader: @noiseShader.vertexShader
      uniforms: @noiseShader.uniforms
      lights:false

  @noiseQuadTarget = new THREE.Mesh( new THREE.PlaneGeometry(window.innerWidth,window.innerHeight,100,100), @noiseMaterial )
  @noiseQuadTarget.position.z = -500
  @noiseScene.add( @noiseQuadTarget )

تعمل التعليمة البرمجية أعلاه على إعداد noiseMap كهدف لعرض Three.js، وتزويده بـ NoiseShader، ثم عرضه باستخدام كاميرا منظّمة، وذلك لتجنّب التشوهات في المنظور.

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

في ما يلي دالة initTerrain التي تمت إعادة صياغتها باستخدام noiseMap كنسيج:

initTerrain:->
  @plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial( { map: @noiseMap, lights: false } ) )
  @plane.rotation.x = -Math.PI/2
  @scene.add( @plane )

بعد أن أضفنا نسيج الرياح، لنلقِ نظرة على WindMeshShader، وهو المكوّن المسؤول عن تشويه نماذج العشب وفقًا للرياح.

لإنشاء هذا المخطّط اللوني، بدأنا من مخطّط اللوني العادي Three.js MeshPhongMaterial وغيّرناه. هذه طريقة سريعة وجيدة للبدء باستخدام مخطّط ألوان يعمل بدون الحاجة إلى البدء من الصفر.

لن ننسخ رمز مخطِّط الألوان بالكامل هنا (يمكنك الاطّلاع عليه في ملف رمز المصدر)، لأنّ معظمه سيكون نسخة طبق الأصل من مخطِّط الألوان MeshPhongMaterial. ولكن لنلقِ نظرة على الأجزاء المعدَّلة ذات الصلة بالرياح في Vertex Shader.

vec4 wpos = modelMatrix * vec4( position, 1.0 );
vec4 wpos = modelMatrix * vec4( position, 1.0 );

wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
vWindForce = texture2D(tWindForce,windUV).x;

float windMod = ((1.0 - vWindForce)* windFactor ) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;

mvPosition = modelViewMatrix *  pos;

وبالتالي، فإنّ ما يفعله هذا المخطِّط هو احتساب إحداثي البحث عن نسيج windUV أولاً، استنادًا إلى موضع الرأس ثنائي الأبعاد xz (أفقي). يتم استخدام إحداثيات UV هذه للبحث عن قوة الرياح، vWindForce، من نسيج الرياح الناتجة عن تشويش Perlin.

يتم دمج قيمة vWindForce هذه مع السمة المخصّصة windFactor الخاصة بالنقطة، والتي تمت مناقشتها أعلاه، من أجل احتساب مقدار التشوه الذي تحتاجه النقطة. لدينا أيضًا مَعلمة windScale شاملة للتحكّم في القوة الإجمالية للرياح، وعنصر windDirection الذي يحدّد الاتجاه الذي يجب أن يحدث فيه تشوه الرياح.

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

كما ذكرنا سابقًا، سنحتاج إلى تمرير نسيج الضوضاء بمرور الوقت على منطقة الرياح لكي يهتز الزجاج.

ويتم ذلك من خلال تغيير متغير vOffset الموحّد الذي يتم تمريره إلى NoiseShader بمرور الوقت. هذه مَعلمة vec2، ستسمح لنا بتحديد إزاحة الضوضاء في اتجاه معيّن (اتجاه الرياح).

ننفّذ ذلك في دالة العرض التي يتمّ استدعاؤها في كلّ لقطة:

render: =>
  delta = @clock.getDelta()

  if @windDirection
      @noiseShader.uniforms[ "fTime" ].value += delta * @noiseSpeed
      @noiseShader.uniforms[ "vOffset" ].value.x -= (delta * @noiseOffsetSpeed) * @windDirection.x
      @noiseShader.uniforms[ "vOffset" ].value.y += (delta * @noiseOffsetSpeed) * @windDirection.z
...

هذا كل ما في الأمر. لقد أنشأنا للتو مشهدًا يتضمّن "عشبًا آليًا" متأثرًا بالرياح.

إضافة غبار إلى المزيج

لنضيف بعض اللمسات إلى المشهد. لنضيف بعض الغبار المتطاير إلى المشهد لجعله أكثر إثارة للاهتمام.

إضافة غبار
إضافة غبار

من المفترض أن يتأثر الغبار بالرياح، لذا من المنطقي أن يطير الغبار في المشهد الذي يتضمّن رياحًا.

يتم إعداد الغبار في الدالة initDust كنظام جسيمات.

initDust:->
  for i in [0...5] by 1
      shader = new WindParticleShader()
      params = {}
      params.fragmentShader = shader.fragmentShader
      params.vertexShader   = shader.vertexShader
      params.uniforms       = shader.uniforms
      params.attributes     = { speed: { type: 'f', value: [] } }

      mat  = new THREE.ShaderMaterial(params)
      mat.map = shader.uniforms["map"].value = THREE.ImageUtils.loadCompressedTexture("textures/dust#{i}.dds")
      mat.size = shader.uniforms["size"].value = Math.random()
      mat.scale = shader.uniforms["scale"].value = 300.0
      mat.transparent = true
      mat.sizeAttenuation = true
      mat.blending = THREE.AdditiveBlending
      shader.uniforms["tWindForce"].value      = @noiseMap
      shader.uniforms[ "windMin" ].value       = new THREE.Vector2(-30,-30 )
      shader.uniforms[ "windSize" ].value      = new THREE.Vector2( 60, 60 )
      shader.uniforms[ "windDirection" ].value = @windDirection            

      geom = new THREE.Geometry()
      geom.vertices = []
      num = 130
      for k in [0...num] by 1

          setting = {}

          vert = new THREE.Vector3
          vert.x = setting.startX = THREE.Math.randFloat(@dustSystemMinX,@dustSystemMaxX)
          vert.y = setting.startY = THREE.Math.randFloat(@dustSystemMinY,@dustSystemMaxY)
          vert.z = setting.startZ = THREE.Math.randFloat(@dustSystemMinZ,@dustSystemMaxZ)

          setting.speed =  params.attributes.speed.value[k] = 1 + Math.random() * 10
          
          setting.sinX = Math.random()
          setting.sinXR = if Math.random() < 0.5 then 1 else -1
          setting.sinY = Math.random()
          setting.sinYR = if Math.random() < 0.5 then 1 else -1
          setting.sinZ = Math.random()
          setting.sinZR = if Math.random() < 0.5 then 1 else -1

          setting.rangeX = Math.random() * 5
          setting.rangeY = Math.random() * 5
          setting.rangeZ = Math.random() * 5

          setting.vert = vert
          geom.vertices.push vert
          @dustSettings.push setting

      particlesystem = new THREE.ParticleSystem( geom , mat )
      @dustSystems.push particlesystem
      @scene.add particlesystem

تم إنشاء 130 جسيم غبار هنا. يُرجى العلم أنّ كلّ منها مزوّد بـ WindParticleShader خاص.

الآن، في كل لقطة، سنحرّك الجسيمات قليلاً باستخدام CoffeeScript، بغض النظر عن الرياح. إليك الرمز.

moveDust:(delta)->

  for setting in @dustSettings

    vert = setting.vert
    setting.sinX = setting.sinX + (( 0.002 * setting.speed) * setting.sinXR)
    setting.sinY = setting.sinY + (( 0.002 * setting.speed) * setting.sinYR)
    setting.sinZ = setting.sinZ + (( 0.002 * setting.speed) * setting.sinZR) 

    vert.x = setting.startX + ( Math.sin(setting.sinX) * setting.rangeX )
    vert.y = setting.startY + ( Math.sin(setting.sinY) * setting.rangeY )
    vert.z = setting.startZ + ( Math.sin(setting.sinZ) * setting.rangeZ )

بالإضافة إلى ذلك، سنغيّر موضع كل جسيم وفقًا للرياح. ويتم ذلك في WindParticleShader. وتحديدًا في برنامج تظليل رؤوس المضلّعات.

رمز هذا المخطِّط اللوني هو نسخة معدَّلة من ParticleMaterial في Three.js، وهذا هو شكل جوهرها:

vec4 mvPosition;
vec4 wpos = modelMatrix * vec4( position, 1.0 );
wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
float vWindForce = texture2D(tWindForce,windUV).x;
float windMod = (1.0 - vWindForce) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;

mvPosition = modelViewMatrix *  pos;

fSpeed = speed;
float fSize = size * (1.0 + sin(time * speed));

#ifdef USE_SIZEATTENUATION
    gl_PointSize = fSize * ( scale / length( mvPosition.xyz ) );
#else,
    gl_PointSize = fSize;
#endif

gl_Position = projectionMatrix * mvPosition;

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

Riders On The Storm

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

مشهد جولة في منطاد

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

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

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

دليل تعليمي: تأثير Storm Shader

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

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

داخل خلية عصبية في فأر باستخدام مخطِّط ألوان مخصّص
داخل خلية عصبية في فأر باستخدام مخطِّط ألوان حجمي مخصّص

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

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

يمكنك العثور على مرجع جيد للخوارزمية في النظرة العامة التي تقدّمها شركة iq: Rendering Worlds With Two Triangles - Iñigo Quilez. يمكنك أيضًا استكشاف معرض مواد التشويش على glsl.heroku.com، حيث تتوفّر العديد من الأمثلة على هذه التقنية التي يمكن تجربتها.

يبدأ جوهر برنامج التظليل بالدالة الرئيسية، التي تهيئ عمليات تحويل الكاميرا وتنتقل إلى حلقة تقيِّم بشكل متكرّر المسافة إلى سطح معيّن. تُجرى عملية احتساب خطوات شعاع الضوء الأساسية في طلب RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ).

for(int i=0;i < number_of_steps;i++) // run the ray marching loop
{
  old_d=d;
  float shape_value=Shape(q); // find out the approximate distance to or density of the tornado cone
  float density=-shape_value;
  d=max(shape_value*step_scaling,0.0);// The max function clamps values smaller than 0 to 0

  float step_dist=d+extra_step; // The point is advanced by larger steps outside the tornado,
  //  allowing us to skip empty space quicker.

  if (density>0.0) {  // When density is positive, we are inside the cloud
    float brightness=exp(-0.6*density);  // Brightness decays exponentially inside the cloud

    // This function combines density layers to create a translucent fog
    FogStep(step_dist*0.2,clamp(density, 0.0,1.0)*vec3(1,1,1), vec3(1)*brightness, colour, multiplier); 
  }
  if(dist>max_dist || multiplier.x < 0.01) { return;  } // if we've gone too far stop, we are done
  dist+=step_dist; // add a new step in distance
  q=org+dist*dir; // trace its direction according to the ray casted
}

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

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

mat2 Spin(float angle){
  return mat2(cos(angle),-sin(angle),sin(angle),cos(angle)); // a rotation matrix
}

// This takes noise function and makes ridges at the points where that function crosses zero
float ridged(float f){ 
  return 1.0-2.0*abs(f);
}

// the isosurface shape function, the surface is at o(q)=0 
float Shape(vec3 q) 
{
    float t=time;

    if(q.z < 0.0) return length(q);

    vec3 spin_pos=vec3(Spin(t-sqrt(q.z))*q.xy,q.z-t*5.0); // spin the coordinates in time

    float zcurve=pow(q.z,1.5)*0.03; // a density function dependent on z-depth

    // the basic cloud of a cone is perturbed with a distortion that is dependent on its spin 
    float v=length(q.xy)-1.5-zcurve-clamp(zcurve*0.2,0.1,1.0)*snoise(spin_pos*vec3(0.1,0.1,0.1))*5.0; 

    // create ridges on the tornado
    v=v-ridged(snoise(vec3(Spin(t*1.5+0.1*q.z)*q.xy,q.z-t*4.0)*0.3))*1.2; 

    return v;
}

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

الجزء الأول من المشكلة: تحسين هذا المخطّط اللوني للمشهد. لحلّ هذه المشكلة، كان علينا اتّباع نهج "آمن" في حال كان تأثير التظليل ثقيلًا جدًا. لإجراء ذلك، دمجنا تأثيرات مظهر الإعصار بدرجة دقة مختلفة عن بقية المشهد. هذا من ملف stormTest.coffee (نعم، كان هذا اختبارًا).

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

...
Line 1383
@tornadoRT = new THREE.WebGLRenderTarget( @SCENE_WIDTH, @SCENE_HEIGHT, paramsN )

... 
Line 1403 
# Change settings based on FPS
if @fpsCount > 0
    if @fpsCur < 20
        @tornadoSamples = Math.min( @tornadoSamples + 1, @MAX_SAMPLES )
    if @fpsCur > 25
        @tornadoSamples = Math.max( @tornadoSamples - 1, @MIN_SAMPLES )
    @tornadoW = @SCENE_WIDTH  / @tornadoSamples // decide tornado resWt
    @tornadoH = @SCENE_HEIGHT / @tornadoSamples // decide tornado resHt

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

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

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

كانت حلول مشاكل التوافق بين لوحات الفيديو المختلفة مشابهة: تأكَّد من إدخال ثوابت ثابتة من نوع البيانات الدقيق كما هو محدّد، أي 0.0 للنوع float و0 للنوع int. انتبه عند كتابة دوال أطول، ويُفضَّل تقسيم المهام إلى دوال متعددة أبسط ومتغيّرات مؤقتة لأنّ برامج التحويل البرمجي لا تعالج حالات معيّنة بشكل صحيح. تأكَّد من أنّ جميع العناصر المركّبة هي ناتج من قاعدة 2، وألّا تكون كبيرة جدًا، ويجب في جميع الأحوال توخي "الحذر" عند البحث عن بيانات العناصر المركّبة في حلقة.

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

إعصار

الموقع الإلكتروني المتوافق مع الأجهزة الجوّالة

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

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

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

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

نصائح وحيل حول الأجهزة الجوّالة

إنّ أداة التحميل المُسبَق ضرورية، وليس من المُفترَض تجنُّبها. ندرك أنّه يحدث ذلك في بعض الأحيان. ويعود السبب في ذلك بشكل أساسي إلى أنّك تحتاج إلى مواصلة صيانة قائمة العناصر التي تحمّلها مسبقًا مع نمو مشروعك. والأسوأ من ذلك، ليس من الواضح تمامًا كيفية احتساب مستوى التقدّم في التحميل إذا كنت تسحب موارد مختلفة، والعديد منها في الوقت نفسه. هذا هو المكان الذي تكون فيه فئة "المهمة" المجردة المخصّصة والعامة جدًا مفيدة. تتمثل الفكرة الرئيسية في السماح ببنية متداخلة بلا حدود حيث يمكن أن تتضمّن المهمة مهام فرعية خاصة بها، ويمكن أن تتضمّن هذه المهام الفرعية مهام فرعية أخرى، وما إلى ذلك. بالإضافة إلى ذلك، تحسب كل مهمة مستوى تقدّمها استنادًا إلى مستوى تقدّم المهام الفرعية (وليس إلى مستوى تقدّم المهمة الرئيسية). من خلال جعل كل من MainPreloadTask وAssetPreloadTask وTemplatePreFetchTask مشتقة من Task، أنشأنا بنية تبدو على النحو التالي:

أداة التحميل المسبق

بفضل هذا النهج وفئته Task، يمكننا بسهولة معرفة مستوى التقدّم العام (MainPreloadTask) أو مستوى التقدّم في مواد العرض (AssetPreloadTask) أو مستوى التقدّم في تحميل النماذج (TemplatePreFetchTask). حتى مستوى تقدّم ملف معيّن. لمعرفة كيفية تنفيذ ذلك، يمكنك الاطّلاع على فئة Task في ‎ /m/javascripts/raw/util/Task.js وعمليات تنفيذ المهام الفعلية في /m/javascripts/preloading/task. على سبيل المثال، في ما يلي مقتطف من كيفية إعداد فئة /m/javascripts/preloading/task/MainPreloadTask.js التي تشكّل حزمة التثبيت المُسبَق النهائية:

Package('preloading.task', [
  Import('util.Task'),
...

  Class('public MainPreloadTask extends Task', {

    _public: {
      
  MainPreloadTask : function() {
        
    var subtasks = [
      new AssetPreloadTask([
        {name: 'cutout/cutout-overlay-1', ext: 'png', type: ImagePreloader.TYPE_BACKGROUND, responsive: true},
        {name: 'journey/scene1', ext: 'jpg', type: ImagePreloader.TYPE_IMG, responsive: false}, ...
...
      ]),

      new TemplatePreFetchTask([
        'page.HomePage',
        'page.CutoutPage',
        'page.JourneyToOzPage1', ...
...
      ])
    ];
    
    this._super(subtasks);

      }
    }
  })
]);

في فئة ‎ /m/javascripts/preloading/task/subtask/AssetPreloadTask.js، بالإضافة إلى ملاحظة كيفية تواصلها مع MainPreloadTask (من خلال تنفيذ Task المشترَك)، من الجدير بالذكر أيضًا كيفية تحميل مواد العرض التي تعتمد على المنصة. في الأساس، لدينا أربعة أنواع من الصور. الدقة العادية للأجهزة الجوّالة (‎.ext، حيث يكون ext هو امتداد الملف، وعادةً ما يكون ‎ .png أو ‎ .jpg)، والدقة العالية للأجهزة الجوّالة (‎-2x.ext)، والدقة العادية للأجهزة اللوحية (‎-tab.ext)، والدقة العالية للأجهزة اللوحية (‎-tab-2x.ext). بدلاً من إجراء عملية الكشف في MainPreloadTask ووضع ترميز ثابت لأربع صفائف مواد عرض، نحدّد فقط اسم مادة العرض وامتدادها لتحميلها مسبقًا وما إذا كانت مادة العرض تعتمد على النظام الأساسي (responsive = true / false). بعد ذلك، ستنشئ AssetPreloadTask اسم الملف لنا:

resolveAssetUrl : function(assetName, extension, responsive) {
  return AssetPreloadTask.ASSETS_ROOT + assetName + (responsive === true ? ((Detection.getInstance().tablet ? '-tab' : '') + (Detection.getInstance().retina ? '-2x' : '')) : '') + '.' +  extension;
}

في أسفل سلسلة الفئات، تظهر التعليمة البرمجية الفعلية التي تُجري عملية التحميل المُسبَق لمادة العرض على النحو التالي (/m/javascripts/raw/util/ImagePreloader.js):

loadUrl : function(url, type, completeHandler) {
  if(type === ImagePreloader.TYPE_BACKGROUND) {
    var $bg = $('<div>').hide().css('background-image', 'url(' + url + ')');
    this.$preloadContainer.append($bg);
  } else {
    var $img= $('<img />').attr('src', url).hide();
    this.$preloadContainer.append($img);
  }

  var image = new Image();
  this.cache[this.generateKey(url)] = image;
  image.onload = completeHandler;
  image.src = url;
}

generateKey : function(url) {
  return encodeURIComponent(url);
}

دليل تعليمي: "معرض الصور" بتنسيق HTML5 (iOS6/Android)

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

كشك تصوير متنقل
كشك الصور المتحرّك

يمكنك الاطّلاع على عرض توضيحي مباشر هنا (شغِّله على هاتف iPhone أو Android):

http://u9html5rocks.appspot.com/demos/mobile_photo_booth

لإعداده، تحتاج إلى مثيل تطبيق مجاني على Google App Engine حيث يمكنك تشغيل الخلفية. رمز الواجهة الأمامية ليس معقّدًا، ولكن هناك بعض الأخطاء المحتملة. لنطّلِع على هذه الخطوات الآن:

  1. نوع ملف الصورة المسموح به نريد أن يتمكّن المستخدمون من تحميل الصور فقط (لأنّه كشك صور وليس كشك فيديو). من الناحية النظرية، يمكنك ببساطة تحديد الفلتر في HTML على النحو التالي: input id="fileInput" class="fileInput" type="file" name="file" accept="image/*" ومع ذلك، يبدو أنّ ذلك يعمل على نظام التشغيل iOS فقط، لذا نحتاج إلى إضافة عملية تحقّق إضافية من RegExp بعد اختيار ملف:
   this.$fileInput.fileupload({
          
   dataType: 'json',
   autoUpload : true,
   
   add : function(e, data) {
     if(!data.files[0].name.match(/(\.|\/)(gif|jpe?g|png)$/i)) {
      return self.onFileTypeNotSupported();
     }
   }
   });
  1. إلغاء عملية تحميل أو اختيار ملف لاحظنا أثناء عملية التطوير تناقضاً آخر في طريقة إرسال إشعارات على الأجهزة المختلفة عند إلغاء اختيار ملف. لا تتخذ هواتف iOS وأجهزة iOS اللوحية أي إجراء ولا ترسل إشعارات على الإطلاق. وبالتالي، لا نحتاج إلى اتخاذ أي إجراء خاص في هذه الحالة، ولكنّ هواتف Android تنشئ الدالة add()‎ على أي حال، حتى إذا لم يتم اختيار أي ملف. في ما يلي كيفية تلبية هذه المتطلبات:
    add : function(e, data) {

    if(data.files.length === 0 || (data.files[0].size === 0 && data.files[0].name === "" && data.files[0].fileName === "")) {
            
    return self.onNoFileSelected();

    } else if(data.files.length > 1) {

    return self.onMultipleFilesSelected();            
    }
    }

أما الباقي، فيعمل بسلاسة على جميع المنصات. الاستمتاع بوقتك

الخاتمة

نظرًا للحجم الهائل للعبة Find Your Way To Oz، والمجموعة الواسعة من التقنيات المختلفة المُستخدَمة فيها، لم نتمكّن في هذه المقالة من تغطية سوى بعض الأساليب التي استخدمناها.

إذا كنت مهتمًا باستكشاف الرمز المصدر الكامل لتطبيق Find Your Way To Oz، يمكنك الاطّلاع عليه من خلال هذا الرابط.

المساهمون

انقر على هذا الرابط للاطّلاع على قائمة المساهمين الكاملة.

المراجع