「MonoGameでハードウェアインスタンシングしてみる」の版間の差分

提供: MonoBook
ナビゲーションに移動 検索に移動
imported>Administrator
 
(同じ利用者による、間の3版が非表示)
5行目: 5行目:
 
これがついにMonoGame 3.7で[[OpenGL]]環境でも[[ハードウェアインスタンシング]]が使えるようになったそうだ。さっそく[[MacOS]]上で試してみた。[[ハードウェアインスタンシング]]が使えるとなると[[ボリュームレンダリング]]の実装が捗る可能性がある。
 
これがついにMonoGame 3.7で[[OpenGL]]環境でも[[ハードウェアインスタンシング]]が使えるようになったそうだ。さっそく[[MacOS]]上で試してみた。[[ハードウェアインスタンシング]]が使えるとなると[[ボリュームレンダリング]]の実装が捗る可能性がある。
  
==HLSLを書く==
+
== Macでシェーダーをコンパイルできるようにする ==
[[HLSL]]はこんな感じ。今回は移動だけで回転はしていない。 [[Mac]]上で[[HLSL]]を[[コンパイル]]する方法は「 [[MonoGameでプログラマブルシェーダーを使う]]」および「[[InfinitespaceStudios.Pipeline]]」の項目を参照。
+
Mac上でHLSLをコンパイルする方法は以下を参照。
 +
* [[MonoGameでプログラマブルシェーダーを使う]]
 +
* [[InfinitespaceStudios.Pipeline]]
  
<syntaxhighlight lang="text">
+
== HLSL側を書く ==
// file: effect4.fx
+
[[ワールド座標]](ワールド空間内での位置や傾きなど)は、
  
// ----------------------------------------------------------------
+
一般的な[[シェーダー]]では[[ユニフォーム変数]]で受け取る。
// MonoGame PipelineでOpenGL環境の場合は「OPENGL」シンボルが立っている。
+
<source lang="hlsl">
// 以下は定型文だと思ってコピペしとけ。
+
float4x4 World; //これがワールド座標
#if OPENGL
+
float4x4 View;
#define SV_POSITION POSITION
+
float4x4 Projection;
#define VS_SHADERMODEL vs_3_0
+
</source>
#define PS_SHADERMODEL ps_3_0
 
#else
 
#define VS_SHADERMODEL vs_4_0_level_9_1
 
#define PS_SHADERMODEL ps_4_0_level_9_1
 
#endif
 
  
// ----------------------------------------------------------------
+
一方、[[ハードウェアインスタンシング]]を使う場合はダイナミック頂点バッファとして複数の値を受け取る。
// 呼び出し側(C#)から設定されるグローバル変数(シェーダー内では実質定数)
+
<source lang="hlsl">
float4x4 myView : VIEW;
+
float4x4 View;
float4x4 myProjection : PROJECTION;
+
float4x4 Projection;
  
// ----------------------------------------------------------------
+
// これがワールド座標群を格納したダイナミック頂点バッファ。
// 入出力用の構造体
+
// C#側にfloat4x4の送信手段がないので「float4が4個」で代用している。
struct VertexPositionColor
+
struct VSInstance
 
{
 
{
float4 Position : POSITION0;
+
float4 w1 : BLENDWEIGHT0;
float4 Color : COLOR;
+
float4 w2 : BLENDWEIGHT1;
 +
float4 w3 : BLENDWEIGHT2;
 +
float4 w4 : BLENDWEIGHT3;
 
};
 
};
  
// ----------------------------------------------------------------
+
// 「float4が4個」をシェーダー関数内でfloat4x4に再構成して利用する。
// バーテックスシェーダー
+
VSOutput VSFunc(VSInput input, VSInstance instance)
VertexPositionColor MyVertexShader(
 
VertexPositionColor input,
 
float3 position : POSITION1
 
)
 
 
{
 
{
     // inputの位置をpositionほど移動させる。
+
     float4x4 world = float4x4(instance.w1, instance.w2, instance.w3, instance.w4);
    // 今回は移動だけ。
 
    // 回転したい場合なども同じ要領でここで変形させる。
 
input.Position.xyz += position;
 
 
 
    // 頂点をカメラから見た座標に変換する。
 
input.Position = mul( input.Position, mul(myView, myProjection));
 
return input;
 
 
}
 
}
  
// ----------------------------------------------------------------
+
</source>
// ピクセルシェーダー
 
float4 MyPixelShader(float4 color : COLOR) : COLOR0
 
{
 
return color;
 
}
 
  
technique HardwareInstancing
+
== C#側を書く ==
 +
まず3Dモデルのインスタンス([[ワールド座標]])を格納する[[クラス]]を作る。
 +
<source lang="csharp">
 +
public struct VertexDynamicInstance : IVertexType
 
{
 
{
pass Pass1
+
    public Matrix World;
{
 
VertexShader = compile VS_SHADERMODEL MyVertexShader();
 
PixelShader  = compile PS_SHADERMODEL MyPixelShader();
 
}
 
}
 
</syntaxhighlight>
 
==C#で呼び出してみる==
 
この[[コード]]は「[[MonoGameのカメラを作る]]」で作ったカメラを使っている。Cameraクラスは[[MonoGame]]標準物ではないので注意。
 
  
<syntaxhighlight lang="csharp">
+
    public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration(
    using System;
+
        new VertexElement( 0, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0),
    using System.Collections.Generic;
+
        new VertexElement(16, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 1),
    using System.Text;
+
        new VertexElement(32, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 2),
    using Microsoft.Xna.Framework;
+
        new VertexElement(48, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 3));
    using Microsoft.Xna.Framework.Graphics;
 
    using Microsoft.Xna.Framework.Input;
 
  
     public class Game1 : Game
+
     VertexDeclaration IVertexType.VertexDeclaration
 
     {
 
     {
         GraphicsDeviceManager graphics;
+
         get { return VertexDynamicInstance.VertexDeclaration; }
 +
    }
 +
}
 +
</source>
  
        Camera camera;
+
とりあえず動作確認用に1000個のテストデータを作る。
        Effect effect;
+
<source lang="csharp">
         VertexBuffer triangleVertexBuffer;
+
var rand = new System.Random();
         IndexBuffer indexBuffer;
+
var instances = new VertexDynamicInstance[1000];
         DynamicVertexBuffer instanceVertexBuffer;
+
for (int i = 0; i < instances.Length; i++)
 +
{
 +
    instances[i].World = Matrix.CreateTranslation(
 +
         (float)rand.NextDouble(),
 +
         (float)rand.NextDouble(),
 +
         (float)rand.NextDouble());
 +
}
 +
</source>
  
        VertexPositionColor[] vertices = {
+
インスタンス群を格納したダイナミック頂点バッファを生成する。
            new VertexPositionColor(new Vector3(-1, 1, 0), Color.Blue ),
+
<source lang="csharp">
            new VertexPositionColor(new Vector3( 1,  1, 0), Color.White),
+
// ダイナミック頂点バッファを作って
            new VertexPositionColor(new Vector3( 1, -1, 0), Color.Red  )
+
var instanceVertexBuffer = new DynamicVertexBuffer(graphicsDevice,
        };
+
    VertexDynamicInstance.VertexDeclaration,
 +
    instances.Length,
 +
    BufferUsage.WriteOnly);
  
        Vector3[] instances = new Vector3[] {
+
// インスタンス群を入れ込む
            new Vector3(0f, 0f, 0f),
+
instanceVertexBuffer.SetData(instances);
            new Vector3(1f, 1f, 0f),
+
</source>
            new Vector3(2f, 2f, 0f)
 
        };
 
  
        public Game1()
+
描画する。
        {
+
<source lang="csharp">
            graphics = new GraphicsDeviceManager(this);
+
// 頂点バッファとダイナミック頂点バッファの2つを指定する。
            Content.RootDirectory = "Content";
+
graphicsDevice.SetVertexBuffers(
        }
+
    new VertexBufferBinding( meshPart.VertexBuffer, meshPart.VertexOffset, 0),
 +
    new VertexBufferBinding( instanceVertexBuffer , 0, 1));
  
        protected override void Initialize()
+
// インデックスバッファは普通に指定する。
        {
+
graphicsDevice.Indices = meshPart.IndexBuffer;
            IsMouseVisible = true;
 
            base.Initialize();
 
        }
 
  
        protected override void LoadContent()
+
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
+
{
            // 即席カメラ
+
    pass.Apply();
            camera = new Camera(this);
 
            camera.Position = new Vector3(0, 0, 10);
 
            camera.Target = new Vector3(0, 0, 0);
 
  
            // エフェクト読み込み
+
    // ドローコールはDrawInstancedPrimitivesで呼び出す。
            effect = Content.Load<Effect>("effect4");
+
    graphicsDevice.DrawInstancedPrimitives(
 
+
         primitiveType: PrimitiveType.TriangleList,
            // バーテックスバッファーを生成
+
        baseVertex: 0,
            triangleVertexBuffer = new VertexBuffer(
+
        startIndex: 0,
                GraphicsDevice,
+
        primitiveCount: meshPart.PrimitiveCount,
                VertexPositionColor.VertexDeclaration,
+
        instanceCount: instances.Length);
                vertices.Length,
+
}
                BufferUsage.WriteOnly);
+
</source>
            triangleVertexBuffer.SetData<VertexPositionColor>(vertices);
 
 
 
            // インデックスバッファーを生成
 
            var index = new ushort[] { 0, 1, 2 };
 
            indexBuffer = new IndexBuffer(
 
                GraphicsDevice,
 
                IndexElementSize.SixteenBits,
 
                index.Length,
 
                BufferUsage.WriteOnly);
 
            indexBuffer.SetData<ushort>(index);
 
 
 
            // インスタンスバッファーを生成
 
            instanceVertexBuffer = new DynamicVertexBuffer(
 
                GraphicsDevice,
 
                new VertexDeclaration(
 
                    new VertexElement(
 
                        offset: 0,
 
                        elementFormat: VertexElementFormat.Vector3,
 
                        elementUsage: VertexElementUsage.Position,
 
                        usageIndex: 1)),
 
                instances.Length,
 
                BufferUsage.WriteOnly);
 
            instanceVertexBuffer.SetData(instances);
 
 
 
         }
 
 
 
        protected override void Update(GameTime gameTime)
 
        {
 
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
 
                Exit();
 
 
 
            // TODO: Add your update logic here
 
            camera.Angle += 1f;
 
 
 
            base.Update(gameTime);
 
        }
 
 
 
        protected override void Draw(GameTime gameTime)
 
        {
 
            GraphicsDevice.Clear(Color.CornflowerBlue);
 
 
 
            // シェーダーのグローバル変数に値をセットする
 
            effect.Parameters["myView"].SetValue(camera.View);
 
            effect.Parameters["myProjection"].SetValue(camera.Projection);
 
 
 
            // バーテックスバッファーを転送
 
            GraphicsDevice.SetVertexBuffers(
 
                new VertexBufferBinding(triangleVertexBuffer, 0, 0),
 
                new VertexBufferBinding(instanceVertexBuffer, 0, 1));
 
 
 
            // インデックスバッファーを転送
 
            GraphicsDevice.Indices = indexBuffer;
 
 
 
            // インスタンスバッファーを転送
 
            instanceVertexBuffer.SetData(instances, 0, instances.Length, SetDataOptions.Discard);
 
 
 
            // 描画
 
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
 
            {
 
                pass.Apply();
 
 
 
                graphics.GraphicsDevice.DrawInstancedPrimitives(
 
                    primitiveType: PrimitiveType.TriangleList,
 
                    baseVertex: 0,
 
                    startIndex: 0,
 
                    primitiveCount: vertices.Length / 3,
 
                    instanceCount: instances.Length);
 
  
            }
+
== Mac上で動かしてみる ==
 
 
            base.Draw(gameTime);
 
        }
 
    }
 
</syntaxhighlight>
 
 
 
==Mac上で動かしてみる==
 
 
こんな感じ。[[Mac]]でも[[HLSL]]で書いた[[シェーダー]]が使えてる。なお、[[OpenGL]]系の環境では[[シェーダーモデル]]は3.0までだ。まあSM3.0もあれば元気があれば何でもできる。
 
こんな感じ。[[Mac]]でも[[HLSL]]で書いた[[シェーダー]]が使えてる。なお、[[OpenGL]]系の環境では[[シェーダーモデル]]は3.0までだ。まあSM3.0もあれば元気があれば何でもできる。
  
 
<movie>https://youtu.be/qU-toUOhvRI</movie>
 
<movie>https://youtu.be/qU-toUOhvRI</movie>
  
==関連項目==
 
  
*[[MonoGameでプログラマブルシェーダーを使う]]
+
 
*[[MonoGameでピクセルシェーダーを使ってテクスチャを貼る‎]]
+
== 関連項目 ==
*[[ハードウェアインスタンシング]]
+
* [[MonoGameでHLSLにMatrixを渡す]]
*[[疑似インスタンシング]]
+
* [[MonoGameでプログラマブルシェーダーを使う]]
*[[メッシュベイカー]]
+
* [[MonoGameでピクセルシェーダーを使ってテクスチャを貼る‎]]
*[[ドローコール]]
+
* [[ハードウェアインスタンシング]]
 +
* [[疑似インスタンシング]]
 +
* [[メッシュベイカー]]
 +
* [[ドローコール]]
  
 
[[category: MonoGame]]
 
[[category: MonoGame]]
 
[[category: HLSL]]
 
[[category: HLSL]]

2020年4月15日 (水) 05:58時点における最新版

1つのポリゴンメッシュGPU側で複製することでDrawコールを減らして描画を高速化する「ハードウェアインスタンシング」という技術がある。

ただMonoGameOpenGLの組み合わせの環境では長らく「new NotImplementedException()」であった。

これがついにMonoGame 3.7でOpenGL環境でもハードウェアインスタンシングが使えるようになったそうだ。さっそくMacOS上で試してみた。ハードウェアインスタンシングが使えるとなるとボリュームレンダリングの実装が捗る可能性がある。

Macでシェーダーをコンパイルできるようにする[編集 | ソースを編集]

Mac上でHLSLをコンパイルする方法は以下を参照。

HLSL側を書く[編集 | ソースを編集]

ワールド座標(ワールド空間内での位置や傾きなど)は、

一般的なシェーダーではユニフォーム変数で受け取る。

float4x4 World; //これがワールド座標
float4x4 View;
float4x4 Projection;

一方、ハードウェアインスタンシングを使う場合はダイナミック頂点バッファとして複数の値を受け取る。

float4x4 View;
float4x4 Projection;

// これがワールド座標群を格納したダイナミック頂点バッファ。
// C#側にfloat4x4の送信手段がないので「float4が4個」で代用している。
struct VSInstance
{
	float4 w1 : BLENDWEIGHT0;
	float4 w2 : BLENDWEIGHT1;
	float4 w3 : BLENDWEIGHT2;
	float4 w4 : BLENDWEIGHT3;
};

// 「float4が4個」をシェーダー関数内でfloat4x4に再構成して利用する。
VSOutput VSFunc(VSInput input, VSInstance instance)
{
    float4x4 world = float4x4(instance.w1, instance.w2, instance.w3, instance.w4);
}

C#側を書く[編集 | ソースを編集]

まず3Dモデルのインスタンス(ワールド座標)を格納するクラスを作る。

public struct VertexDynamicInstance : IVertexType
{
    public Matrix World;

    public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration(
        new VertexElement( 0, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0),
        new VertexElement(16, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 1),
        new VertexElement(32, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 2),
        new VertexElement(48, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 3));

    VertexDeclaration IVertexType.VertexDeclaration
    {
        get { return VertexDynamicInstance.VertexDeclaration; }
    }
}

とりあえず動作確認用に1000個のテストデータを作る。

var rand = new System.Random();
var instances = new VertexDynamicInstance[1000];
for (int i = 0; i < instances.Length; i++)
{
    instances[i].World = Matrix.CreateTranslation(
        (float)rand.NextDouble(), 
        (float)rand.NextDouble(), 
        (float)rand.NextDouble());
}

インスタンス群を格納したダイナミック頂点バッファを生成する。

// ダイナミック頂点バッファを作って
var instanceVertexBuffer = new DynamicVertexBuffer(graphicsDevice,
    VertexDynamicInstance.VertexDeclaration,
    instances.Length,
    BufferUsage.WriteOnly);

// インスタンス群を入れ込む
instanceVertexBuffer.SetData(instances);

描画する。

// 頂点バッファとダイナミック頂点バッファの2つを指定する。
graphicsDevice.SetVertexBuffers(
    new VertexBufferBinding( meshPart.VertexBuffer, meshPart.VertexOffset, 0),
    new VertexBufferBinding( instanceVertexBuffer , 0, 1));

// インデックスバッファは普通に指定する。
graphicsDevice.Indices = meshPart.IndexBuffer;

foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
    pass.Apply();

    // ドローコールはDrawInstancedPrimitivesで呼び出す。
    graphicsDevice.DrawInstancedPrimitives(
        primitiveType: PrimitiveType.TriangleList,
        baseVertex: 0,
        startIndex: 0,
        primitiveCount: meshPart.PrimitiveCount,
        instanceCount: instances.Length);
}

Mac上で動かしてみる[編集 | ソースを編集]

こんな感じ。MacでもHLSLで書いたシェーダーが使えてる。なお、OpenGL系の環境ではシェーダーモデルは3.0までだ。まあSM3.0もあれば元気があれば何でもできる。


関連項目[編集 | ソースを編集]