WebGL 基礎知識

Gregg Tavares
Gregg Tavares

WebGL 能讓您在瀏覽器中顯示令人驚豔的即時 3D 圖形,但許多人不瞭解 WebGL 實際上是 2D API,而非 3D API。讓我解釋一下

WebGL 只會處理 2 件事。2D 中的剪輯區座標和顏色。使用 WebGL 的程式設計師必須為 WebGL 提供這兩項資訊。您可以提供 2 個「著色器」來執行這項操作。頂點著色器提供剪輯空間座標,而片段著色器提供顏色。無論畫布的大小為何,剪輯空間座標一律會從 -1 到 +1。以下是簡單的 WebGL 範例,可顯示 WebGL 最簡單的形式。

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

以下是 2 個著色器

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

同樣地,無論畫布的大小為何,剪輯空間座標一律會從 -1 到 +1。在上述情況中,您可以看到我們只會直接傳遞位置資料。由於位置資料已在剪輯空間中,因此無需執行任何作業。如果您想要 3D 圖形,請自行提供從 3D 轉換為 2D 的著色器,因為 WebGL 是 2D API!對於 2D 內容,您可能會傾向以像素而非剪輯區域進行作業,因此請變更著色器,以便我們提供以像素為單位的矩形,並將其轉換為剪輯區域。以下是新的頂點著色器

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

我們現在可以將資料從剪輯區域變更為像素

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

您可能會發現矩形位於該區域的底部附近。WebGL 會將左下角視為 0,0。為了讓它成為 2D 圖形 API 使用的較傳統的左上角,我們只需翻轉 y 座標即可。

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

我們將定義矩形的程式碼做為函式,以便針對不同大小的矩形呼叫該函式。在這個階段,我們會變更可設定的顏色。 首先,我們讓片段著色器採用顏色統一輸入。

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

以下是新的程式碼,可在隨機位置和隨機顏色中繪製 50 個矩形。

...

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

希望您能發現 WebGL 其實是相當簡單的 API。雖然 3D 處理作業可能會變得更複雜,但您 (程式設計師) 會以更複雜的著色器形式加入這些複雜性。WebGL API 本身是 2D,且相當簡單。

type="x-shader/x-vertex" 和 type="x-shader/x-fragment" 分別代表什麼意思?

<script> 標記預設會包含 JavaScript。您可以不指定類型,也可以指定 type="javascript"type="text/javascript",瀏覽器會將內容解讀為 JavaScript。如果您放入其他內容,瀏覽器會忽略指令碼標記的內容。

我們可以使用這項功能,在指令碼標記中儲存著色器。更棒的是,我們可以自行建立類型,並在 JavaScript 中尋找該類型,決定要將著色器編譯為頂點著色器還是片段著色器。

在此情況下,createShaderFromScriptElement 函式會尋找具有指定 id 的指令碼,然後查看 type 以決定要建立的著色器類型。

WebGL 圖像處理

WebGL 能輕鬆處理圖片。您覺得這有多簡單?請參閱下文。

如要在 WebGL 中繪製圖片,必須使用紋理。與 WebGL 在算繪時預期的裁剪空間座標 (而非像素) 類似,WebGL 在讀取紋理時會預期紋理座標。無論紋理的尺寸為何,紋理座標都會從 0.0 到 1.0。由於我們只繪製單一矩形 (或許是 2 個三角形),因此需要告訴 WebGL 矩形中每個點對應的紋理位置。我們會將這項資訊從頂點著色器傳送至片段著色器,方法是使用名為「varying」的特殊變數。之所以稱為變數,是因為它會變動。WebGL 會在使用片段著色器繪製每個像素時,對頂點著色器中提供的值進行內插。使用上一節結尾的頂點著色器,我們需要新增屬性來傳入紋理座標,然後將這些傳遞給片段著色器。

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

接著,我們會提供片段著色器,以便從紋理中查詢顏色。

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

最後,我們需要載入圖片、建立紋理,並將圖片複製到紋理中。由於我們是在瀏覽器中以非同步方式載入圖片,因此必須稍微重新排列程式碼,等待紋理載入。載入後,我們將繪製該圖片

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

這並不是太令人興奮的內容,因此我們來修改圖片。那麼換成紅色和藍色如何?

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

如果我們想進行圖像處理,實際查看其他像素,該怎麼做?由於 WebGL 會在紋理座標中參照紋理,而紋理座標的範圍為 0.0 到 1.0,因此我們可以使用簡單的數學 onePixel = 1.0 / textureSize 計算 1 個像素的移動量。以下是片段著色器,可計算紋理中每個像素的左側和右側像素平均值。

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

接著,我們需要從 JavaScript 傳入紋理的大小。

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

現在我們已知道如何參照其他像素,接著要使用卷積核心進行大量圖片處理作業。本例將使用 3x3 核心卷積核只是 3x3 矩陣,矩陣中的每個項目代表要將要算繪像素周圍的 8 個像素乘以多少。接著,我們會將結果除以核函式權重 (核函式中所有值的總和) 或 1.0 (兩者取較大者)。請參閱這篇實用文章這裡還有另一篇文章,說明如何在 C++ 中手動編寫此程式碼。在本例中,我們會在著色器中執行這項作業,因此請參閱新的片段著色器。

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

在 JavaScript 中,我們需要提供卷積核。

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

希望這篇文章能讓您瞭解 WebGL 中的圖像處理作業相當簡單。接下來,我將說明如何為圖片套用多個效果。

GLSL 中變數前置詞 a、u 和 v_ 的含義為何?

這只是命名慣例a_ 適用於緩衝區提供的資料。u_ 是著色器的輸入值,v_ 是從頂點著色器傳遞至片段著色器的值,並在繪製每個像素的頂點之間進行插補 (或變化)。

套用多種效果

接下來,最明顯的圖片處理問題就是如何套用多種效果?

您可以嘗試即時產生著色器。提供使用者介面,讓使用者選取要使用的效果,然後產生可執行所有效果的著色器。雖然這項技術通常用於為即時動畫製作效果,但不一定能達到預期效果。更靈活的方法是使用 2 個額外的紋理,並依序算繪至每個紋理,來回彈跳,並在每次應用下一個效果。

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

為此,我們需要建立 framebuffer。在 WebGL 和 OpenGL 中,Framebuffer 其實是一個不恰當的名稱。WebGL/OpenGL Framebuffer 其實只是狀態集合,並非任何類型的緩衝區。不過,只要將紋理附加至框架緩衝區,我們就能在該紋理中進行轉譯。首先,我們要將舊的紋理建立程式碼轉換為函式

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

接下來,我們將使用該函式製作另外 2 個紋理,並將其附加至 2 個 framebuffer。

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

現在,我們來建立一組核心,然後列出要套用的核心。

// 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"
];

最後,讓我們套用每個紋理,並 ping pong 我們要算繪的紋理

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

我要講解一些事情。

使用 null 呼叫 gl.bindFramebuffer,可讓 WebGL 知道您要將圖像算繪至畫布,而非其中一個 framebuffer。WebGL 必須從剪輯區轉換回像素。這項作業會根據 gl.viewport 的設定進行。初始化 WebGL 時,gl.viewport 的設定預設為畫布的大小。由於我們要轉譯的 framebuffer 大小與畫布不同,因此我們需要適當設定檢視區域。最後,在 WebGL 基礎範例中,我們在算繪時翻轉 Y 座標,因為 WebGL 會在顯示畫布的左下角時切換 Y 座標,而非傳統的 2D 左上方較為傳統。不過,如果要轉譯成 framebuffer,就不需要這麼做。由於 framebuffer 從未顯示,因此頂端和底部的位置並不重要。重要的是,在計算中,幀緩衝區中的 0,0 像素會對應至 0,0。為解決這個問題,我新增了一個輸入至著色器,讓您可以設定是否要翻轉。

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

然後在使用 Renderer 時設定

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

為了讓這個範例簡單易懂,我使用了可產生多種效果的單一 GLSL 程式。如果您想進行完整的圖片處理作業,可能需要許多 GLSL 程式。調整色調、飽和度與亮度調整程式。另一種設定亮度和對比。一個用於反轉,另一個用於調整音量等。您需要變更程式碼,才能切換 GLSL 程式並更新該特定程式的參數。我考慮要寫這個範例,但這項練習最好交由讀者自行完成,因為多個 GLSL 程式各自有其參數需求,因此可能需要進行重大重構,才能避免程式變得雜亂無章可循。希望這篇文章和前面的範例能讓您更容易上手 WebGL,並且從 2D 開始學習,讓您更容易理解 WebGL。如果有時間,我會再寫幾篇文章,說明如何處理 3D 問題,以及 WebGL 在幕後實際執行的操作。

WebGL 和 Alpha

我發現部分 OpenGL 開發人員在 WebGL 處理後置緩衝區 (即畫布) 中的 alpha 時遇到問題,因此我想介紹 WebGL 和 OpenGL 在 alpha 方面的差異。

OpenGL 與 WebGL 的最大差異在於,OpenGL 會算繪到未與任何內容合併的後緩衝區,或實際上不會與 OS 的視窗管理員結合,因此不論 Alpha 值為何都不重要。WebGL 是由具有網頁的瀏覽器構成,而預設使用具有透明度和 2d 的 .png <img> 標記。WebGL 有幾種方式可以使此作業更接近 OpenGL。

#1) 告訴 WebGL 您想要以未經預乘 Alpha 值進行合成

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

預設值為 true。當然,結果仍會在網頁上合成,並使用畫布下方的背景顏色 (畫布的背景顏色、畫布的容器背景顏色、網頁的背景顏色、畫布後方的內容 (如果畫布有 z-index > 0) 等),也就是 CSS 為網頁該區域定義的顏色。建議您將畫布的背景設為紅色 (例如紅色),找出是否發生 Alpha 問題。你會立即看到發生什麼事。

<canvas style="background: red;"></canvas>

您也可以將其設為黑色,這樣就能隱藏所有 alpha 問題。

#2) 告訴 WebGL 您不想在後端緩衝區中使用 alpha

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

由於背景緩衝區只有 RGB,這會使它看起來更像 OpenGL。這可能是最佳選項,因為優質瀏覽器可以看到您沒有 alpha,並實際最佳化 WebGL 的組合方式。當然,這也表示它實際上不會在後端緩衝區中顯示 alpha 值,因此如果您在後端緩衝區中使用 alpha 值,可能無法達到預期效果。據我所知,只有少數應用程式會在後端緩衝區使用 alpha 值。我認為這應該是預設值。

#3) 在轉譯結束時清除 alpha

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

清除作業通常非常快速,因為大多數硬體都有專屬的清除作業。我在大多數的示範中都這麼做。如果我很聰明,我會改用上述方法 2。或許我會在發布這則訊息後立即採取行動。大部分的 WebGL 程式庫應該都預設使用此方法。只有少數開發人員會使用 Alpha 來合成特效,因此只有這類開發人員可以提出申請。其餘的則會獲得最佳效能,並減少意外情況。

#4) 清除 alpha 一次,然後不再對其算繪

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

當然,如果您是向自己的 framebuffer 算繪,可能就需要重新開啟算繪 Alpha 版,然後在切換至畫布模式時再次關閉算繪功能。

#5) 處理圖片

此外,如果您要將含有 Alpha 的 PNG 檔案載入紋理,預設情況下,系統會預先乘上 Alpha,但這通常不是大多數遊戲的做法。如要避免這種行為,您需要透過以下方式告知 WebGL:

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) 使用可與預先相乘的 alpha 運算的混合方程式

我編寫或處理過的幾乎所有 OpenGL 應用程式都使用

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

這適用於未預先相乘的 Alpha 紋理。如果您實際上想使用預先相乘的 Alpha 紋理,那麼您可能會想要

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

這些是我知道的方法。如果你知道更多資訊,請在下方貼文。