مقدمة إلى الـ Shaders

Starter pack

حان الوقت أخيرًا للغوص في عالم الـ shaders. فهي أساسية لإنشاء جميع أنواع التأثيرات البصرية. في هذا الفصل، سنتعلم عن الـ shaders، وما يمكننا تحقيقه من خلالها، وكيفية استخدامها في React Three Fiber.

المقدمة

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

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

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

آمل أنني لم أخيفك وأنك متحمس للتعلم عن الـ shaders. دعونا نبدأ!

ما هي الـ Shaders؟

الـ Shaders هي برامج صغيرة تُشغّل على وحدة معالجة الرسوميات (GPU). وُكتبت بلغة تسمى GLSL (وهي لغة التظليل الخاصة بـ OpenGL)، وهي مُشابهة لـ C.

تُستخدم لوضع مواضع قمة الـ mesh (Vertex Shader) وتلوين كل بكسل من الأسطح (Fragment Shader).

في الحقيقة، لقد كنا نستخدم الـ shaders طوال الوقت. عندما ننشئ مادة، فإننا نستخدم shader. على سبيل المثال، عندما ننشئ MeshBasicMaterial، فإننا نستخدم shader الذي يلون الـ mesh بلون واحد. عندما ننشئ MeshStandardMaterial، فإننا نستخدم shader الذي يحاكي الإضاءة والظلال والانعكاسات.

Vertex Shader

الـ vertex shader هو برنامج يُنفّذ لكل قمة من هيكل. مسؤوليته الرئيسية هي تحويل القيم من الفضاء الثلاثي الأبعاد (عالمنا الثلاثي الأبعاد) إلى الفضاء الثنائي الأبعاد (شاشتنا أو مشهد العرض). يحقق هذا التحويل باستخدام عدة مصفوفات:

  • مصفوفة العرض (View Matrix): تمثل هذه المصفوفة موقع واتجاه الكاميرا في المشهد. تحول القمم من الفضاء العالمي إلى فضاء الكاميرا.
  • مصفوفة الإسقاط (Projection Matrix): هذه المصفوفة، سواء كانت منظورية أو متوازنة، تحول القمم من فضاء الكاميرا إلى إحداثيات جهاز نموذجية (NDC)، لتحضيرها للإسقاط النهائي على الشاشة الثنائية الأبعاد.
  • مصفوفة النموذج (Model Matrix): تحتوي هذه المصفوفة على موقع ودوران وتحرير كل كائن فردي في المشهد. تحول القمم من فضاء الكائن إلى الفضاء العالمي.

بالإضافة إلى ذلك، يدمج الـ vertex shader أيضًا موضع القمة الأصلي وأي خصائص أخرى مرتبطة بها.

مخطط للـ vertex shader

سيتم تنفيذ الـ vertex shader لكل قمة من الهيكل.

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

المجزِّئ شيدر

المجزِّئ شيدر، المعروف أيضًا باسم بيكسل شيدر، هو برنامج يُنفذ لكل مجزئ (أو بيكسل) يتم توليده بواسطة عملية التوجيه. تتمثل مهمته الرئيسية في تحديد اللون النهائي لكل بيكسل على الشاشة.

Schema of the fragment shader

لكل مجزِّئ يُنتج أثناء التوجيه، سيتم تنفيذ المجزِّئ شيدر.

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

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

سنعود إلى السمات و المتغيرات الثابتة لاحقًا في هذا الدرس.

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

عندما يُكتمل حساب اللون، يخرج المجزِّئ شيدر اللون النهائي للمجزئ باستخدام المتغير المسبق التعريف gl_FragColor.

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

الشيدر الأول لك

لنبدأ الحزمة الأساسية. ينبغي أن ترى هذا الإطار مع سطح أسود في وسط الشاشة:

A frame with a black plane

افتح الملف ShaderPlane.jsx، يحتوي على شبكة بسيط مع سطح هندسي ومادة أساسية. سنستبدل هذه المادة بمادة شيدر مخصصة.

shaderMaterial

لإنشاء مادة شيدر، نستخدم وظيفة shaderMaterial من مكتبة Drei.

تتطلب 3 معلمات:

  • uniforms: كائن يحتوي على المتغيرات الثابتة المستخدمة في الشيدر. اتركه فارغًا الآن.
  • vertexShader: سلسلة تحتوي على كود GLSL لرأس الشيدر.
  • fragmentShader: سلسلة تحتوي على كود GLSL للمجزِّئ شيدر.

في بداية ملفنا، لنعرف مادة شيدر جديدة باسم MyShaderMaterial:

import { shaderMaterial } from "@react-three/drei";

const MyShaderMaterial = shaderMaterial(
  {},
  `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  `
  void main() {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
  `
);

سنغوص في كود الشيدر بعد قليل.

لاستخدامه بشكل وصفي مع React Three Fiber، نستخدم أسلوب extend:

import { extend } from "@react-three/fiber";
// ...

extend({ MyShaderMaterial });

الآن يمكننا استبدال <meshBasicMaterial> بمادة الشيدر الجديدة لدينا:

import { shaderMaterial } from "@react-three/drei";
import { extend } from "@react-three/fiber";

const MyShaderMaterial = shaderMaterial(
  {},
  `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  `
  void main() {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
  `
);

extend({ MyShaderMaterial });

export const ShaderPlane = ({ ...props }) => {
  return (
    <mesh {...props}>
      <planeGeometry args={[1, 1]} />
      <myShaderMaterial />
    </mesh>
  );
};

ينبغي أن ترى نفس السطح الأسود كما كان من قبل. لم نغير أي شيء بعد، لكننا الآن نستخدم مادة شيدر مخصصة.

للتحقق من أنها تعمل، دعنا نغير اللون الذي نعوده في المجزِّئ شيدر. استبدل سطر gl_FragColor بالتالي:

gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);

gl_FragColor هو متغير مُعرف مسبقًا يمثل لون المجزئ. إنه vec4 (متجه بـ 4 مكونات) يمثل قنوات الأحمر والأخضر والأزرق والفا للون. كل مكون هو float بين 0 و 1.

بتعيين المكون الأول إلى 1.0، نحن نحدد قناة الأحمر لأقصى قيمة لها، مما سينتج لونًا أحمر.

ينبغي أن ترى سطحًا أحمر في وسط الشاشة:

A frame with a red plane

تهانينا! لقد أنشأت للتو أول مادة شيدر لديك. إنها بسيطة، لكنها بداية.

كود Shader

قبل أن ننتقل إلى الأمام، دعونا نجهز بيئة تطويرنا لكتابة الـ shaders بشكل أكثر راحة.

لديك خياران لكتابة كود shader:

  • Inline: يمكنك كتابة كود shader مباشرة في ملف JavaScript.
  • External: يمكنك كتابة كود shader في ملف منفصل بامتداد .glsl واستيراده في ملف JavaScript الخاص بك.

أنا عادة أفضل النهج inline داخل ملفات المادة المناسبة، بهذه الطريقة يكون كود shader قريبًا من إعلان المادة.

لكن لجعل الأمر أسهل في الكتابة والقراءة، أوصي باستخدام محدد تركيب لغوي لـ GLSL. يمكنك استخدام امتداد Comment tagged templates لبرنامج Visual Studio Code. سيسلط الضوء على كود GLSL داخل سلسلة القوالب.

GLSL syntax highlighter

بمجرد التثبيت، لتمكين محدد التركيب اللغوي، تحتاج إلى إضافة التعليق التالي في بداية كود shader:

const MyShaderMaterial = shaderMaterial(
  {},
  /* glsl */ `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  /* glsl */ `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
  `
);

يجب أن تشاهد كود GLSL مظللًا داخل السلاسل القالبية:

GLSL syntax highlighter in action

الـ vertex shader في الأعلى الآن لديه تظليل تركيب صحيح وهو أسهل في القراءة.

هذا كل ما تحتاجه لكود shader الداخلي. لا يزال بإمكانك اختيار استخدام ملف خارجي إذا كنت تفضل الحفاظ على كود shader الخاص بك منفصل. دعونا نرى كيفية القيام بذلك.

استيراد ملفات GLSL

أولاً، قم بإنشاء مجلد جديد باسم shaders في مجلد src. داخل هذا المجلد، قم بإنشاء ملفين: myshader.vertex.glsl و myshader.fragment.glsl وقم بنسخ كود الشيدر المناسب في كل ملف.

myshader.vertex.glsl:

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

myshader.fragment.glsl:

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

يمكنك استخدام نظام تسمية الذي تفضله، وتجميع الشيدرات في مجلدات فرعية إذا كان لديك العديد منها.

ثم، لكي نتمكن من استيراد هذه الملفات في ملف JavaScript الخاص بنا، نحتاج إلى تثبيت إضافة vite-plugin-glsl كاعتمادية تطوير:

yarn add vite-plugin-glsl --dev

بعد ذلك، في ملف vite.config.js الخاص بك، قم باستيراد الإضافة وأضفها إلى مصفوفة plugins:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import glsl from "vite-plugin-glsl";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), glsl()],
});

الآن يمكنك استيراد ملفات GLSL في ملف JavaScript الخاص بك واستخدامها ككود شيدر:

import myShaderFragment from "./shaders/myshader.fragment.glsl";
import myShaderVertex from "./shaders/myshader.vertex.glsl";

const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);

الآن لدينا وسيلة مريحة لكتابة واستيراد كود الشيدر، يمكننا البدء في استكشاف الأجزاء المختلفة من كود الشيدر.

GLSL

يتم كتابة كود الـ shader بلغة GLSL (لغة تظليل OpenGL). إنها لغة مشابهة للـ C، دعونا نستعرض الأساسيات.

الأنواع

تحتوي GLSL على عدة أنواع، ولكن الأكثر شيوعًا هي:

  • bool: قيمة بوليانية (true أو false).
  • int: رقم صحيح.
  • float: رقم عشري.
  • vectors: مجموعة من الأرقام. vec2 هي مجموعة مكونة من رقمين عشريين (x و yvec3 هي مجموعة مكونة من 3 أرقام عشرية (x، y، و z)، وvec4 هي مجموعة مكونة من 4 أرقام عشرية (x، y، z، و w). بدلاً من استخدام x، y، z، و w، يمكنك أيضًا استخدام r، g، b، و a للألوان، وهي قابلة للتبادل.
  • matrices: مجموعة من الـ vectors. على سبيل المثال، mat2 هي مجموعة مكونة من 2 vector، mat3 هي مجموعة مكونة من 3 vectors، و mat4 هي مجموعة مكونة من 4 vectors.

Swizzling والتلاعب

يمكنك الوصول إلى مكونات الـ vector باستخدام swizzling. على سبيل المثال، يمكنك إنشاء vector جديد باستخدام مكونات vector آخر:

vec3 a = vec3(1.0, 2.0, 3.0);
vec2 b = a.xy;

في هذا المثال، سيكون b عبارة عن vector يحتوي على مكونات x و y لـ a.

يمكنك أيضًا استخدام swizzling لتغيير ترتيب المكونات:

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = a.zyx;

في هذا المثال، سيكون b مساويًا لـ vec3(3.0, 2.0, 1.0).

لإنشاء vector جديد بجميع المكونات المتطابقة، يمكنك استخدام الـ constructor:

vec3 a = vec3(1.0);

في هذا المثال، سيكون a مساويًا لـ vec3(1.0, 1.0, 1.0).

المشغلون

يحتوي GLSL على المشغلين الحسابيين الشائعين: +, -, *, /, +=, /=, *= والمشغلين المقارنة الشائعين: ==, !=, >, <, >=, <=.

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

int a = 1;
float b = 2.0;
float c = float(a) + b;

يمكنك أيضًا إجراء عمليات على المتجهات والمصفوفات:

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);
vec3 c = a + b;

وهو نفسه كما يلي:

vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);

الدوال

نقطة الدخول إلى شيفرات vertex و fragment هي الدالة main. وهي الدالة التي سيتم تنفيذها عندما يتم استدعاء الشيفرة.

void main() {
  // الكود الخاص بك هنا
}

void هو نوع الإرجاع للدالة. وهو يعني أن الدالة لا تعيد أي شيء.

يمكنك أيضًا تعريف دوالك الخاصة:

float add(float a, float b) {
  return a + b;
}

ثم يمكنك استدعاء هذه الدالة في دالة main:

void main() {
  float result = add(1.0, 2.0);
  // ...
}

يوفر GLSL العديد من الدوال المدمجة للعمليات الشائعة مثل sin, cos, max, min, abs, round, floor, ceil, والعديد من الدوال الأخرى المفيدة مثل mix, step, length, distance, وأكثر من ذلك.

سوف نكتشف الأساسيات منها ونتدرب عليها في الدرس التالي.

الحلقات والشرط

يدعم GLSL جمل for والجمل الشرطية if. تعمل بطريقة مشابهة لجافا سكريبت:

for (int i = 0; i < 10; i++) {
  // ضع الكود الخاص بك هنا
}

if (condition) {
  // ضع الكود الخاص بك هنا
} else {
  // ضع الكود الخاص بك هنا
}

التسجيل / التصحيح

نظرًا لأن برامج shader تعمل بالتوازي لكل vertex و fragment، لا يمكن استخدام console.log لتصحيح الكود الخاص بك ولا إضافة نقاط توقف. هذا ما يجعل من الصعب تصحيح shaders.

طريقة شائعة لتصحيح shaders هي استخدام gl_FragColor لتصور قيم المتغيرات الخاصة بك.

أخطاء الترجمة

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

دعنا نقم بإزالة قناة alpha من gl_FragColor ونرى ماذا يحدث:

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0);
}

يجب أن ترى خطأ الترجمة في وحدة التحكم:

خطأ الترجمة

موضحًا لنا أن gl_FragColor تتوقع 4 مكونات، ولكننا قدمنا فقط 3.

لا تنسَ استعادة قناة alpha إلى 1.0 لإزالة الخطأ.

Uniforms

لنقل البيانات من كود JavaScript إلى الشيدر نستخدم uniforms. تبقى هذه القيم ثابتة عبر جميع الرؤوس والشظايا.

projectionMatrix و modelViewMatrix و position هي أمثلة على uniforms مدمجة يتم تمريرها إلى الشيدر تلقائيًا.

لنقم بإنشاء uniform مخصص لتمرير لون إلى الشيدر. سنستخدمه لتلوين السطح (plane). سنسميه uColor. من الجيد أن نحضر اسم الـ uniform بحرف u للتوضيح في كودنا أنه uniform.

أولاً، لنقم بإعلانه في كائن الـ uniforms في shaderMaterial:

import { Color } from "three";
// ...
const MyShaderMaterial = shaderMaterial(
  {
    uColor: new Color("pink"),
  }
  // ...
);

// ...

ثم يمكننا استخدامه في الشيدر المقطعي (fragment shader):

uniform vec3 uColor;

void main() {
  gl_FragColor = vec4(uColor, 1.0);
}

يجب أن ترى السطح (plane) ملونًا باللون الوردي:

A frame with a pink plane

هنا يكون اللون الوردي هو القيمة الافتراضية للـ uniform. يمكننا تغييره مباشرة على الـ material:

<MyShaderMaterial uColor={"lightblue"} />

A frame with a light blue plane

السطح ملون الآن باللون الأزرق الفاتح.

يمكن لكل من الشيدرات النقطية والمقطعية (vertex and fragment shaders) الوصول للـ uniforms. لنقم بإضافة الوقت كـ uniform ثانٍ إلى الشيدر النقطي لتحريك السطح لأعلى ولأسفل:

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.