Switched tp PBR

This commit is contained in:
Justin Marshall
2026-05-10 04:34:03 -07:00
parent 4a53e6a186
commit ed959fa4c0
2 changed files with 133 additions and 99 deletions
+132 -98
View File
@@ -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
+1 -1
View File
@@ -2843,7 +2843,7 @@ void idMaterial::ParseMaterial(idLexer& src) {
}
else if (!token.Icmp("materialType")) {
src.ReadTokenOnLine(&token);
materialType = static_cast<const rvDeclMatType*>(declManager->FindType(static_cast<declType_t>(6), token.c_str(), true));
materialType = static_cast<const rvDeclMatType*>(declManager->FindType(static_cast<declType_t>(6), token.c_str(), false));
if (materialType && materialType->IsImplicit()) {
common->Warning("UNKNOWN: materialType '%s' in '%s'", token.c_str(), GetName());
}