diff --git a/Readme.md b/Readme.md index 3949635..401d179 100644 --- a/Readme.md +++ b/Readme.md @@ -10,10 +10,10 @@ Current structure: - GLTF loading and rendering, primitive creation and rendering. - Supports texture compression(BCn, non glTF standard), LRU reload - Object clicking, generation. +- Multi light system Work-In-Progress - [ ] TAA -- [ ] Multiple light - [ ] SSR - [ ] bloom - [ ] Planet Rendering diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 9b1bb98..b87292d 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -3,6 +3,7 @@ #extension GL_EXT_ray_query : require #include "input_structures.glsl" #include "ibl_common.glsl" +#include "lighting_common.glsl" layout(location=0) in vec2 inUV; layout(location=0) out vec4 outColor; @@ -33,8 +34,6 @@ const float SHADOW_MIN_BIAS = 1e-5; const float SHADOW_RAY_TMIN = 0.02;// start a bit away from the surface const float SHADOW_RAY_ORIGIN_BIAS = 0.01;// world units -const float PI = 3.14159265359; - float hash12(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); @@ -263,41 +262,6 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) return vis; } -vec3 fresnelSchlick(float cosTheta, vec3 F0) -{ - return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); -} - -float DistributionGGX(vec3 N, vec3 H, float roughness) -{ - float a = roughness * roughness; - float a2 = a * a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH * NdotH; - - float num = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; - - return num / max(denom, 0.001); -} - -float GeometrySchlickGGX(float NdotV, float roughness) -{ - float r = (roughness + 1.0); - float k = (r * r) / 8.0; - - float denom = NdotV * (1.0 - k) + k; - return NdotV / max(denom, 0.001); -} - -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) -{ - float ggx2 = GeometrySchlickGGX(max(dot(N, V), 0.0), roughness); - float ggx1 = GeometrySchlickGGX(max(dot(N, L), 0.0), roughness); - return ggx1 * ggx2; -} - void main(){ vec4 posSample = texture(posTex, inUV); if (posSample.w == 0.0) @@ -317,28 +281,53 @@ void main(){ vec3 camPos = vec3(inverse(sceneData.view)[3]); vec3 V = normalize(camPos - pos); - vec3 L = normalize(-sceneData.sunlightDirection.xyz); - vec3 H = normalize(V + L); - vec3 F0 = mix(vec3(0.04), albedo, metallic); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - float NDF = DistributionGGX(N, H, roughness); - float G = GeometrySmith(N, V, L, roughness); + // Directional sun term using evaluate_brdf + cascaded shadowing + vec3 Lsun = normalize(-sceneData.sunlightDirection.xyz); + float sunVis = calcShadowVisibility(pos, N, Lsun); + vec3 sunBRDF = evaluate_brdf(N, V, Lsun, albedo, roughness, metallic); + vec3 direct = sunBRDF * sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * sunVis; - vec3 numerator = NDF * G * F; - float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); - vec3 specular = numerator / max(denom, 0.001); + // Punctual point lights + uint pointCount = sceneData.lightCounts.x; + for (uint i = 0u; i < pointCount; ++i) + { + vec3 contrib = eval_point_light(sceneData.punctualLights[i], pos, N, V, albedo, roughness, metallic); - vec3 kS = F; - vec3 kD = (1.0 - kS) * (1.0 - metallic); + // Optional RT shadow for the first few point lights (hybrid mode) + #ifdef GL_EXT_ray_query + if (sceneData.rtOptions.x == 1u && i < 4u) + { + vec3 toL = sceneData.punctualLights[i].position_radius.xyz - pos; + float maxT = length(toL); + if (maxT > 0.01) + { + vec3 dir = toL / maxT; + vec3 origin = pos + N * SHADOW_RAY_ORIGIN_BIAS; - float NdotL = max(dot(N, L), 0.0); - // Shadowing (directional, forward-Z shadow map) - float visibility = calcShadowVisibility(pos, N, L); + rayQueryEXT rq; + rayQueryInitializeEXT( + rq, + topLevelAS, + gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT, + 0xFF, + origin, + SHADOW_RAY_TMIN, + dir, + maxT + ); + while (rayQueryProceedEXT(rq)) { } + bool hit = (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT); + if (hit) + { + contrib = vec3(0.0); + } + } + } + #endif - vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility; - - vec3 color = (kD * albedo / PI + specular) * irradiance; + direct += contrib; + } // Image-Based Lighting: split-sum approximation vec3 R = reflect(-V, N); @@ -347,9 +336,11 @@ void main(){ vec2 uv = dir_to_equirect(R); vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); - color += diffIBL + specIBL; + + vec3 color = direct + diffIBL + specIBL; outColor = vec4(color, 1.0); } diff --git a/shaders/deferred_lighting_nort.frag b/shaders/deferred_lighting_nort.frag index 804199f..5215297 100644 --- a/shaders/deferred_lighting_nort.frag +++ b/shaders/deferred_lighting_nort.frag @@ -2,6 +2,7 @@ #extension GL_GOOGLE_include_directive : require #include "input_structures.glsl" #include "ibl_common.glsl" +#include "lighting_common.glsl" layout(location=0) in vec2 inUV; layout(location=0) out vec4 outColor; @@ -25,8 +26,6 @@ const float SHADOW_RPDB_SCALE = 1.0; // Minimum clamp to keep a tiny bias even on perpendicular receivers const float SHADOW_MIN_BIAS = 1e-5; -const float PI = 3.14159265359; - float hash12(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); @@ -192,41 +191,6 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) return mix(v0, v1, clamp(cm.w1, 0.0, 1.0)); } -vec3 fresnelSchlick(float cosTheta, vec3 F0) -{ - return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); -} - -float DistributionGGX(vec3 N, vec3 H, float roughness) -{ - float a = roughness * roughness; - float a2 = a * a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH * NdotH; - - float num = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; - - return num / max(denom, 0.001); -} - -float GeometrySchlickGGX(float NdotV, float roughness) -{ - float r = (roughness + 1.0); - float k = (r * r) / 8.0; - - float denom = NdotV * (1.0 - k) + k; - return NdotV / max(denom, 0.001); -} - -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) -{ - float ggx2 = GeometrySchlickGGX(max(dot(N, V), 0.0), roughness); - float ggx1 = GeometrySchlickGGX(max(dot(N, L), 0.0), roughness); - return ggx1 * ggx2; -} - void main(){ vec4 posSample = texture(posTex, inUV); if (posSample.w == 0.0) @@ -246,28 +210,19 @@ void main(){ vec3 camPos = vec3(inverse(sceneData.view)[3]); vec3 V = normalize(camPos - pos); - vec3 L = normalize(-sceneData.sunlightDirection.xyz); - vec3 H = normalize(V + L); - vec3 F0 = mix(vec3(0.04), albedo, metallic); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - float NDF = DistributionGGX(N, H, roughness); - float G = GeometrySmith(N, V, L, roughness); + // Directional sun term using evaluate_brdf + cascaded shadowing + vec3 Lsun = normalize(-sceneData.sunlightDirection.xyz); + float sunVis = calcShadowVisibility(pos, N, Lsun); + vec3 sunBRDF = evaluate_brdf(N, V, Lsun, albedo, roughness, metallic); + vec3 direct = sunBRDF * sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * sunVis; - vec3 numerator = NDF * G * F; - float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); - vec3 specular = numerator / max(denom, 0.001); - - vec3 kS = F; - vec3 kD = (1.0 - kS) * (1.0 - metallic); - - float NdotL = max(dot(N, L), 0.0); - // Shadowing (directional, forward-Z shadow map) - float visibility = calcShadowVisibility(pos, N, L); - - vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility; - - vec3 color = (kD * albedo / PI + specular) * irradiance; + // Punctual point lights + uint pointCount = sceneData.lightCounts.x; + for (uint i = 0u; i < pointCount; ++i) + { + direct += eval_point_light(sceneData.punctualLights[i], pos, N, V, albedo, roughness, metallic); + } // Image-Based Lighting: split-sum approximation vec3 R = reflect(-V, N); @@ -276,9 +231,11 @@ void main(){ vec2 uv = dir_to_equirect(R); vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); - color += diffIBL + specIBL; + + vec3 color = direct + diffIBL + specIBL; outColor = vec4(color, 1.0); } diff --git a/shaders/input_structures.glsl b/shaders/input_structures.glsl index bf38e79..abf20ce 100644 --- a/shaders/input_structures.glsl +++ b/shaders/input_structures.glsl @@ -1,5 +1,12 @@ // Maximum number of shadow cascades supported in shaders #define MAX_CASCADES 4 +// Maximum number of punctual (point) lights +#define MAX_PUNCTUAL_LIGHTS 64 + +struct GPUPunctualLight { + vec4 position_radius; + vec4 color_intensity; +}; layout(set = 0, binding = 0) uniform SceneData{ @@ -22,6 +29,9 @@ layout(set = 0, binding = 0) uniform SceneData{ uvec4 rtOptions; // rtParams.x = N·L threshold; others reserved vec4 rtParams; + + GPUPunctualLight punctualLights[MAX_PUNCTUAL_LIGHTS]; + uvec4 lightCounts; } sceneData; layout(set = 1, binding = 0) uniform GLTFMaterialData{ diff --git a/shaders/lighting_common.glsl b/shaders/lighting_common.glsl new file mode 100644 index 0000000..1159922 --- /dev/null +++ b/shaders/lighting_common.glsl @@ -0,0 +1,86 @@ +#ifndef LIGHTING_COMMON_GLSL +#define LIGHTING_COMMON_GLSL + +const float PI = 3.14159265359; + +vec3 fresnelSchlick(float cosTheta, vec3 F0) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +float DistributionGGX(vec3 N, vec3 H, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + + float num = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return num / max(denom, 0.001); +} + +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float r = (roughness + 1.0); + float k = (r * r) / 8.0; + + float denom = NdotV * (1.0 - k) + k; + return NdotV / max(denom, 0.001); +} + +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +{ + float ggx2 = GeometrySchlickGGX(max(dot(N, V), 0.0), roughness); + float ggx1 = GeometrySchlickGGX(max(dot(N, L), 0.0), roughness); + return ggx1 * ggx2; +} + +vec3 evaluate_brdf(vec3 N, vec3 V, vec3 L, vec3 albedo, float roughness, float metallic) +{ + vec3 H = normalize(V + L); + + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + float NDF = DistributionGGX(N, H, roughness); + float G = GeometrySmith(N, V, L, roughness); + + vec3 numerator = NDF * G * F; + float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); + vec3 specular = numerator / max(denom, 0.001); + + vec3 kS = F; + vec3 kD = (1.0 - kS) * (1.0 - metallic); + + float NdotL = max(dot(N, L), 0.0); + return (kD * albedo / PI + specular) * NdotL; +} + +vec3 eval_point_light(GPUPunctualLight light, vec3 pos, vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) +{ + vec3 lightPos = light.position_radius.xyz; + float radius = max(light.position_radius.w, 0.0001); + vec3 L = lightPos - pos; + float dist = length(L); + if (dist <= 0.0001) + { + return vec3(0.0); + } + L /= dist; + + // Smooth falloff: inverse-square with soft clamp at radius + float att = 1.0 / max(dist * dist, 0.0001); + float x = clamp(dist / radius, 0.0, 1.0); + float smth = (1.0 - x * x); + smth *= smth; + float falloff = att * smth; + + vec3 brdf = evaluate_brdf(N, V, L, albedo, roughness, metallic); + vec3 lightColor = light.color_intensity.rgb * light.color_intensity.a; + return brdf * lightColor * falloff; +} + +#endif // LIGHTING_COMMON_GLSL + diff --git a/shaders/mesh.frag b/shaders/mesh.frag index d0a23f5..4d7a68d 100644 --- a/shaders/mesh.frag +++ b/shaders/mesh.frag @@ -3,6 +3,7 @@ #extension GL_GOOGLE_include_directive : require #include "input_structures.glsl" #include "ibl_common.glsl" +#include "lighting_common.glsl" layout (location = 0) in vec3 inNormal; layout (location = 1) in vec3 inColor; @@ -12,43 +13,6 @@ layout (location = 4) in vec4 inTangent; layout (location = 0) out vec4 outFragColor; -const float PI = 3.14159265359; - -vec3 fresnelSchlick(float cosTheta, vec3 F0) -{ - return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); -} - -float DistributionGGX(vec3 N, vec3 H, float roughness) -{ - float a = roughness * roughness; - float a2 = a * a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH * NdotH; - - float num = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; - - return num / max(denom, 0.001); -} - -float GeometrySchlickGGX(float NdotV, float roughness) -{ - float r = (roughness + 1.0); - float k = (r * r) / 8.0; - - float denom = NdotV * (1.0 - k) + k; - return NdotV / denom; -} - -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) -{ - float ggx2 = GeometrySchlickGGX(max(dot(N, V), 0.0), roughness); - float ggx1 = GeometrySchlickGGX(max(dot(N, L), 0.0), roughness); - return ggx1 * ggx2; -} - void main() { // Base color with material factor and texture @@ -73,26 +37,18 @@ void main() vec3 N = normalize(T * Nm.x + B * Nm.y + Nn * Nm.z); vec3 camPos = vec3(inverse(sceneData.view)[3]); vec3 V = normalize(camPos - inWorldPos); - vec3 L = normalize(-sceneData.sunlightDirection.xyz); - vec3 H = normalize(V + L); - vec3 F0 = mix(vec3(0.04), albedo, metallic); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - float NDF = DistributionGGX(N, H, roughness); - float G = GeometrySmith(N, V, L, roughness); + // Directional sun term (no shadows in forward path) + vec3 Lsun = normalize(-sceneData.sunlightDirection.xyz); + vec3 sunBRDF = evaluate_brdf(N, V, Lsun, albedo, roughness, metallic); + vec3 direct = sunBRDF * sceneData.sunlightColor.rgb * sceneData.sunlightColor.a; - vec3 numerator = NDF * G * F; - float denom = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); - vec3 specular = numerator / max(denom, 0.001); - - vec3 kS = F; - vec3 kD = vec3(1.0) - kS; - kD *= 1.0 - metallic; - - float NdotL = max(dot(N, L), 0.0); - vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL; - - vec3 color = (kD * albedo / PI + specular) * irradiance; + // Punctual point lights + uint pointCount = sceneData.lightCounts.x; + for (uint i = 0u; i < pointCount; ++i) + { + direct += eval_point_light(sceneData.punctualLights[i], inWorldPos, N, V, albedo, roughness, metallic); + } // IBL: specular from equirect 2D mips; diffuse from SH vec3 R = reflect(-V, N); @@ -101,9 +57,11 @@ void main() vec2 uv = dir_to_equirect(R); vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); - color += diffIBL + specIBL; + + vec3 color = direct + diffIBL + specIBL; // Alpha from baseColor texture and factor (glTF spec) float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0); diff --git a/src/core/vk_types.h b/src/core/vk_types.h index 9187e59..29911cc 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -70,6 +70,13 @@ struct AllocatedBuffer { VmaAllocationInfo info; }; +struct GPUPunctualLight { + glm::vec4 position_radius; + glm::vec4 color_intensity; +}; + +static constexpr uint32_t kMaxPunctualLights = 64; + struct GPUSceneData { glm::mat4 view; glm::mat4 proj; @@ -84,6 +91,9 @@ struct GPUSceneData { // Hybrid ray-query options (match shaders/input_structures.glsl) glm::uvec4 rtOptions; // x: enabled (1/0), y: cascade mask, z,w: reserved glm::vec4 rtParams; // x: N·L threshold, yzw: reserved + + GPUPunctualLight punctualLights[kMaxPunctualLights]; + glm::uvec4 lightCounts; }; enum class MaterialPass :uint8_t { diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index ae385fc..5b48099 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -27,6 +27,16 @@ SceneManager::~SceneManager() pendingGLTFRelease.size()); } +void SceneManager::addPointLight(const PointLight &light) +{ + pointLights.push_back(light); +} + +void SceneManager::clearPointLights() +{ + pointLights.clear(); +} + void SceneManager::init(EngineContext *context) { _context = context; @@ -39,6 +49,21 @@ void SceneManager::init(EngineContext *context) sceneData.ambientColor = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f); sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f); sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); + + // Seed a couple of default point lights for quick testing. + PointLight warmKey{}; + warmKey.position = glm::vec3(0.0f, 0.0f, 0.0f); + warmKey.radius = 25.0f; + warmKey.color = glm::vec3(1.0f, 0.95f, 0.8f); + warmKey.intensity = 15.0f; + addPointLight(warmKey); + + PointLight coolFill{}; + coolFill.position = glm::vec3(-10.0f, 4.0f, 10.0f); + coolFill.radius = 20.0f; + coolFill.color = glm::vec3(0.6f, 0.7f, 1.0f); + coolFill.intensity = 10.0f; + addPointLight(coolFill); } void SceneManager::update_scene() @@ -290,6 +315,21 @@ void SceneManager::update_scene() sceneData.rtParams = glm::vec4(ss.hybridRayNoLThreshold, 0.0f, 0.0f, 0.0f); } + // Fill punctual lights into GPUSceneData + const uint32_t lightCount = static_cast(std::min(pointLights.size(), static_cast(kMaxPunctualLights))); + for (uint32_t i = 0; i < lightCount; ++i) + { + const PointLight &pl = pointLights[i]; + sceneData.punctualLights[i].position_radius = glm::vec4(pl.position, pl.radius); + sceneData.punctualLights[i].color_intensity = glm::vec4(pl.color, pl.intensity); + } + for (uint32_t i = lightCount; i < kMaxPunctualLights; ++i) + { + sceneData.punctualLights[i].position_radius = glm::vec4(0.0f); + sceneData.punctualLights[i].color_intensity = glm::vec4(0.0f); + } + sceneData.lightCounts = glm::uvec4(lightCount, 0u, 0u, 0u); + auto end = std::chrono::system_clock::now(); auto elapsed = std::chrono::duration_cast(end - start); stats.scene_update_time = elapsed.count() / 1000.f; diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index dfb2a09..fcf245b 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -126,6 +126,18 @@ public: bool setGLTFInstanceAnimation(const std::string &instanceName, const std::string &animationName, bool resetTime = true); bool setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop); + struct PointLight + { + glm::vec3 position; + float radius; + glm::vec3 color; + float intensity; + }; + + void addPointLight(const PointLight &light); + void clearPointLights(); + const std::vector &getPointLights() const { return pointLights; } + struct SceneStats { float scene_update_time = 0.f; @@ -148,6 +160,7 @@ private: Camera mainCamera = {}; GPUSceneData sceneData = {}; DrawContext mainDrawContext; + std::vector pointLights; std::unordered_map > loadedScenes; std::unordered_map > loadedNodes;