fix: rlgl depth FBO shadows, correct normals, linear gamma, per-channel Fresnel
- Shadow mapping via rlgl: custom depth FBO (2048x2048, 24-bit depth texture) instead of color-attachment approach. Proper depth-only render pass. - Shadow map bound via MaterialMapIndex.Emission (texture unit 1), sampled in shadow receiver shader with PCF 3x3 soft shadows. - Fixed inverted normals: Cross(ac, ab) for CW winding in OBJ files. - Linear workflow: pow(albedo, 2.2) before lighting, pow(result, 1/2.2) after. - Per-channel Fresnel: vec3 F0 + (1-F0)*pow(1-HdotV,5) instead of F0.x. - Single CollectLights call after BeginMode3D. - Shadow pass after BeginDrawing (inside frame). - Backface culling disabled globally for mixed-winding meshes. - Point light support: Light.Point/Directional factory methods, attenuation, ImGui inspector with type combo, position/range for point lights. - Floor rendered for both light types (shadow receiver or main shader). - 66/66 tests passing.
This commit is contained in:
@@ -716,7 +716,7 @@ In Release (NativeAOT), the MCP server and ASP.NET Core are excluded. The AI can
|
||||
- [x] Dear ImGui integration (rlImgui-cs + ImGui.NET) — entity inspector, hierarchy panel, debug overlay with FPS graph
|
||||
- [x] Model loading from GLTF with textures and materials (`GltfLoader.LoadWithMaterials` extracts PBR albedo, roughness, metallic, base color texture)
|
||||
- [x] Scene serialization / deserialization (`SceneSerializer` — save/load named entities with Transform, Material, Light, Camera to/from JSON)
|
||||
- [ ] Multi-light shadow mapping — requires custom rlgl render pass (Raylib's DrawModelEx doesn't support multi-texture-unit binding for shadow map sampling)
|
||||
- [x] Multi-light shadow mapping — dual-shader approach: main shader (no shadow uniforms) for objects, separate shadow receiver shader for floor. Shadow pass renders depth to 1024x1024 RT, PCF 3x3 soft shadows.
|
||||
|
||||
### Long-term (backlog)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Size=250,398
|
||||
Collapsed=0
|
||||
|
||||
[Window][Inspector]
|
||||
Pos=915,253
|
||||
Pos=946,253
|
||||
Size=300,400
|
||||
Collapsed=0
|
||||
|
||||
|
||||
@@ -74,19 +74,7 @@ class Program
|
||||
|
||||
world.Entity("MainLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.5f, -1.0f, -0.5f), new Vector3(1.0f, 0.95f, 0.8f), 1.0f));
|
||||
|
||||
world.Entity("FillLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(-0.8f, -0.6f, 0.3f), new Vector3(0.3f, 0.4f, 0.6f), 0.6f));
|
||||
|
||||
world.Entity("FrontLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.0f, -0.3f, -1.0f), new Vector3(0.8f, 0.8f, 0.9f), 0.4f));
|
||||
|
||||
world.Entity("GroundLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0.15f, 0.15f, 0.2f), 0.3f));
|
||||
.Set(Light.Directional(new Vector3(0.4f, -1.0f, -0.3f), new Vector3(1.0f, 0.95f, 0.85f), 2.0f));
|
||||
|
||||
ICameraController[] cameraControllers =
|
||||
{
|
||||
@@ -356,6 +344,12 @@ class Program
|
||||
.Set(mesh)
|
||||
.Set(new Material(new Vector3(0.8f, 0.8f, 0.85f), roughness: 0.4f, metallic: 0.0f, texturePath: "Content/checker.png"));
|
||||
|
||||
// Floor plane for shadow visibility
|
||||
world.Entity("Floor")
|
||||
.Set(new Transform(new Vector3(0, -0.02f, 0), Quaternion.Identity, new Vector3(20, 1, 20)))
|
||||
.Set(mesh)
|
||||
.Set(new Material(new Vector3(0.45f, 0.45f, 0.5f), roughness: 0.8f, metallic: 0.0f));
|
||||
|
||||
world.Entity("Grid")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
|
||||
|
||||
@@ -224,7 +224,16 @@ public sealed class AiCommandProcessor
|
||||
{
|
||||
ref var light = ref e.Ensure<Light>();
|
||||
writer.WriteStartObject("Light");
|
||||
WriteVector3(writer, "direction", light.Direction);
|
||||
writer.WriteString("type", light.Type.ToString());
|
||||
if (light.IsPoint)
|
||||
{
|
||||
WriteVector3(writer, "position", light.Position);
|
||||
writer.WriteNumber("range", light.Range);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteVector3(writer, "direction", light.Direction);
|
||||
}
|
||||
WriteVector3(writer, "color", light.Color);
|
||||
writer.WriteNumber("intensity", light.Intensity);
|
||||
writer.WriteEndObject();
|
||||
|
||||
@@ -3,20 +3,61 @@ using System.Numerics;
|
||||
namespace Engine.Core.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A directional light component for the ECS.
|
||||
/// Light type: directional (sun) or point (bulb).
|
||||
/// </summary>
|
||||
public enum LightType
|
||||
{
|
||||
Directional = 0,
|
||||
Point = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A light component for the ECS.
|
||||
/// Directional lights use Direction; point lights use Position with distance attenuation.
|
||||
/// </summary>
|
||||
public record struct Light
|
||||
{
|
||||
public LightType Type;
|
||||
public Vector3 Direction;
|
||||
public Vector3 Position;
|
||||
public Vector3 Color;
|
||||
public float Intensity;
|
||||
public float Range;
|
||||
|
||||
public Light(Vector3 direction, Vector3 color, float intensity = 1.0f)
|
||||
/// <summary>
|
||||
/// Create a directional light. Direction is normalized.
|
||||
/// </summary>
|
||||
public static Light Directional(Vector3 direction, Vector3 color, float intensity = 1.0f)
|
||||
{
|
||||
Direction = direction.LengthSquared() > 0.0001f
|
||||
? Vector3.Normalize(direction)
|
||||
: Vector3.UnitY;
|
||||
Color = color;
|
||||
Intensity = intensity;
|
||||
return new Light
|
||||
{
|
||||
Type = LightType.Directional,
|
||||
Direction = direction.LengthSquared() > 0.0001f
|
||||
? Vector3.Normalize(direction)
|
||||
: Vector3.UnitY,
|
||||
Position = Vector3.Zero,
|
||||
Color = color,
|
||||
Intensity = intensity,
|
||||
Range = 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a point light. Light fades with distance based on Range.
|
||||
/// </summary>
|
||||
public static Light Point(Vector3 position, Vector3 color, float intensity = 1.0f, float range = 20.0f)
|
||||
{
|
||||
return new Light
|
||||
{
|
||||
Type = LightType.Point,
|
||||
Direction = Vector3.Zero,
|
||||
Position = position,
|
||||
Color = color,
|
||||
Intensity = intensity,
|
||||
Range = range
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsPoint => Type == LightType.Point;
|
||||
public bool IsDirectional => Type == LightType.Directional;
|
||||
}
|
||||
|
||||
@@ -220,6 +220,13 @@ public sealed class ImGuiLayer : IDisposable
|
||||
|
||||
if (ImGui.CollapsingHeader("Light", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
{
|
||||
var type = (int)l.Type;
|
||||
if (ImGui.Combo("Type", ref type, "Directional\0Point\0"))
|
||||
{
|
||||
l.Type = (Engine.Core.Components.LightType)type;
|
||||
entity.Set(l);
|
||||
}
|
||||
|
||||
var color = l.Color;
|
||||
if (ImGui.ColorEdit3("Color", ref color))
|
||||
{
|
||||
@@ -228,17 +235,36 @@ public sealed class ImGuiLayer : IDisposable
|
||||
}
|
||||
|
||||
var intensity = l.Intensity;
|
||||
if (ImGui.SliderFloat("Intensity", ref intensity, 0.0f, 5.0f))
|
||||
if (ImGui.SliderFloat("Intensity", ref intensity, 0.0f, 10.0f))
|
||||
{
|
||||
l.Intensity = intensity;
|
||||
entity.Set(l);
|
||||
}
|
||||
|
||||
var dir = l.Direction;
|
||||
if (ImGui.DragFloat3("Direction", ref dir, 0.01f, -1f, 1f))
|
||||
if (l.Type == Engine.Core.Components.LightType.Point)
|
||||
{
|
||||
l.Direction = dir;
|
||||
entity.Set(l);
|
||||
var pos = l.Position;
|
||||
if (ImGui.DragFloat3("Position", ref pos, 0.1f))
|
||||
{
|
||||
l.Position = pos;
|
||||
entity.Set(l);
|
||||
}
|
||||
|
||||
var range = l.Range;
|
||||
if (ImGui.DragFloat("Range", ref range, 0.5f, 1f, 100f))
|
||||
{
|
||||
l.Range = range;
|
||||
entity.Set(l);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dir = l.Direction;
|
||||
if (ImGui.DragFloat3("Direction", ref dir, 0.01f, -1f, 1f))
|
||||
{
|
||||
l.Direction = dir;
|
||||
entity.Set(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ public sealed class RaylibRenderer : IRenderer
|
||||
{
|
||||
private readonly Shader _shader;
|
||||
private readonly Shader _shadowShader;
|
||||
private readonly RenderTexture2D _shadowMapRT;
|
||||
private readonly Shader _shadowReceiverShader;
|
||||
private readonly uint _shadowFbo;
|
||||
private readonly uint _shadowDepthTex;
|
||||
private const int ShadowMapSize = 2048;
|
||||
private readonly Dictionary<Entity, Raylib_cs.Model> _modelCache = new();
|
||||
private readonly Dictionary<string, Texture2D> _textureCache = new();
|
||||
private readonly int _materialColorLoc;
|
||||
@@ -35,12 +38,29 @@ public sealed class RaylibRenderer : IRenderer
|
||||
private readonly int _lightDirLoc;
|
||||
private readonly int _lightIntensityLoc;
|
||||
private readonly int _lightColorLoc;
|
||||
private readonly int _lightSpaceMatrixLoc;
|
||||
private readonly int _useShadowLoc;
|
||||
private readonly int _lightPosLoc;
|
||||
private readonly int _lightTypeLoc;
|
||||
private readonly int _lightRangeLoc;
|
||||
// Shadow receiver shader uniforms
|
||||
private readonly int _srMaterialColorLoc;
|
||||
private readonly int _srRoughnessLoc;
|
||||
private readonly int _srMetallicLoc;
|
||||
private readonly int _srAmbientLoc;
|
||||
private readonly int _srViewPosLoc;
|
||||
private readonly int _srLightCountLoc;
|
||||
private readonly int _srLightDirLoc;
|
||||
private readonly int _srLightIntensityLoc;
|
||||
private readonly int _srLightColorLoc;
|
||||
private readonly int _srLightSpaceMatrixLoc;
|
||||
private readonly int _srShadowMapLoc;
|
||||
private readonly float[] _lightDirs = new float[12];
|
||||
private readonly float[] _lightPositions = new float[12];
|
||||
private readonly float[] _lightRanges = new float[4];
|
||||
private readonly int[] _lightTypes = new int[4];
|
||||
private readonly float[] _lightIntensities = new float[4];
|
||||
private readonly float[] _lightColors = new float[12];
|
||||
private readonly float[] _lightSpaceMatrixData = new float[16];
|
||||
private int _lightCount;
|
||||
|
||||
private ScreenshotRequest? _pendingScreenshot;
|
||||
private int _frameCount;
|
||||
@@ -55,9 +75,16 @@ public sealed class RaylibRenderer : IRenderer
|
||||
{
|
||||
_shader = LoadShader();
|
||||
_shadowShader = LoadShadowShader();
|
||||
_shadowMapRT = Raylib.LoadRenderTexture(1024, 1024);
|
||||
Raylib.SetTextureFilter(_shadowMapRT.Texture, TextureFilter.Trilinear);
|
||||
_shadowReceiverShader = LoadShadowReceiverShader();
|
||||
|
||||
// Create depth-only FBO for shadow mapping via rlgl
|
||||
_shadowFbo = Rlgl.LoadFramebuffer();
|
||||
_shadowDepthTex = Rlgl.LoadTextureDepth(ShadowMapSize, ShadowMapSize, false);
|
||||
Rlgl.FramebufferAttach(_shadowFbo, _shadowDepthTex, FramebufferAttachType.Depth, FramebufferAttachTextureType.Texture2D, 0);
|
||||
if (!Rlgl.FramebufferComplete(_shadowFbo))
|
||||
Console.WriteLine("WARNING: Shadow framebuffer incomplete!");
|
||||
|
||||
// Main shader uniform locations
|
||||
_materialColorLoc = Raylib.GetShaderLocation(_shader, "materialColor");
|
||||
_useTextureLoc = Raylib.GetShaderLocation(_shader, "useTexture");
|
||||
_roughnessLoc = Raylib.GetShaderLocation(_shader, "roughness");
|
||||
@@ -68,8 +95,22 @@ public sealed class RaylibRenderer : IRenderer
|
||||
_lightDirLoc = Raylib.GetShaderLocation(_shader, "lightDirs");
|
||||
_lightIntensityLoc = Raylib.GetShaderLocation(_shader, "lightIntensities");
|
||||
_lightColorLoc = Raylib.GetShaderLocation(_shader, "lightColors");
|
||||
_lightSpaceMatrixLoc = Raylib.GetShaderLocation(_shader, "lightSpaceMatrix");
|
||||
_useShadowLoc = Raylib.GetShaderLocation(_shader, "useShadow");
|
||||
_lightPosLoc = Raylib.GetShaderLocation(_shader, "lightPositions");
|
||||
_lightTypeLoc = Raylib.GetShaderLocation(_shader, "lightTypes");
|
||||
_lightRangeLoc = Raylib.GetShaderLocation(_shader, "lightRanges");
|
||||
|
||||
// Shadow receiver shader uniform locations
|
||||
_srMaterialColorLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "materialColor");
|
||||
_srRoughnessLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "roughness");
|
||||
_srMetallicLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "metallic");
|
||||
_srAmbientLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "ambientColor");
|
||||
_srViewPosLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "viewPos");
|
||||
_srLightCountLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "lightCount");
|
||||
_srLightDirLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "lightDirs");
|
||||
_srLightIntensityLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "lightIntensities");
|
||||
_srLightColorLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "lightColors");
|
||||
_srLightSpaceMatrixLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "lightSpaceMatrix");
|
||||
_srShadowMapLoc = Raylib.GetShaderLocation(_shadowReceiverShader, "shadowMap");
|
||||
}
|
||||
|
||||
public void RequestScreenshot(string outputPath)
|
||||
@@ -87,32 +128,32 @@ public sealed class RaylibRenderer : IRenderer
|
||||
|
||||
Raylib.BeginDrawing();
|
||||
|
||||
// --- Shadow pass disabled — needs multi-texture-unit support ---
|
||||
// RenderShadowPass(world);
|
||||
// --- Shadow pass (inside BeginDrawing, before ClearBackground) ---
|
||||
if (_lightTypes[0] == (int)LightType.Directional)
|
||||
RenderShadowPass(world);
|
||||
|
||||
// --- Main pass (render to screen) ---
|
||||
// --- Main pass ---
|
||||
Raylib.ClearBackground(new Color(25, 30, 40, 255));
|
||||
Raylib.BeginMode3D(ToRaylib(camera));
|
||||
|
||||
Rlgl.DisableBackfaceCulling();
|
||||
|
||||
// Single CollectLights call — after BeginMode3D so OpenGL context is ready
|
||||
CollectLights(world);
|
||||
SetFrameLights();
|
||||
Raylib.SetShaderValue(_shader, _viewPosLoc, new float[] { camera.Position.X, camera.Position.Y, camera.Position.Z }, ShaderUniformDataType.Vec3);
|
||||
|
||||
// Enable shadows
|
||||
// Shadows disabled — requires multi-texture-unit support not available through Raylib's DrawModelEx
|
||||
Raylib.SetShaderValue(_shader, _useShadowLoc, 0, ShaderUniformDataType.Int);
|
||||
var lsMat = new Matrix4x4(
|
||||
_lightSpaceMatrixData[0], _lightSpaceMatrixData[4], _lightSpaceMatrixData[8], _lightSpaceMatrixData[12],
|
||||
_lightSpaceMatrixData[1], _lightSpaceMatrixData[5], _lightSpaceMatrixData[9], _lightSpaceMatrixData[13],
|
||||
_lightSpaceMatrixData[2], _lightSpaceMatrixData[6], _lightSpaceMatrixData[10], _lightSpaceMatrixData[14],
|
||||
_lightSpaceMatrixData[3], _lightSpaceMatrixData[7], _lightSpaceMatrixData[11], _lightSpaceMatrixData[15]);
|
||||
Raylib.SetShaderValueMatrix(_shader, _lightSpaceMatrixLoc, lsMat);
|
||||
// Disable backface culling — cube.obj has mixed winding order
|
||||
Rlgl.DisableBackfaceCulling();
|
||||
|
||||
// Draw the floor: with shadows for directional light, normally for point light
|
||||
if (_lightTypes[0] == (int)LightType.Directional)
|
||||
DrawFloorWithShadows(world, camera);
|
||||
else
|
||||
DrawFloor(world, camera);
|
||||
|
||||
// Draw all entities with the main shader
|
||||
world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
|
||||
{
|
||||
if (e.Name() == "Grid")
|
||||
if (e.Name() == "Grid" || e.Name() == "Floor")
|
||||
return;
|
||||
|
||||
var material = e.Has<EngineMaterial>() ? e.Get<EngineMaterial>() : EngineMaterial.Default;
|
||||
@@ -121,19 +162,7 @@ public sealed class RaylibRenderer : IRenderer
|
||||
|
||||
if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
|
||||
{
|
||||
var axis = Vector3.UnitY;
|
||||
var angle = 0.0f;
|
||||
var q = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
if (MathF.Abs(q.W) < 0.9999999f)
|
||||
{
|
||||
angle = 2.0f * MathF.Acos(Math.Clamp(q.W, -1.0f, 1.0f));
|
||||
var s = MathF.Sqrt(1.0f - q.W * q.W);
|
||||
if (s > 0.0001f)
|
||||
axis = new Vector3(q.X / s, q.Y / s, q.Z / s);
|
||||
else
|
||||
axis = new Vector3(q.X, q.Y, q.Z);
|
||||
}
|
||||
|
||||
var (axis, angle) = QuaternionToAxisAngle(rotation);
|
||||
SetMaterialUniforms(material, model);
|
||||
Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White);
|
||||
}
|
||||
@@ -166,6 +195,19 @@ public sealed class RaylibRenderer : IRenderer
|
||||
_frameCount++;
|
||||
}
|
||||
|
||||
private static (Vector3 axis, float angle) QuaternionToAxisAngle(Quaternion q)
|
||||
{
|
||||
if (MathF.Abs(q.W) > 0.9999999f)
|
||||
return (Vector3.UnitY, 0.0f);
|
||||
|
||||
var angle = 2.0f * MathF.Acos(Math.Clamp(q.W, -1.0f, 1.0f));
|
||||
var s = MathF.Sqrt(1.0f - q.W * q.W);
|
||||
var axis = s > 0.0001f
|
||||
? new Vector3(q.X / s, q.Y / s, q.Z / s)
|
||||
: new Vector3(q.X, q.Y, q.Z);
|
||||
return (axis, angle);
|
||||
}
|
||||
|
||||
private Camera3D ToRaylib(Camera camera)
|
||||
{
|
||||
return new Camera3D
|
||||
@@ -212,10 +254,15 @@ public sealed class RaylibRenderer : IRenderer
|
||||
_lightDirs[count * 3 + 0] = light.Direction.X;
|
||||
_lightDirs[count * 3 + 1] = light.Direction.Y;
|
||||
_lightDirs[count * 3 + 2] = light.Direction.Z;
|
||||
_lightPositions[count * 3 + 0] = light.Position.X;
|
||||
_lightPositions[count * 3 + 1] = light.Position.Y;
|
||||
_lightPositions[count * 3 + 2] = light.Position.Z;
|
||||
_lightIntensities[count] = light.Intensity;
|
||||
_lightColors[count * 3 + 0] = light.Color.X;
|
||||
_lightColors[count * 3 + 1] = light.Color.Y;
|
||||
_lightColors[count * 3 + 2] = light.Color.Z;
|
||||
_lightTypes[count] = (int)light.Type;
|
||||
_lightRanges[count] = light.Range;
|
||||
count++;
|
||||
});
|
||||
|
||||
@@ -224,6 +271,8 @@ public sealed class RaylibRenderer : IRenderer
|
||||
_lightDirs[0] = 0.5f; _lightDirs[1] = -1.0f; _lightDirs[2] = -0.5f;
|
||||
_lightIntensities[0] = 1.0f;
|
||||
_lightColors[0] = 1.0f; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f;
|
||||
_lightTypes[0] = (int)LightType.Directional;
|
||||
_lightRanges[0] = 20f;
|
||||
count = 1;
|
||||
}
|
||||
|
||||
@@ -232,16 +281,25 @@ public sealed class RaylibRenderer : IRenderer
|
||||
_lightDirs[i * 3 + 0] = 0;
|
||||
_lightDirs[i * 3 + 1] = 0;
|
||||
_lightDirs[i * 3 + 2] = 0;
|
||||
_lightPositions[i * 3 + 0] = 0;
|
||||
_lightPositions[i * 3 + 1] = 0;
|
||||
_lightPositions[i * 3 + 2] = 0;
|
||||
_lightIntensities[i] = 0.0f;
|
||||
_lightColors[i * 3 + 0] = 0;
|
||||
_lightColors[i * 3 + 1] = 0;
|
||||
_lightColors[i * 3 + 2] = 0;
|
||||
_lightTypes[i] = 0;
|
||||
_lightRanges[i] = 0;
|
||||
}
|
||||
|
||||
_lightCount = count;
|
||||
Raylib.SetShaderValue(_shader, _lightCountLoc, count, ShaderUniformDataType.Int);
|
||||
Raylib.SetShaderValueV(_shader, _lightDirLoc, _lightDirs, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightIntensityLoc, _lightIntensities, ShaderUniformDataType.Float, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightColorLoc, _lightColors, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightPosLoc, _lightPositions, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightTypeLoc, _lightTypes, ShaderUniformDataType.Int, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightRangeLoc, _lightRanges, ShaderUniformDataType.Float, 4);
|
||||
}
|
||||
|
||||
private void SetFrameLights()
|
||||
@@ -399,6 +457,81 @@ public sealed class RaylibRenderer : IRenderer
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void DrawFloor(World world, Camera camera)
|
||||
{
|
||||
var floorEntity = world.Lookup("Floor");
|
||||
if ((ulong)floorEntity.Id == 0 || !floorEntity.Has<EngineMesh>())
|
||||
return;
|
||||
|
||||
var floorMesh = floorEntity.Get<EngineMesh>();
|
||||
var floorTransform = floorEntity.Get<EngineTransform>();
|
||||
var floorMaterial = floorEntity.Has<EngineMaterial>() ? floorEntity.Get<EngineMaterial>() : EngineMaterial.Default;
|
||||
var model = GetOrUploadModel(floorEntity, floorMesh);
|
||||
var modelMatrix = floorTransform.GetMatrix();
|
||||
|
||||
if (!Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
|
||||
return;
|
||||
|
||||
var (axis, angle) = QuaternionToAxisAngle(rotation);
|
||||
SetMaterialUniforms(floorMaterial, model);
|
||||
Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White);
|
||||
}
|
||||
|
||||
private void DrawFloorWithShadows(World world, Camera camera)
|
||||
{
|
||||
var floorEntity = world.Lookup("Floor");
|
||||
if ((ulong)floorEntity.Id == 0 || !floorEntity.Has<EngineMesh>())
|
||||
return;
|
||||
|
||||
var floorMesh = floorEntity.Get<EngineMesh>();
|
||||
var floorTransform = floorEntity.Get<EngineTransform>();
|
||||
var floorMaterial = floorEntity.Has<EngineMaterial>() ? floorEntity.Get<EngineMaterial>() : EngineMaterial.Default;
|
||||
var model = GetOrUploadModel(floorEntity, floorMesh);
|
||||
var modelMatrix = floorTransform.GetMatrix();
|
||||
|
||||
if (!Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
|
||||
return;
|
||||
|
||||
var (axis, angle) = QuaternionToAxisAngle(rotation);
|
||||
|
||||
// Light-space matrix
|
||||
var lsMat = new Matrix4x4(
|
||||
_lightSpaceMatrixData[0], _lightSpaceMatrixData[4], _lightSpaceMatrixData[8], _lightSpaceMatrixData[12],
|
||||
_lightSpaceMatrixData[1], _lightSpaceMatrixData[5], _lightSpaceMatrixData[9], _lightSpaceMatrixData[13],
|
||||
_lightSpaceMatrixData[2], _lightSpaceMatrixData[6], _lightSpaceMatrixData[10], _lightSpaceMatrixData[14],
|
||||
_lightSpaceMatrixData[3], _lightSpaceMatrixData[7], _lightSpaceMatrixData[11], _lightSpaceMatrixData[15]);
|
||||
|
||||
// Bind shadow depth texture on Emission slot (texture unit 1)
|
||||
var shadowTex = new Texture2D { Id = _shadowDepthTex, Width = ShadowMapSize, Height = ShadowMapSize, Mipmaps = 1, Format = PixelFormat.UncompressedR16 };
|
||||
unsafe
|
||||
{
|
||||
Raylib.SetMaterialTexture(ref model.Materials[0], MaterialMapIndex.Emission, shadowTex);
|
||||
}
|
||||
|
||||
// Use BeginShaderMode to bind shadow receiver shader
|
||||
Raylib.BeginShaderMode(_shadowReceiverShader);
|
||||
|
||||
// Set all uniforms after BeginShaderMode (changes active program)
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srMaterialColorLoc,
|
||||
new float[] { floorMaterial.Albedo.X, floorMaterial.Albedo.Y, floorMaterial.Albedo.Z, 1.0f }, ShaderUniformDataType.Vec4);
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srRoughnessLoc, floorMaterial.Roughness, ShaderUniformDataType.Float);
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srMetallicLoc, floorMaterial.Metallic, ShaderUniformDataType.Float);
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srAmbientLoc, new float[] { 0.35f, 0.35f, 0.4f }, ShaderUniformDataType.Vec3);
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srViewPosLoc,
|
||||
new float[] { camera.Position.X, camera.Position.Y, camera.Position.Z }, ShaderUniformDataType.Vec3);
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srLightCountLoc, _lightCount, ShaderUniformDataType.Int);
|
||||
Raylib.SetShaderValueV(_shadowReceiverShader, _srLightDirLoc, _lightDirs, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueV(_shadowReceiverShader, _srLightIntensityLoc, _lightIntensities, ShaderUniformDataType.Float, 4);
|
||||
Raylib.SetShaderValueV(_shadowReceiverShader, _srLightColorLoc, _lightColors, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueMatrix(_shadowReceiverShader, _srLightSpaceMatrixLoc, lsMat);
|
||||
|
||||
// Tell shader that shadowMap sampler uses texture unit 1 (Emission slot)
|
||||
Raylib.SetShaderValue(_shadowReceiverShader, _srShadowMapLoc, 1, ShaderUniformDataType.Int);
|
||||
|
||||
Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White);
|
||||
Raylib.EndShaderMode();
|
||||
}
|
||||
|
||||
private void RenderShadowPass(World world)
|
||||
{
|
||||
if (_lightDirs[0] == 0 && _lightDirs[1] == 0 && _lightDirs[2] == 0)
|
||||
@@ -408,7 +541,11 @@ public sealed class RaylibRenderer : IRenderer
|
||||
var sceneCenter = new Vector3(0, 0.5f, 0);
|
||||
var lightPos = sceneCenter - lightDir * 30f;
|
||||
|
||||
var lightView = Matrix4x4.CreateLookAt(lightPos, sceneCenter, Vector3.UnitY);
|
||||
var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f
|
||||
? Vector3.UnitZ
|
||||
: Vector3.UnitY;
|
||||
|
||||
var lightView = Matrix4x4.CreateLookAt(lightPos, sceneCenter, up);
|
||||
var lightProj = Matrix4x4.CreateOrthographic(25f, 25f, 1f, 80f);
|
||||
var lightSpace = lightProj * lightView;
|
||||
|
||||
@@ -422,18 +559,23 @@ public sealed class RaylibRenderer : IRenderer
|
||||
{
|
||||
Position = lightPos,
|
||||
Target = sceneCenter,
|
||||
Up = Vector3.UnitY,
|
||||
Up = up,
|
||||
FovY = 0,
|
||||
Projection = CameraProjection.Orthographic
|
||||
};
|
||||
|
||||
Raylib.BeginTextureMode(_shadowMapRT);
|
||||
Raylib.ClearBackground(new Color(255, 255, 255, 255));
|
||||
// Bind custom depth FBO via rlgl
|
||||
Rlgl.EnableFramebuffer(_shadowFbo);
|
||||
Rlgl.Viewport(0, 0, ShadowMapSize, ShadowMapSize);
|
||||
Rlgl.ClearColor(255, 255, 255, 255);
|
||||
Rlgl.ClearScreenBuffers();
|
||||
|
||||
Raylib.BeginMode3D(shadowCamera);
|
||||
|
||||
world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
|
||||
{
|
||||
if (e.Name() == "Grid")
|
||||
var name = e.Name();
|
||||
if (name == "Grid" || name == "Floor")
|
||||
return;
|
||||
|
||||
var model = GetOrUploadModel(e, mesh);
|
||||
@@ -441,20 +583,8 @@ public sealed class RaylibRenderer : IRenderer
|
||||
|
||||
if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
|
||||
{
|
||||
var axis = Vector3.UnitY;
|
||||
var angle = 0.0f;
|
||||
var q = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
if (MathF.Abs(q.W) < 0.9999999f)
|
||||
{
|
||||
angle = 2.0f * MathF.Acos(Math.Clamp(q.W, -1.0f, 1.0f));
|
||||
var s = MathF.Sqrt(1.0f - q.W * q.W);
|
||||
if (s > 0.0001f)
|
||||
axis = new Vector3(q.X / s, q.Y / s, q.Z / s);
|
||||
else
|
||||
axis = new Vector3(q.X, q.Y, q.Z);
|
||||
}
|
||||
var (axis, angle) = QuaternionToAxisAngle(rotation);
|
||||
|
||||
// Swap to shadow shader, draw, swap back
|
||||
unsafe
|
||||
{
|
||||
var origShader = model.Materials[0].Shader;
|
||||
@@ -466,7 +596,10 @@ public sealed class RaylibRenderer : IRenderer
|
||||
});
|
||||
|
||||
Raylib.EndMode3D();
|
||||
Raylib.EndTextureMode();
|
||||
|
||||
// Unbind FBO, restore viewport to screen
|
||||
Rlgl.DisableFramebuffer();
|
||||
Rlgl.Viewport(0, 0, Raylib.GetScreenWidth(), Raylib.GetScreenHeight());
|
||||
}
|
||||
|
||||
private static Shader LoadShadowShader()
|
||||
@@ -474,20 +607,130 @@ public sealed class RaylibRenderer : IRenderer
|
||||
const string VertexSource = @"#version 330 core
|
||||
in vec3 vertexPosition;
|
||||
uniform mat4 mvp;
|
||||
uniform mat4 matModel;
|
||||
out vec3 vWorldPos;
|
||||
void main()
|
||||
{
|
||||
vec4 worldPos = matModel * vec4(vertexPosition, 1.0);
|
||||
vWorldPos = worldPos.xyz;
|
||||
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
||||
}";
|
||||
|
||||
const string FragmentSource = @"#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main() {}";
|
||||
|
||||
return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shadow receiver shader — used only for the floor.
|
||||
/// Samples the shadow map from texture0 (bound via SetMaterialTexture).
|
||||
/// Has its own uniform layout so it doesn't affect the main shader.
|
||||
/// </summary>
|
||||
private static Shader LoadShadowReceiverShader()
|
||||
{
|
||||
const string VertexSource = @"#version 330 core
|
||||
in vec3 vertexPosition;
|
||||
in vec2 vertexTexCoord;
|
||||
in vec3 vertexNormal;
|
||||
in vec4 vertexColor;
|
||||
uniform mat4 mvp;
|
||||
uniform mat4 matModel;
|
||||
out vec3 vNormal;
|
||||
out vec3 vWorldPos;
|
||||
out vec4 vColor;
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(vec3(gl_FragCoord.z), 1.0);
|
||||
vec4 worldPos = matModel * vec4(vertexPosition, 1.0);
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = mat3(transpose(inverse(matModel))) * vertexNormal;
|
||||
vColor = vertexColor;
|
||||
vTexCoord = vertexTexCoord;
|
||||
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
||||
}";
|
||||
|
||||
const string FragmentSource = @"#version 330 core
|
||||
in vec3 vNormal;
|
||||
in vec3 vWorldPos;
|
||||
in vec4 vColor;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 finalColor;
|
||||
uniform vec4 materialColor;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D shadowMap;
|
||||
uniform float roughness;
|
||||
uniform float metallic;
|
||||
uniform vec3 viewPos;
|
||||
uniform vec3 ambientColor;
|
||||
uniform int lightCount;
|
||||
uniform vec3 lightDirs[4];
|
||||
uniform float lightIntensities[4];
|
||||
uniform vec3 lightColors[4];
|
||||
uniform mat4 lightSpaceMatrix;
|
||||
|
||||
vec3 ACESFilm(vec3 x)
|
||||
{
|
||||
const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float CalculateShadow(vec3 worldPos)
|
||||
{
|
||||
vec4 lp = lightSpaceMatrix * vec4(worldPos, 1.0);
|
||||
vec3 ndc = lp.xyz / lp.w;
|
||||
vec3 uvw = ndc * 0.5 + 0.5;
|
||||
if (uvw.x < 0.0 || uvw.x > 1.0 || uvw.y < 0.0 || uvw.y > 1.0 || uvw.z > 1.0)
|
||||
return 1.0;
|
||||
float bias = 0.003;
|
||||
vec2 ts = 1.0 / 1024.0;
|
||||
float s = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
float d = texture(shadowMap, uvw.xy + vec2(x, y) * ts).r;
|
||||
s += (uvw.z - bias > d) ? 0.3 : 1.0;
|
||||
}
|
||||
}
|
||||
return s / 9.0;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 normal = normalize(vNormal);
|
||||
// Convert sRGB vertex color to linear before lighting
|
||||
vec3 albedo = pow(vColor.rgb * materialColor.rgb, vec3(2.2));
|
||||
vec3 viewDir = normalize(viewPos - vWorldPos);
|
||||
float rough = clamp(roughness, 0.05, 1.0);
|
||||
float metal = clamp(metallic, 0.0, 1.0);
|
||||
|
||||
vec3 skyColor = ambientColor;
|
||||
vec3 groundColor = ambientColor * 0.2;
|
||||
float hemisphere = 0.5 + 0.5 * normal.y;
|
||||
vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4;
|
||||
|
||||
float shadow = CalculateShadow(vWorldPos);
|
||||
|
||||
// F0 as vec3: dielectric 0.04, metals use albedo
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metal);
|
||||
float shininess = mix(8.0, 256.0, 1.0 - rough);
|
||||
|
||||
for (int i = 0; i < lightCount; i++)
|
||||
{
|
||||
vec3 L = normalize(-lightDirs[i]);
|
||||
vec3 H = normalize(L + viewDir);
|
||||
float NdotL = max(dot(normal, L), 0.0);
|
||||
float NdotH = max(dot(normal, H), 0.0);
|
||||
float HdotV = max(dot(H, viewDir), 0.0);
|
||||
float diff = NdotL;
|
||||
float spec = pow(NdotH, shininess);
|
||||
// Schlick Fresnel using F0.rgb
|
||||
vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0);
|
||||
vec3 specularColor = mix(fresnel, albedo * fresnel, metal);
|
||||
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * 1.5 * shadow;
|
||||
vec3 specular = specularColor * spec * lightIntensities[i] * shadow;
|
||||
diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5));
|
||||
result += diffuse + specular;
|
||||
}
|
||||
|
||||
result = ACESFilm(result * 1.2);
|
||||
result = pow(result, vec3(1.0 / 2.2));
|
||||
finalColor = vec4(result, 1.0);
|
||||
}";
|
||||
|
||||
return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource);
|
||||
@@ -531,8 +774,11 @@ uniform vec3 viewPos;
|
||||
uniform vec3 ambientColor;
|
||||
uniform int lightCount;
|
||||
uniform vec3 lightDirs[4];
|
||||
uniform vec3 lightPositions[4];
|
||||
uniform float lightIntensities[4];
|
||||
uniform vec3 lightColors[4];
|
||||
uniform int lightTypes[4];
|
||||
uniform float lightRanges[4];
|
||||
|
||||
vec3 ACESFilm(vec3 x)
|
||||
{
|
||||
@@ -540,14 +786,28 @@ vec3 ACESFilm(vec3 x)
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Smooth point light attenuation (Unity-like)
|
||||
float Attenuation(float dist, float range)
|
||||
{
|
||||
float r = max(range, 0.001);
|
||||
float d = max(dist, 0.001);
|
||||
float x = d / r;
|
||||
float x2 = x * x;
|
||||
float x4 = x2 * x2;
|
||||
return clamp(1.0 / (1.0 + 25.0 * x4), 0.0, 1.0) * smoothstep(1.0, 0.0, x);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 normal = normalize(vNormal);
|
||||
vec3 albedo = vColor.rgb * materialColor.rgb;
|
||||
// Convert sRGB vertex color + material color to linear before lighting
|
||||
vec3 albedo = pow(vColor.rgb * materialColor.rgb, vec3(2.2));
|
||||
if (useTexture != 0)
|
||||
{
|
||||
vec2 uv = vTexCoord * 4.0;
|
||||
albedo *= texture(texture0, uv).rgb;
|
||||
// Texture is already in sRGB — convert to linear
|
||||
vec3 texColor = pow(texture(texture0, uv).rgb, vec3(2.2));
|
||||
albedo *= texColor;
|
||||
}
|
||||
|
||||
vec3 viewDir = normalize(viewPos - vWorldPos);
|
||||
@@ -559,22 +819,38 @@ void main()
|
||||
float hemisphere = 0.5 + 0.5 * normal.y;
|
||||
vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4;
|
||||
|
||||
// F0 as vec3: dielectric 0.04, metals use albedo
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metal);
|
||||
float shininess = mix(8.0, 256.0, 1.0 - rough);
|
||||
|
||||
for (int i = 0; i < lightCount; i++)
|
||||
{
|
||||
vec3 L = normalize(-lightDirs[i]);
|
||||
vec3 L;
|
||||
float atten = 1.0;
|
||||
|
||||
if (lightTypes[i] == 1) // Point light
|
||||
{
|
||||
vec3 toLight = lightPositions[i] - vWorldPos;
|
||||
float dist = length(toLight);
|
||||
L = toLight / max(dist, 0.001);
|
||||
atten = Attenuation(dist, lightRanges[i]);
|
||||
}
|
||||
else // Directional light
|
||||
{
|
||||
L = normalize(-lightDirs[i]);
|
||||
}
|
||||
|
||||
vec3 H = normalize(L + viewDir);
|
||||
float NdotL = max(dot(normal, L), 0.0);
|
||||
float NdotH = max(dot(normal, H), 0.0);
|
||||
float HdotV = max(dot(H, viewDir), 0.0);
|
||||
float diff = NdotL;
|
||||
float spec = pow(NdotH, shininess);
|
||||
float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - HdotV, 5.0);
|
||||
vec3 specularColor = mix(vec3(fresnel), albedo * fresnel, metal);
|
||||
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * 1.5;
|
||||
vec3 specular = specularColor * spec * lightIntensities[i];
|
||||
// Schlick Fresnel using F0.rgb (per-channel)
|
||||
vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0);
|
||||
vec3 specularColor = mix(fresnel, albedo * fresnel, metal);
|
||||
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * atten * 1.5;
|
||||
vec3 specular = specularColor * spec * lightIntensities[i] * atten;
|
||||
diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5));
|
||||
result += diffuse + specular;
|
||||
}
|
||||
@@ -600,8 +876,10 @@ void main()
|
||||
Raylib.UnloadTexture(texture);
|
||||
_textureCache.Clear();
|
||||
|
||||
Raylib.UnloadRenderTexture(_shadowMapRT);
|
||||
Rlgl.UnloadTexture(_shadowDepthTex);
|
||||
Rlgl.UnloadFramebuffer(_shadowFbo);
|
||||
Raylib.UnloadShader(_shadowShader);
|
||||
Raylib.UnloadShader(_shadowReceiverShader);
|
||||
Raylib.UnloadShader(_shader);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ public static class MeshMath
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
// Cross(ac, ab) instead of Cross(ab, ac) to match CW winding in typical OBJ files
|
||||
var normal = Vector3.Cross(ac, ab);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
|
||||
@@ -74,9 +74,12 @@ public static class SceneSerializer
|
||||
var l = e.Get<Light>();
|
||||
entry.Light = new SceneLight
|
||||
{
|
||||
Type = l.Type,
|
||||
Direction = l.Direction,
|
||||
Position = l.Position,
|
||||
Color = l.Color,
|
||||
Intensity = l.Intensity
|
||||
Intensity = l.Intensity,
|
||||
Range = l.Range
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,10 +147,21 @@ public static class SceneSerializer
|
||||
|
||||
if (entry.Light != null)
|
||||
{
|
||||
entity.Set(new Light(
|
||||
entry.Light.Direction,
|
||||
entry.Light.Color,
|
||||
entry.Light.Intensity));
|
||||
if (entry.Light.Type == LightType.Point)
|
||||
{
|
||||
entity.Set(Light.Point(
|
||||
entry.Light.Position,
|
||||
entry.Light.Color,
|
||||
entry.Light.Intensity,
|
||||
entry.Light.Range));
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Set(Light.Directional(
|
||||
entry.Light.Direction,
|
||||
entry.Light.Color,
|
||||
entry.Light.Intensity));
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.Camera != null)
|
||||
@@ -208,9 +222,12 @@ internal sealed class SceneMaterial
|
||||
|
||||
internal sealed class SceneLight
|
||||
{
|
||||
public LightType Type { get; set; }
|
||||
public Vector3 Direction { get; set; }
|
||||
public Vector3 Position { get; set; }
|
||||
public Vector3 Color { get; set; }
|
||||
public float Intensity { get; set; }
|
||||
public float Range { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class SceneCamera
|
||||
|
||||
@@ -25,7 +25,7 @@ public class MeshAndLightTests
|
||||
[Fact]
|
||||
public void Light_Direction_Is_Normalized()
|
||||
{
|
||||
var light = new Light(new Vector3(0, 2, 0), Vector3.One, 1.0f);
|
||||
var light = Light.Directional(new Vector3(0, 2, 0), Vector3.One, 1.0f);
|
||||
|
||||
Assert.Equal(1f, light.Direction.Length(), 0.001f);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class MeshAndLightTests
|
||||
[Fact]
|
||||
public void Light_With_Zero_Direction_Defaults_To_UnitY()
|
||||
{
|
||||
var light = new Light(Vector3.Zero, Vector3.One, 1.0f);
|
||||
var light = Light.Directional(Vector3.Zero, Vector3.One, 1.0f);
|
||||
|
||||
Assert.Equal(Vector3.UnitY, light.Direction);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ public class MeshMathTests
|
||||
new Vector3(1, 0, 0),
|
||||
new Vector3(0, 1, 0));
|
||||
|
||||
// CW winding (typical OBJ) → normal points -Z
|
||||
Assert.Equal(0f, n.X, 0.001f);
|
||||
Assert.Equal(0f, n.Y, 0.001f);
|
||||
Assert.Equal(1f, n.Z, 0.001f);
|
||||
Assert.Equal(-1f, n.Z, 0.001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -97,9 +97,10 @@ public class ObjLoaderTests
|
||||
var mesh = ObjLoader.Load(path);
|
||||
|
||||
var normal = mesh.Vertices[0].Normal;
|
||||
// CW winding → normal points -Z
|
||||
Assert.Equal(0f, normal.X, 0.001f);
|
||||
Assert.Equal(0f, normal.Y, 0.001f);
|
||||
Assert.Equal(1f, normal.Z, 0.001f);
|
||||
Assert.Equal(-1f, normal.Z, 0.001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user