WebGL 正投影 3D

Gregg Tavares
Gregg Tavares

WebGL 正投影 3D

この記事は、WebGL に関する一連の投稿の続きです。最初の記事では基本から始め、前回の記事では 2D 行列について説明しました。まだご覧になっていない場合は、まずご覧ください。前回の投稿では、2 次元行列の仕組みについて説明しました。移動、回転、スケーリング、ピクセルからクリップ空間への投影まで、すべて 1 つのマトリックスとマトリックス演算で行うことができると説明しました。3D に進むのは、そこから一歩進むだけです。前の 2D の例では、3x3 行列に掛ける 2D ポイント(x、y)がありました。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 ~幅ピクセル、高さ 0 ~高さピクセルになりますが、奥行きは -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 つ問題があります。すべての三角形が正しい方向を向いていて、背面を向いている三角形が除外されているにもかかわらず、背面にあるはずの三角形が前面にあるはずの三角形の上に描画されている場所があります。DEPTH BUFFER を入力します。深度バッファ(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 になります。これは、行列演算を機能させるために必要な値です。