ComputeShaderでProcedural

今更ながらに ComputeShader でも触っておくかと思い、何がしたいのか考えを巡らせた挙げ句、 GeometoryShader みたいなことがしたかったというあんまり面白くない結論に達した。

Graphics.DrawProcedural() かなー?と思い、ググって漁ったが、思ったような解説が無かったので少し苦労した。みんな、「インスタンシングで大量表示!物量!」がやりたいみたいで、自分の目的とはちょっと違ったのだ。

自分がやりたかったのは、

  1. 位置のみ渡す
  2. ComputeShader でその点を中心としたポリゴンを生成
  3. 描画

というような、先ほども書いたように GeometoryShader なんである。

まずは、スクリプト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Assertions;
using Unity.Mathematics;

public class TestComputeRenderer : MonoBehaviour
{
public ComputeShader ComputeShader;
private ComputeBuffer PreCalc;
private ComputeBuffer PostCalc;
private Material Material;
private MaterialPropertyBlock Properties;
private int KernelID;

void OnEnable()
{
if (PreCalc == null)
{
PreCalc = new ComputeBuffer(1, Marshal.SizeOf(typeof(Source)));
}
if (PostCalc == null)
{
PostCalc = new ComputeBuffer(3, Marshal.SizeOf(typeof(Output)));
}

KernelID = ComputeShader.FindKernel("CSMain");
ComputeShader.SetBuffer(KernelID, "_Source", PreCalc);
ComputeShader.SetBuffer(KernelID, "_Output", PostCalc);

if (Material == null)
{
Material = new Material(Shader.Find("TestCompute"));
Properties = new MaterialPropertyBlock();
Properties.SetBuffer("_Source", PostCalc);
}
}

void OnDisable()
{
PreCalc.Release();
PostCalc.Release();
Destroy(Material);
Material = null;
}

void LateUpdate()
{
var data = new Source[]
{
new Source(){ Position = new float3(0, 0, 0) },
};
PreCalc.SetData(data);
ComputeShader.Dispatch(KernelID, 1, 1, 1);

var bounds = new Bounds(Vector3.zero, Vector3.one * 1000);
Graphics.DrawProcedural(Material, bounds, MeshTopology.Triangles, 3, 1, null, Properties, ShadowCastingMode.Off, false, 0);
}

[StructLayout(LayoutKind.Sequential)]
public struct Source
{
public float3 Position;
}

[StructLayout(LayoutKind.Sequential)]
public struct Output
{
public float3 Position;
}
}
  • 点が一つだけ入る ComputeBuffer と、計算後の点が三つ入る ComputeBuffer を用意する
  • それぞれを、 ComputeShder 及び描画用の Material/MaterialPropertyBlock に、 .SetBuffer() する
  • 計算前の ComputeBuffer に、点のデータを .SetData() する
  • ComputeShader を .Dispatch() で起動する
  • Graphics.DrawProcedural() を呼ぶ

次に ComputeShader 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma kernel CSMain

struct Source
{
float3 Position;
};

struct Output
{
float3 Position;
};

StructuredBuffer<Source> _Source;
RWStructuredBuffer<Output> _Output;

[numthreads(1, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
_Output[id.x+0].Position = _Source[id.x].Position + float3(-1, 1, 0);
_Output[id.x+1].Position = _Source[id.x].Position + float3(1, 1, 0);
_Output[id.x+2].Position = _Source[id.x].Position + float3(0, -1, 0);
}

大したことはしてない。入ってきた点一つに対して、三つの点を出力してるだけ。

点の番号は SV_DispatchThreadID.x なのだが、この辺の説明は他を読んだ方が早いし詳しいです。

最後に描画用の Vertex/FragmentShader 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Shader "TestCompute"
{
Properties
{
}

SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Cull Off
Lighting Off
BlendOp Add, Add
Blend One One, One One
ZWrite On
ZTest Always
CGPROGRAM
#pragma target 4.5
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct c2v
{
float3 Position;
};

struct v2f
{
float4 vertex : SV_POSITION;
};

StructuredBuffer<c2v> _Source;

v2f vert(uint vertexID : SV_VertexID)
{
v2f o;
o.vertex = UnityObjectToClipPos(_Source[vertexID].Position);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
return fixed4(1, 0, 0, 1);
}
ENDCG
}
}
}

スクリプトで .SetBuffer() したものが StructureBuffer として渡ってくるので、それをいつも通り変換して送り出すだけの簡単なお仕事。

とりあえず、最小構成はこんな感じというのが分かった。