Nguyên tắc cơ bản của WebGL

Gregg Tavares
Gregg Tavares

Kiến thức cơ bản về WebGL

WebGL cho phép hiển thị đồ hoạ 3D thời gian thực tuyệt đẹp trong trình duyệt, nhưng nhiều người không biết rằng 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. Các toạ độ không gian cắt đoạn ở chế độ 2D và bằng màu. Công việc của bạn với tư cách là lập trình viên sử dụng WebGL là cung cấp cho WebGL 2 yếu tố đó. Bạn cung cấp 2 "shader" để thực hiện việc này. Chương trình đổ bóng Vertex cung cấp toạ độ clipspace và chương trình đổ bóng mảnh cung cấp màu sắc. Toạ độ không gian clip luôn đi từ -1 đến +1 bất kể canvas của bạn ở kích thước nào. Dưới đây là một ví dụ WebGL đơn giản hiển thị 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ạ độ không gian cắt 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 trực tiếp truyền dữ liệu vị trí. Vì dữ liệu vị trí đã ở trong không gian cắt nên bạn không cần làm gì cả. Nếu muốn 3D, bạn có thể cung cấp chương trình đổ bóng chuyển đổi từ 3D sang 2D vì WebGL LÀ MỘT API 2D! Đối với nội dung 2D, bạn nên làm việc theo pixel thay vì 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 theo pixel và chuyển đổi thành không gian cắt cho chúng ta. Dưới đâ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ừ không gian cắt sang 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. Để chuyển thành góc trên cùng bên trái truyền thống hơn 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 đó, chúng ta sẽ thiết lập màu sắc. Trước tiên, chúng ta sẽ tạo chương trình đổ bóng mảnh lấy dữ liệu đầu vào là màu đồng nhất.

<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í 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 hình 3D có thể phức tạp hơn, nhưng bạn (với tư cách là lập trình viên) sẽ thêm chức năng đó dưới dạng chương trình đổ bóng phức tạp hơn. Bản thân API WebGL 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 chứa JavaScript. Bạn không thể đặt loại nào hoặc có thể đặt type="javascript" hay type="text/javascript" và trình duyệt sẽ diễn giải nội dung là JavaScript. Nếu bạn đặt bất kỳ nội dung 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. Tốt hơn nữa, chúng ta có thể tạo loại của riêng mình và trong javascript, hãy tìm loại đó để quyết định có 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 hay không.

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 type để quyết định loại chương trình đổ bóng cần tạo.

Xử lý hình ảnh WebGL

Bạn có thể dễ dàng xử lý hình ảnh trong WebGL. Dễ đến mức nào? Hãy đọc thông tin bên dưới.

Để vẽ hình ảnh trong WebGL, chúng ta cần sử dụng hoạ tiết. Tương tự như cách WebGL yêu cầu toạ độ không gian clip 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úng ta chỉ vẽ một hình chữ nhật (tức 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 chóp đến chương trình đổ bóng phân mảnh bằng một loại biến đặc biệt có tên là "varying". Nó được gọi là biến thiên (thay đổi) vì nó thay đổi. WebGL sẽ nội suy các giá trị mà chúng tôi cung cấp trong chương trình đổ bóng đỉnh khi nó vẽ từng pixel bằng cách sử dụng chương trình đổ bóng mảnh. Sử dụng chương trình đổ bóng đỉnh ở cuối phần trước, chúng ta cần thêm một thuộc tính để truyền toạ độ của hoạ tiết, sau đó truyền các toạ độ đó vào 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 chương trình đổ bóng theo mảnh để tra cứu màu sắc từ hoạ tiết.

<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 nên hình ảnh tải không đồng bộ, vì vậy, 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, chúng ta sẽ vẽ hình ảnh đó.

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 quá thú vị, vì vậy hãy điều chỉnh hình ảnh đó. Bạn chỉ cần hoán đổi màu đỏ và màu xanh dương thì sao?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

Điều gì sẽ xảy ra nếu chúng ta muốn xử lý hình ảnh thực sự nhìn vào các pixel khác? Vì WebGL tham chiếu đến hoạ tiết trong toạ độ hoạ tiết từ 0 đến 1, nên chúng ta có thể tính toán lượng di chuyển cho 1 pixel bằng toán học đơn giản onePixel = 1.0 / textureSize. Dưới đây là một 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, chúng ta đã biết cách tham chiếu các pixel khác, hãy sử dụng hạt nhân tích chập để thực hiện một loạt các hoạt động 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ị số lần 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ả các giá trị trong hạt nhân) hoặc 1.0, giá trị nào lớn hơn. Đây là một bài viết khá hữu ích 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 theo cách thủ công bằng C++. Trong trường hợp của chúng ta, 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 bài viết 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ẽ trình bày cách áp dụng nhiều hiệu ứng cho hình ảnh.

Các tiền tố a, u và v_ của các biến trong GLSL có nghĩa là gì?

Đó chỉ là một 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 thông số đồng nhất là dữ liệu đầu vào cho chương trình đổ bóng, v_ cho các thông số biến đổi là các giá trị được truyền từ chương trình đổ bóng đỉnh đến 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 tiếp theo rõ ràng nhất 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 một cách nhanh chóng. Cung cấp một giao diện người dùng cho phép người dùng chọn các hiệu ứng họ muốn sử dụng, sau đó tạo một chương trình đổ bóng thực hiện tất cả các hiệu ứng. Điều đó không phải lúc nào cũng khả thi mặc dù kỹ thuật đó 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 hoạ tiết và kết xuất lần lượt cho từng hoạ tiết, ping pong 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 hình. Trong WebGL và OpenGL, Framebuffer thực sự là một cái tên không phù hợp. Vùng đệm khung hình WebGL/OpenGL thực sự chỉ là một tập hợp trạng thái chứ không phải là vùng đệm bất kỳ. Tuy nhiên, bằng cách đính kèm một hoạ tiết vào vùng đệm khung hình, chúng ta có thể kết xuất vào hoạ tiết đó. Trước tiên, hãy chuyển mã tạo hoạ tiết 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);

Và bây giờ, hãy sử dụng chức năng đó để tạo thêm 2 texture và đính kèm chúng vào 2 bộ đệ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 hạt nhân rồi tạo 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 loại, ping ponging mà chúng ta đang kết xuất kết cấu

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

Tôi cần nói qua một số điều.

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 hình. WebGL phải chuyển đổi từ không gian cắt trở lại thành pixel. Phương thức này thực hiện việc này 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 nên canvas mà chúng ta cần đặt khung nhìn thích hợp. Cuối cùng, trong các ví dụ cơ bản về WebGL, chúng ta đã lật toạ độ Y khi kết xuất vì WebGL hiển thị canvas với 0,0 ở góc dưới cùng bên trái thay vì truyền thống hơn cho 2D trên cùng bên trái. Bạn không cần làm điều đó khi kết xuất vào vùng đệm khung hình. Vì vùng đệm khung hình không bao giờ hiển thị, nên phần nào là trên cùng và phần nào là dưới cùng là không liên quan. Điều quan trọng là pixel 0,0 trong vùng đệm khung hình tương ứng với 0,0 trong phép tính của chúng ta. Để giải quyết 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 đầu vào khác 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ữ ví dụ này đơn giản bằng cách sử dụng một chương trình GLSL có thể đạt được nhiều hiệu ứng. Nếu muốn xử lý toàn bộ hình ảnh, có thể bạn cần nhiều chương trình GLSL. Một chương trình để điều chỉnh màu sắc, độ bão hoà và độ chói. Một thông số khác cho độ sáng và độ tương phản. Một mã để đảo ngược, một mã khác để điều chỉnh cấp độ, v.v. Bạn cần thay đổi mã để chuyển đổi các 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 tốt nhất là bạn nên tự thực hiện bài tập này vì nhiều chương trình GLSL, mỗi chương trình có nhu cầu tham số riêng, có thể sẽ cần phải tái cấu trúc một số phần lớn để tránh việc tất cả 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 bạn dễ tiếp cận WebGL hơn một chút. Tôi cũng hy vọng việc bắt đầu với 2D sẽ giúp bạn dễ hiểu WebGL 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ư chi tiết hơn về những gì WebGL thực sự đang làm nâng cao.

WebGL và Alpha

Tôi nhận thấy một số nhà phát triển OpenGL gặp vấn đề với cách WebGL xử lý alpha trong vùng đệm sau (tức là canvas). Vì vậy, tôi nghĩ rằng bạn nên tìm hiểu một số điểm khác biệt giữa WebGL và OpenGL liên quan đến alpha.

Điểm khác biệt lớn nhất giữa OpenGL và WebGL là OpenGL kết xuất vào vùng đệm sao lưu không được kết hợp với bất kỳ nội dung nào hoặc không được trình quản lý cửa sổ của hệ điều hành kết hợp hiệu quả với bất kỳ nội dung nào, vì vậy, giá trị 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 nhân trước giống như 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 với alpha không 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 cuối cùng 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, màu CSS xác định cho khu vực đó của trang web. Một cách hay để tìm hiểu 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 thành màu đen để ẩn mọi vấn đề về alpha.

#2) Cho WebGL biết bạn không muốn 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 sao lưu 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à lớp phủ này 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ì lớp phủ này có thể không hoạt động. Tôi biết có rất ít ứng dụng sử dụng alpha trong vùng đệm sau. Tôi cho rằng đây phải là chế độ mặc định.

3) Xoá giá trị 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);

Việc xoá thường rất nhanh vì có một trường hợp đặc biệt cho việc 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 thức #2 ở trên. Có lẽ tôi sẽ làm việc đó ngay sau khi đăng bài này. Có vẻ như hầu hết các thư viện WebGL đều sử dụng phương thức này theo mặc định. Một số ít nhà phát triển đang thực sự sử dụng alpha để kết hợp hiệu ứng có thể yêu cầu điều này. Phần còn lại sẽ nhận được hiệu suất tốt nhất và ít gây bất ngờ nhất.

#4) Xoá alpha một lần rồi không hiển thị với alpha đó 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 hình của riêng mình, bạn có thể cần bật lại chế độ kết xuất thành alpha rồi tắt lại khi chuyển sang kết xuất vào 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ì mặc định là alpha của chúng đượ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 cho WebGL biết bằng cách

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Sử dụng phương trình kết hợp hoạt động với alpha 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 hoạt động đối với 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 các hoạ tiết alpha được nhân trước, thì bạn có thể 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 nhiều hơn, vui lòng đăng chúng ở bên dưới.