3D trực quan WebGL

Gregg Tavares
Gregg Tavares

WebGL 3D chỉnh hình

Bài đăng này là phần tiếp theo của loạt bài đăng về WebGL. Nghiên cứu đầu tiên bắt đầu với các khái niệm cơ bản và nội dung trước là về ma trận 2d khoảng ma trận 2D. Nếu bạn chưa đọc những thông tin này, hãy xem chúng trước. Trong bài đăng trước, chúng ta đã tìm hiểu cách hoạt động của ma trận 2d. Chúng ta đã nói về việc dịch chuyển, xoay, chia tỷ lệ và thậm chí là chiếu từ pixel vào không gian cắt đều có thể được thực hiện bằng 1 ma trận và một vài phép toán ma trận kỳ diệu. Chỉ còn một bước nhỏ nữa là tạo ra mô hình 3D. Trong các ví dụ 2D trước đó, chúng ta đã có các điểm 2D (x, y) mà chúng ta nhân với một ma trận 3x3. Để mô phỏng 3D, chúng ta cần các điểm 3D (x, y, z) và ma trận 4x4. Hãy xem ví dụ cuối cùng và thay đổi thành 3D. Chúng ta sẽ sử dụng lại F nhưng lần này là 3D 'F'. Điều đầu tiên chúng ta cần làm là thay đổi chương trình đổ bóng đỉnh để xử lý 3D. Đây là chương trình đổ bóng cũ.

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

Sau đây là một câu đố mới

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

Việc này trở nên đơn giản hơn! Sau đó, chúng ta cần cung cấp dữ liệu 3D.

...

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

Tiếp theo, chúng ta cần thay đổi tất cả các hàm ma trận từ 2D sang 3D Đây là các phiên bản 2D (trước) của makeTranslation, makeRotation và 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
];
}

Và đây là phiên bản 3D đã cập nhật.

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

Lưu ý rằng chúng ta hiện có 3 hàm xoay. Chúng tôi chỉ cần một thành phần trong 2D vì chúng tôi chỉ xoay xung quanh trục Z một cách hiệu quả. Mặc dù để làm 3D, chúng tôi cũng muốn có thể xoay xung quanh trục x và trục y. Bạn có thể thấy rằng tất cả chúng rất giống nhau. Nếu chúng tôi giải quyết vấn đề, bạn sẽ thấy chúng đơn giản hoá như trước đây

Xoay Z

newX = x * c + y * s;
newY = x * -s + y * c;

Xoay Y


newX = x * c + z * s;
newZ = x * -s + z * c;

Xoay trục X

newY = y * c + z * s;
newZ = y * -s + z * c;

Chúng ta cũng cần cập nhật hàm chiếu. Đây là kết quả 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
];
}

giúp chuyển đổi từ pixel thành không gian cắt bớt. Đối với lần đầu tiên mở rộng sang mô hình 3D, chúng ta hãy thử

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

Giống như việc chuyển đổi từ pixel sang không gian cắt cho x và y, chúng ta cũng cần làm tương tự. Trong trường hợp này, tôi cũng sẽ tạo các đơn vị pixel không gian Z. Tôi sẽ truyền một giá trị tương tự như width cho chiều sâu để không gian của chúng ta có chiều rộng từ 0 đến chiều rộng pixel, chiều cao pixel từ 0 đến chiều cao, nhưng đối với chiều sâu thì giá trị này sẽ là -depth / 2 đến +depth / 2. Cuối cùng, chúng ta cần cập nhật mã tính toán ma trận.

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

Vấn đề đầu tiên chúng tôi gặp phải là hình của chúng ta là hình phẳng F, khiến cho khó nhìn thấy 3D. Để khắc phục vấn đề này, hãy mở rộng hình học sang 3D. F hiện tại của chúng ta được tạo từ 3 hình chữ nhật, mỗi hình có 2 tam giác. Để chuyển thành 3D, bạn cần có tổng cộng 16 hình chữ nhật. Có khá nhiều câu hỏi cần nêu ở đây. 16 hình chữ nhật x 2 tam giác mỗi hình chữ nhật x 3 đỉnh mỗi tam giác là 96 đỉnh. Nếu bạn muốn xem tất cả các tập hợp này, hãy xem nguồn trên mẫu. Chúng ta phải vẽ nhiều đỉnh hơn để

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

Di chuyển thanh trượt sẽ rất khó để nhận biết đó là 3D. Hãy thử tô màu cho mỗi hình chữ nhật bằng một màu khác nhau. Để thực hiện việc này, chúng ta sẽ thêm một thuộc tính khác vào chương trình đổ bóng đỉnh và truyền thuộc tính đó từ chương trình đổ bóng đỉnh đến trình đổ bóng mảnh. Đây là chương trình đổ bóng đỉnh (vertex) mới

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

Chúng ta cần sử dụng màu đó trong chương trình đổ bóng mảnh

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

Chúng ta cần tra cứu vị trí để cung cấp màu sắc, sau đó thiết lập một bộ đệm và thuộc tính khác để cung cấp màu sắc.

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

Ồ, thật là lộn xộn gì? Hoá ra tất cả các phần của 3D 'F', mặt trước, mặt sau, mặt bên, v.v. đó đều được vẽ theo thứ tự xuất hiện trong hình của chúng ta. Điều đó không mang lại cho chúng tôi kết quả mong muốn vì đôi khi các kết quả ở phía sau được vẽ sau các kết quả ở phía trước. Hình tam giác trong WebGL có khái niệm hình ảnh mặt trước và mặt sau. Một hình tam giác quay mặt trước có các đỉnh đi theo chiều kim đồng hồ. Một hình tam giác quay mặt sau có các đỉnh đi ngược chiều kim đồng hồ.

Đang tạo hình tam giác.

WebGL có khả năng chỉ vẽ các tam giác hướng về phía trước hoặc mặt sau. Chúng ta có thể bật tính năng đó bằng

gl.enable(gl.CULL_FACE);

chúng tôi chỉ làm một lần, ngay khi bắt đầu chương trình. Khi bật tính năng đó, WebGL sẽ mặc định sử dụng hình tam giác "lọc" ra sau. "Culling" trong trường hợp này là một từ ưa thích của "không vẽ". Xin lưu ý rằng trong chừng từ WebGL được quy định, việc một tam giác có được coi là đi theo chiều kim đồng hồ hay ngược chiều kim đồng hồ hay không phụ thuộc vào các đỉnh của tam giác đó trong không gian đoạn video. Nói cách khác, WebGL sẽ tìm ra hình tam giác ở phía trước hay phía sau SAU KHI bạn áp dụng toán học cho các đỉnh trong chương trình đổ bóng đỉnh. Ví dụ: một tam giác theo chiều kim đồng hồ được điều chỉnh theo tỷ lệ trong X -1 sẽ trở thành hình tam giác ngược chiều kim đồng hồ hoặc một tam giác theo chiều kim đồng hồ xoay 180 độ xung quanh trục X hoặc Y trở thành tam giác ngược chiều kim đồng hồ. Vì đã tắt CULL_FACE, nên chúng ta có thể thấy cả hai hình tam giác theo chiều kim đồng hồ(phía trước) và ngược chiều kim đồng hồ(phía sau). Bây giờ, chúng ta đã bật tính năng này, bất cứ khi nào hình tam giác mặt trước lật xung quanh do điều chỉnh theo tỷ lệ hoặc xoay hoặc vì bất kỳ lý do gì, WebGL sẽ không vẽ. Đây là điều hữu ích vì khi xoay xung quanh ở chế độ 3D, bạn thường muốn hình tam giác hướng về phía mình được coi là hướng chính diện.

Chào bạn! Tất cả các tam giác chuyển đến đâu? Hoá ra, nhiều người trong số họ đang gặp sai lầm. Xoay và bạn sẽ thấy chúng xuất hiện khi bạn nhìn vào phía bên kia. Rất may là sửa lỗi này dễ dàng. Chúng ta chỉ xem đỉnh nào lùi và trao đổi 2 đỉnh của chúng. Ví dụ: nếu một tam giác ngược

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

chúng ta chỉ cần lật 2 đỉnh cuối cùng để làm cho nó tiến về phía trước.

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

Gần hơn nữa, nhưng vẫn còn một vấn đề nữa. Ngay cả khi tất cả các tam giác hướng theo đúng hướng và các tam giác quay mặt sau được cắt bớt, chúng ta vẫn có những vị trí mà trong đó các tam giác phải được vẽ trên các tam giác phải ở phía trước. Nhập BỘ PHẬN BUỔI TẬP LUYỆN. Vùng đệm độ sâu, đôi khi được gọi là Z-Buffer, là một hình chữ nhật có depth pixel, một pixel độ sâu cho mỗi pixel màu dùng để tạo hình ảnh. Khi WebGL vẽ từng pixel màu, nó cũng có thể vẽ một pixel chiều sâu. Công cụ này thực hiện việc này dựa trên các giá trị chúng tôi trả về từ chương trình đổ bóng đỉnh cho Z. Giống như chúng ta phải chuyển đổi thành không gian cắt cho X và Y, vậy thành Z nằm trong không gian cắt hoặc (-1 thành +1). Sau đó giá trị này được chuyển đổi thành giá trị không gian độ sâu (0 đến +1). Trước khi WebGL vẽ một pixel màu, WebGL sẽ kiểm tra pixel độ sâu tương ứng. Nếu giá trị độ sâu cho pixel sắp vẽ lớn hơn giá trị của pixel độ sâu tương ứng, thì WebGL sẽ không vẽ pixel màu mới. Nếu không, thao tác này sẽ vẽ cả pixel màu mới bằng màu từ chương trình đổ bóng mảnh VÀ vẽ pixel độ sâu bằng giá trị độ sâu mới. Điều này có nghĩa là các pixel đứng sau các pixel khác sẽ không được vẽ. Chúng ta có thể bật tính năng này gần như đơn giản như khi bật chế độ lọc bằng

gl.enable(gl.DEPTH_TEST);

Chúng ta cũng cần xoá vùng đệm độ sâu về 1.0 trước khi bắt đầu vẽ.

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

Trong bài đăng tiếp theo, tôi sẽ trình bày cách tạo góc nhìn cho doanh nghiệp.

Tại sao thuộc tính vec4 nhưng gl.vertexAttribPointer kích thước 3

Đối với những ai thiên về định hướng chi tiết, có thể bạn sẽ thấy chúng tôi đã xác định 2 thuộc tính là

attribute vec4 a_position;
attribute vec4 a_color;

cả hai đều là "vec4" nhưng khi chúng tôi cho WebGL biết cách lấy dữ liệu ra khỏi bộ đệm mà chúng tôi đã sử dụng

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

Số '3' trong mỗi mục cho biết chỉ lấy ra 3 giá trị cho mỗi thuộc tính. Cách này hiệu quả vì trong trình đổ bóng đỉnh, WebGL cung cấp chế độ mặc định cho những thiết bị mà bạn không cung cấp. Các giá trị mặc định là 0, 0, 0, 1 trong đó x = 0, y = 0, z = 0 và w = 1. Đây là lý do tại sao trong chương trình đổ bóng đỉnh 2D cũ của chúng tôi, chúng tôi phải cung cấp rõ ràng 1. Chúng ta đã truyền giá trị x và y và cần giá trị 1 cho z nhưng vì giá trị mặc định cho z là 0 nên chúng tôi phải cung cấp giá trị 1 một cách rõ ràng. Tuy nhiên, đối với mô hình 3D, mặc dù chúng tôi không cung cấp "w", giá trị mặc định là 1. Đây là điều chúng tôi cần để phép toán ma trận hoạt động.