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

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

لجعل المشهد يبدو أفضل قليلاً ، أجريت تغييرات طفيفة عليه. يمكنك تخصيص المشهد الخاص بك بالطريقة التي تريدها ، لكنني فعلت ما يلي:

  • نماذج مضافة لمنارة وأخطبوط.
  • تمت إضافة نموذج أرضي بلون #FFA457 .
  • تمت إضافة لون #6CC8FF .
  • تمت إضافة لون الإضاءة الخلفية #FFC480 إلى المشهد (يمكن العثور على هذه المعلمات في إعدادات المشهد).

يبدو المشهد الأصلي الآن مثل هذا.


الطفو


أسهل طريقة لإنشاء الطفو هي استخدام برنامج نصي لدفع الأشياء لأعلى ولأسفل. قم بإنشاء برنامج نصي Buoyancy.js جديد وقم بتعيين ما يلي في التهيئة:

 Buoyancy.prototype.initialize = function() { this.initialPosition = this.entity.getPosition().clone(); this.initialRotation = this.entity.getEulerAngles().clone(); //     ,  //        //     this.time = Math.random() * 2 * Math.PI; }; 

الآن في التحديث نقوم بتشغيل زيادة الوقت وتدوير الكائن:

 Buoyancy.prototype.update = function(dt) { this.time += 0.1; //      var pos = this.entity.getPosition().clone(); pos.y = this.initialPosition.y + Math.cos(this.time) * 0.07; this.entity.setPosition(pos.x,pos.y,pos.z); //    var rot = this.entity.getEulerAngles().clone(); rot.x = this.initialRotation.x + Math.cos(this.time * 0.25) * 1; rot.z = this.initialRotation.z + Math.sin(this.time * 0.5) * 2; this.entity.setLocalEulerAngles(rot.x,rot.y,rot.z); }; 

تطبيق هذا البرنامج النصي على القارب ونرى كيف يقفز صعودا وهبوطا في الماء! يمكنك تطبيق هذا البرنامج النصي على عدة كائنات (بما في ذلك الكاميرا - جربها)!

نسيج السطح


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

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

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

 Water.attributes.add('surfaceTexture', { type: 'asset', assetType: 'texture', title: 'Surface Texture' }); 

ثم قم بتعيينه في المحرر:


الآن نحن بحاجة لتمريرها إلى تظليل. انتقل إلى Water.js وقم بتعيين دالة CreateWaterMaterial معلمة جديدة:

 material.setParameter('uSurfaceTexture',this.surfaceTexture.resource); 

عد الآن إلى Water.frag وأعلن عن زي جديد:

 uniform sampler2D uSurfaceTexture; 

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

متغيرات متغيرة


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

لاختباره في التشغيل ، أعلن عن متغير جديد في Water.vert على أنه متغير:

 varying vec2 ScreenPosition; 

ثم قم بتعيين القيمة gl_Position بعد حسابها:

 ScreenPosition = gl_Position.xyz; 

الآن عدنا إلى Water.frag وأعلن نفس المتغير. لا يمكننا الحصول على إخراج بيانات تصحيح الأخطاء من جهاز تظليل ، ولكن يمكننا استخدام اللون لتصحيح الأخطاء البصرية. إليك كيفية القيام بذلك:

 uniform sampler2D uSurfaceTexture; varying vec3 ScreenPosition; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); //    varying- color = vec4(vec3(ScreenPosition.x),1.0); gl_FragColor = color; } 

يجب أن يبدو المستوى الآن باللونين الأبيض والأسود ، وسيذهب خط تقسيم اللون حيث ScreenPosition.x = 0. تتغير قيم اللون فقط من 0 إلى 1 ، ولكن القيم الموجودة في ScreenPosition يمكن أن تكون خارج هذا الفاصل الزمني. يتم تقييدها تلقائيًا ، لذلك ، عندما ترى اللون الأسود ، يمكن أن يكون 0 أو رقمًا سلبيًا.

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

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

باستخدام الأشعة فوق البنفسجية


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

سنعلن عن سمة جديدة في Water.vert (هذا الاسم مأخوذ من تعريف shader في Water.js):

 attribute vec2 aUv0; 

والآن نحتاج فقط إلى تمريرها إلى جهاز تظليل الأجزاء ، لذا قم فقط بإنشاء متغير وتعيين قيمة السمة له:

 //  Water.vert //        varying vec2 vUv0; // .. //        //  varying,        vUv0 = aUv0; 

الآن سنعلن نفس المتغير المتغير في تظليل الشظية. للتأكد من أن كل شيء يعمل ، يمكننا تصور التصحيح كما كان من قبل ، ومن ثم ستبدو Water.frag على النحو التالي:

 uniform sampler2D uSurfaceTexture; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); //  UV color = vec4(vec3(vUv0.x),1.0); gl_FragColor = color; } 

من المفترض أن ترى تدرجًا يؤكد أن لدينا قيمة 0 من جهة وواحدة من جهة أخرى. الآن لتجربة الملمس بشكل حقيقي ، كل ما علينا القيام به هو:

 color = texture2D(uSurfaceTexture,vUv0); 

بعد ذلك ، سنرى الملمس على السطح:


تصميم الملمس


بدلاً من مجرد تعيين النسيج كلون جديد ، دعنا نجمعه مع اللون الأزرق الحالي:

 uniform sampler2D uSurfaceTexture; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec4 WaterLines = texture2D(uSurfaceTexture,vUv0); color.rgba += WaterLines.r; gl_FragColor = color; } 

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

ومع ذلك ، هذه ليست الطريقة الوحيدة لدمج الألوان.

المهمة 2: هل يمكنك دمج الألوان للحصول على التأثير الأضعف الموضح أدناه؟


تحريك الملمس


كتأثير نهائي ، نريد أن تتحرك الخطوط على طول السطح ولا تبدو ثابتة جدًا. للقيام بذلك ، سوف نستفيد من حقيقة أن أي قيمة خارج الفاصل الزمني من 0 إلى 1 ، تم تمريرها إلى دالة texture2D ، سيتم نقلها (على سبيل المثال ، كل من 1.5 و 2.5 تصبح مساوية لـ 0.5). لذلك ، يمكننا زيادة موضعنا من خلال المتغير الزمني الموحد الذي حددناه بالفعل لزيادة أو تقليل كثافة الخطوط على السطح ، والتي ستعطي شظية الجزء النهائي هذا الشكل:

 uniform sampler2D uSurfaceTexture; uniform float uTime; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec2 pos = vUv0; //      1 //     pos *= 2.0; //   ,      pos.y += uTime * 0.02; vec4 WaterLines = texture2D(uSurfaceTexture,pos); color.rgba += WaterLines.r; gl_FragColor = color; } 

خطوط الرغوة وعازل العمق


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

خدعة


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

ألق نظرة على الصورة أدناه.


ما هي البكسلات التي يجب أن تكون جزءًا من الرغوة؟ نحن نعلم أنه يجب أن يبدو شيء مثل هذا:


لذلك دعونا نلقي نظرة على وحدتي بكسل محددتين. أدناه قمت بتمييزها بعلامات نجمية. سيكون الأسود على الرغوة ، ولن يكون الأحمر. كيف نميزهم في تظليل؟


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


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

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

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

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

عازلة العمق


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

المخزن المؤقت للعمق هو عرض هدف خاص يحتوي على معلومات حول قيم عمق كل بكسل. لا تنس أن القيمة في gl_Position المحسوبة في تظليل الرأس كانت قيمة مساحة الشاشة ، ولكن لها أيضًا gl_Position ثالثًا - قيمة Z. يتم استخدام هذه القيمة لحساب العمق ، الذي يتم كتابته إلى المخزن المؤقت للعمق.

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

في الواقع ، يمكنك تعطيل الكتابة في المخزن المؤقت للعمق لترى كيف سيبدو كل شيء بدونها. دعونا نحاول القيام بذلك في Water.js:

 material.depthTest = false; 

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

تصور المخزن المؤقت العمق


دعونا نضيف طريقة لتقديم المخزن المؤقت العمق لأغراض التصحيح. إنشاء برنامج نصي DepthVisualize.js جديد. قم بتوصيله بالكاميرا.

للوصول إلى المخزن المؤقت للعمق في PlayCanvas ، اكتب ما يلي:

 this.entity.camera.camera.requestDepthMap(); 

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

 uniform sampler2D uDepthMap; 

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

 var DepthVisualize = pc.createScript('depthVisualize'); //  initialize,       DepthVisualize.prototype.initialize = function() { this.entity.camera.camera.requestDepthMap(); this.antiCacheCount = 0; //    ,         this.SetupDepthViz(); }; DepthVisualize.prototype.SetupDepthViz = function(){ var device = this.app.graphicsDevice; var chunks = pc.shaderChunks; this.fs = ''; this.fs += 'varying vec2 vUv0;'; this.fs += 'uniform sampler2D uDepthMap;'; this.fs += ''; this.fs += 'float unpackFloat(vec4 rgbaDepth) {'; this.fs += ' const vec4 bitShift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0);'; this.fs += ' float depth = dot(rgbaDepth, bitShift);'; this.fs += ' return depth;'; this.fs += '}'; this.fs += ''; this.fs += 'void main(void) {'; this.fs += ' float depth = unpackFloat(texture2D(uDepthMap, vUv0)) * 30.0; '; this.fs += ' gl_FragColor = vec4(vec3(depth),1.0);'; this.fs += '}'; this.shader = chunks.createShaderFromCode(device, chunks.fullscreenQuadVS, this.fs, "renderDepth" + this.antiCacheCount); this.antiCacheCount ++; //     ,        this.command = new pc.Command(pc.LAYER_FX, pc.BLEND_NONE, function () { pc.drawQuadWithShader(device, null, this.shader); }.bind(this)); this.command.isDepthViz = true; //    ,      this.app.scene.drawCalls.push(this.command); }; //  update,     DepthVisualize.prototype.update = function(dt) { }; //  swap,      //      DepthVisualize.prototype.swap = function(old) { this.antiCacheCount = old.antiCacheCount; //      for(var i=0;i<this.app.scene.drawCalls.length;i++){ if(this.app.scene.drawCalls[i].isDepthViz){ this.app.scene.drawCalls.splice(i,1); break; } } //    this.SetupDepthViz(); }; //      ,  : // http://developer.playcanvas.com/en/user-manual/scripting/ 

حاول نسخ الرمز والتعليق عليه / إلغاء تعليق السطر this.app.scene.drawCalls.push(this.command); لتمكين / تعطيل عرض العمق. يجب أن تبدو هذه الصورة أدناه.


المهمة 3: لا يتم سحب سطح الماء إلى المخزن المؤقت للعمق. يقوم محرك PlayCanvas بذلك عن قصد. هل يمكنك معرفة السبب؟ ما الذي يميز مواد المياه؟ وبعبارة أخرى ، بالنظر إلى قواعدنا الخاصة بفحص الأعماق ، ماذا سيحدث إذا تمت كتابة بكسلات المياه إلى المخزن المؤقت للعمق؟

تلميح: يمكنك تغيير سطر واحد في Water.js ، مما يسمح لك بكتابة الماء إلى المخزن المؤقت للعمق.

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

تنفيذ خدعة


هناك العديد من الوظائف المساعدة في محرك PlayCanvas للعمل بقيم العمق ، ولكن في وقت كتابة هذا التقرير ، لم يتم إصدارها في الإنتاج ، لذلك سيتعين علينا تكوينها بأنفسنا.

نحدد المتغيرات المنتظمة التالية في Water.frag :

 //   uniform-    PlayCanvas uniform sampler2D uDepthMap; uniform vec4 uScreenSize; uniform mat4 matrix_view; //      uniform vec4 camera_params; 

نحدد هذه الوظائف المساعدة على الوظيفة الرئيسية:

 #ifdef GL2 float linearizeDepth(float z) { z = z * 2.0 - 1.0; return 1.0 / (camera_params.z * z + camera_params.w); } #else #ifndef UNPACKFLOAT #define UNPACKFLOAT float unpackFloat(vec4 rgbaDepth) { const vec4 bitShift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0); return dot(rgbaDepth, bitShift); } #endif #endif float getLinearScreenDepth(vec2 uv) { #ifdef GL2 return linearizeDepth(texture2D(uDepthMap, uv).r) * camera_params.y; #else return unpackFloat(texture2D(uDepthMap, uv)) * camera_params.y; #endif } float getLinearDepth(vec3 pos) { return -(matrix_view * vec4(pos, 1.0)).z; } float getLinearScreenDepth() { vec2 uv = gl_FragCoord.xy * uScreenSize.zw; return getLinearScreenDepth(uv); } 

سنقوم بتمرير معلومات تظليل عن الكاميرا في Water.js . الصق هذا إلى حيث يمكنك تمرير المتغيرات الموحدة الأخرى مثل uTime:

 if(!this.camera){ this.camera = this.app.root.findByName("Camera").camera; } var camera = this.camera; var n = camera.nearClip; var f = camera.farClip; var camera_params = [ 1/f, f, (1-f / n) / 2, (1 + f / n) / 2 ]; material.setParameter('camera_params', camera_params); 

أخيرًا ، نحتاج إلى موضع في العالم لكل بكسل لتظليل الأجزاء. علينا الحصول عليه من تظليل الرأس. لذلك ، سنحدد متغيرًا متغيرًا في Water.frag :

 varying vec3 WorldPosition; 

حدد نفس المتغير المتغير في Water.vert . ثم نعينه موضعًا مشوهًا من جهاز تظليل الرأس بحيث يبدو الرمز الكامل كما يلي:

 attribute vec3 aPosition; attribute vec2 aUv0; varying vec2 vUv0; varying vec3 WorldPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; uniform float uTime; void main(void) { vUv0 = aUv0; vec3 pos = aPosition; pos.y += cos(pos.z*5.0+uTime) * 0.1 * sin(pos.x * 5.0 + uTime); gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); WorldPosition = pos; } 

ندرك خدعة حقيقية


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

 float worldDepth = getLinearDepth(WorldPosition); float screenDepth = getLinearScreenDepth(); 

المهمة 4: لن تكون إحدى هذه القيم أكبر من الأخرى أبدًا (بافتراض deepTest = true). هل يمكنك تحديد أي واحد؟

نحن نعلم أن الرغوة ستكون حيث تكون المسافة بين القيمتين صغيرة. لذلك ، دعونا نجعل هذا الاختلاف لكل بكسل. قم بلصق هذا في نهاية التظليل (وقم بإيقاف البرنامج النصي لتصور العمق من القسم السابق):

 color = vec4(vec3(screenDepth - worldDepth),1.0); gl_FragColor = color; 

ويجب أن يبدو شيء مثل هذا:


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

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

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

 void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec2 pos = vUv0 * 2.0; pos.y += uTime * 0.02; vec4 WaterLines = texture2D(uSurfaceTexture,pos); color.rgba += WaterLines.r * 0.1; float worldDepth = getLinearDepth(WorldPosition); float screenDepth = getLinearScreenDepth(); float foamLine = clamp((screenDepth - worldDepth),0.0,1.0) ; if(foamLine < 0.7){ color.rgba += 0.2; } gl_FragColor = color; } 

لتلخيص


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

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

كود المصدر


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

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


All Articles