はじめに
プログラマの小林です。レンダリングに興味があり、日頃、趣味でサンプルを作り遊んでいます。
その中で最近試してみたボクセル化に関して、その処理フローを書いてみたいと思います。
ラスタライザを使用したボクセル化
ボーンデジタル社さんから出版されている「OpenGL Insights」のChapter22「GPUハードウェアラスタライザを使ったオクトツリーに基づく疎なボクセル化」を参考にしました。
自分が作成したデモでは、第1部で解説されている、3Dテクスチャを使用した単純なサーフェイスボクセル化アルゴリズムを試してみました。
処理の順序は、次の通りです。
- 三角形の支配的な軸の選択
- 三角形を投影
- 保守的なラスタライズ
- ボクセル属性の計算
上記にあげた書籍にそのまま書かれていますが、簡単に説明します。
1.三角形の支配的な軸の選択
まずは、ポリゴン面法線が、XYZ軸のどの方向に、より向いているのかを調べます。
方法はシンプルで、各軸ベクトルと面法線との内積をとり、絶対値の最大となる軸を選びます。
なお、この処理は、ポリゴン(3頂点)のデータが必要になりますので、ジオメトリシェーダーで処理します。
2.三角形を投影
そして、その軸が、Z軸方向になるように各要素を入れ替えます。(↓にシェーダコードを記載してますので、詳しくはそちらを見てください。)
3.保守的なラスタライズ
このまま、座標をラスタライザに流すと、小さな三角形のフラグメントが失われたりします(らしい)。ので、この三角形を一回り大きくします。やり方は、ここでは、割愛します。ここで示すシェーダコードも、この処理を端折ってます。(まあ、デモなので。。。)
4.ボクセル属性の計算
先程、ジオメトリシェーダーで処理したデータをラスタライザに通し、フラグメントにしてもらいます。フラグメントのワールド座標からテクスチャ座標に変換し、対応する箇所に任意な属性を格納します。この格納時に、今回は、imageStoreを使用してますが、本来は、アトミック(atomicCompSwap())操作を使用して、平均化するそうです。(今回、示すコードではやってません…)
シェーダコード
それでは、各シェーダーコードを見ていきます。
まず、頂点シェーダから。
uniform mat4 model_matrix; uniform mat3 normal_matrix; in vec3 position; in vec3 normal; in vec4 color; in vec2 texcoord; out GsInputs { vec3 oNormal; vec2 oTexcoord; vec4 oColor; } gsin; uniform bool enableVertColor; void main(void) { gl_Position = model_matrix * vec4(position.xyz, 1.f); gsin.oNormal = normal_matrix * normal; gsin.oTexcoord = texcoord; if (enableVertColor) gsin.oColor = color; else gsin.oColor = vec4(1.0); }
説明の必要のないくらいにシンプルです。入力された頂点データをワールド座標系に変換しているだけです。
次は、ジオメトリシェーダです。
#version 330 layout( triangles ) in; layout( triangle_strip, max_vertices = 3 ) out; uniform mat4 view_proj_matrix; in GsInputs { vec3 oNormal; vec2 oTexcoord; vec4 oColor; } gsin[3]; out FsInputs { vec3 oPosi; vec3 oWPosi; vec3 oNormal; vec2 oTexcoord; vec4 oColor; flat int axisIdx; } fsin; const vec3 zAxis = vec3(0.f,0.f,1.f); const vec3 xAxis = vec3(1.f,0.f,0.f); const vec3 yAxis = vec3(0.f,1.f,0.f); void main(void) { vec3 norm = normalize(gsin[0].oNormal + gsin[1].oNormal + gsin[2].oNormal); int axisIdx = 0; { float inp0 = abs(dot(norm, xAxis)); float inp1 = abs(dot(norm, yAxis)); float inp2 = abs(dot(norm, zAxis)); // 0 : ZY平面 // 1 : XZ平面 // 2 : XY平面 if (inp0 < inp1) { inp0 = inp1; axisIdx = 1; } if (inp0 < inp2) { inp0 = inp2; axisIdx = 2; } } for (int i = 0; i < 3; i++) { vec4 tpos = view_proj_matrix * gl_in[i].gl_Position; if (axisIdx == 0) { tpos.xyz = tpos.zyx * vec3(1.f, 1.f, -1.f); } else if (axisIdx == 1) { tpos.xyz = tpos.xzy * vec3(1.f, 1.f, -1.f); } gl_Position = tpos; fsin.oPosi = tpos.xyz; fsin.oWPosi = gl_in[i].gl_Position.xyz; fsin.oNormal = gsin[i].oNormal; fsin.oTexcoord = gsin[i].oTexcoord; fsin.oColor = gsin[i].oColor; fsin.axisIdx = axisIdx; EmitVertex(); } EndPrimitive(); }
頂点シェーダから出力された各頂点の法線から面法線を求めます。
その面法線と各軸XYZと内積を取り、最大値となる軸を選びます。選んだ軸をaxisIdxに設定します。選んだ軸に合わせて、面がラスタライズされるように正面に向くように軸を入れ替えます。
入れ替える前に、ワールド座標系からクリップ座標系に変換します。ここで射影行列は、正投影の行列が入ってます。ボクセル化したい空間の大きさで、決めています。
この時、処理系によっては、クリップ座標系の各軸の範囲が違ったりするので注意が必要です。入れ替え後、頂点データとして出力します。ワールド座標、選んだ軸のインデックスも一緒に出力しています。
最後にフラグメントシェーダです。
void main(void) { vec3 voxelPos = fsin.oPosi; if (fsin.axisIdx == 0) // ZY平面 { voxelPos = fsin.oPosi.zyx * vec3(-1.f, 1.f, 1.f); } else if (fsin.axisIdx == 1) // XZ平面 { voxelPos = fsin.oPosi.xzy * vec3( 1.f,-1.f, 1.f); } // 左下を原点に持っていく voxelPos = voxelPos * vec3(1.f, 1.f, -1.f) + 1.f; // Voxel座標系へ変換 vec3 texPos = voxelPos / voxelSize; ivec3 iTexPos = ivec3(texPos.x, texPos.y, texPos.z); // 3Dtextureにデータを格納します imageStore(imgVoxel, iTexPos, vec4(color, 1.0)); // for debug // fragColor = vec4(color, 1.0); }
ジオメトリシェーダーで軸を入れ替えていたので、入れ替えた軸インデックス(axisIdex)を参照し、元に戻してやります。
その後、ワールド座標から、ボクセル座標系(3Dテクスチャ座標系)に変換しています。
最後にimageStore()で、任意の属性値を格納しています。ここでは、colorになんらかのデータが入っているものとしてください。アルファ成分に 1.0 を設定しています。
デモの結果
動作させるとこのようになってます。ボクセル化の描画がされていない状態の時は、1つの点光源があるのみのベーシックな描画です。これを作成した3Dテクスチャを読み取り、データが格納されているところにボクセルを描画すると以下のようになりました。
ポリゴンがある箇所にボクセルデータが格納されているのが確認でき、目標は達成できました。
おわりに
ざっくりとですが、ボクセル化の処理に関して、触れてみました。
ここでは、いくつかの処理を端折ってますが、雰囲気はつかめたのではないかと思います。