WebGL 正投影 3D

Gregg Tavares
Gregg Tavares

WebGL 正面図 3D

この投稿は、WebGL に関する一連の投稿の続きです。最初は Fundamentals から始まり、前者は 2 次元行列に関する 2 次元行列でした。まだ読んでいない場合は、最初にお読みください。 前回の投稿では、2 次元行列の仕組みについて説明しました。移動、回転、スケーリングのほか、ピクセルからクリップ空間への投影まで、すべて 1 つの行列といくつかのマジック マトリックス計算で行うことができます。3D を制作するのは、ほんの一歩です。前の 2 次元の例では、2 次元の点 (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 の

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 つの回転関数が追加されました。実際には Z 軸を中心に回転するので 2D で必要なのは 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 ピクセルから高さピクセルまでになり、深さ - 深さ / 2 から + 深さ / 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 は、それぞれ 2 つの三角形を含む 3 つの長方形で構成されています。3D にするには 合計 16 個の長方形が必要ですその数は膨大です。 16 個の長方形 × 1 つの長方形につき 2 つの三角形 × 1 つの三角形 1 つにつき 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);

これはプログラムの開始時に一度だけ行いますこの機能を有効にすると、WebGL で逆の三角形をデフォルトで「カリング」するようになります。この場合の「Culling」は「描画しない」を表す専門的な言葉です。WebGL に関しては、三角形が時計回りと反時計回りのどちらを向いているかは、クリップ空間内の三角形の頂点によって決まります。つまり、頂点シェーダーの頂点に数式を適用した後、WebGL は三角形が前面か背面かを判断します。つまり、たとえば、X を -1 スケールした時計回りの三角形は反時計回りの三角形になり、X 軸または Y 軸を中心に 180 度回転した時計回りの三角形は反時計回りの三角形になります。CULL_FACE が無効になっているため、時計回り(正面)と反時計回り(背面)の両方の三角形が表示されます。この設定をオンにすると、スケーリングや回転などにより、正面を向いた三角形が反転した場合、WebGL は描画されなくなります。これは、3D で何かを回転させるとき、一般的にどの三角形が正面を向いているかを正面とみなす必要があるためです。

ねえ!すべての三角形はどこにありますか?その多くは間違った方向に 直面していることがわかりました回転させると反対側を見ると 見えますこの問題は簡単に修正できます。どの頂点が後方にあるかを見て 2 つの頂点を交換しますたとえば 逆三角形が

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

最後の 2 つの頂点を反転して先に進みます。

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

もっと近くなりますが、まだ問題が 1 つあります。すべての三角形が正しい方向を向いていて、後ろを向いている三角形が除去されていても、後ろにあるべき三角形が、前にあるべき三角形の上に描画されているところもあります。DEPTH BUFFER を入力します。 Depth バッファ(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 に設定されます。これは、行列計算が機能するために必要な値です。