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

مقدمة

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

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

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

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

  1. أدوات تظليل Vertex
  2. أدوات تظليل الأجزاء

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

2. مظلات Vertex

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

3- أدوات تظليل الأجزاء

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

4. متغيرات أداة التظليل

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

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

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

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

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

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

5- عالم بونجورنو

إذًا، مرحبًا بعالم أدوات تظليل الرأس:

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

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

// 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، لذلك يستخدم التظليل قيمة صفرية بشكل فعال بدلاً من ذلك. إنه نوعًا ما مثل العنصر النائب في الوقت الحالي. وبعد ذلك، سنضيف السمة إلى 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. أولاً، Vertex 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()
});

اكتملت أدوات التظليل في الوقت الحالي. ولكن صحيح، يبدو أننا تراجعنا خطوة إلى الوراء. والسبب الرئيسي في ذلك هو أنّ قيمة السعة تساوي 0، وبما أنّنا نضرب هذه القيمة في الإزاحة، لم نلاحظ أي تغيير. كما أننا لم نقم بإعداد حلقة الرسوم المتحركة، لذا لا نرى أبدًا أن 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. الخلاصة

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

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