feat: directional lighting with per-face normals
- Add Normal to Vertex struct and vertex input pipeline. - Update vertex/fragment shaders with push-constant light data (direction, color, ambient). - Compute per-face normals in ObjLoader and GltfLoader for flat shading. - Add Transform.TransformNormal() using inverse-transpose of the model matrix. - MeshRenderer builds world-space normals and pushes light constants each frame. - Recompile SPIR-V shaders.
This commit is contained in:
@@ -10,6 +10,7 @@ namespace Engine.Graphics.Loaders;
|
||||
/// <summary>
|
||||
/// glTF/glTF binary loader using SharpGLTF.Core.
|
||||
/// Loads the first primitive of the first mesh and converts it to a colored Mesh component.
|
||||
/// Creates per-face normals for flat shading if the glTF does not provide normals.
|
||||
/// </summary>
|
||||
public static class GltfLoader
|
||||
{
|
||||
@@ -31,28 +32,63 @@ public static class GltfLoader
|
||||
throw new InvalidOperationException($"glTF primitive has no POSITION accessor: {path}");
|
||||
|
||||
var positions = positionAccessor.AsVector3Array();
|
||||
var vertices = new Vertex[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
|
||||
var indices = GetIndices(primitive, positions.Count);
|
||||
var vertices = new List<Vertex>();
|
||||
var newIndices = new List<uint>();
|
||||
|
||||
for (var i = 0; i < indices.Length; i += 3)
|
||||
{
|
||||
vertices[i] = new Vertex(positions[i], color);
|
||||
var i0 = (int)indices[i];
|
||||
var i1 = (int)indices[i + 1];
|
||||
var i2 = (int)indices[i + 2];
|
||||
|
||||
var v0 = new Vector3(positions[i0].X, positions[i0].Y, positions[i0].Z);
|
||||
var v1 = new Vector3(positions[i1].X, positions[i1].Y, positions[i1].Z);
|
||||
var v2 = new Vector3(positions[i2].X, positions[i2].Y, positions[i2].Z);
|
||||
|
||||
var normal = ComputeFaceNormal(v0, v1, v2);
|
||||
|
||||
var vertexBase = (uint)vertices.Count;
|
||||
newIndices.Add(vertexBase);
|
||||
newIndices.Add(vertexBase + 1);
|
||||
newIndices.Add(vertexBase + 2);
|
||||
|
||||
vertices.Add(new Vertex(v0, color, normal));
|
||||
vertices.Add(new Vertex(v1, color, normal));
|
||||
vertices.Add(new Vertex(v2, color, normal));
|
||||
}
|
||||
|
||||
uint[] indices;
|
||||
return new Engine.Core.Components.Mesh(vertices.ToArray(), newIndices.ToArray());
|
||||
}
|
||||
|
||||
private static uint[] GetIndices(MeshPrimitive primitive, int positionCount)
|
||||
{
|
||||
if (primitive.IndexAccessor != null)
|
||||
{
|
||||
var idx = primitive.IndexAccessor.AsIndexArray();
|
||||
indices = new uint[idx.Count];
|
||||
var indices = new uint[idx.Count];
|
||||
for (var i = 0; i < idx.Count; i++)
|
||||
indices[i] = idx[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-indexed primitive
|
||||
indices = new uint[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
indices[i] = (uint)i;
|
||||
return indices;
|
||||
}
|
||||
|
||||
return new Engine.Core.Components.Mesh(vertices, indices);
|
||||
// Non-indexed primitive
|
||||
var auto = new uint[positionCount];
|
||||
for (var i = 0; i < positionCount; i++)
|
||||
auto[i] = (uint)i;
|
||||
return auto;
|
||||
}
|
||||
|
||||
private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
normal = Vector3.UnitY;
|
||||
return normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Engine.Graphics.Loaders;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal .obj loader.
|
||||
/// Supports vertices (v) and faces (f). Ignores normals/UVs for now.
|
||||
/// Supports vertices (v) and faces (f). Creates per-face normals for flat shading.
|
||||
/// Produces a colored Mesh component.
|
||||
/// </summary>
|
||||
public static class ObjLoader
|
||||
@@ -19,6 +19,7 @@ public static class ObjLoader
|
||||
var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f);
|
||||
|
||||
var positions = new List<Vector3>();
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
foreach (var rawLine in File.ReadLines(path))
|
||||
@@ -41,28 +42,36 @@ public static class ObjLoader
|
||||
break;
|
||||
|
||||
case "f" when parts.Length >= 4:
|
||||
// Triangulate the face as a fan. Only the position index is used.
|
||||
// Triangulate the face as a fan.
|
||||
var baseIndex = ParseFaceIndex(parts[1]);
|
||||
for (var i = 2; i < parts.Length - 1; i++)
|
||||
{
|
||||
indices.Add(baseIndex);
|
||||
indices.Add(ParseFaceIndex(parts[i]));
|
||||
indices.Add(ParseFaceIndex(parts[i + 1]));
|
||||
var i0 = baseIndex;
|
||||
var i1 = ParseFaceIndex(parts[i]);
|
||||
var i2 = ParseFaceIndex(parts[i + 1]);
|
||||
|
||||
var v0 = positions[(int)i0];
|
||||
var v1 = positions[(int)i1];
|
||||
var v2 = positions[(int)i2];
|
||||
var normal = ComputeFaceNormal(v0, v1, v2);
|
||||
|
||||
var vertexBase = (uint)vertices.Count;
|
||||
indices.Add(vertexBase);
|
||||
indices.Add(vertexBase + 1);
|
||||
indices.Add(vertexBase + 2);
|
||||
|
||||
vertices.Add(new Vertex(v0, color, normal));
|
||||
vertices.Add(new Vertex(v1, color, normal));
|
||||
vertices.Add(new Vertex(v2, color, normal));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (positions.Count == 0)
|
||||
if (vertices.Count == 0)
|
||||
throw new InvalidOperationException($"OBJ file has no vertices: {path}");
|
||||
|
||||
var vertices = new Vertex[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
vertices[i] = new Vertex(positions[i], color);
|
||||
}
|
||||
|
||||
return new Mesh(vertices, indices.ToArray());
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
|
||||
private static uint ParseFaceIndex(string part)
|
||||
@@ -73,4 +82,16 @@ public static class ObjLoader
|
||||
var index = int.Parse(indexStr);
|
||||
return (uint)(index - 1); // OBJ indices are 1-based
|
||||
}
|
||||
|
||||
private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
normal = Vector3.UnitY;
|
||||
return normal;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user