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

Gregg Tavares
Gregg Tavares

Nguyên tắc cơ bản về WebGL

WebGL giúp bạn có thể hiển thị đồ họa 3D theo thời gian thực tuyệt vời trong trình duyệt của mình nhưng điều nhiều người không biết là WebGL thực sự là 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ạ độ của Clipspace ở 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 WebGL với 2 yếu tố đó. Bạn cung cấp 2 "trình đổ bóng" để thực hiện việc này. Chương trình đổ bóng Vertex cung cấp toạ độ của không gian cắt và chương trình đổ bóng mảnh cung cấp màu sắc. Toạ độ của Clipspace luôn có giá trị từ -1 đến +1 bất kể kích thước canvas của bạn là gì. Dưới đây là một ví dụ đơn giản về WebGL 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ạ độ clipspace luôn từ -1 đến +1 bất kể kích thước 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í đã có trong không gian đoạn video nên bạn không cần làm gì cả. Nếu muốn ở chế độ 3D, bạn có thể cung cấp 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, có thể bạn sẽ thích làm việc theo pixel hơn là không gian cắt (clipspace), 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à để nó chuyển đổi thành không gian cắt. Đây là chương trình đổ bóng đỉnh (vertex) 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>

Giờ đây, chúng ta có thể thay đổi dữ liệu từ không gian đoạn video 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ể thấy hình chữ nhật nằm gần cuối vùng đó. WebGL coi góc dưới cùng bên trái là 0,0. Để biến nó thành góc trên cùng bên trái truyền thống hơn được sử dụng cho API đồ hoạ 2d, chúng ta chỉ cần lật toạ độ y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Hãy tạo mã xác định hình chữ nhật thành một hàm để có thể gọi 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ẽ thiết lập màu sắc. Trước tiên, chúng ta làm cho chương trình đổ bóng mảnh nhận 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>

Đây là mã mới vẽ 50 hình chữ nhật tại các vị trí ngẫu nhiên và màu 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 thực hiện 3D có thể phức tạp hơn, nhưng chức năng này do bạn (tức là 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 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ì?

Theo mặc định, các thẻ <script> có JavaScript. Bạn không cần đặt loại nào hoặc có thể đặt type="javascript" hoặc type="text/javascript" và trình duyệt sẽ hiểu nội dung dưới dạng JavaScript. Nếu bạn đặt thẻ 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. Hơn nữa, chúng ta có thể tạo kiểu của riêng mình và trong javascript tìm điều đó để 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 type để quyết định loại chương trình đổ bóng cần tạo.

Xử lý hình ảnh WebGL

Xử lý hình ảnh dễ dàng trong WebGL. Dễ dàng đến mức nào? Hãy đọc thông tin dưới đây.

Để 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 dự kiến sẽ có toạ độ không gian đoạn video khi kết xuất thay vì điểm ảnh, WebGL dự kiến sẽ có toạ độ kết cấu khi đọc kết cấu. Toạ độ texture từ 0,0 đến 1,0 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 duy nhất (tốt, 2 hình tam giác), nên chúng ta cần cho WebGL biết vị trí nào 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 tới chương trình đổ bóng mảnh bằng cách sử dụng một loại biến đặc biệt được gọi là "varying". Nó được gọi là biến đổi vì nó thay đổi. WebGL sẽ nội suy các giá trị chúng tôi cung cấp trong chương trình đổ bóng đỉnh khi thiết bị này 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 toạ độ 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 mảnh để tra cứu màu sắc trên 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 hình ảnh, tạo hoạ tiết và sao chép hình ảnh đó vào hoạ tiết. Vì chúng ta đang tải hình ảnh của trình duyệt không đồng bộ nên chúng ta phải sắp xếp lại mã một chút để chờ hoạ tiết 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 quá thú vị, vì vậy hãy chỉnh sửa hình ảnh đó. Bạn chỉ cần hoán đổi màu đỏ và màu xanh lam 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 kết cấu trong toạ độ kết cấu từ 0,0 đến 1,0, sau đó chúng ta có thể tính toán được lượng dịch 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 số 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 chuyển vào kích thước của hoạ tiết (texture) từ JavaScript.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Bây giờ, khi đã 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 thao tác xử lý hình ảnh phổ biến. Trong trường hợp này, chúng ta sẽ sử dụng nhân 3x3. Hạt nhân tích chập chỉ là ma trận 3x3 trong đó mỗi mục nhập trong ma trận đại diện cho số lượng nhân 8 pixel xung quanh pixel 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. Mời bạn xem một bài viết khá hay về vấn đề đó. Đâ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 thủ công 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, sau đâ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 việc xử lý hình ảnh trong WebGL khá đơn giản. Tiếp theo, tôi sẽ tìm hiểu cách áp dụng nhiều hiệu ứng cho hình ảnh.

Các tiền tố a, u và v_ trong các biến trong GLSL bằng cách nào?

Đó 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 nhất là dữ liệu đầu vào cho chương trình đổ bóng, v_ để thay đổi (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à nội suy (hoặc thay đổi) giữa các đỉnh của mỗi pixel được vẽ.

Áp dụng nhiều hiệu ứng

Câu hỏi rõ ràng nhất 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 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 hiệu ứng họ muốn sử dụng, sau đó tạo một chương trình đổ bóng có thể thực hiện tất cả cá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 đó 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 nữa và kết xuất cho mỗi hoạ tiết lần lượt, đổ bóng 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 điều 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 thực sự là vùng đệm thuộc bất kỳ loại nào. Tuy nhiên, bằng cách gắn một hoạ tiết vào vùng đệm khung hình, chúng ta có thể kết xuất hoạ tiết vào đó. 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);

Bây giờ, hãy sử dụng chức năng đó để tạo thêm 2 kết cấu 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, sau đó 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, đổ bóng lên 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);
}

Một vài điều tôi nên xem lại.

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

WebGL và Alpha

Tôi 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 (tức là canvas), vì vậy tôi nghĩ có thể sẽ tốt hơn nếu bạn nên xem lại 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à ở chỗ OpenGL kết xuất vào một vùng đệm không được kết hợp với bất cứ thứ gì, hoặc thực sự không được kết hợp với bất cứ thứ gì bởi trình quản lý cửa sổ của hệ điều hành, do đó không quan trọng là alpha của bạn là gì. WebGL được trình duyệt tổng hợp với trang web và mặc định là sử dụng alpha được nhân trước giống như thẻ .png <img> có độ trong suốt và thẻ canvas 2d. WebGL có một số cách để làm cho điều này giống với OpenGL.

1) Cho WebGL biết rằng bạn muốn kết hợp WebGL 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ả sẽ vẫn được kết hợp trên trang với bất kỳ màu nền nào xuất hiện 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ó z-index > 0, v.v. nói cách khác, CSS màu xác định cho khu vực đó của trang web. Để tìm hiểu xem bạn có gặp vấn đề về alpha hay không, hãy đặt nền của canvas thành màu sáng như đỏ. Bạn sẽ ngay lập tức thấy điều 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 mà bạn gặp phải.

2) Nói với WebGL rằng bạn không muốn alpha trong phần đệm ngược

gl = canvas.getContext("experimental-webgl", {alpha: false});

Thao tác này sẽ giúp ứng dụng hoạt động giống như OpenGL hơn vì bộ đệm chỉ có RGB. Đây có lẽ 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à nó thực sự sẽ không có alpha trong vùng đệm, vì vậy nếu bạn đang sử dụng alpha trong vùng đệm cho một mục đích nào đó có thể không phù hợp với bạn. Một số ứng dụng mà tôi biết có sử dụng alpha trong vùng đệm đệm. Tôi nghĩ đây đúng ra là lựa chọn 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 rất nhanh vì có một trường hợp đặc biệt cho yêu cầu 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ạ. Nếu thông minh, tôi sẽ chuyển sang phương pháp 2 ở trên. Có thể tôi sẽ làm việc đó ngay sau khi đăng nội dung này. Có vẻ như hầu hết các thư viện WebGL nên mặc định dùng phương thức này. Một số ít nhà phát triển đang thực sự sử dụng alpha để kết hợp các hiệu ứng có thể yêu cầu chức năng này. Phần còn lại sẽ chỉ có hiệu suất tốt nhất và ít bất ngờ nhất.

4) Xóa alpha một lần, sau đó không hiển thị 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 của riêng mình, bạn có thể cần bật lại tính năng kết xuất về alpha rồi tắt tính năng này khi chuyển sang kết xuất canvas.

5) Xử lý hình ảnh

Ngoài ra, nếu bạn đang tải tệp PNG có alpha thành hoạ tiết, thì giá trị mặc định là alpha của chúng được nhân trước. Điều này thường KHÔNG phải là cách mà 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 kết hợp phù hợp 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 sử dụng

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Cách này phù hợp với hoạ tiết alpha không được nhân trước. Nếu thực sự muốn xử lý hoạ tiết alpha được nhân trước, thì có lẽ bạn

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Đó là những phương pháp mà tôi biết. Nếu bạn biết thêm thông tin, vui lòng đăng thông tin đó ở bên dưới.