WebGL Orthographic 3D
هذه المشاركة هي استمرار لسلسلة من المشاركات حول WebGL. بدأ الدرس الأول بالأساسيات، وتناول الدرس السابق المصفوفات ثنائية الأبعاد المصفوفات ثنائية الأبعاد. إذا لم يسبق لك الاطّلاع على هذه المقالات، يُرجى قراءتها أولاً. في المشاركة الأخيرة، اطّلعنا على آلية عمل المصفوفات ثنائية الأبعاد. لقد تحدّثنا عن كيفية استخدام مصفوفة واحدة وبعض العمليات الحسابية السحرية في المصفوفات لتنفيذ عمليات الترجمة والدوران والتكبير وحتى الإسقاط من البكسل إلى مساحة المقطع. وبعد ذلك، يمكنك بسهولة إنشاء محتوى ثلاثي الأبعاد. في الأمثلة السابقة على الأشكال ثنائية الأبعاد، كانت لدينا نقاط ثنائية الأبعاد (x وy) ضاعفناها بصفيف 3×3. لإجراء التحويل إلى 3D، نحتاج إلى نقاط ثلاثية الأبعاد (x وy وz) ومصفّفة 4x4. لنأخذ مثالنا الأخير ونغيّره إلى نموذج ثلاثي الأبعاد. سنستخدم الحرف F مرة أخرى ولكن هذه المرة بتصميم ثلاثي الأبعاد. أول شيء علينا فعله هو تغيير برنامج تشفير قمة المثلث لمعالجة الأشكال الثلاثية الأبعاد. في ما يلي تأثيرات التشويش القديمة.
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>
في ما يلي الإصدار الجديد.
<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
uniform mat4 u_matrix;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>
أصبح الأمر أسهل. بعد ذلك، نحتاج إلى تقديم بيانات ثلاثية الأبعاد.
...
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
...
// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// left column
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// top rung
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// middle rung
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0]),
gl.STATIC_DRAW);
}
بعد ذلك، علينا تغيير جميع دوالّ المصفوفة من ثنائية الأبعاد إلى ثلاثية الأبعاد. في ما يلي الإصدارات ثنائية الأبعاد (السابقة) من makeTranslation وmakeRotation وmakeScale.
function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}
function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}
function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}
في ما يلي النُسخ الثلاثية الأبعاد المعدَّلة.
function makeTranslation(tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
];
}
function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};
function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};
function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
}
function makeScale(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1,
];
}
لاحظ أنّ لدينا الآن 3 دوالّ للّفة. لم نحتاج سوى إلى صورة واحدة ثنائية الأبعاد لأنّنا كنا ندير النموذج بشكل فعّال حول محور Z فقط. الآن، لكي نحصل على صورة ثلاثية الأبعاد، نريد أيضًا أن نتمكّن من التدوير حول محور x ومحور y أيضًا. يمكنك الاطّلاع على أنّها متشابهة جدًا. إذا كنا سنحلّها، ستلاحظ تبسيطها كما في السابق.
دوران محور Z
newX = x * c + y * s;
newY = x * -s + y * c;
دوران المحور ص
newX = x * c + z * s;
newZ = x * -s + z * c;
الدوران على محور X
newY = y * c + z * s;
newZ = y * -s + z * c;
نحتاج أيضًا إلى تعديل دالة الإسقاط. إليك الإصدار القديم.
function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}
تم تحويلها من البكسلات إلى مساحة المقطع في محاولتنا الأولى لتوسيع نطاق استخدامها إلى المحتوى الثلاثي الأبعاد، لنحاول
function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}
تمامًا كما احتجنا إلى التحويل من البكسل إلى مساحة المقطع لهما، يجب إجراء الشيء نفسه بالنسبة إلى
z. في هذه الحالة، أُنشئ وحدات بكسل
لمساحة Z أيضًا. سأُدخل قيمة مشابهة لـ width
للعمق
كي تكون المساحة لدينا من 0 إلى عرض بكسل، ومن 0 إلى ارتفاع بكسل، ولكن
بالنسبة إلى العمق، ستكون من -العمق / 2 إلى +العمق / 2.
أخيرًا، نحتاج إلى تعديل الرمز الذي يحسب المصفوفة.
// Compute the matrices
var projectionMatrix =
make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);
// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);
المشكلة الأولى التي نواجهها هي أنّ الشكل الهندسي هو F مسطّح، ما يجعله صعب الرؤية في أيّ شكل ثلاثي الأبعاد. لحلّ هذه المشكلة، لنوسّع الشكل الهندسي إلى 3D. يتكون الحرف F الحالي من 3 مستطيلات، وكل مستطيل يتكوّن من مثلثَين. لإنشاء شكل ثلاثي الأبعاد، ستحتاج إلى إجمالي 16 مستطيلاً. هناك عدد كبير من هذه العناصر. 16 مستطيلاً × مثلثان لكل مستطيل × 3 رؤوس لكل مثلث = 96 رأسًا. إذا أردت الاطّلاع على جميعها، يمكنك عرض المصدر في العيّنة. علينا رسم المزيد من الرؤوس حتى
// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
عند تحريك أشرطة التمرير، من الصعب جدًا معرفة أنّ الصورة ثلاثية الأبعاد. لنحاول تلوين كل مستطيل بلون مختلف. لتنفيذ ذلك، سنضيف سمة أخرى إلى برنامج تظليل رؤوس المضلّعات ومتغيّرًا لنقله من برنامج تظليل رؤوس المضلّعات إلى برنامج تظليل الأجزاء. في ما يلي برنامج تشفير قمة المثلث الجديد
<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
// Pass the color to the fragment shader.
v_color = a_color;
}
</script>
وعلينا استخدام هذا اللون في برنامج تحويل الشرائح
<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;
// Passed in from the vertex shader.
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
</script>
نحتاج إلى البحث عن الموقع الجغرافي لتقديم الألوان، ثم إعداد ملف تدخّل وسمة أخرى لمنح الألوان.
...
var colorLocation = gl.getAttribLocation(program, "a_color");
...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);
// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
// Set Colors.
setColors(gl);
...
// Fill the buffer with colors for the 'F'.
function setColors(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array([
// left column front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// top rung front
200, 70, 120,
200, 70, 120,
...
...
gl.STATIC_DRAW);
}
أوه، ما هذه المشكلة؟ تبيّن أنّه يتم رسم جميع الأجزاء المختلفة من الحرف "F" الثلاثي الأبعاد، أي الأمام والخلف والجوانب وما إلى ذلك، بالترتيب الذي تظهر به في الشكل الهندسي. لا يمنحنا ذلك النتائج المطلوبة تمامًا، لأنّه في بعض الأحيان يتم رسم العناصر في الخلف بعد العناصر في المقدّمة. تتضمّن المثلثات في WebGL مفهوم السطح الأمامي والخلفي. يشير المثلث الذي يواجه الأمام إلى أنّ رؤوسه تتجه في اتجاه عقارب الساعة. ينطبق ذلك على المثلث الذي يواجه المؤخرة، حيث تتجه رؤوسه في اتجاه عكس عقارب الساعة.
يمكن لـ WebGL رسم مثلثات موجّهة للأمام أو للخلف فقط. يمكننا تفعيل هذه الميزة باستخدام
gl.enable(gl.CULL_FACE);
ونحن نُجري ذلك مرة واحدة فقط في بداية البرنامج. عند تفعيل هذه الميزة، يتم ضبط WebGL تلقائيًا على "استبعاد" المثلثات التي تواجه الخلفية. "الاستبعاد" في هذه الحالة هو كلمة فصيحة تعني "عدم الرسم". يُرجى العِلم أنّه في ما يتعلّق بـ WebGL، يعتمد ما إذا كان المثلث يتحرك باتجاه عقارب الساعة أو عكسها على قمة المثلث في مساحة القصاصة. بعبارة أخرى، يحدِّد WebGL ما إذا كان المثلث أماميًا أو خلفيًا بعد تطبيق العمليات الحسابية على الرؤوس في برنامج تظليل رؤوس المضلّعات. وهذا يعني على سبيل المثال أنّه إذا تم تكبير مثلث في اتجاه عقارب الساعة في محور X بمقدار -1، يصبح مثلثًا في عكس اتجاه عقارب الساعة، أو إذا تم تدوير مثلث في اتجاه عقارب الساعة بزاوية 180 درجة حول محور X أو Y، يصبح مثلثًا في عكس اتجاه عقارب الساعة. بما أنّنا أوقفنا CULL_FACE، يمكننا رؤية المثلثات التي تدور في كلا الاتجاهين، أي بالترتيب التالي: باتجاه عقارب الساعة(الأمام) وبعكس عقارب الساعة(الخلف). بعد أن فعّلناه، لن يرسم WebGL المثلث الذي يواجهك في أي وقت يتم فيه قلبه إما بسبب التمدد أو الدوران أو لأي سبب آخر. وهذا أمر جيد، لأنّه عند تدوير جسم ثلاثي الأبعاد، تريد عادةً أن تكون المثلثات التي تواجهك هي المثلثات الأمامية.
مرحبًا! أين اختفت جميع المثلثات؟ تبيّن أنّ العديد منها موجَّه بطريقة خاطئة. يمكنك تدوير الجهاز وسيظهر لك النص عند النظر إلى الجانب الآخر. لحسن الحظ، يمكن حلّ هذه المشكلة بسهولة. ما عليك سوى الاطّلاع على المثلثات التي تواجه الاتجاه المعاكس وتبادل نقطتَين من نقاطها. على سبيل المثال، إذا كان أحد المثلثات المقلوبة
1, 2, 3,
40, 50, 60,
700, 800, 900,
ما عليك سوى قلب الرأسين الأخيرين للانتقال إلى الأمام.
1, 2, 3,
700, 800, 900,
40, 50, 60,
لقد اقتربنا من حلّ المشكلة، ولكن لا تزال هناك مشكلة أخرى. حتى مع توجيه كل المثلثات
في الاتجاه الصحيح وإزالة المثلثات التي تواجه المؤخرة،
لا يزال لدينا أماكن يتم فيها رسم المثلثات التي يجب أن تكون في الخلف
فوق المثلثات التي يجب أن تكون في الأمام.
أدخِل DEPTH BUFFER.
"ذاكرة التخزين المؤقت للعمق"، التي تُعرف أحيانًا باسم "ذاكرة التخزين المؤقت للمستوى Z"، هي مستطيل من depth
بكسل، بكسل واحد للعمق لكل بكسل لوني مستخدَم لإنشاء الصورة. عندما يرسم مكتبة WebGL كل بكسل من وحدات البكسل الملونة، يمكنها أيضًا رسم بكسل لتحديد العمق. ويتم ذلك
استنادًا إلى القيم التي نعرضها من برنامج تظليل رؤوس المضلّعات لـ Z. تمامًا كما اضطررنا
إلى التحويل إلى مساحة المقطع لـ X وY، يكون Z في مساحة المقطع أو (-1
إلى +1). ويتم بعد ذلك تحويل هذه القيمة إلى قيمة مساحة العمق (من 0 إلى +1).
قبل أن يرسم WebGL بكسلًا ملونًا، سيتحقّق من بكسل العمق المناظر له. إذا كانت قيمة العمق للبكسل الذي سيتم رسمه أكبر
من قيمة وحدات البكسل ذات العمق المماثل، لن يرسم WebGL
وحدة البكسل الجديدة ذات اللون. بخلاف ذلك، يرسم كلّ من بكسل اللون الجديد باستخدام
اللون من برنامج تشويش الشرائح ويرسم بكسل العمق باستخدام قيمة
العمق الجديدة. وهذا يعني أنّه لن يتم رسوم البكسلات التي تكون خلف البكسلات الأخرى.
يمكننا تفعيل هذه الميزة بسهولة تقريبًا مثلما فعّلنا ميزة إزالة المحتوى غير المرغوب فيه.
gl.enable(gl.DEPTH_TEST);
علينا أيضًا محو ذاكرة التخزين المؤقت للعمق إلى 1.0 قبل بدء الرسم.
// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...
في المشاركة التالية، سأشرح كيفية إضافة منظور إلى الصورة.
لماذا تكون السمة vec4 ولكن حجم gl.vertexAttribPointer هو 3
إذا كنت من الأشخاص الذين يهتمون بالتفاصيل، ربما لاحظت أنّنا حدّدنا السمتَين على النحو التالي:
attribute vec4 a_position;
attribute vec4 a_color;
وكلاهما من النوع "vec4"، ولكن عندما نطلب من WebGL كيفية إخراج البيانات من ذاكرات التخزين المؤقت، نستخدم
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
تشير القيمة "3" في كلّ منهما إلى استخراج 3 قيم فقط لكلّ سمة. ويعود سبب نجاح ذلك إلى أنّ WebGL يوفّر في برنامج معالجة قمة المثلث الإعدادات التلقائية لتلك التي لا تقدّمها. القيم التلقائية هي 0 و0 و0 و1 حيث x = 0 وy = 0 وz = 0 وw = 1. لهذا السبب، كان علينا في برنامجنا القديم لظلال رؤوس المضلّعات ثنائية الأبعاد تقديم القيمة 1 صراحةً. لقد أدخلنا x وy وكنا بحاجة إلى 1 لـ z، ولكن لأنّ القيمة التلقائية لـ z هي 0، كان علينا تقديم 1 صراحةً. بالنسبة إلى الأشكال الثلاثية الأبعاد، يتم ضبط القيمة التلقائية لسمة w على 1، حتى إذا لم نوفّر هذه السمة، لأنّها هي القيمة التي نحتاج إليها لكي تعمل العمليات الحسابية للمصفوفة.