WebGL giúp bạn có thể hiển thị đồ hoạ 3D theo thời gian thực tuyệt vời trong trình duyệt, nhưng điều mà nhiều người không biết là WebGL thực sự là một API 2D chứ không phải API 3D. Để tôi giải thích.
WebGL chỉ quan tâm đến 2 điều. Toạ độ không gian cắt ở dạng 2D và màu sắc. Công việc của bạn với tư cách là một lập trình viên sử dụng WebGL là cung cấp cho WebGL 2 thứ đó. Bạn cung cấp 2 "trình đổ bóng" để thực hiện việc này. Chương trình đổ bóng đỉnh cung cấp toạ độ không gian cắt và chương trình đổ bóng mảnh cung cấp màu sắc. Toạ độ không gian cắt luôn nằm trong khoảng từ -1 đến +1, bất kể kích thước canvas của bạn là bao nhiêu. Sau đây là một ví dụ đơn giản về WebGL, cho thấy WebGL ở dạng đơn giản nhất.
// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");
// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
1.0, 1.0]),
gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);
Sau đây là 2 chương trình đổ bóng
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(0,1,0,1); // green
}
</script>
Xin nhắc lại, toạ độ clipspace luôn nằm trong khoảng từ -1 đến +1, bất kể kích thước của canvas. Trong trường hợp trên, bạn có thể thấy chúng ta không làm gì ngoài việc truyền trực tiếp dữ liệu vị trí. Vì dữ liệu vị trí đã nằm trong không gian cắt nên không cần làm gì cả. Nếu muốn có 3D, bạn phải cung cấp các chương trình đổ bóng chuyển đổi từ 3D sang 2D vì WebGL LÀ API 2D! Đối với nội dung 2D, bạn nên làm việc bằng pixel hơn là không gian cắt, vì vậy, hãy thay đổi chương trình đổ bóng để chúng ta có thể cung cấp hình chữ nhật bằng pixel và chuyển đổi thành không gian cắt cho chúng ta. Sau đây là chương trình đổ bóng đỉnh mới
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace, 0, 1);
}
</script>
Bây giờ, chúng ta có thể thay đổi dữ liệu từ clipspace thành pixel
// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
10, 20,
80, 20,
10, 30,
10, 30,
80, 20,
80, 30]), gl.STATIC_DRAW);
Bạn có thể nhận thấy hình chữ nhật nằm gần cuối khu vực đó. WebGL coi góc dưới cùng bên trái là 0,0. Để đặt gốc toạ độ ở góc trên cùng bên trái (vị trí truyền thống hơn được dùng cho các API đồ hoạ 2D), chúng ta chỉ cần lật toạ độ y.
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
Hãy biến mã xác định hình chữ nhật thành một hàm để chúng ta có thể gọi hàm đó cho các hình chữ nhật có kích thước khác nhau. Trong khi thực hiện, chúng ta sẽ đặt màu. Trước tiên, chúng ta sẽ cho chương trình đổ bóng mảnh lấy dữ liệu đầu vào đồng nhất về màu sắc.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
Và đây là mã mới vẽ 50 hình chữ nhật ở các vị trí ngẫu nhiên và màu sắc ngẫu nhiên.
...
var colorLocation = gl.getUniformLocation(program, "u_color");
...
// Create a buffer
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// draw 50 random rectangles in random colors
for (var ii = 0; ii < 50; ++ii) {
// Setup a random rectangle
setRectangle(
gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));
// Set a random color.
gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
}
// Returns a random integer from 0 to range - 1.
function randomInt(range) {
return Math.floor(Math.random() * range);
}
// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2]), gl.STATIC_DRAW);
}
Tôi hy vọng bạn có thể thấy rằng WebGL thực sự là một API khá đơn giản. Mặc dù việc tạo 3D có thể phức tạp hơn, nhưng sự phức tạp đó là do bạn, lập trình viên, thêm vào dưới dạng các chương trình đổ bóng phức tạp hơn. Bản thân WebGL API là 2D và khá đơn giản.
type="x-shader/x-vertex" và type="x-shader/x-fragment" có nghĩa là gì?
Thẻ <script> mặc định có JavaScript trong đó. Bạn có thể không đặt loại hoặc có thể đặt type="javascript" hoặc type="text/javascript" và trình duyệt sẽ diễn giải nội dung dưới dạng JavaScript. Nếu bạn đặt bất kỳ giá trị nào khác, trình duyệt sẽ bỏ qua nội dung của thẻ tập lệnh.
Chúng ta có thể sử dụng tính năng này để lưu trữ chương trình đổ bóng trong thẻ tập lệnh. Thậm chí, chúng ta có thể tạo loại riêng và trong javascript, hãy tìm loại đó để quyết định xem nên biên dịch chương trình đổ bóng dưới dạng chương trình đổ bóng đỉnh hay chương trình đổ bóng mảnh.
Trong trường hợp này, hàm createShaderFromScriptElement sẽ tìm một tập lệnh có id được chỉ định, sau đó xem xét type để quyết định loại chương trình đổ bóng cần tạo.
Xử lý hình ảnh bằng WebGL
Xử lý hình ảnh rất dễ dàng trong WebGL. Dễ như thế nào? Hãy đọc phần bên dưới.
Để vẽ hình ảnh trong WebGL, chúng ta cần sử dụng kết cấu. Tương tự như cách WebGL yêu cầu toạ độ không gian cắt khi kết xuất thay vì pixel, WebGL yêu cầu toạ độ kết cấu khi đọc kết cấu. Toạ độ hoạ tiết nằm trong khoảng từ 0 đến 1, bất kể kích thước của hoạ tiết. Vì chỉ vẽ một hình chữ nhật (thực ra là 2 hình tam giác), nên chúng ta cần cho WebGL biết vị trí trong hoạ tiết mà mỗi điểm trong hình chữ nhật tương ứng. Chúng ta sẽ truyền thông tin này từ chương trình đổ bóng đỉnh sang chương trình đổ bóng phân mảnh bằng cách sử dụng một loại biến đặc biệt gọi là "biến thiên". Nó được gọi là biến vì nó thay đổi. WebGL sẽ nội suy các giá trị mà chúng ta cung cấp trong chương trình đổ bóng đỉnh khi vẽ từng pixel bằng chương trình đổ bóng mảnh. Khi sử dụng chương trình đổ bóng đỉnh từ cuối phần trước, chúng ta cần thêm một thuộc tính để truyền vào toạ độ hoạ tiết, sau đó truyền các toạ độ đó đến chương trình đổ bóng mảnh.
attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;
void main() {
...
// pass the texCoord to the fragment shader
// The GPU will interpolate this value between points
v_texCoord = a_texCoord;
}
Sau đó, chúng ta cung cấp một chương trình đổ bóng mảnh để tra cứu màu sắc từ kết cấu.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
// our texture
uniform sampler2D u_image;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
// Look up a color from the texture.
gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>
Cuối cùng, chúng ta cần tải một hình ảnh, tạo một hoạ tiết và sao chép hình ảnh đó vào hoạ tiết. Vì chúng ta đang ở trong trình duyệt, hình ảnh sẽ tải không đồng bộ nên chúng ta phải sắp xếp lại mã một chút để chờ kết cấu tải. Sau khi tải xong, chúng ta sẽ vẽ nó.
function main() {
var image = new Image();
image.src = "http://someimage/on/our/server"; // MUST BE SAME DOMAIN!!!
image.onload = function() {
render(image);
}
}
function render(image) {
...
// all the code we had before.
...
// look up where the texture coordinates need to go.
var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Create a texture.
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
...
}
Không có gì thú vị, vậy nên hãy chỉnh sửa hình ảnh đó. How about just swapping red and blue?
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
Nếu chúng ta muốn xử lý hình ảnh mà thực sự xem xét các pixel khác thì sao? Vì WebGL tham chiếu các hoạ tiết trong toạ độ hoạ tiết từ 0,0 đến 1,0, nên chúng ta có thể tính toán mức độ di chuyển cho 1 pixel bằng phép toán đơn giản onePixel = 1.0 / textureSize.
Dưới đây là chương trình đổ bóng mảnh tính trung bình các pixel bên trái và bên phải của mỗi pixel trong hoạ tiết.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
// compute 1 pixel in texture coordinates.
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
// average the left, middle, and right pixels.
gl_FragColor = (
texture2D(u_image, v_texCoord) +
texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>
Sau đó, chúng ta cần truyền kích thước của hoạ tiết từ JavaScript.
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...
Giờ đây, khi đã biết cách tham chiếu các pixel khác, hãy sử dụng nhân tích chập để thực hiện một loạt quy trình xử lý hình ảnh phổ biến. Trong trường hợp này, chúng ta sẽ sử dụng hạt nhân 3x3. Hạt nhân tích chập chỉ là một ma trận 3x3, trong đó mỗi mục trong ma trận biểu thị mức độ nhân 8 pixel xung quanh pixel mà chúng ta đang kết xuất. Sau đó, chúng ta chia kết quả cho trọng số của hạt nhân (tổng của tất cả giá trị trong hạt nhân) hoặc 1,0, tuỳ theo giá trị nào lớn hơn. Sau đây là một bài viết khá hay về vấn đề này. Và đây là một bài viết khác cho thấy một số mã thực tế nếu bạn viết mã này bằng tay trong C++. Trong trường hợp này, chúng ta sẽ thực hiện công việc đó trong chương trình đổ bóng, vì vậy, đây là chương trình đổ bóng mảnh mới.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
vec4 colorSum =
texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] +
texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ;
float kernelWeight =
u_kernel[0] +
u_kernel[1] +
u_kernel[2] +
u_kernel[3] +
u_kernel[4] +
u_kernel[5] +
u_kernel[6] +
u_kernel[7] +
u_kernel[8] ;
if (kernelWeight <= 0.0) {
kernelWeight = 1.0;
}
// Divide the sum by the weight but just use rgb
// we'll set alpha to 1.0
gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>
Trong JavaScript, chúng ta cần cung cấp một hạt nhân tích chập.
...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...
Tôi hy vọng điều này đã thuyết phục bạn rằng việc xử lý hình ảnh trong WebGL khá đơn giản. Tiếp theo, tôi sẽ hướng dẫn cách áp dụng nhiều hiệu ứng cho hình ảnh.
Tiền tố a, u và v_ trước các biến trong GLSL có ý nghĩa gì?
Đó chỉ là quy ước đặt tên. a_ cho các thuộc tính là dữ liệu do vùng đệm cung cấp. u_ cho các đồng phục là đầu vào cho chương trình đổ bóng, v_ cho các biến thể là các giá trị được truyền từ chương trình đổ bóng đỉnh sang chương trình đổ bóng mảnh và được nội suy (hoặc thay đổi) giữa các đỉnh cho mỗi pixel được vẽ.
Áp dụng nhiều hiệu ứng
Câu hỏi rõ ràng tiếp theo về việc xử lý hình ảnh là làm cách nào để áp dụng nhiều hiệu ứng?
Bạn có thể thử tạo chương trình đổ bóng nhanh chóng. Cung cấp giao diện người dùng cho phép người dùng chọn hiệu ứng mà họ muốn sử dụng, sau đó tạo một chương trình đổ bóng thực hiện tất cả hiệu ứng. Điều này không phải lúc nào cũng khả thi, mặc dù kỹ thuật này thường được dùng để tạo hiệu ứng cho đồ hoạ theo thời gian thực. Một cách linh hoạt hơn là sử dụng thêm 2 kết cấu và kết xuất lần lượt cho từng kết cấu, chuyển đổi qua lại và áp dụng hiệu ứng tiếp theo mỗi lần.
Original Image -> [Blur] -> Texture 1
Texture 1 -> [Sharpen] -> Texture 2
Texture 2 -> [Edge Detect] -> Texture 1
Texture 1 -> [Blur] -> Texture 2
Texture 2 -> [Normal] -> Canvas
Để làm việc này, chúng ta cần tạo vùng đệm khung. Trong WebGL và OpenGL, Framebuffer thực sự là một cái tên không phù hợp. Bộ đệm khung WebGL/OpenGL thực sự chỉ là một tập hợp trạng thái chứ không phải là một loại bộ đệm. Tuy nhiên, bằng cách đính kèm một kết cấu vào vùng đệm khung, chúng ta có thể kết xuất vào kết cấu đó. Trước tiên, hãy chuyển mã tạo kết cấu cũ thành một hàm
function createAndSetupTexture(gl) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set up texture so we can render any size image and so we are
// working with pixels.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
return texture;
}
// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
Bây giờ, hãy sử dụng hàm đó để tạo thêm 2 hoạ tiết và đính kèm chúng vào 2 vùng đệm khung.
// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
var texture = createAndSetupTexture(gl);
textures.push(texture);
// make the texture the same size as the image
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);
// Create a framebuffer
var fbo = gl.createFramebuffer();
framebuffers.push(fbo);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Attach a texture to it.
gl.framebufferTexture2D(
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
Bây giờ, hãy tạo một tập hợp các hạt nhân rồi tạo một danh sách các hạt nhân để áp dụng.
// Define several convolution kernels
var kernels = {
normal: [
0, 0, 0,
0, 1, 0,
0, 0, 0
],
gaussianBlur: [
0.045, 0.122, 0.045,
0.122, 0.332, 0.122,
0.045, 0.122, 0.045
],
unsharpen: [
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
],
emboss: [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
]
};
// List of effects to apply.
var effectsToApply = [
"gaussianBlur",
"emboss",
"gaussianBlur",
"unsharpen"
];
Và cuối cùng, hãy áp dụng từng hiệu ứng, chuyển đổi qua lại giữa các hoạ tiết mà chúng ta đang kết xuất
// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);
// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);
// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
// Setup to draw into one of the framebuffers.
setFramebuffer(framebuffers[ii % 2], image.width, image.height);
drawWithKernel(effectsToApply[ii]);
// for the next draw, use the texture we just rendered to.
gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}
// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1); // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");
function setFramebuffer(fbo, width, height) {
// make this the framebuffer we are rendering to.
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Tell the shader the resolution of the framebuffer.
gl.uniform2f(resolutionLocation, width, height);
// Tell webgl the viewport setting needed for framebuffer.
gl.viewport(0, 0, width, height);
}
function drawWithKernel(name) {
// set the kernel
gl.uniform1fv(kernelLocation, kernels[name]);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
Có vài điều tôi nên xem qua.
Việc gọi gl.bindFramebuffer bằng null sẽ cho WebGL biết rằng bạn muốn kết xuất vào canvas thay vì vào một trong các vùng đệm khung.
WebGL phải chuyển đổi từ không gian cắt trở lại thành pixel. Việc này được thực hiện dựa trên chế độ cài đặt của gl.viewport. Chế độ cài đặt của gl.viewport mặc định là kích thước của canvas khi chúng ta khởi chạy WebGL. Vì vùng đệm khung mà chúng ta đang kết xuất có kích thước khác với canvas, nên chúng ta cần đặt khung nhìn cho phù hợp.
Cuối cùng, trong các ví dụ về nguyên tắc cơ bản của WebGL, chúng tôi đã lật toạ độ Y khi kết xuất vì WebGL hiển thị canvas với 0,0 là góc dưới cùng bên trái thay vì góc trên cùng bên trái truyền thống hơn cho 2D. Bạn không cần thực hiện việc này khi kết xuất vào vùng đệm khung. Vì vùng đệm khung không bao giờ hiển thị, nên phần nào là phần trên cùng và phần dưới cùng không quan trọng. Điều quan trọng là pixel 0,0 trong vùng đệm khung tương ứng với 0,0 trong các phép tính của chúng ta. Để xử lý vấn đề này, tôi đã cho phép thiết lập việc có lật hay không bằng cách thêm một dữ liệu đầu vào nữa vào chương trình đổ bóng.
<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...
void main() {
...
gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
...
}
</script>
Sau đó, chúng ta có thể đặt giá trị này khi kết xuất bằng
...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);
Tôi đã giữ cho ví dụ này đơn giản bằng cách sử dụng một chương trình GLSL duy nhất có thể đạt được nhiều hiệu ứng. Nếu muốn xử lý hình ảnh toàn diện, bạn có thể cần nhiều chương trình GLSL. Chương trình điều chỉnh sắc độ, độ bão hoà và độ sáng. Một nút khác để điều chỉnh độ sáng và độ tương phản. Một chương trình để đảo ngược, một chương trình khác để điều chỉnh cấp độ, v.v. Bạn cần thay đổi mã để chuyển đổi chương trình GLSL và cập nhật các tham số cho chương trình cụ thể đó. Tôi đã cân nhắc việc viết ví dụ đó, nhưng đây là bài tập tốt nhất nên để người đọc tự làm vì nhiều chương trình GLSL, mỗi chương trình có nhu cầu tham số riêng, có thể cần phải tái cấu trúc lớn để giữ cho tất cả không trở thành một mớ hỗn độn. Tôi hy vọng ví dụ này và các ví dụ trước đó đã giúp WebGL dễ tiếp cận hơn một chút và tôi hy vọng việc bắt đầu với 2D sẽ giúp WebGL dễ hiểu hơn một chút. Nếu có thời gian, tôi sẽ cố gắng viết thêm một vài bài viết về cách tạo 3D cũng như thông tin chi tiết hơn về những gì WebGL thực sự đang làm.
WebGL và alpha
Tôi nhận thấy một số nhà phát triển OpenGL gặp vấn đề về cách WebGL xử lý alpha trong vùng đệm sau (tức là canvas), vì vậy tôi nghĩ nên xem qua một số điểm khác biệt giữa WebGL và OpenGL liên quan đến alpha.
Sự khác biệt lớn nhất giữa OpenGL và WebGL là OpenGL kết xuất thành một vùng đệm phía sau không được kết hợp với bất kỳ thứ gì, hoặc không được kết hợp hiệu quả với bất kỳ thứ gì bởi trình quản lý cửa sổ của hệ điều hành, vì vậy, alpha của bạn không quan trọng.
WebGL được trình duyệt kết hợp với trang web và mặc định là sử dụng alpha được nhân trước giống như các thẻ <img> .png có độ trong suốt và thẻ canvas 2d.
WebGL có một số cách để làm cho việc này giống với OpenGL hơn.
#1. Cho WebGL biết bạn muốn kết hợp WebGL với alpha không được nhân trước
gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});
Giá trị mặc định là "true". Tất nhiên, kết quả vẫn sẽ được kết hợp trên trang với bất kỳ màu nền nào nằm dưới canvas (màu nền của canvas, màu nền của vùng chứa canvas, màu nền của trang, nội dung phía sau canvas nếu canvas có chỉ mục z > 0, v.v.), nói cách khác là màu CSS xác định cho khu vực đó của trang web. Một cách rất hay để tìm xem bạn có gặp vấn đề về alpha hay không là đặt nền của canvas thành một màu sáng như màu đỏ. Bạn sẽ thấy ngay những gì đang diễn ra.
<canvas style="background: red;"></canvas>
Bạn cũng có thể đặt màu nền thành màu đen để ẩn mọi vấn đề về độ trong suốt.
#2. Cho WebGL biết bạn không muốn có alpha trong vùng đệm sau
gl = canvas.getContext("experimental-webgl", {alpha: false});
Điều này sẽ giúp nó hoạt động giống OpenGL hơn vì vùng đệm lùi sẽ chỉ có RGB. Đây có thể là lựa chọn tốt nhất vì một trình duyệt tốt có thể thấy rằng bạn không có alpha và thực sự tối ưu hoá cách kết hợp WebGL. Tất nhiên, điều đó cũng có nghĩa là thực tế nó sẽ không có alpha trong vùng đệm sau, vì vậy nếu bạn đang sử dụng alpha trong vùng đệm sau cho một số mục đích thì có thể không hiệu quả. Tôi biết một vài ứng dụng sử dụng alpha trong vùng đệm sau. Tôi nghĩ rằng đây nên là chế độ mặc định.
#3. Xoá alpha ở cuối quá trình kết xuất
..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);
Quá trình xoá thường diễn ra rất nhanh vì có một trường hợp đặc biệt cho quá trình này trong hầu hết phần cứng. Tôi đã làm việc này trong hầu hết các bản minh hoạ của mình. Nếu thông minh, tôi sẽ chuyển sang phương pháp số 2 ở trên. Có thể tôi sẽ làm việc đó ngay sau khi đăng video này. Có vẻ như hầu hết các thư viện WebGL đều nên mặc định sử dụng phương thức này. Một số ít nhà phát triển thực sự đang sử dụng alpha cho hiệu ứng kết hợp có thể yêu cầu tính năng này. Phần còn lại sẽ chỉ nhận được hiệu suất tốt nhất và ít bất ngờ nhất.
#4. Xoá alpha một lần rồi không kết xuất vào đó nữa
// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);
// Turn off rendering to alpha
gl.colorMask(true, true, true, false);
Tất nhiên, nếu đang kết xuất vào vùng đệm khung của riêng mình, bạn có thể cần bật lại tính năng kết xuất thành alpha rồi tắt lại khi chuyển sang kết xuất thành canvas.
#5. Xử lý hình ảnh
Ngoài ra, nếu bạn đang tải tệp PNG có alpha vào kết cấu, thì theo mặc định, alpha của các tệp này sẽ được nhân trước, đây thường KHÔNG phải là cách hầu hết các trò chơi thực hiện. Nếu muốn ngăn hành vi đó, bạn cần thông báo cho WebGL bằng
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
#6. Sử dụng phương trình phối hoạt động với alpha được nhân trước
Hầu hết các ứng dụng OpenGL mà tôi đã viết hoặc làm việc đều sử dụng
gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
Điều này áp dụng cho các kết cấu alpha không được nhân trước. Nếu thực sự muốn làm việc với hoạ tiết alpha được nhân trước, thì có thể bạn sẽ muốn
gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);
Đó là những phương thức mà tôi biết. Nếu bạn biết thêm, vui lòng đăng bên dưới.