Files
2026-11-01 23:39:55 +02:00

571 lines
24 KiB
C#

using System;
using Unity.Collections;
using static UnityEngine.Rendering.Universal.ShadowUtility;
namespace UnityEngine.Rendering.Universal
{
[Serializable]
internal class ShadowMesh2D : ShadowShape2D
{
internal const int k_CapsuleCapSegments = 8;
internal const float k_TrimEdgeUninitialized = -1;
public enum EdgeProcessing
{
None,
Clipping,
}
[NonSerialized] Mesh m_Mesh;
[NonSerialized] bool m_IsDirty;
[SerializeField] ShadowMeshVertex[] m_Vertices;
[SerializeField] int[] m_Indices;
NativeArray<ShadowMeshVertex> m_NativeVertices;
NativeArray<int> m_NativeIndices;
[SerializeField] bool m_IsTransformable = true;
[SerializeField] Bounds m_LocalBounds;
[SerializeField] EdgeProcessing m_EdgeProcessing = EdgeProcessing.Clipping;
[SerializeField] float m_TrimEdge = k_TrimEdgeUninitialized;
[SerializeField] bool m_FlipX;
[SerializeField] bool m_FlipY;
[SerializeField] float m_InitialTrim = 0;
public Mesh mesh
{
get
{
if(m_Mesh == null || m_Mesh.vertexCount == 0 || m_IsDirty)
GenerateShadowMesh(ref m_Mesh, m_NativeVertices, m_NativeIndices);
return m_Mesh;
}
}
public void Clear()
{
m_Vertices = null;
m_Indices = null;
}
public bool isTransformable => m_IsTransformable;
public BoundingSphere boundingSphere { get => m_BoundingSphere; }
internal BoundingSphere m_BoundingSphere; // update to world space
public EdgeProcessing edgeProcessing { get { return m_EdgeProcessing; } set { m_EdgeProcessing = value; } }
public float trimEdge { get { return m_TrimEdge; } set { m_TrimEdge = value; } }
static internal void DuplicateShadowMesh(Mesh source, out Mesh dest)
{
// This is not gc tested as this generates garbage
dest = new Mesh();
dest.Clear();
if (source != null)
{
dest.vertices = source.vertices;
dest.tangents = source.tangents;
dest.triangles = source.triangles;
dest.bounds = source.bounds;
}
}
internal void OnBeforeSerialize()
{
if(m_NativeVertices.IsCreated)
m_Vertices = m_NativeVertices.ToArray();
if (m_NativeIndices.IsCreated)
m_Indices = m_NativeIndices.ToArray();
}
internal void OnAfterDeserialize()
{
if(m_NativeVertices.IsCreated)
m_NativeVertices.Dispose();
m_NativeVertices = new NativeArray<ShadowMeshVertex>(m_Vertices, Allocator.Persistent);
if(m_NativeIndices.IsCreated)
m_NativeIndices.Dispose();
m_NativeIndices = new NativeArray<int>(m_Indices, Allocator.Persistent);
}
internal void CopyFrom(ShadowMesh2D source)
{
// This is not gc tested as this generates garbage (calls DuplicateShadowMesh)
//DuplicateShadowMesh(source.m_Mesh, out m_Mesh);
m_TrimEdge = source.trimEdge;
m_LocalBounds = source.m_LocalBounds;
m_EdgeProcessing = source.edgeProcessing;
m_Vertices.CopyTo(source.m_Vertices, 0);
m_Indices.CopyTo(source.m_Indices,0);
GenerateShadowMesh(ref m_Mesh, m_NativeVertices, m_NativeIndices);
}
internal void AddCircle(Vector3 center, float r, NativeArray<Vector3> generatedVertices, NativeArray<int> generatedIndices, bool reverseWindingOrder, ref int vertexWritePos, ref int indexWritePos)
{
float direction = reverseWindingOrder ? 1 : -1;
// Special case a full circle
float segments = 2 * k_CapsuleCapSegments;
float angle;
int startWritePos = vertexWritePos;
for (int i = 0; i < segments; i++)
{
angle = direction * (2 * Mathf.PI * (float)i / (float)segments);
float x = r * Mathf.Cos(angle) + center.x;
float y = r * Mathf.Sin(angle) + center.y;
generatedIndices[indexWritePos++] = vertexWritePos;
generatedIndices[indexWritePos++] = i + 1 < segments ? vertexWritePos + 1 : startWritePos;
generatedVertices[vertexWritePos++] = new Vector3(x, y, 0);
}
}
internal void AddCapsuleCap(Vector3 center, float r, Vector3 otherCenter, NativeArray<Vector3> generatedVertices, NativeArray<int> generatedIndices, bool reverseWindingOrder, ref int vertexWritePos, ref int indexWritePos)
{
float startAngle;
float endAngle;
// Special case a full circle
float segments = k_CapsuleCapSegments;
Vector3 otherCenterDir = (otherCenter - center).normalized;
float absCenterAngle = Mathf.Acos(Vector3.Dot(otherCenterDir, new Vector3(1, 0, 0)));
float angleSign = Vector3.Dot(otherCenterDir, new Vector3(0, 1, 0)) < 0 ? -1f : 1f;
float centerAngle = absCenterAngle * angleSign;
// This is hard coded for a half circle
if (reverseWindingOrder)
{
float HalfPI = 0.5f * Mathf.PI;
startAngle = centerAngle + HalfPI;
endAngle = startAngle + Mathf.PI;
}
else
{
float ThreeHalfsPI = 1.5f * Mathf.PI;
startAngle = centerAngle + ThreeHalfsPI;
endAngle = startAngle - Mathf.PI;
}
float deltaAngle = endAngle - startAngle;
float angle;
for (int i = 0; i < segments; i++)
{
angle = (deltaAngle * (float)i / (float)segments) + startAngle;
float x = r * Mathf.Cos(angle) + center.x;
float y = r * Mathf.Sin(angle) + center.y;
generatedIndices[indexWritePos++] = vertexWritePos;
generatedIndices[indexWritePos++] = vertexWritePos + 1;
generatedVertices[vertexWritePos++] = new Vector3(x, y, 0);
}
angle = deltaAngle + startAngle;
generatedVertices[vertexWritePos++] = new Vector3(r * Mathf.Cos(angle) + center.x, r * Mathf.Sin(angle) + center.y, 0);
}
internal void AddCapsule(Vector3 pt0, Vector3 pt1, float r0, float r1, NativeArray<Vector3> generatedVertices, NativeArray<int> generatedIndices, bool reverseWindingOrder, ref int vertexWritePos, ref int indexWritePos)
{
// Add Straight Segments
Vector3 delta = (pt1 - pt0).normalized;
Vector3 relOffset0 = new Vector3(delta.y, -delta.x, 0);
Vector3 relOffset1 = new Vector3(-delta.y, delta.x, 0);
if (pt1.x < pt0.x)
{
Vector3 temp = pt0;
pt0 = pt1;
pt1 = temp;
}
int circle0Start = vertexWritePos;
// Add circles
AddCapsuleCap(pt0, r0, pt1, generatedVertices, generatedIndices, reverseWindingOrder, ref vertexWritePos, ref indexWritePos);
generatedIndices[indexWritePos++] = vertexWritePos - 1;
generatedIndices[indexWritePos++] = vertexWritePos;
AddCapsuleCap(pt1, r1, pt0, generatedVertices, generatedIndices, reverseWindingOrder, ref vertexWritePos, ref indexWritePos);
generatedIndices[indexWritePos++] = vertexWritePos - 1;
generatedIndices[indexWritePos++] = circle0Start;
}
internal int AddShape(NativeArray<Vector3> vertices, NativeArray<int> indices, int indicesProcessed, NativeArray<Vector3> generatedVertices, NativeArray<int> generatedIndices, ref int vertexWritePos, ref int indexWritePos)
{
int indexToProcess = indicesProcessed;
int prevIndex = indices[indexToProcess];
int startIndex = indices[indexToProcess];
int startWriteIndex = vertexWritePos;
generatedVertices[vertexWritePos++] = vertices[prevIndex];
bool continueProcessing = true;
while (indexToProcess < indices.Length && continueProcessing)
{
int index0 = indices[indexToProcess++];
int index1 = indices[indexToProcess++];
generatedIndices[indexWritePos++] = vertexWritePos - 1;
if (index1 != startIndex)
{
generatedIndices[indexWritePos++] = vertexWritePos;
generatedVertices[vertexWritePos++] = vertices[index1];
continueProcessing = index0 == prevIndex;
}
else
{
generatedIndices[indexWritePos++] = startWriteIndex;
continueProcessing = false;
}
prevIndex = index1;
}
return indexToProcess;
}
public override void SetShape(NativeArray<Vector3> vertices, NativeArray<int> indices, NativeArray<float> radii, Matrix4x4 transform, ShadowShape2D.WindingOrder windingOrder = ShadowShape2D.WindingOrder.Clockwise, bool allowTriming = true, bool createInteriorGeometry = false)
{
m_IsTransformable = true;
bool reverseWindingOrder = windingOrder == ShadowShape2D.WindingOrder.CounterClockwise;
GenerateShapeWithRadiiShared_Internal(vertices, indices, radii, reverseWindingOrder, out NativeArray<Vector3> generatedVertices, out NativeArray<int> generatedIndices);
for (int i = 0; i < generatedVertices.Length; i++)
generatedVertices[i] = transform.MultiplyPoint(generatedVertices[i]);
ProcessShapeWithRadiiShared_Internal(generatedVertices, generatedIndices, reverseWindingOrder, createInteriorGeometry, out NativeArray<ShadowEdge> calculatedEdges, out NativeArray<int> calculatedStartingEdges, out NativeArray<bool> calculatedIsClosedArray);
generatedVertices.Dispose();
generatedIndices.Dispose();
calculatedEdges.Dispose();
calculatedIsClosedArray.Dispose();
calculatedStartingEdges.Dispose();
}
// Clipper scales incoming coordinates by ShadowUtility's fixed point precision and keeps them in a 64 bit
// integer. Its own range test is compiled out when exceptions are disabled, as they are in player builds, so
// a coordinate past this bound overflows inside native Clipper and faults instead of being rejected. Staying
// within Clipper's low range (0x3FFFFFFF) after scaling by the precision (65536) keeps that arithmetic valid.
const float k_MaxClipperCoordinate = 16383.0f;
static bool IsRepresentableCoordinate(float value)
{
// Both comparisons are false for NaN, so this also rejects NaN and infinities.
return value >= -k_MaxClipperCoordinate && value <= k_MaxClipperCoordinate;
}
// Returns true when any vertex holds a coordinate Clipper cannot represent, which makes the whole shape
// unusable. Callers must bail out instead of passing the data on to native code.
static bool HasUnrepresentableVertices(NativeArray<Vector3> vertices)
{
if (vertices == null)
return true;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 vertex = vertices[i];
if (!IsRepresentableCoordinate(vertex.x) || !IsRepresentableCoordinate(vertex.y))
return true;
}
return false;
}
public override void SetShape(NativeArray<Vector3> vertices, NativeArray<int> indices, NativeArray<float> radii, ShadowShape2D.WindingOrder windingOrder = ShadowShape2D.WindingOrder.Clockwise, bool allowTriming = true, bool createInteriorGeometry = false, bool inWorldSpace = false)
{
m_IsTransformable = !inWorldSpace;
bool reverseWindingOrder = windingOrder == ShadowShape2D.WindingOrder.CounterClockwise;
GenerateShapeWithRadiiShared_Internal(vertices, indices, radii, reverseWindingOrder, out NativeArray<Vector3> generatedVertices, out NativeArray<int> generatedIndices);
ProcessShapeWithRadiiShared_Internal(generatedVertices, generatedIndices, reverseWindingOrder, createInteriorGeometry, out NativeArray<ShadowEdge> calculatedEdges, out NativeArray<int> calculatedStartingEdges, out NativeArray<bool> calculatedIsClosedArray);
generatedVertices.Dispose();
generatedIndices.Dispose();
calculatedEdges.Dispose();
calculatedIsClosedArray.Dispose();
calculatedStartingEdges.Dispose();
}
bool AreDegenerateVertices(NativeArray<Vector3> vertices)
{
if (vertices == null || vertices.Length == 0)
return true;
if (HasUnrepresentableVertices(vertices))
return true;
// This should is a trade off between perfomance and accuracy. This may need to be refined later if we find cases where this is not good enough.
int prevIndex = vertices.Length - 1;
for (int i=0;i< vertices.Length; i++)
{
if (vertices[prevIndex].x != vertices[i].x || vertices[prevIndex].y != vertices[i].y)
return false;
prevIndex = i;
}
return true;
}
public override void SetShape(NativeArray<Vector3> vertices, NativeArray<int> indices, ShadowShape2D.OutlineTopology outlineTopology, ShadowShape2D.WindingOrder windingOrder = ShadowShape2D.WindingOrder.Clockwise, bool allowTrimming = true, bool createInteriorGeometry = false, bool inWorldSpace = false)
{
m_IsTransformable = !inWorldSpace;
if (AreDegenerateVertices(vertices) || indices == null || indices.Length == 0)
{
Clear();
return;
}
if (m_TrimEdge == k_TrimEdgeUninitialized)
m_TrimEdge = m_InitialTrim;
bool disposeVertices = false;
NativeArray<ShadowEdge> edges;
NativeArray<int> shapeStartingIndices;
NativeArray<bool> shapeIsClosedArray;
if (outlineTopology == ShadowShape2D.OutlineTopology.Triangles)
{
NativeArray<Vector3> newVertices;
ShadowUtility.CalculateEdgesFromTriangles(ref vertices, ref indices, true, out newVertices, out edges, out shapeStartingIndices, out shapeIsClosedArray);
disposeVertices = true;
vertices = newVertices;
}
else // if (outlineTopology == ShadowShape2D.OutlineTopology.Lines)
{
ShadowUtility.CalculateEdgesFromLines(ref indices, out edges, out shapeStartingIndices, out shapeIsClosedArray);
}
if (windingOrder == ShadowShape2D.WindingOrder.CounterClockwise)
ShadowUtility.ReverseWindingOrder(ref shapeStartingIndices, ref edges);
// It would be better if we don't have to rerun SetShape after a trimEdge change.
if (m_EdgeProcessing == EdgeProcessing.Clipping && allowTrimming)
{
NativeArray<Vector3> clippedVertices;
NativeArray<ShadowEdge> clippedEdges;
NativeArray<int> clippedStartingIndices;
ShadowUtility.ClipEdges(ref vertices, ref edges, ref shapeStartingIndices, ref shapeIsClosedArray, trimEdge, out clippedVertices, out clippedEdges, out clippedStartingIndices);
m_LocalBounds = ShadowUtility.GenerateShadowGeometry(ref m_NativeVertices, ref m_NativeIndices, clippedVertices, clippedEdges, clippedStartingIndices, createInteriorGeometry, outlineTopology);
m_IsDirty = true;
clippedVertices.Dispose();
clippedEdges.Dispose();
clippedStartingIndices.Dispose();
}
else
{
m_LocalBounds = ShadowUtility.GenerateShadowGeometry(ref m_NativeVertices, ref m_NativeIndices, vertices, edges, shapeStartingIndices, createInteriorGeometry, outlineTopology);
m_IsDirty = true;
}
if (m_Mesh == null)
m_Mesh = new Mesh();
ShadowUtility.GenerateShadowMesh(ref m_Mesh, m_NativeVertices, m_NativeIndices);
if(disposeVertices)
vertices.Dispose();
edges.Dispose();
shapeStartingIndices.Dispose();
shapeIsClosedArray.Dispose();
}
public override void SetShapeDirect(NativeArray<Vector3> vertices, NativeArray<int> indices, bool inWorldSpace = false)
{
m_IsTransformable = !inWorldSpace;
NativeArray<ShadowEdge> edges = indices.Reinterpret<ShadowEdge>(sizeof(int));
// Create a shape starting indices array indicating a single shape starting at edge 0
NativeArray<int> shapeStartingIndices = new NativeArray<int>(1, Allocator.Temp);
shapeStartingIndices[0] = 0;
m_LocalBounds = ShadowUtility.GenerateShadowGeometry(ref m_NativeVertices, ref m_NativeIndices, vertices, edges, shapeStartingIndices, false, OutlineTopology.Lines);
m_IsDirty = true;
if (m_Mesh == null)
m_Mesh = new Mesh();
ShadowUtility.GenerateShadowMesh(ref m_Mesh, m_NativeVertices, m_NativeIndices);
shapeStartingIndices.Dispose();
}
public void SetShapeWithLines(NativeArray<Vector3> vertices, NativeArray<int> indices, bool allowTrimming)
{
SetShape(vertices, indices, ShadowShape2D.OutlineTopology.Lines, allowTrimming: allowTrimming);
}
public override void SetFlip(bool flipX, bool flipY)
{
m_FlipX = flipX;
m_FlipY = flipY;
}
public override void GetFlip(out bool flipX, out bool flipY)
{
flipX = m_FlipX;
flipY = m_FlipY;
}
public override void SetDefaultTrim(float trim)
{
m_InitialTrim = trim;
}
public void UpdateBoundingSphere(Transform transform)
{
var maxBound = m_LocalBounds.max;
var minBound = m_LocalBounds.min;
if (isTransformable)
{
maxBound = transform.TransformPoint(m_LocalBounds.max);
minBound = transform.TransformPoint(m_LocalBounds.min);
}
var center = 0.5f * (maxBound + minBound);
var radius = Vector3.Magnitude(maxBound - center);
m_BoundingSphere = new BoundingSphere(center, radius);
}
~ShadowMesh2D()
{
if(m_NativeIndices.IsCreated)
m_NativeIndices.Dispose();
if(m_NativeVertices.IsCreated)
m_NativeVertices.Dispose();
}
private void GenerateShapeWithRadiiShared_Internal(NativeArray<Vector3> vertices, NativeArray<int> indices, NativeArray<float> radii, bool reverseWindingOrder, out NativeArray<Vector3> generatedVertices, out NativeArray<int> generatedIndices)
{
if (m_TrimEdge == k_TrimEdgeUninitialized)
m_TrimEdge = m_InitialTrim;
if (indices.Length == 0 || HasUnrepresentableVertices(vertices))
{
Clear();
generatedIndices = new NativeArray<int>();
generatedVertices = new NativeArray<Vector3>();
return;
}
int circleCount = 0;
int capsuleCount = 0;
for (int i = 0; i < indices.Length; i += 2)
{
int index0 = indices[i];
int index1 = indices[i + 1];
if (radii[index0] > 0 || radii[index1] > 0)
{
if (index0 == index1)
circleCount++;
else
capsuleCount++;
}
}
int capsuleStraightSegments = capsuleCount * 2;
int capsuleCapSegments = capsuleCount * k_CapsuleCapSegments; // This can be refined later
int circleSegments = circleCount * 2 * k_CapsuleCapSegments;
int lineCount = (indices.Length >> 1) - (capsuleCount + circleCount);
int indexCount = 2 * (lineCount + capsuleStraightSegments + (2 * capsuleCapSegments) + circleSegments);
int vertexCount = indexCount; // Keep this simple for now
generatedVertices = new NativeArray<Vector3>(vertexCount, Allocator.Temp);
generatedIndices = new NativeArray<int>(indexCount, Allocator.Temp);
int vertexWritePos = 0;
int indexWritePos = 0;
int indicesProcessed = 0;
while (indicesProcessed < indices.Length)
{
int v0 = indices[indicesProcessed];
int v1 = indices[indicesProcessed + 1];
float r0 = radii[v0];
float r1 = radii[v1];
if (radii[v0] > 0 || radii[v1] > 0)
{
Vector3 pt0 = vertices[v0];
Vector3 pt1 = vertices[v1];
if (vertices[v0].x == vertices[v1].x && vertices[v0].y == vertices[v1].y)
AddCircle(pt0, r0, generatedVertices, generatedIndices, reverseWindingOrder, ref vertexWritePos, ref indexWritePos);
else
AddCapsule(pt0, pt1, r0, r1, generatedVertices, generatedIndices, reverseWindingOrder, ref vertexWritePos, ref indexWritePos);
indicesProcessed += 2;
}
else
{
// Will add edges or polygons
indicesProcessed = AddShape(vertices, indices, indicesProcessed, generatedVertices, generatedIndices, ref vertexWritePos, ref indexWritePos);
}
}
}
private void ProcessShapeWithRadiiShared_Internal(NativeArray<Vector3> generatedVertices, NativeArray<int> generatedIndices, bool reverseWindingOrder, bool createInteriorGeometry, out NativeArray<ShadowEdge> calculatedEdges, out NativeArray<int> calculatedStartingEdges, out NativeArray<bool> calculatedIsClosedArray)
{
ShadowUtility.CalculateEdgesFromLines(ref generatedIndices, out calculatedEdges, out calculatedStartingEdges, out calculatedIsClosedArray);
if (reverseWindingOrder)
ShadowUtility.ReverseWindingOrder(ref calculatedStartingEdges, ref calculatedEdges);
if (m_EdgeProcessing == EdgeProcessing.Clipping)
{
NativeArray<Vector3> clippedVertices;
NativeArray<ShadowEdge> clippedEdges;
NativeArray<int> clippedStartingIndices;
ShadowUtility.ClipEdges(ref generatedVertices, ref calculatedEdges, ref calculatedStartingEdges, ref calculatedIsClosedArray, trimEdge, out clippedVertices, out clippedEdges, out clippedStartingIndices);
if (clippedStartingIndices.Length > 0)
{
m_LocalBounds = ShadowUtility.GenerateShadowGeometry(ref m_NativeVertices, ref m_NativeIndices, clippedVertices, clippedEdges, clippedStartingIndices, createInteriorGeometry, ShadowShape2D.OutlineTopology.Lines);
m_IsDirty = true;
}
else
{
m_LocalBounds = new Bounds();
Clear();
}
clippedVertices.Dispose();
clippedEdges.Dispose();
clippedStartingIndices.Dispose();
}
else
{
m_LocalBounds = ShadowUtility.GenerateShadowGeometry(ref m_NativeVertices, ref m_NativeIndices, generatedVertices, calculatedEdges, calculatedStartingEdges, createInteriorGeometry, ShadowShape2D.OutlineTopology.Lines);
m_IsDirty = true;
}
}
}
}