/ Assets / scripts / GreasePencilRenderer.cs
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  }