WebGL 正投影 3D

Gregg Tavares
Gregg Tavares

WebGL 正投影 3D

この投稿は、WebGL に関する一連の投稿の続きです。最初の記事は基本から始め、前回の記事は 2D 行列についてでした。まだお読みになっていない場合は、まずそちらをご覧ください。前回の投稿では、2D 行列の仕組みについて説明しました。移動、回転、スケーリング、さらにはピクセルからクリップ空間への投影も、1 つの行列といくつかの魔法のような行列演算ですべて実行できることを説明しました。3D にするには、そこからほんの少しのステップを踏むだけです。前述の 2D の例では、2D の点(x、y)に 3x3 の行列を乗算しました。3D を行うには、3D ポイント(x、y、z)と 4x4 マトリックスが必要です。最後の例を 3D に変更してみましょう。今回も「F」を使用しますが、3D の「F」です。まず、3D を処理するように頂点シェーダーを変更する必要があります。こちらが古いシェーダーです。

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

こちらが新しいものです

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

さらに簡単になりました。 次に、3D データを提供する必要があります。

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

次に、すべての行列関数を 2D から 3D に変更する必要があります。 以下は、makeTranslation、makeRotation、makeScale の 2D(変更前)バージョンです。

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

そして、こちらが更新された 3D バージョンです。

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

3 つの回転関数が追加されました。2D では、Z 軸を中心に回転するだけなので、1 つだけで済みました。3D を実現するには、X 軸と Y 軸を中心に回転できるようにする必要もあります。これらはすべてよく似ています。これらを計算すると、以前と同様に簡略化されます。

Z 回転

newX = x * c + y * s;
newY = x * -s + y * c;

Y 軸回転


newX = x * c + z * s;
newZ = x * -s + z * c;

X 軸回転

newY = y * c + z * s;
newZ = y * -s + z * c;

また、投影関数も更新する必要があります。こちらが古いものです

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

これはピクセルからクリップ空間に変換するものでした。3D に拡張する最初の試みとして、

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

x と y をピクセルからクリップ空間に変換する必要があったように、z についても同じことを行う必要があります。この例では、Z 空間のピクセル単位も作成しています。奥行きには width のような値を渡します。これにより、空間の幅は 0 ~ width ピクセル、高さは 0 ~ height ピクセルになりますが、奥行きは -depth / 2 ~+depth / 2 になります。最後に、行列を計算するコードを更新する必要があります。

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

最初の問題は、ジオメトリが平らな F であるため、3D が見えにくいことです。この問題を解決するには、ジオメトリを 3D に拡張します。現在の F は 3 つの長方形で構成され、それぞれに 2 つの三角形があります。3D にするには、合計 16 個の長方形が必要です。ここにすべてを列挙することはできません。16 個の長方形 × 長方形あたり 2 個の三角形 × 三角形あたり 3 個の頂点 = 96 個の頂点。すべてを確認するには、サンプルのソースを表示してください。より多くの頂点を描画する必要があるため、

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

スライダーを動かしても、3D であることがわかりにくい。各長方形を異なる色で塗りつぶしてみましょう。これを行うには、頂点シェーダーに別の属性を追加し、頂点シェーダーからフラグメント シェーダーに渡すように変更します。新しい頂点シェーダーは次のとおりです。

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

その色をフラグメント シェーダーで使用する必要があります。

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

// Passed in from the vertex shader.
varying vec4 v_color;

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

色を指定する場所を検索し、別のバッファと属性を設定して色を指定する必要があります。

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

おや、この散らかりようはなんだ?3D の「F」のさまざまな部分(前面、背面、側面など)は、ジオメトリに表示される順序で描画されます。これでは、前面の要素の後に背面の要素が描画されることがあるため、望ましい結果が得られません。WebGL の三角形には、表面と裏面の概念があります。前面の三角形の頂点は時計回りになります。背面三角形の頂点は反時計回りになります。

三角形の巻き。

WebGL には、前向きまたは後ろ向きの三角形のみを描画する機能があります。この機能は、

gl.enable(gl.CULL_FACE);

これは、プログラムの開始時に 1 回だけ行います。この機能を有効にすると、WebGL はデフォルトで背面三角形を「カリング」します。この場合の「カリング」は、「描画しない」ということを意味する専門用語です。WebGL に関する限り、三角形が時計回りか反時計回りかは、クリップ空間内のその三角形の頂点によって決まります。つまり、WebGL は、頂点シェーダーで頂点に数学を適用した後に、三角形が前面か背面かを判断します。たとえば、時計回りの三角形を X 軸方向に -1 倍すると反時計回りの三角形になり、時計回りの三角形を X 軸または Y 軸を中心に 180 度回転させると反時計回りの三角形になります。CULL_FACE が無効になっているため、時計回り(前面)と反時計回り(背面)の両方の三角形が表示されます。これを有効にすると、拡大縮小や回転などの理由で表向きの三角形が反転した場合、WebGL はそれを描画しません。これは、3D で何かを回転させるときに、自分に面している三角形を前面と見なすのが一般的であるため、良いことです。

ねえ!三角形はどこに消えたのでしょうか?その多くが間違った方向を向いていることがわかりました。回転させると、反対側に表示されます。幸い、この問題は簡単に解決できます。逆向きのものを探して、その頂点のうち 2 つを交換するだけです。たとえば、1 つの逆三角形が

1,   2,   3,
40,  50,  60,
700, 800, 900,

最後の 2 つの頂点を反転して前方にします。

1,   2,   3,
700, 800, 900,
40,  50,  60,

近づいてきましたが、まだ問題が 1 つあります。すべての三角形が正しい方向を向いており、背面を向いている三角形がカリングされている場合でも、背面にあるべき三角形が前面にあるべき三角形の上に描画されることがあります。深度バッファを入力します。深度バッファ(Z バッファとも呼ばれます)は、depth ピクセルの長方形で、画像の作成に使用されるカラーピクセルごとに 1 つの深度ピクセルがあります。WebGL は各カラーピクセルを描画する際に、深度ピクセルも描画できます。これは、頂点シェーダーから Z に返される値に基づいて行われます。X と Y をクリップ空間に変換する必要があったように、Z もクリップ空間(-1 ~+1)に変換する必要があります。この値は、深度空間の値(0 ~+1)に変換されます。WebGL は、カラーピクセルを描画する前に、対応する深度ピクセルを確認します。描画しようとしているピクセルの深度値が、対応する深度ピクセルの値よりも大きい場合、WebGL は新しいカラーピクセルを描画しません。それ以外の場合は、フラグメント シェーダーの色で新しいカラーピクセルを描画し、新しい深度値で深度ピクセルを描画します。つまり、他のピクセルの背後にあるピクセルは描画されません。この機能を有効にするには、

gl.enable(gl.DEPTH_TEST);

また、描画を開始する前に、深度バッファを 1.0 に戻す必要があります。

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

次の投稿では、遠近感を持たせる方法について説明します。

属性が vec4 なのに gl.vertexAttribPointer のサイズが 3 である理由

細部にこだわる方は、2 つの属性を次のように定義したことに気づいたかもしれません。

attribute vec4 a_position;
attribute vec4 a_color;

どちらも「vec4」ですが、WebGL にバッファからデータを取り出す方法を指示するときに、

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

各属性の「3」は、属性ごとに 3 つの値のみを抽出することを示します。これが機能するのは、頂点シェーダーで指定されていない属性に対して、WebGL がデフォルト値を提供するからです。デフォルトは 0、0、0、1 です。ここで、x = 0、y = 0、z = 0、w = 1 です。そのため、古い 2D 頂点シェーダーでは 1 を明示的に指定する必要がありました。x と y を渡して、z に 1 が必要でしたが、z のデフォルトは 0 であるため、明示的に 1 を指定する必要がありました。ただし、3D の場合、「w」を指定しなくても、デフォルトで 1 になります。これは、行列の計算を機能させるために必要な値です。