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 ซึ่งเป็นสิ่งที่เราต้องการเพื่อให้คณิตศาสตร์เมทริกซ์ทำงานได้