頂点シェーダー
前々回は白い三角形、前回は3色三角形を描きました。
両方共三角形は全く動きません。
静止画でした。
今回はこの三角形を動かしてみたいと思います。
アニメーションです。
上の静止画2つだけでは動いているように見えませんが、ともかくアニメーションするのです。
横にゆらゆら揺れます。
かんが妙に鋭い人は
「フームなるほど。ビューポートを動かして描くんだな。そうすれば三角形が描かれる場所をコントロールできる」
と思われるでしょうが、違います。
あるいは
「いやこういうことだな。頂点バッファの中に入っている頂点データを書き換えていくんだ。そうすれば三角形が動いたように見えるはず」
こちらはある意味正しくいい線行っています。
もちろんビューポートを動かしても全く同じ結果が得られるでしょうが、
ここでやる方法はそれよりはるかに柔軟性のある方法です。
頂点シェーダーを使って頂点の情報をいじくり回すのです。
この方法を応用すれば三角形を回転させたり、遠近法の効果を付け加えることも可能です。
ここでは話を簡単にするために、平行移動みたいな簡単なことをするのです。
頂点バッファの書き換えがある意味正しいといったのは、頂点シェーダーがまさにバッファのデータをいじくりまわしているからです。
「ある意味」という言い方をしたのは、実は頂点シェーダーの中に入っているデータそのものは変わらないということです。
そのものではなく、データのコピーが変えられるのです。
頂点バッファはあまり頻繁にデータを書き換えするのには向いていません。
CPU側からデバイスにあるデータ(バッファ)を書き換えるのには時間がかかるのです。
今回するのは、頂点バッファのデータを元にちょっと変更を加えて描画にのみ利用する方法です。
頂点バッファそのもののデータは変わりません。
コード
Program.cs
using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.DXGI;
using SlimDX.D3DCompiler;
class Program
{
static void Main()
{
using (Game game = new MyGame())
{
game.Run();
}
}
}
class MyGame : Game
{
Effect effect;
InputLayout vertexLayout;
Buffer vertexBuffer;
protected override void Draw()
{
GraphicsDevice.ImmediateContext.ClearRenderTargetView(
RenderTarget,
new SlimDX.Color4(1, 0, 0, 1)
);
initTriangleInputAssembler();
setTriangleTranslation();
drawTriangle();
SwapChain.Present(0, PresentFlags.None);
}
private void initTriangleInputAssembler()
{
GraphicsDevice.ImmediateContext.InputAssembler.InputLayout = vertexLayout;
GraphicsDevice.ImmediateContext.InputAssembler.SetVertexBuffers(
0,
new VertexBufferBinding(vertexBuffer, sizeof(float) * 3, 0)
);
GraphicsDevice.ImmediateContext.InputAssembler.PrimitiveTopology
= PrimitiveTopology.TriangleList;
}
private void setTriangleTranslation()
{
double time = System.Environment.TickCount / 500d;
effect.GetVariableByName("TranslationX")
.AsScalar().Set((float)System.Math.Cos(time));
}
private void drawTriangle()
{
effect.GetTechniqueByIndex(0).GetPassByIndex(0).Apply(GraphicsDevice.ImmediateContext);
GraphicsDevice.ImmediateContext.Draw(3, 0);
}
protected override void LoadContent()
{
initEffect();
initVertexLayout();
initVertexBuffer();
}
private void initEffect()
{
using (ShaderBytecode shaderBytecode = ShaderBytecode.CompileFromFile(
"myEffect.fx", "fx_5_0",
ShaderFlags.None,
EffectFlags.None
))
{
effect = new Effect(GraphicsDevice, shaderBytecode);
}
}
private void initVertexLayout()
{
vertexLayout = new InputLayout(
GraphicsDevice,
effect.GetTechniqueByIndex(0).GetPassByIndex(0).Description.Signature,
new[] {
new InputElement
{
SemanticName = "SV_Position",
Format = Format.R32G32B32_Float
}
}
);
}
private void initVertexBuffer()
{
vertexBuffer = MyDirectXHelper.CreateVertexBuffer(
GraphicsDevice,
new[] {
new SlimDX.Vector3(0, 0.5f, 0),
new SlimDX.Vector3(0.5f, 0, 0),
new SlimDX.Vector3(-0.5f, 0, 0),
});
}
protected override void UnloadContent()
{
effect.Dispose();
vertexLayout.Dispose();
vertexBuffer.Dispose();
}
}
class Game : System.Windows.Forms.Form
{
public SlimDX.Direct3D11.Device GraphicsDevice;
public SwapChain SwapChain;
public RenderTargetView RenderTarget;
public void Run()
{
initDevice();
SlimDX.Windows.MessagePump.Run(this, Draw);
disposeDevice();
}
private void initDevice()
{
MyDirectXHelper.CreateDeviceAndSwapChain(
this, out GraphicsDevice, out SwapChain
);
initRenderTarget();
initViewport();
LoadContent();
}
private void initRenderTarget()
{
using (Texture2D backBuffer
= SlimDX.Direct3D11.Resource.FromSwapChain<Texture2D>(SwapChain, 0)
)
{
RenderTarget = new RenderTargetView(GraphicsDevice, backBuffer);
GraphicsDevice.ImmediateContext.OutputMerger.SetTargets(RenderTarget);
}
}
private void initViewport()
{
GraphicsDevice.ImmediateContext.Rasterizer.SetViewports(
new Viewport
{
Width = ClientSize.Width,
Height = ClientSize.Height,
}
);
}
private void disposeDevice()
{
UnloadContent();
RenderTarget.Dispose();
GraphicsDevice.Dispose();
SwapChain.Dispose();
}
protected virtual void Draw() { }
protected virtual void LoadContent() { }
protected virtual void UnloadContent() { }
}
class MyDirectXHelper
{
public static void CreateDeviceAndSwapChain(
System.Windows.Forms.Form form,
out SlimDX.Direct3D11.Device device,
out SlimDX.DXGI.SwapChain swapChain
)
{
SlimDX.Direct3D11.Device.CreateWithSwapChain(
DriverType.Hardware,
DeviceCreationFlags.None,
new SwapChainDescription
{
BufferCount = 1,
OutputHandle = form.Handle,
IsWindowed = true,
SampleDescription = new SampleDescription
{
Count = 1,
Quality = 0
},
ModeDescription = new ModeDescription
{
Width = form.ClientSize.Width,
Height = form.ClientSize.Height,
RefreshRate = new SlimDX.Rational(60, 1),
Format = Format.R8G8B8A8_UNorm
},
Usage = Usage.RenderTargetOutput
},
out device,
out swapChain
);
}
public static Buffer CreateVertexBuffer(
SlimDX.Direct3D11.Device graphicsDevice,
System.Array vertices
)
{
using (SlimDX.DataStream vertexStream
= new SlimDX.DataStream(vertices, true, true))
{
return new Buffer(
graphicsDevice,
vertexStream,
new BufferDescription
{
SizeInBytes= (int)vertexStream.Length,
BindFlags = BindFlags.VertexBuffer,
}
);
}
}
}
myEffect.fx
float TranslationX;
float4 MyVertexShader(float4 position : SV_Position) : SV_Position
{
return position + float4(TranslationX, 0, 0, 0);
}
float4 MyPixelShader() : SV_Target
{
return float4(1, 1, 1, 1);
}
technique10 MyTechnique
{
pass MyPass
{
SetVertexShader( CompileShader( vs_5_0, MyVertexShader() ) );
SetPixelShader( CompileShader( ps_5_0, MyPixelShader() ) );
}
}
このプログラムで一番重要なのはHLSLの変数TranslationXです。 この値が三角形を横に動かします。 TranslationXはC#側のコードによって値がセットされます。 値はMath.Sinに時間が引数になったもの。 つまりTranslationXは時とともに増えたり減ったりします。 それがHLSL側で頂点の座標に足されます。 つまりゆらゆらと横に揺れるようになるのです。 頂点バッファのデータそのものを変えなくても、違った描き方をすることができる。 それが頂点シェーダーです。
HLSLの変数を書き換える 今回のサンプルでやったことは、C#側からHLSL側の変数の書き換えを行うというものでした。 書き換えの結果三角形が動くのです。 それにはHLSL側の変数を操作するためのクラスが必要です。 それがSlimDX.Direct3D11.EffectVariableクラスです。
public class EffectVariable
このオブジェクトはEffect.GetVariableByName()メソッドにより取得します。
public EffectVariable GetVariableByName(string name);
nameはHLSLファイルに書いた変数の名前です。
戻り値はEffectVariableのインスタンスです。
EffectVariableは変数を司るものです。
HLSLの変数を読み書きします。
ただ、どんな変数も読み書きしなくてはならないので、型情報がありません。
この変数がfloatなのか、Vector3なのか、テクスチャなのかマトリクスなのか…
使う前に、まずそれをはっきりさせる必要があります。
それにはEffectVariaible.AsXXX()という形式のメソッドを使います。
このメソッドに引数はありません。
戻り値は型と関連付けられたEffectVariableの派生クラスです。
メソッド名 |
戻り値 |
AsBlend |
EffectBlendVariable |
AsClassInstance |
EffectClassInstanceVariable |
AsConstantBuffer |
EffectConstantBuffer |
AsDepthStencil |
EffectDepthStencilVariable |
AsDepthStencilView |
EffectDepthStencilViewVariable |
AsInterface |
EffectInterfaceVariable |
AsMatrix |
EffectMatrixVariable |
AsRasterizer |
EffectRasterizerVariable |
AsRenderTargetView |
EffectRenderTargetViewVariable |
AsResource |
EffectResourceVariable |
AsSampler |
EffectSamplerVariable |
AsScalar |
EffectScalarVariable |
AsShader |
EffectShaderVariable |
AsString |
EffectStringVariable |
AsUnorderedAccessView |
EffectUnorderedAccessViewVariable |
AsVector |
EffectVectorVariable |
今回はfloatの変数を書き換えるので、AsScalar()メソッドを使います。
戻り値はSlimDX.Direct3D11.EffectScalarVariableです。
このクラスを使うとHLSL側のfloat変数に書き込みができます。
書き込みにはEffectScalarVariable.Set()メソッドを使います。
public Result Set(float value);
valueは変数にセットする値です。
こうしてC#側で用意した値をHLSL側に書き込めます。
HLSLでは描画に使われる頂点データにアクセスできるので、描くものをアニメーションすることができるのです。