メモの穴

メモ化

Unityでテクスチャ画像をCubeで表現:DrawMeshInstancedIndirect + Compute Shader

前: Unityでテクスチャ画像をCubeで表現:Instantiate - メモの穴


前回はInstantiateを使ってCubeを生成して並べた。

ただこの方法では、実行するととんでもなく遅い。

Cubeを表示せずに実行した場合がこれ。

f:id:memonoana:20180901163316p:plain:w500

FPSが90~100くらいで、SetPass callsが9。

ここから、Cubeを表示すると、

f:id:memonoana:20180901163734p:plain:w500

FPSが8~9くらいで、SetPass callsが32779。

SetPass callsやここでは出ていないDraw callsは、CPUからGPUに向けてどれだけ命令を送っているのかを示す値。

nn-hokuson.hatenablog.com

light11.hatenadiary.com

前回のスクリプトでは、Cubeを表示する際に、テクスチャ画像のピクセル分(128x128)だけInstantiateで生成しているので、当然SetPass callsが多くなる。

パフォーマンスの改善点は他にもあるとは思うけど、今回はこのSetPass callsを減らしてみる。

環境

Windows10
Unity 2018.2.2f1

GPUインスタンシング

同じようなオブジェクトを大量に描画するアプローチとしてGPUインスタンシングというものがある。

docs.unity3d.com

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp


UnityでGPUインスタンシングを扱うにはいくつか方法がある。

gottaniprogramming.seesaa.net

今回はとりあえず上記のサイトに合わせてDrawMeshInstancedIndirectを使ってやってみる。

スクリプト、シェーダー

今回のスクリプトやシェーダーは以下のサイト達を参考にしている。特に1番上のものがベースになっている。同じようなことをやりたい場合はまずこれらのサイトを参考にするといいと思う。

github.com

tips.hecomi.com

gottaniprogramming.seesaa.net

基本的には、制御するためのスクリプトと、描画用のシェーダー(サーフェイスシェーダーや頂点・フラグメントシェーダーなど)が必要で、プラスアルファでコンピュートシェーダーを使う感じ?

今回はCubeの位置をコンピュートシェーダーで計算する。今回みたいにただ並べるだけなら、コンピュートシェーダーを使うメリットは無いような気がするけど、今後の勉強のために使う。

スクリプト

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CopyImage : MonoBehaviour
{
  [SerializeField]
  private Mesh pixelMesh;           // ピクセルを表現するメッシュ
  [SerializeField]
  private Material pixelMaterial;   // ピクセルのマテリアル(描画用シェーダー)
  [SerializeField]
  private ShadowCastingMode castShadows = ShadowCastingMode.Off;
  [SerializeField]
  private bool receiveShadows = false;
  [SerializeField]
  private ComputeShader positionComputeShader;  // コンピュートシェーダー
  [SerializeField]
  private GameObject image;                     // テクスチャを貼ってるオブジェクト

  private int pixelCount;                 // ピクセル数
  private int positionComputeKernelId;    // カーネルID
  private ComputeBuffer positionBuffer;   // ピクセル位置を格納するバッファ
  private ComputeBuffer colorBuffer;      // ピクセルカラーを格納するバッファ
  private ComputeBuffer argsBuffer;       // DrawMeshInstancedIndirect用のバッファ
  private uint[] args = new uint[] { 0, 0, 0, 0, 0 };  // DrawMeshInstancedIndirect用の配列

  void Start()
  {
    CreateBuffers();
  }

  void Update()
  {
    UpdateBuffers();
    Graphics.DrawMeshInstancedIndirect(pixelMesh, 0, pixelMaterial, pixelMesh.bounds, argsBuffer, 0, null, castShadows, receiveShadows);
  }

  private void CreateBuffers()
  {
    // テクスチャ画像の取得
    Texture2D texture = (Texture2D)image.GetComponent<Renderer>().material.mainTexture;
    // ピクセル数の計算
    int dim = texture.width;
    pixelCount = dim * dim;

    // カーネルIDの取得
    positionComputeKernelId = positionComputeShader.FindKernel("CSMain");

    // バッファの初期化
    argsBuffer = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments);
    positionBuffer = new ComputeBuffer(pixelCount, 16);
    colorBuffer = new ComputeBuffer(pixelCount, 16);
    
    // テクスチャのピクセルカラーをcolorBufferにセット
    colorBuffer.SetData(texture.GetPixels());

    // 描画用シェーダーにバッファをセット
    pixelMaterial.SetBuffer("positionBuffer", positionBuffer);
    pixelMaterial.SetBuffer("colorBuffer", colorBuffer);

    // argsBufferにメッシュの頂点数とメッシュの数を格納
    uint numIndices = (pixelMesh != null) ? (uint)pixelMesh.GetIndexCount(0) : 0;
    args[0] = numIndices;
    args[1] = (uint)pixelCount;
    argsBuffer.SetData(args);

    // コンピュートシェーダーにバッファ、値をセット
    positionComputeShader.SetBuffer(positionComputeKernelId, "positionBuffer", positionBuffer);
    positionComputeShader.SetFloat("_Dim", dim);
    float cubeScale = image.transform.localScale.x * 10 / dim;
    positionComputeShader.SetFloat("_CubeScale", cubeScale);
    positionComputeShader.SetVector("_Pivot", this.transform.position);
  }

  private void UpdateBuffers()
  {
    // カーネルの実行
    positionComputeShader.Dispatch(positionComputeKernelId, pixelCount / 8, 1, 1);
  }

  void OnDisable()
  {
    // バッファの解放
    if (positionBuffer != null) positionBuffer.Release();
    positionBuffer = null;
    if (colorBuffer != null) colorBuffer.Release();
    colorBuffer = null;
    if (argsBuffer != null) argsBuffer.Release();
    argsBuffer = null;
  }
}

スクリプトでは主に各種バッファ、値の用意とシェーダーへの設定、DrawMeshInstancedIndirectの実行を行う。

Updateの中でカーネルを実行してるけど、今回の目的だと、ただCubeの位置を1度計算するだけなので、Startでやれば十分。この後動かしたりする場合は、Updateで更新される情報をコンピュートシェーダーに渡したりする。

テクスチャ画像のピクセルカラーを取得するのは前回同様GetPixelsでやってるけど、コンピュートシェーダー使ってもできると思う。

コンピュートシェーダー

#pragma kernel CSMain

RWStructuredBuffer<float4> positionBuffer;

float _Dim;         // 1辺のピクセル数
float _PixelScale;  // ピクセル1個のスケール
float3 _Pivot;      // 並べる基準点

[numthreads(8, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
  // ピクセル位置の計算
  float2 uv = float2((id.x % (int)_Dim) / _Dim, floor(id.x / _Dim) / _Dim);
  float x = _Pivot.x + uv.x * _PixelScale * _Dim;
  float y = _Pivot.y + uv.y * _PixelScale * _Dim;
  float z = _Pivot.z;
  float4 pos = float4(x, y, z, _PixelScale);
  positionBuffer[id.x] = pos;
}

コンピュートシェーダーでは、ピクセル位置を計算してpositionBufferに格納する。ピクセル1個のスケールもここで格納する。

描画用シェーダー(サーフェイスシェーダー)

Shader "Custom/CopyImage" {
  Properties {
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200

    CGPROGRAM
    #pragma surface surf Standard addshadow
    #pragma multi_compile_instancing
    #pragma instancing_options procedural:setup
    #pragma target 3.0

    sampler2D _MainTex;
    half _Glossiness;
    half _Metallic;

    struct Input {
      float2 uv_MainTex;
    };

    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<float4> positionBuffer;
    StructuredBuffer<float4> colorBuffer;
    #endif

    void setup () {
      #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
      // ピクセル位置の反映
      float4 position = positionBuffer[unity_InstanceID];
      float scale = position.w;
      unity_ObjectToWorld._11_21_31_41 = float4(scale, 0, 0, 0);
      unity_ObjectToWorld._12_22_32_42 = float4(0, scale, 0, 0);
      unity_ObjectToWorld._13_23_33_43 = float4(0, 0, scale, 0);
      unity_ObjectToWorld._14_24_34_44 = float4(position.xyz, 1);
      #endif
    }

    void surf (Input IN, inout SurfaceOutputStandard o) {
      float4 col = 1.0f;
      #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
      // ピクセルカラーの反映
      col = colorBuffer[unity_InstanceID];
      #else
      col = float4(0, 0, 1, 1);
      #endif
      fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * col;
      o.Albedo = c.rgb;
      o.Metallic = _Metallic;
      o.Smoothness = _Glossiness;
      o.Alpha = c.a;
    }
    ENDCG
  }
  FallBack "Diffuse"
}

サーフェイスシェーダーではpositionBufferとcolorBufferを元に、ピクセルの位置や色を反映させる。

実行

f:id:memonoana:20180902231737p:plain:w500

FPSが90~100くらいで、SetPass callsが13。

だいぶ改善できた。