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

مقدمة

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

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

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

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

نظرة خاطفة

يتضمّن تطبيق "العثور على طريقك إلى Oz" على الكمبيوتر المكتبي عالمًا غنيًا وغامرًا. نستخدم مؤثرات ثلاثية الأبعاد وطبقات عديدة من التأثيرات التقليدية المستوحاة من صناعة الأفلام لإضفاء مشهد شبه واقعي. أبرز التقنيات هي WebGL مع 3.js وأدوات تظليل مخصصة وعناصر DOM متحركة باستخدام ميزات CSS3. بالإضافة إلى ذلك، توفّر getUserMedia API (WebRTC) تجارب تفاعلية تتيح للمستخدم إضافة صورته مباشرةً من كاميرا الويب وWeb Audio للحصول على صوت ثلاثي الأبعاد.

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

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

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

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

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

مشهد عاصفة

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

اللوحة غير اللامعة قليلاً

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

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

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

برنامج تعليمي: التوافق مع Sprite Sheets و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 التي تم إنشاؤها

بمجرد إنشاء ورقة 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]
   },
}

المكان:

  • تشير الصورة إلى عنوان 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. عند التفكير في مشهد ثلاثي الأبعاد، يكون أحد أصعب الأسئلة هو كيفية التأكد من إمكانية إنشاء محتوى يتيح له إمكانية التعبير إلى أقصى حد من جانبَي النمذجة والرسوم المتحركة والتأثيرات. تشتمل هذه المشكلة على طُرق عديدة، منها مسار المحتوى، وهو عملية متفق عليها يجب اتّباعها لإنشاء محتوى للمشهد الثلاثي الأبعاد.

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

ولقد كنا نعمل على هذا النوع من المشكلات لبعض الوقت لأنه في كل مرة أنشأنا فيها موقعًا ثلاثي الأبعاد في الماضي كنا قد وجدنا قيودًا في الأدوات التي يمكننا استخدامها. لذلك أنشأنا هذه الأداة، المسماة 3D Librarian: جزء من البحث الداخلي. وكان تقريبًا جاهزًا للتقدم في وظيفة حقيقية.

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

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

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

فيديو تعليمي: السماح بالرياح

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

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

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

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

قماش ناعم.

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

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

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

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

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

برنامج تعليمي بسيط لرياح ثلاثية الأبعاد

لننشئ الآن تأثير الرياح في مشهد ثلاثي الأبعاد بسيط في 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 )

إذًا، ما أنجزناه حتى الآن

لا يوجد شيء فاخر حتى الآن.

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

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

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

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

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

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

يتم إنشاء ضوضاء بيرلين إجرائيًا من خلال أداة تظليل تُسمى NoiseShader. يستخدم عنصر التظليل هذا خوارزميات ضوضاء بسيطة ثلاثية الأبعاد من: https://github.com/ashima/webgl-noise . تم الحصول على نسخة WebGL بشكل حرفي من أحد نماذج MrDoob Three.js على: 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 إلى زخرفة. ويتم ذلك في الدالة 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 المُعاد صياغتها باستخدام الضوضاء عند مزجها كزخرفة:

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

يتم تركيب قيمة 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;

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

ركاب على العاصفة

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

مشهد ركوب البالون

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

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

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

فيديو تعليمي: The Storm Shader

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

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

داخل خلية عصبية للماوس باستخدام أداة تظليل حجمي مخصصة
داخل خلية عصبية للماوس باستخدام أداة تظليل حجمي مخصّصة

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

يتضمّن أسلوب التظليل خدعة تستخدم بشكل أساسي أداة تظليل GLSL واحدة لعرض كائن كامل باستخدام خوارزمية عرض مبسّطة تسمى عرض Ray marching (العرض السريع للأشعة) مع حقل مسافة. في هذه التقنية يتم إنشاء أداة تظليل وحدات البكسل التي تقدر أقرب مسافة إلى سطح لكل نقطة على الشاشة.

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

يبدأ قلب أداة التظليل بالوظيفة الرئيسية، ويعمل على إعداد تحويلات الكاميرا وإدخال حلقة تقيّم المسافة إلى السطح بشكل متكرر. الاستدعاء RaytraceFoggy( أكثر اتجاه متجه, max_iterations, لون, 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 مبسطة، (لتجنب المظهر المليء بالأجزاء) @line 1107 في stormTest.coffee. وهذا يعني أنه في حالة أسوأ، ينتهي الأمر بإعصار أكثر ضبابية ولكنه يعمل على الأقل دون أن يسلب المستخدم السيطرة.

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

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

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

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

إعصار قمعي

موقع الويب المتوافق مع الأجهزة الجوّالة

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

اعتقدنا أنه سيكون من الرائع الحصول على Carnival Photo-Booth من سطح المكتب كتطبيق ويب على الأجهزة الجوّالة قد يستخدم كاميرا الهاتف الجوّال للمستخدم. شيء لم نراه حتى الآن.

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

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

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

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

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

وبفضل هذه الطريقة وفئة المهمة، يمكننا بسهولة معرفة مستوى التقدّم على مستوى العالم (MainPreloadTask) أو مستوى تقدّم مواد العرض (AssetPreloadTask) أو مستوى تقدّم تحميل النماذج (TemplatePreFetchTask). التقدم المتساوي في ملف معين. للتعرّف على كيفية إجراء ذلك، يمكنك إلقاء نظرة على فئة المهمة على الرابط /m/JavaScript/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 (من خلال تنفيذ المهمّة المشتركة)، عليك أيضًا ملاحظة كيفية تحميل الأصول التي تعتمد على النظام الأساسي. في الأساس، لدينا أربعة أنواع من الصور. المعيار القياسي للأجهزة الجوّالة ( .ext، حيث يكون امتداد الملف بتنسيق .png أو .jpg) عادةً فبدلاً من إجراء عملية الرصد في MainPreloadTask والترميز الثابت لأربعة مصفوفات لمواد العرض، نذكر فقط اسم مادة العرض المطلوب تحميلها وامتدادها وما إذا كانت مادة العرض تعتمد على النظام الأساسي (متجاوب = صحيح / خطأ). بعد ذلك، ستنشئ لنا 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 Photo Booth (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 فقط، لذا نحتاج إلى إضافة فحص إضافي مقابل التعبير العادي بعد اختيار الملف:
   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 أي إجراء، ولا تُعلِم على الإطلاق. لذلك لا نحتاج إلى أي إجراء خاص لهذه الحالة، ومع ذلك، تقوم هواتف 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 على هذا الرابط.

المساهمون

انقر هنا للاطّلاع على القائمة الكاملة للأرصدة.

المراجع