Editor: Added ability to hide/unhide utility brushes and target lines.

Tweaked path tracing so lighting is more realistic.
This commit is contained in:
Justin Marshall
2026-05-11 09:35:14 -07:00
parent e7ff9997ce
commit 4da1776627
2 changed files with 273 additions and 43 deletions
+104 -42
View File
@@ -3374,6 +3374,35 @@ float3 LoadSceneSpecularAlbedo(uint2 pixel, float3 baseAlbedo)
return Doom3PseudoSpecularMask(baseAlbedo);
}
float ComputeDiffuseLightingTerm(float3 N, float3 L)
{
// Keep the original light volume/range attenuation exactly where it is, but
// make the surface response less gamey. The old 0.28-0.32 Half-Lambert wrap
// pushed too much light around silhouettes and into back-facing normal-map
// detail. This smaller squared wrap keeps Doom/idTech readability while
// giving a more Lambert-like, physically plausible rolloff.
float rawNoL = dot(normalize(N), normalize(L));
if (gEnableHalfLambert != 0u)
{
const float REALISTIC_WRAP = 0.12;
float wrapped = saturate((rawNoL + REALISTIC_WRAP) / (1.0 + REALISTIC_WRAP));
return wrapped * wrapped;
}
return saturate(rawNoL);
}
float EstimateSpecularRoughness(float3 specularAlbedo)
{
// No roughness map is available in this G-buffer path, so infer a stable
// perceptual roughness from the specular map strength. Brighter spec maps
// get tighter highlights; dark/missing maps stay broad and subdued.
float peak = SpecularPeak3(saturate(specularAlbedo));
return clamp(lerp(0.68, 0.34, peak), 0.28, 0.72);
}
float3 ComputeSpecular(
float3 N,
float3 V,
@@ -3387,47 +3416,66 @@ 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);
float NoL = saturate(dot(N, L));
float NoV = saturate(dot(N, V));
if (atten <= 0.0 || shadow <= 0.0)
if (NoL <= 1.0e-4 || NoV <= 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 NoH = saturate(dot(N, H));
float VoH = saturate(dot(V, H));
float specTerm = Doom3SpecularLookup(NdotH) * lightFacing * viewFacing;
if (specTerm <= 1.0e-5)
if (NoH <= 1.0e-4 || VoH <= 1.0e-4)
return 0.0;
float3 specMask = saturate(specularAlbedo);
float specPeak = SpecularPeak3(specMask);
if (specPeak <= 0.001)
return 0.0;
// 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;
float roughness = EstimateSpecularRoughness(specMask);
float a = roughness * roughness;
float a2 = max(a * a, 1.0e-4);
const float PI = 3.14159265;
// GGX/Trowbridge-Reitz distribution with Smith masking and Schlick Fresnel.
// This is still intentionally stylized for the existing idTech-style assets,
// but it produces more believable view-dependent highlights than the previous
// additive lookup lobe and does not alter light attenuation distance.
float dDenom = NoH * NoH * (a2 - 1.0) + 1.0;
float D = a2 / max(PI * dDenom * dDenom, 1.0e-4);
float k = ((roughness + 1.0) * (roughness + 1.0)) * 0.125;
float Gv = NoV / max(NoV * (1.0 - k) + k, 1.0e-4);
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);
float specTerm = (D * G) / max(4.0 * NoL * NoV, 1.0e-4);
specTerm = min(specTerm, 5.0);
const float SPECULAR_ENERGY_SCALE = 2.35;
float3 specular =
lightColor *
lightIntensity *
atten *
shadow *
specMask *
NoL *
F *
specTerm *
DOOM3_SPECULAR_SCALE;
SPECULAR_ENERGY_SCALE;
return clamp(specular, 0.0, 8.0);
}
@@ -3768,17 +3816,16 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3
float3 L = toLight / dist;
float wrap = 0.28;
float NdotLWrap = saturate((dot(N, L) + wrap) / (1.0 + wrap));
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && NdotLWrap > 0.0001)
if (Lgt.samples != 0u && diffuseNoL > 0.0001)
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
if (Lgt.pointRadiusPad <= 0.5)
specAccum += ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * NdotLWrap * shadow);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float invSamples = 1.0 / (float)sampleCount;
@@ -3798,17 +3845,16 @@ float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 base
float3 L = toLight / dist;
float atten = ComputeSpotLightAttenuation(worldPos, Lgt);
float wrap = 0.28;
float NdotLWrap = saturate((dot(N, L) + wrap) / (1.0 + wrap));
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && NdotLWrap > 0.0001 && atten > 0.0)
if (Lgt.samples != 0u && diffuseNoL > 0.0001 && 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);
return Lgt.color * (Lgt.intensity * atten * NdotLWrap * shadow);
return Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut)
@@ -3852,7 +3898,7 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
continue;
float3 L = sampleVec / sampleDist;
float NdotL = saturate(dot(N, L));
float NdotL = ComputeDiffuseLightingTerm(N, L);
if (NdotL <= 0.0)
continue;
@@ -4019,8 +4065,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
if (atten <= 0.0)
return 0.0;
float wrap = 0.32;
float nDotL = saturate((dot(hitN, L) + wrap) / (1.0 + wrap));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
@@ -4035,8 +4080,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
if (atten <= 0.0)
return 0.0;
float wrap = 0.32;
float nDotL = saturate((dot(hitN, L) + wrap) / (1.0 + wrap));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
@@ -4053,7 +4097,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
return 0.0;
float3 L = toCenter / centerDist;
float nDotL = saturate(dot(hitN, L));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
if (nDotL <= 0.0)
return 0.0;
@@ -4507,8 +4551,8 @@ float3 PathTraceDeterministicLighting(
float3 skyColor = skyColorRGB * (0.35 + 0.65 * upness);
float3 lightingAccum = gAmbientColor.rgb * (gAmbientColor.a * 0.04);
lightingAccum += skyColor * (0.70 * skyVis);
lightingAccum += ambientSkyVis * (skyColorRGB * 0.15);
lightingAccum += skyColor * (0.42 * skyVis);
lightingAccum += ambientSkyVis * (skyColorRGB * 0.12);
// Glow-map emissive is direct/bloom-only for now. It is added after lighting
// in RayGen and is intentionally not injected into lightingAccum.
@@ -4805,6 +4849,24 @@ float3 EstimateRayTracedSpecularReflection(
}
)"
R"(
float3 ApplyRealisticOutputCurve(float3 color)
{
// Final photographic shoulder only: it does not change light radius or
// attenuation, but it prevents intense local lights/specular/bloom from
// clipping into a flat white patch. Values below 1.0 are left untouched.
color = max(color, 0.0);
float peak = max(max(color.r, color.g), color.b);
if (peak > 1.0)
{
float over = peak - 1.0;
float shoulderPeak = 1.0 + over / (1.0 + over * 0.38);
color *= shoulderPeak / max(peak, 1.0e-5);
}
return max(color, 0.0);
}
[shader("raygeneration")]
void RayGen()
{
@@ -4822,7 +4884,7 @@ void RayGen()
if (depthSample <= 0.0 || depthSample >= 1.0)
{
gOutputTex[pixel] = float4(albedoSample.rgb + emissiveSurface + emissiveBloom, albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(albedoSample.rgb + emissiveSurface + emissiveBloom), albedoSample.a);
return;
}
@@ -4840,7 +4902,7 @@ void RayGen()
if (isUnlit)
{
gOutputTex[pixel] = float4(baseAlbedo + emissiveSurface + emissiveBloom, albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(baseAlbedo + emissiveSurface + emissiveBloom), albedoSample.a);
return;
}
@@ -4851,8 +4913,8 @@ void RayGen()
// then reuse them for every stochastic GI sample.
float cavity = ComputeCavity(pixel, worldPos, N);
float ao = ComputeAmbientOcclusion(worldPos, N, pixel);
float skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel); // jmarshall - fix me later
float ambientSkyVis = TraceStraightUpToSky(worldPos, N);
float skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel);
float ambientSkyVis = 0; //TraceStraightUpToSky(worldPos, N);
float microShadow = lerp(0.75, 1.0, cavity);
float3 reactiveFinalGather = 0.0;
@@ -4947,7 +5009,7 @@ void RayGen()
// the eventual swap-chain/backbuffer is LDR.
finalColor += emissiveSurface + emissiveBloom;
gOutputTex[pixel] = float4(max(finalColor * ao, 0.0), albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(finalColor), albedoSample.a);
}
)";