連載
.NET&Windows Vistaへ広がるDirectXの世界

第7回 プログラマブル・シェーダによる積極的なGPUの活用

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2007)
2007/05/08

ピクセル・シェーダによるマンデルブロ集合の計算と描画

 サンプル2を開いていただきたい。

 マンデルブロ集合は次のような数列が発散しない複素平面上の点cの集合である。

P(0) = 0
P(n+1) = P(n)^2 + c

 数学的には、P(n)が2を超えれば必ず発散することが分かっている。マンデルブロ集合の可視化方法はいろいろあるが、ある上限を決めて数列を計算し、何回目の計算で2を超えたか、つまり発散速度を基に色を付ける方法が有名である。

 次のHLSLコードは、サンプル2のエフェクト・ファイルから、ピクセル・シェーダに関係する部分を抜き出したものだ。

float Aspect = 1.0f;
float Zoom = 1.0f;
float2 Offset = float2(0.0f, 0.0f);

PixelOut MandelbrotShader(PixelIn input, uniform int MaxIterate)
{
  const float2 c =
    (input.UV - 0.5) * float2(1, Aspect) * Zoom + Offset;

  float2 p = 0;
  int n = 0;
  for (n = 0; n < MaxIterate && dot(p,p) < 4.0f; ++n)
  {
    p = float2(p.x * p.x - p.y * p.y, 2.0f * p.x * p.y ) + c;
  }

  PixelOut output;
  if (n < MaxIterate)
  {
    float u = (float)n / (float) MaxIterate;
    output.Color.rgb = float3(u, u, 1.0f - u);
  }
  else
  {
    output.Color.rgb = 0.0f;
  }
  output.Color.a = 1.0f;
  return output;
}
ピクセル・シェーダに関係する部分のコード
UV座標から画面上の位置を求め、その点をcとして数列の反復計算を行っている。反復の停止条件はMaxIterate回反復が行われるか(発散しないと見なす)、pの絶対値が2を超えること(発散が確定)である。停止の仕方によって出力色を変えている。
発散が確定した場合は、そのときの反復回数を基に出力色を決めている。
一方、MaxIterate回反復しても発散することが決まらなかった場合は、黒色を出力する。厳密には、マンデルブロ集合という言葉が指すのはこの黒色の部分のことである。

 描画ステート側も、ピクセル・シェーダの変更に合わせて修正を行っている。

technique Render
{
  pass Pass0
  {
    VertexShader = compile vs_3_0 MyVertexShader();
    PixelShader = compile ps_3_0 MandelbrotShader(128);
  }
}
プログラマブル・シェーダの設定
MandelbrotShaderメソッドのパラメータに渡されている128という値は、マンデルブロ集合を計算するときのループ回数である。この値を大きくするときは注意が必要で、1024といった大きな値を指定すると、現行のHLSLコンパイラは途中でループを打ち切るコードを出力してしまうようだ。ループ回数を増やす方法については、サンプル3を参考にしてほしい。

 次に、マンデルブロ集合シェーダで使用する3つの変数、Aspect、Zoom、OffsetをC#のプログラム側から変更する方法について説明する。変数の内容を読み書きするには、EffectクラスのParametersプロパティを使用する。

Effect effect;

Viewport vp = graphics.GraphicsDevice.Viewport;
float aspectRatio = (float)vp.Height / (float)vp.Width;
effect.Parameters["Zoom"].SetValue(zoom);
effect.Parameters["Aspect"].SetValue(aspectRatio);
effect.Parameters["Offset"].SetValue(offset);
エフェクト・ファイル内で定義された変数の読み書き
Parametersプロパティのインデクサに変数名を渡すことで、変数の内容にアクセスできる。エフェクト・ファイル内の変数の型とC#側の変数の型が一致しているかは実行時に検証される。この検証をコンパイル時に行いたければ、ビルド・プロセスを拡張し、エフェクト・ファイルから型付けされたラッパー・クラスを生成するなどのアプローチが有効だろう。

 SetValueメソッドは多数の基本的な型でオーバーロード定義されており、float型やVector2型をキャストなしでそのまま渡すことができる。サンプル・コードにあるように、Xbox360コントローラやキーボード操作によってoffsetとzoomの値を変化させることで、マンデルブロ集合の上を移動したり拡大したりといったアニメーションが可能になる。色の変化や反復回数の変更といった改造にも挑戦してみていただきたい。

 サンプル3はサンプル2に若干の修正を加えたものだ。ページの都合上詳しい解説は行わないが、負荷の軽減や255回以上のループ計算への対応などを行っている。詳しくはソース・コードを参照していただきたい。

サンプル3の実行結果
これは、サンプル3のループ回数を1024回に増やしたときの実行結果である。テクスチャを使用して色分けを行っている。黒い部分がマンデルブロ集合と呼ばれる部分に当たる。
 

【コラム】シェーダ・プログラミングと計算精度

 マンデルブロ集合の特徴は、どこまで拡大しても新しいフラクタル図形が見えてくることだ。しかし、今回のプログラムで拡大していくと、大体107倍ぐらいのところで画面がモザイク状になってしまい、それ以上拡大しても意味のある図形が見えなくなってしまう。

 この原因としては計算精度が考えられる。次の図は実際にdouble精度とfloat精度で計算したマンデルブロ集合を描画した例である。float精度の方はモザイク状になってしまっている。

座標(-0.3115348, 0.63287215)近辺の拡大図(上:double精度、下:float精度)
いずれも、GPUではなくC#による等価コードで作成した。上がdouble精度、下がfloat精度である。float精度ではモザイク状にイメージがつぶれてしまっているが、同じ倍率でもdouble精度であれば細部の構造がはっきりと見えている。

 ここで定数cの計算部分を見てみよう。

float2 c = (input.UV - 0.5) * float2(1, Aspect) * Zoom + Offset;

 IEEE 754(2進化浮動小数点数演算の標準規格)では、float型は少なくとも23bitの仮数部と、8bitの指数部を持つと定められているが、この仮数部は10進数で大体6けたから7けたに相当する。従って、cを求める過程で、6けたから7けた近くの異なる2つの数の足し算が発生すると、小さな数の方の情報はほとんど失われてしまうわけだ。この結果、input.UVの小さな違いがcに現れなくなり、結果的にcの分布がモザイク状になってしまう。

 この問題は、ピクセル・シェーダの計算をdouble精度で行うことですぐに解決できそうに思われるかもしれない。しかしdouble精度が正式にサポートされるのはDirect3D 10のShader Model 4.0以降である。さらに、Direct3D 10に対応したハードウェアだからといってdouble精度の計算に対応しているとは限らない。GPUによるdouble精度の大規模計算が一般的になるには、もうしばらく時間がかかると考えられる。

 一方、Direct3D 9のようにfloat精度までしか使用できない場合でも、複数のfloat値を束ねて高精度演算を行う方法が知られている。興味がある方は、GPGPU.orgのフォーラムの「Unevaluated sums in Cg」というスレッドでいくつか論文が紹介されているので、参照してみていただきたい。

まとめ

 今回は、レンダリング・パイプラインの大まかな解説と、純粋なGPUプログラミングの例としてマンデルブロ集合の計算を取り上げてみたが、いかがだったであろうか? 世間では、Direct3DやGPUはまだまだ3Dゲーム向けの技術と思われているようだが、このように描画の流れを一通り見てみると、いろいろな応用が可能であることがお分かりいただけたかと思う。

 さて、7回にわたってDirectXについて取り上げてきた本連載だが、DirectXの記事としてはずいぶんと異色なトピックを並べることとなった。内容が概論寄りになってしまったことで、実用的なテクニックの紹介が少なくなってしまったのは残念ではあるが、新しいOSとの関係、ライブラリのバージョニング問題、ビルド時処理の拡張という開発手法、ガベージ・コレクションへの対応、マルチコア時代の並列プログラミング、描画処理を記述するドメイン特化言語(DSL)など、DirectXの世界がいまもさまざまな方向へと広がっていることを、少しでも多くの方に知っていただければ幸いである。End of Article

 

 INDEX
  .NET&Windows Vistaへ広がるDirectXの世界
  第7回 プログラマブル・シェーダによる積極的なGPUの活用
    1.トピックの由来とサンプル・コード
    2.描画の流れ(1)
    3.描画の流れ(2)
    4.描画の流れ(3)
  5.描画の実行
 
インデックス・ページヘ  「.NET&Windows Vistaへ広がるDirectXの世界」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間