pow 関数の挙動が昔と今で変わった!という話を聞いたので調査することにしました。
今回はスペキュラーの計算で使用している pow 関数の計算が妙なことになっているとのことだったので同じようにスペキュラの計算を行ってみることにします。なおインターネットで調べてみると、同じような症状に出遭っているような文面を見かけることができました。
傾き指向プログラミング / Direct3D 11のカリング設定
こちらの方では、「Specularの計算中powを使うのですが、これの結果がやたらとマイナスの値になるため、最終的に合算した色が真っ黒になるようです。」 と記述しています。
実験開始
昔は~といっている部分についてですが、これは DirectX 9.0c の世代のもので、 Shader Model 2.0 もしくは 3.0 あたりのものを指し示していると考えられます。そして、現在挙動が変わってしまったと言っていたものは、 DirectX 11.0 世代で、 Shader Model 5.0 を指し示していると考えられます。
この2つの環境を準備してみて、差異が出てくるかどうかを見てみることにします。
シェーダーコード
使用したシェーダーコードは以下のようになっています。ほぼ DirectX9.0/11.0で同様のものとなっています。
// for DirectX9 struct VS_OUTPUT { float4 Position : POSITION; float3 Normal : NORMAL; float4 WorldPos : TEXCOORD0; }; float4 EyePosition; float4 LightDir; float4 main(VS_OUTPUT _In ) : COLOR { float3 e = normalize( EyePosition .xyz - _In.WorldPos.xyz); float3 l = normalize( LightDir.xyz ); float k = dot( normalize(e + l), normalize(_In.Normal.xyz) ); float spc = pow( k, LightDir.w ); float4 result = float4(_In.Normal.xyz*0.5+0.5,1); result.xyz += spc; return result; }
// for DirectX11 struct VS_OUTPUT { float4 Position : SV_POSITION; float3 Normal : NORMAL; float4 WorldPos : TEXCOORD0; }; float4 EyePosition; float4 LightDir; float4 main(VS_OUTPUT _In ) : SV_TARGET { float3 e = normalize( EyePosition .xyz - _In.WorldPos.xyz); float3 l = normalize( LightDir.xyz ); float k = ( dot( normalize(e + l), normalize(_In.Normal.xyz) ) ); float spc = pow( k, LightDir.w ); float4 result = float4(_In.Normal.xyz*0.5+0.5,1); result.xyz += spc; return result; }
これらを fxc コンパイラでコンパイルしてみて、シェーダーアセンブリを比較してみます。使用する fxc ですが、DirectX 9.0 版のほうは、 DirectX SDK 2010 June に含まれる fxc を使用しました。一方で、 DirectX11.0 版のほうでは、 Visual Studio 2015 の開発環境で参照されるものを使用しました。
それぞれバージョンは以下のようになっています。
世代 | バージョン |
---|---|
DirectX9 | Microsoft (R) Direct3D Shader Compiler 9.29.952.3111 |
DirectX11 | Microsoft (R) Direct3D Shader Compiler 10.1 (using C:\Program Files (x86)\Windows Kits\10\bin\x86\D3DCOMPILER_47.dll) |
コンパイル後のアセンブリ結果は以下のようになりました。pow 計算の部分に差異が見られます。
DirectX9 のほうでは、 pow 関数は組み込み関数を使用しているようです。一方 DirectX11 版では、 pow 関数部分は log,exp の関数を使用するように最適化されていることがわかります。
pow 関数がこのように最適化される話については、「Low-level Thinking in High-level Shading Languages」 とう GDC 2013 のスライドにて説明があります。(日本語訳されたスライドを公開してくれている方がいらっしゃったのでそちらの方をリンクしてあります。感謝)
なお、 pow 関数の引数について、コンパイル時に警告が発生します。pow(f,e) の f は負数はダメだよという警告です。この時点でとても怪しい動きをしそうな感じですね。
実行結果
DirectX9.0, 11.0 版で球体を描画するプログラムを作成しました。その中で先ほどのシェーダーコードを使用しています。実行結果は以下のようになりました。
見事に差が出てきました! 素直に差異が出てきてくれたことに一安心です。
考察
差異が出てきた部分を見ると、 pow 関数に負数が入る箇所となっているようでした。シェーダーコンパイル時の警告は正しかったわけです。
つまり pow関数に渡す乗数が、偶数べき乗数であっても、 数値が負数なのはマズイということのようです。結果は正になるだろうという想定が崩れる箇所はここにあります。
さらにシェーダーの計算部分についてもう1段潜ってみることにしました。
DirectX9の場合
PIX を用いて、pow 計算付近でどのようになったかを確認してみます。
PIX のシェーダーデバッグの機能により、 DirectX11 版で黒く描画されてしまう領域で値の確認をしてみました。
上記の画像はデバッグ過程のものです。
pow 計算は r0.x, c1.w によって行われています。直前の内積の結果が負数であることが確認できます。そして計算結果は r0.w に格納されているのですが、ゼロが格納されているのが分かります。
DirectX9.0 Shader Model 3.0 の環境で 組み込み pow 関数は 負数の入力でゼロを返しているようです。
DirectX11の場合
同様にシェーダーのデバッグを行ってみました。全く同一のピクセル位置を選べなかったのですが、内積結果が負数となる部分を確認してみます。
上記の画像はデバッグ過程のものです。VisualStudio 2015 に備わっているグラフィックスデバッグの機能で行っています。
こちらはシェーダーのソースコードを表示しつつ、変数の確認が出来ています。ここで pow の戻り値を見てみると、 NaN が格納されていることが分かります。その後、出力カラーの計算で、この NaN を加算されることで結果が狂ってしまったということが考えられます。
自分は NVIDIA Geforce を使用しており、この環境では、この NaN は、カラーバッファに描かれるときに 0 へと処理されたようです。他の環境では、NaN がそのまま格納されたり、他の値へ丸め込まれたりする可能性はあるのではないかと思います。 たとえば +Inf などに丸め込められて、物体が真っ白に光るような結果になったりしそうです。
まとめ
確かに挙動の差が確認できました。
しかし、一応シェーダーのコンパイル時には警告が出ている(出ていた)ようなので、正しくコードを書いていれば問題にはならなかったのでは?と思いました。今回の場合で言えば、dot 計算後には saturate や abs などで値を0以上の値になるようにクリッピングしておく処理が必要です。
※ なお HLSL ShaderModel 3付近の pow には、引数の定義域が明記されていないようにも見えるので、どこが悪いのかは判定不能です。
昔はリソースが限られていたこともあり、おそらく未定義の結果はいい感じで値が丸め込められていたのでしょう。DirectX9.0世代のシェーダーコードを参考にしつつ、今のシェーダーコードへ移植する場合には丁寧に行う必要がありそうです。
コメント
突然すいません
以前から訪問させてもらっていますが
ここ数日、このサイトにアクセスしようとすると
最初は良いのですが読み込みが終わると
危険なサイトに飛ばされるのですが
何か攻撃受けてサイト改ざんされていませんか?
お知らせありがとうございます!
即座に調査して修正したいと思います
追記:プラグイン設定の見直し・一時停止で対応策をひとまずおこないました。近いうちにしっかりした対応を行う予定です
復旧処理まで適用して手直したつもりです。
もしもまだ警告が出てくるようであればお知らせ頂ければと思います。