using System; using CultMath; namespace GameCult.Geometry { /// /// Derives per-vertex normals for a welded quad mesh by face-weighted averaging. /// public static class CultGeometryQuadNormals { /// /// Computes a per-vertex normal for as the sum, over every quad /// touching that vertex, of the quad's diagonal cross product (which points along the /// quad's normal and whose length is twice a non-planar quad's area), then normalized. /// A vertex touched only by zero-area quads gets a zero normal. /// public static float[] FaceWeighted(CultGeometryQuadMesh mesh) { if (mesh == null) throw new ArgumentNullException(nameof(mesh)); var sums = new float3[mesh.VertexCount]; for (var quad = 0; quad < mesh.QuadCount; quad++) { var baseIndex = quad * 4; var i0 = mesh.Quads[baseIndex]; var i1 = mesh.Quads[baseIndex + 1]; var i2 = mesh.Quads[baseIndex + 2]; var i3 = mesh.Quads[baseIndex + 3]; var p0 = Position(mesh.Positions, i0); var p1 = Position(mesh.Positions, i1); var p2 = Position(mesh.Positions, i2); var p3 = Position(mesh.Positions, i3); var faceVector = math.cross(p2 - p0, p3 - p1); sums[i0] += faceVector; sums[i1] += faceVector; sums[i2] += faceVector; sums[i3] += faceVector; } var normals = new float[mesh.VertexCount * 3]; for (var vertex = 0; vertex < sums.Length; vertex++) { var sum = sums[vertex]; // A vertex whose contributions cancel or were all zero-area gets a zero normal; // normalize follows HLSL and would return NaN for it (matches Extract's degenerate rule). var normal = math.dot(sum, sum) > 0f ? math.normalize(sum) : float3.zero; normals[vertex * 3] = normal.x; normals[vertex * 3 + 1] = normal.y; normals[vertex * 3 + 2] = normal.z; } return normals; } private static float3 Position(float[] positions, uint index) { var offset = (int)index * 3; return new float3(positions[offset], positions[offset + 1], positions[offset + 2]); } } }