頂点シェーダーのコマンド


~何ができる、何をする~




■はじめに

頂点シェーダーの使い方を紹介しましたが、実際には何ができるのでしょうか?
いったんここでまとめてみたいと思います。
さらに、いくつかの基本計算の方法を紹介したいと思います。
今回のネタは、nVIDIA のホームページに置いてあった、

IMatthias Wolka, “Where Is That Instruction? How To Implement "Missing" Vertex Shader Instructions”

から、かっさらってきました。

■頂点シェーダーの概要

そもそも頂点シェーダーとは、何をするものでしょうか?
ます、下の図を見てください。

以前は、透視変換の部分までが、CPU の役割でした。 ところが GeForce 等の高性能なビデオチップの登場により、 光源計算、透視変換がビデオチップでできるようになりました。 さらにそれらは、プログラミングによって、光源計算の内容が変えられます。
この、(主にビデオチップによる演算の為の)光源、透視変換プログラムが、頂点シェーダープログラムです。 それらのプログラム及び、そのレンダリング方法が頂点シェーダーです。

では、何をすればいいのでしょうか?
下を見てください。

基本的に、頂点入力レジスタ v0~v15 を加工して、頂点出力レジスタに出力します。
途中で、作業用のレジスタとして、r0~r11 が使えます。
さらに、変換行列や、光源の色の為の定数レジスタ c0~c95 を読み込むことができます。
vs1.1以上では、定数レジスタにc[a0.x+n]の使い方ができます。 定数 a0.x を次々切り替えると、多数の光源の処理が手短にできます。

注意点として、cn, vn は、一命令に一回しか使えません。つまり、

add r0, c0, c1

は、駄目です。

出力レジスタは、次のものがあります。 使うものだけ出力すれば OK です。

oDn 2個出力データ レジスタ:頂点カラー データを出力するために使用
oPos1個出力座標:同次クリッピング空間内の位置座標。クリッピングのために必要
oTn 4(2)個出力テクスチャ座標:テクスチャ座標として使用される出力データ レジスタの配列
oPts1(0)個出力位置座標サイズ レジスタ:ポイント サイズのスカラー。x 要素だけ使用。
oFog1(0)個出力フォグ値レジスタ:補間された後フォグ テーブルに転送されるフォグ係数。x 要素だけ使用。

個数は、(4次元浮動小数ベクトル)レジスタの個数です。 括弧の中の個数は、ピクセルシェーダーがサポートされていない場合のレジスタの個数です。
まぁ、これだけしかないと思うと、気分が楽です。

■命令

では、どんな命令があるかというと、まず、バージョン設定命令と定数宣言命令があります。

vs. mainVer . subVer          タイプおよびバージョンを指定
def cn     f0, f1, f2, f3     定数宣言

定数宣言命令は DXAssembleShaderFromFile で、使うことを宣言しないと、使えないので注意しましょう。

算術命令としては、次があります。

命令 動作
nop 何もしない
mov out, in out へ in の内容をコピーする。
mul out, in1, in2 out に in1 と in2 の積を代入する。
add out, in1, in2 in1 と in2 を加算する。
sub out, in1, in2 in1 と in2 を減算する。
mad out, in1, in2, in3 in1 と in2 の乗算の結果に in3 を加算して、dst に代入する。
rcp out, in.w 逆数
out.x = out.y = out.z = out.w = 1 / in
rsq out, in in の逆数平方根
out.x = out.y = out.z = out.w = 1/sqrt(in)
dp3 out, in1, in2 3 要素の内積
dp4 out, in1, in2 4 要素の内積
dst out, in1, in2 距離ベクトル
in1 ベクトルは (NA,d*d,d*d,NA)
in2 は (NA,1/d,NA,1/d)
out は (1,d,d*d,1/d)
lit out, in 光係数。out.z = (in.x<0) ? 0 : (in.y^in.w)
  • in.x = n ・ l (単位法線ベクトルと光ベクトル)
  • in.y = n ・ h (単位法線ベクトルと半角ベクトル)
  • in.z は使用されない。
  • in.w = べき乗 (+128 ~ -128じゃなきゃだめ)
min out, in1, in2 最小値 out = (in1 < in2) ? in1 : in2
max out, in1, in2 最大値 out = (in1 < in2) ? in2 : in1
slt out, in1, in2 未満 out = (in1 < in2) ? 1 : 0
sge out, in1, in2 以上 out = (in1 >= in2) ? 1 : 0
expp out, in.w 2^x の部分精度
  • out.x = 2 ^ (int)in.w
  • out.y = 小数部(in.w)
  • out.z = 2 ^ in.w (近似解)
  • out.w = 1.0
logp out, in.w log2(x) の部分精度
  • out.x = exponent((int)in.w)
  • out.y = mantissa(in.w)
  • out.z = log2(in.w) (近似解)
  • out.w = 1.0

以上は、一命令一クロックで実行されます。これ以外に、マクロ(複合)命令として、次のようなものがあります。
内部で最適化されるので、それぞれをべたで展開するよりも、速さは高速です。

命令 動作
m3x2 out, in0, in1 3 × 2 ベクトル行列の乗算 (2クロック以下)
  • dp3 out.x, in0, in[1]
  • dp3 out.y, in0, in[2]
m3x3 out, in0, in1 3 × 3 ベクトル行列の乗算 (2クロック以下)
  • dp3 out.x, in0, in[1]
  • dp3 out.y, in0, in[2]
  • dp3 out.z, in0, in[3]
m3x4 out, in0, in1 3 × 4 ベクトル行列の乗算 (2クロック以下)
  • dp3 out.x, in0, in[1]
  • dp3 out.y, in0, in[2]
  • dp3 out.z, in0, in[3]
  • dp3 out.w, in0, in[4]
m4x3 out, in0, in1 4 × 3 ベクトル行列の乗算 (2クロック以下)
  • dp4 out.x, in0, in[1]
  • dp4 out.y, in0, in[2]
  • dp4 out.z, in0, in[3]
m4x4 out, in0, in1 4 × 4 ベクトル行列の乗算 (2クロック以下)
  • dp4 out.x, in0, in[1]
  • dp4 out.y, in0, in[2]
  • dp4 out.z, in0, in[3]
  • dp4 out.w, in0, in[4]
exp out, in 指数2^xの完全精度(12クロック以下)
log out, in log2(x) の完全浮動小数点精度(12クロック以下)
frc out, in 小数部 (3クロック以下)

また、修飾子と分類される特殊な方法があります。
添え字を使って、成分の入れ替えなどをします。

命令 動作
r.{x}{y}{z}{w} 成分出力マスク
ex. mov r0.x r1 (x成分だけのコピー)
-r 符号反転
ex. add r0, r0, -r1 = sub r0, r0, r1
r.[xyzw][xyzw][xyzw][xyzw] 成分の入れ換え
ex. mov r0 r1.yzwx (r0.x=r1.y, r0.y=r1.z, r0.z=r1.w, r0.w=r1.x)

では、以上の基本命令から、いろいろな計算をしていきましょう。

■行列計算

最初は、行列の計算です。
c0~c3 の4つのベクトルからなる行列に、ベクトル r1 を作用して、r0 に入れます。
演算の各成分はベクトルと行列の成分の積の和なので、内積で表現できます。
従って、下の計算でOKです。

dp4   r0.x,  c0,  r1
dp4   r0.y,  c1,  r1
dp4   r0.z,  c2,  r1
dp4   r0.w,  c3,  r1

■転置行列

行列の行と列を入れ替えたものが転置行列です。
実は、D3DX で作られる行列は、上の計算では上手く演算できません。転置する必要があります。 転置行列の計算ができれば、それがいいのですが、それができます。

mul   r0,  c0,  r1.x
mad   r0,  c1,  r1.y,  r0
mad   r0,  c2,  r1.z,  r0
mad   r0,  c3,  r1.w,  r0

これを上手く使えば、外部で転置するオーバーヘッドがなくなるので、少し有利かもしれません。

■定数レジスタを消費しない方法

定数レジスタは、96個しかないので、無駄に使わないに越したことはありません。
通常、便利な定数

def c0 0.0f, 0.5f, 1.0f, 2.0f

を使って、計算を楽にするものですが、ホントにカリカリにチューニングすると、 それらのレジスタを使うのさえ、もったいなくなります(ホントか?俺はそんな場面に出会ったことは無いぞ)。
ということで、r0 = r1 * c0 + 1.0f を例にとって、効率化を考えます。
さて、上記の計算をするときは、最初に次のように組んでしまいます。

; だめな例
; r0 = r1 * c0 + 1.0f

def c1 1.0f, 0.0f, 0.0f, 0.0f
mad r0, r1, c0, c1.x             ; × : 定数レジスタ cnは、一つしか使えません

ですが、これは動きません。
なぜなら、定数レジスタは、レジスタの引数に一つしか使えないからです。
ということで、二行に分けて実行します。

; 安直な例
; r0 = r1 * c0 + 1.0f

def c1 1.0f, 0.0f, 0.0f, 0.0f
mov r0.x, c1.x
mad r0, r1, c0, r0.x

mov 命令は、r0.x をコピーするだけなので、もったいないですね。
実は 1.0f という値は、slt, sge の結果の値に存在します。 ということで、sge の判定を必ず true にすることで、1.0f を引き出せば、定数レジスタを一つ浮かすことができます。

; 定数レジスタを使わない例
; r0 = r1 * c0 + 1.0f

sge r0, r0, r0                  ; r0 = (1.0f, 1.0f, 1.0f, 1.0f)
mad r0, r1, c0, r0

逆に 0.0f を使いたいときは、

slt r0, r0, r0                  ; r0 = (0.0f, 0.0f, 0.0f, 0.0f)

とすればいいです。

■冪乗

ライティング用関数 lit は、実はべき乗計算に利用できます。
ただ、次の注意点があるそうです。

Iべきの範囲は [-128, +128] に自動的に制限される
II元の数は正でなければ行けない
III結果も 8 桁分だけしか保証されない。

以上の制限がありますが、べき乗の計算は1命令でできます。

; r0.z = r1.x^r1.y
lit r0.z, r1.xxyy                  ; r1.x は正でなければいけない

うん、コリャ効果でかい。

■小数部

小数部を導出する命令は、DX8 では、マクロ命令で用意されていますが、nVIDIAは『使ってはいけない』といいます。
実は、expp の y 成分は、入力の w 成分の小数部が入るので、入力を工夫することにより小数部を導出します。

; r1.xyzw の小数部を r0.xyzw に入れる
expp  r0.y,  r1.x                  ; r0.y = 小数部(r1.x)
mov   r0.x,  r0.y
expp  r0.y,  r1.z
mov   r0.z,  r0.y
expp  r0.y,  r1.w
mov   r0.w,  r0.y
expp  r0.y,  r1.y

■整数部

元の数から、小数部引いたものが整数部です。

; r0.y = 整数部(r1.y)
expp  r0.y,  r1.y
add   r0.y,  r1.y, -r0.y

■値より大きい最小の整数

現在の数より大きい整数で、最小のもの(floor(a+1))を求めます。

; r0.y = 天井数(r1.y)
expp  r0.y, -r1.y
add   r0.y,  r1.y, r0.y


■絶対値

-a と a を比較しすると、大きい方は|a|なので、次で絶対値が取れます。

; r0 = |r1|
max   r0,  r1, -r1

■割り算

Vertex Shader では、割り算はありません。 逆数を求める命令はあるので、どうしても使いたい場合は、これを使います。

; r0.x = r1.x / r2.x
rcp   r0.x,  r2.x               ; r0.x = 1 / r2.x
mul   r0.x,  r1.x,  r0.x        ; r0.x = r1.x * (1/r2.x) = r1.x / r2.x

■平方根

逆数平方根を求める命令があるので、さらに、その逆数をとっても平方根は計算できるのですが、 逆数計算は誤差が発生しますので、逆数平方根と元の数の積をとって、平方根を求めるのが、よりベターです。

; r0.x = sqrt(r1.x)
rsq   r0.x,  r1.x               ; r0.x = 1/sqrt(r1.x)
mul   r0.x,  r1.x,  r0.x        ; r0.x = r1.x * (1/sqrt(r1.x)) = sqrt(r1.x)

■以下、超過

以上、未満はありますが、以下、超過はありません。次で、計算します(全部まとめました)。

sge   r0,  r1,  r2        ; r0 = (r1 >= r2) ? 1 : 0
sge   r0, -r1, -r2        ; r0 = (r1 <= r2) ? 1 : 0
slt   r0,  r1,  r2        ; r0 = (r1 <  r2) ? 1 : 0
slt   r0, -r1, -r2        ; r0 = (r1 >  r2) ? 1 : 0

■等しい、等しくない

組み合わせて、等しい時を計算します。

; r0 = (r1 == r2) ? 1 : 0;
sge   r0, -r1, -r2
sge   r2,  r1,  r2
mul   r0,  r0,  r2
; r0 = (r1 != r2) ? 1 : 0;
slt   r0,  r1,  r2
slt   r2, -r1, -r2
add   r0,  r0,  r2

■符号関数

さらに、応用して、符号を引き出すこともできます。

;       1 (0  < r0)
; r0 =  0 (r0 == 0)
;      -1 (r0 <  0)
def   c0,  0.0f,  0.0f,  0.0f,  0.0f
slt   r1,  r0,  c0.x
slt   r0, -r0,  c0.x
add   r0,  r0, -r1

■もし-ならば

Vertex Shader では、分岐命令そのものはありませんが、状態を見て、値を変えることはできます。

; r0 = (r1 >= r2) ? r3 : r4
sge   r0,  r1,  r2
add   r1,  r3, -r4
mad   r0,  r0,  r1, r4

但し、r3, r4 等のどちらかが無限大の時には、絶対に無限大になる等、一部怪しいので、 確実性を求めるときは、次を使うのがよろし。

def   c0,  1.0f,  1.0f,  1.0f,  1.0f
sge   r0,  r1,  r2
add   r1,  c0, -r0
add   r1,  r0,  r4
mad   r0,  r0,  r3,  r1

■[0,1] の間の範囲制限

値を0から、1の間に制限します。

; r0 = (r0 < 0) ? 0 : (1 < r0) ? 1 : r0
def   c0,  0.0f, 1.0f, 0.0f, 0.0f
max   r0,  r0,  c0.x
min   r0,  r0,  c0.y

■外積

成分入れ替えのいい例ですね。

; r0 = r1 × r2
max   r0,  r1.yzxw,  r2.zxyw
mad   r0, -r2.yzxw,  r1.zxyw,  r0

■cos sin

冪展開で、cos, sin を求めます。 おそらく数値計算的に合っているのでしょうから、ここでは紹介にとどめます。

; r0.x = cos(r1.x)
;
def c0, 0.00f,  0.50f,  1.00f,  0.0f
def c1, 0.25f, -9.00f,  0.75f,  1.0f/(2.0f*PI)
def c2, 24.9808039603f, -24.9808039603f, -60.1458091736f, 60.1458091736f
def c3, 85.4537887573f, -85.4537887573f, -64.9393539429f, 64.9393539429f
def c4, 19.7392082214f, -19.7392082214f, - 1.0f,           1.0f
mul   r1.x,   c1.w,    r1.x
expp  r1.y,   r1.x
slt   r2.x,   r1.y,    c1
sge   r2.yz,  r1.y,    c1
dp3   r2.y,   r2,      c4.zwzw
add   r0.xyz,-r1.y,    c0
mul   r0,     r0,      r0
mad   r1,     c2.xyxy, r0, c2,zwzw
mad   r1,     r1,      r0, c3,xyxy
mad   r1,     r1,      r0, c3,zwzw
mad   r1,     r1,      r0, c4,xyxy
mad   r1,     r1,      r0, c4,zwzw
dp3   r0.x,   r1,     -r2
; r0.x = sin(r1.x)
;
def c0, 0.00f,  0.50f,  1.00f,  0.0f
def c1, 0.25f, -9.00f,  0.75f,  1.0f/(2.0f*PI)
def c2, 24.9808039603f, -24.9808039603f, -60.1458091736f, 60.1458091736f
def c3, 85.4537887573f, -85.4537887573f, -64.9393539429f, 64.9393539429f
def c4, 19.7392082214f, -19.7392082214f, - 1.0f,           1.0f
mad   r1.x,   c1.w,    r1.x, -c1.x
expp  r1.y,   r1.x
slt   r2.x,   r1.y,    c1
sge   r2.yz,  r1.y,    c1
dp3   r2.y,   r2,      c4.zwzw
add   r0.xyz,-r1.y,    c0
mul   r0,     r0,      r0
mad   r1,     c2.xyxy, r0,    c2,zwzw
mad   r1,     r1,      r0,    c3,xyxy
mad   r1,     r1,      r0,    c3,zwzw
mad   r1,     r1,      r0,    c4,xyxy
mad   r1,     r1,      r0,    c4,zwzw
dp3   r0.x,   r1,     -r2

■速い cos sin

以上の命令は正確ですが、遅いです。 次の計算が(少々不正確ですが)速い解を与えます。

; r0.x = cos(r1.x)
; r0.y = sin(r1.x)
;
def c0, PI,    1.0f/2.0f,  2.0f*PI,     1.0f/(  2.0f*PI)
def c1, 1.0f, -1.0f/2.0f,  1.0f/24.0f, -1.0f/ 720.0f
def c1, 1.0f, -1.0f/6.0f,  1.0f/120.0f,-1.0f/5040.0f
mad   r0.x,   r1.x,    c0.w,   c0.y
expp  r0.y,   r0.x
mad   r0.x,   r0.y,    c0.z,  -c0.x
dst   r2.xy,  r0.x,    r0.x
mul   r2.z,   r2.y,    r2.y
mul   r2.w,   r2.y,    r2.z
mul   r0,     r2,      r2.x
dp4   r0.y,   r0,      c2
dp4   r0.x,   r2,      c1

■その間の cos sin

速い版よりは正確だけど、正確な版で sin / cos を計算するよりは速い版です。

; r0.x = sin(r1.x)
;
def c0, 0.25f,  0.50f,  0.75f,  1.0f
def c1,-24.9808039603f,  60.1458091736f, -85.4537887573f, 64.9393539429f
def c2,-19.7392082214f,   1.0f,          - 1.0f,           1.0f/(2*PI)
mul   r1.x,   c2.w,     r1.x
expp  r1.y,   r1.x
slt   r2.x,   r1.yyyy,  c0
add   r2.yzw, r2.xyzw, -r2.xxyz
dp3   r1.z,   r2.yzwx,  c0.yywx
dp4   r1.w,   r2,       c0.xxzz
add   r0.xz,  r1.yyyy, -r1.zzww
mul   r0.xz,  r0.xxzz,  r0.xxzz
mul   r0.yw,  r0.xxzz,  r0.xxzz
mad   r1,     c1.xyxy,  r0.yyww,    c1,zwzw
mad   r1,     r1,       r0.yyww,    c2,xyxy
mad   r1.xz,  r1,       r0.xxzz,    r1,yyww
dp4   r0.x,   r2,       c2.yzzy
dp4   r0.y,   r2,       c2.yyzz
dp3   r0.xy,  r0.xyww,  r1.xzww

■倍精度指数対数

より精度の高い exp, log を求めるためのものです。

; r0.z = 2^(r1.z)
;
def c0, 1.00000000,     -6.93147182e-1,  2.40226462e-1,  -5.55036440e-2
def c1, 0.61597636e-3,  -1.32823968e-3,  1.47491097e-4,  -1.08635004e-5
exp   r0.xy,  r1.z
dst   r1,     r0.y,     r0.y
mul   r1,     r1.xxxy,  r1.xxxy         ; 1,x,x^2,x^3
dp4   r0.z,   r1,       c0
dp4   r0.w,   r1,       c1
mul   r1.y,   r1.z,     r1.z
mad   r0.w,   r0.w,     r1.y,  r0.z
rcp   r0.w,   r0.w
mul   r0.z,   r0.w,     r0.x
; r0.z = log2(r1.x)
;
def c0, 1.44268966,     -7.21165776e-1,  4.78684813e-1,  -3.47305417e-1
def c1, 2.41873696e-1,  -1.37531206e-1,  5.20646796e-2,  -9.31049418e-3
def c2, 1.0f             0.0f,           0.0f,            0.0f
log   r0.x,   r1.x
add   r0.y,   r0.x,    -c2.x
dst   r1,     r0.y,     r0.y
mul   r1,     r1.xxxy,  r1.xxxy         ; 1,x,x^2,x^3
dp4   r0.z,   r1,       c0
dp4   r0.w,   r1,       c1
mul   r1.y,   r1.z,     r1.z
mad   r0.w,   r0.w,     r1.y,  r0.z
mad   r0.x,   r0.w,     r0.y,  r0.x

■まとめ

今回のネタは、BBXでボケをかましたときに、Kano さんから教えていただいたことが元になっています。
見直してみると、XFC 2001 での発表はかなり無駄があります。
僕は、『とりあえず動けやいいや』とやってしまうことが多いので、細かな高速化や誤差の見積もりおざなりにしがいです。
こういった論文を読んだりすると、細かなひらめきで絶妙な解決がしてあったりするので、かなりハッとします。 ということで、今回一番ためになったのは、ほかならぬ僕でした。
これからもガンバるぞ。





もどる

imagire@gmail.com