في تعليمي
"إنشاء تظليل" ، نظرت بشكل رئيسي إلى تظليل الأجزاء ، وهي كافية لتنفيذ أي تأثيرات ثنائية الأبعاد وأمثلة على
ShaderToy . ولكن هناك فئة كاملة من التقنيات التي تتطلب استخدام أجهزة تظليل الرأس. في هذا البرنامج التعليمي ، سأتحدث عن إنشاء تظليل ماء الكرتون المنمق وأقدم لك تظليل الرأس. سأتحدث أيضًا عن المخزن المؤقت للعمق وكيفية استخدامه للحصول على مزيد من المعلومات حول المشهد وإنشاء خطوط من رغوة البحر.
إليك ما سيبدو عليه التأثير النهائي. يمكن رؤية عرض توضيحي تفاعلي
هنا .
يتكون هذا التأثير من العناصر التالية:
- شبكة شفافة من الماء مع مضلعات مقسمة وقمم تعويضات لخلق موجات.
- خطوط المياه الساكنة على السطح.
- محاكاة طفو القارب.
- خطوط رغوة ديناميكية حول حدود الأشياء في الماء.
- ما بعد المعالجة لخلق تشويه كل شيء تحت الماء.
في هذا التأثير ، أحب حقيقة أنه يتطرق إلى العديد من المفاهيم المختلفة لرسومات الكمبيوتر ، لذلك سيسمح لنا باستخدام الأفكار من الدروس السابقة ، بالإضافة إلى تطوير التقنيات التي يمكن تطبيقها في تأثيرات جديدة.
في هذا البرنامج التعليمي ،
سأستخدم PlayCanvas ، لأنه ببساطة بيئة ويب مجانية مناسبة ، ولكن يمكن تطبيق كل شيء على أي بيئة WebGL أخرى دون أي مشاكل. في نهاية المقال ، سيتم تقديم إصدار شفرة المصدر لـ Three.js. سنفترض أنك على دراية جيدة بالفعل بتظليل الأجزاء وواجهة PlayCanvas. يمكنك تحديث معرفتك حول التظليل
هنا ، والتعرف على PlayCanvas
هنا .
إعداد البيئة
الغرض من هذا القسم هو تكوين مشروع PlayCanvas وإدخاله في العديد من الكائنات البيئية التي ستؤثر عليها المياه.
إذا لم يكن لديك حساب PlayCanvas ،
فقم بتسجيله وإنشاء
مشروع فارغ جديد. بشكل افتراضي ، يجب أن يكون لديك شيئان في المشهد وكاميرا ومصدر للضوء.
إدراج نماذج
يعد Google
Poly أحد الموارد الرائعة للعثور على نماذج ثلاثية الأبعاد للويب. أخذت
نموذج القارب من هناك. بعد تنزيل الأرشيف
.obj
، ستجد ملفات
.obj
و.
.png
.
- اسحب كلا الملفين إلى نافذة الأصول لمشروع PlayCanvas.
- حدد المادة التي تم إنشاؤها تلقائيًا وحدد ملف
.png
كخريطة نشرها.
الآن يمكنك سحب
Tugboat.json إلى المشهد وحذف كائنات Box و Plane. إذا كان القارب يبدو صغيرًا جدًا ، يمكنك زيادة حجمه (قمت بتعيين القيمة إلى 50).
وبالمثل ، يمكنك إضافة أي نماذج أخرى إلى المشهد.
الكاميرا المدارية
لتكوين الكاميرا في مدار ، سنقوم بنسخ البرنامج النصي من
مثال PlayCanvas هذا . اتبع الرابط وانقر على
محرر لفتح المشروع.
- انسخ محتويات
mouse-input.js
و orbit-camera.js
من هذا المشروع التعليمي إلى ملفات تحمل نفس الأسماء من مشروعك. - أضف مكون Script إلى الكاميرا.
- إرفاق نصين للكاميرا.
تلميح: لتنظيم المشروع ، يمكنك إنشاء مجلدات في نافذة الأصول. أضع هذان البرنامجان للكاميرا في مجلد البرامج النصية / الكاميرا / ونموذجي في النماذج / والمواد الموجودة في المواد / المجلد.
الآن عندما تبدأ اللعبة (زر التشغيل في الجزء العلوي الأيمن من نافذة المشهد) ، يجب أن ترى قاربًا يمكنك فحصه بكاميرا عن طريق تحريكه في مدار باستخدام الماوس.
قسم مضلع سطح الماء
الغرض من هذا القسم هو إنشاء شبكة مقسمة يتم استخدامها كسطح للمياه.
لإنشاء سطح مائي ، نقوم بتكييف جزء من الشفرة من
البرنامج التعليمي لتوليد الإغاثة . قم
Water.js
برنامج نصي
Water.js
جديد. افتح هذا النص البرمجي للتحرير وإنشاء وظيفة
GeneratePlaneMesh
جديدة ستبدو كما يلي:
Water.prototype.GeneratePlaneMesh = function(options){
الآن يمكننا تسميتها في وظيفة
initialize
:
Water.prototype.initialize = function() { this.GeneratePlaneMesh({subdivisions:100, width:10, height:10}); };
الآن عندما تبدأ اللعبة يجب أن ترى سطحًا مستويًا فقط. لكن هذا ليس مجرد سطح مستوي ، بل هو شبكة تتكون من آلاف القمم. كتمرين ، حاول التحقق من ذلك بنفسك (هذا سبب جيد لدراسة الرمز الذي تم نسخه للتو).
المشكلة 1: قم بتحويل الإحداثيات Y لكل قمة بقيمة عشوائية بحيث يبدو المستوى بالشكل أدناه.
الأمواج
الغرض من هذا القسم هو تحديد سطح الماء للمواد الخاصة بك وإنشاء موجات متحركة.
للحصول على التأثيرات التي نحتاجها ، تحتاج إلى تكوين المواد الخاصة بك. تحتوي معظم المحركات ثلاثية الأبعاد على مجموعة من التظليل المحددة مسبقًا لعرض الكائنات وطريقة لإعادة تعريفها. إليك
رابط جيد حول كيفية القيام بذلك في PlayCanvas.
مرفق شادر
لنقم بإنشاء دالة
CreateWaterMaterial
جديدة
CreateWaterMaterial
مادة جديدة
CreateWaterMaterial
:
Water.prototype.CreateWaterMaterial = function(){
تأخذ هذه الوظيفة رمز تظليل الذروة والجزء من سمات البرنامج النصي. لذا دعنا
pc.createScript
في أعلى الملف (بعد سطر
pc.createScript
):
Water.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Water.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' });
الآن يمكننا إنشاء ملفات تظليل هذه وإرفاقها بنصنا. ارجع إلى المحرر وأنشئ ملفين تظليل:
Water.frag و
Water.vert . أرفق هذه التظليل إلى النص كما هو موضح في الشكل أدناه.
إذا لم يتم عرض السمات الجديدة في المحرر ، فانقر فوق زر
التحليل لتحديث البرنامج النصي.
الآن قم بلصق هذا التظليل الأساسي في
Water.frag :
void main(void) { vec4 color = vec4(0.0,0.0,1.0,0.5); gl_FragColor = color; }
وهذا موجود في
Water.vert :
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0); }
أخيرًا ،
عد إلى
Water.js لاستخدام المواد الجديدة بدلاً من المواد القياسية. أي بدلاً من:
var material = new pc.StandardMaterial();
أدخل:
var material = this.CreateWaterMaterial();
الآن ، بعد بدء اللعبة ، يجب أن تكون الطائرة زرقاء.
إعادة التشغيل الساخن
في الوقت الحالي ، قمنا فقط بإعداد فراغات تظليل لموادنا الجديدة. قبل أن أبدأ في كتابة تأثيرات حقيقية ، أريد إعداد إعادة تحميل التعليمات البرمجية تلقائيًا.
بعد إلغاء وظيفة
swap
في أي ملف نصي (على سبيل المثال ، في Water.js) ، سنقوم بتمكين إعادة التحميل السريع. سنرى لاحقًا كيفية استخدام هذا للحفاظ على الحالة حتى عند تحديث الرمز في الوقت الفعلي. لكن في الوقت الحالي ، نريد فقط إعادة تطبيق تظليل بعد إجراء التغييرات. قبل التشغيل في WebGL ، يتم تجميع التظليل ، لذلك نحتاج إلى إعادة إنشاء المواد الخاصة بنا.
سوف نتحقق مما إذا كانت محتويات كود التظليل قد تغيرت ، وإذا كان الأمر كذلك ، فقم بإنشاء المادة مرة أخرى. أولاً ، احفظ التظليل الحالي في
التهيئة :
وفي
التحديث نتحقق من حدوث أي تغييرات:
الآن ، للتأكد من أن هذا يعمل ، ابدأ اللعبة وقم بتغيير لون الطائرة في
Water.frag إلى اللون الأزرق أكثر متعة. بعد حفظ الملف ، يجب تحديثه حتى بدون إعادة تشغيل وإعادة تشغيل! هنا هو اللون الذي اخترته:
vec4 color = vec4(0.0,0.7,1.0,0.5);
Vertex Shaders
لإنشاء موجات ، يجب علينا تحريك كل قمة من شبكتنا في كل إطار. يبدو أنه سيكون غير فعال للغاية ، ولكن كل قمة في كل نموذج يتم تحويلها بالفعل في كل إطار معروض. هذا ما يفعله جهاز تظليل الرأس.
إذا رأينا وجود تظليل جزء كدالة يتم تنفيذها لكل بكسل ، ويحصل على موضعه ويعيد لونه ، فإن
تظليل الرأس هو وظيفة تعمل لكل بكسل ، ويحصل على موضعه ويعيد
موضعه .
افتراضيًا ، يحصل جهاز تظليل الرأس على
موضع في عالم الطراز ويعيد
موضعه على الشاشة . تم تعيين المشهد ثلاثي الأبعاد الخاص بنا بإحداثيات س وص وص ، ولكن الشاشة مستوية مسطحة ثنائية الأبعاد ، لذا فإننا نعرض عالمًا ثلاثي الأبعاد على شاشة ثنائية الأبعاد. تشارك مصفوفات النوع والإسقاط والنموذج في مثل هذا الإسقاط ، وبالتالي لن نأخذها في الاعتبار في هذا البرنامج التعليمي. ولكن إذا كنت ترغب في فهم ما يحدث بالضبط في كل مرحلة ، فإليك
دليل جيد جدًا .
أي هذا الخط:
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
يستقبل
aPosition
في العالم ثلاثي الأبعاد
aPosition
معينة ويحوله إلى
gl_Position
، أي إلى الموضع النهائي على الشاشة ثنائية الأبعاد. تشير البادئة "a" في aPosition إلى أن هذه القيمة هي
سمة . لا تنس أن
الزي المتغير هو قيمة يمكننا تحديدها في وحدة المعالجة المركزية ونقلها إلى التظليل. يحتفظ بنفس القيمة لجميع وحدات البكسل / القمم. من ناحية أخرى ، يتم الحصول على قيمة السمة من
صفيف وحدة المعالجة المركزية المحدد. يتم استدعاء جهاز تظليل الرأس لكل قيمة من صفيف السمة هذا.
يمكنك أن ترى أن هذه السمات قد تم تكوينها في تعريف التظليل الذي قمنا بتعيينه في Water.js:
var shaderDefinition = { attributes: { aPosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, }, vshader: vertexShader, fshader: fragmentShader };
تهتم PlayCanvas بإعداد ونقل مجموعة من مواضع قمة الرأس لـ
aPosition
عند اجتياز هذا التعداد ، ولكن في الحالة العامة ، يمكننا تمرير أي صفيف بيانات إلى shader vertex.
حركة قمة الرأس
افترض أننا نريد ضغط المستوى بأكمله بضرب جميع قيم
x
0.5. هل نحتاج إلى تغيير
aPosition
أو
gl_Position
؟
لنجرب
aPosition
أولاً. لا يمكننا تغيير السمة مباشرة ، ولكن يمكننا إنشاء نسخة:
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition; pos.x *= 0.5; gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); }
الآن يجب أن تبدو الطائرة أكثر مثل مستطيل. وليس هناك شيء غريب في ذلك. ولكن ماذا يحدث إذا حاولنا تغيير
gl_Position
؟
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition;
حتى تبدأ في تحريك الكاميرا ، قد تبدو متشابهة. نقوم بتغيير إحداثيات مساحة الشاشة ، أي أن الصورة ستعتمد على
كيفية النظر إليها .
حتى نتمكن من تحريك القمم ، وفي نفس الوقت من المهم التمييز بين العمل في العالم ومساحات الشاشة.
المهمة 2: هل يمكنك تحريك سطح الطائرة بأكمله عدة وحدات لأعلى (على طول المحور Y) في جهاز تظليل الرأس دون تشويه شكله؟
المهمة 3: قلت أن gl_Position ثنائي الأبعاد ، ولكن gl_Position.z موجود أيضًا. هل يمكنك التحقق لمعرفة ما إذا كانت هذه القيمة تؤثر على شيء ما ، وإذا كان الأمر كذلك ، فما الغرض منها؟
مضيفا الوقت
آخر شيء نحتاجه قبل البدء في إنشاء موجات متحركة هو متغير موحد يمكن استخدامه مع مرور الوقت. أعلنوا عن الزي الموحد في تظليل الرأس:
uniform float uTime;
الآن ، لتمريرها إلى Shader ،
دعنا نعود إلى
Water.js وتحديد متغير الوقت في التهيئة:
Water.prototype.initialize = function() { this.time = 0;
الآن ، لنقل المتغير إلى التظليل ، نستخدم
material.setParameter
. أولاً ، نقوم بتعيين القيمة الأولية في نهاية دالة
CreateWaterMaterial
:
الآن ، في وظيفة
update
، يمكننا إجراء زيادة في الوقت والوصول إلى المواد باستخدام الرابط الذي تم إنشاؤه لهذا:
this.time += 0.1; this.material.setParameter('uTime',this.time);
أخيرًا ، في وظيفة المبادلة ، نقوم بنسخ قيمة الوقت القديمة بحيث تستمر في الزيادة حتى بعد تغيير الرمز ، بدون إعادة الضبط إلى 0.
Water.prototype.swap = function(old) { this.time = old.time; };
الآن كل شيء جاهز. قم بتشغيل اللعبة للتأكد من عدم وجود أخطاء. الآن دعنا ننقل
Water.vert
باستخدام وظيفة الوقت في
Water.vert
:
pos.y += cos(uTime)
ويجب أن تبدأ طائرتنا في التحرك صعودا وهبوطا! نظرًا لأن لدينا الآن وظيفة تبديل ، يمكننا أيضًا تحديث Water.js دون الحاجة إلى إعادة التشغيل. للتأكد من أن هذا يعمل ، حاول تغيير زيادة الوقت.
المهمة 4: هل يمكنك تحريك القمم بحيث تبدو مثل الموجات في الشكل أدناه؟
دعني أخبرك أنني درست بالتفصيل موضوع الطرق المختلفة لخلق موجات
هنا . ترتبط المقالة ب 2D ، لكن الحسابات الرياضية تنطبق على حالتنا. إذا كنت تريد فقط رؤية الحل ،
فهنا هو الجوهر .
شفافية
الغرض من هذا القسم هو إنشاء سطح ماء شفاف.
قد تلاحظ أن اللون الذي تم إرجاعه إلى Water.frag له قيمة قناة ألفا 0.5 ، لكن السطح لا يزال معتمًا. في كثير من الحالات ، لا تزال الشفافية قضية غير محلولة في رسومات الكمبيوتر. طريقة منخفضة التكلفة لحلها هي استخدام الخلط.
عادة ، قبل رسم البكسل ، يتحقق من القيمة في
المخزن المؤقت للعمق ويقارنه بقيمة العمق الخاصة به (موضعه على المحور Z) لتحديد ما إذا كان سيتم إعادة رسم البكسل الحالي للشاشة أم لا. هذا ما يسمح لك بتقديم المشهد بشكل صحيح دون الحاجة إلى فرز الكائنات إلى الأمام.
عند المزج ، بدلاً من رفض البكسل أو الكتابة الفوقية ببساطة ، يمكننا دمج لون البكسل المعروض بالفعل (الهدف) مع البكسل الذي سنرسمه (المصدر). يمكن العثور على قائمة بجميع وظائف المزج المتوفرة في WebGL
هنا .
لكي تعمل قناة ألفا وفقًا لتوقعاتنا ، نريد أن يكون اللون المدمج للنتيجة مصدرًا مضروبًا في قناة ألفا بالإضافة إلى بكسل الهدف مضروبًا في ناقص ألفا واحد. بعبارة أخرى ، إذا كانت alpha = 0.4 ، فيجب أن يكون للون النهائي قيمة:
finalColor = source * 0.4 + destination * 0.6;
في PlayCanvas ، هذا هو جهاز
الكمبيوتر الذي يعمل .
لتمكينها ، ما
CreateWaterMaterial
سوى تعيين خاصية المواد داخل
CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
إذا بدأت اللعبة الآن ، فستصبح المياه شفافة! ومع ذلك ، فإنه لا يزال غير كامل. تنشأ المشكلة عندما يتم فرض السطح الشفاف على نفسه ، كما هو موضح أدناه.
يمكننا القضاء عليها باستخدام
ألفا للتغطية ، وهي تقنية متعددة العينات للشفافية ، بدلاً من المزج:
ولكنه متاح فقط في WebGL 2. في بقية البرنامج التعليمي ، من أجل البساطة ، سأستخدم الخلط.
لتلخيص
أنشأنا البيئة وخلقنا سطحًا شفافًا للماء بموجات متحركة من تظليل الرأس. في الجزء الثاني من البرنامج التعليمي ، سنأخذ في الاعتبار ازدهار الأشياء ، وإضافة خطوط إلى سطح الماء وإنشاء خطوط رغوية على طول حدود الأشياء المتقاطعة مع السطح.
في الجزء الثالث (الأخير) ، سننظر في تطبيق تأثير ما بعد المعالجة للتشوهات تحت الماء وننظر في أفكار لمزيد من التحسين.
كود المصدر
يمكن العثور على مشروع PlayCanvas النهائي
هنا . يحتوي
مستودعنا أيضًا على
منفذ مشروع تحت Three.js .