ภาพ 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 มิติ สิ่งแรกที่เราต้องทำคือเปลี่ยนเวอร์เท็กซ์เชเดอร์เพื่อจัดการ 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 ด้วย ฉันจะส่งค่าที่คล้ายกับ 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 Shader และตัวแปรเพื่อส่งจาก Vertex Shader ไปยัง Fragment 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>

และเราต้องใช้สีนั้นใน Fragment Shader

<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 สามเหลี่ยมจะถือว่าหมุนตามเข็มนาฬิกาหรือทวนเข็มนาฬิกาหรือไม่นั้นขึ้นอยู่กับจุดยอดของสามเหลี่ยมนั้นใน Clipspace กล่าวคือ WebGL จะพิจารณาว่าสามเหลี่ยมอยู่ด้านหน้าหรือด้านหลังหลังจากที่คุณใช้คณิตศาสตร์กับจุดยอดใน Vertex Shader ซึ่งหมายความว่า ตัวอย่างเช่น สามเหลี่ยมตามเข็มนาฬิกาที่ปรับขนาดในแกน 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 อย่าง แม้ว่าสามเหลี่ยมทั้งหมดจะหันไปในทิศทางที่ถูกต้องและมีการคัดสามเหลี่ยมที่หันหลังออกแล้ว แต่ก็ยังมีบางจุดที่สามเหลี่ยมที่ควรอยู่ด้านหลังถูกวาดทับสามเหลี่ยมที่ควรอยู่ด้านหน้า ป้อนบัฟเฟอร์ความลึก บัฟเฟอร์ความลึก (บางครั้งเรียกว่า Z-Buffer) คือสี่เหลี่ยมผืนผ้าขนาด depth พิกเซล โดยมีพิกเซลความลึก 1 พิกเซลสำหรับพิกเซลสีแต่ละพิกเซลที่ใช้สร้างรูปภาพ เมื่อ WebGL วาดพิกเซลสีแต่ละพิกเซล ก็จะวาดพิกเซลความลึกด้วย โดยจะดำเนินการตามค่าที่เราแสดงผลจาก Vertex Shader สำหรับ Z เช่นเดียวกับที่เราต้องแปลงเป็นพื้นที่คลิปสำหรับ X และ Y ดังนั้น Z จึงอยู่ในพื้นที่คลิปหรือ (-1 ถึง +1) จากนั้นระบบจะแปลงค่าดังกล่าวเป็นค่าพื้นที่ความลึก (0 ถึง +1) ก่อนที่ WebGL จะวาดพิกเซลสี ระบบจะตรวจสอบพิกเซลความลึกที่สอดคล้องกัน หากค่าความลึกของพิกเซลที่กำลังจะวาดมีค่ามากกว่าค่าของพิกเซลความลึกที่สอดคล้องกัน WebGL จะไม่วาดพิกเซลสีใหม่ มิฉะนั้น ระบบจะวาดทั้งพิกเซลสีใหม่ด้วยสีจาก Fragment Shader และวาดพิกเซลความลึกด้วยค่าความลึกใหม่ ซึ่งหมายความว่าระบบจะไม่วาดพิกเซลที่อยู่หลังพิกเซลอื่นๆ เราเปิดใช้ฟีเจอร์นี้ได้ง่ายพอๆ กับการเปิดใช้การคัดออกด้วย

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