GreasePencilRenderer.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Runtime.InteropServices; 4 using Unity.Mathematics; 5 using UnityEngine; 6 7 public class GreasePencilRenderer : MonoBehaviour 8 { 9 public const int GP_IS_STROKE_VERTEX_BIT = (1 << 30); 10 11 public GreasePencilSO greasePencil; 12 public int frameIdx = 0; 13 14 GraphicsBuffer _gPencilIbo; 15 GraphicsBuffer _verts; 16 GraphicsBuffer _cols; 17 ComputeBuffer _materialBuffer; 18 19 public Material material; 20 21 [Range(0.0f, 1.0f)] public float opacity = 1.0f; 22 public Color colorTint = new(1, 1, 1, 0); 23 void Start() 24 { 25 CreateMesh(); 26 } 27 28 29 void OnDestroy() 30 { 31 ReleaseBuffers(); 32 } 33 34 void ReleaseBuffers() 35 { 36 _gPencilIbo?.Dispose(); 37 _gPencilIbo = null; 38 _verts?.Dispose(); 39 _verts = null; 40 _cols?.Dispose(); 41 _cols = null; 42 _materialBuffer?.Dispose(); 43 _materialBuffer = null; 44 } 45 46 void Update() 47 { 48 // Create the MaterialPropertyBlock to hold our per-draw data. 49 var matProps = new MaterialPropertyBlock(); 50 51 // Set your custom data buffers 52 matProps.SetBuffer("_Pos", _verts); 53 matProps.SetBuffer("_Color", _cols); 54 matProps.SetBuffer("gp_materials", _materialBuffer); 55 56 // Set the standard object-to-world matrix uniform. 57 matProps.SetMatrix("_ObjectToWorld", transform.localToWorldMatrix); 58 matProps.SetFloat("gp_layer_opacity", opacity); 59 matProps.SetColor("gp_layer_tint", colorTint); 60 61 RenderParams rp = new RenderParams(material); 62 rp.worldBounds = new Bounds(Vector3.zero, 1000*Vector3.one); // use tighter bounds 63 rp.matProps = matProps; 64 Graphics.RenderPrimitivesIndexed(rp, MeshTopology.Triangles, _gPencilIbo, _gPencilIbo.count); 65 } 66 67 [StructLayout(LayoutKind.Sequential)] 68 public struct GreasePencilStrokeVert 69 { 70 // float3 pos; 71 public Vector3 pos; 72 73 // float radius; 74 public float radius; 75 76 // int4 mat, stroke_id, point_id, packed_asp_hard_rot; 77 public int mat; 78 public int stroke_id; 79 public int point_id; 80 public int packed_asp_hard_rot; 81 82 // float2 uv_fill; 83 public Vector2 uv_fill; 84 85 // float u_stroke, opacity; 86 public float u_stroke; 87 public float opacity; 88 89 public static int SizeOf => Marshal.SizeOf(typeof(GreasePencilStrokeVert)); 90 }; 91 92 [StructLayout(LayoutKind.Sequential)] 93 public struct GreasePencilColorVert { 94 public float4 vcol; /* Vertex color */ 95 public float4 fcol; /* Fill color */ 96 public static int SizeOf => Marshal.SizeOf(typeof(GreasePencilColorVert)); 97 }; 98 99 [ContextMenu("Build Stroke Mesh")] 100 void CreateMesh() 101 { 102 var vertsStartOffsetsPerLayer = CalculateOffsetsPerLayer(out var totalNumPoints, out var totalVertexOffset); 103 104 // Add extra space at the end of the buffer because of quad load. 105 GreasePencilStrokeVert[] verts = new GreasePencilStrokeVert[totalVertexOffset+2]; 106 GreasePencilColorVert[] cols = new GreasePencilColorVert[totalVertexOffset+2]; 107 108 // a quad for every strokePoint 109 int[] triangleIbo = new int[totalNumPoints*2*3]; 110 int triangleIboIndex = 0; 111 112 for (int layerIdx = 0; layerIdx < greasePencil.data.layers.Count; layerIdx++) 113 { 114 var layer = greasePencil.data.layers[layerIdx]; 115 var strokes = layer.frames[frameIdx].strokes; 116 if (strokes.Count == 0) 117 { 118 continue; 119 } 120 121 var vertsStartOffSets = vertsStartOffsetsPerLayer[layerIdx]; 122 for (int strokeIdx = 0; strokeIdx < strokes.Count; strokeIdx++) 123 { 124 var stroke = strokes[strokeIdx]; 125 var pointsCount = stroke.points.Count; 126 bool isCyclic = stroke.cyclic && pointsCount >= 3; 127 var vertsStartOffset = vertsStartOffSets[strokeIdx]; 128 var numVerts = 1 + pointsCount + (isCyclic ? 1 : 0) + 1; 129 130 // First vertex is not drawn 131 verts[vertsStartOffset].mat = -1; 132 // The first vertex will have the index of the last vertex. 133 verts[vertsStartOffset].stroke_id = vertsStartOffset + numVerts - 1; 134 // 135 // // If the stroke has more than 2 points, add the triangle indices to the index buffer. 136 // if (pointsCount >= 3) 137 // { 138 // var trisSlice = new Span<int3>(triangles.ToArray(), trisStartOffset, numVerts); 139 // foreach (var tri in trisSlice) 140 // { 141 // triangleIbo[triangleIboIndex + 0] = (vertsStartOffset + 1 + tri.x) << 2; 142 // triangleIbo[triangleIboIndex + 1] = (vertsStartOffset + 1 + tri.y) << 2; 143 // triangleIbo[triangleIboIndex + 2] = (vertsStartOffset + 1 + tri.z) << 2; 144 // triangleIboIndex += 3; 145 // } 146 // } 147 148 // Write all the point attributes to the vertex buffers. Create a quad for each point. 149 for (int pointIdx = 0; pointIdx < pointsCount; pointIdx++) 150 { 151 var strokePoint = stroke.points[pointIdx]; 152 var idx = vertsStartOffset + pointIdx + 1; 153 PopulatePoint(strokePoint, out verts[idx], out cols[idx], idx, isCyclic); 154 } 155 verts[vertsStartOffset + pointsCount + 1].mat = -1; 156 void PopulatePoint(PointData strokePoint, out GreasePencilStrokeVert sVert, out GreasePencilColorVert cVert, int idx, bool cyclic) 157 { 158 sVert.pos = strokePoint.Position; 159 // sVert.pos = new float3(idx, 0, 0); 160 var posNext = stroke.points[(idx) % pointsCount].Position; 161 sVert.radius = strokePoint.radius; 162 sVert.opacity = strokePoint.opacity; 163 var offset = vertsStartOffset + idx + 1; 164 // Store if the curve is cyclic in the sign of the point index. 165 sVert.point_id = cyclic ? -offset : offset; 166 sVert.stroke_id = vertsStartOffset; 167 168 /* The material index is allowed to be negative as it's stored as a generic attribute. To 169 * ensure the material used by the shader is valid this needs to be clamped to zero. */ 170 sVert.mat = Math.Max(stroke.material_index, 0) % 256; 171 172 sVert.packed_asp_hard_rot = 0; //todo 173 sVert.u_stroke = 0; //todo 174 sVert.uv_fill = float2.zero; //todo 175 176 cVert.vcol = strokePoint.VertexColor; 177 cVert.fcol = Vector4.one; 178 179 // quad 180 int vertIdxMarkedStroke = ((vertsStartOffset + idx) << 2) | GP_IS_STROKE_VERTEX_BIT; 181 triangleIbo[triangleIboIndex + 0] = vertIdxMarkedStroke + 0; 182 triangleIbo[triangleIboIndex + 1] = vertIdxMarkedStroke + 1; 183 triangleIbo[triangleIboIndex + 2] = vertIdxMarkedStroke + 2; 184 triangleIboIndex += 3; 185 triangleIbo[triangleIboIndex + 0] = vertIdxMarkedStroke + 2; 186 triangleIbo[triangleIboIndex + 1] = vertIdxMarkedStroke + 1; 187 triangleIbo[triangleIboIndex + 2] = vertIdxMarkedStroke + 3; 188 triangleIboIndex += 3; 189 } 190 } 191 } 192 193 verts[totalVertexOffset + 0].mat = -1; 194 verts[totalVertexOffset + 1].mat = -1; 195 196 ReleaseBuffers(); 197 _gPencilIbo = new GraphicsBuffer(GraphicsBuffer.Target.Structured, triangleIbo.Length, sizeof(int)); 198 _gPencilIbo.SetData(triangleIbo); 199 200 _verts = new GraphicsBuffer(GraphicsBuffer.Target.Structured, verts.Length, sizeof(float)*4*3); 201 _verts.SetData(verts); 202 _cols = new GraphicsBuffer(GraphicsBuffer.Target.Structured, verts.Length, sizeof(float)*4*2); 203 _cols.SetData(cols); 204 205 CreateMaterialBuffer(); 206 } 207 208 [StructLayout(LayoutKind.Sequential)] 209 public struct GpMaterialData 210 { 211 public Vector4 stroke_color; 212 public Vector4 fill_color; 213 public Vector4 fill_mix_color; 214 public Vector4 fill_uv_rot_scale; 215 public Vector4 fill_uv_offset_alignment_rot; // Combined for packing 216 public float stroke_texture_mix; 217 public float stroke_u_scale; 218 public float fill_texture_mix; 219 public int flag; 220 221 public static int SizeOf => Marshal.SizeOf(typeof(GpMaterialData)); 222 } 223 private void CreateMaterialBuffer() 224 { 225 var materialDataList = new List<GpMaterialData>(); 226 foreach (var mat in greasePencil.data.materials) 227 { 228 GpMaterialData gpuMat = new GpMaterialData(); 229 230 // Populate the C# struct from your GreasePencilSO data. 231 gpuMat.stroke_color = new Vector4(mat.stroke_color[0], mat.stroke_color[1], mat.stroke_color[2], 1.0f); 232 gpuMat.fill_color = new Vector4(mat.fill_color[0], mat.fill_color[1], mat.fill_color[2], 1.0f); 233 gpuMat.fill_mix_color = new Vector4(mat.fill_mix_color[0], mat.fill_mix_color[1], mat.fill_mix_color[2], 1.0f); 234 gpuMat.fill_uv_rot_scale = new Vector4(mat.fill_uv_rot_scale[0], mat.fill_uv_rot_scale[1], mat.fill_uv_rot_scale[2], mat.fill_uv_rot_scale[3]); 235 236 // Pack fill_uv_offset and alignment_rot into a single Vector4 237 gpuMat.fill_uv_offset_alignment_rot = new Vector4(mat.fill_uv_offset[0], mat.fill_uv_offset[1], mat.alignment_rot[0], mat.alignment_rot[1]); 238 239 gpuMat.stroke_texture_mix = mat.stroke_texture_mix; 240 gpuMat.stroke_u_scale = mat.stroke_u_scale; 241 gpuMat.fill_texture_mix = mat.fill_texture_mix; 242 gpuMat.flag = mat.flag; 243 244 materialDataList.Add(gpuMat); 245 } 246 247 // Create the ComputeBuffer and upload the data. 248 _materialBuffer = new ComputeBuffer(materialDataList.Count, System.Runtime.InteropServices.Marshal.SizeOf(typeof(GpMaterialData))); 249 _materialBuffer.SetData(materialDataList); 250 } 251 252 private List<int[]> CalculateOffsetsPerLayer(out int totalNumPoints, out int totalVertexOffset) 253 { 254 totalNumPoints = 0; 255 totalVertexOffset = 0; 256 var vertsStartOffsetsPerLayer = new List<int[]>(); 257 258 foreach (var layer in greasePencil.data.layers) 259 { 260 var strokes = layer.frames[frameIdx].strokes; 261 int numStrokes = strokes.Count; 262 int[] vertsStartOffsets = new int[numStrokes]; 263 264 // Calculate the triangle and vertex offsets for all the strokes 265 int numCyclic = 0; 266 int numPoints = 0; 267 for (int strokeIdx = 0; strokeIdx < numStrokes; strokeIdx++) 268 { 269 var stroke = strokes[strokeIdx]; 270 var pointsCount = stroke.points.Count; 271 bool isCyclic = stroke.cyclic && pointsCount >= 3; 272 if (isCyclic) numCyclic++; 273 vertsStartOffsets[strokeIdx] = totalVertexOffset; 274 // One vertex is stored before and after as padding. Cyclic strokes have one extra vertex. 275 totalVertexOffset += 1 + pointsCount + (isCyclic ? 1 : 0) + 1; 276 numPoints += pointsCount; 277 } 278 279 // the strokes 280 totalNumPoints += (numPoints + numCyclic); 281 282 vertsStartOffsetsPerLayer.Add(vertsStartOffsets); 283 } 284 285 return vertsStartOffsetsPerLayer; 286 } 287 }