รูปแบบ 3 มิติของ WebGL
โพสต์นี้มีเนื้อหาต่อเนื่องมาจากชุดโพสต์เกี่ยวกับ WebGL เมตริกแรกเริ่มต้นด้วยเมทริกซ์และก่อนหน้าประมาณ 2 มิติประมาณเมทริกซ์ 2 มิติ หากคุณยังไม่ได้อ่าน โปรดอ่านก่อน ในโพสต์ล่าสุด เราได้พูดถึงวิธีการทำงานของเมทริกซ์ 2 มิติ เราพูดถึงการแปล การหมุน การปรับขนาด และแม้แต่การฉายภาพจากพิกเซลไปยังพื้นที่คลิปก็สามารถทำได้โดย 1 เมทริกซ์และเมทริกซ์มายากล ส่วนในการสร้างแบบ 3 มิตินั้น อีกเพียงไม่กี่ขั้นตอนเท่านั้น ในตัวอย่าง 2 มิติก่อนหน้านี้ เรามีจุด 2 มิติ (x, y) ที่คูณด้วยเมทริกซ์ 3x3 ในการสร้าง 3 มิติ เราต้องใช้จุด 3 มิติ (x, y, z) และเมทริกซ์ 4x4 มาดูตัวอย่างล่าสุดของเราและเปลี่ยนให้เป็น 3 มิติ เราจะใช้ F อีกครั้ง แต่คราวนี้จะเป็น 'F' แบบ 3 มิติ สิ่งแรกที่เราต้องทำคือการเปลี่ยนตัวปรับแสงสี Vertex ให้รองรับ 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);
}
ต่อไปเราต้องเปลี่ยนฟังก์ชันเมทริกซ์ทั้งหมดจาก 2D เป็น 3D นี่คือเวอร์ชัน 2D (ก่อน) ของ 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
];
}
และนี่คือเวอร์ชัน 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,
];
}
เช่นเดียวกับที่เราต้องแปลงจากพิกเซลเป็น Clipspace สำหรับ x และ y สำหรับ z เราต้องทำสิ่งเดียวกัน ในที่นี้ผมกำลังสร้างหน่วยพิกเซล
ของ Z Space ด้วย ผมจะส่งค่าบางส่วนที่คล้ายคลึงกับ 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 มิติ เรามาลองระบายสี สี่เหลี่ยมแต่ละรูปเป็นสีที่ต่างกัน วิธีการคือ เราจะเพิ่มแอตทริบิวต์อื่นลงในตัวปรับแสงเงาจุดยอด และเพิ่มแอตทริบิวต์อื่นที่จะส่งผ่านจากเฉดสีจุดยอดไปยังตัวปรับแสงเงาส่วน นี่คือ Vertex Shades ใหม่
<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>
และเราต้องใช้สีนั้นใน Fragment Shades
<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 จะมีค่าเริ่มต้นเป็น "ตัด" รูปสามเหลี่ยมที่หันด้านหลังเข้าหากัน "Culling" (Culling) ในกรณีนี้คือคำหรูๆ ที่หมายถึง "ไม่ได้วาด" โปรดทราบว่าเท่าที่ 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,
อยู่ใกล้ๆ นี่แล้ว แต่ยังมีปัญหาอื่นอีก ถึงแม้ว่าสามเหลี่ยมทั้งหมดจะหันไปในทิศทางที่ถูกต้อง และสามเหลี่ยมหันหลังถูกบีบออก เราก็ยังคงมีที่ที่สามเหลี่ยมที่ควรอยู่ด้านหลังจะถูกวาดในรูปสามเหลี่ยมที่ควรจะอยู่ด้านหน้า
ป้อน DEPTH BUFFER
บัฟเฟอร์ความลึกซึ่งบางครั้งเรียกว่า Z-Buffer คือสี่เหลี่ยมผืนผ้าขนาด depth
พิกเซล โดยมีความลึก 1 พิกเซลต่อพิกเซลสีแต่ละพิกเซลที่ใช้ในการทําให้รูปภาพ ขณะที่ 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 ค่าต่อแอตทริบิวต์ ซึ่งทำงานได้เพราะใน vertex Shades WebGL คือ WebGL ที่เราไม่ได้ใส่ค่าเริ่มต้น ค่าเริ่มต้นคือ 0, 0, 0, 1 โดยที่ x = 0, y = 0, z = 0 และ w = 1 นี่คือเหตุผลที่ต้องใส่ 1 ลงในตัวปรับแสงเงาเวอร์เท็กซ์แบบ 2 มิติแบบเก่าอย่างชัดแจ้ง เราส่ง x และ y และต้องการ 1 สำหรับ z แต่เนื่องจากค่าเริ่มต้นของ z คือ 0 เราจึงต้องระบุ 1 อย่างชัดเจน สำหรับแบบ 3 มิติ ถึงแม้ว่าเราไม่มี "w" แต่ค่าเริ่มต้นเป็น 1 ซึ่งเป็นสิ่งที่เราต้องใช้เพื่อให้คณิตศาสตร์เมทริกซ์ทำงาน