diff --git a/neo/engine/opengl/gl_d3d12raylight.cpp b/neo/engine/opengl/gl_d3d12raylight.cpp index a7cb7382..8a1e0400 100644 --- a/neo/engine/opengl/gl_d3d12raylight.cpp +++ b/neo/engine/opengl/gl_d3d12raylight.cpp @@ -2930,7 +2930,8 @@ float Doom3ProjectionTexture2D(float2 centeredCoord) return Doom3QuadraticCentered(centeredCoord.x) * Doom3QuadraticCentered(centeredCoord.y); } - +)" +R"( float Doom3ProjectedDepthFalloff(float depth, float nearClip, float farClip) { // For spot/projected lights, the renderer API supplies a conventional near/far @@ -2939,8 +2940,7 @@ float Doom3ProjectedDepthFalloff(float depth, float nearClip, float farClip) float depth01 = saturate((depth - nearClip) / max(farClip - nearClip, 1e-4)); return Doom3QuadraticFalloffImage(0.5 + depth01 * 0.5); } -)" -R"( + float ComputePointLightAttenuation(float3 worldPos, Light Lgt) { float3 radii = GetPointLightRadius(Lgt); @@ -3305,25 +3305,29 @@ float ComputeCavity(uint2 pixel, float3 worldPos, float3 N) return 1.0 - cavity * 0.18; } -static const float PBR_PI = 3.14159265; +float Doom3SpecularLookup(float x) +{ + // Doom 3 used a lookup table for specular falloff. A single high-power + // lobe is too binary with this G-buffer path: small normal-map/grazing + // differences make some materials lose specular completely. Use a broad + // plastic lobe plus a tighter hot spot so highlights stay readable without + // turning into a flat additive wash. + x = saturate(x); + + float broad = pow(x, 12.0); + float tight = pow(x, 48.0); + + return saturate(broad * 0.55 + tight * 0.85); +} float SpecularPeak3(float3 c) { return max(max(c.r, c.g), c.b); } -float3 PbrDefaultSpecularF0() -{ - // Missing legacy spec maps become a standard dielectric F0 instead of the - // previous Doom/Phong pseudo-spec mask. Authored black specular maps still - // resolve to black because the alpha-valid bit is handled below. - return float3(0.04, 0.04, 0.04); -} - bool LooksLikeAuthoredSpecularSample(float4 specSample, float3 baseAlbedo) { - // Normal path: alpha is the raster G-buffer validity bit. This preserves - // authored black Phong spec maps: black+alpha means intentionally no specular. + // Normal path: alpha is the raster G-buffer validity bit. if (specSample.a > 0.5) return true; @@ -3338,75 +3342,36 @@ bool LooksLikeAuthoredSpecularSample(float4 specSample, float3 baseAlbedo) return rgbPeak > 0.025 && rgbDiff > 0.035; } +float3 Doom3PseudoSpecularMask(float3 baseAlbedo) +{ + // Doom 3 normally uses a dedicated specular map. + // This fallback is only used for pixels whose specular G-buffer says no + // specular map was written by the raster pass. + float lum = dot(saturate(baseAlbedo), float3(0.299, 0.587, 0.114)); + + float specStrength = lerp(0.22, 0.72, saturate(lum * 1.35)); + + // Slight warm/colored contribution from the diffuse texture, but mostly neutral + // like a missing/default specular map. + float3 neutralSpec = float3(specStrength, specStrength, specStrength); + float3 tintedSpec = saturate(baseAlbedo) * 0.35 + neutralSpec * 0.65; + + return max(tintedSpec, float3(0.18, 0.18, 0.18)); +} + float3 LoadSceneSpecularAlbedo(uint2 pixel, float3 baseAlbedo) { - // Name kept for compatibility with the rest of this shader. The value is - // now interpreted as legacy Phong specular input that is remapped to PBR F0 - // and roughness by the helpers below. float4 specSample = gSpecularTex.Load(int3(pixel, 0)); + // The raster G-buffer writer stores alpha as a validity bit. This matters + // for black specular maps: black should mean zero specular, not "missing map". + // Also accept RGB-only specular inputs when they are clearly not the fallback + // albedo descriptor, which fixes materials whose specular buffer forgot to + // set the alpha-valid bit. if (LooksLikeAuthoredSpecularSample(specSample, baseAlbedo)) return saturate(specSample.rgb); - return PbrDefaultSpecularF0(); -} - -float3 PbrF0FromPhongSpecularInput(float3 phongSpecular) -{ - // Legacy Phong/specular-workflow maps are already artist-authored F0-like - // colors. Keep the color rather than converting through metalness, because - // there is no metallic/roughness texture bound in this pass yet. - return saturate(phongSpecular); -} - -float PbrRoughnessFromPhongSpecularInput(float3 phongSpecular) -{ - // There is no roughness channel in the current input. Use spec intensity as - // a compatibility heuristic: bright Phong spec usually meant a tighter, - // smoother highlight; dark maps become rougher. This gives old content a - // stable GGX lobe without requiring new material assets immediately. - float specPeak = SpecularPeak3(saturate(phongSpecular)); - float glossHint = saturate((specPeak - 0.02) / 0.98); - glossHint = pow(glossHint, 0.55); - - float perceptualRoughness = lerp(0.86, 0.18, glossHint); - return clamp(perceptualRoughness, 0.3, 0.92); -} - -float3 PbrDiffuseAlbedoFromPhongSpecularInput(float3 baseAlbedo, float3 phongSpecular) -{ - // Without a metallic channel, keep diffuse mostly intact. Apply only a mild - // energy-conservation term so very reflective legacy materials do not also - // receive the full diffuse response. - float f0Peak = SpecularPeak3(PbrF0FromPhongSpecularInput(phongSpecular)); - float diffuseEnergy = 1.0 - saturate(f0Peak); - return saturate(baseAlbedo) * diffuseEnergy; -} - -float DistributionGGX(float NdotH, float roughness) -{ - float a = max(roughness * roughness, 0.001); - float a2 = a * a; - float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; - return a2 / max(PBR_PI * denom * denom, 1.0e-6); -} - -float GeometrySchlickGGX(float NdotX, float roughness) -{ - float r = roughness + 1.0; - float k = (r * r) * 0.125; - return NdotX / max(NdotX * (1.0 - k) + k, 1.0e-6); -} - -float GeometrySmithGGX(float NdotV, float NdotL, float roughness) -{ - return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); -} - -float3 FresnelSchlick(float cosTheta, float3 F0) -{ - float f = pow(saturate(1.0 - cosTheta), 5.0); - return F0 + (1.0 - F0) * f; + return Doom3PseudoSpecularMask(baseAlbedo); } float3 ComputeSpecular( @@ -3422,40 +3387,47 @@ float3 ComputeSpecular( if (gEnableSpecular == 0) return 0.0; - if (atten <= 0.0 || shadow <= 0.0) - return 0.0; - N = normalize(N); V = normalize(V); L = normalize(L); - float NdotL = saturate(dot(N, L)); - float NdotV = saturate(dot(N, V)); - if (NdotL <= 1.0e-4 || NdotV <= 1.0e-4) + float NdotL = dot(N, L); + float NdotV = dot(N, V); + + if (atten <= 0.0 || shadow <= 0.0) return 0.0; + // Keep Doom/idTech's front-side behavior, but make the gate soft. A hard + // NdotL/NdotV cutoff was making normal-mapped and grazing surfaces randomly + // lose all specular even when the half-angle lobe should still be visible. + float lightFacing = smoothstep(-0.08, 0.18, NdotL); + float viewFacing = smoothstep(-0.04, 0.14, NdotV); + if (lightFacing <= 0.0 || viewFacing <= 0.0) + return 0.0; + + // idTech4/Doom 3 interaction shader uses half-angle style specular, + // not the reflect(-L,N) Phong vector used in the old code here. float3 H = Doom3SafeNormalizeOr(L + V, N); float NdotH = saturate(dot(N, H)); - float VdotH = saturate(dot(V, H)); - float3 F0 = PbrF0FromPhongSpecularInput(specularAlbedo); - if (SpecularPeak3(F0) <= 1.0e-5) + float specTerm = Doom3SpecularLookup(NdotH) * lightFacing * viewFacing; + if (specTerm <= 1.0e-5) return 0.0; - float roughness = PbrRoughnessFromPhongSpecularInput(specularAlbedo); - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmithGGX(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(VdotH, F0); + float3 specMask = saturate(specularAlbedo); - float3 specularBRDF = (D * G) * F / max(4.0 * NdotV * NdotL, 1.0e-4); + // Doom 3's interaction pass is strongly additive. Keep it punchy, + // but clamp enough to avoid fireflies with stochastic light sampling. + const float DOOM3_SPECULAR_SCALE = 4.75; - // The renderer's light intensities are authored for the older additive - // Phong/Doom path, not calibrated physical lux. This small unit bridge keeps - // PBR highlights readable without returning to a hand-shaped Phong lobe. - const float LEGACY_LIGHT_UNIT_TO_PBR_SPECULAR = 1.35; - - float3 incidentRadiance = lightColor * (lightIntensity * atten * shadow); - float3 specular = incidentRadiance * specularBRDF * NdotL * LEGACY_LIGHT_UNIT_TO_PBR_SPECULAR; + float3 specular = + lightColor * + lightIntensity * + atten * + shadow * + specMask * + specTerm * + DOOM3_SPECULAR_SCALE; return clamp(specular, 0.0, 8.0); } @@ -4220,9 +4192,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3 // Outgoing diffuse radiance from the bounce surface. The primary surface's // albedo is applied later in RayGen, so only the secondary hit albedo belongs // here. - float3 hitSpecularInput = LoadSceneSpecularAlbedo(hitPixel, hitAlbedo); - float3 hitDiffuseAlbedo = PbrDiffuseAlbedoFromPhongSpecularInput(hitAlbedo, hitSpecularInput); - return clamp(hitDiffuseAlbedo * max(lighting, 0.0), 0.0, 16.0); + return clamp(hitAlbedo * max(lighting, 0.0), 0.0, 16.0); } )" @@ -4607,6 +4577,100 @@ float3 EstimateFallbackReflectionHitRadiance(float3 hitPos, float3 hitNormal, fl return clamp(NEUTRAL_UNKNOWN_ALBEDO * max(lighting, 0.0), 0.0, 10.0); } + +float ComputeSpecularReflectionLocalEnergy(float3 worldPos, float3 N) +{ + float energy = 0.0; + + [loop] + for (uint i = 0; i < gLightCount; ++i) + { + Light Lgt = gLights[i]; + + float atten = 0.0; + + if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT) + { + atten = ComputePointLightAttenuation(worldPos, Lgt); + } + else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) + { + atten = ComputeSpotLightAttenuation(worldPos, Lgt); + } + else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT) + { + float dist = length(Lgt.position - worldPos); + float range = max(Lgt.radius, 1.0); + atten = saturate((range - dist) / range); + atten = atten * atten; + } + + if (atten <= 0.0) + continue; + + float3 toLight = SafeNormalizeOr(Lgt.position - worldPos, N); + float facing = saturate(dot(N, toLight)); + float lightPeak = max(max(Lgt.color.r, Lgt.color.g), Lgt.color.b); + + energy += atten * facing * Lgt.intensity * lightPeak; + } + + // Keep reflections tied to nearby light contribution instead of becoming + // a global mirror pass. + return saturate(energy * 0.08); +} + +float ComputeSpecularReflectionMaxDistance(float3 worldPos) +{ + float maxDistance = 0.0; + + [loop] + for (uint i = 0; i < gLightCount; ++i) + { + Light Lgt = gLights[i]; + + float atten = 0.0; + float range = 0.0; + + if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT) + { + atten = ComputePointLightAttenuation(worldPos, Lgt); + range = GetPointLightMaxRadius(Lgt); + } + else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) + { + atten = ComputeSpotLightAttenuation(worldPos, Lgt); + range = max(Lgt.radius, 1.0); + } + else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT) + { + float dist = length(Lgt.position - worldPos); + range = max(Lgt.radius, 1.0); + atten = saturate((range - dist) / range); + atten = atten * atten; + } + + if (atten <= 0.0) + continue; + + // Reflections should not reach the full light range like a mirror. + // This makes the reflection proportional to local light influence. + maxDistance = max(maxDistance, range * lerp(0.18, 0.55, saturate(atten))); + } + + return clamp(maxDistance, 24.0, 768.0); +} + +float ComputeSpecularReflectionDistanceFade(float hitT, float maxT) +{ + float t = saturate(hitT / max(maxT, 1.0)); + + // Strong near reflection, smooth fade before the end of the local volume. + float fade = 1.0 - smoothstep(0.35, 1.0, t); + + return fade * fade; +} + float3 EstimateRayTracedSpecularReflection( uint2 pixel, float3 worldPos, @@ -4623,52 +4687,57 @@ float3 EstimateRayTracedSpecularReflection( N = SafeNormalizeOr(N, float3(0.0, 0.0, 1.0)); V = SafeNormalizeOr(V, -N); - float3 F0 = PbrF0FromPhongSpecularInput(specularAlbedo); - float f0Peak = SpecularPeak3(F0); - if (f0Peak <= 1.0e-5) + // Prefer authored specular maps, but do not hard-disable reflections on + // missing/RGB-only specular inputs. Missing spec maps get a muted fallback + // reflection from the pseudo-spec mask; authored black spec maps still return + // zero because LooksLikeAuthoredSpecularSample() preserves them. + float4 rawSpecular = gSpecularTex.Load(int3(pixel, 0)); + bool hasSpecularMap = LooksLikeAuthoredSpecularSample(rawSpecular, baseAlbedo); + + float specPeak = SpecularPeak3(specularAlbedo); + if (specPeak <= 0.015) return 0.0; float NoV = saturate(dot(N, V)); - float3 mirrorR = SafeNormalizeOr(reflect(-V, N), N); - float NoMirrorR = saturate(dot(N, mirrorR)); - if (NoMirrorR <= 0.001) + float3 R = SafeNormalizeOr(reflect(-V, N), N); + float NoR = saturate(dot(N, R)); + if (NoR <= 0.001) return 0.0; - float roughness = PbrRoughnessFromPhongSpecularInput(specularAlbedo); + // Schlick fresnel keeps reflections strongest at grazing angles while still + // honoring the artist-authored Doom/idTech-style specular map color. Missing + // spec maps use a much lower F0 so the fallback is glossy, not mirror-like. + float3 F0 = hasSpecularMap + ? saturate(specularAlbedo) + : saturate(lerp(float3(0.02, 0.02, 0.02), specularAlbedo, 0.38)); + float3 fresnel = F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0); - // The current material path has no prefiltered environment map or roughness - // mip chain, so trace one glossy ray. Roughness widens the ray cone and then - // fades the result so rough materials read as broad/subtle reflections rather - // than sharp mirrors. - float3 R = mirrorR; - if (roughness > 0.08) - { - float coneRadius = roughness * roughness * 0.42; - float3 glossyR = SampleConeWorld(mirrorR, coneRadius, rng); - R = SafeNormalizeOr(lerp(mirrorR, glossyR, saturate(roughness * 0.85)), mirrorR); + float localReflectionEnergy = ComputeSpecularReflectionLocalEnergy(worldPos, N); + if (localReflectionEnergy <= 0.001) + return 0.0; - if (dot(N, R) <= 0.001) - R = mirrorR; - } - - float NoR = saturate(dot(N, R)); - float3 fresnel = FresnelSchlick(NoV, F0); + float reflectionMaxT = ComputeSpecularReflectionMaxDistance(worldPos); float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NoR); float3 reflectionOrigin = worldPos + N * normalBias + R * (gShadowBias * 0.5); float hitT = 0.0; uint materialFlags = 0u; - bool hit = TraceSpecularReflection(reflectionOrigin, R, 1000000.0, hitT, materialFlags); + bool hit = TraceSpecularReflection(reflectionOrigin, R, reflectionMaxT, hitT, materialFlags); float3 reflectedRadiance = 0.0; + float distanceFade = 1.0; if (!hit) { - reflectedRadiance = GetSkyRadiance(R); + // 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; } else { + distanceFade = ComputeSpecularReflectionDistanceFade(hitT, reflectionMaxT); float3 rayHitPos = reflectionOrigin + R * hitT; uint2 hitPixel = pixel; @@ -4714,13 +4783,25 @@ float3 EstimateRayTracedSpecularReflection( } } - float cavityFade = lerp(0.48, 1.0, saturate(cavity)); - float roughReflectionVisibility = saturate(1.0 - roughness * 0.68); + 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; - if (roughReflectionVisibility <= 0.001) + if (reflectionStrength <= 0.001) return 0.0; - return clamp(reflectedRadiance * fresnel * cavityFade * roughReflectionVisibility, 0.0, 8.0); + return clamp( + reflectedRadiance * + fresnel * + reflectionStrength * + cavityFade * + reflectionScale * + localReflectionEnergy * + distanceFade, + 0.0, + 3.5); } )" R"( @@ -4843,15 +4924,14 @@ void RayGen() cavity, reflectionRng); - float3 pbrDiffuseAlbedo = PbrDiffuseAlbedoFromPhongSpecularInput(baseAlbedo, specularAlbedo); - float3 albedo = pbrDiffuseAlbedo * cavity; + float3 albedo = baseAlbedo * cavity; float3 finalColor = (albedo * lightingAccum) + specularAccum + reflectedSpecular; if (gMaxBounces > 1u) { // reactiveFinalGather is incoming indirect radiance. Apply the primary // diffuse albedo here, matching the regular lighting path. - finalColor += pbrDiffuseAlbedo * reactiveFinalGather; + finalColor += baseAlbedo * reactiveFinalGather; } // Volumetric light scattering is radiance in the camera ray, not surface @@ -4867,7 +4947,7 @@ void RayGen() // the eventual swap-chain/backbuffer is LDR. finalColor += emissiveSurface + emissiveBloom; - gOutputTex[pixel] = float4(max(finalColor, 0.0), albedoSample.a); + gOutputTex[pixel] = float4(max(finalColor * ao, 0.0), albedoSample.a); } )"; diff --git a/neo/engine/renderer/draw_dx.cpp b/neo/engine/renderer/draw_dx.cpp index 50cddbe6..14b4df25 100644 --- a/neo/engine/renderer/draw_dx.cpp +++ b/neo/engine/renderer/draw_dx.cpp @@ -176,7 +176,7 @@ void RB_DXDrawInteractions(void) const float r = srcLight.shaderParms[SHADERPARM_RED]; const float g = srcLight.shaderParms[SHADERPARM_GREEN]; const float b = srcLight.shaderParms[SHADERPARM_BLUE]; - const float intensity = 2.0f; + const float intensity = 4.0f; glRaytracingLight_t light = {}; bool supported = true; @@ -187,9 +187,9 @@ void RB_DXDrawInteractions(void) vLight->globalLightOrigin.x, vLight->globalLightOrigin.y, vLight->globalLightOrigin.z, - srcLight.lightRadius[0] * 1.7f, - srcLight.lightRadius[1] * 1.7f, - srcLight.lightRadius[2] * 1.7f, + srcLight.lightRadius[0] * 1.4f, + srcLight.lightRadius[1] * 1.4f, + srcLight.lightRadius[2] * 1.4f, r, g, b, intensity); diff --git a/neo/quake4/game/Game_local.cpp b/neo/quake4/game/Game_local.cpp index 3e8af040..0f5cf98a 100644 --- a/neo/quake4/game/Game_local.cpp +++ b/neo/quake4/game/Game_local.cpp @@ -601,6 +601,8 @@ void idGameLocal::Init( void ) { // RAVEN END networkSystem->AddSortFunction( filterByMod ); + + bse->Init(); } /* @@ -626,6 +628,8 @@ void idGameLocal::Shutdown( void ) { FlushBanList(); // RAVEN END + bse->Shutdown(); + Printf( "--------------- Game Shutdown ---------------\n" ); networkSystem->RemoveSortFunction( filterByMod );