From f718e6de495eba454359fba3cea68486a62f7864 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 19:23:00 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20PBR=20point=20light=20=E2=80=94=20Unity?= =?UTF-8?q?-style=20attenuation,=20warm=20glow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UBO expanded from 64 to 128 bytes: vp(64) + lightPos(16) + lightColor(16) - Vertex shader: passes point light UBO data through - Fragment shader: refactored calcLight() function, shared by directional + point - Point light: Unity-style attenuation pow(1 - dist/range, 2) - Directional light reduced intensity (0.4) to balance with point light - Point light: position (0,8,0), warm color (1,0.9,0.7), intensity 15, range 25 - Renderer: reads Light component from ECS, packs into UBO with Marshal.StructureToPtr - Scene: MainLight entity with Light.Point at (0,8,0) - 0 C# struct changes — pure UBO layout + shader upgrade --- src/CortexEngine.App/Program.cs | 4 ++ .../Shaders/triangle.frag | 68 ++++++++++++------ .../Shaders/triangle.frag.spv | Bin 7392 -> 10104 bytes .../Shaders/triangle.vert | 2 + .../Shaders/triangle.vert.spv | Bin 2080 -> 2184 bytes .../VulkanFrameResources.cs | 2 +- src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 26 +++++-- 7 files changed, 77 insertions(+), 25 deletions(-) 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 33d1e5f33a14ce1451c4453578a492deff583b24..b37f97236bc490e72f196c7c430855174ddce26c 100644 GIT binary patch literal 10104 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 literal 7392 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 + { + 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);