مقدمة في برمجة Shader للتخطيطات


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


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


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

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


الأفكار الرئيسية


ما هو تظليل؟


ما هو تظليل الشظايا؟ هذا في الأساس برنامج صغير. يتم تنفيذه لكل بكسل على anvas . إذا كان لدينا canvas بحجم 1000 × 500 بكسل ، فسيتم تنفيذ هذا البرنامج 500000 مرة ، في كل مرة يتلقى فيها معلمات الإدخال إحداثيات البكسل التي يتم تشغيلها حاليًا لها. كل هذا يحدث على GPU في مجموعة متنوعة من الخيوط المتوازية. على المعالج المركزي ، ستستغرق هذه الحسابات وقتًا أطول.


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


علاوة على ذلك ، في سياق مهمتنا ، يحدث ما يلي:


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

هذا وصف مبسط للغاية ، ولكن يجب أن يكون واضحًا من يفعل ماذا.


قليلا عن النحو


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


جناح مع صور





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


الأنواع الرئيسية للمتغيرات في GLSL متواضعة إلى حد ما - void ، bool ، int ، float ... إذا كنت تعمل مع أي لغة تشبه لغة C ، فقد رأيتها بالفعل. هناك أنواع أخرى ، ولا سيما ناقلات ذات أبعاد مختلفة - vec2 ، vec3 ، vec4 . سنستخدمها باستمرار للإحداثيات والألوان. المتغيرات التي يمكننا إنشاؤها هي من ثلاثة تعديلات مهمة:


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

من المفيد إضافة بادئة u / a / v إلى جميع المتغيرات في التظليل لتسهيل فهم البيانات التي جاءت منها.

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


قالب بدء الطهي


لنبدأ بـ JS. كما يحدث عادة عند العمل مع canvas ، نحن بحاجة إليها والسياق. حتى لا يتم تحميل نموذج التعليمات البرمجية ، سنقوم بعمل متغيرات عامة:


 const CANVAS = document.getElementById(IDs.canvas); const GL = canvas.getContext('webgl'); 

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


 function createProgram() { const shaders = getShaders(); PROGRAM = GL.createProgram(); GL.attachShader(PROGRAM, shaders.vertex); GL.attachShader(PROGRAM, shaders.fragment); GL.linkProgram(PROGRAM); GL.useProgram(PROGRAM); } 

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


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


 const vertexPositionAttribute = GL.getAttribLocation(PROGRAM, 'a_position'); 

بعد ذلك ، نشير إلى أن مصفوفة بإحداثيات سيتم تمريرها من خلال هذا المتغير (في التظليل نفسه ، سوف ندركه بالفعل كمتجه). سوف يحدد WebGL بشكل مستقل إحداثيات النقاط التي يجب تمريرها إلى أي مكالمة تظليل. نقوم فقط بتعيين المعلمات لصفيف المتجه الذي سيتم نقله: البعد - 2 (سنرسل الإحداثيات (x,y) ) ، ويتكون من أرقام ولا يتم تطبيعه. المعلمات الأخيرة ليست مثيرة للاهتمام بالنسبة لنا ، نترك الأصفار بشكل افتراضي.


 GL.enableVertexAttribArray(vertexPositionAttribute); GL.vertexAttribPointer(vertexPositionAttribute, 2, GL.FLOAT, false, 0, 0); 

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


 function createPlane() { GL.bindBuffer(GL.ARRAY_BUFFER, GL.createBuffer()); GL.bufferData( GL.ARRAY_BUFFER, new Float32Array([ -1, -1, -1, 1, 1, -1, 1, 1 ]), GL.STATIC_DRAW ); } 

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


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


 function getShaders() { return { vertex: compileShader( GL.VERTEX_SHADER, document.getElementById(IDs.shaders.vertex).textContent ), fragment: compileShader( GL.FRAGMENT_SHADER, document.getElementById(IDs.shaders.fragment).textContent ) }; } function compileShader(type, source) { const shader = GL.createShader(type); GL.shaderSource(shader, source); GL.compileShader(shader); return shader; } 

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


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


 console.log(GL.getShaderInfoLog(shader)); 

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

دعنا ننتقل إلى تظليل أنفسهم


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


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

أولاً ، تظليل الرأس. سيتم نقل متجه ثنائي الأبعاد بإحداثيات (x,y) إلى متغير سمة a_position ، كما قلنا. يجب أن يعيد التظليل متجهًا من أربع قيم (x,y,z,w) . لن تحرك أي شيء في الفضاء ، لذلك على المحور z ، فإننا ببساطة نخرج كل شيء ونضع قيمة w على الوحدة القياسية. إذا كنت تتساءل عن سبب وجود أربعة إحداثيات بدلاً من ثلاثة إحداثيات ، فيمكنك استخدام بحث الشبكة عن "إحداثيات موحدة".


 <script id='vertex-shader' type='x-shader/x-vertex'> precision mediump float; attribute vec2 a_position; void main() { gl_Position = vec4(position, 0, 1); } </script> 

يتم تسجيل نتيجة العمل في gl_Position متغير خاص. لا يحتوي Shaders على return بالمعنى الكامل للكلمة ، فهم يكتبون جميع نتائج عملهم في متغيرات محفوظة خصيصًا لهذه الأغراض.


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

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


 <script id='fragment-shader' type='x-shader/x-fragment'> precision mediump float; #define GOLD vec4(1.0, 0.86, 0.6, 1.0) void main() { gl_FragColor = GOLD; } </script> 

يمكنك الانتباه إلى الأرقام التي تصف اللون. هذا أمر مألوف لجميع المطبوعات من نوع RGBA ، فقط. القيم ليست أعدادًا صحيحة من 0 إلى 255 ، ولكنها كسرية من 0 إلى 1. الترتيب هو نفسه.


لا تنس استخدام المعالج المسبق لجميع الثوابت السحرية في المشاريع الحقيقية - وهذا يجعل الشفرة أكثر قابلية للفهم دون التأثير على الأداء (يحدث الاستبدال ، كما هو الحال في C ، أثناء التجميع).

تجدر الإشارة إلى نقطة أخرى حول المعالج الأولي:


إن استخدام الشيكات المستمرة #ifdef GL_ES في دروس مختلفة يخلو من المعنى العملي. في متصفحنا اليوم ، لا توجد خيارات GL أخرى موجودة ببساطة.

ولكن حان الوقت لإلقاء نظرة على النتيجة بالفعل:



يشير المربع الذهبي إلى أن التظليل يعمل كما هو متوقع. من المنطقي اللعب معهم قليلاً قبل الانتقال إلى العمل مع الصور.


التدرج وتحويل المتجهات


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


 void main() { gl_FragColor = vec4(gl_FragCoord.zxy / 500.0, 1.0); } 

في هذا المثال ، نستخدم إحداثيات البكسل الحالي كلون. غالبًا ما سترى هذا في أمثلة على الشبكة. كلاهما نواقل. لذلك لا أحد يزعج خلط كل شيء في كومة. يجب أن يهاجم المبشرون في TypeScript هجومًا هنا. نقطة مهمة هي كيف نحصل على جزء فقط من الإحداثيات من المتجه. الخصائص .x ، .z ، .xy ، .zy ، .xyz ، .zyx ، .xyzw ، .xyzw ، إلخ. في تسلسلات مختلفة تسمح لك بسحب عناصر المتجه في ترتيب معين في شكل ناقل آخر. تنفيذ مريح للغاية. أيضًا ، يمكن صنع متجه البعد الأعلى من متجه البعد السفلي عن طريق إضافة القيم المفقودة ، كما فعلنا.


حدد دائمًا صراحة الجزء الكسري من الأرقام. لا يوجد تحويل تلقائي int>> تعويم هنا.


الزي الرسمي ومرور الزمن


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


 GL.getUniformLocation(PROGRAM, 'u_time') 

ثم يمكننا ضبطها قبل كل إطار. كما هو الحال مع المتجهات ، هناك العديد من الطرق المتشابهة هنا ، بدءًا من كلمة uniform ، ثم يأتي بعد المتغير (1 للأرقام ، 2 ، 3 أو 4 للمتجهات) والنوع (f - float، i - int، v - vector) .


 function draw(timeStamp) { GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_time'), timeStamp / 1000.0); GL.drawArrays(GL.TRIANGLE_STRIP, 0, 4); window.requestAnimationFrame(draw); } 

في الواقع ، نحن لا نحتاج دائمًا إلى 60 إطارًا في الثانية في الواجهات. من الممكن تمامًا إضافة تباطؤ إلى RequestAnimationFrame وتقليل تكرار إعادة رسم الإطارات.

على سبيل المثال ، سنقوم بتغيير لون التعبئة. في التظليل ، تتوفر جميع الوظائف الرياضية الأساسية - sin ، cos ، tan ، acos ، acos ، acos ، pow ، exp ، log ، sqrt ، abs وغيرها. سنستخدم اثنين منهم.


 uniform float u_time; void main() { gl_FragColor = vec4( abs(sin(u_time)), abs(sin(u_time * 3.0)), abs(sin(u_time * 5.0)), 1.0); } 

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



لكن ما يكفي من الأمثلة المجردة ، دعنا ننتقل إلى استخدام الصور.


تحميل صورة في نسيج


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


 function createTexture() { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => { // .... }; image.src = 'example.jpg'; } 

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


 const texture = GL.createTexture(); GL.activeTexture(GL.TEXTURE0); GL.bindTexture(GL.TEXTURE_2D, texture); 

يبقى لإضافة صورة. نقول أيضًا على الفور أنه يجب قلبه على طول المحور ص ، لأنه في WebGL ، يكون المحور مقلوبًا:


 GL.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGB, GL.RGB, GL.UNSIGNED_BYTE, image); 

من الناحية النظرية ، يجب أن يكون النسيج مربعًا. بتعبير أدق ، يجب أن يكون لها حجم يساوي قوة اثنين - 32 بكسل ، 64 بكسل ، 128 بكسل ، إلخ. لكننا نفهم جميعًا أنه لن يقوم أحد بمعالجة الصور وستكون بنسب مختلفة في كل مرة. سيؤدي هذا إلى حدوث أخطاء حتى إذا كانت canvas تتناسب تمامًا مع النسيج. لذلك ، نملأ المساحة الكاملة لحواف المستوى بالبكسل المتطرف للصورة. هذه ممارسة قياسية ، على الرغم من أنها تبدو عكازًا قليلاً.


 GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); 

يبقى نقل الملمس إلى التظليل. هذه البيانات مشتركة بين الجميع ، لذلك نستخدم المُعدِّل uniform .


 GL.uniform1i(GL.getUniformLocation(PROGRAM, 'u_texture'), 0); 

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


 GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_canvas_size'), Math.max(CANVAS.height, CANVAS.width)); 

وتقسم الإحداثيات إليها:


 uniform sampler2D u_texture; uniform float u_canvas_size; void main() { gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size); } 


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


تأثيرات


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


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

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


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y)); 

والنتيجة غريبة. من الواضح أن كل شيء يتحرك بسعة كبيرة جدًا. اقسم كل شيء على رقم:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y) / 250.0); 

أفضل بالفعل. الآن من الواضح أننا حصلنا على القليل من الإثارة. نظريًا ، من أجل زيادة كل موجة ، نحتاج إلى تقسيم حجة الجيب - الإحداثيات. لنقم بذلك:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y / 30.0) / 250.0); 


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


مرآة ملتوية ، دراجات وتجارب


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


, " ", . ?


, , ? . , rand() - . , , , , . . . , . . . -, . . , , , . , "":


 float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } 

, , , NVIDIA ATI . , .


, , :


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy) / 100.0); 

:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy + vec2(sin(u_time))) / 250.0); 

, , :



, . , , . — . كيف تفعل ذلك؟ . .


0 1, - . 5 — . , .


 vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; gl_FragColor = texture2D(u_texture, texture_coord + rand(floor(texture_coord * 5.0) + vec2(sin(u_time))) / 100.0); 

, - . - . , , . ?


, , , - . , . , .. -. , . . , , . .


sin cos , . . .


 gl_FragColor = texture2D(u_texture, texture_coord + vec2( noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0)); 

. fract . 1 1 — :


 float noise(vec2 position) { vec2 block_position = floor(position); float top_left_value = rand(block_position); float top_right_value = rand(block_position + vec2(1.0, 0.0)); float bottom_left_value = rand(block_position + vec2(0.0, 1.0)); float bottom_right_value = rand(block_position + vec2(1.0, 1.0)); vec2 computed_value = fract(position); // ... } 

. WebGL smoothstep , :


 vec2 computed_value = smoothstep(0.0, 1.0, fract(position)); 

, . , X :


 return computed_value.x; 


… , , ...


- , , ... .

y — , . ?


 return length(computed_value); 


.


. 0.5 — .


 return mix(top_left_value, top_right_value, computed_value.x) + (bottom_left_value - top_left_value) * computed_value.y * (1.0 - computed_value.x) + (bottom_right_value - top_right_value) * computed_value.x * computed_value.y - 0.5; 

:



, , , .



, , . - .


uniform-, . 0 1, 0 — , 1 — .


 uniform float u_intensity; 

:


 gl_FragColor = texture2D(u_texture, texture_coord + vec2(noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0) * u_intensity); 

, .



( 0 1), .


, , , . — requestAnimationFrame. , FPS.


, . uniform-.


 document.addEventListener('mousemove', (e) => { let rect = CANVAS.getBoundingClientRect(); MOUSE_POSITION = [ e.clientX - rect.left, rect.height - (e.clientY - rect.top) ]; GL.uniform2fv(GL.getUniformLocation(PROGRAM, 'u_mouse_position'), MOUSE_POSITION); }); 

, . — , .


 void main() { vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; vec2 direction = u_mouse_position / u_canvas_size - texture_coord; float dist = distance(gl_FragCoord.xy, u_mouse_position) / u_canvas_size; if (dist < 0.4) { gl_FragColor = texture2D(u_texture, texture_coord + u_intensity * direction * dist * 1.2 ); } else { gl_FragColor = texture2D(u_texture, texture_coord); } } 

- . .


. , .



. Glitch- , SVG. . — . ? — , , , .


 float random_value = rand(vec2(texture_coord.y, u_time)); if (random_value < 0.05) { gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 5.0, texture_coord.y)); } else { gl_FragColor = texture2D(u_texture, texture_coord); } 

" ?" — , . .

. — , .


 float random_value = rand(vec2(floor(texture_coord.y * 20.0), u_time)); 

. , :


 gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 4.0, texture_coord.y)) + vec4(vec3(random_value), 1.0); 

. — . , — .r , .g , .b , .rg , .rb , .rgb , .bgr , ... .


:


 float random_value = u_intensity * rand(vec2(floor(texture_coord.y * 20.0), u_time)); 


ما هي النتيجة؟


, , . , , — .

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


All Articles