Lighting tweaks and BSE crash fix.

This commit is contained in:
Justin Marshall
2026-05-10 11:55:28 -07:00
parent ed959fa4c0
commit e31d905358
3 changed files with 221 additions and 137 deletions
+213 -133
View File
@@ -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);
}
)";
+4 -4
View File
@@ -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);
+4
View File
@@ -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 );