WebGL の基礎

Gregg Tavares
Gregg Tavares

WebGL を使用すると、ブラウザで驚くほどリアルタイムな 3D グラフィックを表示できますが、多くの人が知らないのは、WebGL は実際には 3D API ではなく 2D API であるということです。説明しましょう。

WebGL は 2 つの点にのみ関心があります。2D のクリップスペース座標と色。WebGL を使用するプログラマの仕事は、WebGL にこれらの 2 つを提供することです。そのために、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 の範囲になります。上記の例では、位置情報を直接渡す以外は何も行いません。位置データはすでにクリップ空間にあるため、処理は不要です。3D が必要な場合は、WebGL は 2D API であるため、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 がレンダリング時にピクセルではなくクリップスペース座標を想定するのと同様に、テクスチャの読み取り時にテクスチャ座標を想定します。テクスチャ座標は、テクスチャのサイズに関係なく 0.0 ~ 1.0 の範囲で指定します。ここでは 1 つの長方形(つまり 2 つの三角形)のみを描画するため、長方形の各点が対応するテクスチャ内の場所を WebGL に伝える必要があります。「変動」と呼ばれる特殊な変数を使用して、この情報を頂点シェーダーからフラグメント シェーダーに渡します。変動するため、可変変数と呼ばれます。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);
}

確認しておきたい点がいくつかあります。

nullgl.bindFramebuffer を呼び出すと、フレームバッファではなくキャンバスにレンダリングすることを WebGL に指示します。WebGL は、クリップスペースからピクセルに戻す必要があります。これは、gl.viewport の設定に基づいて行われます。gl.viewport の設定は、WebGL の初期化時にデフォルトでキャンバスのサイズに設定されます。レンダリング先のフレームバッファはキャンバスとはサイズが異なるため、ビューポートを適切に設定する必要があります。最後に、WebGL の基本例では、レンダリング時に Y 座標を反転しました。これは、WebGL ではキャンバスが 2D の従来の左上ではなく、0,0 が左下隅として表示されるからです。フレームバッファにレンダリングする場合は不要です。フレームバッファが表示されないため、上下のどの部分があっても関係ありません。重要なのは、フレームバッファのピクセル 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>

次に、

...
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 はブラウザによってウェブページと合成されます。デフォルトでは、透明性のある .png <img> タグと 2D キャンバス タグと同じように、事前乗算されたアルファを使用します。WebGL には、これを OpenGL に近づける方法がいくつかあります。

#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 の合成方法が実際に最適化されるため、これが最適なオプションである可能性があります。もちろん、バックバッファにアルファが実際にはないため、バックバッファでアルファをなんらかの目的のために使用している場合は、機能しない可能性があります。バックバッファでアルファを使用するアプリはほとんどありません。これがデフォルトであるべきだったという意見もあると思います。

#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)アルファを一度クリアしてから、それ以降はレンダリングしない

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

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