From 73f84ca650c0350af0455fc2ce4ebb6fe8f2b724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:49:08 +0200 Subject: [PATCH 1/2] renderer: unify 3D lighting integrator and add FXAA anti-aliasing to realtime mode --- .../weird-renderer/core/SDF3DRenderPipeline.h | 5 + .../core/SDF3DRenderPipeline.cpp | 70 ++- .../shaders/3d/sdf_raymarching.frag | 468 ++++++++++-------- .../shaders/postprocess/linear_to_srgb.frag | 169 ++++++- 4 files changed, 481 insertions(+), 231 deletions(-) diff --git a/include/weird-renderer/core/SDF3DRenderPipeline.h b/include/weird-renderer/core/SDF3DRenderPipeline.h index 6499a620..c544c058 100644 --- a/include/weird-renderer/core/SDF3DRenderPipeline.h +++ b/include/weird-renderer/core/SDF3DRenderPipeline.h @@ -61,12 +61,17 @@ namespace WeirdEngine void free(); void showDebugUI(); + void setAntialiasing(bool enable); + void setPathTracer(bool enable); + Config& getConfig() { return m_config; } private: + void updatePipelineDefines(); + Config m_config; RenderPlane& m_renderPlane; diff --git a/src/weird-renderer/core/SDF3DRenderPipeline.cpp b/src/weird-renderer/core/SDF3DRenderPipeline.cpp index 945f7b78..43b119af 100644 --- a/src/weird-renderer/core/SDF3DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF3DRenderPipeline.cpp @@ -31,23 +31,64 @@ namespace WeirdEngine m_shapeDataBuffer = new DataBuffer(); resize(config.renderWidth, config.renderHeight); + updatePipelineDefines(); + + m_sdfShader.addDefine("MESH_SHADOW_SDF"); + } + + SDF3DRenderPipeline::~SDF3DRenderPipeline() + { + free(); + delete m_shapeDataBuffer; + } + + void SDF3DRenderPipeline::updatePipelineDefines() + { if (m_config.enablePathTracer) { m_sdfShader.addDefine("PATH_TRACING"); + m_resolveShader.removeDefine("FXAA"); + if (m_config.enableAntialiasing) + { + m_sdfShader.addDefine("ANTIALIASING"); + } + else + { + m_sdfShader.removeDefine("ANTIALIASING"); + } } - - if (m_config.enableAntialiasing) + else { - m_sdfShader.addDefine("ANTIALIASING"); + m_sdfShader.removeDefine("PATH_TRACING"); + m_sdfShader.removeDefine("ANTIALIASING"); + if (m_config.enableAntialiasing) + { + m_resolveShader.addDefine("FXAA"); + } + else + { + m_resolveShader.removeDefine("FXAA"); + } } + } - m_sdfShader.addDefine("MESH_SHADOW_SDF"); + void SDF3DRenderPipeline::setAntialiasing(bool enable) + { + if (m_config.enableAntialiasing != enable) + { + m_config.enableAntialiasing = enable; + updatePipelineDefines(); + } } - SDF3DRenderPipeline::~SDF3DRenderPipeline() + void SDF3DRenderPipeline::setPathTracer(bool enable) { - free(); - delete m_shapeDataBuffer; + if (m_config.enablePathTracer != enable) + { + m_config.enablePathTracer = enable; + m_frameCounter = 0; + updatePipelineDefines(); + } } Shader& SDF3DRenderPipeline::getShader() @@ -151,6 +192,8 @@ namespace WeirdEngine m_resolveShader.use(); m_resolveShader.setUniform("t_input", 0); + m_resolveShader.setUniform("u_inverseResolution", + glm::vec2(1.0f / m_config.renderWidth, 1.0f / m_config.renderHeight)); m_accumTexture[m_accumIdx].bind(0); glDisable(GL_DEPTH_TEST); @@ -223,22 +266,15 @@ namespace WeirdEngine { WeirdEngine::Logger::log(std::string("Path tracer ") + (m_config.enablePathTracer ? "enabled" : "disabled")); - - if (m_config.enablePathTracer) - m_sdfShader.addDefine("PATH_TRACING"); - else - m_sdfShader.removeDefine("PATH_TRACING"); + m_frameCounter = 0; + updatePipelineDefines(); } if (ImGui::Checkbox("Enable Anti-Aliasing", &m_config.enableAntialiasing)) { WeirdEngine::Logger::log(std::string("Anti-Aliasing ") + (m_config.enableAntialiasing ? "enabled" : "disabled")); - - if (m_config.enableAntialiasing) - m_sdfShader.addDefine("ANTIALIASING"); - else - m_sdfShader.removeDefine("ANTIALIASING"); + updatePipelineDefines(); } if (ImGui::SliderFloat("Contrast", &m_config.contrast, 1.0f, 10.0f)) diff --git a/src/weird-renderer/shaders/3d/sdf_raymarching.frag b/src/weird-renderer/shaders/3d/sdf_raymarching.frag index 030b2b73..9be10a0a 100644 --- a/src/weird-renderer/shaders/3d/sdf_raymarching.frag +++ b/src/weird-renderer/shaders/3d/sdf_raymarching.frag @@ -211,7 +211,6 @@ const float NORMAL_EPSILON = 0.0001; uniform float u_near; uniform float u_far; -const float OVERSHOOT = 1.0; const float DOT_BLEND_K = 0.2; // === Render Style Parameters (Post-Processing) ================= @@ -667,29 +666,6 @@ vec3 getNormal(vec3 p) return normalize(n); } -vec3 getPointLight(vec3 p, vec3 rd, vec3 color) -{ - - vec3 lightPos = u_lights[0].position; - - vec3 L = normalize(lightPos - p); - vec3 N = getNormal(p); - vec3 V = -rd; - vec3 R = reflect(-L, N); - - vec3 specColor = vec3(0.5); - vec3 specular = specColor * pow(clamp(dot(R, V), 0.0, 1.0), 100.0); - - vec3 diffuse = color * clamp(dot(L, N), 0.0, 1.0); - - vec3 ambient = color * 0.05; - vec3 fresnel = 0.1 * color * pow(1.0 + dot(rd, N), 3.0); - - float d = rayMarch(p - rd * 0.02, normalize(lightPos)); - - return (d > length(lightPos - p)) ? diffuse + ambient + specular + fresnel : ambient + fresnel; -} - // Random functions for path tracing // Based on PCG integer hash uint pcg(inout uint state) @@ -764,15 +740,220 @@ vec3 getSkyColor(vec3 dir) return sky; } +// === Unified BRDF parameters (shared by realtime & path tracer) ============= +// Both render modes compile from the same integrator below, so every +// parameter that affects the look lives here exactly once. Tune these to +// change BOTH modes at the same time. + +// Fresnel exponent: dielectrics get a tight retro rim, metals broaden out. +const float FRESNEL_POWER_DIELECTRIC = 50.0; +const float FRESNEL_POWER_METALLIC = 1.0; + +// specMultiplier: intensity/scaling of the direct specular lobe. +// Retro just blows it out for that classic '90s CG highlight. +const vec3 SPEC_MULTIPLIER = vec3(5.0); + +// Sky ambient weights. The path tracer converges to these stochastically; +// the realtime path applies them analytically, so ambient matches in both. +const float SKY_AMBIENT_DIFFUSE_WEIGHT = 0.5; +const float SKY_AMBIENT_FILL = 0.15; + +// Realtime only: where SDF occlusion detects nearby geometry, that geometry +// bounces a bit of light back instead of going black. Approximates the +// multi-bounce interreflection the path tracer resolves naturally. +const float SKY_BOUNCE_APPROX = 0.3; + +// Realtime only: ambient floor so indoor / black-sky scenes never crush to +// pure black. The path tracer resolves this via interreflection bounces. +const float LIGHT_BOUNCE_AMBIENT = 0.03; // scales Σ light color × intensity +const float AMBIENT_FLOOR = 0.03; // tiny constant neutral floor + +// One mapping from material roughness to Blinn-Phong exponent, used by BOTH +// realtime and path-traced direct lighting so highlight sizes always match. +float specExponentFromRoughness(float roughness) +{ + return exp2(mix(11.0, 5.0, sqrt(clamp(roughness, 0.0, 1.0)))); +} + +// Unified Schlick fresnel +float computeFresnel(float f0, float cosTheta) +{ + float fresnelPower = mix(FRESNEL_POWER_DIELECTRIC, FRESNEL_POWER_METALLIC, f0); + return f0 + (1.0 - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), fresnelPower); +} + +// Stable screen-space ordered dither in [-1,1]^3, decorrelated by `salt`. +// Realtime mode uses this instead of RNG: it mimics the path tracer's +// converged soft look without needing temporal accumulation. +vec3 dither3(float salt) +{ + vec2 coord = gl_FragCoord.xy + HashID(salt); + return vec3(GetBayerThreshold(coord), GetBayerThreshold(coord + vec2(37.0, 17.0)), + GetBayerThreshold(coord + vec2(17.0, 37.0))) * + 2.0 - + 1.0; +} + +// Light-direction jitter for soft shadows. +// Path tracing: stochastic sample per pixel/frame — temporal accumulation +// averages it into soft shadows. +// Realtime: zero — exact light directions; shadow softness comes from the +// SDF penumbra estimate in softShadow() instead. +vec3 shadowJitter(float scale, inout float seed) +{ +#ifdef PATH_TRACING + return (vec3(hash2(seed), hash2(seed).x) * 2.0 - 1.0) * scale; +#else + return vec3(0.0); +#endif +} + +// Smooth SDF soft shadows (realtime only): classic penumbra estimate. While +// marching toward the light, the ratio of closest-approach distance to +// travel distance gives a smooth shadow factor in [0,1]. k controls the +// penumbra angle (larger k = harder shadows). +float softShadow(vec3 ro, vec3 rd, float lightDist, float k) +{ + float res = 1.0; + float t = 0.05; // start bias to avoid self-shadow acne + for (int i = 0; i < MAX_STEPS; i++) + { + float h = sceneSdf(ro + rd * t).x; + res = min(res, k * h / t); + if (res < RAYMARCH_EPSILON || t > lightDist) + break; + t += max(h, 0.02); // full SDF step: fast in open space, dense near occluders + } + return clamp(res, 0.0, 1.0); +} + +// Shared per-light direct lighting: same attenuation and same BRDF terms in +// both modes. Only the shadow technique differs: the path tracer jitters the +// light direction stochastically (accumulates into soft shadows), realtime +// uses exact directions with an analytic SDF penumbra. +vec3 evalDirectLighting(vec3 p, vec3 N, vec3 rd, vec3 albedo, float roughness, float fresnel, inout float seed) +{ + vec3 V = -rd; + vec3 directLighting = vec3(0.0); + + for (int i = 0; i < u_numLights; i++) + { + Light light = u_lights[i]; + + vec3 L; + float lightDist; + float attenuation = 1.0; + // Angular softness of this light. The path tracer jitters L by this + // amount; realtime feeds 1/softness as the penumbra hardness below, + // so both modes produce matching penumbra sizes. + float softness; + + if (light.type == 0) + { + // Directional + L = light.direction; + lightDist = u_far * 0.5; + softness = 0.05; + } + else if (light.type == 1) + { + // Point + vec3 lightVec = light.position - p; + lightDist = length(lightVec); + L = lightVec; + + float a = 3.0; + float b = 0.7; + attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); + + softness = 0.25; + } + else + { + // Cone + vec3 lightVec = light.position - p; + lightDist = length(lightVec); + L = lightVec; + + float a = 3.0; + float b = 0.7; + attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); + + // Cone falloff (spot effect) + vec3 surfaceToLight = normalize(light.position - p); + float spotEffect = dot(surfaceToLight, normalize(light.direction)); + + float innerCone = 0.95; + float outerCone = 0.80; + + attenuation *= smoothstep(outerCone, innerCone, spotEffect); + + softness = 0.15; + } + + // Stochastic jitter in the path tracer only (see shadowJitter) + L = normalize(L + shadowJitter(softness, seed)); + + // Shadowing +#ifdef PATH_TRACING + float dLight = rayMarch(p + N * 0.01, L); + + // Check if we hit nothing before reaching the light (or if the ray just ran out of steps grazing the + // surface) + bool hitObstacle = (dLight < lightDist) && (sceneSdf(p + N * 0.01 + dLight * L).x < RAYMARCH_EPSILON * 5.0); + float shadow = hitObstacle ? 0.0 : 1.0; +#else + float shadow = softShadow(p + N * 0.01, L, lightDist, 1.0 / softness); +#endif + if (shadow > 0.0) + { + vec3 diffuse = albedo * max(dot(N, L), 0.0) * (1.0 - fresnel); + + vec3 H = normalize(L + V); + float specPow = pow(max(dot(N, H), 0.0), specExponentFromRoughness(roughness)); + vec3 specular = SPEC_MULTIPLIER * specPow * fresnel; + + directLighting += (diffuse + specular) * shadow * attenuation * light.color.xyz * light.color.w; + } + } + + return directLighting; +} + +// Cheap SDF ambient occlusion (realtime only): samples the SDF a few times +// along the surface normal; nearby geometry blocks the sky ambient. Stands in +// for the contact darkening the path tracer resolves through light bounces. +float sdfAmbientOcclusion(vec3 p, vec3 N) +{ + float occlusion = 0.0; + float weight = 1.0; + for (int i = 1; i <= 4; i++) + { + float h = 0.02 + 0.12 * float(i); + float d = sceneSdf(p + N * h).x; + occlusion += (h - d) * weight; + weight *= 0.9; + } + return clamp(1.0 - 1.2 * occlusion, 0.0, 1.0); +} + // ───────────────────────────────────────────────────────────────────────────── -// Core path-tracing kernel shared by SDF and mesh surfaces. +// Unified shading integrator shared by SDF and mesh surfaces. +// +// PATH_TRACING defined: offline Monte-Carlo path tracer (stochastic +// sampling + temporal accumulation, multi-bounce GI). +// PATH_TRACING undefined: realtime — the same integrator evaluated with a +// single deterministic ray per pixel. Monte-Carlo +// randomness is replaced by stable ordered dither, +// and multi-bounce GI is approximated with analytic +// sky ambient + cheap SDF occlusion. // // firstN – surface normal at the primary hit point. // • SDF surfaces: pass getNormal(p) before calling. // • Mesh surfaces: pass the GBuffer normal directly. // All secondary-bounce normals are always derived from the SDF analytically. // ───────────────────────────────────────────────────────────────────────────── -vec3 pathTrace(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, inout float seed) +vec3 integrate(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, inout float seed) { int currentMatId = clamp(materialId, 0, 15); @@ -780,39 +961,29 @@ vec3 pathTrace(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, vec3 finalColor = max(initialColor - vec3(1.0), vec3(0.0)) + (u_materials[currentMatId].emission * initialColor); vec3 currentAlbedo = min(initialColor, vec3(1.0)); // Albedo reflects 100% light max - vec3 currentRd = rd; - - // === Render Style Parameters =================================== - // roughness and f0 are driven by the material palette; other params remain constant + // The material palette drives the unified BRDF in both modes float roughness = u_materials[currentMatId].roughness; float f0 = u_materials[currentMatId].metallic; - // fresnelPower: exponent for viewing angle reflection falloff - float fresnelPower = 5.0; // Retro: 2.0, Realistic: 5.0 - - // specExponent: tightness of the direct specular highlight - float specExponent = 2000.0; // Retro: 400.0, Realistic: 100.0 - - // specMultiplier: intensity/scaling of the direct specular lobe - // Retro just blows it out for that classic '90s CG highlight. - vec3 specMultiplier = vec3(5.0f); - // Perform bounces +#ifdef PATH_TRACING + // ── Offline: stochastic Monte-Carlo path tracing ─────────────────────── + vec3 currentRd = rd; vec3 throughput = vec3(1.0); for (int bounce = 0; bounce <= u_rayBounces; bounce++) { float currentF0 = f0; + // Stylization hack: roughness grows with each bounce to prevent infinite + // mirror-like reflections (aesthetic choice, not physical correctness). float currentRoughness = u_rayBounces == 1 ? max(roughness, 0.2) - : min(roughness + (float(bounce) * 0.1 / float(u_rayBounces)), - 1.0); // Add roughness with each bounce to prevent infinite mirror-like reflections - fresnelPower = mix(50.0, 1.0, currentF0); + : min(roughness + (float(bounce) * 0.1 / float(u_rayBounces)), 1.0); // On the first bounce use the injected normal (supports both SDF and mesh // primary hits). Subsequent bounces always use the SDF analytical gradient. vec3 N = (bounce == 0) ? firstN : getNormal(p); - float fresnel = currentF0 + ((1.0 - currentF0) * pow(clamp(1.0 - dot(-currentRd, N), 0.0, 1.0), fresnelPower)); + float fresnel = computeFresnel(currentF0, dot(-currentRd, N)); if (bounce == u_rayBounces && bounce > 0) { @@ -823,89 +994,17 @@ vec3 pathTrace(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, float randVal = hash2(seed).x; bool isSpecular = randVal < fresnel; - // 1. Accumulate Direct Lighting from all active lights - vec3 directLighting = vec3(0.0); - - for (int i = 0; i < u_numLights; i++) - { - Light light = u_lights[i]; - - vec3 L; - float lightDist; - float attenuation = 1.0; - - if (light.type == 0) - { - // Directional - L = normalize(light.direction + (vec3(hash2(seed), hash2(seed).x) * 2.0 - 1.0) * 0.05); - lightDist = u_far * 0.5; - } - else if (light.type == 1) - { - // Point - vec3 lightVec = light.position - p; - lightDist = length(lightVec); - vec3 jitter = (vec3(hash2(seed), hash2(seed).x) * 2.0 - 1.0) * 0.25; - L = normalize(lightVec + jitter); - - float a = 3.0; - float b = 0.7; - attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); - } - else - { - // Cone - vec3 lightVec = light.position - p; - lightDist = length(lightVec); - vec3 jitter = (vec3(hash2(seed), hash2(seed).x) * 2.0 - 1.0) * 0.15; - L = normalize(lightVec + jitter); - - float a = 3.0; - float b = 0.7; - attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); - - // Cone falloff (spot effect) - vec3 surfaceToLight = normalize(light.position - p); - float spotEffect = dot(surfaceToLight, normalize(light.direction)); - - float innerCone = 0.95; - float outerCone = 0.80; - - attenuation *= smoothstep(outerCone, innerCone, spotEffect); - } - - // Direct shadow ray for this specific light - float dLight = rayMarch(p + N * 0.01, L); - - // Check if we hit nothing before reaching the light (or if the ray just ran out of steps grazing the - // surface) - bool hitObstacle = (dLight < lightDist) && (sceneSdf(p + N * 0.01 + dLight * L).x < RAYMARCH_EPSILON * 5.0); - if (!hitObstacle) - { - vec3 diffuse = currentAlbedo * max(dot(N, L), 0.0) * (1.0 - fresnel); - - vec3 H = normalize(L - currentRd); - float specPow = pow(max(dot(N, H), 0.0), specExponent); - vec3 specular = specMultiplier * specPow * - fresnel; // Note: for realistic, multiply this by 'fresnel' for energy conservation - - directLighting += (diffuse + specular) * attenuation * light.color.xyz * light.color.w; - } - } - - // Add all direct light contributions for this bounce - finalColor += throughput * directLighting; + // 1. Direct lighting from all active lights (stochastic soft shadows) + finalColor += throughput * evalDirectLighting(p, N, currentRd, currentAlbedo, currentRoughness, fresnel, seed); if (bounce == u_rayBounces) { - // If this final hit is in a shadow, it will still evaluate to black. // Add a little bit of sky ambient so deep shadows always have some color. if (bounce > 0) { - finalColor += throughput * currentAlbedo * getSkyColor(N) * 0.15; + finalColor += throughput * currentAlbedo * getSkyColor(N) * SKY_AMBIENT_FILL; } - // ========================================== break; } @@ -937,7 +1036,7 @@ vec3 pathTrace(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, // Path Tracing natively evaluates the bounceDir hemisphere into the procedural sky dome! // Keep sky at full brightness for reflections, but dim it for diffuse ambient lighting - float skyIntensity = isSpecular ? 1.0 : 0.5; + float skyIntensity = isSpecular ? 1.0 : SKY_AMBIENT_DIFFUSE_WEIGHT; finalColor += throughput * getSkyColor(bounceDir) * skyIntensity; break; } @@ -967,96 +1066,47 @@ vec3 pathTrace(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, } return finalColor; -} - -// Standard forward lighting combining all light types and sky reflections -vec3 standardLighting(vec3 p, vec3 rd, vec3 albedo, int materialId, vec3 N) -{ - vec3 V = -rd; - - // === Render Style Parameters =================================== - int matId = clamp(materialId, 0, 15); - float f0 = u_materials[matId].metallic; - float roughness = u_materials[matId].roughness; - float fresnelPower = 10.0; - float specExponent = 100.0; - vec3 specMultiplier = vec3(5.0); - // =============================================================== - - float fresnel = f0 + (1.0 - f0) * pow(clamp(1.0 - dot(V, N), 0.0, 1.0), fresnelPower); +#else + // ── Realtime: deterministic single-ray evaluation of the same integrator ─ + vec3 N = firstN; + float fresnel = computeFresnel(f0, dot(-rd, N)); - vec3 directLighting = vec3(0.0); + // 1. Direct lighting (dithered soft shadows) + finalColor += evalDirectLighting(p, N, rd, currentAlbedo, roughness, fresnel, seed); + // 2. Sky reflection, dither-blurred by roughness. Matches the path tracer's + // expected specular-branch value: fresnel * metallicTint * sky(mirrorDir). + vec3 R = reflect(rd, N); + vec3 specularDir = normalize(R + dither3(100.0) * roughness); + if (dot(specularDir, N) < 0.0) + specularDir = R; // prevent pointing inside + finalColor += fresnel * mix(vec3(1.0), currentAlbedo, f0) * getSkyColor(specularDir); + + // 3. Ambient. The path tracer lights shadowed areas via multi-bounce + // interreflection; realtime approximates that with these terms: + // - sky dome ambient (occluded by cheap SDF AO) + // - fake bounce: occluded sky fraction becomes interreflection + // - a small unoccluded sky fill + // - an interreflection floor driven by the scene lights themselves + // (light-colored, works even when the sky is fully black) + // - a tiny constant neutral floor so nothing reaches pure black + float ao = sdfAmbientOcclusion(p, N); + vec3 skyAmbient = getSkyColor(N); + vec3 ambient = skyAmbient * ao * SKY_AMBIENT_DIFFUSE_WEIGHT * (1.0 - fresnel); + ambient += skyAmbient * (1.0 - ao) * SKY_BOUNCE_APPROX; // fake interreflection + ambient += skyAmbient * SKY_AMBIENT_FILL; // unoccluded fill + + vec3 lightBounce = vec3(0.0); for (int i = 0; i < u_numLights; i++) { - Light light = u_lights[i]; - - vec3 L; - float lightDist; - float attenuation = 1.0; - - if (light.type == 0) - { - // Directional - L = normalize(light.direction); - lightDist = u_far * 0.5; - } - else if (light.type == 1) - { - // Point - vec3 lightVec = light.position - p; - lightDist = length(lightVec); - L = normalize(lightVec); - - float a = 3.0; - float b = 0.7; - attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); - } - else - { - // Cone - vec3 lightVec = light.position - p; - lightDist = length(lightVec); - L = normalize(lightVec); - - float a = 3.0; - float b = 0.7; - attenuation = 10.0 / (a * lightDist * lightDist + b * lightDist + 1.0); - - // Cone falloff (spot effect) - vec3 surfaceToLight = normalize(light.position - p); - float spotEffect = dot(surfaceToLight, normalize(light.direction)); - - float innerCone = 0.95; - float outerCone = 0.80; - - attenuation *= smoothstep(outerCone, innerCone, spotEffect); - } - - // Hard Shadows - float dLight = rayMarch(p + N * 0.01, L); - - // Check if we hit nothing before reaching the light (or ray exhausted steps) - bool hitObstacle = - (dLight < lightDist * 0.95) && (sceneSdf(p + N * 0.01 + dLight * L).x < RAYMARCH_EPSILON * 5.0); - if (!hitObstacle) - { - vec3 diffuse = albedo * max(dot(N, L), 0.0) * (1.0 - fresnel); - - vec3 H = normalize(L + V); - float specPow = pow(max(dot(N, H), 0.0), specExponent); - vec3 specular = specMultiplier * specPow * fresnel; - - directLighting += (diffuse + specular) * attenuation * light.color.xyz * light.color.w; - } + lightBounce += u_lights[i].color.xyz * u_lights[i].color.w; } + ambient += lightBounce * LIGHT_BOUNCE_AMBIENT + vec3(AMBIENT_FLOOR); - // Ambient and Sky Reflections - vec3 R = reflect(rd, N); - vec3 reflectionColor = getSkyColor(R); - vec3 ambient = albedo * 0.1 + reflectionColor * fresnel; + finalColor += currentAlbedo * ambient; - return directLighting + ambient; + return finalColor; +#endif } // Returns the linearised (eye-space) depth from a [0,1] depth-buffer value. @@ -1138,11 +1188,7 @@ vec4 render(in vec2 uv) int materialId = texture(t_gbufferMaterial, gbufferUV).r; meshAlbedo *= getMaterial(meshWorldPos, materialId); // Apply material palette color to mesh albedo -#ifdef PATH_TRACING - col = pathTrace(meshWorldPos, rd, meshAlbedo, materialId, meshNormal, seed); -#else - col = standardLighting(meshWorldPos, rd, meshAlbedo, materialId, meshNormal); -#endif + col = integrate(meshWorldPos, rd, meshAlbedo, materialId, meshNormal, seed); // Fog at mesh depth float fog = smoothstep(u_far * 0.0, u_far, meshDepthLinear); @@ -1174,11 +1220,7 @@ vec4 render(in vec2 uv) int materialId = int(sceneResult.y); vec3 baseColor = getMaterial(materialSamplePos, materialId); -#ifdef PATH_TRACING - col = pathTrace(p, rd, baseColor, materialId, getNormal(p), seed); -#else - col = standardLighting(p, rd, baseColor, materialId, getNormal(p)); -#endif + col = integrate(p, rd, baseColor, materialId, getNormal(p), seed); float fog = smoothstep(u_far * 0.0, u_far, minDepth); col = mix(col, getSkyColor(rd), fog); diff --git a/src/weird-renderer/shaders/postprocess/linear_to_srgb.frag b/src/weird-renderer/shaders/postprocess/linear_to_srgb.frag index f34d91eb..80cf9c61 100644 --- a/src/weird-renderer/shaders/postprocess/linear_to_srgb.frag +++ b/src/weird-renderer/shaders/postprocess/linear_to_srgb.frag @@ -1,10 +1,177 @@ #version 300 es precision highp float; + in vec2 v_texCoord; out vec4 FragColor; + uniform sampler2D t_input; + +#ifdef FXAA + +uniform vec2 u_inverseResolution; + +const float FXAA_EDGE_THRESHOLD = 0.125; +const float FXAA_EDGE_THRESHOLD_MIN = 0.05; +const float FXAA_SUBPIX_TRIM = 0.75; + +vec3 sampleLinear(vec2 uv) +{ + return texture(t_input, uv).rgb; +} + +vec3 linearToSRGB(vec3 linearCol) +{ + return pow(max(linearCol, vec3(0.0)), vec3(0.4545)); +} + +float rgbToLuma(vec3 srgb) +{ + return dot(srgb, vec3(0.299, 0.587, 0.114)); +} + +vec3 applyFXAA(vec2 uv) +{ + vec2 rcpFrame = u_inverseResolution; + + vec3 rgbM = sampleLinear(uv); + float lumaM = rgbToLuma(linearToSRGB(rgbM)); + + float lumaN = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(0.0, -rcpFrame.y)))); + float lumaS = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(0.0, rcpFrame.y)))); + float lumaW = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(-rcpFrame.x, 0.0)))); + float lumaE = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(rcpFrame.x, 0.0)))); + + float rangeMin = min(lumaM, min(min(lumaN, lumaS), min(lumaW, lumaE))); + float rangeMax = max(lumaM, max(max(lumaN, lumaS), max(lumaW, lumaE))); + float range = rangeMax - rangeMin; + + // Early exit if contrast below threshold (not an edge) + if (range < max(FXAA_EDGE_THRESHOLD_MIN, rangeMax * FXAA_EDGE_THRESHOLD)) + { + return linearToSRGB(rgbM); + } + + float lumaNW = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(-rcpFrame.x, -rcpFrame.y)))); + float lumaNE = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(rcpFrame.x, -rcpFrame.y)))); + float lumaSW = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(-rcpFrame.x, rcpFrame.y)))); + float lumaSE = rgbToLuma(linearToSRGB(sampleLinear(uv + vec2(rcpFrame.x, rcpFrame.y)))); + + // Sub-pixel antialiasing filter + float lumaL = lumaN + lumaS + lumaW + lumaE; + float subpixA = 2.0 * lumaL + (lumaNW + lumaNE + lumaSW + lumaSE); + float subpixB = subpixA * (1.0 / 12.0) - lumaM; + float subpixC = clamp(abs(subpixB) / range, 0.0, 1.0); + float subpixBlend = smoothstep(0.0, 1.0, subpixC); + float subpixelOffset = subpixBlend * subpixBlend * FXAA_SUBPIX_TRIM; + + // Edge direction (horizontal vs vertical) + float edgeHorz = abs(-2.0 * lumaM + lumaN + lumaS) * 2.0 + abs(-2.0 * lumaW + lumaNW + lumaSW) + + abs(-2.0 * lumaE + lumaNE + lumaSE); + float edgeVert = abs(-2.0 * lumaM + lumaW + lumaE) * 2.0 + abs(-2.0 * lumaN + lumaNW + lumaNE) + + abs(-2.0 * lumaS + lumaSW + lumaSE); + bool isHorizontal = (edgeHorz >= edgeVert); + + // Select edge orientation + float luma1 = isHorizontal ? lumaN : lumaW; + float luma2 = isHorizontal ? lumaS : lumaE; + float grad1 = abs(luma1 - lumaM); + float grad2 = abs(luma2 - lumaM); + + bool is1Steeper = grad1 >= grad2; + float gradScaled = max(grad1, grad2) * 0.25; + + float stepLength = isHorizontal ? rcpFrame.y : rcpFrame.x; + if (is1Steeper) + { + stepLength = -stepLength; + } + + vec2 uvEdge = uv; + if (isHorizontal) + { + uvEdge.y += stepLength * 0.5; + } + else + { + uvEdge.x += stepLength * 0.5; + } + + vec2 stepDir = isHorizontal ? vec2(rcpFrame.x, 0.0) : vec2(0.0, rcpFrame.y); + + // Explore along edge tangent in negative and positive directions + vec2 uvNeg = uvEdge - stepDir; + vec2 uvPos = uvEdge + stepDir; + + float lumaNeg = rgbToLuma(linearToSRGB(sampleLinear(uvNeg))); + float lumaPos = rgbToLuma(linearToSRGB(sampleLinear(uvPos))); + + float edgeLuma = is1Steeper ? (luma1 + lumaM) * 0.5 : (luma2 + lumaM) * 0.5; + + bool doneNeg = abs(lumaNeg - edgeLuma) >= gradScaled; + bool donePos = abs(lumaPos - edgeLuma) >= gradScaled; + + const int MAX_STEPS = 8; + for (int i = 1; i < MAX_STEPS; i++) + { + float stepScale = (i < 4) ? 1.0 : ((i < 6) ? 1.5 : 2.0); + if (!doneNeg) + { + uvNeg -= stepDir * stepScale; + lumaNeg = rgbToLuma(linearToSRGB(sampleLinear(uvNeg))); + doneNeg = abs(lumaNeg - edgeLuma) >= gradScaled; + } + if (!donePos) + { + uvPos += stepDir * stepScale; + lumaPos = rgbToLuma(linearToSRGB(sampleLinear(uvPos))); + donePos = abs(lumaPos - edgeLuma) >= gradScaled; + } + if (doneNeg && donePos) + { + break; + } + } + + float dstNeg = isHorizontal ? (uv.x - uvNeg.x) : (uv.y - uvNeg.y); + float dstPos = isHorizontal ? (uvPos.x - uv.x) : (uvPos.y - uv.y); + + bool isCloserNeg = dstNeg < dstPos; + float dst = min(dstNeg, dstPos); + float edgeSpan = dstNeg + dstPos; + + float edgeOffset = 0.5 - dst / edgeSpan; + + // Check if end of edge has same sign as center (if not, it's not a valid edge span) + float lumaEnd = isCloserNeg ? lumaNeg : lumaPos; + bool goodSpan = (lumaEnd - edgeLuma < 0.0) != (lumaM - edgeLuma < 0.0); + if (!goodSpan) + { + edgeOffset = 0.0; + } + + float finalOffset = max(subpixelOffset, edgeOffset); + + vec2 finalUV = uv; + if (isHorizontal) + { + finalUV.y += stepLength * finalOffset; + } + else + { + finalUV.x += stepLength * finalOffset; + } + + return linearToSRGB(sampleLinear(finalUV)); +} + +#endif // FXAA + void main() { +#ifdef FXAA + FragColor = vec4(applyFXAA(v_texCoord), texture(t_input, v_texCoord).a); +#else vec4 col = texture(t_input, v_texCoord); - FragColor = vec4(pow(col.rgb, vec3(0.4545)), col.a); + FragColor = vec4(pow(max(col.rgb, vec3(0.0)), vec3(0.4545)), col.a); +#endif } \ No newline at end of file From d27c67f58c19c0e719bf46be3a37c73fc93e16e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:39:59 +0200 Subject: [PATCH 2/2] renderer: add analytic anisotropic checkerboard filtering and surface differentials --- .../core/SDF3DRenderPipeline.cpp | 4 +- .../shaders/3d/sdf_raymarching.frag | 101 +++++++++++++----- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/weird-renderer/core/SDF3DRenderPipeline.cpp b/src/weird-renderer/core/SDF3DRenderPipeline.cpp index 43b119af..e4e5b2a5 100644 --- a/src/weird-renderer/core/SDF3DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF3DRenderPipeline.cpp @@ -100,7 +100,7 @@ namespace WeirdEngine const std::vector& lights, const Camera& camera, double time, const GBuffer& gbuffer, const Material3D* materials) { - // Reset frame counter when path tracer is disabled (no accumulation) + // Reset frame counter when path tracer is disabled (no accumulation in realtime) if (!m_config.enablePathTracer) m_frameCounter = 0; @@ -182,7 +182,7 @@ namespace WeirdEngine m_sdfShader.setUniform("u_loadedObjects", (int)dataSize); m_sdfShader.setUniform("u_shapeCount", (int)shapeCount); - if (m_frameCounter < m_config.maxAccumulationFrames) + if (!m_config.enablePathTracer || m_frameCounter < m_config.maxAccumulationFrames) m_renderPlane.draw(m_sdfShader); m_frameCounter++; diff --git a/src/weird-renderer/shaders/3d/sdf_raymarching.frag b/src/weird-renderer/shaders/3d/sdf_raymarching.frag index 9be10a0a..bf876418 100644 --- a/src/weird-renderer/shaders/3d/sdf_raymarching.frag +++ b/src/weird-renderer/shaders/3d/sdf_raymarching.frag @@ -411,7 +411,7 @@ float modifyDistanceBasedOnMaterial(float dist, int materialId, int objectId) { vec2 offset = HashID(float(objectId)); // +1 to avoid zero ID correlation -#ifdef PATH_TRACING +#if defined(PATH_TRACING) || defined(ANTIALIASING) // Step through all 16 states of the 4x4 Bayer matrix using the frame counter. // Since we have Temporal Accumulation (TAA), this perfectly averages out the dither noise // into smooth glass-like transparency over 16 frames! @@ -598,7 +598,7 @@ vec3 sceneSdf(vec3 p) return vec3(minDist, max(float(finalMaterialId), 0.0), globalBlend); } -vec3 getMaterial(vec3 p, int id) +vec3 getMaterial(vec3 p, int id, vec3 N, vec3 dpdx, vec3 dpdy) { vec3 color = id < 16 ? u_materials[id].color.xyz : (0.83 * vec3(1.0)); vec3 secondaryColor = id < 16 ? u_materials[id].secondaryColor.xyz : vec3(0.0); @@ -612,7 +612,39 @@ vec3 getMaterial(vec3 p, int id) if (pattern == 1) { - patternShape = mod(floor(sp.x) + floor(sp.y) + floor(sp.z), 2.0); + // Triplanar 2D coordinates: project onto the surface tangent plane using dominant normal axis. + // This avoids 3D voxel boundary numerical artifacts and step-count concentric rings on planes. + vec2 uv; + vec2 d_uv_x; + vec2 d_uv_y; + + vec3 absN = abs(N); + if (absN.y >= absN.x && absN.y >= absN.z) + { + uv = sp.xz; + d_uv_x = dpdx.xz * patternScale; + d_uv_y = dpdy.xz * patternScale; + } + else if (absN.x >= absN.z) + { + uv = sp.yz; + d_uv_x = dpdx.yz * patternScale; + d_uv_y = dpdy.yz * patternScale; + } + else + { + uv = sp.xy; + d_uv_x = dpdx.xy * patternScale; + d_uv_y = dpdy.xy * patternScale; + } + + vec2 w = max(abs(d_uv_x) + abs(d_uv_y), 0.001); + vec2 i = 2.0 * (abs(fract((uv - 0.5 * w) * 0.5) - 0.5) - abs(fract((uv + 0.5 * w) * 0.5) - 0.5)) / w; + + // Window out periodic box-filter sidelobes to guarantee smooth monotonic fade without ringing + i *= clamp(1.0 - smoothstep(0.6, 1.0, w), 0.0, 1.0); + + patternShape = 0.5 - 0.5 * i.x * i.y; } else if (pattern == 2) // Perlin Noise { @@ -630,6 +662,16 @@ vec3 getMaterial(vec3 p, int id) return color; } +vec3 getMaterial(vec3 p, int id, vec3 dpdx, vec3 dpdy) +{ + return getMaterial(p, id, vec3(0.0, 1.0, 0.0), dpdx, dpdy); +} + +vec3 getMaterial(vec3 p, int id) +{ + return getMaterial(p, id, vec3(0.0, 1.0, 0.0), dFdx(p), dFdy(p)); +} + vec3 getColor(vec3 p) { vec3 scene = sceneSdf(p); @@ -765,8 +807,8 @@ const float SKY_BOUNCE_APPROX = 0.3; // Realtime only: ambient floor so indoor / black-sky scenes never crush to // pure black. The path tracer resolves this via interreflection bounces. -const float LIGHT_BOUNCE_AMBIENT = 0.03; // scales Σ light color × intensity -const float AMBIENT_FLOOR = 0.03; // tiny constant neutral floor +const float LIGHT_BOUNCE_AMBIENT = 0.01; // scales Σ light color × intensity +const float AMBIENT_FLOOR = 0.05; // tiny constant neutral floor // One mapping from material roughness to Blinn-Phong exponent, used by BOTH // realtime and path-traced direct lighting so highlight sizes always match. @@ -976,8 +1018,7 @@ vec3 integrate(vec3 p, vec3 rd, vec3 initialColor, int materialId, vec3 firstN, // Stylization hack: roughness grows with each bounce to prevent infinite // mirror-like reflections (aesthetic choice, not physical correctness). float currentRoughness = - u_rayBounces == 1 ? max(roughness, 0.2) - : min(roughness + (float(bounce) * 0.1 / float(u_rayBounces)), 1.0); + u_rayBounces == 1 ? max(roughness, 0.2) : min(roughness + (float(bounce) * 0.1 / float(u_rayBounces)), 1.0); // On the first bounce use the injected normal (supports both SDF and mesh // primary hits). Subsequent bounces always use the SDF analytical gradient. @@ -1133,7 +1174,10 @@ vec4 render(in vec2 uv) rd = (vec4(rd, 0) * u_camMatrix).xyz; #endif - float meshDepthSample = texture(t_depthTexture, v_texCoord).r; // [0,1], 1.0 = no mesh + float _ar = u_resolution.x / u_resolution.y; + vec2 gbufferUV = vec2(uv.x / _ar, uv.y) * 0.5 + 0.5; + + float meshDepthSample = texture(t_depthTexture, gbufferUV).r; // [0,1], 1.0 = no mesh float meshDepthLinear = linearizeGBufferDepth(meshDepthSample); // eye-space metres // Ray march SDF @@ -1161,6 +1205,7 @@ vec4 render(in vec2 uv) // alpha: 1.0 = SDF/sky path (accumulate), 0.0 = mesh path (skip accumulation) float accumAlpha = 1.0; + float finalDepth = 1.0; uint state = floatBitsToUint(uv.x) ^ floatBitsToUint(uv.y) ^ uint(u_frameCounter); pcg(state); // Warm up the state @@ -1176,9 +1221,6 @@ vec4 render(in vec2 uv) // so mesh shading varies per frame and accumulates correctly in the temporal denoiser. // Note: this can produce edge artifacts when the jitter pushes the lookup outside // the mesh silhouette — that is an accepted trade-off. - float _ar = u_resolution.x / u_resolution.y; - vec2 gbufferUV = vec2(uv.x / _ar, uv.y) * 0.5 + 0.5; - // --- Mesh surface is in front: apply SDF-aware deferred lighting --- vec3 meshAlbedo = texture(t_gbufferAlbedo, gbufferUV).rgb; vec3 meshWorldPos = texture(t_gbufferWorldPos, gbufferUV).rgb; @@ -1186,7 +1228,8 @@ vec4 render(in vec2 uv) vec3 meshNormal = normalize(texture(t_gbufferNormal, gbufferUV).rgb); int materialId = texture(t_gbufferMaterial, gbufferUV).r; - meshAlbedo *= getMaterial(meshWorldPos, materialId); // Apply material palette color to mesh albedo + meshAlbedo *= getMaterial(meshWorldPos, materialId, meshNormal, dFdx(meshWorldPos), + dFdy(meshWorldPos)); // Apply material palette color to mesh albedo col = integrate(meshWorldPos, rd, meshAlbedo, materialId, meshNormal, seed); @@ -1194,15 +1237,14 @@ vec4 render(in vec2 uv) float fog = smoothstep(u_far * 0.0, u_far, meshDepthLinear); col = mix(col, getSkyColor(rd), fog); - // Write the mesh depth to the depth buffer - gl_FragDepth = meshDepthSample; + finalDepth = meshDepthSample; } else if (minDepth < u_far) { // --- SDF hit is in front --- vec3 p = ro + object * rd; -#ifdef PATH_TRACING +#if defined(PATH_TRACING) || defined(ANTIALIASING) // Jitter the material sampling point with high-frequency 3D noise. // Temporal accumulation will blend material colors at smooth-union // boundaries into smooth gradients. @@ -1218,22 +1260,31 @@ vec4 render(in vec2 uv) vec3 sceneResult = sceneSdf(materialSamplePos); int materialId = int(sceneResult.y); - vec3 baseColor = getMaterial(materialSamplePos, materialId); + vec3 N = getNormal(p); + + vec3 d_rd_x = dFdx(rd); + vec3 d_rd_y = dFdy(rd); + float NdotRD = dot(N, rd); + float denom = -max(abs(NdotRD), 0.05); + vec3 dpdx = object * (d_rd_x - rd * (dot(N, d_rd_x) / denom)); + vec3 dpdy = object * (d_rd_y - rd * (dot(N, d_rd_y) / denom)); + + vec3 baseColor = getMaterial(materialSamplePos, materialId, N, dpdx, dpdy); - col = integrate(p, rd, baseColor, materialId, getNormal(p), seed); + col = integrate(p, rd, baseColor, materialId, N, seed); float fog = smoothstep(u_far * 0.0, u_far, minDepth); col = mix(col, getSkyColor(rd), fog); - gl_FragDepth = sdfDepthNDC; + finalDepth = sdfDepthNDC; } else { // Sky - gl_FragDepth = sdfDepthNDC; + finalDepth = sdfDepthNDC; } - return vec4(col, 1.0); // accumAlpha); + return vec4(col, finalDepth); } float rand(vec2 co) @@ -1244,11 +1295,9 @@ float rand(vec2 co) void main() { vec2 screenUV = v_texCoord; - vec2 uv = (2.0 * v_texCoord) - 1.0; float aspectRatio = u_resolution.x / u_resolution.y; // TODO: uniform - // Temporal jitter for anti-aliasing #ifdef PATH_TRACING #ifdef ANTIALIASING if (u_frameCounter > 0) @@ -1259,11 +1308,9 @@ void main() #endif uv.x *= aspectRatio; - - // Because AA jitter is baked into the original UVs being sent to render(), - // we just evaluate the render directly to extract the jittered target color. vec4 data = render(uv); vec3 col = data.xyz; + gl_FragDepth = data.w; // Firefly suppression: clamp per-sample luminance before accumulation. // Fireflies are caused by rare specular paths hitting a bright light, @@ -1284,10 +1331,6 @@ void main() if (u_frameCounter > 0) { vec4 prevColor = texture(t_previousColor, screenUV); - - // Temporal Denoiser: Use an Exponential Moving Average (EMA) cap - // to prevent the weight from becoming too small, allowing the image to - // continually refine and denoise even after many frames. float weight = max(1.0 / float(u_frameCounter + 1), 0.02); col = mix(prevColor.xyz, col, weight); }