diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index d11f716..0dc7c94 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -264,6 +264,10 @@ class Program .Set(new Transform(Vector3.Zero, Quaternion.Identity, Vector3.One)) .Set(grid); + world.Entity("MainLight") + .Set(new Transform(new Vector3(0, 8, 0), Quaternion.Identity, Vector3.One)) + .Set(Light.Point(new Vector3(0, 8, 0), new Vector3(1.0f, 0.9f, 0.7f), intensity: 15.0f, range: 25.0f)); + var entityCount = 0; world.Each((Entity e, ref Transform _) => entityCount++); Console.WriteLine($"[Scene] {entityCount} entities: torus knot + 4 dynamic cubes + 3 dynamic spheres + static floor + grid"); diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag index 6995f34..4f25879 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag @@ -6,9 +6,15 @@ layout(location = 2) in vec3 fragAlbedo; layout(location = 0) out vec4 outColor; -const vec3 LIGHT_DIR = normalize(vec3(0.5, 0.8, 0.3)); -const vec3 LIGHT_COLOR = vec3(1.0, 0.95, 0.85); -const vec3 AMBIENT = vec3(0.15, 0.18, 0.22); +layout(set = 0, binding = 0) uniform CameraUBO { + mat4 vp; + vec4 pointLightPos; // xyz = position, w = intensity + vec4 pointLightColor; // xyz = color, w = range +}; + +const vec3 DIR_LIGHT_DIR = normalize(vec3(0.5, 0.8, 0.3)); +const vec3 DIR_LIGHT_COLOR = vec3(0.4, 0.38, 0.33); +const vec3 AMBIENT = vec3(0.08, 0.09, 0.12); const float PI = 3.14159265359; @@ -35,9 +41,7 @@ float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); - float ggx2 = geometrySchlickGGX(NdotV, roughness); - float ggx1 = geometrySchlickGGX(NdotL, roughness); - return ggx2 * ggx1; + return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness); } vec3 fresnelSchlick(float cosTheta, vec3 F0) @@ -55,19 +59,10 @@ vec3 acesTonemap(vec3 color) return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0); } -void main() +vec3 calcLight(vec3 N, vec3 V, vec3 L, vec3 radiance, vec3 albedo, float roughness, float metallic) { - vec3 N = normalize(fragNormal); - vec3 V = normalize(-fragWorldPos); - - vec3 albedo = fragAlbedo; - float roughness = 0.5; - float metallic = 0.1; - - vec3 F0 = mix(vec3(0.04), albedo, metallic); - - vec3 L = LIGHT_DIR; vec3 H = normalize(V + L); + vec3 F0 = mix(vec3(0.04), albedo, metallic); float NDF = distributionGGX(N, H, roughness); float G = geometrySmith(N, V, L, roughness); @@ -81,11 +76,44 @@ void main() vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic); float NdotL = max(dot(N, L), 0.0); - vec3 Lo = (kD * albedo / PI + specular) * LIGHT_COLOR * NdotL; + return (kD * albedo / PI + specular) * radiance * NdotL; +} - vec3 ambient = AMBIENT * albedo; - vec3 color = ambient + Lo; +void main() +{ + vec3 N = normalize(fragNormal); + vec3 V = normalize(-fragWorldPos); + vec3 albedo = fragAlbedo; + float roughness = 0.5; + float metallic = 0.1; + + // Directional light (sun) + vec3 color = calcLight(N, V, DIR_LIGHT_DIR, DIR_LIGHT_COLOR, albedo, roughness, metallic); + + // Point light + vec3 lightPos = pointLightPos.xyz; + float lightIntensity = pointLightPos.w; + vec3 lightColor = pointLightColor.xyz; + float lightRange = pointLightColor.w; + + vec3 toLight = lightPos - fragWorldPos; + float dist = length(toLight); + vec3 L = toLight / max(dist, 0.001); + + // Unity-style attenuation: smooth falloff at range edge + float attenuation = pow(clamp(1.0 - dist / lightRange, 0.0, 1.0), 2.0); + vec3 radiance = lightColor * lightIntensity * attenuation; + + if (lightIntensity > 0.0 && lightRange > 0.0) + { + color += calcLight(N, V, L, radiance, albedo, roughness, metallic); + } + + // Ambient + color += AMBIENT * albedo; + + // Tonemap + gamma color = acesTonemap(color); color = pow(color, vec3(1.0 / 2.2)); diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv index 33d1e5f..b37f972 100644 Binary files a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv and b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv differ diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert index e19ff90..e59270c 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert @@ -10,6 +10,8 @@ layout(location = 2) out vec3 fragAlbedo; layout(set = 0, binding = 0) uniform CameraUBO { mat4 vp; + vec4 pointLightPos; // xyz = position, w = intensity + vec4 pointLightColor; // xyz = color, w = range }; layout(push_constant) uniform PC { diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv index f511e34..658f908 100644 Binary files a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv and b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv differ diff --git a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs index 229f830..a203f0f 100644 --- a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs +++ b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs @@ -5,7 +5,7 @@ namespace Engine.Graphics.Vulkan; internal sealed unsafe class VulkanFrameResources : IDisposable { public const int MaxFramesInFlight = 2; - public const ulong UboSize = 64; + public const ulong UboSize = 128; public VkCommandPool CommandPool; public VkCommandBuffer[] CommandBuffers = new VkCommandBuffer[MaxFramesInFlight]; diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index 647282d..285c7d8 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -77,6 +77,24 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen vp = Matrix4x4.CreateLookAt(new Vector3(0, 0, -6), Vector3.Zero, Vector3.UnitY) * proj; } + // Collect point light from ECS (first point light found) + var lightPos = new Vector4(0, 5, 0, 0); + var lightColor = new Vector4(1, 1, 1, 0); + world.Each((Entity e, ref Light l) => + { + if (l.IsPoint && lightPos.W == 0) + { + lightPos = new Vector4(l.Position, l.Intensity); + lightColor = new Vector4(l.Color, l.Range); + } + }); + + // Pack UBO: mat4 vp (64 bytes) + vec4 lightPos (16) + vec4 lightColor (16) = 96 bytes + var uboData = stackalloc byte[128]; + Marshal.StructureToPtr(vp, (nint)uboData, false); + *(Vector4*)(uboData + 64) = lightPos; + *(Vector4*)(uboData + 80) = lightColor; + var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)>(); world.Each((Entity e, ref Transform t, ref Mesh m) => @@ -95,10 +113,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix())); }); - Render(vp, drawCalls); + Render(vp, drawCalls, uboData); } - private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls) + private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls, byte* uboData) { _frameResources.WaitFrame(_frameIndex); @@ -110,7 +128,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen { _swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width, _ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height); - Render(vp, drawCalls); + Render(vp, drawCalls, uboData); return; } @@ -119,7 +137,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _totalTime += 0.016f; - _frameResources.UpdateUbo(_frameIndex, &vp, VulkanFrameResources.UboSize); + _frameResources.UpdateUbo(_frameIndex, uboData, VulkanFrameResources.UboSize); var cmd = _frameResources.CommandBuffers[_frameIndex]; Vk.vkResetCommandBuffer(cmd, 0);