75行のコードで最小限のWebGL

最新のOpenGL、より広くはWebGLは、私が過去に研究した古いOpenGLとは大きく異なります。ラスター化がどのように機能するかを理解しているので、概念に精通しています。しかし、私が読んだすべてのチュートリアルは、どの部分がOpenGL API自体に属しているのかを理解するのを難しくする、抽象化とヘルパー関数を提供していました。



明確にするために、位置データの分割や機能のレンダリング機能などの抽象化は、実際のアプリケーションでは重要です。ただし、これらの抽象化により、コードがさまざまな領域に分散し、ボイラープレートと論理ユニット間のデータ転送により冗長性が追加されます。各行がこのトピックに直接関連しているコードの線形フローに関するトピック研究するのが最も便利だと思います



まず、使用しチュートリアルの作成者に感謝する必要があります。それを基礎として、「最小限の実行可能なプログラム」を取得するまで、すべての抽象化を取り除きました。うまくいけば、それはあなたが現代のOpenGLを始めるのに役立つでしょう。これが私たちがすることです:





等辺の三角形。上部が緑、左下が黒、右下が赤で、点の間に色が補間されています。黒い三角形の少し明るいバージョン[ Habréでの翻訳]。



初期化



WebGLでは、canvas描画する必要があります。もちろん、通常のHTMLボイラープレート、スタイルなどをすべて追加する必要がありますが、キャンバスが最も重要です。DOMがロードされたら、Javascriptを使用してキャンバスにアクセスできます。



<canvas id="container" width="500" height="500"></canvas>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // All the Javascript code below goes here
  });
</script>


キャンバスにアクセスすることで、WebGLレンダリングコンテキストを取得し、そのクリアカラーを初期化できます。OpenGLの世界の色はRGBAとして保存され、各コンポーネントの値はから0まで1です。クリアカラーは、各フレームの開始時にキャンバスを描画し、シーンを再描画するために使用される色です。



const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');

gl.clearColor(1, 1, 1, 1);


実際のプログラムでは、初期化をより詳細にすることができます。特に、Z座標に基づいてジオメトリを並べ替えることができる深度バッファ含まれていることに言及する必要があります。これは、1つの三角形だけで構成される単純なプログラムでは行いません。



シェーダーのコンパイル



その中核となるのは、OpenGLはラスタライズフレームワークである私たちはラスタライズ以外のすべてのものを実装する方法を決定する必要があります。したがって、GPUでは少なくとも2段階のコードを実行する必要があります。



  1. すべての入力データを処理し、入力ごとに1つの3D位置(実際には均一な座標の4D位置)を出力する頂点シェーダー
  2. 画面上の各ピクセルを処理し、ピクセルをペイントする色をレンダリングするフラグメントシェーダー。


これらの2つの段階の間で、OpenGLは頂点シェーダーからジオメトリを取得し、そのジオメトリでカバーされる画面ピクセルを決定します。これがラスター化の段階です。



両方のシェーダーは通常GLSL(OpenGL Shading Language)で記述され、GPUのマシンコードにコンパイルされます。次に、マシンコードがGPUに渡され、レンダリングプロセス中に実行できるようになります。非常に基本的なことだけを示したいので、GLSLについては詳しく説明しませんが、言語はCに十分近いため、ほとんどのプログラマーに馴染みがあります。



まず、頂点シェーダーをコンパイルしてGPUに渡します。以下に示すフラグメントでは、シェーダーのソースコードは文字列として保存されていますが、他の場所からロードすることもできます。最後に、文字列がWebGLAPIに渡されます。



const sourceV = `
  attribute vec3 position;
  varying vec4 color;

  void main() {
    gl_Position = vec4(position, 1);
    color = gl_Position * 0.5 + 0.5;
  }
`;

const shaderV = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shaderV, sourceV);
gl.compileShader(shaderV);

if (!gl.getShaderParameter(shaderV, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderV));
  throw new Error('Failed to compile vertex shader');
}


ここでGLSLコードのいくつかの変数を説明する価値があります。



  1. (attribute) position. , , .
  2. Varying color. ( ) . .
  3. gl_Position. , , varying-. , ,


均一な 変数タイプもあります。これは、すべての頂点シェーダー呼び出しにわたって定数です。このようなユニフォームは、1つの幾何学的要素のすべての頂点に対して一定である変換行列などのプロパティに使用されます。



次に、フラグメントシェーダーで同じことを行います。コンパイルしてGPUに転送します。color頂点シェーダーからの変数がフラグメントシェーダーによって読み取られることに注意してください



const sourceF = `
  precision mediump float;
  varying vec4 color;

  void main() {
    gl_FragColor = color;
  }
`;

const shaderF = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shaderF, sourceF);
gl.compileShader(shaderF);

if (!gl.getShaderParameter(shaderF, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderF));
  throw new Error('Failed to compile fragment shader');
}


さらに、頂点シェーダーとフラグメントシェーダーの両方が1つのOpenGLプログラムにリンクされています。



const program = gl.createProgram();
gl.attachShader(program, shaderV);
gl.attachShader(program, shaderF);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
  throw new Error('Failed to link program');
}

gl.useProgram(program);


上記のシェーダーを実行することをGPUに通知します。ここで、入力データを作成し、GPUにこのデータを処理させる必要があります。



着信データをGPUに送信する



着信データはGPUメモリに保存され、そこから処理されます。対応するデータを一度に1チャンクずつ転送する、着信データごとに個別の描画呼び出しを行う代わりに、すべての着信データ全体がGPUに転送され、GPUから読み取られます。(古いOpenGLは個々の要素でデータを渡し、パフォーマンスが低下しました。)



OpenGLは、Vertex Buffer Object(VBO)と呼ばれる抽象化を提供します。私はまだそれがどのように機能するかを理解していますが、それを使用するために次のことを行うことになります:



  1. データのシーケンスを中央処理装置(CPU)のメモリに保存します。
  2. gl.createBuffer()アンカーポイントで 作成された一意のバッファを介して、バイトをGPUメモリに転送しますgl.ARRAY_BUFFER


頂点シェーダーの入力データ(属性)の変数ごとに1つのVBOがありますが、入力データの複数の要素に1つのVBOを使用することもできます。



const positionsData = new Float32Array([
  -0.75, -0.65, -1,
   0.75, -0.65, -1,
   0   ,  0.65, -1,
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positionsData, gl.STATIC_DRAW);


通常、アプリケーションが理解できる座標でジオメトリを定義し、頂点シェーダーで一連の変換を使用して、それらをOpenGLクリップスペースにマップしますXとYが-1から+1の範囲で変化することを知っておく必要があるだけですが、切り捨てスペース(均一な座標に関連付けられています)については詳しく説明しません。頂点シェーダーは入力をそのまま渡すだけなので、クリッピングスペースに直接座標を設定できます。



次に、バッファを頂点シェーダーの変数の1つにバインドします。コードでは、次のことを行います。



  1. position上で作成したプログラムから変数記述子を取得します
  2. OpenGLにgl.ARRAY_BUFFER、オフセットとストライドが0など、特定のパラメーターを使用して3つのグループのアンカーポイントからデータを読み取るように指示します




const attribute = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);


これらの関数を次々に実行するため、この方法でVBOを作成し、それを頂点シェーダー属性にバインドできることは注目に値します。2つの関数を分離する場合(たとえば、1つのパスですべてのVBOを作成し、それらを別々の属性にバインドする場合)、各VBOを対応する属性にマッピングする前に、毎回呼び出す必要がありgl.bindBuffer(...)ます。



レンダリング!



最後に、GPUメモリ内のすべてのデータが適切に準備されたら、OpenGLに画面をクリアし、プログラムを実行して準備したアレイを処理するように指示できます。ラスター化ステップ(どのピクセルが頂点によってカバーされるかを決定する)の一部として、OpenGLに3つのグループの頂点を三角形として扱うように指示します。



gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);


このような線形スキームでは、プログラムは一度に実行されます。実際のアプリケーションでは、構造化された方法でデータを保存し、変更に応じてGPUに送信し、フレームごとにレンダリングします。






要約すると、以下は、画面に最初の三角形を表示するために必要な最小限の概念のセットを含む図です。ただし、このスキームでさえ大幅に単純化されているため、この記事で紹介する75行のコードを記述して調査することをお勧めします。





三角形をレンダリングするために必要な最後の非常に単純化された一連の手順



私にとって、OpenGLを学ぶ上で最も難しい部分は、最も単純な画像を表示するために必要なボイラープレートの量でした。ラスター化フレームワークでは3Dレンダリング機能を提供する必要があり、GPUとの通信は非常に大きいため、多くの概念を直接検討する必要があります。この記事で、他のチュートリアルよりも簡単な方法で基本を説明できれば幸いです。



参照:








参照:






All Articles