مقدمة عن أدوات التظليل

مقدمة

لقد قدّمنا لك سابقًا مقدمة عن Three.js. إذا لم تكن قد قرأت هذه المقالة، ننصحك بذلك لأنّها الأساس الذي سنبني عليه أثناء هذه المقالة.

ما أريد فعله هو مناقشة تأثيرات التظليل. إنّ WebGL رائع، وكما سبق أن ذكرت، تؤدي Three.js (والمكتبات الأخرى) عملًا رائعًا في تبسيط الصعوبات. ولكن ستكون هناك أوقات تريد فيها تحقيق أثر معيّن، أو تريد التعمّق قليلاً في كيفية ظهور هذه المؤثرات المدهشة على شاشتك، ومن المؤكد أنّ تأثيرات التظليل ستكون جزءًا من هذه المعادلة. إذا كنت مثلي، قد تحتاج إلى الانتقال من العناصر الأساسية في البرنامج التعليمي الأخير إلى محتوى أكثر تعقيدًا. سأعمل على أساس أنّك تستخدم Three.js، لأنّه يُجري الكثير من العمل المتعلّق بالshader. سأوضّح في البداية أيضًا أنّني سأشرح في البداية سياق تأثيرات التظليل، وأنّ الجزء الأخير من هذا الدليل التعليمي هو الذي سننتقل فيه إلى مستوى أكثر تقدمًا. ويعود السبب في ذلك إلى أنّshaders غير مألوفة للوهلة الأولى وتتطلّب بعض الشرح.

1. Shaders لدينا

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

  1. برامج تظليل رؤوس المضلّعات
  2. أدوات تظليل الأجزاء

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

2. برامج Shaders لرؤوس المضلّعات

اختَر شكلًا أساسيًا عاديًا، مثل كرة. تتكوّن من رؤوس، أليس كذلك؟ يتم منح برنامج تشفير قمة كل واحدة من هذه النقاط بدورها ويمكنه التلاعب بها. يعتمد ما يفعله برنامج Shader للرؤوس فعليًا مع كل واحد منها على البرنامج، ولكن لديه مسؤولية واحدة: يجب أن يضبط في مرحلة ما عنصرًا يُسمى gl_Position، وهو متجه 4D معرّف بنقطة عائمة، وهو الموضع النهائي ل الرأس على الشاشة. هذه العملية مثيرة للاهتمام بحد ذاتها، لأنّنا نتحدث عن الحصول على موضع ثلاثي الأبعاد (نقطة مع x وy وz) على شاشة ثنائية الأبعاد أو projected عليها. لحسن الحظ، إذا كنا نستخدم أداة مثل Three.js، ستتوفّر لدينا طريقة مختصرة لضبط gl_Position بدون أن تصبح الأمور ثقيلة جدًا.

3- Shaders للعناصر

لدينا إذًا الجسم مع رؤوسه، وقد عرَّفناه على الشاشة ثنائية الأبعاد، ولكن ماذا عن الألوان التي نستخدمها؟ ماذا عن النسيج والإضاءة؟ وهذا هو بالضبط الغرض من برنامج تحويل الشرائح. تمامًا مثل برنامج تظليل رؤوس المضلّعات، لا يتضمّن برنامج تظليل الشرائح سوى مهمة واحدة مهمة: يجب ضبط المتغيّر gl_FragColor أو تجاهله، وهو متجه آخر من النوع 4D float، وهو اللون النهائي للشريحة. ما هو المقصود بالقطعة؟ فكِّر في ثلاثة رؤوس تشكل مثلثًا. يجب التمويه على كل بكسل ضمن هذا المثلث. والقطعة هي البيانات التي تقدّمها هذه الرؤوس الثلاثة بغرض رسم كل بكسل في هذا المثلث. ولهذا السبب، تتلقّى الأجزاء قيمًا متداخلة من رؤوسها المكونة. إذا كان أحد الرؤوس باللون الأحمر، وكان الرأس المجاور له باللون الأزرق، سنرى قيم الألوان تتغيّر من الأحمر إلى الأرجواني إلى الأزرق.

4. متغيّرات Shader

عند الحديث عن المتغيّرات، هناك ثلاثة تعريفات يمكنك إجراؤها: الملصقات، السمات والاختلافات. عندما سمعتُ عن هذه الأنواع الثلاثة لأول مرة، شعرتُ بالحيرة الشديدة لأنّها لا تتطابق مع أي نوع آخر سبق لي التعامل معه. في ما يلي كيفية التفكير في هذه الميزة:

  1. يتم إرسال العناصر الموحدة إلى كلّ منshaders vertex وshaders fragment، وتحتوي على قيم تظلّ كما هي على مستوى الإطار الكامل الذي يتم عرضه. يمكن أن يكون موضع المصباح مثالاً جيدًا على ذلك.

  2. السمات هي قيم تُطبَّق على رؤوس فردية. لا تتوفّر السمات إلا لمخطّط تظليل رؤوس المضلّعات. يمكن أن يكون هذا شيئًا مثل كلّ رأس له لون مميز. تتضمّن السمات علاقة مباشرة مع الرؤوس.

  3. المتغيّرات هي متغيّرات تمّت الإشارة إليها فيshader vertex الذي نريد مشاركته مع shader fragment. لإجراء ذلك، نحرص على تحديد متغيّر متغير من النوع والاسم نفسهما في كلّ من برنامج تحويل قمة الهرم وبرنامج تحويل القطعة. ومن الاستخدامات الكلاسيكية لهذا الإجراء هو متجه رأس النقطة لأنّه يمكن استخدامه في عمليات حساب الإضاءة.

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

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

5- Bonjourno World

في ما يلي مثال على برنامج "مرحبًا بك في عالم" برامج تظليل رؤوس المضلّعات:

/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}   

وإليك الإجراء نفسه لبرنامج تشفير أجزاء الصورة:

/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

ليس الأمر معقّدًا جدًا، أليس كذلك؟

في برنامج Shader الخاص بالرأس، يتم إرسال عنصرَين من عناصر الملابس من خلال Three.js. هذان الزيّان متطابقان ويمثّلان مصفوفات رباعية الأبعاد تُعرف باسم مصفوفة العرض والنموذج ومصفّفة الإسقاط. ليس عليك معرفة كيفية عمل هذه الأدوات بدقة، ولكن من الأفضل دائمًا معرفة كيفية عملها إذا أمكن. في الأساس، يشير ذلك إلى طريقة إسقاط موضع الرأس الثلاثي الأبعاد إلى الموضع النهائي ثنائي الأبعاد على الشاشة.

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

6- استخدام MeshShaderMaterial

حسنًا، لقد أعددنا مخطّط ألوان، ولكن كيف يمكننا استخدامه مع Three.js؟ لقد تبيّن أنّ الأمر سهل للغاية. يُرجى اتّباع الخطوات التالية:

/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader:   $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});

من هناك، ستُجمِّع Three.js مواد التشويش وتشغّلها مع إرفاقها بالشبكة التي تمنحها هذه المادة. لا يمكن أن يكون الأمر أبسط من ذلك حقًا. حسنًا، من المحتمل أن يكون الأمر كذلك، ولكننا نتحدث عن عرض ملفات ثلاثية الأبعاد في المتصفّح، لذا أعتقد أنّك تتوقّع قدرًا معيّنًا من التعقيد.

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

7- الخطوات التالية

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

8. ضوء مزيّف

لنعدّل عملية التلوين لكي لا يكون الكائن مسطحًا ولونه موحّدًا. يمكننا إلقاء نظرة على كيفية تعامل Three.js مع الإضاءة، ولكن أعتقد أنّك تدرك أنّها أكثر تعقيدًا مما نحتاجه الآن، لذا سنستخدم الإضاءة الزائفة. يجب أن تطّلع على تأثيرات التظليل الرائعة التي تشكّل جزءًا من Three.js، بالإضافة إلى التأثيرات من مشروع WebGL الرائع الذي أنشأه مؤخرًا "كريس ميلك" وGoogle، وهو روما. لنعود إلى ملفات التظليل. سنعدّل برنامج Vertex Shader لتقديم كلّ Vertex normal إلى برنامج Fragment Shader. ننفّذ ذلك من خلال مجموعة متنوعة من:

// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;

void main() {

// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}

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

// same name and type as VS
varying vec3 vNormal;

void main() {

// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
    
// ensure it's normalized
light = normalize(light);

// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));

// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

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

ما هي الخطوات التالية؟ حسنًا، من الأفضل محاولة تعديل بعض مواضع رؤوس المضلّعات.

9- السمات

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

لنبدأ بإضافة السمة إلى برنامج تظليل رؤوس المضلّعات:

attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

كيف يبدو؟

لا فرق كبير في الواقع. ويعود سبب ذلك إلى عدم إعداد السمة في MeshShaderMaterial، لذا يستخدمshader قيمة صفرية بدلاً من ذلك. يشبه ذلك العنصر النائب في الوقت الحالي. بعد لحظة، سنضيف السمة إلى MeshShaderMaterial في JavaScript، وستعمل Three.js على ربط المكوّنين معًا تلقائيًا.

يُرجى العلم أيضًا أنّني اضطررت إلى تحديد الموضع المعدَّل لمتغيّر vec3 جديد لأنّ السمة الأصلية، مثل جميع السمات، للقراءة فقط.

10. تعديل MeshShaderMaterial

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

var attributes = {
displacement: {
    type: 'f', // a float
    value: [] // an empty array
}
};

// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}

نرى الآن كرة مجمّعة، ولكن المثير هو أنّه تتم جميع عمليات النقل على وحدة معالجة الرسومات.

11. إضافة حركة إلى هذا المصاص

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

  1. قيمة ثابتة لتحريك مقدار الإزاحة التي يجب تطبيقها في كل لقطة يمكننا استخدام دالة الجيب أو دالة جيب التمام لذلك لأنّ قيمتهما تتراوح بين -1 و1.
  2. تكرار الصور المتحركة في JavaScript

سنضيف المادة الموحدة إلى كل من MeshShaderMaterial وVertex Shader. أولاً، Shader لرأس المضلع:

uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement *
                        amplitude);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

بعد ذلك، نعدّل MeshShaderMaterial:

// add a uniform for the amplitude
var uniforms = {
amplitude: {
    type: 'f', // a float
    value: 0
}
};

// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms:       uniforms,
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

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

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

var frame = 0;
function update() {

// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;

renderer.render(scene, camera);

// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);

12. الخاتمة

هذا كل ما في الأمر! يمكنك الآن رؤية أنّه يتحرك بطريقة غريبة (ومشوّشة قليلاً).

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