ภาพ 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 มิติ สิ่งแรกที่เราต้องทำคือการเปลี่ยนตัวปรับแสงสี 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 ซึ่งเป็นสิ่งที่เราต้องใช้เพื่อให้คณิตศาสตร์เมทริกซ์ทำงาน