ポイントスプライト
ポイントスプライトを使用すると非常に綺麗なエフェクトを表現することが出来ます。今回は上空から雪が舞い落ちてくるような神秘的なエフェクトを実現してみました。

下のリンクから今回のプロジェクトをダウンロードできます。
今回のメインコードファイルを載せます。重要なコードを赤色で表示させています。
MainSample.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
/// <summary>
///
/// </summary>
public partial class MainSample : IDisposable
{
/// <summary>
/// 頂点の数
/// </summary>
private static readonly int VertexCount = 1000;
/// <summary>
/// ポイントスプライトの描画範囲(最小値)
/// </summary>
private static readonly Vector3 MinRange = new Vector3(-30.0f, -30.0f, -30.0f);
/// <summary>
/// ポイントスプライトの描画範囲(最大値)
/// </summary>
private static readonly Vector3 MaxRange = new Vector3(30.0f, 30.0f, 30.0f);
/// <summary>
/// 各頂点の位置
/// </summary>
private CustomVertex.PositionOnly[] _vertices = new CustomVertex.PositionOnly[VertexCount];
/// <summary>
/// ポイントスプライト用のテクスチャー
/// </summary>
private Texture _texture = null;
/// <summary>
///
/// </summary>
/// <param name="topLevelForm"></param>
/// <returns></returns>
/// <remarks>
///
/// </remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
this._form = topLevelForm;
this.CreateInputEvent(topLevelForm);
try
{
this.CreateDevice(topLevelForm);
this.CreateFont();
this.CreateXYZLine();
}
catch (DirectXException ex)
{
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
this.SetCameraPosition(5.0f, 270.0f, 0.0f);
// 頂点の位置をランダムに設定
Random rand = new Random();
Vector3 range = MaxRange - MinRange;
for (int i = 0; i < VertexCount; i++)
{
this._vertices[i].X = (float)(rand.NextDouble() * range.X) + MinRange.X;
this._vertices[i].Y = (float)(rand.NextDouble() * range.Y) + MinRange.Y;
this._vertices[i].Z = (float)(rand.NextDouble() * range.Z) + MinRange.Z;
}
// ポイントスプライト用のテクスチャー作成
this._texture = TextureLoader.FromFile(this._device, "Texture.bmp");
// Zバッファ無効
this._device.RenderState.ZBufferEnable = false;
// アルファブレンディングを有効にする
this._device.RenderState.AlphaBlendEnable = true;
// アルファブレンディング方法を設定
this._device.RenderState.SourceBlend = Blend.One;
this._device.RenderState.DestinationBlend = Blend.One;
// ポイントスプライト有効
this._device.RenderState.PointSpriteEnable = true;
// 点をカメラ空間でスケール有効化
this._device.RenderState.PointScaleEnable = true;
// ポイントサイズの設定
this._device.RenderState.PointSize = 0.25f;
// ポイントのスケール計算����
this._device.RenderState.PointScaleA = 0.0f;
this._device.RenderState.PointScaleB = 0.0f;
this._device.RenderState.PointScaleC = 1.0f;
// ライトを無効
this._device.RenderState.Lighting = false;
return true;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceLost(object sender, EventArgs e)
{
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceReset(object sender, EventArgs e)
{
}
/// <summary>
///
/// </summary>
public void Update()
{
if (this._keys[(int)Keys.Escape])
{
this._form.Close();
return;
}
this.SettingCamera();
// 各頂点を下に移動
for (int i = 0; i < VertexCount; i++)
{
this._vertices[i].Y -= 0.02f;
// 一番下まで行ったら一番上に戻す
if (this._vertices[i].Y < MinRange.Y)
{
this._vertices[i].Y = MaxRange.Y;
}
}
}
/// <summary>
///
/// </summary>
public void Draw()
{
if (!this.EnsureDevice())
{
return;
}
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.Black, 1.0f, 0);
this._device.BeginScene();
this.RenderXYZLine();
// テクスチャーセット
this._device.SetTexture(0, this._texture);
// 頂点フォーマットセット
this._device.VertexFormat = CustomVertex.PositionOnly.Format;
// ポイントスプライト描画
this._device.DrawUserPrimitives(PrimitiveType.PointList, VertexCount, this._vertices);
this.RenderText();
this._device.EndScene();
this._device.Present();
}
/// <summary>
///
/// </summary>
private void RenderText()
{
this._font.DrawText(null, "[Escape]終了", 0, 0, Color.White);
this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 12, Color.White);
this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 24, Color.White);
}
/// <summary>
///
/// </summary>
public void Dispose()
{
// テクスチャー破棄
this.SafeDispose(this._texture);
this.DisposeResource();
}
}
}
|
では、赤文字の部分を説明していきます。そのほかのファイルのコードはこちらです。
ポイントスプライトとはその名のとおり「点のスプライト」です。以前の Tips で普通のスプライトをやりましたが、あれは四角形ポリゴンにテクスチャーを貼ったものです。今回は「点にテクスチャーを貼る」という処理を行います。
しかし、実際「点」には大きさというものが存在しないし、頂点データは1つなのでUV座標も複数登録することが出来ません。
そこでポイントスプライト独自の設定が用意されており、座標系や大きさ、計算方法などを設定することにより点の位置に画像を貼り付けることができるようになります。
また、基本的に向きという概念がないのでどの方向から見ても画像が正面を向く、「ビルボード」と呼ばれる表現方法も同時に実現することが可能です。
/// <summary>
///
/// </summary>
private static readonly int VertexCount = 1000;
/// <summary>
///
/// </summary>
private static readonly Vector3 MinRange = new Vector3(-30.0f, -30.0f, -30.0f);
/// <summary>
///
/// </summary>
private static readonly Vector3 MaxRange = new Vector3(30.0f, 30.0f, 30.0f);
|
今回はポイントスプライトを1000個同時に描画することにします。静的フィールドとしてこの値を保持しておき、このフィールドを他のコード部分に使用すれば、描画個数を変更するときに楽になります。
次の描画範囲ですが、雪が降るようなエフェクトを使用する際、無限遠まで表示させるのは不可能なので下の図のようにボックス型の描画範囲を設定しておきます。

/// <summary>
///
/// </summary>
private CustomVertex.PositionOnly[] _vertices = new CustomVertex.PositionOnly[VertexCount];
/// <summary>
///
/// </summary>
private Texture _texture = null;
|
今回は頂点の位置が常に変化するので頂点バッファは使用せず、直接描画時に頂点データを転送する形を取ります。
今回使用するデータは「位置」だけなので「CustomVertex.PositionOnly」構造体の配列を作成します。色は全てテクスチャーに任せるのでいりません。点なので法線は必要ありませんし、ポイントスプライトという特殊な表示方法をとるのでUV座標もいりません。
で、あらかじめ配列を作成していますが、今回頂点は 1000 個使うのでそのまま 1000 をいれてもいいのですが、先ほど宣言した「VertexCount」を指定すれば、数を変更したい場合に「VertexCount」の値だけを変えればいいことになります。
後はポイントスプライト用のテクスチャーを宣言しておきます。
Random rand = new Random();
Vector3 range = MaxRange - MinRange;
for (int i = 0; i < VertexCount; i++)
{
this._vertices[i].X = (float)(rand.NextDouble() * range.X) + MinRange.X;
this._vertices[i].Y = (float)(rand.NextDouble() * range.Y) + MinRange.Y;
this._vertices[i].Z = (float)(rand.NextDouble() * range.Z) + MinRange.Z;
}
|
各頂点の位置を描画範囲内でランダムに配置するようにしています。全部原点にあったらエフェクトも何も無いです。
ランダムな値を取得するには「Random」クラスを作成します。コンストラクタでは「シード」と呼ばれる値を指定できますが、このシードによってどの値から計算を始めるかを指定することが出来ます。何も指定しない場合はPC起動経過時間が使われ。アプリケーション起動ごとに毎回違う位置に配置を行うことが出来ます。
ためしに 0 を入れると、見た目はランダムに配置されているように見えますが、実は毎回同じ位置に配置されていることが分かるかと思います。
後は描画範囲内に配置するように計算していますが、次のランダム値を取得するには「Random.NextDouble」メソッドを使用します。これはランダムで 0.0~1.0 の値を返してくれます。他には「Random.Next」「Random.NextBytes」メソッドがあります。
this._texture = TextureLoader.FromFile(this._device, "Texture.bmp");
|
ポイントスプライト用の画像を読み込み、テクスチャーを作成します。
this._device.RenderState.ZBufferEnable = false;
|
Zバッファは、本来は有効でいいのですが、たまに前後関係の問題でアルファブレンディングがかからないことがあるので無効にしています。
this._device.RenderState.AlphaBlendEnable = true;
this._device.RenderState.SourceBlend = Blend.One;
this._device.RenderState.DestinationBlend = Blend.One;
|
綺麗なエフェクトを表現する場合は大抵アルファブレンディングを使用します。さらに重なったときにより光って見えるように「Blend.One」を指定して色を加算合成するようにします。
this._device.RenderState.PointSpriteEnable = true;
this._device.RenderState.PointScaleEnable = true;
this._device.RenderState.PointSize = 0.25f;
|
さて、ここからがポイントスプライト独自の設定になります。
まず「PointSpriteEnable」を true に設定することにより、各頂点に画像を表示させることが出来ます。
次の「PointScaleEnable」を true にするとスプライトのサイズがカメラ空間で計算されるので、近くだと大きく、遠くだと小さく見えるようになります。 false を指定するとスクリーン座標での計算になるので距離に関係なく大きさが一定になります。
「PointSize」はそのままポイントのサイズになります。ただこれは上の「PointScaleEnable」の設定によって基準が異なり、true の場合はカメラ空間での大きさになり、false だとスクリーン座標での大きさになります。
this._device.RenderState.PointScaleA = 0.0f;
this._device.RenderState.PointScaleB = 0.0f;
this._device.RenderState.PointScaleC = 1.0f;
|
ここで設定する A,B,C の値はポイントスプライトの最終的な大きさを導き出すための計算式の値になります。その計算式は下のようになります。
Sz = Vh * Si * sqrt(1 / (A + B * Di + C * Di * Di))
| Sz |
最終的な表示サイズ |
| Vh |
ビューポートの高さ |
| Si |
PointSize で設定した値 |
| A |
PointScaleA で設定した値 |
| B |
PointScaleB で設定した値 |
| C |
PointScaleC で設定した値 |
| Di |
視点から頂点の位置までの距離 |
詳しくはヘルプなどを参照してください。
this._device.RenderState.Lighting = false;
|
ポイントスプライトでは通常ライトは使用しないので無効にしておきます。
for (int i = 0; i < VertexCount; i++)
{
this._vertices[i].Y -= 0.02f;
if (this._vertices[i].Y < MinRange.Y)
{
this._vertices[i].Y = MaxRange.Y;
}
}
|
毎フレームごとに各頂点を -Y 方向に移動させています。
一番下まで行ったら、また一番上に戻すようにしてループさせています。
this._device.SetTexture(0, this._texture);
this._device.VertexFormat = CustomVertex.PositionOnly.Format;
this._device.DrawUserPrimitives(PrimitiveType.PointList, VertexCount, this._vertices);
|
ポイントスプライトの描画コードです。ポイントスプライトのテクスチャーをセットし、頂点のフォーマットを設定します。
描画は直接頂点データを送る「Device.DrawUserPrimitives」メソッドを使用します。
第1引数には点を描画するので「PrimitiveType.PointList」を渡します。
第2引数は「点の数=プリミティブの数」なので頂点数をそのまま渡します。
第3引数に頂点データを渡します。
this.SafeDispose(this._texture);
|
最後に使用したテクスチャーを破棄します。
|