1153 lines
42 KiB
C#

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
}
}
}
}
}