diff --git a/neo/engine/opengl/gl_d3d12raylight.cpp b/neo/engine/opengl/gl_d3d12raylight.cpp index face37a4..a7cb7382 100644 --- a/neo/engine/opengl/gl_d3d12raylight.cpp +++ b/neo/engine/opengl/gl_d3d12raylight.cpp @@ -2930,8 +2930,7 @@ 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 @@ -2940,7 +2939,8 @@ 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,29 +3305,25 @@ float ComputeCavity(uint2 pixel, float3 worldPos, float3 N) return 1.0 - cavity * 0.18; } -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); -} +static const float PBR_PI = 3.14159265; 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. + // Normal path: alpha is the raster G-buffer validity bit. This preserves + // authored black Phong spec maps: black+alpha means intentionally no specular. if (specSample.a > 0.5) return true; @@ -3342,36 +3338,75 @@ 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 Doom3PseudoSpecularMask(baseAlbedo); + 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; } float3 ComputeSpecular( @@ -3387,47 +3422,40 @@ 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 = dot(N, L); - float NdotV = dot(N, V); - - if (atten <= 0.0 || shadow <= 0.0) + float NdotL = saturate(dot(N, L)); + float NdotV = saturate(dot(N, V)); + if (NdotL <= 1.0e-4 || NdotV <= 1.0e-4) 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)); - float specTerm = Doom3SpecularLookup(NdotH) * lightFacing * viewFacing; - if (specTerm <= 1.0e-5) + float3 F0 = PbrF0FromPhongSpecularInput(specularAlbedo); + if (SpecularPeak3(F0) <= 1.0e-5) return 0.0; - float3 specMask = saturate(specularAlbedo); + float roughness = PbrRoughnessFromPhongSpecularInput(specularAlbedo); + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmithGGX(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(VdotH, F0); - // 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; + float3 specularBRDF = (D * G) * F / max(4.0 * NdotV * NdotL, 1.0e-4); - float3 specular = - lightColor * - lightIntensity * - atten * - shadow * - specMask * - specTerm * - DOOM3_SPECULAR_SCALE; + // 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; return clamp(specular, 0.0, 8.0); } @@ -4192,7 +4220,9 @@ 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. - return clamp(hitAlbedo * max(lighting, 0.0), 0.0, 16.0); + float3 hitSpecularInput = LoadSceneSpecularAlbedo(hitPixel, hitAlbedo); + float3 hitDiffuseAlbedo = PbrDiffuseAlbedoFromPhongSpecularInput(hitAlbedo, hitSpecularInput); + return clamp(hitDiffuseAlbedo * max(lighting, 0.0), 0.0, 16.0); } )" @@ -4593,30 +4623,36 @@ float3 EstimateRayTracedSpecularReflection( N = SafeNormalizeOr(N, float3(0.0, 0.0, 1.0)); V = SafeNormalizeOr(V, -N); - // 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) + float3 F0 = PbrF0FromPhongSpecularInput(specularAlbedo); + float f0Peak = SpecularPeak3(F0); + if (f0Peak <= 1.0e-5) return 0.0; float NoV = saturate(dot(N, V)); - float3 R = SafeNormalizeOr(reflect(-V, N), N); - float NoR = saturate(dot(N, R)); - if (NoR <= 0.001) + float3 mirrorR = SafeNormalizeOr(reflect(-V, N), N); + float NoMirrorR = saturate(dot(N, mirrorR)); + if (NoMirrorR <= 0.001) return 0.0; - // 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); + float roughness = PbrRoughnessFromPhongSpecularInput(specularAlbedo); + + // 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); + + if (dot(N, R) <= 0.001) + R = mirrorR; + } + + float NoR = saturate(dot(N, R)); + float3 fresnel = FresnelSchlick(NoV, F0); float normalBias = lerp(gShadowBias * 3.0, gShadowBias * 0.75, NoR); float3 reflectionOrigin = worldPos + N * normalBias + R * (gShadowBias * 0.5); @@ -4678,16 +4714,13 @@ float3 EstimateRayTracedSpecularReflection( } } - 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; + float cavityFade = lerp(0.48, 1.0, saturate(cavity)); + float roughReflectionVisibility = saturate(1.0 - roughness * 0.68); - if (reflectionStrength <= 0.001) + if (roughReflectionVisibility <= 0.001) return 0.0; - return clamp(reflectedRadiance * fresnel * reflectionStrength * cavityFade * reflectionScale, 0.0, 8.0); + return clamp(reflectedRadiance * fresnel * cavityFade * roughReflectionVisibility, 0.0, 8.0); } )" R"( @@ -4810,14 +4843,15 @@ void RayGen() cavity, reflectionRng); - float3 albedo = baseAlbedo * cavity; + float3 pbrDiffuseAlbedo = PbrDiffuseAlbedoFromPhongSpecularInput(baseAlbedo, specularAlbedo); + float3 albedo = pbrDiffuseAlbedo * 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 += baseAlbedo * reactiveFinalGather; + finalColor += pbrDiffuseAlbedo * reactiveFinalGather; } // Volumetric light scattering is radiance in the camera ray, not surface diff --git a/neo/engine/renderer/Material.cpp b/neo/engine/renderer/Material.cpp index 1385f91c..bc1fa53f 100644 --- a/neo/engine/renderer/Material.cpp +++ b/neo/engine/renderer/Material.cpp @@ -2843,7 +2843,7 @@ void idMaterial::ParseMaterial(idLexer& src) { } else if (!token.Icmp("materialType")) { src.ReadTokenOnLine(&token); - materialType = static_cast(declManager->FindType(static_cast(6), token.c_str(), true)); + materialType = static_cast(declManager->FindType(static_cast(6), token.c_str(), false)); if (materialType && materialType->IsImplicit()) { common->Warning("UNKNOWN: materialType '%s' in '%s'", token.c_str(), GetName()); }