頂点データによる半透明処理
當前顯示的頁面不支援所選的顯示語言。
ポリゴンを頂点データの色指定によって半透明で表示されています。きちんと後が透けていることが分かると思います。
今回のメインコードファイルを載せます。
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>
頂点データ1
</summary>
private CustomVertex.PositionColored[] _vertices1 = new CustomVertex.PositionColored[3];
<summary>
頂点データ2
</summary>
private CustomVertex.PositionColored[] _vertices2 = new CustomVertex.PositionColored[3];
<summary>
アプリケーションの初期化
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<returns>全ての初期化がOKなら true, ひとつでも失敗したら false を返すようにする</returns>
<remarks>
false を返した場合は、自動的にアプリケーションが終了するようになっている
</remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
// フォームの参照を保持
this._form = topLevelForm;
// 入力イベント作成
this.CreateInputEvent(topLevelForm);
try
{
// Direct3D デバイス作成
this.CreateDevice(topLevelForm);
// フォントの作成
this.CreateFont();
}
catch (DirectXException ex)
{
// 例外発生
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// XYZライン作成
this.CreateXYZLine();
// 2つのポリゴン作成
// 1つ目
this._vertices1[0].Position = new Vector3(-1.0f, 5.0f, 1.0f);
this._vertices1[0].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
this._vertices1[1].Position = new Vector3(3.0f, -3.0f, 1.0f);
this._vertices1[1].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
this._vertices1[2].Position = new Vector3(-5.0f, -3.0f, 1.0f);
this._vertices1[2].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
// 2つ目
this._vertices2[0].Position = new Vector3(0.0f, 5.0f, -1.0f);
this._vertices2[0].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
this._vertices2[1].Position = new Vector3(4.0f, -3.0f, -1.0f);
this._vertices2[1].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
this._vertices2[2].Position = new Vector3(-4.0f, -3.0f, -1.0f);
this._vertices2[2].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
// ライト無効
this._device.RenderState.Lighting = false;
// ポリゴンの両面を描画するようにカリングを無効にする
this._device.RenderState.CullMode = Cull.None;
// アルファブレンディング方法を設定
this._device.RenderState.SourceBlend = Blend.SourceAlpha;
this._device.RenderState.DestinationBlend = Blend.InvSourceAlpha;
// アルファブレンディングを有効にする
this._device.RenderState.AlphaBlendEnable = true;
// Zバッファを無効
//this._device.RenderState.ZBufferEnable = false;
return true;
}
<summary>
メインループ処理
</summary>
public void MainLoop()
{
// カメラの設定
this.SettingCamera();
// 描画内容を単色でクリアし、Zバッファもクリア
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.Black, 1.0f, 0);
// 「BeginScene」と「EndScene」の間に描画内容を記述する
this._device.BeginScene();
// ライトを無効
this._device.RenderState.Lighting = false;
// 原点に配置
this._device.SetTransform(TransformType.World, Matrix.Identity);
// XYZラインを描画
this.RenderXYZLine();
// 三角形ポリゴン描画
// 描画する頂点のフォーマットをセット
this._device.VertexFormat = CustomVertex.PositionColored.Format;
// 1個目
this._device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, this._vertices1);
// 2個目
this._device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, this._vertices2);
// 文字列の描画
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);
// 描画はここまで
this._device.EndScene();
// 実際のディスプレイに描画
this._device.Present();
// アプリケーションの終了操作
if (this._keys[(int)Keys.Escape])
{
this._form.Close();
}
}
<summary>
リソースの破棄をするために呼ばれる
</summary>
public void Dispose()
{
// リソースの破棄
this.DisposeResource();
}
}
}
MainSamplePartial.cs ファイルのコードはこちらです。
MainSamplePartial.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
{
public partial class MainSample
{
<summary>
メインフォーム
</summary>
private MainForm _form = null;
<summary>
Direct3D デバイス
</summary>
private Device _device = null;
<summary>
Direct3D 用フォント
</summary>
private Microsoft.DirectX.Direct3D.Font _font = null;
<summary>
キーのプレス判定
</summary>
private bool[] _keys = new bool[256];
<summary>
1つ前のマウスの位置
</summary>
private Point _oldMousePoint = Point.Empty;
<summary>
カメラレンズの位置(R)
</summary>
private float _lensPosRadius = 12.0f;
<summary>
カメラレンズの位置(θ)
</summary>
private float _lensPosTheta = 255.0f;
<summary>
カメラレンズの位置(φ)
</summary>
private float _lensPosPhi = 10.0f;
<summary>
XYZライン用頂点バッファ
</summary>
private VertexBuffer _xyzLineVertexBuffer = null;
<summary>
入力イベント作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateInputEvent(MainForm topLevelForm)
{
// キーイベント作成
topLevelForm.KeyDown += new KeyEventHandler(this.form_KeyDown);
topLevelForm.KeyUp += new KeyEventHandler(this.form_KeyUp);
// マウス移動イベント
topLevelForm.MouseMove += new MouseEventHandler(this.form_MouseMove);
}
<summary>
キーボードのキーを押した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyDown(object sender, KeyEventArgs e)
{
// 押されたキーコードのフラグを立てる
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = true;
}
}
<summary>
キーボードのキーを放した瞬間
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_KeyUp(object sender, KeyEventArgs e)
{
// 放したキーコードのフラグを下ろす
if ((int)e.KeyCode < this._keys.Length)
{
this._keys[(int)e.KeyCode] = false;
}
}
<summary>
マウス移動イベント
</summary>
<param name="sender"></param>
<param name="e"></param>
private void form_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
// 回転
this._lensPosTheta -= e.Location.X - this._oldMousePoint.X;
this._lensPosPhi += e.Location.Y - this._oldMousePoint.Y;
// φに関しては制限をつける
if (this._lensPosPhi >= 90.0f)
{
this._lensPosPhi = 89.9999f;
}
else if (this._lensPosPhi <= -90.0f)
{
this._lensPosPhi = -89.9999f;
}
}
// ???ウスの位置を記憶
this._oldMousePoint = e.Location;
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
private void CreateDevice(MainForm topLevelForm)
{
// PresentParameters。デバイスを作成する際に必須
// どのような環境でデバイスを使用するかを設定する
PresentParameters pp = new PresentParameters();
// ウインドウモードなら true、フルスクリーンモードなら false を指定
pp.Windowed = true;
// スワップ効果。とりあえず「Discard」を指定。
pp.SwapEffect = SwapEffect.Discard;
// 深度ステンシルバッファ。3Dでは前後関係があるので通常 true
pp.EnableAutoDepthStencil = true;
// 自動深度ステンシル サーフェイスのフォーマット。
// 「D16」に対応しているビデオカードは多いが、前後関係の精度があまりよくない。
// できれば「D24S8」を指定したいところ。
pp.AutoDepthStencilFormat = DepthFormat.D16;
try
{
// デバイスの作成
this.CreateDevice(topLevelForm, pp);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
Direct3D デバイスの作成
</summary>
<param name="topLevelForm">トップレベルウインドウ</param>
<param name="presentationParameters">PresentParameters 構造体</param>
private void CreateDevice(MainForm topLevelForm, PresentParameters presentationParameters)
{
// 実際にデバイスを作成します。
// 常に最高のパフォーマンスで作成を試み、
// 失敗したら下位パフォーマンスで作成するようにしている。
try
{
// ハードウェアによる頂点処理、ラスタライズを行う
// 最高のパフォーマンスで処理を行えます。
// ビデオカードによっては実装できない処理が存在します。
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.HardwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex1)
{
// 作成に失敗
Debug.WriteLine(ex1.ToString());
try
{
// ソフトウェアによる頂点処理、ハードウェアによるラスタライズを行う
this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex2)
{
// 作成に失敗
Debug.WriteLine(ex2.ToString());
try
{
// ソフトウェアによる頂点処理、ラスタライズを行う
// パフォーマンスはとても低いです。
// その代わり、ほとんどの処理を制限なく行えます。
this._device = new Device(0, DeviceType.Reference, topLevelForm.Handle,
CreateFlags.SoftwareVertexProcessing, presentationParameters);
}
catch (DirectXException ex3)
{
// 作成に失敗
// 事実??デバイスは作成で?????ません。
throw ex3;
}
}
}
}
<summary>
フォントの作成
</summary>
private void CreateFont()
{
try
{
// フォントデータの構造体を作成
FontDescription fd = new FontDescription();
// 構造体に必要なデータをセット
fd.Height = 12;
fd.FaceName = "MS ゴシック";
// フォントを作成
this._font = new Microsoft.DirectX.Direct3D.Font(this._device, fd);
}
catch (DirectXException ex)
{
// 例外発生
throw ex;
}
}
<summary>
XYZライン作成
</summary>
private void CreateXYZLine()
{
// 6つ分の頂点を作成
this._xyzLineVertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
6, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);
// 頂点バッファをロックして、位置、色情報を書き込む
using (GraphicsStream data = this._xyzLineVertexBuffer.Lock(0, 0, LockFlags.None))
{
// 今回は各XYZのラインを原点(0.0f, 0.0f, 0.0f)からプラス方向に 10.0f 伸びた線を作成
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(10.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 10.0f, 0.0f, Color.Green.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Blue.ToArgb()));
data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 10.0f, Color.Blue.ToArgb()));
this._xyzLineVertexBuffer.Unlock();
}
}
<summary>
ライトの設定
</summary>
private void SettingLight()
{
// 平行光線を使用
this._device.Lights[0].Type = LightType.Directional;
// ライトの方向
this._device.Lights[0].Direction = new Vector3(1.0f, -1.5f, 2.0f);
// 光の色は白
this._device.Lights[0].Diffuse = Color.White;
// 環境光
this._device.Lights[0].Ambient = Color.FromArgb(255, 128, 128, 128);
// 0 番のライトを有効
this._device.Lights[0].Enabled = true;
// 0 番のライトを更新
this._device.Lights[0].Update();
}
<summary>
カメラの設定
</summary>
private void SettingCamera()
{
// レンズの位置を三次元極座標で変換
float radius = this._lensPosRadius;
float theta = Geometry.DegreeToRadian(this._lensPosTheta);
float phi = Geometry.DegreeToRadian(this._lensPosPhi);
Vector3 lensPosition = new Vector3(
(float)(radius * Math.Cos(theta) * Math.Cos(phi)),
(float)(radius * Math.Sin(phi)),
(float)(radius * Math.Sin(theta) * Math.Cos(phi)));
// ビュー変換行列を設定
this._device.Transform.View = Matrix.LookAtLH(
lensPosition, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f));
// 射影変換を設定
this._device.Transform.Projection = Matrix.PerspectiveFovLH(
Geometry.DegreeToRadian(60.0f),
(float)this._device.Viewport.Width / (float)this._device.Viewport.Height,
1.0f, 100.0f);
}
<summary>
XYZライン描画
</summary>
private void RenderXYZLine()
{
this._device.SetStreamSource(0, this._xyzLineVertexBuffer, 0);
this._device.VertexFormat = CustomVertex.PositionColored.Format;
this._device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
}
<summary>
リソースの破棄
</summary>
private void DisposeResource()
{
// XYZラインの破棄
if (this._xyzLineVertexBuffer != null)
{
this._xyzLineVertexBuffer.Dispose();
}
// フォントのリソース解放
if (this._font != null)
{
this._font.Dispose();
}
// Direct3D デバイスのリソース解放
if (this._device != null)
{
this._device.Dispose();
}
}
}
}
<summary>
頂点データ1
</summary>
private CustomVertex.PositionColored[] _vertices1 = new CustomVertex.PositionColored[3];
<summary>
頂点データ2
</summary>
private CustomVertex.PositionColored[] _vertices2 = new CustomVertex.PositionColored[3];
半透明がうまく出来ているか確認するために色違いの三角形ポリゴンを2つ描画します。今回はわざわざ頂点バッファを作る必要性もないので、頂点データを描画時に直接転送する形で描画します。頂点に必要なデータは「位置」と「色」です。半透明処理はこの「色」の部分を使用します。
// 2つのポリゴン作成
// 1つ目
this._vertices1[0].Position = new Vector3(-1.0f, 5.0f, 1.0f);
this._vertices1[0].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
this._vertices1[1].Position = new Vector3(3.0f, -3.0f, 1.0f);
this._vertices1[1].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
this._vertices1[2].Position = new Vector3(-5.0f, -3.0f, 1.0f);
this._vertices1[2].Color = Color.FromArgb(128, 255, 0, 0).ToArgb();
// 2つ目
this._vertices2[0].Position = new Vector3(0.0f, 5.0f, -1.0f);
this._vertices2[0].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
this._vertices2[1].Position = new Vector3(4.0f, -3.0f, -1.0f);
this._vertices2[1].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
this._vertices2[2].Position = new Vector3(-4.0f, -3.0f, -1.0f);
this._vertices2[2].Color = Color.FromArgb(128, 0, 0, 255).ToArgb();
2つのポリゴンの頂点データを設定しています。1枚目が赤のポリゴン、2枚目が青のポリゴンになります。位置と色を個別に設定していますが、特に深い意味はありません。このような設定方法もあると言うことです。
さて、色の設定についてですが、「Color」構造体ですでに定義されている色(Color.Red など)をそのまま使用せず、今回は直接数値を設定するようにしています。
本来は直接 int の数値で指定してもいいのですが、わかりやすくするために「Color.FromArgb」メソッドを使用しています。
このメソッドで渡している値は、メソッド名と同じ順番で左から「A(アルファ)」「R(レッド)」「G(グリーン)」「B(ブルー)」になります。
ここで重要なのは「A(アルファ)」です。値は0~255の範囲で、255が不透明、0が透明、その間が半透明になります。今回は「128」を両方のポリゴンに設定して半透明になるようにしています。
頂点データ上は半透明に設定しましたが、実際にどう表示するかはこれだけでは決まりません。後のセクションの説明を見てください。
// ライト無効
this._device.RenderState.Lighting = false;
// ポリゴンの両面を描画するようにカリングを無効にする
this._device.RenderState.CullMode = Cull.None;
今回はライトは使用しないので無効にしておきます。
ポリゴンを裏からも見えるようにしたいので、カリングを無効にしておきます。
// アルファブレンディング方法を設定
this._device.RenderState.SourceBlend = Blend.SourceAlpha;
this._device.RenderState.DestinationBlend = Blend.InvSourceAlpha;
// アルファブレンディングを有効にする
this._device.RenderState.AlphaBlendEnable = true;
半透明処理を行うには Direct3D デバイスに半透明処理を行うように指定する必要があります。ちなみに半透明処理のことを「アルファブレンディング」とも言います。
とりあえず上の5行目にある「Device.RenderState.AlphaBlendEnable」プロパティを「true」に設定すれば、半透明処理は行うことが出来ます。
ただ、一言にアルファブレンディングと言ってもその計算方法は何通りかあります。それを決めるのが2、3行目にある「Device.RenderState.SourceBlend」プロパティと「Device.RenderState.DestinationBlend」プロパティです。
アルファブレンディングを使用した場合、各ピクセルの描画色は下のような計算で求まります。
ピクセル色 = source × ブレンディング係数 + destination × ブレンディング係数
「source」はこれから描画しようとするピクセルの色で、頂点色やマテリアル、テクスチャーなどで計算されたポリゴンの色などのことを指します。
「destination」はすでに描画されている色で、通常はスクリーンに描画されている色になります。(正確にはレンダリングターゲット上の色)
そして「ブレンディング係数」は「SourceBlend」や「DestinationBlend」に設定したパラメータによって式が変わります。主に下のような種類が使われています。
種類 | 式 |
---|---|
One | 1.0 |
SourceAlpha | source のアルファ値 |
InvSourceAlpha | 1 - source のアルファ値 |
他にもたくさん種類か存在する 今回は「SourceBlend」に「SourceAlpha」、「DestinationBlen」に「InvSourceAlpha」を指定しているため、下のような計算になります。
- 黒の背景に赤ポリゴンの描画
- (255, 0, 0) * 0.5 + (0, 0, 0) * (1 - 0.5) = (128, 0, 0)
- さらにその上に青ポリゴンを乗せた場合
- (0, 0, 255) * 0.5 + (128, 0, 0) * (1- 0.5) = (64, 0, 128)
というような色になるはずです。
ちなみに両方に「One」を指定すると色が加算合成させるために下のように明るい色になります。これは爆発などのエフェクトによく使用されるパターンです。
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.Black, 1.0f, 0);
半透明がうまくいっているかわかりやすくするために背景を黒で塗りつぶします。
// 三角形ポリゴン描画
// 描画する頂点のフォーマットをセット
this._device.VertexFormat = CustomVertex.PositionColored.Format;
// 1個目
this._device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, this._vertices1);
// 2個目
this._device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, this._vertices2);
描画は通常通りに行えばOKです。
さて、これでアルファブレンディングに関しては終わりなのですが、実は1つ問題があります。初期位置からポリゴンを見ると普通に透けているのですが、カメラを反対に持っていくとなんと赤のポリゴンが青のポリゴンに対してだけ透けていない現象が下のように発生します。
実はこれは「Zバッファ」の作用のためにこうなってしまうのです。Zバッファを使用した場合、手前にすでに描画されたポリゴンなどがあった場合、描画処理を減らすために奥にあるポリゴンは描画しないという優れた機能を使用できます。(厳密にはポリゴンではなくピクセル)
今回、半透明なポリゴンは「赤」「青」の順番で描画しているため、初期位置から見た場合、奥から描画しているのできちんと見えるのですが、カメラを回して後ろから見ると赤が最初に描画されるので、奥にある青のポリゴンの一部がZ値の判定テストで落ちてしまいます。このため青のポリゴンが半透明処理に参加できないのです。
これを回避する方法のひとつとして「Zバッファの無効化」があります。初期化メソッドの一番最後に下のようなコメントにしたコードがあるのでこのコメントをはずすとZバッファを無効に出来ます。
// Zバッファを無効
this._device.RenderState.ZBufferEnable = false;
その結果見事に半透明になります。
しかし、この方法はあまりお勧めしません。今回のようにポリゴン2つだけならあまり問題は無いのですが、実際にソフトを作るときは、もっとたくさんのポリゴンが必要になったり、不透明なポリゴンとも混在する可能性があります。そうした場合「Zバッファ」を使用しないと前後関係の維持が非常に困難になってしまいます。
また、上の図を見てもらうとわかるとおり、青ポリゴンが手前にあるように見えてしまっています。これは最初に赤ポリゴンが背景と計算しているため、どうしても青が計算上強くなってしまうのです。
そこで今一番使われているのが、「半透明ポリゴンは奥から描画するようにソート」する方法です(Zソートと呼ばれている)。こうすればZバッファで落ちることもないですし、半透明の計算も自然に見えるようになります。
ちなみにソートですが、位置関係が常に変わる可能性がある場合は毎フレーム行う必要があります。
また、「不透明ポリゴン」が混在する場合は先に「不透明ポリゴン」をすべて描画してください。そうでないと先ほどのようにZバッファの所為で半透明ポリゴンの後に不透明ポリゴンが描画できなくなります。
ただZソートも完璧ではありません。ポリゴン同士が交差したりするとうまく判定できません。また、ポリゴン数が多くなるとソートに時間がかかってしまいます。ここら辺の判断は各プログラマが決めることになります。ポリゴンをより細かく分割してきちんと前後関係をはっきりさせたり、またはポリゴン単位ではなくオブジェクト単位でソートし処理を軽くしたりなど様々です。自分のソフトに合わせて仕様を決めてください。
理想な表示