diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/GeometryBuilder.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/GeometryBuilder.cs | 1152 |
1 files changed, 1152 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/GeometryBuilder.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/GeometryBuilder.cs new file mode 100644 index 0000000..743b3bb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Drawing/GeometryBuilder.cs @@ -0,0 +1,1152 @@ +using System; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using Unity.Jobs.LowLevel.Unsafe; +using UnityEngine; +using Unity.Burst; +using UnityEngine.Profiling; +using Unity.Collections; +using Unity.Jobs; + +namespace Pathfinding.Drawing { + using static DrawingData; + using static CommandBuilder; + using Pathfinding.Drawing.Text; + using Unity.Profiling; + using System.Collections.Generic; + using UnityEngine.Rendering; + + static class GeometryBuilder { + public struct CameraInfo { + public float3 cameraPosition; + public quaternion cameraRotation; + public float2 cameraDepthToPixelSize; + public bool cameraIsOrthographic; + + public CameraInfo(Camera camera) { + var tr = camera?.transform; + cameraPosition = tr != null ? (float3)tr.position : float3.zero; + cameraRotation = tr != null ? (quaternion)tr.rotation : quaternion.identity; + cameraDepthToPixelSize = (camera != null ? CameraDepthToPixelSize(camera) : 0); + cameraIsOrthographic = camera != null ? camera.orthographic : false; + } + } + + internal static unsafe JobHandle Build (DrawingData gizmos, ProcessedBuilderData.MeshBuffers* buffers, ref CameraInfo cameraInfo, JobHandle dependency) { + // Create a new builder and schedule it. + // Why is characterInfo passed as a pointer and a length instead of just a NativeArray? + // This is because passing it as a NativeArray invokes the safety system which adds some tracking to the NativeArray. + // This is normally not a problem, but we may be scheduling hundreds of jobs that use that particular NativeArray and this causes a bit of a slowdown + // in the safety checking system. Passing it as a pointer + length makes the whole scheduling code about twice as fast compared to passing it as a NativeArray. + return new GeometryBuilderJob { + buffers = buffers, + currentMatrix = Matrix4x4.identity, + currentLineWidthData = new LineWidthData { + pixels = 1, + automaticJoins = false, + }, + lineWidthMultiplier = DrawingManager.lineWidthMultiplier, + currentColor = (Color32)Color.white, + cameraPosition = cameraInfo.cameraPosition, + cameraRotation = cameraInfo.cameraRotation, + cameraDepthToPixelSize = cameraInfo.cameraDepthToPixelSize, + cameraIsOrthographic = cameraInfo.cameraIsOrthographic, + characterInfo = (SDFCharacter*)gizmos.fontData.characters.GetUnsafeReadOnlyPtr(), + characterInfoLength = gizmos.fontData.characters.Length, + maxPixelError = GeometryBuilderJob.MaxCirclePixelError / math.max(0.1f, gizmos.settingsRef.curveResolution), + }.Schedule(dependency); + } + + /// <summary> + /// Helper for determining how large a pixel is at a given depth. + /// A a distance D from the camera a pixel corresponds to roughly value.x * D + value.y world units. + /// Where value is the return value from this function. + /// </summary> + private static float2 CameraDepthToPixelSize (Camera camera) { + if (camera.orthographic) { + return new float2(0.0f, 2.0f * camera.orthographicSize / camera.pixelHeight); + } else { + return new float2(Mathf.Tan(camera.fieldOfView * Mathf.Deg2Rad * 0.5f) / (0.5f * camera.pixelHeight), 0.0f); + } + } + + private static NativeArray<T> ConvertExistingDataToNativeArray<T>(UnsafeAppendBuffer data) where T : struct { + unsafe { + var arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(data.Ptr, data.Length / UnsafeUtility.SizeOf<T>(), Allocator.Invalid); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, AtomicSafetyHandle.GetTempMemoryHandle()); +#endif + return arr; + } + } + + internal static unsafe void BuildMesh (DrawingData gizmos, List<MeshWithType> meshes, ProcessedBuilderData.MeshBuffers* inputBuffers) { + if (inputBuffers->triangles.Length > 0) { + CommandBuilderSamplers.MarkerUpdateBuffer.Begin(); + var mesh = AssignMeshData<GeometryBuilderJob.Vertex>(gizmos, inputBuffers->bounds, inputBuffers->vertices, inputBuffers->triangles, MeshLayouts.MeshLayout); + meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Lines }); + CommandBuilderSamplers.MarkerUpdateBuffer.End(); + } + + if (inputBuffers->solidTriangles.Length > 0) { + var mesh = AssignMeshData<GeometryBuilderJob.Vertex>(gizmos, inputBuffers->bounds, inputBuffers->solidVertices, inputBuffers->solidTriangles, MeshLayouts.MeshLayout); + meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Solid }); + } + + if (inputBuffers->textTriangles.Length > 0) { + var mesh = AssignMeshData<GeometryBuilderJob.TextVertex>(gizmos, inputBuffers->bounds, inputBuffers->textVertices, inputBuffers->textTriangles, MeshLayouts.MeshLayoutText); + meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Text }); + } + } + + private static Mesh AssignMeshData<VertexType>(DrawingData gizmos, Bounds bounds, UnsafeAppendBuffer vertices, UnsafeAppendBuffer triangles, VertexAttributeDescriptor[] layout) where VertexType : struct { + CommandBuilderSamplers.MarkerConvert.Begin(); + var verticesView = ConvertExistingDataToNativeArray<VertexType>(vertices); + var trianglesView = ConvertExistingDataToNativeArray<int>(triangles); + CommandBuilderSamplers.MarkerConvert.End(); + var mesh = gizmos.GetMesh(verticesView.Length); + + CommandBuilderSamplers.MarkerSetLayout.Begin(); + // Resize the vertex buffer if necessary + // Note: also resized if the vertex buffer is significantly larger than necessary. + // This is because apparently when executing the command buffer Unity does something with the whole buffer for some reason (shows up as Mesh.CreateMesh in the profiler) + // TODO: This could potentially cause bad behaviour if multiple meshes are used each frame and they have differing sizes. + // We should query for meshes that already have an appropriately sized buffer. + // if (mesh.vertexCount < verticesView.Length || mesh.vertexCount > verticesView.Length * 2) { + + // } + // TODO: Use Mesh.GetVertexBuffer/Mesh.GetIndexBuffer once they stop being buggy. + // Currently they don't seem to get refreshed properly after resizing them (2022.2.0b1) + mesh.SetVertexBufferParams(math.ceilpow2(verticesView.Length), layout); + mesh.SetIndexBufferParams(math.ceilpow2(trianglesView.Length), IndexFormat.UInt32); + CommandBuilderSamplers.MarkerSetLayout.End(); + + CommandBuilderSamplers.MarkerUpdateVertices.Begin(); + // Update the mesh data + mesh.SetVertexBufferData(verticesView, 0, 0, verticesView.Length); + CommandBuilderSamplers.MarkerUpdateVertices.End(); + CommandBuilderSamplers.MarkerUpdateIndices.Begin(); + // Update the index buffer and assume all our indices are correct + mesh.SetIndexBufferData(trianglesView, 0, 0, trianglesView.Length, MeshUpdateFlags.DontValidateIndices); + CommandBuilderSamplers.MarkerUpdateIndices.End(); + + + CommandBuilderSamplers.MarkerSubmesh.Begin(); + mesh.subMeshCount = 1; + var submesh = new SubMeshDescriptor(0, trianglesView.Length, MeshTopology.Triangles) { + vertexCount = verticesView.Length, + bounds = bounds + }; + mesh.SetSubMesh(0, submesh, MeshUpdateFlags.DontRecalculateBounds | MeshUpdateFlags.DontNotifyMeshUsers); + mesh.bounds = bounds; + CommandBuilderSamplers.MarkerSubmesh.End(); + return mesh; + } + } + + /// <summary>Some static fields that need to be in a separate class because Burst doesn't support them</summary> + static class MeshLayouts { + internal static readonly VertexAttributeDescriptor[] MeshLayout = { + new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), + new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3), + new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4), + new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), + }; + + internal static readonly VertexAttributeDescriptor[] MeshLayoutText = { + new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), + new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4), + new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), + }; + } + + /// <summary> + /// Job to build the geometry from a stream of rendering commands. + /// + /// See: <see cref="CommandBuilder"/> + /// </summary> + // Note: Setting FloatMode to Fast causes visual artificats when drawing circles. + // I think it is because math.sin(float4) produces slightly different results + // for each component in the input. + [BurstCompile(FloatMode = FloatMode.Default)] + internal struct GeometryBuilderJob : IJob { + [NativeDisableUnsafePtrRestriction] + public unsafe ProcessedBuilderData.MeshBuffers* buffers; + + [NativeDisableUnsafePtrRestriction] + public unsafe SDFCharacter* characterInfo; + public int characterInfoLength; + + public Color32 currentColor; + public float4x4 currentMatrix; + public LineWidthData currentLineWidthData; + public float lineWidthMultiplier; + float3 minBounds; + float3 maxBounds; + public float3 cameraPosition; + public quaternion cameraRotation; + public float2 cameraDepthToPixelSize; + public float maxPixelError; + public bool cameraIsOrthographic; + + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct Vertex { + public float3 position; + public float3 uv2; + public Color32 color; + public float2 uv; + } + + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct TextVertex { + public float3 position; + public Color32 color; + public float2 uv; + } + + static unsafe void Add<T>(UnsafeAppendBuffer* buffer, T value) where T : unmanaged { + int size = UnsafeUtility.SizeOf<T>(); + // We know that the buffer has enough capacity, so we can just write to the buffer without + // having to add branches for the overflow case (like buffer->Add will do). +#if ENABLE_UNITY_COLLECTIONS_CHECKS + UnityEngine.Assertions.Assert.IsTrue(buffer->Length + size <= buffer->Capacity); +#endif + *(T*)(buffer->Ptr + buffer->Length) = value; + buffer->Length = buffer->Length + size; + } + + static unsafe void Reserve (UnsafeAppendBuffer* buffer, int size) { + var newSize = buffer->Length + size; + + if (newSize > buffer->Capacity) { + buffer->SetCapacity(math.max(newSize, buffer->Capacity * 2)); + } + } + + internal static float3 PerspectiveDivide (float4 p) { + return p.xyz * math.rcp(p.w); + } + + unsafe void AddText (System.UInt16* text, TextData textData, Color32 color) { + var pivot = PerspectiveDivide(math.mul(currentMatrix, new float4(textData.center, 1.0f))); + + AddTextInternal( + text, + pivot, + math.mul(cameraRotation, new float3(1, 0, 0)), + math.mul(cameraRotation, new float3(0, 1, 0)), + textData.alignment, + textData.sizeInPixels, + true, + textData.numCharacters, + color + ); + } + + unsafe void AddText3D (System.UInt16* text, TextData3D textData, Color32 color) { + var pivot = PerspectiveDivide(math.mul(currentMatrix, new float4(textData.center, 1.0f))); + var m = math.mul(currentMatrix, new float4x4(textData.rotation, float3.zero)); + + AddTextInternal( + text, + pivot, + m.c0.xyz, + m.c1.xyz, + textData.alignment, + textData.size, + false, + textData.numCharacters, + color + ); + } + + + unsafe void AddTextInternal (System.UInt16* text, float3 pivot, float3 right, float3 up, LabelAlignment alignment, float size, bool sizeIsInPixels, int numCharacters, Color32 color) { + var distance = math.abs(math.dot(pivot - cameraPosition, math.mul(cameraRotation, new float3(0, 0, 1)))); + var pixelSize = cameraDepthToPixelSize.x * distance + cameraDepthToPixelSize.y; + float fontWorldSize = size; + + if (sizeIsInPixels) fontWorldSize *= pixelSize; + + right *= fontWorldSize; + up *= fontWorldSize; + + // Calculate the total width (in pixels divided by fontSize) of the text + float maxWidth = 0; + float currentWidth = 0; + float numLines = 1; + + for (int i = 0; i < numCharacters; i++) { + var characterInfoIndex = text[i]; + if (characterInfoIndex == SDFLookupData.Newline) { + maxWidth = math.max(maxWidth, currentWidth); + currentWidth = 0; + numLines++; + } else { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (characterInfoIndex >= characterInfoLength) throw new System.Exception("Invalid character. No info exists. This is a bug."); +#endif + currentWidth += characterInfo[characterInfoIndex].advance; + } + } + maxWidth = math.max(maxWidth, currentWidth); + + // Calculate the world space position of the text given the camera and text alignment + var pos = pivot; + pos -= right * maxWidth * alignment.relativePivot.x; + // Size of a character as a fraction of a whole line using the current font + const float FontCharacterFractionOfLine = 0.75f; + // Where the upper and lower parts of the text will be assuming we start to write at y=0 + var lower = 1 - numLines; + var upper = FontCharacterFractionOfLine; + var yAdjustment = math.lerp(lower, upper, alignment.relativePivot.y); + pos -= up * yAdjustment; + pos += math.mul(cameraRotation, new float3(1, 0, 0)) * (pixelSize * alignment.pixelOffset.x); + pos += math.mul(cameraRotation, new float3(0, 1, 0)) * (pixelSize * alignment.pixelOffset.y); + + var textVertices = &buffers->textVertices; + var textTriangles = &buffers->textTriangles; + + // Reserve all buffer space beforehand + Reserve(textVertices, numCharacters * VerticesPerCharacter * UnsafeUtility.SizeOf<TextVertex>()); + Reserve(textTriangles, numCharacters * TrianglesPerCharacter * UnsafeUtility.SizeOf<int>()); + + var lineStart = pos; + + for (int i = 0; i < numCharacters; i++) { + var characterInfoIndex = text[i]; + + if (characterInfoIndex == SDFLookupData.Newline) { + lineStart -= up; + pos = lineStart; + continue; + } + + // Get character rendering information from the font + SDFCharacter ch = characterInfo[characterInfoIndex]; + + int vertexIndexStart = textVertices->Length / UnsafeUtility.SizeOf<TextVertex>(); + + float3 v; + + v = pos + ch.vertexTopLeft.x * right + ch.vertexTopLeft.y * up; + minBounds = math.min(minBounds, v); + maxBounds = math.max(maxBounds, v); + Add(textVertices, new TextVertex { + position = v, + uv = ch.uvTopLeft, + color = color, + }); + + v = pos + ch.vertexTopRight.x * right + ch.vertexTopRight.y * up; + minBounds = math.min(minBounds, v); + maxBounds = math.max(maxBounds, v); + Add(textVertices, new TextVertex { + position = v, + uv = ch.uvTopRight, + color = color, + }); + + v = pos + ch.vertexBottomRight.x * right + ch.vertexBottomRight.y * up; + minBounds = math.min(minBounds, v); + maxBounds = math.max(maxBounds, v); + Add(textVertices, new TextVertex { + position = v, + uv = ch.uvBottomRight, + color = color, + }); + + v = pos + ch.vertexBottomLeft.x * right + ch.vertexBottomLeft.y * up; + minBounds = math.min(minBounds, v); + maxBounds = math.max(maxBounds, v); + Add(textVertices, new TextVertex { + position = v, + uv = ch.uvBottomLeft, + color = color, + }); + + Add(textTriangles, vertexIndexStart + 0); + Add(textTriangles, vertexIndexStart + 1); + Add(textTriangles, vertexIndexStart + 2); + + Add(textTriangles, vertexIndexStart + 0); + Add(textTriangles, vertexIndexStart + 2); + Add(textTriangles, vertexIndexStart + 3); + + // Advance character position + pos += right * ch.advance; + } + } + + float3 lastNormalizedLineDir; + float lastLineWidth; + + public const float MaxCirclePixelError = 0.5f; + + public const int VerticesPerCharacter = 4; + public const int TrianglesPerCharacter = 6; + + void AddLine (LineData line) { + // Store the line direction in the vertex. + // A line consists of 4 vertices. The line direction will be used to + // offset the vertices to create a line with a fixed pixel thickness + var a = PerspectiveDivide(math.mul(currentMatrix, new float4(line.a, 1.0f))); + var b = PerspectiveDivide(math.mul(currentMatrix, new float4(line.b, 1.0f))); + + float lineWidth = currentLineWidthData.pixels; + var normalizedLineDir = math.normalizesafe(b - a); + + if (math.any(math.isnan(normalizedLineDir))) throw new Exception("Nan line coordinates"); + if (lineWidth <= 0) { + return; + } + + // Update the bounding box + minBounds = math.min(minBounds, math.min(a, b)); + maxBounds = math.max(maxBounds, math.max(a, b)); + + unsafe { + var outlineVertices = &buffers->vertices; + + // Make sure there is enough allocated capacity for 4 more vertices + Reserve(outlineVertices, 4 * UnsafeUtility.SizeOf<Vertex>()); + + // Insert 4 vertices + // Doing it with pointers is faster, and this is the hottest + // code of the whole gizmo drawing process. + var ptr = (Vertex*)((byte*)outlineVertices->Ptr + outlineVertices->Length); + + var startLineDir = normalizedLineDir * lineWidth; + var endLineDir = normalizedLineDir * lineWidth; + + // If dot(last dir, this dir) >= 0 => use join + if (lineWidth > 1 && currentLineWidthData.automaticJoins && outlineVertices->Length > 2*UnsafeUtility.SizeOf<Vertex>()) { + // has previous vertex + Vertex* lastVertex1 = (Vertex*)(ptr - 1); + Vertex* lastVertex2 = (Vertex*)(ptr - 2); + + var cosAngle = math.dot(normalizedLineDir, lastNormalizedLineDir); + if (math.all(lastVertex2->position == a) && lastLineWidth == lineWidth && cosAngle >= -0.6f) { + // Safety: tangent cannot be 0 because cosAngle > -1 + var tangent = normalizedLineDir + lastNormalizedLineDir; + // From the law of cosines we get that + // tangent.magnitude = sqrt(2)*sqrt(1+cosAngle) + + // Create join! + // Trigonometry gives us + // joinRadius = lineWidth / (2*cos(alpha / 2)) + // Using half angle identity for cos we get + // joinRadius = lineWidth / (sqrt(2)*sqrt(1 + cos(alpha)) + // Since the tangent already has mostly the same factors we can simplify the calculation + // normalize(tangent) * joinRadius * 2 + // = tangent / (sqrt(2)*sqrt(1+cosAngle)) * joinRadius * 2 + // = tangent * lineWidth / (1 + cos(alpha) + var joinLineDir = tangent * lineWidth / (1 + cosAngle); + + startLineDir = joinLineDir; + lastVertex1->uv2 = startLineDir; + lastVertex2->uv2 = startLineDir; + } + } + + outlineVertices->Length = outlineVertices->Length + 4 * UnsafeUtility.SizeOf<Vertex>(); + *ptr++ = new Vertex { + position = a, + color = currentColor, + uv = new float2(0, 0), + uv2 = startLineDir, + }; + *ptr++ = new Vertex { + position = a, + color = currentColor, + uv = new float2(1, 0), + uv2 = startLineDir, + }; + + *ptr++ = new Vertex { + position = b, + color = currentColor, + uv = new float2(0, 1), + uv2 = endLineDir, + }; + *ptr++ = new Vertex { + position = b, + color = currentColor, + uv = new float2(1, 1), + uv2 = endLineDir, + }; + + lastNormalizedLineDir = normalizedLineDir; + lastLineWidth = lineWidth; + } + } + + /// <summary>Calculate number of steps to use for drawing a circle at the specified point and radius to get less than the specified pixel error.</summary> + internal static int CircleSteps (float3 center, float radius, float maxPixelError, ref float4x4 currentMatrix, float2 cameraDepthToPixelSize, float3 cameraPosition) { + var centerv4 = math.mul(currentMatrix, new float4(center, 1.0f)); + + if (math.abs(centerv4.w) < 0.0000001f) return 3; + var cc = PerspectiveDivide(centerv4); + // Take the maximum scale factor among the 3 axes. + // If the current matrix has a uniform scale then they are all the same. + var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq(currentMatrix.c0.xyz), math.lengthsq(currentMatrix.c1.xyz)), math.lengthsq(currentMatrix.c2.xyz))) / centerv4.w; + var realWorldRadius = radius * maxScaleFactor; + var distance = math.length(cc - cameraPosition); + + var pixelSize = cameraDepthToPixelSize.x * distance + cameraDepthToPixelSize.y; + // realWorldRadius += pixelSize * this.currentLineWidthData.pixels * 0.5f; + var cosAngle = 1 - (maxPixelError * pixelSize) / realWorldRadius; + int steps = cosAngle < 0 ? 3 : (int)math.ceil(math.PI / (math.acos(cosAngle))); + return steps; + } + + void AddCircle (CircleData circle) { + // If the circle has a zero normal then just ignore it + if (math.all(circle.normal == 0)) return; + + circle.normal = math.normalize(circle.normal); + // Canonicalize + if (circle.normal.y < 0) circle.normal = -circle.normal; + + float3 tangent1; + if (math.all(math.abs(circle.normal - new float3(0, 1, 0)) < 0.001f)) { + // The normal was (almost) identical to (0, 1, 0) + tangent1 = new float3(0, 0, 1); + } else { + // Common case + tangent1 = math.normalizesafe(math.cross(circle.normal, new float3(0, 1, 0))); + } + + var ex = tangent1; + var ey = circle.normal; + var ez = math.cross(ey, ex); + var oldMatrix = currentMatrix; + + currentMatrix = math.mul(currentMatrix, new float4x4( + new float4(ex, 0) * circle.radius, + new float4(ey, 0) * circle.radius, + new float4(ez, 0) * circle.radius, + new float4(circle.center, 1) + )); + + AddCircle(new CircleXZData { + center = new float3(0, 0, 0), + radius = 1, + startAngle = 0, + endAngle = 2 * math.PI, + }); + + currentMatrix = oldMatrix; + } + + void AddDisc (CircleData circle) { + // If the circle has a zero normal then just ignore it + if (math.all(circle.normal == 0)) return; + + var steps = CircleSteps(circle.center, circle.radius, maxPixelError, ref currentMatrix, cameraDepthToPixelSize, cameraPosition); + + circle.normal = math.normalize(circle.normal); + float3 tangent1; + if (math.all(math.abs(circle.normal - new float3(0, 1, 0)) < 0.001f)) { + // The normal was (almost) identical to (0, 1, 0) + tangent1 = new float3(0, 0, 1); + } else { + // Common case + tangent1 = math.cross(circle.normal, new float3(0, 1, 0)); + } + + float invSteps = 1.0f / steps; + + unsafe { + var solidVertices = &buffers->solidVertices; + var solidTriangles = &buffers->solidTriangles; + Reserve(solidVertices, steps * UnsafeUtility.SizeOf<Vertex>()); + Reserve(solidTriangles, 3*(steps-2) * UnsafeUtility.SizeOf<int>()); + + var matrix = math.mul(currentMatrix, Matrix4x4.TRS(circle.center, Quaternion.LookRotation(circle.normal, tangent1), new Vector3(circle.radius, circle.radius, circle.radius))); + + var mn = minBounds; + var mx = maxBounds; + int vertexCount = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>(); + + for (int i = 0; i < steps; i++) { + var t = math.lerp(0, 2*Mathf.PI, i * invSteps); + math.sincos(t, out float sin, out float cos); + + var p = PerspectiveDivide(math.mul(matrix, new float4(cos, sin, 0, 1))); + // Update the bounding box + mn = math.min(mn, p); + mx = math.max(mx, p); + + Add(solidVertices, new Vertex { + position = p, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + } + + minBounds = mn; + maxBounds = mx; + + for (int i = 0; i < steps - 2; i++) { + Add(solidTriangles, vertexCount); + Add(solidTriangles, vertexCount + i + 1); + Add(solidTriangles, vertexCount + i + 2); + } + } + } + + void AddSphereOutline (SphereData circle) { + var centerv4 = math.mul(currentMatrix, new float4(circle.center, 1.0f)); + + if (math.abs(centerv4.w) < 0.0000001f) return; + var center = PerspectiveDivide(centerv4); + // Figure out the actual radius of the sphere after all the matrix multiplications. + // In case of a non-uniform scale, pick the largest radius + var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq(currentMatrix.c0.xyz), math.lengthsq(currentMatrix.c1.xyz)), math.lengthsq(currentMatrix.c2.xyz))) / centerv4.w; + var realWorldRadius = circle.radius * maxScaleFactor; + + if (cameraIsOrthographic) { + var prevMatrix = this.currentMatrix; + this.currentMatrix = float4x4.identity; + AddCircle(new CircleData { + center = center, + normal = math.mul(this.cameraRotation, new float3(0, 0, 1)), + radius = realWorldRadius, + }); + this.currentMatrix = prevMatrix; + } else { + var dist = math.length(this.cameraPosition - center); + // Camera is inside the sphere, cannot draw + if (dist <= realWorldRadius) return; + + var offsetTowardsCamera = realWorldRadius * realWorldRadius / dist; + var outlineRadius = math.sqrt(realWorldRadius * realWorldRadius - offsetTowardsCamera * offsetTowardsCamera); + var normal = math.normalize(this.cameraPosition - center); + var prevMatrix = this.currentMatrix; + this.currentMatrix = float4x4.identity; + AddCircle(new CircleData { + center = center + normal * offsetTowardsCamera, + normal = normal, + radius = outlineRadius, + }); + this.currentMatrix = prevMatrix; + } + } + + void AddCircle (CircleXZData circle) { + circle.endAngle = math.clamp(circle.endAngle, circle.startAngle - Mathf.PI * 2, circle.startAngle + Mathf.PI * 2); + + unsafe { + var m = math.mul(currentMatrix, new float4x4( + new float4(circle.radius, 0, 0, 0), + new float4(0, circle.radius, 0, 0), + new float4(0, 0, circle.radius, 0), + new float4(circle.center, 1) + )); + var steps = CircleSteps(float3.zero, 1.0f, maxPixelError, ref m, cameraDepthToPixelSize, cameraPosition); + var lineWidth = currentLineWidthData.pixels; + if (lineWidth < 0) return; + + var byteSize = steps * 4 * UnsafeUtility.SizeOf<Vertex>(); + Reserve(&buffers->vertices, byteSize); + var ptr = (Vertex*)(buffers->vertices.Ptr + buffers->vertices.Length); + buffers->vertices.Length += byteSize; + math.sincos(circle.startAngle, out float sin0, out float cos0); + var prev = PerspectiveDivide(math.mul(m, new float4(cos0, 0, sin0, 1))); + var prevTangent = math.normalizesafe(math.mul(m, new float4(-sin0, 0, cos0, 0)).xyz) * lineWidth; + var invSteps = math.rcp(steps); + + for (int i = 1; i <= steps; i++) { + var t = math.lerp(circle.startAngle, circle.endAngle, i * invSteps); + math.sincos(t, out float sin, out float cos); + var next = PerspectiveDivide(math.mul(m, new float4(cos, 0, sin, 1))); + var tangent = math.normalizesafe(math.mul(m, new float4(-sin, 0, cos, 0)).xyz) * lineWidth; + *ptr++ = new Vertex { + position = prev, + color = currentColor, + uv = new float2(0, 0), + uv2 = prevTangent, + }; + *ptr++ = new Vertex { + position = prev, + color = currentColor, + uv = new float2(1, 0), + uv2 = prevTangent, + }; + *ptr++ = new Vertex { + position = next, + color = currentColor, + uv = new float2(0, 1), + uv2 = tangent, + }; + *ptr++ = new Vertex { + position = next, + color = currentColor, + uv = new float2(1, 1), + uv2 = tangent, + }; + + prev = next; + prevTangent = tangent; + } + + // Update the global bounds with the bounding box of the circle + var b0 = PerspectiveDivide(math.mul(m, new float4(-1, 0, 0, 1))); + var b1 = PerspectiveDivide(math.mul(m, new float4(0, -1, 0, 1))); + var b2 = PerspectiveDivide(math.mul(m, new float4(+1, 0, 0, 1))); + var b3 = PerspectiveDivide(math.mul(m, new float4(0, +1, 0, 1))); + minBounds = math.min(math.min(math.min(math.min(b0, b1), b2), b3), minBounds); + maxBounds = math.max(math.max(math.max(math.max(b0, b1), b2), b3), maxBounds); + } + } + + void AddDisc (CircleXZData circle) { + var steps = CircleSteps(circle.center, circle.radius, maxPixelError, ref currentMatrix, cameraDepthToPixelSize, cameraPosition); + + circle.endAngle = math.clamp(circle.endAngle, circle.startAngle - Mathf.PI * 2, circle.startAngle + Mathf.PI * 2); + + float invSteps = 1.0f / steps; + + unsafe { + var solidVertices = &buffers->solidVertices; + var solidTriangles = &buffers->solidTriangles; + Reserve(solidVertices, (2+steps) * UnsafeUtility.SizeOf<Vertex>()); + Reserve(solidTriangles, 3*steps * UnsafeUtility.SizeOf<int>()); + + var matrix = math.mul(currentMatrix, Matrix4x4.Translate(circle.center) * Matrix4x4.Scale(new Vector3(circle.radius, circle.radius, circle.radius))); + + var worldCenter = PerspectiveDivide(math.mul(matrix, new float4(0, 0, 0, 1))); + Add(solidVertices, new Vertex { + position = worldCenter, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + + var mn = math.min(minBounds, worldCenter); + var mx = math.max(maxBounds, worldCenter); + int vertexCount = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>(); + + for (int i = 0; i <= steps; i++) { + var t = math.lerp(circle.startAngle, circle.endAngle, i * invSteps); + math.sincos(t, out float sin, out float cos); + + var p = PerspectiveDivide(math.mul(matrix, new float4(cos, 0, sin, 1))); + // Update the bounding box + mn = math.min(mn, p); + mx = math.max(mx, p); + + Add(solidVertices, new Vertex { + position = p, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + } + + minBounds = mn; + maxBounds = mx; + + for (int i = 0; i < steps; i++) { + // Center vertex + Add(solidTriangles, vertexCount - 1); + Add(solidTriangles, vertexCount + i + 0); + Add(solidTriangles, vertexCount + i + 1); + } + } + } + + void AddSolidTriangle (TriangleData triangle) { + unsafe { + var solidVertices = &buffers->solidVertices; + var solidTriangles = &buffers->solidTriangles; + Reserve(solidVertices, 3 * UnsafeUtility.SizeOf<Vertex>()); + Reserve(solidTriangles, 3 * UnsafeUtility.SizeOf<int>()); + var matrix = currentMatrix; + var a = PerspectiveDivide(math.mul(matrix, new float4(triangle.a, 1))); + var b = PerspectiveDivide(math.mul(matrix, new float4(triangle.b, 1))); + var c = PerspectiveDivide(math.mul(matrix, new float4(triangle.c, 1))); + int startVertex = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>(); + + minBounds = math.min(math.min(math.min(minBounds, a), b), c); + maxBounds = math.max(math.max(math.max(maxBounds, a), b), c); + + Add(solidVertices, new Vertex { + position = a, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + Add(solidVertices, new Vertex { + position = b, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + Add(solidVertices, new Vertex { + position = c, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }); + + Add(solidTriangles, startVertex + 0); + Add(solidTriangles, startVertex + 1); + Add(solidTriangles, startVertex + 2); + } + } + + void AddWireBox (BoxData box) { + var min = box.center - box.size * 0.5f; + var max = box.center + box.size * 0.5f; + AddLine(new LineData { a = new float3(min.x, min.y, min.z), b = new float3(max.x, min.y, min.z) }); + AddLine(new LineData { a = new float3(max.x, min.y, min.z), b = new float3(max.x, min.y, max.z) }); + AddLine(new LineData { a = new float3(max.x, min.y, max.z), b = new float3(min.x, min.y, max.z) }); + AddLine(new LineData { a = new float3(min.x, min.y, max.z), b = new float3(min.x, min.y, min.z) }); + + AddLine(new LineData { a = new float3(min.x, max.y, min.z), b = new float3(max.x, max.y, min.z) }); + AddLine(new LineData { a = new float3(max.x, max.y, min.z), b = new float3(max.x, max.y, max.z) }); + AddLine(new LineData { a = new float3(max.x, max.y, max.z), b = new float3(min.x, max.y, max.z) }); + AddLine(new LineData { a = new float3(min.x, max.y, max.z), b = new float3(min.x, max.y, min.z) }); + + AddLine(new LineData { a = new float3(min.x, min.y, min.z), b = new float3(min.x, max.y, min.z) }); + AddLine(new LineData { a = new float3(max.x, min.y, min.z), b = new float3(max.x, max.y, min.z) }); + AddLine(new LineData { a = new float3(max.x, min.y, max.z), b = new float3(max.x, max.y, max.z) }); + AddLine(new LineData { a = new float3(min.x, min.y, max.z), b = new float3(min.x, max.y, max.z) }); + } + + void AddPlane (PlaneData plane) { + var oldMatrix = currentMatrix; + + currentMatrix = math.mul(currentMatrix, float4x4.TRS(plane.center, plane.rotation, new float3(plane.size.x * 0.5f, 1, plane.size.y * 0.5f))); + + AddLine(new LineData { a = new float3(-1, 0, -1), b = new float3(1, 0, -1) }); + AddLine(new LineData { a = new float3(1, 0, -1), b = new float3(1, 0, 1) }); + AddLine(new LineData { a = new float3(1, 0, 1), b = new float3(-1, 0, 1) }); + AddLine(new LineData { a = new float3(-1, 0, 1), b = new float3(-1, 0, -1) }); + + currentMatrix = oldMatrix; + } + + internal static readonly float4[] BoxVertices = { + new float4(-1, -1, -1, 1), + new float4(-1, -1, +1, 1), + new float4(-1, +1, -1, 1), + new float4(-1, +1, +1, 1), + new float4(+1, -1, -1, 1), + new float4(+1, -1, +1, 1), + new float4(+1, +1, -1, 1), + new float4(+1, +1, +1, 1), + }; + + internal static readonly int[] BoxTriangles = { + // Bottom two triangles + 0, 1, 5, + 0, 5, 4, + + // Top + 7, 3, 2, + 7, 2, 6, + + // -X + 0, 1, 3, + 0, 3, 2, + + // +X + 4, 5, 7, + 4, 7, 6, + + // +Z + 1, 3, 7, + 1, 7, 5, + + // -Z + 0, 2, 6, + 0, 6, 4, + }; + + void AddBox (BoxData box) { + unsafe { + var solidVertices = &buffers->solidVertices; + var solidTriangles = &buffers->solidTriangles; + Reserve(solidVertices, BoxVertices.Length * UnsafeUtility.SizeOf<Vertex>()); + Reserve(solidTriangles, BoxTriangles.Length * UnsafeUtility.SizeOf<int>()); + + var scale = box.size * 0.5f; + var matrix = math.mul(currentMatrix, new float4x4( + new float4(scale.x, 0, 0, 0), + new float4(0, scale.y, 0, 0), + new float4(0, 0, scale.z, 0), + new float4(box.center, 1) + )); + + var mn = minBounds; + var mx = maxBounds; + int vertexOffset = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>(); + var ptr = (Vertex*)(solidVertices->Ptr + solidVertices->Length); + for (int i = 0; i < BoxVertices.Length; i++) { + var p = PerspectiveDivide(math.mul(matrix, BoxVertices[i])); + // Update the bounding box + mn = math.min(mn, p); + mx = math.max(mx, p); + + *ptr++ = new Vertex { + position = p, + color = currentColor, + uv = new float2(0, 0), + uv2 = new float3(0, 0, 0), + }; + } + solidVertices->Length += BoxVertices.Length * UnsafeUtility.SizeOf<Vertex>(); + + minBounds = mn; + maxBounds = mx; + + var triPtr = (int*)(solidTriangles->Ptr + solidTriangles->Length); + for (int i = 0; i < BoxTriangles.Length; i++) { + *triPtr++ = vertexOffset + BoxTriangles[i]; + } + solidTriangles->Length += BoxTriangles.Length * UnsafeUtility.SizeOf<int>(); + } + } + + // AggressiveInlining because this is only called from a single location, and burst doesn't inline otherwise + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Next (ref UnsafeAppendBuffer.Reader reader, ref NativeArray<float4x4> matrixStack, ref NativeArray<Color32> colorStack, ref NativeArray<LineWidthData> lineWidthStack, ref int matrixStackSize, ref int colorStackSize, ref int lineWidthStackSize) { + var fullCmd = reader.ReadNext<Command>(); + var cmd = fullCmd & (Command)0xFF; + Color32 oldColor = default; + + if ((fullCmd & Command.PushColorInline) != 0) { + oldColor = currentColor; + currentColor = reader.ReadNext<Color32>(); + } + + switch (cmd) { + case Command.PushColor: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (colorStackSize >= colorStack.Length) throw new System.Exception("Too deeply nested PushColor calls"); +#else + if (colorStackSize >= colorStack.Length) colorStackSize--; +#endif + colorStack[colorStackSize] = currentColor; + colorStackSize++; + currentColor = reader.ReadNext<Color32>(); + break; + case Command.PopColor: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (colorStackSize <= 0) throw new System.Exception("PushColor and PopColor are not matched"); +#else + if (colorStackSize <= 0) break; +#endif + colorStackSize--; + currentColor = colorStack[colorStackSize]; + break; + case Command.PushMatrix: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (matrixStackSize >= matrixStack.Length) throw new System.Exception("Too deeply nested PushMatrix calls"); +#else + if (matrixStackSize >= matrixStack.Length) matrixStackSize--; +#endif + matrixStack[matrixStackSize] = currentMatrix; + matrixStackSize++; + currentMatrix = math.mul(currentMatrix, reader.ReadNext<float4x4>()); + break; + case Command.PushSetMatrix: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (matrixStackSize >= matrixStack.Length) throw new System.Exception("Too deeply nested PushMatrix calls"); +#else + if (matrixStackSize >= matrixStack.Length) matrixStackSize--; +#endif + matrixStack[matrixStackSize] = currentMatrix; + matrixStackSize++; + currentMatrix = reader.ReadNext<float4x4>(); + break; + case Command.PopMatrix: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (matrixStackSize <= 0) throw new System.Exception("PushMatrix and PopMatrix are not matched"); +#else + if (matrixStackSize <= 0) break; +#endif + matrixStackSize--; + currentMatrix = matrixStack[matrixStackSize]; + break; + case Command.PushLineWidth: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (lineWidthStackSize >= lineWidthStack.Length) throw new System.Exception("Too deeply nested PushLineWidth calls"); +#else + if (lineWidthStackSize >= lineWidthStack.Length) lineWidthStackSize--; +#endif + lineWidthStack[lineWidthStackSize] = currentLineWidthData; + lineWidthStackSize++; + currentLineWidthData = reader.ReadNext<LineWidthData>(); + currentLineWidthData.pixels *= lineWidthMultiplier; + break; + case Command.PopLineWidth: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (lineWidthStackSize <= 0) throw new System.Exception("PushLineWidth and PopLineWidth are not matched"); +#else + if (lineWidthStackSize <= 0) break; +#endif + lineWidthStackSize--; + currentLineWidthData = lineWidthStack[lineWidthStackSize]; + break; + case Command.Line: + AddLine(reader.ReadNext<LineData>()); + break; + case Command.SphereOutline: + AddSphereOutline(reader.ReadNext<SphereData>()); + break; + case Command.CircleXZ: + AddCircle(reader.ReadNext<CircleXZData>()); + break; + case Command.Circle: + AddCircle(reader.ReadNext<CircleData>()); + break; + case Command.DiscXZ: + AddDisc(reader.ReadNext<CircleXZData>()); + break; + case Command.Disc: + AddDisc(reader.ReadNext<CircleData>()); + break; + case Command.Box: + AddBox(reader.ReadNext<BoxData>()); + break; + case Command.WirePlane: + AddPlane(reader.ReadNext<PlaneData>()); + break; + case Command.WireBox: + AddWireBox(reader.ReadNext<BoxData>()); + break; + case Command.SolidTriangle: + AddSolidTriangle(reader.ReadNext<TriangleData>()); + break; + case Command.PushPersist: + // This command does not need to be handled by the builder + reader.ReadNext<PersistData>(); + break; + case Command.PopPersist: + // This command does not need to be handled by the builder + break; + case Command.Text: + var data = reader.ReadNext<TextData>(); + unsafe { + System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf<System.UInt16>() * data.numCharacters); + AddText(ptr, data, currentColor); + } + break; + case Command.Text3D: + var data2 = reader.ReadNext<TextData3D>(); + unsafe { + System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf<System.UInt16>() * data2.numCharacters); + AddText3D(ptr, data2, currentColor); + } + break; + case Command.CaptureState: + unsafe { + buffers->capturedState.Add(new ProcessedBuilderData.CapturedState { + color = this.currentColor, + matrix = this.currentMatrix, + }); + } + break; + default: +#if ENABLE_UNITY_COLLECTIONS_CHECKS + throw new System.Exception("Unknown command"); +#else + break; +#endif + } + + if ((fullCmd & Command.PushColorInline) != 0) { + currentColor = oldColor; + } + } + + void CreateTriangles () { + // Create triangles for all lines + // A triangle consists of 3 indices + // A line (4 vertices) consists of 2 triangles, so 6 triangle indices + unsafe { + var outlineVertices = &buffers->vertices; + var outlineTriangles = &buffers->triangles; + var vertexCount = outlineVertices->Length / UnsafeUtility.SizeOf<Vertex>(); + // Each line is made out of 4 vertices + var lineCount = vertexCount / 4; + var trianglesSizeInBytes = lineCount * 6 * UnsafeUtility.SizeOf<int>(); + if (trianglesSizeInBytes >= outlineTriangles->Capacity) { + outlineTriangles->SetCapacity(math.ceilpow2(trianglesSizeInBytes)); + } + + int* ptr = (int*)outlineTriangles->Ptr; + for (int i = 0, vi = 0; i < lineCount; i++, vi += 4) { + // First triangle + *ptr++ = vi + 0; + *ptr++ = vi + 1; + *ptr++ = vi + 2; + + // Second triangle + *ptr++ = vi + 1; + *ptr++ = vi + 3; + *ptr++ = vi + 2; + } + outlineTriangles->Length = trianglesSizeInBytes; + } + } + + public const int MaxStackSize = 32; + + public void Execute () { + unsafe { + buffers->vertices.Reset(); + buffers->triangles.Reset(); + buffers->solidVertices.Reset(); + buffers->solidTriangles.Reset(); + buffers->textVertices.Reset(); + buffers->textTriangles.Reset(); + buffers->capturedState.Reset(); + } + + currentLineWidthData.pixels *= lineWidthMultiplier; + + minBounds = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + maxBounds = new float3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + + var matrixStack = new NativeArray<float4x4>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + var colorStack = new NativeArray<Color32>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + var lineWidthStack = new NativeArray<LineWidthData>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + int matrixStackSize = 0; + int colorStackSize = 0; + int lineWidthStackSize = 0; + + CommandBuilderSamplers.MarkerProcessCommands.Begin(); + unsafe { + var reader = buffers->splitterOutput.AsReader(); + while (reader.Offset < reader.Size) Next(ref reader, ref matrixStack, ref colorStack, ref lineWidthStack, ref matrixStackSize, ref colorStackSize, ref lineWidthStackSize); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (reader.Offset != reader.Size) throw new Exception("Didn't reach the end of the buffer"); +#endif + } + CommandBuilderSamplers.MarkerProcessCommands.End(); + + CommandBuilderSamplers.MarkerCreateTriangles.Begin(); + CreateTriangles(); + CommandBuilderSamplers.MarkerCreateTriangles.End(); + + unsafe { + var outBounds = &buffers->bounds; + *outBounds = new Bounds((minBounds + maxBounds) * 0.5f, maxBounds - minBounds); + + if (math.any(math.isnan(outBounds->min)) && (buffers->vertices.Length > 0 || buffers->solidTriangles.Length > 0)) { + // Fall back to a bounding box that covers everything + *outBounds = new Bounds(Vector3.zero, new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity)); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + throw new Exception("NaN bounds. A Draw.* command may have been given NaN coordinates."); +#endif + } + } + } + } +} |