Unityでテクスチャ画像をCubeで表現:DrawMeshInstancedIndirect + Compute Shader
前: Unityでテクスチャ画像をCubeで表現:Instantiate - メモの穴
前回はInstantiateを使ってCubeを生成して並べた。
ただこの方法では、実行するととんでもなく遅い。
Cubeを表示せずに実行した場合がこれ。
FPSが90~100くらいで、SetPass callsが9。
ここから、Cubeを表示すると、
FPSが8~9くらいで、SetPass callsが32779。
SetPass callsやここでは出ていないDraw callsは、CPUからGPUに向けてどれだけ命令を送っているのかを示す値。
前回のスクリプトでは、Cubeを表示する際に、テクスチャ画像のピクセル分(128x128)だけInstantiateで生成しているので、当然SetPass callsが多くなる。
パフォーマンスの改善点は他にもあるとは思うけど、今回はこのSetPass callsを減らしてみる。
環境
Windows10
Unity 2018.2.2f1
GPUインスタンシング
同じようなオブジェクトを大量に描画するアプローチとしてGPUインスタンシングというものがある。
UnityでGPUインスタンシングを扱うにはいくつか方法がある。
今回はとりあえず上記のサイトに合わせてDrawMeshInstancedIndirectを使ってやってみる。
スクリプト、シェーダー
今回のスクリプトやシェーダーは以下のサイト達を参考にしている。特に1番上のものがベースになっている。同じようなことをやりたい場合はまずこれらのサイトを参考にするといいと思う。
基本的には、制御するためのスクリプトと、描画用のシェーダー(サーフェイスシェーダーや頂点・フラグメントシェーダーなど)が必要で、プラスアルファでコンピュートシェーダーを使う感じ?
今回は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を元に、ピクセルの位置や色を反映させる。
実行
FPSが90~100くらいで、SetPass callsが13。
だいぶ改善できた。