[Work/TechInfo/other]

WebGL2(GLSL3)によるTransformFeedbackを用いたGPGPU その2 VTF / 2017-10-20 (金)

VTF - Vertex Texture Fetch

要するに,普通はフラグメントシェーダの中からしか呼べないテクスチャを頂点シェーダで読み込む機能である.

値3つだけでは足りない計算

前項のTransformFeedbackを用いたコードだと,GPGPUをやっている頂点シェーダ(の各スレッド)にはx, y, zの3つの値しか送れない.

uniformで任意の要素数の配列を送るためには,vec4に割り当てるしかなく,vec4変数を大量に宣言,転送しなければならないのでコードが汚くなるし,vec4が256個分が通常の限界らしい.

というわけで,テクスチャに値を入れて,頂点シェーダの中で受け取る方法がVTFである.通常全く問題がない大きさのテクスチャでも16*16*RGBAの4要素分つまり1024個の値を頂点シェーダに一発で送ることができる.現代の普通のマシンなら64*64*RGBAは余裕で送れるはずなので16384個は可能のはず.テクスチャもuniformなので各スレッドで共用の配列として使える.

Intel HD Graphics 5000では頂点シェーダで受け取れるテクスチャの最大枚数が16のようなので,最大16384*16つまり262144個の値を送れるようになる.(ただしOpenGLのテクスチャは定数で管理しているので,1枚増えるごとにコード量は増えることに注意)

ただしテクスチャはあくまでunsgined charであり,頂点シェーダプログラムの中では入力された値が255で割られた値が出てくるので,整数値のラベルを送ったり,1バイト以上の精度が必要な場合は,この方法は難しい.

HTMLファイル

前項と同じくdisplay:noneするcanvasと,計算結果を出力するdivがあるだけ.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>WebGL2 GPGPU Vertex Texture Fetch</title>

    <script type="text/javascript" src="./webgl2_gpgpu_vtf.js"></script>

  </head>
  <body>

    <canvas id="webgl_canvas"></canvas>
    <div id="result_display"></div>

  </body>
</html>

JavaScriptファイル


//webgl2_gpgpu_vtf.js
'use strict';
window.addEventListener('load', () => {
    // 参考にした: 
    // https://qiita.com/edo_m18/items/3258afe3a1d8ce2c6cd9
    

    if(window.navigator.userAgent.toLowerCase().indexOf('chrome') == -1){
	alert('Currently Only Chrome Browser can allow GLSL3.0(WebGL2)');
    }

    window.glCanvas = document.getElementById("webgl_canvas");
    window.glCanvas.width = 640;
    window.glCanvas.height = 480;
    window.glCanvas.setAttribute("style", "display: none");
    
    const gl2 = window.glCanvas.getContext("webgl2");

    // Vertex Texture Fetchが可能かどうかをチェックする
    let info = gl2.getParameter(gl2.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
    if(info > 0){
	console.log('max_vertex_texture_imaeg_unit: ', info);
	// この値までテクスチャをVertexシェーダ側で読み込める
    }
    else {
	alert('VTF not supported');
	return;
    }

    let vsScriptText = "#version 300 es \n" + 
	"out float result;" + 
	"in vec3 vertexPositions;" + 
	"uniform sampler2D vtfData;" + 
	"const float pixelSize = 2.0;" + 
	"const float frag = 1.0 / pixelSize;" +
	"const float shiftWidth = frag / pixelSize;" + 
	"const float texPosX = 1.0 * frag + shiftWidth;" + 
	"const float texPosY = (1.0 * frag) * frag + shiftWidth;" + 
	"void main(void){" +
             "vec4 valueFromTexture = texture(vtfData, vec2(texPosX, texPosY)).rgba;" + 
              "result = valueFromTexture[0] + valueFromTexture[1] + valueFromTexture[2] + valueFromTexture[3]" +
	" + vertexPositions[0] + vertexPositions[1] + vertexPositions[2];" + 
	"}";
    // uniform sampler2D vtfData; がテクスチャオブジェクト
    // ほんとに頂点座標オブジェクトならtexelFetch()で読み出せるのだが,
    // GPGPUの場合,頂点座標オブジェクトではないので,古典的な方法を使う

    // const float pixelSize = 2.0; //テクスチャの元々の1辺のサイズ
    // 元画素数は2*2だったが,GLSL内部では1.0*1.0に強制変換されるため,
    // 1.0を1辺のサイズで割って,1ピクセル辺りの幅を出している.ここが座標の最小単位になる
    // const float frag = 1.0 / pixelSize;

    // ただしここから0.5ピクセル分の幅ずらして読み込まなければならない
    // これは,ピクセルの真ん中に読み込みカーソル持ってくるという意味
    // ピクセルの端っこだと隣り合ったピクセルと補間されて
    // 「滲んでしまった色」を読み込む可能性があるため
    // float shiftWidth = frag / pixelSize;

    // float texPosX = 座標 * frag + shiftWidth; 
    // float texPosY = (座標 * frag) * frag + shiftWidth;

    // 読み出す座標を決定
    // vec2 readingPixelPos = vec2(texPosX, texPosY);
    // 値の取り出し
    // vec4 values = texture(vtfData, readingPixelPos).rgba;

    // 頂点座標のattrbuteの方も,シェーダプログラム内部できちんと使ってやらないと,
    // Warningを吐く
    
    window.vertexShader = gl2.createShader(gl2.VERTEX_SHADER);
    gl2.shaderSource(window.vertexShader, vsScriptText);
    gl2.compileShader(window.vertexShader);
    console.log(gl2.getShaderInfoLog(window.vertexShader));

    let fsScriptText = "#version 300 es \n void main(void){}";
    window.fragmentShader = gl2.createShader(gl2.FRAGMENT_SHADER);
    gl2.shaderSource(window.fragmentShader, fsScriptText);
    gl2.compileShader(window.fragmentShader);

    window.program = gl2.createProgram();
    gl2.attachShader(window.program, window.vertexShader);
    gl2.attachShader(window.program, window.fragmentShader);


    // TransformFeedbackを行うVertexシェーダ内の変数の登録登録
    gl2.transformFeedbackVaryings(window.program, ["result"], gl2.SEPARATE_ATTRIBS);
    // シェーダのリンク
    gl2.linkProgram(window.program);
    gl2.useProgram(window.program);

    // Vertex Texture Fetchを用いてVertexシェーダに大量の値を渡す
    // GPU内部へテクスチャオブジェクトを生成する
    let vtfDataTexture = gl2.createTexture();
    // 生成したテクスチャオブジェクトをWebGLにバインドする
    //gl2.activeTexture(gl2.GL_TEXTURE0);
    gl2.bindTexture(gl2.TEXTURE_2D, vtfDataTexture);
    // テクスチャの各種パラメータの設定
    // gl.TEXTURE_MAG(MIN) _FILTERにはgl.NEARESTを用いる
    // これはgl.LINEARを使うと頂点と頂点の間で補間されてしまうので,
    // データの受け渡しには向かないから
    gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.NEAREST);
    gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.NEAREST);
    gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_S, gl2.CLAMP_TO_EDGE);
    gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_T, gl2.CLAMP_TO_EDGE);

    // 渡す値の配列を作る
    // Textureの1辺は2の累乗でRGBAでなければならないので,2*2*4 = 16の1次元配列
    let textureWidth = 2;
    let textureHeight = 2;
    let textureSize = textureWidth * textureHeight * 4;
    let vtfData = new Uint8Array(textureSize);
    for(let i=0; i<textureSize; i++){
        // テクスチャは当然Uint8なのだが,
	// Vertexシェーダ内で取り出される時255で除算されて0.0~1.0で出てくる
	// なので,あらかじめ掛け算をしてfloorしておくなど,0〜255の整数で値を入れておく
	vtfData[i] = Math.random() * 255;
    }
    // テクスチャにデータを設定 テクスチャオブジェクトに指定するのではないことに注意
    gl2.texImage2D(gl2.TEXTURE_2D, //Target
		   0, //Mip map level
		   gl2.RGBA, //テクスチャのピクセルフォーマット
		   textureWidth, textureHeight, // width, height
		   0, //Border
		   gl2.RGBA, //ピクセルの配列形式(RGBAの順で並んでいた)
		   gl2.UNSIGNED_BYTE, //1ピクセルのデータ形式(Uint8)
		   vtfData); //


    // 頂点座標の登録とVBOの生成
    let vertexPosArray = 
	new Array(1.0, 2.0, 3,0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
    let numOfPoint = 3; //vec3(x, y, y)で3つ分の配列がある.

    let vboPos = gl2.createBuffer();
    gl2.bindBuffer(gl2.ARRAY_BUFFER, vboPos);
    gl2.bufferData(gl2.ARRAY_BUFFER, new Float32Array(vertexPosArray), gl2.STATIC_DRAW);
    
    let vertexPosAttribLocation = gl2.getAttribLocation(window.program, 'vertexPositions');
    gl2.enableVertexAttribArray(vertexPosAttribLocation);
    let vertexPosAttribStride = 3; //GLSL3.0コードの中でvec3型なので3
    gl2.vertexAttribPointer(vertexPosAttribLocation,
			    vertexPosAttribStride,
			    gl2.FLOAT,
			    false, 0, 0);
    gl2.bindBuffer(gl2.ARRAY_BUFFER, null);
    // 頂点座標の登録とVBOの生成: ここまで


    // TransformFeedbackの指定
    // GPU内へメモリを確保し,登録する
    let vertexTransformFeedback = gl2.createBuffer();
    let transformFeedback = gl2.createTransformFeedback();
    gl2.bindBuffer(gl2.ARRAY_BUFFER, vertexTransformFeedback);
    gl2.bufferData(gl2.ARRAY_BUFFER, numOfPoint * Float32Array.BYTES_PER_ELEMENT, gl2.DYNAMIC_COPY);
    gl2.bindBuffer(gl2.ARRAY_BUFFER, null);

    gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, transformFeedback);
    gl2.bindBufferBase(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, vertexTransformFeedback);

    // シェーダの実行
    gl2.beginTransformFeedback(gl2.POINTS);
    gl2.drawArrays(gl2.POINTS, 0, numOfPoint);
    gl2.endTransformFeedback();

    // TransformFeedbackの結果を取り出す
    let arrBuffer = new ArrayBuffer(numOfPoint * Float32Array.BYTES_PER_ELEMENT);
    arrBuffer = new Float32Array(arrBuffer);
    gl2.getBufferSubData(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, arrBuffer);

    // 結果を表示する
    document.getElementById('result_display').innerHTML = arrBuffer[0] + "," + arrBuffer[1] + "," + arrBuffer[2];
});
[ ツッコミの受付は終了しています ]
この記事のリンク元