├── .gitignore ├── Assets ├── Resources │ └── Shaders │ │ ├── basic │ │ ├── basic-shader-per-renderer.shader │ │ └── basic-shader.shader │ │ ├── compute │ │ └── countTris.compute │ │ └── procedural │ │ ├── indirect-shader-single-call-ids.shader │ │ ├── indirect-shader-single-call.shader │ │ ├── indirect-shader.shader │ │ └── unpacked-indirect-shader.shader └── Scripts │ ├── ImportStructuredBufferMesh.cs │ ├── RenderingApproach.cs │ ├── StaticCoroutine.cs │ └── Tester.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /Assets/AssetStoreTools* 7 | 8 | # Visual Studio 2015 cache directory 9 | /.vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | 26 | # Unity3D generated meta files 27 | *.pidb.meta 28 | 29 | # Unity3D Generated File On Crash Reports 30 | sysinfo.txt 31 | 32 | # Builds 33 | *.apk 34 | *.unitypackage 35 | 36 | # Added 37 | /Assets/Resources/Models/ 38 | /Assets/Resources/Materials/ 39 | -------------------------------------------------------------------------------- /Assets/Resources/Shaders/basic/basic-shader-per-renderer.shader: -------------------------------------------------------------------------------- 1 | Shader "Basic Shader Per Renderer" { 2 | 3 | Properties { 4 | [PerRendererData]_Color("Main Color", Color) = (1,1,0,1) 5 | } 6 | 7 | SubShader { 8 | 9 | Tags { "LightMode" = "ForwardBase" } 10 | 11 | Pass { 12 | CGPROGRAM 13 | #include "UnityCG.cginc" 14 | #pragma target 5.0 15 | #pragma vertex vert 16 | #pragma fragment frag 17 | 18 | struct appdata { 19 | float4 vertex : POSITION; 20 | float4 normal : NORMAL; 21 | }; 22 | 23 | struct v2f { 24 | float4 pos : SV_POSITION; 25 | float4 col : COLOR; 26 | }; 27 | 28 | uniform fixed4 _LightColor0; 29 | float4 _Color; 30 | 31 | v2f vert(appdata i) 32 | { 33 | v2f o; 34 | 35 | // Position 36 | float4 pos = i.vertex; 37 | float4 nor = i.normal; 38 | o.pos = UnityObjectToClipPos(pos); 39 | 40 | // Lighting 41 | float3 normalDirection = normalize(nor.xyz); 42 | float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT; 43 | float4 LightDirection = normalize(_WorldSpaceLightPos0); 44 | float4 DiffuseLight = saturate(dot(LightDirection, normalDirection))*_LightColor0; 45 | o.col = float4(AmbientLight + DiffuseLight); 46 | 47 | return o; 48 | } 49 | 50 | fixed4 frag(v2f i) : SV_Target 51 | { 52 | fixed4 final = _Color; 53 | final *= i.col; 54 | return final; 55 | } 56 | 57 | ENDCG 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/basic/basic-shader.shader: -------------------------------------------------------------------------------- 1 | Shader "Basic Shader" { 2 | 3 | Properties { 4 | _Color("Main Color", Color) = (1,1,0,1) 5 | } 6 | 7 | SubShader { 8 | 9 | Tags { "LightMode" = "ForwardBase" } 10 | 11 | Pass { 12 | CGPROGRAM 13 | #include "UnityCG.cginc" 14 | #pragma target 5.0 15 | #pragma vertex vert 16 | #pragma fragment frag 17 | 18 | struct appdata { 19 | float4 vertex : POSITION; 20 | float4 normal : NORMAL; 21 | }; 22 | 23 | struct v2f { 24 | float4 pos : SV_POSITION; 25 | float4 col : COLOR; 26 | }; 27 | 28 | uniform fixed4 _LightColor0; 29 | float4 _Color; 30 | 31 | v2f vert(appdata i) 32 | { 33 | v2f o; 34 | 35 | // Position 36 | float4 pos = i.vertex; 37 | float4 nor = i.normal; 38 | o.pos = UnityObjectToClipPos(pos); 39 | 40 | // Lighting 41 | float3 normalDirection = normalize(nor.xyz); 42 | float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT; 43 | float4 LightDirection = normalize(_WorldSpaceLightPos0); 44 | float4 DiffuseLight = saturate(dot(LightDirection, normalDirection))*_LightColor0; 45 | o.col = float4(AmbientLight + DiffuseLight); 46 | 47 | return o; 48 | } 49 | 50 | fixed4 frag(v2f i) : SV_Target 51 | { 52 | fixed4 final = _Color; 53 | final *= i.col; 54 | return final; 55 | } 56 | 57 | ENDCG 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/compute/countTris.compute: -------------------------------------------------------------------------------- 1 | #pragma kernel AccumulateTriangles 2 | #pragma kernel MapTris 3 | 4 | Texture2D _idTex; 5 | RWStructuredBuffer _idaccum; 6 | AppendStructuredBuffer _triappend; 7 | 8 | // kernel to increment / count the triangles ids in the 9 | // id rendertexture and set a buffer per triangle id 10 | [numthreads(1, 1, 1)] 11 | void AccumulateTriangles(uint3 id : SV_DispatchThreadID) 12 | { 13 | float4 px = _idTex[id.xy]; 14 | uint index = 15 | (((int)(px.r * 255) & 0xFF) << 0) | 16 | (((int)(px.g * 255) & 0xFF) << 8) | 17 | (((int)(px.b * 255) & 0xFF) << 16) | 18 | (((int)(px.a * 255) & 0xFF) << 24); 19 | 20 | if(index != 0xFFFFFFFF) _idaccum[index] = true; 21 | } 22 | 23 | // Accumulate a list of 24 | [numthreads(1, 1, 1)] 25 | void MapTris(uint3 id : SV_DispatchThreadID) 26 | { 27 | uint index = id.x; // + id.y + id.z; 28 | bool val = _idaccum[index]; 29 | 30 | if (val) _triappend.Append(id.x); 31 | 32 | _idaccum[index] = false; 33 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/procedural/indirect-shader-single-call-ids.shader: -------------------------------------------------------------------------------- 1 | Shader "Indirect Shader Single Call Ids" { 2 | 3 | Properties {} 4 | 5 | SubShader { 6 | 7 | Tags { "LightMode" = "ForwardBase" } 8 | 9 | Pass { 10 | CGPROGRAM 11 | #include "UnityCG.cginc" 12 | #pragma target 5.0 13 | #pragma vertex vert 14 | #pragma fragment frag 15 | 16 | struct Point { 17 | int modelid; 18 | float3 vertex; 19 | float3 normal; 20 | }; 21 | 22 | struct Other { 23 | float4x4 mat; 24 | float4 color; 25 | }; 26 | 27 | struct v2f { 28 | float4 pos : SV_POSITION; 29 | float4 id : COLOR; 30 | }; 31 | 32 | uniform fixed4 _LightColor0; 33 | 34 | StructuredBuffer other; 35 | StructuredBuffer points; 36 | int idOffset = 0; 37 | 38 | v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID) 39 | { 40 | v2f o; 41 | 42 | // Position 43 | int idx = id + (int)idOffset; 44 | int modelid = points[idx].modelid; 45 | float4x4 mat = other[modelid].mat; 46 | float4 pos = float4(points[idx].vertex,1.0f); 47 | pos = mul(mat, pos); 48 | pos = UnityObjectToClipPos(pos); 49 | 50 | id = floor(idx / 3); 51 | o.id = float4( 52 | ((id >> 0) & 0xFF) / 255.0, 53 | ((id >> 8) & 0xFF) / 255.0, 54 | ((id >> 16) & 0xFF) / 255.0, 55 | ((id >> 24) & 0xFF) / 255.0 56 | ); 57 | o.pos = pos; 58 | 59 | return o; 60 | } 61 | 62 | float4 frag(v2f i) : SV_Target 63 | { 64 | return i.id; 65 | } 66 | 67 | ENDCG 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/procedural/indirect-shader-single-call.shader: -------------------------------------------------------------------------------- 1 | Shader "Indirect Shader Single Call" { 2 | 3 | Properties {} 4 | 5 | SubShader { 6 | 7 | Tags { "LightMode" = "ForwardBase" } 8 | 9 | Pass { 10 | //Cull Off 11 | 12 | CGPROGRAM 13 | #include "UnityCG.cginc" 14 | #pragma target 5.0 15 | #pragma vertex vert 16 | #pragma fragment frag 17 | 18 | struct Point { 19 | int modelid; 20 | float3 vertex; 21 | float3 normal; 22 | }; 23 | 24 | struct Other { 25 | float4x4 mat; 26 | float4 color; 27 | }; 28 | 29 | struct v2f { 30 | float4 pos : SV_POSITION; 31 | float4 col : COLOR; 32 | }; 33 | 34 | uniform fixed4 _LightColor0; 35 | 36 | StructuredBuffer triappend; 37 | StructuredBuffer trilist; 38 | StructuredBuffer other; 39 | StructuredBuffer points; 40 | 41 | v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID) 42 | { 43 | v2f o; 44 | 45 | // Position 46 | uint tid = triappend[id / 3]; 47 | int idx = tid * 3 + (id % 3); 48 | Point pt = points[idx]; 49 | int modelid = pt.modelid; 50 | 51 | float4x4 mat = other[modelid].mat; 52 | float4 pos = float4(pt.vertex,1.0f); 53 | pos = mul(mat, pos); 54 | pos = UnityObjectToClipPos(pos); 55 | 56 | float4 nor = float4(pt.normal, 0.0f); 57 | nor = mul(mat, nor); 58 | nor = mul(unity_ObjectToWorld, nor); 59 | 60 | // Lighting 61 | float3 normalDirection = normalize(nor.xyz); 62 | float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT; 63 | float4 LightDirection = normalize(_WorldSpaceLightPos0); 64 | float4 DiffuseLight = saturate(dot(LightDirection, normalDirection))*_LightColor0; 65 | o.col = float4(AmbientLight + DiffuseLight) * other[modelid].color; 66 | o.pos = pos; 67 | 68 | return o; 69 | } 70 | 71 | fixed4 frag(v2f i) : SV_Target 72 | { 73 | return i.col; 74 | } 75 | 76 | ENDCG 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/procedural/indirect-shader.shader: -------------------------------------------------------------------------------- 1 | Shader "Indirect Shader" { 2 | 3 | Properties { 4 | _Color("Main Color", Color) = (1,1,0,1) 5 | } 6 | 7 | SubShader { 8 | 9 | Tags { "LightMode" = "ForwardBase" } 10 | 11 | Pass { 12 | CGPROGRAM 13 | #include "UnityCG.cginc" 14 | #pragma target 5.0 15 | #pragma vertex vert 16 | #pragma fragment frag 17 | 18 | struct Point { 19 | half modelid; 20 | float3 vertex; 21 | float3 normal; 22 | }; 23 | 24 | struct v2f { 25 | float4 pos : SV_POSITION; 26 | float4 col : COLOR; 27 | }; 28 | 29 | uniform fixed4 _LightColor0; 30 | float4 _Color; 31 | 32 | StructuredBuffer indices; 33 | StructuredBuffer points; 34 | 35 | v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID) 36 | { 37 | v2f o; 38 | 39 | // Position 40 | int idx = indices[id]; 41 | float4 pos = float4(points[idx].vertex,1.0f); 42 | float4 nor = float4(points[idx].normal, 1.0f); 43 | o.pos = UnityObjectToClipPos(pos); 44 | 45 | // Lighting 46 | float3 normalDirection = normalize(nor.xyz); 47 | float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT; 48 | float4 LightDirection = normalize(_WorldSpaceLightPos0); 49 | float4 DiffuseLight = saturate(dot(LightDirection, normalDirection))*_LightColor0; 50 | o.col = float4(AmbientLight + DiffuseLight); 51 | 52 | return o; 53 | } 54 | 55 | fixed4 frag(v2f i) : SV_Target 56 | { 57 | fixed4 final = _Color; 58 | final *= i.col; 59 | return final; 60 | } 61 | 62 | ENDCG 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /Assets/Resources/Shaders/procedural/unpacked-indirect-shader.shader: -------------------------------------------------------------------------------- 1 | Shader "Unpacked Indirect Shader" { 2 | 3 | Properties { 4 | _Color("Main Color", Color) = (1,1,0,1) 5 | } 6 | 7 | SubShader { 8 | 9 | Tags { "LightMode" = "ForwardBase" } 10 | 11 | Pass { 12 | CGPROGRAM 13 | #include "UnityCG.cginc" 14 | #pragma target 5.0 15 | #pragma vertex vert 16 | #pragma fragment frag 17 | 18 | struct Point { 19 | half modelid; 20 | float3 vertex; 21 | float3 normal; 22 | }; 23 | 24 | struct v2f { 25 | float4 pos : SV_POSITION; 26 | float4 col : COLOR; 27 | }; 28 | 29 | uniform fixed4 _LightColor0; 30 | float4 _Color; 31 | 32 | StructuredBuffer points; 33 | 34 | v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID) 35 | { 36 | v2f o; 37 | 38 | // Position 39 | float4 pos = float4(points[id].vertex,1.0f); 40 | float4 nor = float4(points[id].normal, 1.0f); 41 | o.pos = UnityObjectToClipPos(pos); 42 | 43 | // Lighting 44 | float3 normalDirection = normalize(nor.xyz); 45 | float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT; 46 | float4 LightDirection = normalize(_WorldSpaceLightPos0); 47 | float4 DiffuseLight = saturate(dot(LightDirection, normalDirection))*_LightColor0; 48 | o.col = float4(AmbientLight + DiffuseLight); 49 | 50 | return o; 51 | } 52 | 53 | fixed4 frag(v2f i) : SV_Target 54 | { 55 | fixed4 final = _Color; 56 | final *= i.col; 57 | return final; 58 | } 59 | 60 | ENDCG 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Assets/Scripts/ImportStructuredBufferMesh.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using UnityEngine; 3 | 4 | // Utility for converting a unity mesh into 5 | // a buffer of indices and a buffer of attributes 6 | public static class ImportStructuredBufferMesh { 7 | 8 | struct Point 9 | { 10 | public int modelid; 11 | public Vector3 vertex; 12 | public Vector3 normal; 13 | } 14 | 15 | public static void Import(Mesh mesh, ref ComputeBuffer indices, ref ComputeBuffer attr) 16 | { 17 | int[] tris = mesh.triangles; 18 | Vector3[] verts = mesh.vertices; 19 | Vector3[] norms = mesh.normals; 20 | 21 | indices = new ComputeBuffer(tris.Length, Marshal.SizeOf(typeof(int)), ComputeBufferType.Default); 22 | indices.SetData(tris); 23 | 24 | Point[] data = new Point[verts.Length]; 25 | for(int i = 0; i < data.Length; i++) 26 | { 27 | data[i] = new Point() 28 | { 29 | vertex = verts[i], 30 | normal = norms[i] 31 | }; 32 | } 33 | 34 | attr = new ComputeBuffer(data.Length, Marshal.SizeOf(typeof(Point)), ComputeBufferType.Default); 35 | attr.SetData(data); 36 | } 37 | 38 | public static void ImportAndUnpack(Mesh mesh, ref ComputeBuffer attr) 39 | { 40 | int[] tris = mesh.triangles; 41 | Vector3[] verts = mesh.vertices; 42 | Vector3[] norms = mesh.normals; 43 | 44 | Point[] data = new Point[tris.Length]; 45 | for (int i = 0; i < data.Length; i++) 46 | { 47 | int idx = tris[i]; 48 | data[i] = new Point() 49 | { 50 | vertex = verts[idx], 51 | normal = norms[idx] 52 | }; 53 | } 54 | 55 | attr = new ComputeBuffer(data.Length, Marshal.SizeOf(typeof(Point)), ComputeBufferType.Default); 56 | attr.SetData(data); 57 | } 58 | 59 | public static void ImportAllAndUnpack(Mesh[] meshes, ref ComputeBuffer attrbuff) 60 | { 61 | uint[] offsets = new uint[meshes.Length]; 62 | uint totalTriangles = 0; 63 | for (int i = 0; i < meshes.Length; i++) 64 | { 65 | offsets[i] = totalTriangles; 66 | totalTriangles += (uint)meshes[i].triangles.Length; 67 | } 68 | 69 | Point[] data = new Point[totalTriangles]; 70 | for (int i = 0; i < meshes.Length; i++) 71 | { 72 | Mesh mesh = meshes[i]; 73 | uint offset = offsets[i]; 74 | 75 | int[] tris = mesh.triangles; 76 | Vector3[] verts = mesh.vertices; 77 | Vector3[] norms = mesh.normals; 78 | 79 | for (int j = 0; j < tris.Length; j++) 80 | { 81 | int idx = tris[j]; 82 | data[j + offset] = new Point() 83 | { 84 | modelid = i, 85 | vertex = verts[idx], 86 | normal = norms[idx] 87 | }; 88 | } 89 | } 90 | 91 | attrbuff = new ComputeBuffer(data.Length, Marshal.SizeOf(typeof(Point)), ComputeBufferType.Default); 92 | attrbuff.SetData(data); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Assets/Scripts/RenderingApproach.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System.Collections; 5 | 6 | public abstract class RenderingApproach 7 | { 8 | virtual public void Prepare(GameObject model) { } 9 | virtual public void SetEnabled(bool enabled) { } 10 | virtual public void OnRenderObject(Camera cam = null, Transform root = null) { } 11 | virtual public void LateUpdate(Camera cam = null, Transform root = null) { } 12 | virtual public void Dispose() { } 13 | virtual public void OnGUI() { } 14 | } 15 | 16 | // Use the basic Unity Renderer component 17 | public class RendererTest : RenderingApproach 18 | { 19 | GameObject go; 20 | 21 | public override void Prepare(GameObject model) 22 | { 23 | Dictionary matDict = new Dictionary(); 24 | Shader shader = Shader.Find("Basic Shader"); 25 | go = Object.Instantiate(model); 26 | 27 | foreach (var r in go.GetComponentsInChildren()) 28 | { 29 | Color32 col = r.sharedMaterial.color; 30 | if (!matDict.ContainsKey(col)) 31 | { 32 | Material mat = new Material(shader); 33 | mat.color = col; 34 | 35 | matDict[col] = mat; 36 | } 37 | 38 | r.sharedMaterial = matDict[col]; 39 | } 40 | } 41 | 42 | public override void SetEnabled(bool state) 43 | { 44 | go.SetActive(state); 45 | } 46 | 47 | public override void Dispose() 48 | { 49 | Object.Destroy(go); 50 | } 51 | } 52 | 53 | // Use the Graphics.DrawMesh() API 54 | public class DrawMeshTest : RenderingApproach 55 | { 56 | struct DrawSet 57 | { 58 | public Material material; 59 | public Mesh mesh; 60 | public Matrix4x4 localMat; 61 | } 62 | 63 | DrawSet[] _drawArray; 64 | 65 | public override void Prepare(GameObject model) 66 | { 67 | List drawList = new List(); 68 | Dictionary matDict = new Dictionary(); 69 | Shader shader = Shader.Find("Basic Shader"); 70 | 71 | foreach (var r in model.GetComponentsInChildren()) 72 | { 73 | Mesh mesh = r.GetComponent().sharedMesh; 74 | Transform transform = r.transform; 75 | 76 | Color col = r.sharedMaterial.color; 77 | if (!matDict.ContainsKey(col)) 78 | { 79 | Material mat = new Material(shader); 80 | mat.color = col; 81 | matDict.Add(col, mat); 82 | } 83 | 84 | drawList.Add(new DrawSet() 85 | { 86 | material = matDict[col], 87 | mesh = mesh, 88 | localMat = transform.localToWorldMatrix 89 | }); 90 | } 91 | 92 | _drawArray = drawList.ToArray(); 93 | } 94 | 95 | public override void LateUpdate(Camera cam = null, Transform root = null) 96 | { 97 | for(int i = 0; i < _drawArray.Length; i++) 98 | { 99 | DrawSet ds = _drawArray[i]; 100 | Graphics.DrawMesh(ds.mesh, root.localToWorldMatrix * ds.localMat, ds.material, 0); 101 | } 102 | } 103 | } 104 | 105 | // Use the Graphics.DrawMesh() API with MaterialPropertyBlocks 106 | public class DrawMeshWithPropBlockTest : RenderingApproach 107 | { 108 | struct DrawSet 109 | { 110 | public MaterialPropertyBlock propBlock; 111 | public Mesh mesh; 112 | public Matrix4x4 localMat; 113 | } 114 | 115 | DrawSet[] _drawArray; 116 | Material _mat; 117 | 118 | public override void Prepare(GameObject model) 119 | { 120 | List drawList = new List(); 121 | Shader shader = Shader.Find("Basic Shader Per Renderer"); 122 | _mat = new Material(shader); 123 | 124 | foreach (var r in model.GetComponentsInChildren()) 125 | { 126 | Mesh mesh = r.GetComponent().sharedMesh; 127 | Transform transform = r.transform; 128 | 129 | MaterialPropertyBlock mpb = new MaterialPropertyBlock(); 130 | mpb.SetColor("_Color", r.sharedMaterial.color); 131 | 132 | drawList.Add(new DrawSet() 133 | { 134 | propBlock = mpb, 135 | mesh = mesh, 136 | localMat = transform.localToWorldMatrix 137 | }); 138 | } 139 | 140 | _drawArray = drawList.ToArray(); 141 | } 142 | 143 | public override void LateUpdate(Camera cam = null, Transform root = null) 144 | { 145 | Debug.Log(cam); 146 | for (int i = 0; i < _drawArray.Length; i++) 147 | { 148 | DrawSet ds = _drawArray[i]; 149 | Graphics.DrawMesh(ds.mesh, root.localToWorldMatrix * ds.localMat, _mat, 0, cam, 0, ds.propBlock); 150 | } 151 | } 152 | } 153 | 154 | // Use the Graphics.DrawProcedural API with separate 155 | // buffers for both the indices and attributes 156 | public class DrawProceduralTest : RenderingApproach 157 | { 158 | struct DrawSet 159 | { 160 | public Material material; 161 | public ComputeBuffer idxsBuffer; 162 | public ComputeBuffer attrBuffer; 163 | public Matrix4x4 localMat; 164 | public int count; 165 | } 166 | 167 | DrawSet[] drawArray; 168 | 169 | protected virtual void GetBuffers(Mesh mesh, ref ComputeBuffer idxbuff, ref ComputeBuffer attrbuff, ref int count) 170 | { 171 | ImportStructuredBufferMesh.Import(mesh, ref idxbuff, ref attrbuff); 172 | count = idxbuff.count; 173 | } 174 | 175 | protected virtual Shader GetShader() { 176 | return Shader.Find("Indirect Shader"); 177 | } 178 | 179 | public override void Prepare(GameObject model) 180 | { 181 | List drawList = new List(); 182 | Shader shader = GetShader(); 183 | foreach (var r in model.GetComponentsInChildren()) 184 | { 185 | MeshFilter mf = r.GetComponent(); 186 | Mesh mesh = mf.sharedMesh; 187 | ComputeBuffer idxbuff = null; 188 | ComputeBuffer attrbuff = null; 189 | int count = 0; 190 | 191 | GetBuffers(mesh, ref idxbuff, ref attrbuff, ref count); 192 | 193 | Material mat = new Material(shader); 194 | mat.SetBuffer("indices", idxbuff); 195 | mat.SetBuffer("points", attrbuff); 196 | mat.color = r.sharedMaterial.color; 197 | 198 | drawList.Add(new DrawSet() 199 | { 200 | material = mat, 201 | idxsBuffer = idxbuff, 202 | attrBuffer = attrbuff, 203 | count = count, 204 | localMat = r.transform.localToWorldMatrix 205 | }); 206 | } 207 | 208 | drawArray = drawList.ToArray(); 209 | } 210 | 211 | public override void OnRenderObject(Camera cam = null, Transform root = null) 212 | { 213 | for (int i = 0; i < drawArray.Length; i++) 214 | { 215 | DrawSet ds = drawArray[i]; 216 | GL.PushMatrix(); 217 | GL.MultMatrix(root.localToWorldMatrix * ds.localMat); 218 | ds.material.SetPass(0); 219 | Graphics.DrawProcedural(MeshTopology.Triangles, ds.count, 1); 220 | GL.PopMatrix(); 221 | } 222 | } 223 | 224 | public override void Dispose() 225 | { 226 | for (int i = 0; i < drawArray.Length; i++) 227 | { 228 | if(drawArray[i].idxsBuffer != null) drawArray[i].idxsBuffer.Dispose(); 229 | if(drawArray[i].attrBuffer != null) drawArray[i].attrBuffer.Dispose(); 230 | } 231 | } 232 | } 233 | 234 | // Use the Graphics.DrawProcedural API with separate 235 | // buffers for both the indices and attributes 236 | public class UnpackedDrawProceduralTest : DrawProceduralTest 237 | { 238 | protected override void GetBuffers(Mesh mesh, ref ComputeBuffer idxbuff, ref ComputeBuffer attrbuff, ref int count) 239 | { 240 | idxbuff = null; 241 | ImportStructuredBufferMesh.ImportAndUnpack(mesh, ref attrbuff); 242 | count = attrbuff.count; 243 | } 244 | 245 | protected override Shader GetShader() 246 | { 247 | return Shader.Find("Unpacked Indirect Shader"); 248 | } 249 | } 250 | 251 | // Use the Graphics.DrawProcedural API with separate 252 | // buffers for both the indices and attributes 253 | public class VisibleTriangleRenderTest : RenderingApproach 254 | { 255 | struct OtherAttrs 256 | { 257 | public Matrix4x4 matrix; 258 | public Color color; 259 | } 260 | 261 | // Render constants 262 | const int OC_RESOLUTION = 1024; // OC Texture Resolution 263 | const int OC_RENDER_FRAMES = 5; // Number of frames to render the OC over 264 | const float OC_FOV_RATIO = 1.25f; // FOV ratio for the OC render camera 265 | const int MAX_TRIANGLES = 350000; // Number of triangles to render per frame 266 | 267 | // Compute Shader Kernels 268 | const int ACCUM_KERNEL = 0; 269 | const int MAP_KERNEL = 1; 270 | 271 | RenderTexture octex; 272 | Camera occam; 273 | 274 | ComputeShader compute; 275 | ComputeBuffer idaccum, triappend; 276 | 277 | ComputeBuffer attrbuff, otherbuff; 278 | Material mat; 279 | Material idmat; 280 | 281 | Coroutine ocRoutine; 282 | 283 | Transform root; 284 | 285 | public override void Prepare(GameObject model) 286 | { 287 | // OC camera and rendertexture 288 | octex = new RenderTexture(OC_RESOLUTION, OC_RESOLUTION, 16, RenderTextureFormat.ARGB32); 289 | octex.enableRandomWrite = true; 290 | octex.Create(); 291 | occam = new GameObject("OC CAM").AddComponent(); 292 | occam.targetTexture = octex; 293 | occam.enabled = false; 294 | 295 | // Collect the mesh and attribute buffers 296 | List meshes = new List(); 297 | List otherattrs = new List(); 298 | foreach (var r in model.GetComponentsInChildren()) 299 | { 300 | MeshFilter mf = r.GetComponent(); 301 | meshes.Add(mf.sharedMesh); 302 | otherattrs.Add(new OtherAttrs(){ 303 | matrix = r.transform.localToWorldMatrix, 304 | color = r.sharedMaterial.color 305 | }); 306 | } 307 | 308 | // Triangle buffers 309 | ImportStructuredBufferMesh.ImportAllAndUnpack(meshes.ToArray(), ref attrbuff); 310 | otherbuff = new ComputeBuffer(otherattrs.Count, Marshal.SizeOf(typeof(OtherAttrs)), ComputeBufferType.Default); 311 | otherbuff.SetData(otherattrs.ToArray()); 312 | 313 | // Compute Shader Buffers 314 | idaccum = new ComputeBuffer(attrbuff.count / 3, Marshal.SizeOf(typeof(bool))); 315 | triappend = new ComputeBuffer(MAX_TRIANGLES, Marshal.SizeOf(typeof(uint)), ComputeBufferType.Append); 316 | 317 | // Compute Shader & Buffers 318 | compute = Resources.Load("Shaders/compute/countTris"); 319 | 320 | compute.SetBuffer(ACCUM_KERNEL, "_idaccum", idaccum); 321 | compute.SetTexture(ACCUM_KERNEL, "_idTex", octex); 322 | 323 | compute.SetBuffer(MAP_KERNEL, "_idaccum", idaccum); 324 | compute.SetBuffer(MAP_KERNEL, "_triappend", triappend); 325 | 326 | // Material 327 | mat = new Material(Shader.Find("Indirect Shader Single Call")); 328 | mat.SetBuffer("other", otherbuff); 329 | mat.SetBuffer("points", attrbuff); 330 | mat.SetBuffer("triappend", triappend); 331 | 332 | idmat = new Material(Shader.Find("Indirect Shader Single Call Ids")); 333 | idmat.SetBuffer("other", otherbuff); 334 | idmat.SetBuffer("points", attrbuff); 335 | } 336 | 337 | public override void SetEnabled(bool enabled) 338 | { 339 | if (enabled) ocRoutine = StaticCoroutine.Start(GatherTriangles()); 340 | else StaticCoroutine.Stop(ocRoutine); 341 | } 342 | 343 | IEnumerator GatherTriangles() 344 | { 345 | // TODO: Dispatch multiple threads to better use 346 | // compute shader 347 | // TODO: On particularly large models iterating over 348 | // the texture is simpler than iterating over 349 | // every triangle id 1024^2 = 1048576, so models 350 | // will less geometry are more expensive to iterate over 351 | // The data may be more expensive to transfer, though 352 | // TODO: Could use a triangle idex to save on attribute buffer memory 353 | // TODO: Could use predictive positioning with the camera 354 | while (true) 355 | { 356 | while (this.root == null) yield return null; 357 | 358 | Transform root = this.root; 359 | 360 | // Render the OC frame 361 | occam.CopyFrom(Camera.main); 362 | occam.fieldOfView *= OC_FOV_RATIO; 363 | occam.targetTexture = octex; 364 | 365 | // Render over several frames 366 | int totaltris = attrbuff.count / 3; 367 | int trisperframe = totaltris / OC_RENDER_FRAMES; 368 | for (int i = 0; i < totaltris; i += trisperframe) 369 | { 370 | // Set the oc texture to active and clear it 371 | // with an id-color that doesn't clash with a 372 | // triangle id 373 | RenderTexture prev = RenderTexture.active; 374 | RenderTexture.active = octex; 375 | if (i == 0) 376 | { 377 | GL.Clear(true, true, new Color32(0xFF, 0xFF, 0xFF, 0xFF)); 378 | } 379 | 380 | // Draw the set number of triangles 381 | GL.PushMatrix(); 382 | GL.LoadIdentity(); 383 | GL.modelview = occam.worldToCameraMatrix; 384 | GL.LoadProjectionMatrix(occam.projectionMatrix); 385 | GL.MultMatrix(root.localToWorldMatrix); 386 | idmat.SetInt("idOffset", i * 3); 387 | idmat.SetPass(0); 388 | Graphics.DrawProcedural(MeshTopology.Triangles, trisperframe * 3, 1); 389 | 390 | GL.PopMatrix(); 391 | RenderTexture.active = prev; 392 | 393 | if ( i + trisperframe < totaltris) yield return null; 394 | } 395 | 396 | // NOTE 397 | // It's possible that this yield may 398 | // not be necessary -- the compute shaders 399 | // seem to run fairly quickly and 400 | // can just be run after the previous draw 401 | yield return null; 402 | 403 | // accumulate the ids 404 | triappend.SetCounterValue(0); 405 | compute.Dispatch(ACCUM_KERNEL, octex.width, octex.height, 1); 406 | compute.Dispatch(MAP_KERNEL, idaccum.count, 1, 1); 407 | 408 | yield return null; 409 | } 410 | } 411 | 412 | public override void OnRenderObject(Camera cam = null, Transform root = null) 413 | { 414 | this.root = root; 415 | if (Camera.main != cam) return; 416 | 417 | GL.PushMatrix(); 418 | GL.MultMatrix(root.localToWorldMatrix); 419 | mat.SetPass(0); 420 | Graphics.DrawProcedural(MeshTopology.Triangles, MAX_TRIANGLES * 3, 1); 421 | GL.PopMatrix(); 422 | } 423 | 424 | public override void Dispose() 425 | { 426 | Object.Destroy(octex); 427 | attrbuff.Dispose(); 428 | otherbuff.Dispose(); 429 | triappend.Dispose(); 430 | 431 | triappend.Dispose(); 432 | idaccum.Dispose(); 433 | } 434 | } -------------------------------------------------------------------------------- /Assets/Scripts/StaticCoroutine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using UnityEngine; 3 | 4 | // Static helper class to enable anything to use Unity's 5 | // Monobehaviour coroutines 6 | public class StaticCoroutine : MonoBehaviour { 7 | static StaticCoroutine _instance = null; 8 | static StaticCoroutine instance 9 | { 10 | get { 11 | return _instance = _instance ?? new GameObject().AddComponent(); 12 | } 13 | } 14 | 15 | public static Coroutine Start(IEnumerator func) 16 | { 17 | return instance.StartCoroutine(func); 18 | } 19 | 20 | public static void Stop(Coroutine c) 21 | { 22 | instance.StopCoroutine(c); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Assets/Scripts/Tester.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | public class Tester : MonoBehaviour { 5 | public enum Approach 6 | { 7 | DRAW_MESH, 8 | DRAW_MESH_PROP_BLOCK, 9 | DRAW_PROCEDURAL, 10 | UNPACKED_DRAW_PROCEDURAL, 11 | VISIBLE_TRIANGLES, 12 | UNITY_RENDERER 13 | } 14 | 15 | public GameObject testObj; 16 | public Approach approach = Approach.UNITY_RENDERER; 17 | Approach lastApproach = Approach.UNITY_RENDERER; 18 | 19 | Dictionary approaches = new Dictionary(); 20 | RenderingApproach curr { get { return approaches.ContainsKey(approach) ? approaches[approach] : null; } } 21 | 22 | const int DEFAULT_FRAME_TIMEOUT = 20; 23 | int frameTimeout = DEFAULT_FRAME_TIMEOUT; 24 | int frames = 0; 25 | float frametime = 0; 26 | 27 | void Start () { 28 | //approaches[Approach.DRAW_MESH] = new DrawMeshTest(); 29 | //approaches[Approach.DRAW_MESH_PROP_BLOCK] = new DrawMeshWithPropBlockTest(); 30 | //approaches[Approach.DRAW_PROCEDURAL] = new DrawProceduralTest(); 31 | //approaches[Approach.UNPACKED_DRAW_PROCEDURAL] = new UnpackedDrawProceduralTest(); 32 | approaches[Approach.VISIBLE_TRIANGLES] = new VisibleTriangleRenderTest(); 33 | // approaches[Approach.UNITY_RENDERER] = new RendererTest(); 34 | 35 | foreach (var kv in approaches) kv.Value.Prepare(testObj); 36 | } 37 | 38 | private void Update() 39 | { 40 | //if (Camera.current != Camera.main) return; 41 | 42 | // Debug.Log("COUNT: " + Time.frameCount + " : " + Camera.current); 43 | if (approach != lastApproach) 44 | { 45 | frameTimeout = DEFAULT_FRAME_TIMEOUT; 46 | 47 | if (approaches.ContainsKey(lastApproach)) approaches[lastApproach].SetEnabled(false); 48 | if (approaches.ContainsKey(approach)) approaches[approach].SetEnabled(true); 49 | 50 | lastApproach = approach; 51 | 52 | frametime = 0; 53 | frames = 0; 54 | } 55 | 56 | frameTimeout--; 57 | if (frameTimeout < 0) 58 | { 59 | frames++; 60 | frametime += (Time.deltaTime - frametime) / frames; 61 | Debug.Log("FPS: " + (1.0f / frametime).ToString("0.00000") + " - " + (1.0f / Time.deltaTime).ToString("0.00000")); 62 | } 63 | } 64 | 65 | private void LateUpdate() 66 | { 67 | if (curr != null) curr.LateUpdate(Camera.main, transform); 68 | } 69 | 70 | void OnRenderObject() 71 | { 72 | if (curr != null) curr.OnRenderObject(Camera.current, transform); 73 | } 74 | 75 | private void OnDestroy() 76 | { 77 | foreach (var kv in approaches) kv.Value.Dispose(); 78 | } 79 | 80 | private void OnGUI() 81 | { 82 | if (curr != null) curr.OnGUI(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unity-rendering-investigation 2 | 3 | A basic performance investigation around a variety of rendering techniques within Unity. 4 | 5 | # Approaches 6 | ## Unity Renderer MonoBehaviour 7 | Every mesh gets its own _GameObject_, _MeshFilter_, and _Renderer_ components. 8 | 9 | ## Graphics.DrawMesh API 10 | Transform matrices and materials are cached and all meshes are drawn using the `Graphics.DrawMesh()` function. 11 | 12 | ### Details 13 | No overhead and management of _Renderers_ and _MeshFilters_. 14 | 15 | ## Graphics.DrawMesh API with MaterialPropertyBlock 16 | Same as above, but one material is used and each mesh gets its own _MaterialPropertyBlock_ to augment that material. 17 | 18 | ### Details 19 | _MaterialPropertyBlock_ is supposed to be more efficient, but seems to take more time render. 20 | 21 | It's possible that this is just more memory efficient. Results in the same number of set pass calls and batches. 22 | 23 | ## Graphics.DrawProcedural API 24 | Each mesh is converted into two _ComputeBuffers_ for both indices and attributes which are referenced in the vertex shader. A material and matrix are cached for each mesh and rendered using the `Graphics.DrawProcedural()` function and `GL.PushMatrix()` to set the transform of the draw. 25 | 26 | Each material takes both that "points" and "attributes" compute buffers as parameters. 27 | 28 | ### Details 29 | Despite many draws, the Unity stats window only displays that two "draw calls" are being made. 30 | 31 | Every draw requires a new set pass call. 32 | 33 | Because the mesh is procedural this will by-pass all of Unity's internal calculations rendering logic like frustum culling. 34 | 35 | Attributes can be stored in custom formats and unpacked in the vertex shader to save on memory. 36 | 37 | ## Graphics.DrawProcedural API with Unpacked Vertex Buffer 38 | Same as above, but the attributes for the mesh are unpacked into a single _ComputeBuffer_ with three vertices per triangle. 39 | 40 | ### Details 41 | Only pro might be that there is less array access in the vertex shader. 42 | 43 | This approach takes more memory and transforms more vertices. 44 | 45 | ## Graphics.DrawProcedural with Visible Triangles Array 46 | Runs an occlusion pass, counts the triangles, generates an array of visible triangles, and renders only the visible triangles using DrawProcedural. 47 | 48 | ### Details 49 | Requires a few compute shader and prepasses to run. These passes can be done over multiple frames: 50 | - Render whole model to renderTexture 51 | - Clear the "visible" array 52 | - Mark triangles as "visible" in a buffer with a compute shader pass 53 | - Write the triangle ids needed to an append buffer in a compute shader pass 54 | 55 | More memory is required for the extra textures and buffers. 56 | 57 | This limits the amount of triangles that have to be drawn to the primary buffer every frame to a minimal and possibly consistent count. 58 | 59 | Moving a mesh requires updating a buffer with the model attributes. 60 | 61 | # Other Concepts 62 | ## Single ComputeBuffer for All Meshes 63 | One compute buffer could be used to store all the attributes for all meshes with an offset buffer to help address a specific point in the attribute buffer to render. Multiple meshes could then be drawn by instancing and the instanceId can be used to address the specific mesh to draw. 64 | 65 | The biggest issue is that when using `DrawProcedural()`, you have to specify the amount of vertices, which means every mesh must be the same size. 66 | 67 | ## Dynamic ComputeBuffer 68 | Decide which triangles should be drawn and generate the data for the compute buffer that will be drawn. 69 | --------------------------------------------------------------------------------