ソースコード
本プログラムのソースコードは、github で公開してます。(MIT ライセンスです)
beziex/Beziex_Op [github] : (https://github.com/beziex/Beziex_Op)
OpenTK テッセレーション ビューワ
2ヶ月ほど前に、「独自の高次元曲面パッチデータを WebGL 上でリアルタイムにテッセレーションし、ウェブブラウザに表示する 3Dモデルビューワ」を作ったということで、寄稿させていただきました。
[前編]: WebGL でテッセレーションして、曲面パッチ間が滑らかに繋がった物体を表示(1)
今回の投稿も「テッセレーション ビューワ」なのですが WebGL では無く、「OpenTK」で実装しています。よって、Web アプリではありません。(デスクトップアプリケーションです)
ちなみに WebGL 版では、ベースとなっている OpenGL のバージョンの制限からテッセレーションシェーダを使うことが出来ず、バーテックスシェーダとフラグメントシェーダだけでテッセレーションしていました。ジオメトリインスタンシングという方法です。(詳しくは、上記の記事をご参照ください)
しかし OpenTK であれば、OpenGL のバージョン制限はありません。ということで、素直にテッセレーションシェーダを使う方法でプログラミングすることにしました。
でもそれだけだと、テッセレーションシェーダ使用の方が良いのかどうか分かりませんね。そこで、ジオメトリインスタンシング法にも切り替えれるようにして、さらにそれぞれの実行速度も表示できるようにしています。
実行速度の比較が出来れば、どちらが良いか一目瞭然でしょう。
ビルド環境と実行環境
まずビルド環境/実行環境ともに、OS は 64bit 版 Windows のみ(Windows 10 以外は未確認)です。また使用言語は C# となっています。「WebGL 版テッセレーションビューワ」は一応マルチプラットフォームだったので、制限が加わり申し訳ないです。
また WebGL 版では デモ を用意したので、即座に動きを見ることが出来ましたが、OpenTK 版ではセキュリティ上の観点から実行ファイルは置いていません。
そのため実行させるためには、前もってビルドが必要です。面倒くさくなってしまいましたが、ご容赦願います。
インストール/ビルド/実行
まずビルドの為には、Visual Studio 2019 が必要です。ビルドについては、下記が参考になると思います。
Install environment for build / ビルド環境のインストール
あとは普通にビルド&実行できます。実行後の使用方法については、下記をご覧ください。
Usage / 使い方
なお上記リンクのページにも書いてますが、起動しただけでは 3D モデルは表示されていないので、「General タブの "Open gzjson" ボタン」を押して、gzjson ファイルを選択します。
例えば「MascuteAzur001.gzjson」を選ぶと、下記のようなウインドウが現れるはずです。(スクリーンショット 左: "General" タブ選択時, 右: "Transform" タブ選択時)
ちなみにこの 3D モデルの元データは、WebGL 版の記事と同じく、クールなびじゅつかん館長さんのツイート を使わせていただきました。
ということで、ユーザー寄りの説明はココまで。次項からは、技術的な説明に移りたいと思います。
前知識
WebGL 版の記事と同じことはあまり書きたくないので、WebGL 版では使っていない本プログラム(OpenTK 版)独自の技術について記述します。具体的には、以下の技術です。
- 独自の高次元曲面パッチを、テッセレーションシェーダを使ってテッセレーションする方法
ただし多少の前知識は必要なので、前もって WebGL 版記事の下記の項目を読んでおくと分かりやすいと思います。
高次元曲面パッチ
曲面パッチと法線の関連性
シェーダ内での法線の計算
コントロールポイント組み直し&微分値用パラメータ生成の処理
この処理は、WebGL 版記事の下記の項目の前半と同様です。
コントロールポイント組み直し&微分値用パラメータ生成の処理
しかし WebGL 版記事にあるソースコードは TypeScript で書かれているので、本プログラム(C#)での該当箇所を以下に示しました。
private void GetPosDiff(
BxCmSeparatePatch_Object patch, uint surfaceNo, out BxBezier3Line3F hPosBez0, out BxBezier6Line3F hPosBez1, out BxBezier6Line3F hPosBez2, out BxBezier3Line3F hPosBez3,
out BxBezier3Line3F vPosBez0, out BxBezier6Line3F vPosBez1, out BxBezier6Line3F vPosBez2, out BxBezier3Line3F vPosBez3, out BxBezier2Line3F hDiffBez0,
out BxBezier5Line3F hDiffBez1, out BxBezier5Line3F hDiffBez2, out BxBezier2Line3F hDiffBez3, out BxBezier2Line3F vDiffBez0, out BxBezier5Line3F vDiffBez1,
out BxBezier5Line3F vDiffBez2, out BxBezier2Line3F vDiffBez3 )
{
GetPosBezierH( patch, surfaceNo, out hPosBez0, out hPosBez1, out hPosBez2, out hPosBez3 );
GetPosBezierV( patch, surfaceNo, out vPosBez0, out vPosBez1, out vPosBez2, out vPosBez3 );
GetDiffBezierH( patch, surfaceNo, out hDiffBez0, out hDiffBez1, out hDiffBez2, out hDiffBez3 );
GetDiffBezierV( patch, surfaceNo, out vDiffBez0, out vDiffBez1, out vDiffBez2, out vDiffBez3 );
}
なおこれにより変換後の値は、1パッチ辺り「ベクトル値ベースで 80個」になります。
パラメータの配列化
WebGL 版ではパラメータ生成後、1パッチ毎に「80個の配列(配列要素はベクトル値)」に変換していました。
しかし OpenTK 版のテッセレーションシェーダを使う方法では少し違います。配列にすることに関しては同じなのですが、
- 配列の個数は1パッチ辺り 6個。ただし配列要素は、14個のメンバ変数(ベクトル値)から成るオブジェクト
という形にします。
つまりこれにより $6\times14=84$ で、計84個の値となるわけです。なお必要な個数は 80個なので少し余ることになりますが、余った部分は使わなければ良いだけなので問題ありません。
ちなみに実際のコードでは、「テッセレーション関連の整数値(tessDenom
)」も配列に入れてます。これについての詳細は割愛しますが、結果として配列に 81個の値を入れていることになります。
そしてこの処理を行っているのが、同じく BxGlShadeVbo1
クラスの SetVertexBufferOne()
関数です。
private void SetVertexBufferOne(
BxBezier3Line3F hPosBez0, BxBezier6Line3F hPosBez1, BxBezier6Line3F hPosBez2, BxBezier3Line3F hPosBez3, BxBezier3Line3F vPosBez0, BxBezier6Line3F vPosBez1,
BxBezier6Line3F vPosBez2, BxBezier3Line3F vPosBez3, BxBezier2Line3F hDiffBez0, BxBezier5Line3F hDiffBez1, BxBezier5Line3F hDiffBez2, BxBezier2Line3F hDiffBez3,
BxBezier2Line3F vDiffBez0, BxBezier5Line3F vDiffBez1, BxBezier5Line3F vDiffBez2, BxBezier2Line3F vDiffBez3, int tessDenom, uint surfaceNo, VertexInfo[] vertexAry )
{
SetVertexBuffer0( hPosBez0, hPosBez1, hDiffBez0, surfaceNo, vertexAry );
SetVertexBuffer1( hPosBez2, hPosBez3, hDiffBez3, surfaceNo, vertexAry );
SetVertexBuffer2( hDiffBez1, hDiffBez2, tessDenom, surfaceNo, vertexAry );
SetVertexBuffer3( vPosBez0, vPosBez1, vDiffBez0, surfaceNo, vertexAry );
SetVertexBuffer4( vPosBez2, vPosBez3, vDiffBez3, surfaceNo, vertexAry );
SetVertexBuffer5( vDiffBez1, vDiffBez2, surfaceNo, vertexAry );
}
このコードを見ると、SetVertexBuffer0()
~ SetVertexBuffer5()
という 6個の関数がありますが、この各関数はそれぞれ別の配列インデックスのオブジェクトに値をセットしています。
例えば SetVertexBuffer0()
の中身を見てみると、下記のようになっていることが分かります。
private void SetVertexBuffer0( BxBezier3Line3F hPosBez0, BxBezier6Line3F hPosBez1, BxBezier2Line3F hDiffBez0, uint surfaceNo, VertexInfo[] vertexAry )
{
uint dstIndex = ( surfaceNo * 6 ) + 0;
ToVector3( hPosBez0[ 0 ], ref vertexAry[ dstIndex ].pnt0 );
ToVector3( hPosBez0[ 1 ], ref vertexAry[ dstIndex ].pnt1 );
ToVector3( hPosBez0[ 2 ], ref vertexAry[ dstIndex ].pnt2 );
ToVector3( hPosBez0[ 3 ], ref vertexAry[ dstIndex ].pnt3 );
ToVector3( hPosBez1[ 0 ], ref vertexAry[ dstIndex ].pnt4 );
ToVector3( hPosBez1[ 1 ], ref vertexAry[ dstIndex ].pnt5 );
ToVector3( hPosBez1[ 2 ], ref vertexAry[ dstIndex ].pnt6 );
ToVector3( hPosBez1[ 3 ], ref vertexAry[ dstIndex ].pnt7 );
ToVector3( hPosBez1[ 4 ], ref vertexAry[ dstIndex ].pnt8 );
ToVector3( hPosBez1[ 5 ], ref vertexAry[ dstIndex ].pnt9 );
ToVector3( hPosBez1[ 6 ], ref vertexAry[ dstIndex ].pnt10 );
ToVector3( hDiffBez0[ 0 ], ref vertexAry[ dstIndex ].pnt11 );
ToVector3( hDiffBez0[ 1 ], ref vertexAry[ dstIndex ].pnt12 );
ToVector3( hDiffBez0[ 2 ], ref vertexAry[ dstIndex ].pnt13 );
}
具体的には、pnt0
~ pnt13
という 14個のメンバ変数に値を入れていますね。
なお vertexAry
は 1パッチ辺りでは「配列 6個分」ですが、実際には全パッチの情報をすべて vertexAry
に入れるので、「6$\times$パッチ数」の個数の配列となります。
VBO
全パッチについて前項の GetPosDiff()
と SetVertexBufferOne()
を行い、vertexAry
の情報を VBO に転送する処理を行っているのが、BxGlShadeVbo1.SetVertexBuffer()
です。
そしてこの関数内の GL.BufferData()
が、実際に転送を行っている箇所になります。
protected override void SetVertexBuffer( BxCmSeparatePatch_Object patch )
{
if( Buf.HasVertexBuffer() == true )
Buf.ReleaseVertexBuffer();
Buf.NumVertices = ( int )( patch.NumSurface * 6 );
VertexInfo[] vertexAry = new VertexInfo[ Buf.NumVertices ];
for( uint i=0; i<patch.NumSurface; i++ ) {
GetPosDiff( patch, i, out BxBezier3Line3F hPosBez0, out BxBezier6Line3F hPosBez1, out BxBezier6Line3F hPosBez2, out BxBezier3Line3F hPosBez3, out BxBezier3Line3F vPosBez0,
out BxBezier6Line3F vPosBez1, out BxBezier6Line3F vPosBez2, out BxBezier3Line3F vPosBez3, out BxBezier2Line3F hDiffBez0, out BxBezier5Line3F hDiffBez1,
out BxBezier5Line3F hDiffBez2, out BxBezier2Line3F hDiffBez3, out BxBezier2Line3F vDiffBez0, out BxBezier5Line3F vDiffBez1, out BxBezier5Line3F vDiffBez2,
out BxBezier2Line3F vDiffBez3 );
int tessDenom = GetTessDenom( patch, i );
SetVertexBufferOne( hPosBez0, hPosBez1, hPosBez2, hPosBez3, vPosBez0, vPosBez1, vPosBez2, vPosBez3, hDiffBez0, hDiffBez1, hDiffBez2, hDiffBez3, vDiffBez0, vDiffBez1,
vDiffBez2, vDiffBez3, tessDenom, i, vertexAry );
}
GL.GenBuffers( 1, Buf.VboID );
GL.BindBuffer( BufferTarget.ArrayBuffer, Buf.VboID[ 0 ] );
GL.BufferData( BufferTarget.ArrayBuffer, new IntPtr( Buf.NumVertices * VertexInfo.Length ), vertexAry, BufferUsageHint.StaticDraw );
GL.BindBuffer( BufferTarget.ArrayBuffer, 0 );
}
VAO
前項で、GL.BufferData()
により「vertexAry
の情報を VBO に転送」しましたが、実際には vertexAry
の各要素は、14個のメンバ変数から成るオブジェクトです。そのためバーテックスシェーダのコード内では、「14個の中の任意の値が、どのメンバ変数と対応しているか」を知っていなければなりません。
これを行うための OpenGL 上での仕組みが VAO で、本プログラムでは BxGlShadeVbo1.SetVAO()
でこれを行っています。
protected override void SetVAO()
{
GL.GenVertexArrays( 1, out Buf.VaoHandle[ 0 ] );
GL.BindVertexArray( Buf.VaoHandle[ 0 ] );
GL.BindBuffer( BufferTarget.ArrayBuffer, Buf.VboID[ 0 ] );
int pnt0Location = GL.GetAttribLocation( fProgramObj.ShaderProgramID(), "vertPnt0" );
int pnt1Location = GL.GetAttribLocation( fProgramObj.ShaderProgramID(), "vertPnt1" );
:
int pnt12Location = GL.GetAttribLocation( fProgramObj.ShaderProgramID(), "vertPnt12" );
int pnt13Location = GL.GetAttribLocation( fProgramObj.ShaderProgramID(), "vertPnt13" );
GL.EnableVertexAttribArray( pnt0Location );
GL.EnableVertexAttribArray( pnt1Location );
:
GL.EnableVertexAttribArray( pnt12Location );
GL.EnableVertexAttribArray( pnt13Location );
GL.VertexAttribPointer( pnt0Location, 3, VertexAttribPointerType.Float, false, VertexInfo.Length, 0 );
GL.VertexAttribPointer( pnt1Location, 3, VertexAttribPointerType.Float, false, VertexInfo.Length, ( Vector3.SizeInBytes * 1 ) );
:
GL.VertexAttribPointer( pnt12Location, 3, VertexAttribPointerType.Float, false, VertexInfo.Length, ( Vector3.SizeInBytes * 12 ) );
GL.VertexAttribPointer( pnt13Location, 3, VertexAttribPointerType.Float, false, VertexInfo.Length, ( Vector3.SizeInBytes * 13 ) );
GL.BindVertexArray( 0 );
}
具体的には、
-
GL.GetAttribLocation()
で「バーテックスシェーダ コード内の入力変数」を attribute location という ID の形で求める。 -
GL.EnableVertexAttribArray()
で、指定した attribute location を有効化する。 -
GL.VertexAttribPointer()
で、attribute location と「バーテックスデータのパッチ内におけるオフセット」を結びつける。
ということにより、対応付けが成されるわけです。
なお上記コードを見ると「バーテックスシェーダ コード内の入力変数」として、vertPnt0
~ vertPnt13
が定義されていることが分かります。そしてこの変数名は、この後バーテックスシェーダのコードを説明する際にも出てくることになります。
ドローコール
CPU 側では、前項までで各種設定を行ってきました。そして最後にドローコールすることにより、実際に 3D モデルが表示されることになります。
これをやっているのが BxGlShaderBase
クラスの DrawMain()
関数です。
virtual protected void DrawMain( BxCmUiParam param )
{
SetFaceColor();
PatchParameter();
GL.BindVertexArray( Buf.VaoHandle[ 0 ] );
BeginBenchmark( param );
GL.DrawArrays( PrimitiveType.Patches, 0, Buf.NumVertices );
EndBenchmark( param );
GL.BindVertexArray( 0 );
}
この中で重要なのは、OpenGL(OpenTK)の Draw 系関数である GL.DrawArrays()
となります。
さらに GL.DrawArrays()
の引数として PrimitiveType.Patches
を指定していることにご注目ください。この指定により、テッセレーション系シェーダが使われることになります。
バーテックスシェーダ
ここでようやく GPU 側のプログラムに入ってきました。まずはバーテックスシェーダです。
#version 400
in vec3 vertPnt0;
in vec3 vertPnt1;
:
in vec3 vertPnt12;
in vec3 vertPnt13;
out vec3 tescPnt0;
out vec3 tescPnt1;
:
out vec3 tescPnt12;
out vec3 tescPnt13;
void main()
{
tescPnt0 = vertPnt0;
tescPnt1 = vertPnt1;
:
tescPnt12 = vertPnt12;
tescPnt13 = vertPnt13;
}
このバーテックスシェーダのコードは、上記を見てわかるように大したことは行っていません。入力変数である vertPnt0
~ vertPnt13
の値を、出力変数の tescPnt0
~ tescPnt13
に代入しているだけです。
ただ vertPnt0
~ vertPnt13
という変数名が、前述した VAO の項にも出てくることについては注目する必要があります。
すなわち CPU 側のバーテックスバッファである vertexAry
配列について、配列の各要素の 14個のメンバ変数が、GPU 側では vertPnt0
~ vertPnt13
という変数になるわけです。
テッセレーション・コントロールシェーダ
次に実行されるのは「テッセレーション・コントロールシェーダ」ですが、これも大したことはしていません。
#version 400
layout( vertices = 6 ) out;
in vec3 tescPnt0[];
in vec3 tescPnt1[];
:
in vec3 tescPnt12[];
in vec3 tescPnt13[];
out vec3 tesePnt0[];
out vec3 tesePnt1[];
:
out vec3 tesePnt12[];
out vec3 tesePnt13[];
uniform float tessLevel;
void main()
{
tesePnt0[ gl_InvocationID ] = tescPnt0[ gl_InvocationID ];
tesePnt1[ gl_InvocationID ] = tescPnt1[ gl_InvocationID ];
:
tesePnt12[ gl_InvocationID ] = tescPnt12[ gl_InvocationID ];
tesePnt13[ gl_InvocationID ] = tescPnt13[ gl_InvocationID ];
if( gl_InvocationID == 0 ) {
float tessDenom = tescPnt12[ 2 ].x;
gl_TessLevelOuter[ 0 ] = tessLevel / tessDenom;
gl_TessLevelOuter[ 1 ] = tessLevel / tessDenom;
gl_TessLevelOuter[ 2 ] = tessLevel / tessDenom;
gl_TessLevelOuter[ 3 ] = tessLevel / tessDenom;
gl_TessLevelInner[ 0 ] = tessLevel / tessDenom;
gl_TessLevelInner[ 1 ] = tessLevel / tessDenom;
}
}
以下のように、
- 入力変数の
tescPnt0[gl_InvocationID]
~tescPnt13[gl_InvocationID]
の値を、出力変数のtesePnt0[gl_InvocationID]
~tesePnt13[gl_InvocationID]
に代入する。 - パッチ境界の分割数(
gl_TessLevelOuter
)と、パッチ内部の分割数(gl_TessLevelInner
)を指定する。
を行っているだけです。
テッセレーション・エバリュエーションシェーダ(fromVbo
)
そして「テッセレーション・エバリュエーションシェーダ」のコードが実行されますが、これが GPU 側プログラムの要となります。
まずここでは、fromVbo()
という関数を見てみましょう。
void fromVbo( out vec3 hPosBez0[4], out vec3 hPosBez1[7], out vec3 hPosBez2[7], out vec3 hPosBez3[4], out vec3 vPosBez0[4], out vec3 vPosBez1[7], out vec3 vPosBez2[7], out vec3 vPosBez3[4],
out vec3 hDiffBez0[3], out vec3 hDiffBez1[6], out vec3 hDiffBez2[6], out vec3 hDiffBez3[3], out vec3 vDiffBez0[3], out vec3 vDiffBez1[6], out vec3 vDiffBez2[6], out vec3 vDiffBez3[3] )
{
hPosBez0[ 0 ] = tesePnt0[ 0 ];
hPosBez0[ 1 ] = tesePnt1[ 0 ];
hPosBez0[ 2 ] = tesePnt2[ 0 ];
hPosBez0[ 3 ] = tesePnt3[ 0 ];
hPosBez1[ 0 ] = tesePnt4[ 0 ];
hPosBez1[ 1 ] = tesePnt5[ 0 ];
:
hPosBez1[ 5 ] = tesePnt9[ 0 ];
hPosBez1[ 6 ] = tesePnt10[ 0 ];
hPosBez2[ 0 ] = tesePnt0[ 1 ];
hPosBez2[ 1 ] = tesePnt1[ 1 ];
:
hPosBez2[ 5 ] = tesePnt5[ 1 ];
hPosBez2[ 6 ] = tesePnt6[ 1 ];
hPosBez3[ 0 ] = tesePnt7[ 1 ];
hPosBez3[ 1 ] = tesePnt8[ 1 ];
hPosBez3[ 2 ] = tesePnt9[ 1 ];
hPosBez3[ 3 ] = tesePnt10[ 1 ];
vPosBez0[ 0 ] = tesePnt0[ 3 ];
vPosBez0[ 1 ] = tesePnt1[ 3 ];
vPosBez0[ 2 ] = tesePnt2[ 3 ];
vPosBez0[ 3 ] = tesePnt3[ 3 ];
vPosBez1[ 0 ] = tesePnt4[ 3 ];
vPosBez1[ 1 ] = tesePnt5[ 3 ];
:
vPosBez1[ 5 ] = tesePnt9[ 3 ];
vPosBez1[ 6 ] = tesePnt10[ 3 ];
vPosBez2[ 0 ] = tesePnt0[ 4 ];
vPosBez2[ 1 ] = tesePnt1[ 4 ];
:
vPosBez2[ 5 ] = tesePnt5[ 4 ];
vPosBez2[ 6 ] = tesePnt6[ 4 ];
vPosBez3[ 0 ] = tesePnt7[ 4 ];
vPosBez3[ 1 ] = tesePnt8[ 4 ];
vPosBez3[ 2 ] = tesePnt9[ 4 ];
vPosBez3[ 3 ] = tesePnt10[ 4 ];
hDiffBez0[ 0 ] = tesePnt11[ 0 ];
hDiffBez0[ 1 ] = tesePnt12[ 0 ];
hDiffBez0[ 2 ] = tesePnt13[ 0 ];
hDiffBez1[ 0 ] = tesePnt0[ 2 ];
hDiffBez1[ 1 ] = tesePnt1[ 2 ];
:
hDiffBez1[ 4 ] = tesePnt4[ 2 ];
hDiffBez1[ 5 ] = tesePnt5[ 2 ];
hDiffBez2[ 0 ] = tesePnt6[ 2 ];
hDiffBez2[ 1 ] = tesePnt7[ 2 ];
:
hDiffBez2[ 4 ] = tesePnt10[ 2 ];
hDiffBez2[ 5 ] = tesePnt11[ 2 ];
hDiffBez3[ 0 ] = tesePnt11[ 1 ];
hDiffBez3[ 1 ] = tesePnt12[ 1 ];
hDiffBez3[ 2 ] = tesePnt13[ 1 ];
vDiffBez0[ 0 ] = tesePnt11[ 3 ];
vDiffBez0[ 1 ] = tesePnt12[ 3 ];
vDiffBez0[ 2 ] = tesePnt13[ 3 ];
vDiffBez1[ 0 ] = tesePnt0[ 5 ];
vDiffBez1[ 1 ] = tesePnt1[ 5 ];
:
vDiffBez1[ 4 ] = tesePnt4[ 5 ];
vDiffBez1[ 5 ] = tesePnt5[ 5 ];
vDiffBez2[ 0 ] = tesePnt6[ 5 ];
vDiffBez2[ 1 ] = tesePnt7[ 5 ];
:
vDiffBez2[ 4 ] = tesePnt10[ 5 ];
vDiffBez2[ 5 ] = tesePnt11[ 5 ];
vDiffBez3[ 0 ] = tesePnt11[ 4 ];
vDiffBez3[ 1 ] = tesePnt12[ 4 ];
vDiffBez3[ 2 ] = tesePnt13[ 4 ];
}
この関数は、入力変数 tesePnt0[i]
~ tesePnt13[i]
(i
は 0~5)を、hPosBez0
、vDiffBez3
等といった「3次または6次のベジェ曲線」「2次または5次の微分値用ベジェ曲線」に代入するためのものです。
なおこれら複数のベジェ曲線によって、独自高次元曲面パッチ(Beziex パッチ)が生成されるわけですが、これに関する式については下記を参照ください。
独自高次元曲面パッチの概要 ~ Beziex パッチの頂点位置に関する式
法線に関する おさらい ~ 曲面 A と曲面 B の偏微分値の混ぜ合わせ
一つのパッチの為に大量のコントロールポイントを置く方法
本プログラムでは、1個のパッチに対して
- 80個のベクトル値をテッセレーション・エバリュエーションシェーダに送り込む
ということを行っています。このベクトル値はコントロールポイントと見なすことが出来るので、1個のパッチに 80個のコントロールポイントを使っているわけです。
しかし下記サイトを見てみると、1個のパッチの最大コントロールポイント数である GL_MAX_PATCH_VERTICES
は、GeForce GTX 970M で「32」となっています。
Compute Performance of NVIDIA GeForce GTX 970M
(「OpenGL API : GeForce GTX 970M/PCIe/SSE2」を開いて検索してください)
32 だと、80個のコントロールポイントは送り込めないですよね。ではどうすれば良いのでしょうか?
ヒントは、GL_MAX_PATCH_VERTICES
の名前に VERTICES
と書かれているというところにあります。VERTICES
は頂点のことですが、例えばバーテックスシェーダで頂点を扱う際、1個の頂点には位置情報の他に法線/色/テクスチャ座標値等を付けたりしますね。
すなわち 1個の頂点に複数のベクトル値を設定することが出来るわけです。テッセレーション・エバリュエーションシェーダでもそれは同じで、1個のコントロールポイント(頂点)に複数のベクトル値を置くことが出来ます。
実際上記サイトでは GTX 970M で、1頂点辺りの最大属性数である GL_MAX_VERTEX_ATTRIBS
は「16」となっていました。
つまり $32\times16=512$ で、1パッチ辺り最大 512個のベクトル値を置けるはずです(やったことは無いですが)。そのため 80個であれば、問題ないわけなのです。
ただし「1個の頂点に設定する属性の数」は、すべての頂点で同じにしなければなりません。そこで、
- 1コントロールポイントに設定する属性数は「14」個に固定。これは前述した
vertexAry
配列の要素(オブジェクト)が、14個のメンバ変数から成っていることに対応してます。 - 1パッチのコントロールポイント数は「6」個に固定。1パッチ辺りの
vertexAry
配列内の個数が 6個なのは、このためです。 - コントロールポイントの属性数は 14個ですが、ベジェ曲線のコントロールポイント数はそれと無関係な個数なので、うまく調整して $6\times14$ 内に収まるようにします。
- CPU 側の
vertexAry
配列の中身と、テッセレーション・エバリュエーションシェーダのfromVbo()
関数内での代入との間で整合性が取れていれば、データの引き渡しは成功するはず。
としています。
バーテックスシェーダ/テッセレーション・コントロールシェーダの補足
「バーテックスシェーダ」の項で、vertPnt0
~ vertPnt13
の値を tescPnt0
~ tescPnt13
に代入していると書きました。つまりこれらの変数は、1コントロールポイントに設定する属性ということになります。
ということは、バーテックスシェーダの 1回の実行で処理されるのは、1個のコントロールポイントということです。1パッチ辺り 6個のコントロールポイントがあるので、1個のパッチで 6回バーテックスシェーダが実行されることになりますね。
もうここまで来ると、バーテックスシェーダの名前の由来である「頂点」とは関係が薄くなってきました。。
また「テッセレーション・コントロールシェーダ」の項でも、tescPnt0[gl_InvocationID]
~ tescPnt13[gl_InvocationID]
の値を tesePnt0[gl_InvocationID]
~ tesePnt13[gl_InvocationID]
に代入するということについて書いています。
バーテックスシェーダとは異なり配列の一要素を代入する形になっていますが、これも 1回の実行で処理されるのは、1個のコントロールポイントです。
テッセレーション・エバリュエーションシェーダ(getPosNormal
)
テッセレーション・エバリュエーションシェーダの関数「fromVbo()
」は、下記の関数からコールされます。
void getPosNormal( out vec3 pos, out vec3 normalE )
{
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
vec3 hPosBez0[ 4 ], hPosBez1[ 7 ], hPosBez2[ 7 ], hPosBez3[ 4 ], vPosBez0[ 4 ], vPosBez1[ 7 ], vPosBez2[ 7 ], vPosBez3[ 4 ];
vec3 hDiffBez0[ 3 ], hDiffBez1[ 6 ], hDiffBez2[ 6 ], hDiffBez3[ 3 ], vDiffBez0[ 3 ], vDiffBez1[ 6 ], vDiffBez2[ 6 ], vDiffBez3[ 3 ];
fromVbo( hPosBez0, hPosBez1, hPosBez2, hPosBez3, vPosBez0, vPosBez1, vPosBez2, vPosBez3, hDiffBez0, hDiffBez1, hDiffBez2, hDiffBez3, vDiffBez0, vDiffBez1, vDiffBez2, vDiffBez3 );
vec3 posH, posV;
getPosition( hPosBez0, hPosBez1, hPosBez2, hPosBez3, vPosBez0, vPosBez1, vPosBez2, vPosBez3, u, v, posH, posV, pos );
getNormalE( hDiffBez0, hDiffBez1, hDiffBez2, hDiffBez3, vPosBez0, vPosBez1, vPosBez2, vPosBez3, hPosBez0, hPosBez1, hPosBez2, hPosBez3,
vDiffBez0, vDiffBez1, vDiffBez2, vDiffBez3, u, v, posH, posV, normalE );
}
これを見ると分かるように、fromVbo()
の実行後、getPosition()
や getNormalE()
がコールされています。これらの関数は、分割後の頂点位置や法線を算定するためのものですが、詳細は下記リンクをご覧ください。
このリンクは WebGL 版の記事なので言語等が異なりますが、何となく分かるのではないでしょうか?
Beziex パッチの式(頂点位置)の実装
u 方向の偏微分値の式(曲面 A) ~ 曲面 A と曲面 B の偏微分値の混ぜ合わせ
ちなみにテッセレーション・エバリュエーションシェーダでは、u および v の値を取得する際に gl_TessCoord
を使います。
Tessellation algorithm / Surface type / GPU Benchmark (FPS)
本プログラムの UI の、General タブには「Tessellation algorithm」「Surface type」「GPU Benchmark (FPS)」という項目があります。
Tessellation algorithm
Tessellation algorithm では、「Tessellation shader アルゴリズム」と「Instance geometry アルゴリズム」を切り替えることが出来ます。
このうち Tessellation shader は本稿でこれまで述べてきたアルゴリズムです。Instance geometry は WebGL版で使用しているアルゴリズムですが、本プログラムにも搭載されているので切り替えが可能なのです。
なお少し詳しい情報は、下記に載っています。
Tessellation algorithm / テッセレーション・アルゴリズム
Surface type
「Solid」はシェーディング面表示、「Wire」はワイヤーフレーム表示です。
Wire はオマケですが、ポリゴン分割の状態を見る際に分かりやすいと思います。
GPU Benchmark (FPS)
本プログラムのベンチマークは「一つのフレームを作り上げるための全ての時間」を示すものでは無く、ドローコールの時間を計るものです。詳細は下記に述べています。
GPU Benchmark (FPS) / GPU ベンチマーク (FPS)
GPU Benchmark (FPS) の実行例
以下に、「i7-6700HQ + GTX 970M + 64bit 版 Windows 10 Pro (1903)」上で、3D モデルとして「MascuteAzur001.gzjson」、分割数 8 で測定した実行例を載せました。ただし実際には Analyze ボタンを押す度に数値は変わるので、あくまでも目安と考えてください。
Tessellation algorithm | Surface type | FPS |
---|---|---|
Tessellation shader | Solid | 1682.2386 |
Wire | 280.6474 | |
Instance geometry | Solid | 485.1686 |
Wire | 306.8509 |
上記の環境では、Wire 選択時のベンチマークがかなり遅いのですが、ゲーム等でワイヤーフレーム表示を行うことは少ないので、とりあえず無視しましょう。
そんなわけで Solid どうしで比較すると、
- Tessellation shader アルゴリズムは、Instance geometry アルゴリズムの約 3.5倍速い
ということが分かります。ちなみに分割数を上げると、さらに速度差が広がりますよ。
終わりに
ということで、WebGL 版テッセレーション ビューワに続いて、OpenTK 版も作ってみたのでした。
ちなみに本プログラム (C#) の UI は、
- WPF + Prism + ReactiveProperty + Material Design In XAML
で作っています。つまり MVVM パターンなのですが、これについては何も説明してませんでしたね。
何か機会がありましたらその時に、ということで。。。