WebGL の基礎

Gregg Tavares
Gregg Tavares

WebGL の基礎

WebGL により、ブラウザで素晴らしいリアルタイム 3D グラフィックスを表示できますが、WebGL が実際には 3D API ではなく 2D API であることはご存じない方も多いでしょう。説明しましょう。

WebGL で考慮されるのは 2 つのことのみです。2D とカラーのクリップスペース座標。WebGL を使用するプログラマーの仕事は、この 2 つで WebGL を提供することです。そのために「シェーダー」を 2 つ用意します。クリップ空間座標を提供する Vertex シェーダーと、色を提供するフラグメント シェーダー。キャンバスのサイズに関係なく、クリップスペース座標は常に -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 の範囲になります。上記の例では、位置データを直接渡すだけです。位置データはすでにクリップ空間にあるため、何もする必要はありません。WebGL は 2D API であるため、3D が必要な場合は、3D から 2D に変換するシェーダーを用意する必要があります。 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 になります。ここでは 1 つの長方形(実際には 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_ は可変要素です。可変の値は、頂点シェーダーからフラグメント シェーダーに渡され、描画される各ピクセルの頂点間で補間(または可変)されます。

複数のエフェクトの適用

画像処理に関して次に最も明白な疑問は、複数のエフェクトをどのように適用するかということです。

その場でシェーダーを生成することもできます。使用したいエフェクトをユーザーが選択できる UI を用意し、すべてのエフェクトを適用するシェーダーを生成する。必ずしも可能とは限りませんが、この手法はリアルタイム グラフィックのエフェクトを作成する際によく使用されます。より柔軟な方法は、さらに 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

そのためには、フレームバッファを作成する必要があります。WebGL と OpenGL では、フレームバッファは実際には不適切な名前です。WebGL/OpenGL フレームバッファは、実際には単なる状態の集合体であり、実際にはいかなる種類のバッファでもありません。しかし、テクスチャをフレームバッファにアタッチすれば、そのテクスチャにレンダリングできます。 まず、古いテクスチャ作成コードを関数に変換しましょう。

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 つのフレームバッファにアタッチします。

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

最後に、一つずつ適用して、レンダリングするテクスチャをピンポン操作してみましょう。

// 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 に指示できます。WebGL では、クリップ空間からピクセルに変換する必要があります。これは、gl.viewport の設定に基づいて行われます。gl.viewport の設定は、WebGL を初期化する際にキャンバスのサイズにデフォルトで設定されます。レンダリング先のフレームバッファはキャンバスのサイズとは異なるため、ビューポートを適切に設定する必要があります。最後に、WebGL の基礎的な例では、レンダリング時に Y 座標を反転させました。これは、従来の 2D の左上隅ではなく、0,0 が左下隅のキャンバスを表示するためです。フレームバッファにレンダリングする場合は必要ありません。フレームバッファは表示されないため、上下のどちらの部分がどちらであるかは関係ありません。重要なのは、今回の計算でフレームバッファ内のピクセル 0,0 が 0,0 に対応することだけです。これに対処するために、シェーダーにもう 1 つ入力を追加することで、反転するかどうかを設定できるようにしました。

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

そして、画像を使ってレンダリングするときに設定できます。

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

今回は、複数の効果を実現できる単一の GLSL プログラムを使用することで、この例をシンプルにしました。画像処理をすべて行いたい場合は、おそらく多くの GLSL プログラムが必要になるでしょう。色相、彩度、輝度の調整プログラム。明るさとコントラストを調節します。1 つは反転用、もう 1 つはレベルの調整用などです。GLSL プログラムを切り替えて、その特定のプログラムのパラメータを更新するには、コードを変更する必要があります。その例を書くことも検討しましたが、これは読み手に任せるのが最善です。なぜなら、それぞれに独自のパラメータを必要とする複数の GLSL プログラムは、おそらく、すべてが散らかった問題にならないように、大規模なリファクタリングを行う必要があるからです。 今回の例と上記の例で WebGL がもう少し身近に感じられたら幸いです。2D から始めることで、WebGL の理解が少しでも簡単になれば幸いです。もしよろしければ、3D の作成方法や WebGL の仕組みについてさらに記事をいくつか投稿していきたいと思います。

WebGL とアルファ版

OpenGL デベロッパーが、WebGL がバックバッファ(キャンバス)内のアルファを処理する方法に問題があることに気づいたので、アルファに関連する WebGL と OpenGL の違いをいくつか確認することをおすすめします。

OpenGL と WebGL の最大の違いは、OpenGL は何も合成されていないバックバッファにレンダリングされること、または OS のウィンドウ マネージャーによって実質的に合成されていないバックバッファにレンダリングされることです。そのため、アルファが何であるかは関係ありません。WebGL はブラウザによってウェブページに合成されます。デフォルトでは、透明度と 2d タグのある .png <img> タグと同様に、乗算済みのアルファが使用されます。WebGL に似た処理を行う WebGL には、いくつかの方法があります。

#1) 乗算されていないアルファで合成するよう WebGL に指示する

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

デフォルトは true です。もちろん、結果はキャンバスの下に配置される背景色(キャンバスの背景色、キャンバスのコンテナの背景色、ページの背景色、キャンバスの背後にあるもの(キャンバスの Z-Index が 0 より大きい場合など)など)を使用してページ上に合成されます。つまり、ウェブページのその領域の色を CSS で定義します。 アルファの問題があるかどうかを確認する方法は、キャンバスの背景を赤などの明るい色に設定することです。状況がすぐにわかります。

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

黒に設定すると、アルファの問題がすべて非表示になります。

#2) バックバッファにアルファを適用したくない WebGL に指示する

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

これにより、バックバッファは RGB のみを持つため、OpenGL のように動作します。優れたブラウザであればアルファ値がないと認識し、WebGL の合成方法を実際に最適化できるため、おそらくこれが最善のオプションです。もちろん、これは実際にはバックバッファに alpha がないことを意味します。そのため、なんらかの目的でバックバッファで alpha を使用している場合は、うまくいかない可能性があります。私が知っているアプリはほとんどありませんが、バックバッファでアルファ版を使用しています。おそらくこれがデフォルトだったと思います。

#3)レンダリングの終了時にアルファをクリアする

..
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 ライブラリで、この方法がデフォルトになっているようです。コンポジション エフェクトにアルファ版を使用している少数のデベロッパーは、この機能を求めることができます。それ以外は最高のパフォーマンスを発揮し、予想外のことを最小限に抑えます。

#4)アルファを 1 回クリアして、もうレンダリングしない

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

もちろん、独自のフレームバッファにレンダリングする場合は、レンダリングをアルファに戻してから、キャンバスへのレンダリングに切り替えたときに再びオフにする必要があります。

#5)画像の処理

また、アルファを含む PNG ファイルをテクスチャに読み込む場合、デフォルトではアルファが事前に乗算されます。これは通常、ほとんどのゲームが行う方法ではありません。この動作を防ぐには、WebGL に

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6)事前に乗算したアルファで機能するブレンド方程式を使用する

私が書いた、または手がけた OpenGL アプリはほぼすべて

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

これは、事前に乗算されていないアルファ テクスチャで機能します。 乗算済みのアルファ テクスチャを実際に操作する場合は、

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

私が把握している方法は以上です。他にもご存じの方は、以下に投稿してください。