ภาพ 3 มิติของ WebGL

3 มิติแบบออร์โธกราฟิกของ WebGL

โพสต์นี้เป็นส่วนหนึ่งของชุดโพสต์เกี่ยวกับ WebGL บทแรกเริ่มต้นด้วยพื้นฐาน และบทก่อนหน้านั้นเกี่ยวกับเมทริกซ์ 2 มิติเกี่ยวกับเมทริกซ์ 2 มิติ หากคุณยังไม่ได้อ่าน โปรดอ่านก่อน ในโพสต์ที่แล้วเราได้พูดถึงวิธีการทํางานของเมทริกซ์ 2 มิติ เราได้พูดถึงการแปล การหมุน การปรับขนาด และแม้แต่การฉายจากพิกเซลไปยังพื้นที่คลิป ซึ่งทั้งหมดนี้ทำได้ด้วยเมทริกซ์ 1 รายการและคณิตศาสตร์เมทริกซ์อันน่าอัศจรรย์ การทำภาพ 3 มิตินั้นง่ายนิดเดียว ในตัวอย่าง 2 มิติก่อนหน้านี้ เรามีจุด 2 มิติ (x, y) ที่คูณด้วยเมทริกซ์ 3x3 หากต้องการทำ 3 มิติ เราต้องใช้จุด 3 มิติ (x, y, z) และเมทริกซ์ 4x4 มาเปลี่ยนตัวอย่างสุดท้ายเป็นโมเดล 3 มิติกัน เราจะใช้ F อีกครั้ง แต่ครั้งนี้เป็น "F" แบบ 3 มิติ สิ่งแรกที่เราต้องทําคือเปลี่ยนเวิร์กเทกซ์ Shader เพื่อจัดการกับ 3 มิติ นี่คือชิเดอร์แบบเก่า

<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>

การดำเนินการนี้ง่ายขึ้นกว่าเดิม จากนั้นเราต้องให้ข้อมูล 3 มิติ

...

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

ถัดไป เราต้องเปลี่ยนฟังก์ชันเมทริกซ์ทั้งหมดจาก 2 มิติเป็น 3 มิติ นี่คือ makeTranslation, makeRotation และ makeScale เวอร์ชัน 2 มิติ (เวอร์ชันก่อน)

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

และนี่คือเวอร์ชัน 3 มิติที่อัปเดตแล้ว

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 รายการ เราต้องการเพียงภาพเดียวในแบบ 2 มิติเนื่องจากเราหมุนรอบแกน Z เท่านั้น แต่ตอนนี้หากต้องการทำโมเดล 3 มิติ เราก็ต้องหมุนรอบแกน x และแกน y ด้วย คุณจะเห็นได้ว่าวิดีโอทั้งหมดมีความคล้ายคลึงกันมาก หากเราแก้ปัญหาได้ คุณจะเห็นการแก้ปัญหานั้นง่ายขึ้นเหมือนที่เคยเป็น

การหมุนตามแกน 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
];
}

ซึ่งแปลงจากพิกเซลเป็นพื้นที่คลิป ในการขยายการให้บริการเป็น 3 มิติครั้งแรกนี้

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

เช่นเดียวกับที่เราต้องแปลงจากพิกเซลเป็นพื้นที่คลิปสำหรับ x และ y เราต้องทำสิ่งเดียวกันสำหรับ 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 แบบแบน ซึ่งทำให้มองเห็นแบบ 3 มิติได้ยาก วิธีแก้ไขคือให้ขยายเรขาคณิตเป็น 3 มิติ F เวอร์ชันปัจจุบันของเราประกอบด้วยสี่เหลี่ยมผืนผ้า 3 รูป แต่ละรูปมีสามเหลี่ยม 2 รูป หากต้องการทําเป็น 3 มิติ ต้องใช้สี่เหลี่ยมผืนผ้าทั้งหมด 16 รูป มีหลายรายการที่ควรระบุ สี่เหลี่ยมผืนผ้า 16 รูป x สามเหลี่ยม 2 รูปต่อสี่เหลี่ยมผืนผ้า x จุดยอด 3 จุดต่อสามเหลี่ยม เท่ากับ 96 จุดยอด หากต้องการดูทั้งหมด ให้ดูที่แหล่งที่มาของตัวอย่าง เราต้องวาดจุดยอดเพิ่มเติมเพื่อให้

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

เมื่อเลื่อนแถบเลื่อน แทบจะแยกไม่ออกว่าเป็นภาพ 3 มิติ ลองระบายสีสี่เหลี่ยมผืนผ้าแต่ละรูปเป็นสีที่ต่างกัน โดยเราจะเพิ่มแอตทริบิวต์อื่นลงในเวิร์กเชดเวอร์เทกซ์และตัวแปรเพื่อส่งจากเวิร์กเชดเวอร์เทกซ์ไปยังเวิร์กเชดแฟรกเมนต์ นี่คือเวิร์กเทกซ์ Shader ใหม่

<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" 3 มิตินั้น ไม่ว่าจะเป็นด้านหน้า ด้านหลัง ด้านข้าง ฯลฯ จะวาดตามลำดับที่ปรากฏในเรขาคณิตของเรา แต่วิธีนี้ไม่ได้ให้ผลลัพธ์ที่ต้องการเนื่องจากบางครั้งวัตถุที่อยู่ด้านหลังจะวาดหลังวัตถุที่อยู่ด้านหน้า สามเหลี่ยมใน WebGL มีแนวคิดของด้านหน้าและด้านหลัง รูปสามเหลี่ยมที่หันหน้าไปทางด้านหน้าจะมีจุดยอดเรียงตามเข็มนาฬิกา รูปสามเหลี่ยมที่หันหลังจะมีจุดยอดทวนเข็มนาฬิกา

การเดินสายแบบสามเหลี่ยม

WebGL สามารถวาดได้เฉพาะสามเหลี่ยมที่หันหน้าไปข้างหน้าหรือหันหลัง เราเปิดฟีเจอร์ดังกล่าวได้โดยใช้

gl.enable(gl.CULL_FACE);

ซึ่งเราดำเนินการเพียงครั้งเดียวในตอนต้นของโปรแกรม เมื่อเปิดใช้ฟีเจอร์นี้ WebGL จะ "ตัด" รูปสามเหลี่ยมที่หันหลังไว้โดยค่าเริ่มต้น "การตัด" ในกรณีนี้คือคำศัพท์ที่หมายถึง "ไม่วาด" โปรดทราบว่าสำหรับ WebGL การพิจารณาว่ารูปสามเหลี่ยมหมุนตามเข็มนาฬิกาหรือทวนเข็มนาฬิกานั้นขึ้นอยู่กับจุดยอดของรูปสามเหลี่ยมนั้นในคลิปเพลซ กล่าวคือ WebGL จะพิจารณาว่ารูปสามเหลี่ยมอยู่ด้านหน้าหรือด้านหลังหลังจากที่คุณใช้คณิตศาสตร์กับจุดยอดในเวิร์กเชดจุดยอด ซึ่งหมายความว่าสามเหลี่ยมตามเข็มนาฬิกาที่ปรับขนาดใน X เป็น -1 จะกลายเป็นสามเหลี่ยมทวนเข็มนาฬิกา หรือสามเหลี่ยมตามเข็มนาฬิกาที่หมุน 180 องศารอบแกน X หรือ Y จะกลายเป็นสามเหลี่ยมทวนเข็มนาฬิกา เนื่องจากเราได้ปิดใช้ CULL_FACE เราจึงเห็นทั้งสามเหลี่ยมตามเข็มนาฬิกา(ด้านหน้า) และทวนเข็มนาฬิกา(ด้านหลัง) เมื่อเปิดแล้ว WebGL จะไม่วาดรูปสามเหลี่ยมที่หันหน้าเข้าเนื่องจากการปรับขนาดหรือการหมุนหรือเหตุผลใดก็ตาม ซึ่งเป็นเรื่องที่ดีเนื่องจากเมื่อคุณหมุนวัตถุในแบบ 3 มิติ คุณมักจะต้องการให้สามเหลี่ยมที่หันมาทางคุณถือเป็นด้านหน้า

สวัสดี รูปสามเหลี่ยมหายไปไหน แต่ปรากฏว่าป้ายจำนวนมากหันไปทางผิด หมุนแล้วคุณจะเห็นข้อความปรากฏขึ้นเมื่อมองไปที่อีกด้านหนึ่ง แต่โชคดีที่ปัญหานี้แก้ไขได้ง่าย เราแค่ดูว่ารูปภาพใดที่กลับหัวแล้วสลับจุดยอด 2 จุดของรูปภาพนั้น เช่น หากรูปสามเหลี่ยมย้อนกลับ 1 รูปคือ

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

เราแค่พลิกจุดยอด 2 จุดสุดท้ายเพื่อทำให้เดินไปข้างหน้า

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

ใกล้ขึ้นแล้ว แต่ยังมีอีก 1 ปัญหา แม้ว่ารูปสามเหลี่ยมทั้งหมดจะหันไปในทิศทางที่ถูกต้องและรูปสามเหลี่ยมที่หันหลังจะได้รับการคัดออกแล้ว แต่เรายังคงเห็นจุดที่รูปสามเหลี่ยมที่ควรอยู่ด้านหลังวาดทับรูปสามเหลี่ยมที่ควรอยู่ด้านหน้า ป้อน DEPTH BUFFER บัฟเฟอร์ความลึกหรือที่บางครั้งเรียกว่า 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.vertexAttribPointer มีขนาดใหญ่ 3

สำหรับผู้ที่มีความละเอียดรอบคอบ คุณอาจสังเกตเห็นว่าเราได้กําหนดแอตทริบิวต์ 2 รายการของเราดังนี้

attribute vec4 a_position;
attribute vec4 a_color;

ซึ่งทั้ง 2 รายการเป็น "vec4" แต่เมื่อเราบอก WebGL ว่าจะนำข้อมูลออกจากบัฟเฟอร์อย่างไร

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

"3" ในแต่ละรายการหมายความว่าให้ดึงค่าเพียง 3 ค่าต่อแอตทริบิวต์ ซึ่งได้ผลเนื่องจากในเวิร์กเท็กเตอร์ Shader นั้น WebGL มีค่าเริ่มต้นสำหรับค่าที่คุณไม่ได้ระบุ ค่าเริ่มต้นคือ 0, 0, 0, 1 โดยที่ x = 0, y = 0, z = 0 และ w = 1 ด้วยเหตุนี้ ในเวิร์กเทกซ์ Shader 2 มิติแบบเก่า เราจึงต้องระบุ 1 อย่างชัดเจน เราส่ง x และ y และต้องการ 1 สำหรับ z แต่เนื่องจากค่าเริ่มต้นของ z คือ 0 เราจึงต้องระบุ 1 อย่างชัดเจน สำหรับโมเดล 3 มิติ แม้ว่าเราจะไม่ได้ระบุ "w" แต่ค่าเริ่มต้นจะเป็น 1 ซึ่งเป็นสิ่งที่เราต้องการเพื่อให้คณิตศาสตร์เมทริกซ์ทำงานได้