diff --git a/neo/engine/opengl/gl_d3d12raylight.cpp b/neo/engine/opengl/gl_d3d12raylight.cpp index d008ce64..bff0ea19 100644 --- a/neo/engine/opengl/gl_d3d12raylight.cpp +++ b/neo/engine/opengl/gl_d3d12raylight.cpp @@ -2643,7 +2643,7 @@ struct Light // The scalar radius above is still kept as a max/fallback range for point lights, // as the influence range for rect lights, and as the far clip distance for spot lights. float3 pointRadius; - float pointRadiusPad; // non-zero disables specular for this light + float pointRadiusPad; uint iesTextureIndex; // 0 = none, 1..8 = gIesTextures index + 1 float iesStrength; @@ -3745,7 +3745,7 @@ float EstimateSpecularRoughness(float3 specularAlbedo) // get tighter highlights, but keep a floor high enough that POM/normal-map // detail cannot collapse into one-pixel GGX fireflies. float peak = SpecularPeak3(saturate(specularAlbedo)); - return clamp(lerp(0.70, 0.42, peak), 0.38, 0.74); + return clamp(lerp(0.66, 0.40, peak), 0.36, 0.72); } float3 LimitSpecularPeak(float3 c, float peakLimit) @@ -3757,7 +3757,7 @@ float3 LimitSpecularPeak(float3 c, float peakLimit) return c; } -float3 ComputeSpecular( +float3 ComputeSpecularShaped( float3 N, float3 V, float3 L, @@ -3765,7 +3765,13 @@ float3 ComputeSpecular( float lightIntensity, float atten, float shadow, - float3 specularAlbedo) + float3 specularAlbedo, + float roughnessBias, + float specularWrap, + float f0Blend, + float fresnelToWhiteScale, + float energyScale, + float peakLimit) { if (gEnableSpecular == 0) return 0.0; @@ -3777,7 +3783,10 @@ float3 ComputeSpecular( V = normalize(V); L = normalize(L); - float NoL = saturate(dot(N, L)); + float rawNoL = dot(N, L); + float NoL = (specularWrap > 0.0) + ? saturate((rawNoL + specularWrap) / (1.0 + specularWrap)) + : saturate(rawNoL); float NoV = saturate(dot(N, V)); if (NoL <= 1.0e-4 || NoV <= 1.0e-4) @@ -3795,7 +3804,7 @@ float3 ComputeSpecular( if (specPeak <= 0.001) return 0.0; - float roughness = EstimateSpecularRoughness(specMask); + float roughness = clamp(EstimateSpecularRoughness(specMask) + roughnessBias, 0.38, 0.90); float a = roughness * roughness; float a2 = max(a * a, 1.0e-4); @@ -3813,11 +3822,11 @@ float3 ComputeSpecular( float Gl = NoL / max(NoL * (1.0 - k) + k, 1.0e-4); float G = Gv * Gl; - float3 F0 = saturate(lerp(float3(0.025, 0.025, 0.025), specMask, 0.58)); - float3 F = F0 + (1.0 - F0) * pow(1.0 - VoH, 5.0); + float3 F0 = saturate(lerp(float3(0.025, 0.025, 0.025), specMask, saturate(f0Blend))); + float3 F = F0 + (1.0 - F0) * (pow(1.0 - VoH, 5.0) * saturate(fresnelToWhiteScale)); float specTerm = (D * G) / max(4.0 * NoL * NoV, 1.0e-4); - specTerm = min(specTerm, 3.0); + specTerm = min(specTerm, 3.6); const float SPECULAR_ENERGY_SCALE = 2.35; @@ -3829,9 +3838,155 @@ float3 ComputeSpecular( NoL * F * specTerm * - SPECULAR_ENERGY_SCALE; + (SPECULAR_ENERGY_SCALE * max(energyScale, 0.0)); - return LimitSpecularPeak(specular, 4.0); + return LimitSpecularPeak(specular, peakLimit); +} + +float3 ComputeSpecular( + float3 N, + float3 V, + float3 L, + float3 lightColor, + float lightIntensity, + float atten, + float shadow, + float3 specularAlbedo) +{ + return ComputeSpecularShaped( + N, + V, + L, + lightColor, + lightIntensity, + atten, + shadow, + specularAlbedo, + -0.04, + 0.12, + 0.82, + 1.0, + 1.42, + 4.8); +} + +float EstimateSkeletalSubsurfaceMask(float3 baseAlbedo, float3 specularAlbedo) +{ + // Doom 3 characters have no authored SSS/thickness maps. Use a conservative + // skin/flesh proxy from the visible albedo and damp it on shiny/metal-like + // pieces so armor and gear do not glow like wax. + baseAlbedo = saturate(baseAlbedo); + + float luma = dot(baseAlbedo, float3(0.299, 0.587, 0.114)); + float warm = saturate((baseAlbedo.r - baseAlbedo.b * 0.55) * 2.35); + float redBias = saturate((baseAlbedo.r - max(baseAlbedo.g, baseAlbedo.b)) * 3.25 + 0.18); + float fleshRange = smoothstep(0.035, 0.18, luma) * (1.0 - smoothstep(0.82, 1.0, luma)); + float matte = 1.0 - saturate(SpecularPeak3(specularAlbedo) * 0.85); + + return saturate((0.08 + warm * 0.42 + redBias * 0.38) * fleshRange * lerp(0.55, 1.0, matte)); +} + +float3 ComputeSkeletalSpecularAlbedo(float3 baseAlbedo, float3 specularAlbedo) +{ + float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo); + + // Old character spec maps were authored for Doom 3's stylized interaction + // lights. In the path tracer they behave like wet plastic unless we make the + // BRDF much softer and cap the F0 on skin/flesh-like areas. + float3 spec = saturate(specularAlbedo); + float3 softSpec = spec * lerp(0.48, 0.24, subsurfaceMask); + float cap = lerp(0.16, 0.075, subsurfaceMask); + return min(softSpec, float3(cap, cap, cap)); +} + +float3 ComputeSkeletalSpecular( + float3 N, + float3 V, + float3 L, + float3 baseAlbedo, + float3 lightColor, + float lightIntensity, + float atten, + float shadow, + float3 specularAlbedo) +{ + float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo); + float3 characterSpec = ComputeSkeletalSpecularAlbedo(baseAlbedo, specularAlbedo); + + // Character MD5 specular maps often encode art-directed shine, not material + // F0. Shape the whole BRDF, including grazing Fresnel, so faces and bodies + // keep a soft oily skin response instead of reflecting like sealed plastic. + return ComputeSpecularShaped( + N, + V, + L, + lightColor, + lightIntensity, + atten, + shadow, + characterSpec, + lerp(0.10, 0.20, subsurfaceMask), + 0.0, + 0.58, + lerp(0.32, 0.14, subsurfaceMask), + lerp(0.40, 0.24, subsurfaceMask), + lerp(0.55, 0.18, subsurfaceMask)); +} + +float ComputeSubsurfaceShape(float3 N, float3 V, float3 L) +{ + N = normalize(N); + V = normalize(V); + L = normalize(L); + + float NoL = dot(N, L); + float wrap = saturate((NoL + 0.58) / 1.58); + wrap = wrap * wrap; + + // Transmission becomes visible around the terminator and when the light is + // behind the viewed surface, which is the bit missing most painfully on MD5s. + float back = saturate((-NoL + 0.22) / 1.22); + float forward = pow(saturate(dot(-L, V)), 2.25); + float rim = pow(saturate(1.0 - abs(dot(N, V))), 1.60); + + return saturate(wrap * 0.42 + back * (0.55 + forward * 0.85) + rim * wrap * 0.18); +} + +float3 ComputeSkeletalSubsurfaceRadiance( + float3 N, + float3 V, + float3 L, + float3 baseAlbedo, + float3 lightColor, + float lightIntensity, + float atten, + float shadow, + float subsurfaceMask) +{ + if (subsurfaceMask <= 0.001 || atten <= 0.0 || lightIntensity <= 0.0) + return 0.0; + + float shape = ComputeSubsurfaceShape(N, V, L); + if (shape <= 0.001) + return 0.0; + + float softShadow = lerp(shadow, 1.0, 0.18 * subsurfaceMask); + float3 scatterTint = saturate(lerp( + float3(1.0, 0.30, 0.18), + saturate(baseAlbedo) * float3(1.22, 0.68, 0.52) + float3(0.10, 0.03, 0.02), + 0.58)); + + const float SUBSURFACE_DIRECT_SCALE = 0.34; + return clamp( + lightColor * + lightIntensity * + atten * + softShadow * + scatterTint * + shape * + (subsurfaceMask * SUBSURFACE_DIRECT_SCALE), + 0.0, + 2.25); } )" R"( @@ -4255,10 +4410,12 @@ float3 EstimatePathTracedSky(float3 worldPos, float3 N, inout uint rng) accum /= (float)bounceSamples; return accum * 0.55; } - -float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut) +)" +R"( +float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut) { specularOut = 0.0; + subsurfaceOut = 0.0; float3 toCenter = Lgt.position - worldPos; float centerDist = length(toCenter); @@ -4284,6 +4441,7 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 float3 diffuseAccum = 0.0; float3 specAccum = 0.0; + float3 sssAccum = 0.0; [loop] for (uint s = 0u; s < sampleCount; ++s) @@ -4303,24 +4461,28 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 float diffuseNoL = ComputeDiffuseLightingTerm(N, L); float shadow = 1.0; - if (Lgt.samples != 0u && diffuseNoL > 0.0001) + if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001)) shadow = TraceVisibilityBiased(worldPos, N, L, dist); - if (Lgt.pointRadiusPad <= 0.5) - specAccum += ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo); + specAccum += isSkeletal + ? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo) + : ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo); + sssAccum += ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask); diffuseAccum += Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow); } float invSamples = 1.0 / (float)sampleCount; specularOut = specAccum * invSamples; + subsurfaceOut = sssAccum * invSamples; return diffuseAccum * invSamples; } )" R"( -float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut) +float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut) { specularOut = 0.0; + subsurfaceOut = 0.0; float3 toLight = Lgt.position - worldPos; float dist = length(toLight); @@ -4333,19 +4495,22 @@ float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 base float diffuseNoL = ComputeDiffuseLightingTerm(N, L); float shadow = 1.0; - if (Lgt.samples != 0u && diffuseNoL > 0.0001 && atten > 0.0) + if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001) && atten > 0.0) shadow = TraceVisibilityBiased(worldPos, N, L, dist); - if (Lgt.pointRadiusPad <= 0.5) - specularOut = ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo); + specularOut = isSkeletal + ? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo) + : ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo); + subsurfaceOut = ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask); return Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow); } )" R"( -float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut) +float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut) { specularOut = 0.0; + subsurfaceOut = 0.0; float3 toCenter = Lgt.position - worldPos; float centerDist = length(toCenter); @@ -4365,6 +4530,7 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V float3 diffuseAccum = 0.0; float3 specAccum = 0.0; + float3 sssAccum = 0.0; [loop] for (uint s = 0u; s < sampleCount; ++s) @@ -4385,7 +4551,8 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V float3 L = sampleVec / sampleDist; float NdotL = ComputeDiffuseLightingTerm(N, L); - if (NdotL <= 0.0) + float sssShape = (subsurfaceMask > 0.001) ? ComputeSubsurfaceShape(N, V, L) : 0.0; + if (NdotL <= 0.0 && sssShape <= 0.001) continue; float faceTerm = (Lgt.twoSided != 0) @@ -4399,9 +4566,18 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V if (Lgt.samples != 0u) shadow = TraceVisibilityBiased(worldPos, N, L, sampleDist); - if (Lgt.pointRadiusPad <= 0.5) - { - specAccum += ComputeSpecular( + specAccum += (isSkeletal + ? ComputeSkeletalSpecular( + N, + V, + L, + baseAlbedo, + Lgt.color, + Lgt.intensity * faceTerm, + 1.0, + shadow, + specularAlbedo) + : ComputeSpecular( N, V, L, @@ -4409,14 +4585,25 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V Lgt.intensity * faceTerm, 1.0, shadow, - specularAlbedo) * atten; - } + specularAlbedo)) * atten; + + sssAccum += ComputeSkeletalSubsurfaceRadiance( + N, + V, + L, + baseAlbedo, + Lgt.color, + Lgt.intensity * faceTerm, + atten, + shadow, + subsurfaceMask); diffuseAccum += clamp(Lgt.color * (Lgt.intensity * NdotL * faceTerm * atten * shadow), 0.0, 4.0); } float invSamples = 1.0 / (float)sampleCount; specularOut = specAccum * invSamples; + subsurfaceOut = sssAccum * invSamples; return diffuseAccum * invSamples; } @@ -4618,29 +4805,31 @@ float3 EstimateUnresolvedBounceRadiance(float3 hitPos, float3 hitN, float3 albed return clamp(albedo * max(lighting, 0.0), 0.0, 6.0); } -float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, Light Lgt, inout uint rng) +float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, bool hitIsSkeletal, Light Lgt, inout uint rng) { float3 spec = 0.0; + float3 sss = 0.0; float3 diffuse = 0.0; float3 hitSpecularAlbedo = LoadSceneSpecularAlbedo(hitPixel, hitAlbedo); + float subsurfaceMask = hitIsSkeletal ? EstimateSkeletalSubsurfaceMask(hitAlbedo, hitSpecularAlbedo) * 0.45 : 0.0; // Use the same visibility-capable direct-light samplers as the primary hit. // This supplies proper next-event estimation at secondary hits instead of the // old unoccluded light-list approximation. if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT) { - diffuse = PathTraceDirectPointLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec); + diffuse = PathTraceDirectPointLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss); } else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) { - diffuse = PathTraceDirectSpotLight(hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec); + diffuse = PathTraceDirectSpotLight(hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss); } else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT) { - diffuse = PathTraceDirectRectLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec); + diffuse = PathTraceDirectRectLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss); } - return clamp(diffuse, 0.0, 12.0); + return clamp(diffuse + sss * 0.35, 0.0, 12.0); } float3 EstimateBounceSkyLighting(float3 hitPos, float3 hitN, inout uint rng) @@ -4731,7 +4920,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3 uint lightIndex = (start + c * stride) % gLightCount; Light Lgt = gLights[lightIndex]; float3 fast = EstimateFastBounceLight(hitPos, hitN, Lgt); - float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, lightingRng); + float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitIsSkeletal, Lgt, lightingRng); lighting += shadowed - fast; } } @@ -5056,7 +5245,10 @@ float3 ApplyPrimarySpecularPost(float3 specularAccum, float ao, bool isSkeletal) // surfaces. Keep a mild occlusion tint, but let direct-light specular read. float specAo = lerp(0.58, 1.0, saturate(ao)); if (isSkeletal) + { specAo = lerp(specAo, 1.0, 0.50); + specularAccum *= 0.42; + } specularAccum *= specAo; //if (isSkeletal) @@ -5077,9 +5269,11 @@ float3 PathTraceDeterministicLighting( float ao, float skyVis, float ambientSkyVis, - out float3 specularAccum) + out float3 specularAccum, + out float3 subsurfaceAccum) { specularAccum = 0.0; + subsurfaceAccum = 0.0; float microShadow = lerp(0.75, 1.0, cavity); @@ -5104,32 +5298,37 @@ float3 PathTraceDeterministicLighting( // argument only because the functions share the same signature as stochastic // helpers; it is not consumed by PathTraceDirect* in the current shader. uint directRng = InitRng(pixel, 0u, 0xD17EC7u); + float subsurfaceMask = isSkeletal ? EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo) : 0.0; [loop] for (uint i = 0; i < gLightCount; ++i) { Light Lgt = gLights[i]; float3 spec = 0.0; + float3 sss = 0.0; float3 diffuse = 0.0; if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT) { - diffuse = PathTraceDirectPointLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec); + diffuse = PathTraceDirectPointLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss); } else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) { - diffuse = PathTraceDirectSpotLight(worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec); + diffuse = PathTraceDirectSpotLight(worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss); } else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT) { - diffuse = PathTraceDirectRectLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec); + diffuse = PathTraceDirectRectLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss); } lightingAccum += diffuse; specularAccum += spec; + subsurfaceAccum += sss; } specularAccum = ApplyPrimarySpecularPost(specularAccum, ao, isSkeletal); + if (isSkeletal) + subsurfaceAccum *= lerp(0.72, 1.0, saturate(ao)); return ApplyPrimaryDiffusePost(lightingAccum, ao, microShadow, isSkeletal); } @@ -5193,7 +5392,7 @@ float ComputeSpecularReflectionLocalEnergy(float3 worldPos, float3 N) continue; float3 toLight = SafeNormalizeOr(Lgt.position - worldPos, N); - float facing = saturate(dot(N, toLight)); + float facing = saturate((dot(N, toLight) + 0.18) / 1.18); float lightPeak = max(max(Lgt.color.r, Lgt.color.g), Lgt.color.b); energy += atten * facing * Lgt.intensity * lightPeak; @@ -5201,7 +5400,7 @@ float ComputeSpecularReflectionLocalEnergy(float3 worldPos, float3 N) // Keep reflections tied to nearby light contribution instead of becoming // a global mirror pass. - return saturate(energy * 0.08); + return saturate(energy * 0.12); } float ComputeSpecularReflectionMaxDistance(float3 worldPos) @@ -5264,11 +5463,18 @@ float3 EstimateRayTracedSpecularReflection( float3 baseAlbedo, float3 specularAlbedo, float cavity, + bool isSkeletal, inout uint rng) { if (gEnableSpecular == 0u || gMaxBounces <= 1u) return 0.0; + // Character materials should get soft direct specular, not mirror-like local + // reflections. The G-buffer material model has no skeletal roughness map, so + // reflected TLAS hits make skin/armor read much wetter than intended. + if (isSkeletal) + return 0.0; + N = SafeNormalizeOr(N, float3(0.0, 0.0, 1.0)); V = SafeNormalizeOr(V, -N); @@ -5280,7 +5486,7 @@ float3 EstimateRayTracedSpecularReflection( bool hasSpecularMap = LooksLikeAuthoredSpecularSample(rawSpecular, baseAlbedo); float specPeak = SpecularPeak3(specularAlbedo); - if (specPeak <= 0.015) + if (specPeak <= 0.006) return 0.0; float NoV = saturate(dot(N, V)); @@ -5319,8 +5525,8 @@ float3 EstimateRayTracedSpecularReflection( { // Do not reflect sky forever. A missed finite reflection ray contributes // only a tiny local glossy sheen. - reflectedRadiance = GetSkyRadiance(R) * 0.035; - distanceFade = 0.20; + reflectedRadiance = GetSkyRadiance(R) * 0.055; + distanceFade = 0.28; } else { @@ -5373,13 +5579,13 @@ float3 EstimateRayTracedSpecularReflection( } reflectedRadiance *= reflectedSurfaceAverageColor; - reflectedRadiance = CompressEmissiveRadiance(reflectedRadiance, hasSpecularMap ? 5.0 : 3.5); + reflectedRadiance = CompressEmissiveRadiance(reflectedRadiance, hasSpecularMap ? 3.5 : 2.5); float cavityFade = lerp(0.42, 1.0, saturate(cavity)); float reflectionStrength = hasSpecularMap - ? saturate(specPeak * 1.35) - : saturate((specPeak - 0.10) * 0.65); - float reflectionScale = hasSpecularMap ? 0.90 : 0.55; + ? saturate(specPeak * 1.30) + : saturate((specPeak - 0.08) * 0.70); + float reflectionScale = hasSpecularMap ? 0.85 : 0.52; if (reflectionStrength <= 0.001) return 0.0; @@ -5393,7 +5599,7 @@ float3 EstimateRayTracedSpecularReflection( localReflectionEnergy * distanceFade, 0.0, - 3.5); + 2.6); } )" R"( @@ -5488,6 +5694,7 @@ void RayGen() } float3 specularAccum = 0.0; + float3 subsurfaceAccum = 0.0; float3 lightingAccum = PathTraceDeterministicLighting( pixel, worldPos, @@ -5500,7 +5707,8 @@ void RayGen() ao, skyVis, ambientSkyVis, - specularAccum); + specularAccum, + subsurfaceAccum); lightingAccum += EstimateScreenSpaceEmissiveParticleLighting(pixel, worldPos, N); // Only the indirect bounce path uses per-SPP randomness now. Direct lights, @@ -5540,6 +5748,7 @@ void RayGen() baseAlbedo, specularAlbedo, cavity, + isSkeletal, reflectionRng); float3 albedo = baseAlbedo * cavity; @@ -5554,6 +5763,7 @@ void RayGen() float outputAo = isSkeletal ? lerp(ao, 1.0, 0.55) : ao; finalColor *= clamp(outputAo + 0.3, 0.0, 1.0); + finalColor += subsurfaceAccum; // Volumetric light scattering is radiance in the camera ray, not surface // reflectance, so add it after surface albedo/specular composition. Because diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index e1da11af..41f04dbd 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -2971,7 +2971,7 @@ cbuffer DrawCB : register(b0) // Pixel-shader POM depth in UV space. Keep this conservative: legacy idTech // normal maps often do not have a true height channel, so the POM path below // gates fallback height by local detail before applying any offset. -#define gParallaxScale (clamp(gNormalMapStrength, 0.0, 4.0) * 0.020) +#define gParallaxScale (clamp(gNormalMapStrength, 0.0, 4.0) * 0.028) #define gCameraWorldPos gCameraPomPad.xyz #define gCameraPomValid gCameraPomPad.w #define gUseNeuralPOM gNeuralPomPad.x @@ -3002,6 +3002,7 @@ cbuffer DrawCB : register(b0) #define QD3D12_TESS_TARGET_EDGE_PIXELS 28.0 #define QD3D12_TESS_DISTANCE_NEAR 512.0 #define QD3D12_TESS_DISTANCE_FAR 2200.0 +#define QD3D12_GEOMETRY_FLAG_SKELETAL 1u #define QD3D12_POM_DISTANCE_NEAR 384.0 #define QD3D12_POM_DISTANCE_FAR 1800.0 @@ -3443,6 +3444,43 @@ float QD3D12_GetPomDepth(float2 uv) return saturate(1.0 - QD3D12_GetPomHeightFromSamples(uv)); } +float QD3D12_GetPomDepthLOD(float2 uv, float lod) +{ + float4 nm = gNormalMap.SampleLevel(gSamp2, uv, lod); + float authored = QD3D12_AuthoredPomAlphaWeight(nm.a); + if (authored > 0.5) + return saturate(1.0 - nm.a); + + float3 decoded = nm.xyz * 2.0 - 1.0; + decoded.y *= gNormalMapYSign; + + float slope = saturate(length(decoded.xy)); + float normalHeight = saturate(0.5 + (pow(slope, 0.80) - 0.35) * 0.42); + + if (gUseTex0 > 0.5) + { + float normalDetailGate = saturate((slope - 0.040) * 8.0); + if (normalDetailGate > 0.001) + { + float mipScale = exp2(lod); + float2 texel = QD3D12_NormalMapTexelSize() * mipScale; + float lumC = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv, lod).rgb); + float lumL = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv - float2(texel.x, 0.0), lod).rgb); + float lumR = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv + float2(texel.x, 0.0), lod).rgb); + float lumU = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv - float2(0.0, texel.y), lod).rgb); + float lumD = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv + float2(0.0, texel.y), lod).rgb); + + float lumAvg = (lumL + lumR + lumU + lumD) * 0.25; + float localContrast = abs(lumC - lumAvg); + float diffuseWeight = saturate((localContrast - 0.018) * 16.0) * normalDetailGate; + float diffuseHeight = saturate(0.5 + (lumC - lumAvg) * 1.85); + normalHeight = lerp(normalHeight, diffuseHeight, diffuseWeight * 0.28); + } + } + + return saturate(1.0 - normalHeight); +} + float QD3D12_GetPomConfidence(float2 uv) { float minHeight = 0.5; @@ -3484,6 +3522,13 @@ float QD3D12_GetRegularPomFade(VSOut i, float2 baseUv) return saturate(confidence * distanceFade * grazingFade); } +struct QD3D12PomTraceResult +{ + float2 uv; + float confidence; + float visibility; +}; + // Keep the runtime network bounded. Most payloads do not need the full 128-wide // MLP in a fixed-function material pass; clamping evaluation here prevents a @@ -3750,106 +3795,145 @@ QD3D12NeuralPOMResult QD3D12_EvaluateNeuralPOM(VSOut i, float2 baseUv) r.active = 1.0; return r; } +)HLSL" +R"HLSL( +QD3D12PomTraceResult QD3D12_TraceReliefPOM(VSOut i, float2 baseUv) +{ + QD3D12PomTraceResult r; + r.uv = baseUv; + r.confidence = 0.0; + r.visibility = 1.0; + + if (gUseNormalMap < 0.5) + return r; + + if (gAlphaBlendPass > 0.5) + return r; + + if (gUseTex0 > 0.5) + { + float baseAlpha = gTex0.SampleLevel(gSamp0, baseUv, 0.0).a; + if (baseAlpha < 0.985) + return r; + } + + float minHeight = 0.5; + float maxHeight = 0.5; + float authoredWeight = 0.0; + QD3D12_GetPomHeightStats(baseUv, minHeight, maxHeight, authoredWeight); + + float heightRange = maxHeight - minHeight; + float threshold = lerp(0.055, 0.014, authoredWeight); + float confidence = saturate((heightRange - threshold) / max(0.18 - threshold, 0.001)); + confidence = confidence * confidence * (3.0 - 2.0 * confidence); + if (confidence <= 0.035) + return r; + + float3 n, t, b; + QD3D12_BuildPixelTBN(i, n, t, b); + + float cameraConfidence = (gCameraPomValid >= 0.5) ? 1.0 : 0.35; + float3 rawViewWS = (gCameraPomValid >= 0.5) ? (gCameraWorldPos - i.worldPos) : (-i.worldPos); + float3 viewWS = QD3D12_SafeNormalize(rawViewWS, n); + float NoV = dot(n, viewWS); + if (NoV <= 0.025) + return r; + + float3 viewTS = QD3D12_SafeNormalize(float3(dot(viewWS, t), dot(viewWS, b), NoV), float3(0.0, 0.0, 1.0)); + float ndotv = saturate(viewTS.z); + float viewDistance = (gCameraPomValid >= 0.5) ? max(length(gCameraWorldPos - i.worldPos), 1.0) : max(abs(i.currClip.w), 1.0); + float distanceFade = 1.0 - smoothstep(QD3D12_POM_DISTANCE_NEAR, QD3D12_POM_DISTANCE_FAR, viewDistance); + float grazingFade = smoothstep(0.055, 0.18, ndotv); + float fade = saturate(confidence * distanceFade * grazingFade * cameraConfidence); + if (fade <= 0.001) + return r; + + float2 texel = QD3D12_NormalMapTexelSize(); + float2 uvGradX = ddx(baseUv); + float2 uvGradY = ddy(baseUv); + float footprint = max(length(uvGradX / max(texel, float2(1.0e-6, 1.0e-6))), length(uvGradY / max(texel, float2(1.0e-6, 1.0e-6)))); + float lod = clamp(log2(max(footprint, 1.0)), 0.0, 5.0); + + float depthScale = gParallaxScale * lerp(0.68, 1.18, authoredWeight) * confidence * distanceFade * cameraConfidence; + float vz = max(ndotv, lerp(0.18, 0.10, authoredWeight)); + float2 parallaxVector = (viewTS.xy / vz) * depthScale; + + float parallaxLen = length(parallaxVector); + float maxParallaxShift = lerp(0.014, 0.046, authoredWeight) * lerp(0.85, 1.18, confidence); + if (parallaxLen > maxParallaxShift && parallaxLen > 1.0e-6) + parallaxVector *= maxParallaxShift / parallaxLen; + + float layerCountF = lerp(14.0, 46.0, saturate(1.0 - ndotv)); + layerCountF = lerp(10.0, layerCountF, distanceFade); + layerCountF = lerp(layerCountF * 0.70, layerCountF, authoredWeight); + uint layerCount = (uint)clamp(layerCountF + 0.5, 10.0, 48.0); + float invLayerCount = rcp((float)layerCount); + float2 deltaUv = parallaxVector * invLayerCount; + + float2 prevUv = baseUv; + float2 uv = baseUv; + float prevRayDepth = 0.0; + float rayDepth = 0.0; + float prevSurfaceDepth = QD3D12_GetPomDepthLOD(baseUv, lod); + float surfaceDepth = prevSurfaceDepth; + + [loop] + for (uint layer = 0u; layer < 48u; ++layer) + { + if (layer >= layerCount || rayDepth >= surfaceDepth) + break; + + prevUv = uv; + prevRayDepth = rayDepth; + prevSurfaceDepth = surfaceDepth; + + uv -= deltaUv; + rayDepth += invLayerCount; + surfaceDepth = QD3D12_GetPomDepthLOD(uv, lod); + } + + float after = surfaceDepth - rayDepth; + float before = prevSurfaceDepth - prevRayDepth; + float denom = after - before; + float w = (abs(denom) > 1.0e-5) ? saturate(after / denom) : 0.0; + float2 refinedUv = lerp(uv, prevUv, w); + + float2 loUv = uv; + float2 hiUv = prevUv; + [unroll] + for (uint refine = 0u; refine < 5u; ++refine) + { + float2 midUv = (loUv + hiUv) * 0.5; + float midT = dot(baseUv - midUv, parallaxVector) / max(dot(parallaxVector, parallaxVector), 1.0e-8); + float midDepth = saturate(midT); + float midSurface = QD3D12_GetPomDepthLOD(midUv, lod); + if (midDepth < midSurface) + hiUv = midUv; + else + loUv = midUv; + } + + refinedUv = lerp(refinedUv, (loUv + hiUv) * 0.5, 0.65); + + float2 finalOffset = refinedUv - baseUv; + float maxOffset = maxParallaxShift * 1.05; + float finalLen = length(finalOffset); + if (finalLen > maxOffset && finalLen > 1.0e-6) + refinedUv = baseUv + finalOffset * (maxOffset / finalLen); + + r.uv = lerp(baseUv, refinedUv, fade); + r.confidence = fade; + r.visibility = saturate(1.0 - length(r.uv - baseUv) / max(maxParallaxShift, 1.0e-5) * 0.18); + return r; +} float2 QD3D12_ComputeParallaxUVWithNeural(VSOut i, float2 baseUv, QD3D12NeuralPOMResult nr) { if (nr.active > 0.5) return nr.uv; - if (gUseNormalMap < 0.5) - return baseUv; - - float confidence = QD3D12_GetPomConfidence(baseUv); - if (confidence <= 0.05) - return baseUv; - - float parallaxScale = gParallaxScale * confidence; - if (abs(parallaxScale) < 1e-6) - return baseUv; - - float3 n, t, b; - QD3D12_BuildPixelTBN(i, n, t, b); - - // Correct view vector. The old code used -worldPos, which only works when - // the camera is exactly at world origin and causes the obvious offset bugs - // as soon as the camera moves. Keep -worldPos only as a reduced compatibility - // fallback for older paths that cannot derive a perspective camera. - float cameraConfidence = (gCameraPomValid >= 0.5) ? 1.0 : 0.35; - float3 rawViewWS = (gCameraPomValid >= 0.5) ? (gCameraWorldPos - i.worldPos) : (-i.worldPos); - float3 viewWS = QD3D12_SafeNormalize(rawViewWS, n); - float NoV = dot(n, viewWS); - if (NoV <= 0.035) - return baseUv; - - float3 viewTS = QD3D12_SafeNormalize(float3( - dot(viewWS, t), - dot(viewWS, b), - NoV - ), float3(0.0, 0.0, 1.0)); - - float ndotv = saturate(viewTS.z); - float vz = max(ndotv, 0.22); - - // Fade out before far surfaces shimmer or when the viewing angle is too - // grazing for estimated height maps. - float viewDistance = (gCameraPomValid >= 0.5) ? max(length(gCameraWorldPos - i.worldPos), 1.0) : max(abs(i.currClip.w), 1.0); - float distanceFade = 1.0 - smoothstep(QD3D12_POM_DISTANCE_NEAR, QD3D12_POM_DISTANCE_FAR, viewDistance); - float grazingFade = smoothstep(0.08, 0.22, ndotv); - - float scale = parallaxScale * distanceFade * grazingFade * cameraConfidence; - if (scale <= 0.00045) - return baseUv; - - float layerCountF = lerp(10.0, 30.0, saturate(1.0 - ndotv)); - layerCountF = lerp(8.0, layerCountF, saturate(distanceFade)); - uint layerCount = (uint)clamp(layerCountF + 0.5, 8.0, 32.0); - - // Offset-limited POM. Divide by a softened z term and clamp the max UV walk; - // this removes the extreme stretched-offset artifacts on steep angles. - // Standard POM walks opposite the view vector in tangent space. The shift - // cap is intentionally tight for generated RGB-only height, looser for real - // authored alpha height. - float2 parallaxVector = (viewTS.xy / vz) * scale; - float parallaxLen = length(parallaxVector); - float maxParallaxShift = lerp(0.016, 0.040, confidence); - if (parallaxLen > maxParallaxShift && parallaxLen > 1e-6) - parallaxVector *= maxParallaxShift / parallaxLen; - - float invLayerCount = rcp((float)layerCount); - float2 deltaUv = parallaxVector * invLayerCount; - - float2 uv = baseUv; - float2 prevUv = uv; - float currentLayerDepth = 0.0; - float prevLayerDepth = 0.0; - float currentDepth = QD3D12_GetPomDepth(uv); - float prevDepth = currentDepth; - - [loop] - for (uint layer = 0u; layer < 32u; ++layer) - { - if (layer >= layerCount || currentLayerDepth >= currentDepth) - break; - - prevUv = uv; - prevLayerDepth = currentLayerDepth; - prevDepth = currentDepth; - - uv -= deltaUv; - currentLayerDepth += invLayerCount; - currentDepth = QD3D12_GetPomDepth(uv); - } - - float afterDepth = currentDepth - currentLayerDepth; - float beforeDepth = prevDepth - prevLayerDepth; - float denom = afterDepth - beforeDepth; - float weight = (abs(denom) > 1e-5) ? saturate(afterDepth / denom) : 0.0; - float2 refinedUv = lerp(uv, prevUv, weight); - - // Blend the final result in instead of applying a manual half-vector center - // correction. The center correction was the source of several texture-offset - // bugs on flat/low-confidence areas. - return lerp(baseUv, refinedUv, saturate(distanceFade * grazingFade * confidence)); + QD3D12PomTraceResult trace = QD3D12_TraceReliefPOM(i, baseUv); + return trace.uv; } )HLSL" R"HLSL( @@ -4006,10 +4090,7 @@ float4 BuildSpecularAlbedoCached(VSOut i, QD3D12MaterialEval m) { if (gUseSpecularMap > 0.0) { - float regularPomFade = (gUseNeuralPOM <= 0.5 || m.neural.active <= 0.5) - ? QD3D12_GetRegularPomFade(i, i.uv0) - : 0.0; - float2 specUv = lerp(i.uv0, m.uv0, regularPomFade); + float2 specUv = m.uv0; float4 spec = gSpecularMap.Sample(gSamp4, specUv); float strength = max(gSpecularMapStrength, 0.0); @@ -4021,8 +4102,6 @@ float4 BuildSpecularAlbedoCached(VSOut i, QD3D12MaterialEval m) bool alphaLooksForcedOpaque = (spec.a >= 0.999); float alphaMask = alphaLooksForcedOpaque ? 1.0 : saturate(spec.a); float3 specRgb = saturate(spec.rgb) * alphaMask * strength; - if (regularPomFade > 0.0) - specRgb = min(specRgb, float3(0.82, 0.82, 0.82)); return float4(specRgb, 1.0); } @@ -4235,11 +4314,42 @@ float QD3D12_TessellationPatchEdgeFade(float3 bary) return smoothstep(0.0, 0.08, edgeDistance); } +uint QD3D12_DecodeGeometryFlag(float flag) +{ + return (uint)floor(max(flag, 0.0) + 0.5); +} + +bool QD3D12_IsSkeletalGeometry(float flag) +{ + return (QD3D12_DecodeGeometryFlag(flag) & QD3D12_GEOMETRY_FLAG_SKELETAL) != 0u; +} + +float QD3D12_ComputeCharacterTessEdgeFactor(VSOut a, VSOut b) +{ + float distanceFade = QD3D12_TessellationEdgeDistanceFade(a, b); + if (distanceFade <= 0.001) + return 1.0; + + float edgePixels = QD3D12_EdgeLengthPixels(a.currClip, b.currClip); + float screenTerm = sqrt(max(edgePixels, 1.0) / 24.0); + float strength = clamp(max(gNormalMapStrength, 0.0), 0.0, 4.0); + + // Character tessellation is for silhouette/deformation smoothness, not + // normal-map relief. Keep it bounded so animated MD5s do not turn rubbery. + float nearFactor = 1.0 + screenTerm * 2.65 + strength * 0.22; + nearFactor = clamp(nearFactor, 1.0, 8.0); + + return clamp(lerp(1.0, nearFactor, distanceFade), QD3D12_TESS_MIN_FACTOR, 8.0); +} + float QD3D12_ComputeNormalMapTessEdgeFactor(VSOut a, VSOut b) { if (gUseNormalMap <= 0.5) return 1.0; + if (QD3D12_IsSkeletalGeometry(a.attr.x) || QD3D12_IsSkeletalGeometry(b.attr.x)) + return QD3D12_ComputeCharacterTessEdgeFactor(a, b); + float distanceFade = QD3D12_TessellationEdgeDistanceFade(a, b); if (distanceFade <= 0.001) return 1.0; @@ -4331,6 +4441,38 @@ float4 QD3D12_Interp4(float4 a, float4 b, float4 c, float3 w) return a * w.x + b * w.y + c * w.z; } +float3 QD3D12_ProjectPointToTangentPlane(float3 p, float3 planePoint, float3 planeNormal) +{ + planeNormal = QD3D12_SafeNormalize(planeNormal, float3(0.0, 0.0, 1.0)); + return p - planeNormal * dot(p - planePoint, planeNormal); +} + +float3 QD3D12_CharacterPhongTessellate(const OutputPatch patch, float3 bary, float3 linearObjPos, float distanceFade) +{ + float3 n0 = QD3D12_SafeNormalize(patch[0].objNormal, float3(0.0, 0.0, 1.0)); + float3 n1 = QD3D12_SafeNormalize(patch[1].objNormal, n0); + float3 n2 = QD3D12_SafeNormalize(patch[2].objNormal, n0); + + float3 q0 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[0].objPos, n0); + float3 q1 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[1].objPos, n1); + float3 q2 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[2].objPos, n2); + float3 phongObjPos = q0 * bary.x + q1 * bary.y + q2 * bary.z; + + float normalAgreement = saturate((dot(n0, n1) + dot(n1, n2) + dot(n2, n0)) * 0.1667 + 0.5); + float smoothAmount = 0.78 * distanceFade * smoothstep(0.10, 0.82, normalAgreement); + + float3 delta = phongObjPos - linearObjPos; + float maxEdgeLen = max( + length(patch[0].objPos - patch[1].objPos), + max(length(patch[1].objPos - patch[2].objPos), length(patch[2].objPos - patch[0].objPos))); + float maxDelta = max(maxEdgeLen * 0.075, 0.01); + float deltaLen = length(delta); + if (deltaLen > maxDelta) + delta *= maxDelta / max(deltaLen, 1.0e-5); + + return linearObjPos + delta * smoothAmount; +} + [domain("tri")] VSOut DSMain(HSConstOut tessFactors, float3 bary : SV_DomainLocation, const OutputPatch patch) { @@ -4353,13 +4495,17 @@ VSOut DSMain(HSConstOut tessFactors, float3 bary : SV_DomainLocation, const Outp VSOut o; float3 objNormal = QD3D12_SafeNormalize(i.objNormal, float3(0.0, 0.0, 1.0)); + bool isSkeletal = QD3D12_IsSkeletalGeometry(i.attr.x); + float distanceFade = QD3D12_TessellationDisplacementFade(i.currClip); + float3 baseObjPos = isSkeletal + ? QD3D12_CharacterPhongTessellate(patch, bary, i.objPos, distanceFade) + : i.objPos; float height = QD3D12_GetFilteredTessHeight(i.uv0); float centeredHeight = QD3D12_CleanCenteredTessHeight(height); - float displacementFade = QD3D12_TessellationDisplacementFade(i.currClip); - displacementFade *= QD3D12_TessellationPatchEdgeFade(bary); + float displacementFade = isSkeletal ? 0.0 : distanceFade * QD3D12_TessellationPatchEdgeFade(bary); float reliefConfidence = QD3D12_GetPomConfidence(i.uv0); float displacement = centeredHeight * gTessellationDisplacement * displacementFade * reliefConfidence; - float3 displacedObjPos = i.objPos + objNormal * displacement; + float3 displacedObjPos = baseObjPos + objNormal * displacement; float4 worldPos = mul(gModelMatrix, float4(displacedObjPos, 1.0)); float4 currClip = mul(gMVP, float4(displacedObjPos, 1.0));