MonoGameでハードウェアインスタンシングしてみる

提供: MonoBook
2019年10月27日 (日) 00:01時点におけるimported>Administratorによる版
ナビゲーションに移動 検索に移動

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

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

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

HLSLを書く

HLSLはこんな感じ。今回は移動だけで回転はしていない。 Mac上でHLSLコンパイルする方法は「 MonoGameでプログラマブルシェーダーを使う」および「InfinitespaceStudios.Pipeline」の項目を参照。

// file: effect4.fx

// ----------------------------------------------------------------
// MonoGame PipelineでOpenGL環境の場合は「OPENGL」シンボルが立っている。
// 以下は定型文だと思ってコピペしとけ。
#if OPENGL
	#define SV_POSITION POSITION
	#define VS_SHADERMODEL vs_3_0
	#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#)から設定されるグローバル変数(シェーダー内では実質定数)
float4x4 myView : VIEW;
float4x4 myProjection : PROJECTION;

// ----------------------------------------------------------------
// 入出力用の構造体
struct VertexPositionColor
{
	float4 Position : POSITION0;
	float4 Color : COLOR;
};

// ----------------------------------------------------------------
// バーテックスシェーダー
VertexPositionColor MyVertexShader(
	VertexPositionColor input,
	float3 position : POSITION1
	)
{
    // inputの位置をpositionほど移動させる。
    // 今回は移動だけ。
    // 回転したい場合なども同じ要領でここで変形させる。
	input.Position.xyz += position;

    // 頂点をカメラから見た座標に変換する。
	input.Position = mul( input.Position, mul(myView, myProjection));
	return input;
}

// ----------------------------------------------------------------
// ピクセルシェーダー
float4 MyPixelShader(float4 color : COLOR) : COLOR0
{
	return color;
}

technique HardwareInstancing
{
	pass Pass1
	{
		VertexShader = compile VS_SHADERMODEL MyVertexShader();
		PixelShader  = compile PS_SHADERMODEL MyPixelShader();
	}
}

C#で呼び出してみる

このコードは「MonoGameのカメラを作る」で作ったカメラを使っている。CameraクラスはMonoGame標準物ではないので注意。

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;

    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;

        Camera camera;
        Effect effect;
        VertexBuffer triangleVertexBuffer;
        IndexBuffer indexBuffer;
        DynamicVertexBuffer instanceVertexBuffer;

        VertexPositionColor[] vertices = {
            new VertexPositionColor(new Vector3(-1,  1, 0), Color.Blue ),
            new VertexPositionColor(new Vector3( 1,  1, 0), Color.White),
            new VertexPositionColor(new Vector3( 1, -1, 0), Color.Red  )
        };

        Vector3[] instances = new Vector3[] {
            new Vector3(0f, 0f, 0f),
            new Vector3(1f, 1f, 0f),
            new Vector3(2f, 2f, 0f)
        };

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            IsMouseVisible = true;
            base.Initialize();
        }

        protected override void LoadContent()
        {
            // 即席カメラ
            camera = new Camera(this);
            camera.Position = new Vector3(0, 0, 10);
            camera.Target = new Vector3(0, 0, 0);

            // エフェクト読み込み
            effect = Content.Load<Effect>("effect4");

            // バーテックスバッファーを生成
            triangleVertexBuffer = new VertexBuffer(
                GraphicsDevice,
                VertexPositionColor.VertexDeclaration,
                vertices.Length,
                BufferUsage.WriteOnly);
            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);

            }

            base.Draw(gameTime);
        }
    }

Mac上で動かしてみる

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

関連項目