3Dグラフィックス:WebGLチュートリアル
公開: 2022-03-113Dグラフィックスの世界は、非常に恐ろしいものになる可能性があります。 インタラクティブな3Dロゴを作成する場合でも、本格的なゲームをデザインする場合でも、3Dレンダリングの原則がわからない場合は、多くのことを抽象化するライブラリを使用することに悩まされます。
ライブラリを使用することはちょうどいいツールでありえます、そしてJavaScriptはthree.jsの形で驚くべきオープンソースのものを持っています。 ただし、事前に作成されたソリューションを使用することにはいくつかの欠点があります。
- 彼らはあなたが使用する予定のない多くの機能を持つことができます。 縮小されたベースのthree.js機能のサイズは約500kBであり、追加機能(実際のモデルファイルのロードもその1つです)により、ペイロードがさらに大きくなります。 あなたのウェブサイトに回転するロゴを表示するためだけにそれだけのデータを転送するのは無駄です。
- 抽象化レイヤーを追加すると、他の方法では簡単な変更を行うのが難しくなる可能性があります。 画面上のオブジェクトをシェーディングするクリエイティブな方法は、実装が簡単な場合もあれば、ライブラリの抽象化に組み込むために数十時間の作業が必要な場合もあります。
- ライブラリはほとんどのシナリオで非常によく最適化されていますが、ユースケースに合わせて多くのベルやホイッスルを切り取ることができます。 レンダラーは、グラフィックカード上で特定のプロシージャを何百万回も実行させる可能性があります。 このような手順から削除されたすべての命令は、弱いグラフィックカードが問題なくコンテンツを処理できることを意味します。
高レベルのグラフィックライブラリを使用することにした場合でも、内部の知識を持っていると、より効果的に使用できます。 ライブラリには、 three.js
ShaderMaterial
の高度な機能を含めることもできます。 グラフィックレンダリングの原則を知っていると、そのような機能を使用できます。
私たちの目標は、3Dグラフィックスのレンダリングと、WebGLを使用したそれらの実装の背後にあるすべての重要な概念を簡単に紹介することです。 行われる最も一般的なことは、空のスペースで3Dオブジェクトを表示および移動することです。
最終的なコードは、フォークして試してみることができます。
3Dモデルの表現
最初に理解する必要があるのは、3Dモデルがどのように表現されるかです。 モデルは三角形のメッシュで構成されています。 各三角形は、三角形の各コーナーに対して3つの頂点で表されます。 頂点にアタッチされる最も一般的なプロパティは3つあります。
頂点位置
位置は、頂点の最も直感的なプロパティです。 これは、座標の3Dベクトルで表される、3D空間内の位置です。 空間内の3点の正確な座標がわかっている場合は、それらの間に単純な三角形を描くために必要なすべての情報が得られます。 レンダリング時にモデルを実際に見栄えよくするために、レンダラーに提供する必要のあるものがさらにいくつかあります。
頂点法線
上記の2つのモデルを検討してください。 それらは同じ頂点位置で構成されていますが、レンダリングするとまったく異なって見えます。 そんなことがあるものか?
頂点を配置する場所をレンダラーに指示するだけでなく、サーフェスがその正確な位置でどのように傾斜しているかについてのヒントを与えることもできます。 ヒントは、モデル上の特定のポイントでのサーフェスの法線の形式であり、3Dベクトルで表されます。 次の画像は、それがどのように処理されるかをより説明的に示しているはずです。
左と右のサーフェスは、それぞれ前の画像の左と右のボールに対応しています。 赤い矢印は頂点に指定された法線を表し、青い矢印は法線が頂点間のすべてのポイントをどのように探すかについてのレンダラーの計算を表します。 この画像は2D空間のデモンストレーションを示していますが、同じ原理が3Dにも当てはまります。
通常は、ライトが表面をどのように照らすかについてのヒントです。 光線の方向が法線に近いほど、ポイントは明るくなります。 法線方向に徐々に変化すると、光の勾配が発生しますが、間に変化がない急激な変化があると、サーフェス全体で一定の照明が発生し、それらの間の照明が突然変化します。
テクスチャ座標
最後の重要なプロパティは、一般にUVマッピングと呼ばれるテクスチャ座標です。 モデルと、それに適用するテクスチャがあります。 テクスチャにはさまざまな領域があり、モデルのさまざまな部分に適用する画像を表しています。 どの三角形をテクスチャのどの部分で表現するかをマークする方法が必要です。 そこで、テクスチャマッピングが登場します。
頂点ごとに、UとVの2つの座標をマークします。これらの座標はテクスチャ上の位置を表し、Uは水平軸を表し、Vは垂直軸を表します。 値はピクセル単位ではなく、画像内のパーセンテージ位置です。 画像の左下隅は2つのゼロで表され、右上隅は2つのゼロで表されます。
三角形は、三角形の各頂点のUV座標を取得し、それらの座標の間にキャプチャされたイメージをテクスチャに適用することによってペイントされます。
上の画像でUVマッピングのデモンストレーションを見ることができます。 球形モデルを取得し、2Dサーフェス上に平坦化するのに十分小さいパーツにカットしました。 カットが行われた縫い目は太い線でマークされています。 パッチの1つが強調表示されているので、状況がどのように一致しているかをよく確認できます。 また、笑顔の真ん中の縫い目が口の一部を2つの異なるパッチに配置する方法もわかります。
ワイヤーフレームはテクスチャの一部ではありませんが、画像の上にオーバーレイされるだけなので、物事がどのようにマッピングされるかを確認できます。
OBJモデルのロード
信じられないかもしれませんが、独自のシンプルなモデルローダーを作成するために知っておく必要があるのはこれだけです。 OBJファイル形式は、数行のコードでパーサーを実装するのに十分なほど単純です。
このファイルは、頂点の位置をv <float> <float> <float>
形式でリストし、オプションの4番目のfloatを使用して、単純にするために無視します。 頂点法線は、 vn <float> <float> <float>
で同様に表されます。 最後に、テクスチャ座標はvt <float> <float>
で表され、オプションの3番目のfloatは無視します。 3つのケースすべてで、フロートはそれぞれの座標を表します。 これらの3つのプロパティは、3つの配列に蓄積されます。
面は頂点のグループで表されます。 各頂点は各プロパティのインデックスで表され、インデックスは1から始まります。これを表す方法はさまざまですが、 f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
形式に固執します。 、3つのプロパティすべてを提供する必要があり、面ごとの頂点の数を3つに制限します。 これらの制限はすべて、ローダーを可能な限り単純に保つために行われています。他のすべてのオプションは、WebGLが好む形式になる前に、余分な些細な処理を必要とするためです。
ファイルローダーには多くの要件があります。 これは制限に聞こえるかもしれませんが、3Dモデリングアプリケーションでは、モデルをOBJファイルとしてエクスポートするときにこれらの制限を設定できる傾向があります。
次のコードは、OBJファイルを表す文字列を解析し、面の配列の形式でモデルを作成します。
function Geometry (faces) { this.faces = faces || [] } // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^v\s+([\d\.\+\-eE]+)\s+([\d\.\+\-eE]+)\s+([\d\.\+\-eE]+)/ var NORMAL = /^vn\s+([\d\.\+\-eE]+)\s+([\d\.\+\-eE]+)\s+([\d\.\+\-eE]+)/ var UV = /^vt\s+([\d\.\+\-eE]+)\s+([\d\.\+\-eE]+)/ var FACE = /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/ lines = src.split('\n') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i < 10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) { this.vertices = vertices || [] } function Vertex (position, normal, uv) { this.position = position || new Vector3() this.normal = normal || new Vector3() this.uv = uv || new Vector2() } function Vector3 (x, y, z) { this.x = Number(x) || 0 this.y = Number(y) || 0 this.z = Number(z) || 0 } function Vector2 (x, y) { this.x = Number(x) || 0 this.y = Number(y) || 0 }
Geometry
構造は、モデルをグラフィックカードに送信して処理するために必要な正確なデータを保持します。 ただし、その前に、モデルを画面上で移動できるようにする必要があります。
空間変換の実行
ロードしたモデルのすべてのポイントは、その座標系を基準にしています。 モデルを平行移動、回転、およびスケーリングする場合は、その座標系でその操作を実行するだけです。 座標系Bに対する座標系Aは、その中心の位置をベクトルp_ab
として定義し、その各軸のベクトルx_ab
、 y_ab
、およびz_ab
は、その軸の方向を表します。 したがって、点が座標系Aのx
軸上で10だけ移動すると、座標系Bでは、 x_ab
の方向に10を掛けて移動します。
この情報はすべて、次のマトリックス形式で保存されます。
x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1
3Dベクトルq
を変換する場合は、変換行列にベクトルを乗算するだけです。
qx qy qz 1
これにより、ポイントは新しいx
軸に沿ってqx
、新しいy
軸に沿ってqy
、新しいz
軸に沿ってqz
します。 最後に、ポイントがp
ベクトルによってさらに移動します。これが、乗算の最後の要素として1を使用する理由です。
これらの行列を使用する大きな利点は、頂点で実行する複数の変換がある場合、頂点自体を変換する前に、それらの行列を乗算することにより、それらを1つの変換にマージできることです。
実行できるさまざまな変換があり、主要な変換を見ていきます。
変換なし
変換が発生しない場合、 p
ベクトルはゼロベクトル、 x
ベクトルは[1, 0, 0]
、 y
は[0, 1, 0]
、 z
は[0, 0, 1]
です。 これ以降、これらの値をこれらのベクトルのデフォルト値と呼びます。 これらの値を適用すると、単位行列が得られます。
1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
これは、変換を連鎖させるための良い出発点です。
翻訳
平行移動を実行すると、 p
ベクトルを除くすべてのベクトルがデフォルト値になります。 これにより、次のマトリックスが作成されます。
1 0 0 px 0 1 0 py 0 0 1 pz 0 0 0 1
スケーリング
モデルのスケーリングとは、各座標がポイントの位置に寄与する量を減らすことを意味します。 スケーリングによる均一なオフセットはないため、 p
ベクトルはデフォルト値を維持します。 デフォルトの軸ベクトルにそれぞれのスケーリング係数を掛けると、次の行列になります。
s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1
ここで、 s_x
、 s_y
、およびs_z
は、各軸に適用されるスケーリングを表します。
回転
上の画像は、座標フレームをZ軸を中心に回転させたときに何が起こるかを示しています。
回転すると均一なオフセットが発生しないため、 p
ベクトルはデフォルト値を維持します。 今、物事は少しトリッキーになります。 回転により、元の座標系の特定の軸に沿った動きが別の方向に移動します。 したがって、座標系をZ軸を中心に45度回転すると、元の座標系のx
軸に沿って移動すると、新しい座標系のx
軸とy
軸の間で斜め方向に移動します。
簡単にするために、変換行列が主軸の周りの回転をどのように探すかを示します。
Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1
実装
これらはすべて、16個の数値を格納し、行列を列優先順に格納するクラスとして実装できます。
function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row < 4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) { var mat = new Transformation() mat.fields[12] = Number(x) || 0 mat.fields[13] = Number(y) || 0 mat.fields[14] = Number(z) || 0 return this.mult(mat) } // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) { var mat = new Transformation() mat.fields[0] = Number(x) || 0 mat.fields[5] = Number(y) || 0 mat.fields[10] = Number(z) || 0 return this.mult(mat) } // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) { angle = Number(angle) || 0 var c = Math.cos(angle) var s = Math.sin(angle) var mat = new Transformation() mat.fields[5] = c mat.fields[10] = c mat.fields[9] = -s mat.fields[6] = s return this.mult(mat) } // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) { angle = Number(angle) || 0 var c = Math.cos(angle) var s = Math.sin(angle) var mat = new Transformation() mat.fields[0] = c mat.fields[10] = c mat.fields[2] = -s mat.fields[8] = s return this.mult(mat) } // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle) { angle = Number(angle) || 0 var c = Math.cos(angle) var s = Math.sin(angle) var mat = new Transformation() mat.fields[0] = c mat.fields[5] = c mat.fields[4] = -s mat.fields[1] = s return this.mult(mat) }
カメラを通して見る
画面にオブジェクトを表示する際の重要な部分であるカメラがここにあります。 カメラには2つの重要なコンポーネントがあります。 つまり、その位置と、観測されたオブジェクトを画面に投影する方法です。
カメラの位置は、1つの簡単なトリックで処理されます。 カメラを1メートル前方に動かすことと、全世界を1メートル後方に動かすことの間に視覚的な違いはありません。 したがって、当然のことながら、変換として行列の逆行列を適用することにより、後者を実行します。
2番目の重要な要素は、観察されたオブジェクトがレンズに投影される方法です。 WebGLでは、画面に表示されるすべてのものがボックスに配置されます。 ボックスは、各軸で-1から1の間にあります。 表示されているものはすべてそのボックス内にあります。 変換行列と同じアプローチを使用して、射影行列を作成できます。
正射影
最も単純な投影法は正投影です。 中心がゼロの位置にあると仮定して、幅、高さ、深さを示すボックスを空間に配置します。 次に、プロジェクションはボックスのサイズを変更して、WebGLがオブジェクトを監視する前述のボックスに合わせます。 各寸法のサイズを2に変更するため、各軸を2/size
でスケーリングします。これにより、 size
はそれぞれの軸の寸法になります。 小さな注意点は、Z軸に負の値を掛けているという事実です。 これは、その次元の方向を反転させたいために行われます。 最終的なマトリックスの形式は次のとおりです。
2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1
透視投影
このプロジェクションがどのように設計されているかについては詳しく説明しませんが、現在ではほとんど標準となっている最終的な式を使用するだけです。 投影をx軸とy軸のゼロ位置に配置し、右/左と上/下の制限をそれぞれwidth/2
とheight/2
2に等しくすることで、これを単純化できます。 パラメータn
とf
は、カメラがポイントをキャプチャできる最小距離と最大距離である、 near
とfar
のクリッピング平面を表します。 それらは上の画像の錐台の平行な側面によって表されます。
透視投影は通常、視野(垂直方向を使用します)、アスペクト比、および近平面と遠平面の距離で表されます。 その情報を使用してwidth
とheight
を計算し、次のテンプレートからマトリックスを作成できます。
2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(nf) 2*f*n/(nf) 0 0 -1 0
幅と高さを計算するには、次の式を使用できます。
height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height
FOV(視野)は、カメラがレンズでキャプチャする垂直角度を表します。 アスペクト比は、画像の幅と高さの比率を表し、レンダリング先の画面のサイズに基づいています。
実装
これで、カメラを、カメラの位置と投影行列を格納するクラスとして表すことができます。 また、逆変換の計算方法も知っておく必要があります。 一般的な行列の反転を解くことは問題になる可能性がありますが、私たちの特別な場合には単純化されたアプローチがあります。
function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i < 3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }
これは、画面に絵を描き始める前に必要な最後のピースです。
WebGLグラフィックパイプラインを使用したオブジェクトの描画
描画できる最も単純なサーフェスは三角形です。 実際、3D空間で描くものの大部分は、多数の三角形で構成されています。
最初に理解する必要があるのは、WebGLで画面がどのように表現されるかです。 これは、 x 、 y 、 z軸上で-1から1の間に広がる3D空間です。 デフォルトでは、このz軸は使用されませんが、3Dグラフィックスに関心があるため、すぐに有効にする必要があります。
このことを念頭に置いて、このサーフェスに三角形を描画するために必要な3つの手順を次に示します。
描画する三角形を表す3つの頂点を定義できます。 そのデータをシリアル化し、GPU(グラフィックスプロセッシングユニット)に送信します。 モデル全体が利用可能であるため、モデル内のすべての三角形に対してそれを行うことができます。 指定する頂点の位置は、ロードしたモデルのローカル座標空間にあります。 簡単に言うと、指定する位置はファイルからの正確な位置であり、行列変換を実行した後に取得する位置ではありません。
GPUに頂点を指定したので、頂点を画面に配置するときに使用するロジックをGPUに指示します。 このステップは、行列変換を適用するために使用されます。 GPUは、多くの4x4行列の乗算に非常に優れているため、その機能を有効に活用します。
最後のステップで、GPUはその三角形をラスタライズします。 ラスタライズは、ベクターグラフィックを取得し、そのベクターグラフィックオブジェクトを表示するために画面のどのピクセルをペイントする必要があるかを決定するプロセスです。 この場合、GPUは各三角形内に配置されているピクセルを判別しようとしています。 GPUは、ピクセルごとに、ペイントする色を尋ねます。
これらは、必要なものを描画するために必要な4つの要素であり、グラフィックスパイプラインの最も単純な例です。 以下は、それらのそれぞれの外観と簡単な実装です。
デフォルトのフレームバッファ
WebGLアプリケーションの最も重要な要素は、WebGLコンテキストです。 現在使用されているブラウザがまだすべてのWebGL機能をサポートしていない場合は、 gl = canvas.getContext('webgl')
を使用してアクセスするか、フォールバックとして'experimental-webgl'
を使用できます。 参照したcanvas
は、描画するキャンバスのDOM要素です。 コンテキストには多くのものが含まれ、その中にはデフォルトのフレームバッファがあります。
フレームバッファは、描画できる任意のバッファ(オブジェクト)として大まかに説明できます。 デフォルトでは、デフォルトのフレームバッファは、WebGLコンテキストがバインドされているキャンバスの各ピクセルの色を格納します。 前のセクションで説明したように、フレームバッファを描画すると、各ピクセルはx軸とy軸の-1と1の間に配置されます。 また、デフォルトでは、WebGLはz軸を使用しないという事実についても触れました。 この機能は、 gl.enable(gl.DEPTH_TEST)
を実行することで有効にできます。 すばらしいですが、深度テストとは何ですか?
深度テストを有効にすると、ピクセルに色と深度の両方を保存できます。 深さは、そのピクセルのz座標です。 特定の深さzのピクセルに描画した後、そのピクセルの色を更新するには、カメラに近いz位置に描画する必要があります。 それ以外の場合、描画の試行は無視されます。 これにより、3Dの錯覚が可能になります。これは、他のオブジェクトの背後にあるオブジェクトを描画すると、それらのオブジェクトがそれらの前のオブジェクトによって遮られるためです。
実行するドローは、クリアするように指示するまで画面に表示されたままになります。 これを行うには、 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
を呼び出す必要があります。 これにより、カラーバッファと深度バッファの両方がクリアされます。 クリアされたピクセルが設定されている色を選択するには、 gl.clearColor(red, green, blue, alpha)
を使用します。
キャンバスを使用し、要求に応じてクリアするレンダラーを作成しましょう。
function Renderer (canvas) { var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') gl.enable(gl.DEPTH_TEST) this.gl = gl } Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) } var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }
このスクリプトを次のHTMLに添付すると、画面上に明るい青色の長方形が表示されます。
<!DOCTYPE html> <html> <head> </head> <body> <canvas width="800" height="500"></canvas> <script src="script.js"></script> </body> </html>
requestAnimationFrame
呼び出しにより、前のフレームのレンダリングが完了し、すべてのイベント処理が終了するとすぐに、ループが再度呼び出されます。
頂点バッファオブジェクト
最初に行う必要があるのは、描画する頂点を定義することです。 あなたは3D空間のベクトルを介してそれらを記述することによってそれを行うことができます。 その後、新しい頂点バッファオブジェクト(VBO)を作成して、そのデータをGPURAMに移動します。
一般に、バッファオブジェクトは、GPUにメモリチャンクの配列を格納するオブジェクトです。 VBOであるということは、GPUがメモリを何に使用できるかを示しているにすぎません。 ほとんどの場合、作成するバッファオブジェクトはVBOになります。
N
個の頂点をすべて取得し、頂点位置と頂点法線VBOに3N
要素、テクスチャ座標VBOに2N
要素を含むフロートの配列を作成することで、VBOを埋めることができます。 3つのフロートの各グループ、またはUV座標の場合は2つのフロートは、頂点の個々の座標を表します。 次に、これらの配列をGPUに渡し、頂点は残りのパイプラインの準備が整います。
データはGPURAMにあるため、汎用RAMから削除できます。 つまり、後で変更して再度アップロードする場合を除きます。 JSアレイの変更は、実際のGPU RAMのVBOには適用されないため、各変更の後にアップロードを行う必要があります。
以下は、説明されているすべての機能を提供するコード例です。 注意すべき重要な点は、GPUに格納されている変数はガベージコレクションされないという事実です。 つまり、使用しなくなったら手動で削除する必要があります。 ここでは、それがどのように行われるかの例を示します。これ以上、その概念に焦点を当てることはしません。 GPUから変数を削除する必要があるのは、プログラム全体で特定のジオメトリの使用を停止する場合のみです。
また、 Geometry
クラスとその中の要素にシリアル化を追加しました。
Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(vx, vy, vz) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(vx, vy, vz) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(vx, vy) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) }
VBO
データ型は、2番目のパラメーターとして渡された配列に基づいて、渡されたWebGLコンテキストでVBOを生成します。
gl
コンテキストへの3つの呼び出しを見ることができます。 createBuffer()
呼び出しは、バッファーを作成します。 bindBuffer()
呼び出しは、特に指示がない限り、この特定のメモリを将来のすべての操作の現在のVBO( ARRAY_BUFFER
)として使用するようにWebGLステートマシンに指示します。 その後、 bufferData()
を使用して、現在のVBOの値を提供されたデータに設定します。
また、 deleteBuffer()
を使用して、GPURAMからバッファオブジェクトを削除するdestroyメソッドも提供します。
3つのVBOと変換を使用して、メッシュのすべてのプロパティとその位置を記述することができます。
function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() }
例として、モデルをロードし、そのプロパティをメッシュに保存してから破棄する方法を次に示します。
Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })
シェーダー
以下は、ポイントを目的の位置に移動し、すべての個々のピクセルをペイントする、前述の2段階のプロセスです。 これを行うために、グラフィックカードで何度も実行されるプログラムを作成します。 このプログラムは通常、少なくとも2つの部分で構成されています。 最初の部分は頂点シェーダーです。これは頂点ごとに実行され、特に画面上の頂点を配置する場所を出力します。 2番目の部分はフラグメントシェーダーです。これは、三角形が画面上でカバーする各ピクセルに対して実行され、ピクセルがペイントされる色を出力します。
頂点シェーダー
画面上で左右に動くモデルが必要だとします。 単純なアプローチでは、各頂点の位置を更新して、GPUに再送信できます。 そのプロセスは費用がかかり、時間がかかります。 または、GPUが各頂点に対して実行するプログラムを提供し、それらすべての操作を、まさにそのジョブを実行するために構築されたプロセッサと並行して実行します。 これが頂点シェーダーの役割です。
頂点シェーダーは、個々の頂点を処理するレンダリングパイプラインの一部です。 頂点シェーダーの呼び出しは、単一の頂点を受け取り、頂点へのすべての可能な変換が適用された後、単一の頂点を出力します。

シェーダーはGLSLで記述されています。 この言語には多くの固有の要素がありますが、構文のほとんどは非常にCに似ているため、ほとんどの人が理解できるはずです。
頂点シェーダーに出入りする変数には3つのタイプがあり、それらはすべて特定の用途に使用されます。
-
attribute
—これらは頂点の特定のプロパティを保持する入力です。 以前は、頂点の位置を3要素のベクトルの形式で属性として説明しました。 属性は、1つの頂点を表す値と見なすことができます。 -
uniform
—これらは、同じレンダリング呼び出し内のすべての頂点で同じ入力です。 変換行列を定義することにより、モデルを移動できるようにしたいとします。uniform
変数を使用してそれを説明できます。 テクスチャのように、GPU上のリソースを指すこともできます。 ユニフォームは、モデルまたはモデルの一部を表す値と見なすことができます。 -
varying
する—これらはフラグメントシェーダーに渡す出力です。 頂点の三角形には数千のピクセルが存在する可能性があるため、各ピクセルは、位置に応じて、この変数の補間値を受け取ります。 したがって、1つの頂点が500を出力として送信し、別の頂点が100を送信する場合、それらの中間にあるピクセルは、その変数の入力として300を受け取ります。 変化は、頂点間のサーフェスを表す値として見ることができます。
So, let's say you want to create a vertex shader that receives a position, normal, and uv coordinates for each vertex, and a position, view (inverse camera position), and projection matrix for each rendered object. Let's say you also want to paint individual pixels based on their uv coordinates and their normals. “How would that code look?” あなたは尋ねるかもしれません。
attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); }
Most of the elements here should be self-explanatory. The key thing to notice is the fact that there are no return values in the main
function. All values that we would want to return are assigned, either to varying
variables, or to special variables. Here we assign to gl_Position
, which is a four-dimensional vector, whereby the last dimension should always be set to one. Another strange thing you might notice is the way we construct a vec4
out of the position vector. You can construct a vec4
by using four float
s, two vec2
s, or any other combination that results in four elements. There are a lot of seemingly strange type castings which make perfect sense once you're familiar with transformation matrices.
You can also see that here we can perform matrix transformations extremely easily. GLSL is specifically made for this kind of work. The output position is calculated by multiplying the projection, view, and model matrix and applying it onto the position. The output normal is just transformed to the world space. We'll explain later why we've stopped there with the normal transformations.
For now, we will keep it simple, and move on to painting individual pixels.
Fragment Shaders
A fragment shader is the step after rasterization in the graphics pipeline. It generates color, depth, and other data for every pixel of the object that is being painted.
The principles behind implementing fragment shaders are very similar to vertex shaders. There are three major differences, though:
- There are no more
varying
outputs, andattribute
inputs have been replaced withvarying
inputs. We have just moved on in our pipeline, and things that are the output in the vertex shader are now inputs in the fragment shader. - Our only output now is
gl_FragColor
, which is avec4
. The elements represent red, green, blue, and alpha (RGBA), respectively, with variables in the 0 to 1 range. You should keep alpha at 1, unless you're doing transparency. Transparency is a fairly advanced concept though, so we'll stick to opaque objects. - At the beginning of the fragment shader, you need to set the float precision, which is important for interpolations. In almost all cases, just stick to the lines from the following shader.
With that in mind, you can easily write a shader that paints the red channel based on the U position, green channel based on the V position, and sets the blue channel to maximum.
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); }
The function clamp
just limits all floats in an object to be within the given limits. The rest of the code should be pretty straightforward.
With all of this in mind, all that is left is to implement this in WebGL.
Combining Shaders into a Program
The next step is to combine the shaders into a program:
function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } }
There isn't much to say about what's happening here. Each shader gets assigned a string as a source and compiled, after which we check to see if there were compilation errors. Then, we create a program by linking these two shaders. Finally, we store pointers to all relevant attributes and uniforms for posterity.
Actually Drawing the Model
Last, but not least, you draw the model.
First you pick the shader program you want to use.
ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }
Then you send all the camera related uniforms to the GPU. These uniforms change only once per camera change or movement.
Transformation.prototype.sendToGpu = function (gl, uniform, transpose) { gl.uniformMatrix4fv(uniform, transpose || false, new Float32Array(this.fields)) } Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }
Finally, you take the transformations and VBOs and assign them to uniforms and attributes, respectively. Since this has to be done to each VBO, you can create its data binding as a method.
VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }
Then you assign an array of three floats to the uniform. Each uniform type has a different signature, so documentation and more documentation are your friends here. Finally, you draw the triangle array on the screen. You tell the drawing call drawArrays()
from which vertex to start, and how many vertices to draw. The first parameter passed tells WebGL how it shall interpret the array of vertices. Using TRIANGLES
takes three by three vertices and draws a triangle for each triplet. Using POINTS
would just draw a point for each passed vertex. There are many more options, but there is no need to discover everything at once. Below is the code for drawing an object:
Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) }
The renderer needs to be extended a bit to accommodate all the extra elements that need to be handled. It should be possible to attach a shader program, and to render an array of objects based on the current camera position.
Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
We can combine all the elements that we have to finally draw something on the screen:
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }
This looks a bit random, but you can see the different patches of the sphere, based on where they are on the UV map. You can change the shader to paint the object brown. Just set the color for each pixel to be the RGBA for brown:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }
It doesn't look very convincing. It looks like the scene needs some shading effects.
Adding Light
Lights and shadows are the tools that allow us to perceive the shape of objects. Lights come in many shapes and sizes: spotlights that shine in one cone, light bulbs that spread light in all directions, and most interestingly, the sun, which is so far away that all the light it shines on us radiates, for all intents and purposes, in the same direction.
Sunlight sounds like it's the simplest to implement, since all you need to provide is the direction in which all rays spread. For each pixel that you draw on the screen, you check the angle under which the light hits the object. This is where the surface normals come in.
You can see all the light rays flowing in the same direction, and hitting the surface under different angles, which are based on the angle between the light ray and the surface normal. The more they coincide, the stronger the light is.
If you perform a dot product between the normalized vectors for the light ray and the surface normal, you will get -1 if the ray hits the surface perfectly perpendicularly, 0 if the ray is parallel to the surface, and 1 if it illuminates it from the opposite side. So anything between 0 and 1 should add no light, while numbers between 0 and -1 should gradually increase the amount of light hitting the object. You can test this by adding a fixed light in the shader code.
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }
太陽を前後左右に照らします。 モデルが非常にギザギザになっているにもかかわらず、シェーディングがどれほど滑らかであるかがわかります。 また、左下がどれほど暗いかがわかります。 周囲光のレベルを追加して、影の領域を明るくすることができます。
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
これと同じ効果は、光の方向と周囲光の強度を格納するライトクラスを導入することで実現できます。 次に、その追加に対応するようにフラグメントシェーダーを変更できます。
これで、シェーダーは次のようになります。
#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
次に、ライトを定義できます。
function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }
シェーダープログラムクラスで、必要なユニフォームを追加します。
this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')
プログラムで、レンダラーの新しいライトへの呼び出しを追加します。
Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
その後、ループはわずかに変化します。
var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
すべてを正しく行った場合、レンダリングされた画像は最後の画像と同じになります。
考慮すべき最後のステップは、モデルに実際のテクスチャを追加することです。 今それをしましょう。
テクスチャの追加
HTML5は画像の読み込みを強力にサポートしているため、画像の解析を狂わせる必要はありません。 画像は、バインドされたテクスチャのどれをサンプリングするかをシェーダーに指示することにより、 sampler2D
としてGLSLに渡されます。 バインドできるテクスチャの数には制限があり、その制限は使用するハードウェアに基づいています。 sampler2D
は、特定の位置の色を照会できます。 これがUV座標の出番です。これは、茶色をサンプル色に置き換えた例です。
#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
新しいユニフォームをシェーダープログラムのリストに追加する必要があります。
this.diffuse = gl.getUniformLocation(program, 'diffuse')
最後に、テクスチャの読み込みを実装します。 前述のように、HTML5は画像をロードするための機能を提供します。 画像をGPUに送信するだけです。
function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) { binding = Number(binding) || 0 var gl = this.gl // We can bind multiple textures, and here we pick which of the bindings // we're setting right now gl.activeTexture(gl['TEXTURE' + binding]) // After picking the binding, we set the texture gl.bindTexture(gl.TEXTURE_2D, this.data) // Finally, we pass to the uniform the binding ID we've used gl.uniform1i(uniform, binding) // The previous 3 lines are equivalent to: // texture[i] = this.data // uniform = i } Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }
このプロセスは、VBOのロードとバインドに使用されるプロセスと大差ありません。 主な違いは、属性にバインドするのではなく、テクスチャのインデックスを整数のユニフォームにバインドすることです。 sampler2D
タイプは、テクスチャへのポインタオフセットにすぎません。
ここで行う必要があるのは、 Mesh
クラスを拡張して、テクスチャも処理することだけです。
function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) }
そして、最終的なメインスクリプトは次のようになります。
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
この時点で、アニメーション化も簡単になります。 カメラをオブジェクトの周りで回転させたい場合は、コードを1行追加するだけで実行できます。
function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }
シェーダーを自由に試してみてください。 1行のコードを追加すると、このリアルな照明が漫画的なものに変わります。
void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
設定されたしきい値を超えたかどうかに基づいて、照明に極端になるように指示するのと同じくらい簡単です。
次はどこへ
WebGLのすべてのトリックと複雑さを学ぶための多くの情報源があります。 そして最良の部分は、WebGLに関連する答えが見つからない場合、OpenGLでそれを探すことができることです。WebGLはほとんどOpenGLのサブセットに基づいており、一部の名前が変更されているためです。
順不同で、WebGLとOpenGLの両方について、より詳細な情報を提供する優れた情報源をいくつか紹介します。
- WebGLの基礎
- WebGLの学習
- ここで説明するすべての基本原則を非常にゆっくりと詳細にガイドする非常に詳細なOpenGLチュートリアル。
- そして、コンピュータグラフィックスの原理を教えることに専念している他の多くのサイトがあります。
- WebGLのMDNドキュメント
- すべてのエッジケースでWebGLAPIがどのように機能するかについてのより技術的な詳細を理解することに関心がある場合は、KhronosWebGL1.0仕様。