إنشاء تظليل المياه الكرتون للويب. الجزء 3

الصورة

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

تأثيرات الانكسار وما بعد المعالجة


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

المعالجة اللاحقة


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

في PlayCanvas ، يمكنك تخصيص تأثير ما بعد المعالجة هذا من خلال إنشاء برنامج نصي جديد. دعنا نسميها Refraction.js ونسخ هذا القالب إليها فارغاً:

//---------------   ------------------------// pc.extend(pc, function () { //  -      var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //      this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; //      pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //    ,    , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //---------------  ------------------------// 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       Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); //     postEffects var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; //       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(); }; 

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

 precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; } 

و Refract.vert مع تظليل قمة الرأس الأساسي:

 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.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>>    this.time = 0; }; 

الآن داخل وظيفة التجسيد ، نمرره إلى جهاز تظليله لزيادة حجمه:

 RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //      , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>>    uniform-  scope.resolve("uTime").setValue(this.time); this.time += 0.1; //       .       . //          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(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; } 

إذا تم عمل كل شيء بشكل صحيح ، فيجب أن تبدو الصورة بأكملها كما لو كانت تحت الماء تمامًا.


المهمة 1: تأكد من أن التشويه ينطبق فقط على الجزء السفلي من الشاشة.

أقنعة الكاميرا


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


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

دعنا نضيف سمة منطقية على سطح الماء لمعرفة ما إذا كانت تستخدم كقناع. أضف ما يلي إلى Water.js:

 Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"}); 

ثم ، كالعادة ، يمكننا تمريره إلى جهاز تظليل باستخدام material.setParameter('isMask',this.isMask); . ثم قم بتعريفه في Water.frag ولون البيكسل الأبيض إذا كانت السمة صحيحة.

 //    uniform uniform bool isMask; //      ,    //    true if(isMask){ color = vec4(1.0); } 

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

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

لطلب كاميرا جديدة لعرضها على نسيج بدلاً من شاشة ، قم بإنشاء برنامج نصي CameraMask.js جديد وإرفاقه بالكاميرا الجديدة. نقوم بإنشاء RenderTarget لالتقاط إخراج هذه الكاميرا:

 //  initialize       CameraMask.prototype.initialize = function() { //  512x512x24-      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); // ... //     ,    }; 

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

 ////       var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<<    this.buffer = buffer; }; 

أخيرًا ، في وظيفة التجسيد ، نقوم بتمرير المخزن المؤقت إلى جهاز التظليل لدينا:

 scope.resolve("uMaskBuffer").setValue(this.buffer); 

الآن ، للتأكد من أن كل هذا يعمل ، سأتركه لك كمهمة.

المهمة 2: اعرض uMaskBuffer على الشاشة للتأكد من أنه ناتج الكاميرا الثانية.

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


يجب أن تبدو الكاميرا الثانية دائمًا بنفس طريقة العرض الأصلية ، لذلك دعونا نجعلها تتبع دائمًا موضع برنامج 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); }; 

في التهيئة ، حدد CameraToFollow :

 this.CameraToFollow = this.app.root.findByName('Camera'); 

أقنعة القص


تقدم كلتا الكاميرات الآن نفس الشيء. نريد من كاميرا القناع تقديم كل شيء ما عدا الماء الحقيقي ، والكاميرا الحقيقية لتقديم كل شيء ما عدا ماء القناع.

للقيام بذلك ، يمكننا استخدام قناع لقطة بت الكاميرا. يعمل على غرار أقنعة التصادم . سيتم قطع كائن (أي لا يتم تقديمه) إذا كانت نتيجة bitwise AND بين قناعه وقناع الكاميرا 1.

لنفترض أن الماء يحتوي على بت 2 ، ووتر ماسك يحتوي على بت 3. يجب تعيين جميع البتات باستثناء 3 لكاميرا حقيقية ، وجميع البتات باستثناء 2 لكاميرا قناع. أسهل طريقة لقول "جميع البتات باستثناء N" هي كما يلي الطريقة:

 ~(1 << N) >>> 0 

اقرأ المزيد عن عمليات أحادي المعامل هنا .

لتكوين أقنعة لقطة الكاميرا ، يمكننا إدراج ما يلي في أسفل تهيئة البرنامج النصي CameraMask.js :

  //   ,  2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; //   ,  3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; //      ,   : // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2)); 

الآن في Water.js ، سنضع البت 2 من قناع شبكة المياه ، وإصدار القناع منه في البت 3:

 //      initialize  Water.js //    var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit); 

الآن نوع واحد سيكون بالماء العادي ، والثاني بالمياه البيضاء الصلبة. تُظهر الصورة الموجودة على اليسار المنظر من الكاميرا الأصلية ، وعلى اليمين المنظر من كاميرا القناع.


تطبيق القناع


والآن الخطوة الأخيرة! نحن نعلم أن المناطق تحت الماء مميزة ببيكسلات بيضاء. نحتاج فقط إلى التحقق مما إذا كنا في بكسل أبيض ، وإذا لم يكن كذلك ، فقم بإيقاف التشويه في Refraction.frag :

 //   ,      vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); //     ? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ //      pos = vUv0; } 

وهذا يجب أن يحل مشكلتنا!

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

تنعيم


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

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

خواطر في الختام


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

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

الآن لديك تأثير مائي جميل يمكنك تطبيقه في ألعابك! يمكنك تخصيصه وإضافة التفاصيل الخاصة بك. يمكن عمل الكثير بالماء (لم أذكر أي نوع من أنواع الانعكاسات). فيما يلي بعض الأفكار.

موجات الضوضاء


بدلاً من مجرد تحريك الأمواج بمزيج من جيب التمام وجيب الجيب ، يمكنك تجربة النسيج بحيث تبدو الموجات أكثر طبيعية وأقل توقعًا.

آثار رغوة ديناميكية


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

كود المصدر


يمكن العثور على مشروع PlayCanvas النهائي هنا . يحتوي مستودعنا أيضًا على منفذ مشروع تحت Three.js .

Source: https://habr.com/ru/post/ar417091/


All Articles