Xna Creators Clubのサンプル、Particle 3D Sampleをいじっていたら
障害(?)っぽいものにぶち当たったので問題とその解決、
事の顛末を書いておきます。
Particle 3D Sampleというのは爆発や煙、炎などを
ポイントスプライトで描画するサンプルなのですが、
どうもカメラの距離によっておかしくなっているように感じられたのです。
Particle 3D Sampleのクラスを取り出して
別のプロジェクトで遊んでいたのですが、
カメラを爆発に近づけすぎるとどうやら変になるようなのです。
まず、カメラを爆発から150離れた位置におくと
次のようにきちんと爆発が起こっているように見えます:
カメラからの距離:150
これは爆発です。
これは静止画なのでよくわかりませんが、実際にサンプルを実行すると
爆炎がまわりにゆっくりと広がっていくのがわかります。
ここまでは問題ありません。
しかし、カメラをこの爆発に近づけるとどうもおかしなことになるようです。
次の画像はカメラを先ほどの1/10の距離、つまり15離れた位置に
動かしたときの画像です:
カメラからの距離:15
ウィンドウにはいくつもの炎のようなものが映っています。
これは別に複数の爆発を起こしたと言うわけではありません。
ここで映っている爆発は1つだけです。
どうやら爆炎がバラバラになって表示されているようです。
爆発を構成している一つ一つの爆炎が本来よりも小さく表示されているのでしょう。
実際、この一つ一つの爆炎はサンプルで使われている
"explosion.png"と同じものです。

(explosion.png : Particle 3DのContentフォルダ内より)
つまりここでの問題は
「爆発に近づいたときに一つ一つの爆炎のポイントスプライトが拡大されない」
ことにあるようです。
そのため、遠くから表示するとうまくいっているように見える一方で、
近づいたら、各々の炎が拡大されないために、本来そうであるべきよりも炎が小さく見え、
バラバラになっているように見えるのでしょう。
ここで考えられるのは
「もとのサンプルからクラスを取り出すときに何か間違ったことをしてしまったのではないか」
ということです。
しかしどうやら元のサンプルでも同じような現象が起きているようなのです。

Particle 3Dで爆発に近づいた場合。やはり爆発がバラバラになっています。
(※見やすくするために爆発の周りの煙は表示していません。)
ちなみに、サンプルでも爆発を遠くから見ると問題なく表示されます。

ということはこの問題はサンプルの問題だったのでしょうか?
サンプルがポイントのサイズを固定していたのでしょうか?
HLSLのソースコードを読む限りそうではありませんでした。
きちんと、カメラから離れればサイズが小さく、近づけばサイズが大きくなるように
書かれていました。
これはプログラムの実行によっても検証できます。
カメラを爆発からうんと離してやるのです。
もし各ポイントのサイズが固定されているのなら
どんなに離してもそれ以上爆発が小さくならないような
カメラの距離が存在するはずです。
爆発がexplosion.pngの爆炎より小さくなることはありえないはずです。

小さくなっていますね・・・。
ということはやはり、各ポイントのサイズはカメラの距離によって変わっているようです。
それではどうして近づきすぎるとおかしくなるのでしょう?
これではまるでポイントのサイズの上限が存在しているようではありませんか!
・・・・・・ポイントのサイズの上限?
そういえばPointSizeMaxというプロパティがあったような気がします。
もしかしたらそれが関係しているのかも!
調べてみるとParticleSystemクラスに次のような行が見つかりました:
renderState.PointSizeMax = 256;
とりあえずこれをコメントアウトして実行してみました。
すると、なんとうまく描画されたのです。
カメラからの距離:15
よし!
問題は解決です。
解決法は
「"renderState.PointSizeMax = 256";をコメントアウトする」
です。
・・・しかしちょっと待ってください。
このステートメントはいったい何をしていたのでしょうか?
まさかサンプルを実行する人を困らせるためにこんなことをしたわけではないでしょう。
なにか理由があるはずです。
とりあえずこのプロパティを調べてみることにしました。
msdnによると、これはポイントのサイズの上限を表しているのだそうです。
(「そんなものは最初からわかってる!」という声が聞こえてきそうです。)
さらに、デフォルトの値は64.0fなのだそうです。
なるほど、ポイントのサイズはデフォルトでは64が上限と言うことですね。
読めてきました。
ということは、"renderState.PointSizeMax = 256;"というのは
「ポイントのサイズの上限を大きくして、より大きなポイントも表示できるようにした」
ということなのでしょう。
しかし、そうだとすると新たな疑問が浮かびます。
なぜ"renderState.PointSizeMax = 256;" をコメントアウトして上手くいったのでしょうか?
ポイントサイズの上限を大きくするステートメントをコメントアウトしたのですから、症状は悪化するはずです。
これではアベコベではありませんか!
もともと、ここでの問題は「ポイントのサイズがもっと大きくならない」
というものでした。
そこで「より大きなポイントも表示できるようにする」ステートメントをコメントアウトしたのなら
問題はますますひどくなるはずです。
しかし実際にはこれで問題が解決したのです。
ひとつの可能性として、「実はデフォルトの値は64.0fではなくもっと大きい値だった」
ということが考えられます。
たとえばデフォルトの値が実は512.0fくらいで、"renderState.PointSizeMax = 256;"が
ポイントサイズの上限を引き下げていたのかもしれません。
さっそくかんたんなステートメントを追加して調べてみました。
LoadGraphicsContentメソッド内に次の行を追加したのです。
Window.Title += "maxdefault:" + graphics.GraphicsDevice.RenderState.PointSize;
これでウィンドウのタイトルにPointSizeプロパティのデフォルトの値が表示されるはずです。
結果は"maxdefault:8192"でした。
デフォルトの値は8192です。
・・・あれ、何でこんなに大きいんだろ・・・・・・?
びっくりです。
予想では512、大きくてもせいぜい1024くらいだと思っていたのですがそれを大きく上回りました。
ぼくがこのとき使ったディスプレイの解像度は1920 x 1200ですから上限など無いも同然です。
ディスプレイの4倍以上の大きさのポイントを描画できるわけですからね。
これでは "renderState.PointSizeMax = 256;"なんてしたらおかしくなるのもうなずけます。
気にかかるのはこの値がmsdnの記述と大きくかけ離れている事実です。
8192と64では128倍の差があるではありませんか!
どう考えてもおかしいです。
この理由はわかりませんが、ぼくは次のように考えました:
msdnのPointSizeMaxプロパティのページをさらに見ると、
このプロパティはGraphicsDeviceCapabilities.PointSizeMaxの値を
超えられないとも書いてあります。
グラフィックスデバイスには表示できるポイントの大きさに限界があって、
それをこのプロパティが表しているのでしょう。
ということは、もしかするとRenderState.PointSizeMaxは
グラフィックスデバイスの能力を調べて、自動的に上限を
最大の値にまで引き上げてくれるのかもしれません。
おそらくこのパソコンのグラフィックスデバイスのポイントサイズの上限が8192だったのでしょう。(後で調べてみると、実際そうでした)
それをRenderState.PointSizeMaxは読み取って、
自動的に最大限の機能を発揮できるようにしてくれたのかもしれないというわけです。
別のパソコンならRenderState.PointSizeMaxからはまた違った値が返されるでしょう。
蛇足
msdnのサンプルを参考にして、グラフィックスデバイスのポイントサイズの上限を調べるのに使ったソースコードを書いておきます。
foreach (GraphicsAdapter adapter in GraphicsAdapter.Adapters)
{
GraphicsDeviceCapabilities caps = adapter.GetCapabilities(DeviceType.Hardware);
Window.Title += "max:" + caps.MaxPointSize;
}
グラフィックスデバイスの能力を調べるには
Microsoft.Xna.Framework.Graphics.GraphicsDeviceCapabilitiesクラスを使います。
[Serializable]
public sealed class GraphicsDeviceCapabilities : IDisposable
このクラスはポイントサイズの上限のほかにも、使用できるテクスチャの大きさの上限や、
頂点のインデックスの上限も調べることが出来ます。
ポイントサイズの上限を表すプロパティは、MaxPointSizeです。
public float MaxPointSize { get; }
このクラスのインスタンスを得るには、GraphicsAdapter.GetCapabilitiesメソッドを使います。
public GraphicsDeviceCapabilities GetCapabilities (
DeviceType deviceType
)
このメソッドは、引数のdeviceTypeに指定された種類の
デバイスの能力を表すGraphicsDeviceCapabilitiesのインスタンスを返します。
DeviceTypeは列挙型で、3つのメンバ、つまりHardware, NullReference, Referenceを持ちます。
ここではHardwareを使いました。
このメソッドを持っているのはグラフィックスアダプタを表す、
Microsoft.Xna.Framework.Graphics.GraphicsAdapterクラスです。(そのまんまですが)
このクラスのインスタンスを得るには、コンストラクタではなく
自分のクラスのAdapters静的プロパティを使います。
public static ReadOnlyCollection<GraphicsAdapter> Adapters { get; }
このプロパティの中にシステムで使うことの出来る全てのグラフィックス・アダプタが入っています。
foreachなんかでまわして使うと良いでしょう。
[0回]
PR