إنشاء تموج المياه للويب: الجزء 3
Arabic (العربية/عربي) translation by Maryam Abbas (you can also view the original English article)
مرحبًا بك مرة أخرى في هذه السلسلة المكونة من ثلاثة أجزاء حول إنشاء تموج الماء الانيق في PlayCanvas باستخدام مظلل قمة الرأس. في الجزء الثاني، قمنا بتغطية خطوط الطفو والرغوة. في هذا الجزء الأخير، سنقوم بتطبيق التشويه تحت الماء كإجراء مابعد المعالجة.
الانكسار وتأثيرات ما بعد المعالجة
هدفنا هو توصيل انكسار الضوء عبر الماء بصريًا. لقد قمنا بالفعل بتغطية كيفية إنشاء هذا النوع من التشويه في جزء مظلل في برنامج تعليمي سابق لمشهد ثنائي الأبعاد. والفرق الوحيد هنا هو أننا سنحتاج إلى معرفة أي منطقة من الشاشة تكون تحت الماء وتطبق التشويه فقط هناك.
ما بعد المعالجة
بشكل عام، فإن تطبيق تأثير ما بعد المعالجة هو أي شيء يطبق على المشهد بأكمله بعد عرضه، مثل مسحة ملونة أو تأثير شاشة CRT القديمة. بدلاً من عرض المشهد
الخاص بك مباشرةً على الشاشة، تقوم أولاً بعرضه على مخزن مؤقت أو محتوى، ثم عرض
ذلك على الشاشة، مرورًا بتظليل مخصص.
في PlayCanvas، يمكنك إعداد تأثير ما بعد المعالجة بإنشاء نص برمجي جديد. قم بتسميته Refraction.js، وانسخ هذا القالب للبدء بـه:
//--------------- POST EFFECT DEFINITION------------------------// pc.extend(pc, function () { // Constructor - Creates an instance of our post effect var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; // this is the shader definition for our effect this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; // Our effect must derive from pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { // Every post effect must implement the render method which // sets any parameters that the shader might require and // also renders the effect on the screen render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; // Set the input render target to the shader. This is the image rendered from our camera scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); // Draw a full screen quad on the output target. In this case the output target is the screen. // Drawing a full screen quad will run the shader that we defined above pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //--------------- SCRIPT DEFINITION------------------------// var Refraction = pc.createScript('refraction'); Refraction.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Refraction.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' }); // initialize code called once per entity Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); // add the effect to the camera's postEffects queue var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; // Save the current shaders for hot reload this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; }; Refraction.prototype.update = function(){ if(this.savedFS != this.fs.resource || this.savedVS != this.vs.resource){ this.swap(this); } }; Refraction.prototype.swap = function(old){ this.entity.camera.postEffects.removeEffect(old.effect); this.initialize(); };
هذا مثل البرنامج النصي العادي، لكننا نحدد فئة ما بعد تأثير الانكسار
التي يمكن تطبيقها على الكاميرا. هذا يحتاج إلى أن يعرض قمة الرأس ومظلل الجزء. تم إعداد السمات بالفعل، لذلك لننشئ Refraction.frag بهذا المحتوى:
precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; }
ويقطع الانكسار بمظلل القمة الاساسي
attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; }
الآن قم بإرفاق البرنامج النصي Refraction.js بالكاميرا، وعين المظلل بالسمات المناسبة. عند إطلاق اللعبة، يجب أن تشاهد المشهد تمامًا كما كان من قبل. هذا تأثير تالي أبيض ببساطة يعيد عرض المشهد. للتحقق من نجاح ذلك، حاول إعطاء المشهد صبغة حمراء.
في Refraction.frag، بدلاً من مجرد إرجاع اللون، حاول تعيين المكون الأحمر إلى 1.0، والذي يجب أن يبدو مثل الصورة أدناه.



مظلل التشويه
نحتاج إلى إضافة وقت موحد للتشويه المتحرك، لذا انطلق وقم بإنشاء ذلك في Refraction.js، داخل هذا المنشئ لما بعد التأثير:
var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; // this is the shader definition for our effect this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>> Initialize the time here this.time = 0; };
الآن، داخل وظيفة الuvq هذه، نمررها إلى مظللنا ونزيدها:
RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { // Every post effect must implement the render method which // sets any parameters that the shader might require and // also renders the effect on the screen render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; // Set the input render target to the shader. This is the image rendered from our camera scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>> Pass the time uniform here scope.resolve("uTime").setValue(this.time); this.time += 0.1; // Draw a full screen quad on the output target. In this case the output target is the screen. // Drawing a full screen quad will run the shader that we defined above pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } });
الآن يمكننا استخدام نفس شفيرة المظلل من البرنامج التعليمي لتشويه الماء، مما يصنع مظهر شظايا كامل لدينا يبدو كالتالي:
precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying vec2 vUv0; void main() { vec2 pos = vUv0; float X = pos.x*15.+uTime*0.5; float Y = pos.y*15.+uTime*0.5; pos.y += cos(X+Y)*0.01*cos(Y); pos.x += sin(X-Y)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; }
إذا تم تنفيذ كل شيء، يجب أن يبدو كل شيء كما لو كان تحت الماء، كما هو موضح أدناه.



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



سيتم تقديم ذلك إلى محتوى يعمل كقناع. سنقوم بعد ذلك بتمرير هذا المحتوى إلى آلة التظليل الانكسارية، والتي من شأنها أن تشوه بكسل فقط في الصورة النهائية إذا كان البكسل المقابل في القناع أبيض.
دعونا نضيف خاصية منطقية على سطح الماء لمعرفة ما إذا كان يتم استخدامها كقناع. أضف هذا إلى Water.js:
Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"});
يمكننا بعد ذلك تمريرها إلى المظلل مع بارامتر ضبط المادة ('isMask'، this.isMask)
؛ كما هو معتاد. ثم نعلن عن ذلك في Water.frag ونعين اللون الأبيض إذا كان صحيحًا.
// Declare the new uniform at the top uniform bool isMask; // At the end of the main function, override the color to be white // if the mask is true if(isMask){ color = vec4(1.0); }
تأكد من أن هذا يعمل عن طريق تبديل خاصية "Is Mask?" في المحرر وإعادة إطلاق اللعبة. يجب أن تبدو بيضاء، كما في الصورة السابقة.
الآن، لإعادة عرض المشهد، نحتاج إلى كاميرا ثانية. قم بإنشاء كاميرا جديدة في المحرر وأطلق عليها اسم CameraMask. انسخ كيان المياه في المحرر كذلك، واطلق عليه WaterMask. تأكد من "Is Mask?" غير صحيح بالنسبة لكيان المياه ولكن صحيح لـ WaterMask.
لإخبار الكاميرا الجديدة بأن يتم عرضها على نسيج بدلاً من الشاشة، قم بإنشاء برنامج نصي جديد يسمى CameraMask.js وقم بإرفاقه بالكاميرا الجديدة. نحن ننشئ هدف العارض لالتقاط خرج هذه الكاميرا كما يلي:
// initialize code called once per entity CameraMask.prototype.initialize = function() { // Create a 512x512x24-bit render target with a depth buffer var colorBuffer = new pc.Texture(this.app.graphicsDevice, { width: 512, height: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true }); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = new pc.RenderTarget(this.app.graphicsDevice, colorBuffer, { depth: true }); this.entity.camera.renderTarget = renderTarget; };
الآن، إذا قمت بالتشغيل، فسترى أن هذه الكاميرا لم تعد تعرض على الشاشة. يمكننا الحصول على الخرج للعارض المستهدف في Refraction.js مثل هذا:
Refraction.prototype.initialize = function() { var cameraMask = this.app.root.findByName('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer); // ... // The rest of this function is the same as before };
لاحظ أنني قمت بتمرير محتوى القناع هذا كوسيطة لمنشئ تأثير آخر. نحتاج إلى إنشاء مرجع له في منشئنا، لذا يبدو الأمر كما يلي:
//// Added an extra argument on the line below var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; // this is the shader definition for our effect this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<< Saving the buffer here this.buffer = buffer; };
أخيراً في وظيفة العارض، مرر العازل الى مظللنا مع:
scope.resolve("uMaskBuffer").setValue(this.buffer);
الآن للتحقق من أن هذا كله يعمل، سأترك ذلك كتحدي.
# 2 التحدي: تقديم uMaskBuffer إلى الشاشة لتأكيد أنه هو خرج الكاميرا الثانية.
هناك شيء واحد يجب أن تدركه هو أن هدف العرض تم إعداده في تهيئة CameraMask.js، ويجب أن يكون جاهزًا في الوقت الذي يتم فيه استدعاء Refraction.js. إذا كانت البرامج النصية تعمل بالعكس، فسوف تحصل على خطأ. للتأكد من أنها تعمل بالترتيب الصحيح، اسحب قناع الكاميرا إلى أعلى قائمة الكيانات في المحرر، كما هو موضح أدناه.



يجب أن تبحث الكاميرا الثانية دائمًا عن نفس طريقة العرض الأصلية ، لذلك دعنا نتابع دائمًا موقعها ودورانها في تحديث CameraMask.js:
CameraMask.prototype.update = function(dt) { var pos = this.CameraToFollow.getPosition(); var rot = this.CameraToFollow.getRotation(); this.entity.setPosition(pos.x,pos.y,pos.z); this.entity.setRotation(rot); };
وحدد متابعة الكاميرا
في البدء:
this.CameraToFollow = this.app.root.findByName('Camera');
ابطال الاقنعة
كل من الكاميرات تقدم في الوقت نفسه الشيء نفسه. نريد من كاميرا القناع أن تقدم كل شيء باستثناء الماء الحقيقي، ونريد أن تعرض الكاميرا الحقيقية كل شيء ما عدا ماء القناع.
للقيام بذلك، يمكننا استخدام قناع ابطال صغير للكاميرا. يعمل هذا بطريقة مماثلة لأقنعة التصادم إذا سبق لك أن استخدمت هذه الأقنعة. سيتم إبطال كائن (لا يتم عرضه) إذا كانت نتيجة البت الموافق AND
بين قناعه وقناع الكاميرا 1.
لنفترض أن الماء سيكون لديه مجموعة 2 بت، وسيكون لقناع الماء 3 بت. عندئذ تحتاج الكاميرا الحقيقية إلى ضبط جميع البتات باستثناء 3، وتحتاج كاميرا القناع إلى ضبط جميع البتات باستثناء 2. طريقة سهلة للقول "جميع البتات باستثناء N" هي:
~(1 << N) >>> 0
يمكنك قراءة المزيد حول العوامل التي تعمل بالتبادل هنا.
لإعداد أقنعة التخلص من الكاميرا، يمكننا وضع هذا داخل CameraMask.js الذي يتم تهيئته في الأسفل:
// Set all bits except for 2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; // Set all bits except for 3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; // If you want to print out this bit mask, try: // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2));
الآن، في Water.js، عيّن قناع شبكة الماء على بت 2، ونسخة القناع له على البت 3:
// Put this at the bottom of the initialize of Water.js // Set the culling masks var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit);
الآن، سيكون هناك عرض واحد للمياه العادية، والآخر سوف يكون الماء الأبيض المستمر. النصف الأيسر من الصورة أدناه هو العرض من الكاميرا الأصلية، والنصف الأيمن من كاميرا القناع.



تطبيق القناع
خطوة أخيرة الآن! نحن نعلم أن المناطق تحت الماء يتم تمييزها بالبكسلات البيضاء. نحتاج فقط للتحقق مما إذا لم يكن لدينا بكسل أبيض، وإذا كان الأمر كذلك، فأوقف التشوه في Refraction.frag:
// Check original position as well as new distorted position vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); // We're not at a white pixel? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ // Return it back to the original position pos = vUv0; }
وينبغي أن تفعل ذلك!
شيء واحد هو أن نلاحظ أنه منذ تهيئة القناع عند الإطلاق، إذا قمت بتغيير حجم الإطار في وقت التشغيل، فإنه لن يتطابق مع حجم الشاشة.
تنعيم
كخطوة اختيارية للتنظيف، ربما لاحظت أن الحواف في المشهد تبدو الآن حادة بعض الشيء. هذا لأنه عندما طبقنا ما بعد التأثير لدينا، فقدنا النعومة.
يمكننا تطبيق تنعيم إضافي فوق تأثيرنا كتأثير تالي آخر. لحسن الحظ، هناك تنعيم متاح في متجر PlayCanvas يمكننا استخدامه تماماً. انتقل إلى صفحة أصول النص البرمجي، وانقر على زر التنزيل الأخضر الكبير، واختر مشروعك من القائمة التي تظهر. سيظهر النص في جذر نافذة الأصول الخاصة بك كـ posteffect-fxaa.js. ما عليك سوى إرفاقه بكيان الكاميرا، ويجب أن يبدو مشهدك أجمل قليلاً!
افكار اخيرة
إذا كنت قد وصلت إلى هذا الحد فهنئ نفسك! غطينا الكثير من التقنيات في هذه السلسلة. يجب عليك الآن أن تكون مرتاحاً مع تظليل قمة الرأس، وتقديم القوام، وتطبيق تأثيرات ما بعد المعالجة، واختيار الأشياء بشكل انتقائي، واستخدام المخزن المؤقت العميق، والعمل مع المزج والشفافية. على الرغم من أننا ننفذ هذا في PlayCanvas، فهذه كلها مفاهيم رسومات عامة ستجدها بشكل ما على أي نظام أساسي ينتهي بك الأمر به.
كل هذه التقنيات تنطبق أيضا على مجموعة متنوعة من التأثيرات الأخرى. أحد التطبيقات المثيرة للاهتمام التي وجدتها لتظليل قمة الرأس هو في هذا الحديث عن فن أبزو، حيث يشرحون كيف استخدموا تظليل قمة الرأس في تحريك عشرات الآلاف من الأسماك على الشاشة بكفاءة.
يجب أن يكون لديك الآن تأثير مائي لطيف يمكنك تطبيقه على ألعابك! يمكنك بسهولة تخصيصه الآن بعد أن قمت بتجميع كل التفاصيل بنفسك. لا يزال هناك الكثير الذي يمكنك القيام به مع الماء (لم أذكر حتى أي نوع من أنواع التأمل على الإطلاق). فيما يلي بعض الأفكار.
موجات مستندة إلى الضوضاء
فبدلاً من تحريك الأمواج بمزيج من الجيب وجيب التمام، يمكنك أخذ عينة من الضجيج لجعل الأمواج تبدو طبيعية أكثر ولا يمكن التنبؤ بها.
مسارات رغوية ديناميكية
بدلاً من خطوط المياه الساكنة تمامًا على السطح، يمكنك أن ترسم على ذلك النسيج عندم تتحرك الأشياء، لإنشاء مسار رغوي ديناميكي. هناك الكثير من الطرق للقيام بهذا، لذا قد يكون هذا هو مشروعه الخاص.
مصدر الشيفرة
يمكنك العثور على مشروع PlayCanvas المستضاف النهائي هنا. يتوفر أيضًا منفذ Three.js في هذا المستودع.