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);
}
///
/// 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.
///
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 ConvertExistingDataToNativeArray(UnsafeAppendBuffer data) where T : struct {
unsafe {
var arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(data.Ptr, data.Length / UnsafeUtility.SizeOf(), Allocator.Invalid);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, AtomicSafetyHandle.GetTempMemoryHandle());
#endif
return arr;
}
}
internal static unsafe void BuildMesh (DrawingData gizmos, List meshes, ProcessedBuilderData.MeshBuffers* inputBuffers) {
if (inputBuffers->triangles.Length > 0) {
CommandBuilderSamplers.MarkerUpdateBuffer.Begin();
var mesh = AssignMeshData(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(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(gizmos, inputBuffers->bounds, inputBuffers->textVertices, inputBuffers->textTriangles, MeshLayouts.MeshLayoutText);
meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Text });
}
}
private static Mesh AssignMeshData(DrawingData gizmos, Bounds bounds, UnsafeAppendBuffer vertices, UnsafeAppendBuffer triangles, VertexAttributeDescriptor[] layout) where VertexType : struct {
CommandBuilderSamplers.MarkerConvert.Begin();
var verticesView = ConvertExistingDataToNativeArray(vertices);
var trianglesView = ConvertExistingDataToNativeArray(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;
}
}
/// Some static fields that need to be in a separate class because Burst doesn't support them
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),
};
}
///
/// Job to build the geometry from a stream of rendering commands.
///
/// See:
///
// 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(UnsafeAppendBuffer* buffer, T value) where T : unmanaged {
int size = UnsafeUtility.SizeOf();
// 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());
Reserve(textTriangles, numCharacters * TrianglesPerCharacter * UnsafeUtility.SizeOf());
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();
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());
// 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()) {
// 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();
*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;
}
}
/// Calculate number of steps to use for drawing a circle at the specified point and radius to get less than the specified pixel error.
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());
Reserve(solidTriangles, 3*(steps-2) * UnsafeUtility.SizeOf());
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();
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();
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());
Reserve(solidTriangles, 3*steps * UnsafeUtility.SizeOf());
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();
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());
Reserve(solidTriangles, 3 * UnsafeUtility.SizeOf());
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();
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());
Reserve(solidTriangles, BoxTriangles.Length * UnsafeUtility.SizeOf());
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();
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();
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();
}
}
// 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 matrixStack, ref NativeArray colorStack, ref NativeArray lineWidthStack, ref int matrixStackSize, ref int colorStackSize, ref int lineWidthStackSize) {
var fullCmd = reader.ReadNext();
var cmd = fullCmd & (Command)0xFF;
Color32 oldColor = default;
if ((fullCmd & Command.PushColorInline) != 0) {
oldColor = currentColor;
currentColor = reader.ReadNext();
}
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();
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());
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();
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();
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());
break;
case Command.SphereOutline:
AddSphereOutline(reader.ReadNext());
break;
case Command.CircleXZ:
AddCircle(reader.ReadNext());
break;
case Command.Circle:
AddCircle(reader.ReadNext());
break;
case Command.DiscXZ:
AddDisc(reader.ReadNext());
break;
case Command.Disc:
AddDisc(reader.ReadNext());
break;
case Command.Box:
AddBox(reader.ReadNext());
break;
case Command.WirePlane:
AddPlane(reader.ReadNext());
break;
case Command.WireBox:
AddWireBox(reader.ReadNext());
break;
case Command.SolidTriangle:
AddSolidTriangle(reader.ReadNext());
break;
case Command.PushPersist:
// This command does not need to be handled by the builder
reader.ReadNext();
break;
case Command.PopPersist:
// This command does not need to be handled by the builder
break;
case Command.Text:
var data = reader.ReadNext();
unsafe {
System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf() * data.numCharacters);
AddText(ptr, data, currentColor);
}
break;
case Command.Text3D:
var data2 = reader.ReadNext();
unsafe {
System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf() * 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();
// Each line is made out of 4 vertices
var lineCount = vertexCount / 4;
var trianglesSizeInBytes = lineCount * 6 * UnsafeUtility.SizeOf();
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(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var colorStack = new NativeArray(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var lineWidthStack = new NativeArray(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
}
}
}
}
}