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

مقدمة

لقد أعطيتك سابقًا مقدمة عن Three.js. إذا لم تكن قد قرأت ذلك فقد ترغب في ذلك لأنه الأساس الذي سأبني عليه خلال هذه المقالة.

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

1. أداتا التظليل

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

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

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

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

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

3- Shaders للعناصر

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

4. متغيرات التظليل

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

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

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

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

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

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

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);
}

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

في برنامج تظليل رؤوس المضلّعات، يتم إرسال عنصرَين ثابتَين من خلال 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 لتوفير كل رأس عادي لـ 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. الخاتمة

هذا كل ما في الأمر! وأصبحت الآن تظهر لك وهي تتحرك بطريقة غريبة (ومبهرة بعض الشيء).

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