رسومات بيانية ثلاثية الأبعاد لـ WebGL

ثلاثي الأبعاد لتقويم WebGL

تعد هذه المشاركة استمرارًا لسلسلة من المشاركات حول WebGL. بدأ الأول بالأساسيات بينما كان السابق حوالي المصفوفات الثنائية الأبعاد حول المصفوفات الثنائية الأبعاد. إذا لم تكن قد قرأتها، يُرجى عرضها أولاً. في المنشور الأخير استعرضنا كيفية عمل المصفوفات الثنائية الأبعاد. فتحدثنا عن الترجمة والتدوير والتحجيم وحتى العرض من وحدات البكسل إلى مساحة المقطع، يمكن إجراء كل ذلك باستخدام مصفوفة واحدة وبعض العمليات الحسابية للمصفوفة السحرية. أما إنشاء المحتوى ثلاثي الأبعاد، فليس هناك سوى خطوة صغيرة. في الأمثلة السابقة ثنائية الأبعاد، كانت لدينا نقاط ثنائية الأبعاد (س، ص) ضربناها في مصفوفة 3×3. لعمل محتوى ثلاثي الأبعاد، نحتاج إلى نقاط ثلاثية الأبعاد (س، ص، ع) ومصفوفة 4×4. لنأخذ مثالنا الأخير ونغيره إلى ثلاثي الأبعاد. سنستخدم F مرة أخرى ولكن هذه المرة "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. والآن على الرغم من إنشاء رسومات ثلاثية الأبعاد، نريد أيضًا أن نكون قادرين على التدوير حول المحور س والمحور ص أيضًا. يمكنك أن ترى من خلال النظر إليها أنها متشابهة جدًا. إذا أردنا العمل عليها، فسوف ستراها تبسط تمامًا كما كان من قبل

تدوير Z

newX = x * c + y * s;
newY = x * -s + y * c;

تدوير Y


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,
];
}

تمامًا مثلما احتجنا إلى التحويل من البكسل إلى playspace مع x وy، فنحن نحتاج إلى z لكي نفعل الشيء نفسه. في هذه الحالة، سأصنع وحدات بكسل المسافة Z أيضًا. سأضع قيمة مماثلة للعمق width بحيث تكون المساحة من 0 إلى بكسل للعرض، ومن 0 إلى بكسل الارتفاع للارتفاع، لكن بالنسبة للعمق ستكون -depth / 2 to ++ / 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 مسطح مما يجعل من الصعب رؤية أي ثلاثي الأبعاد. لإصلاح ذلك، يمكننا توسيع الشكل الهندسي إلى ثلاثي الأبعاد. يتكون كل من F الحالي من 3 مستطيلات، لكل مثلثين. لجعله ثلاثي الأبعاد، سيتطلب ما مجموعه 16 مستطيلاً. هذا عدد قليل جدًا لسرده هنا. 16 مستطيلاً × 2 مثلث لكل مستطيل × 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);
}

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

مثلث متعرج.

لدى WebGL القدرة على رسم مثلثات للأمام أو الخلف فقط. يمكننا تشغيل هذه الميزة باستخدام

gl.enable(gl.CULL_FACE);

وهو ما نقوم به مرة واحدة فقط، في بداية برنامجنا. عند تفعيل هذه الميزة، يتم ضبط WebGL افتراضيًا على "اختيار" المثلّثات المواجهة للخلف. في هذه الحالة، تعد كلمة "مسدود" كلمة خيالية لـ "لا يتم رسم". وبقدر ما يتعلق الأمر بالتوافق مع WebGL، يعتمد ما إذا كان المثلث يسير في اتجاه عقارب الساعة أو عكس عقارب الساعة على رؤوس هذا المثلث في المقطع المقاطع. بمعنى آخر، يحدد WebGL ما إذا كان المثلث في الأمام أو الخلف بعد تطبيق الرياضيات على الرؤوس في أداة تظليل الرأس. هذا يعني على سبيل المثال أن المثلث في اتجاه عقارب الساعة الذي يتم قياسه بقياس -1 يصبح مثلثًا عكس عقارب الساعة أو مثلثًا في اتجاه عقارب الساعة يتم تدويره 180 درجة حول المحور س أو ص يصبح مثلثًا عكس عقارب الساعة. نظرًا لأنه تم إيقاف CULL_FACE، يمكننا رؤية كل من المثلثين في اتجاه عقارب الساعة(الأمام) والعكس في اتجاه عقارب الساعة(الخلف). الآن وبعد أن تم تشغيلها، فإن المثلث الأمامي يقلب حوله في أي وقت إما بسبب التحجيم أو التدوير أو لأي سبب من الأسباب، لن يرسمه WebGL. وهذا أمر جيد لأنه عند تدوير شيء ما بتنسيق ثلاثي الأبعاد، سترغب بشكل عام في تحديد أي مثلثات مواجهة لك لاعتبارها واجهة أمامية.

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

1,   2,   3,
40,  50,  60,
700, 800, 900,

فقط نقلب الرأسين الأخيرين لتقديمه.

1,   2,   3,
700, 800, 900,
40,  50,  60,

اقتربت من الإجابة، ولكن لا تزال هناك مشكلة أخرى. حتى مع توجيه جميع المثلثات في الاتجاه الصحيح ومع اختيار المثلثات المواجهة للخلف، لا يزال لدينا أماكن تُرسم فيها المثلثات التي ينبغي أن تكون في الخلف فوق المثلثات التي يجب أن تكون في الأمام. أدخِل "التخزين المؤقت للصفحات". المخزن المؤقت للعمق، والذي يُطلق عليه أحيانًا اسم المخزن المؤقت Z-Buffer، عبارة عن مستطيل بحجم 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.VERexAttribPointer بحجم 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، وهو ما نحتاجه لكي تعمل رياضيات المصفوفة.