POM Relief Mapping

This commit is contained in:
Justin Marshall
2026-05-24 18:27:11 -07:00
parent 26dd7e649e
commit 754f5b4aa0
2 changed files with 508 additions and 152 deletions
+258 -48
View File
@@ -2643,7 +2643,7 @@ struct Light
// The scalar radius above is still kept as a max/fallback range for point lights,
// as the influence range for rect lights, and as the far clip distance for spot lights.
float3 pointRadius;
float pointRadiusPad; // non-zero disables specular for this light
float pointRadiusPad;
uint iesTextureIndex; // 0 = none, 1..8 = gIesTextures index + 1
float iesStrength;
@@ -3745,7 +3745,7 @@ float EstimateSpecularRoughness(float3 specularAlbedo)
// get tighter highlights, but keep a floor high enough that POM/normal-map
// detail cannot collapse into one-pixel GGX fireflies.
float peak = SpecularPeak3(saturate(specularAlbedo));
return clamp(lerp(0.70, 0.42, peak), 0.38, 0.74);
return clamp(lerp(0.66, 0.40, peak), 0.36, 0.72);
}
float3 LimitSpecularPeak(float3 c, float peakLimit)
@@ -3757,7 +3757,7 @@ float3 LimitSpecularPeak(float3 c, float peakLimit)
return c;
}
float3 ComputeSpecular(
float3 ComputeSpecularShaped(
float3 N,
float3 V,
float3 L,
@@ -3765,7 +3765,13 @@ float3 ComputeSpecular(
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo)
float3 specularAlbedo,
float roughnessBias,
float specularWrap,
float f0Blend,
float fresnelToWhiteScale,
float energyScale,
float peakLimit)
{
if (gEnableSpecular == 0)
return 0.0;
@@ -3777,7 +3783,10 @@ float3 ComputeSpecular(
V = normalize(V);
L = normalize(L);
float NoL = saturate(dot(N, L));
float rawNoL = dot(N, L);
float NoL = (specularWrap > 0.0)
? saturate((rawNoL + specularWrap) / (1.0 + specularWrap))
: saturate(rawNoL);
float NoV = saturate(dot(N, V));
if (NoL <= 1.0e-4 || NoV <= 1.0e-4)
@@ -3795,7 +3804,7 @@ float3 ComputeSpecular(
if (specPeak <= 0.001)
return 0.0;
float roughness = EstimateSpecularRoughness(specMask);
float roughness = clamp(EstimateSpecularRoughness(specMask) + roughnessBias, 0.38, 0.90);
float a = roughness * roughness;
float a2 = max(a * a, 1.0e-4);
@@ -3813,11 +3822,11 @@ float3 ComputeSpecular(
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);
float3 F0 = saturate(lerp(float3(0.025, 0.025, 0.025), specMask, saturate(f0Blend)));
float3 F = F0 + (1.0 - F0) * (pow(1.0 - VoH, 5.0) * saturate(fresnelToWhiteScale));
float specTerm = (D * G) / max(4.0 * NoL * NoV, 1.0e-4);
specTerm = min(specTerm, 3.0);
specTerm = min(specTerm, 3.6);
const float SPECULAR_ENERGY_SCALE = 2.35;
@@ -3829,9 +3838,155 @@ float3 ComputeSpecular(
NoL *
F *
specTerm *
SPECULAR_ENERGY_SCALE;
(SPECULAR_ENERGY_SCALE * max(energyScale, 0.0));
return LimitSpecularPeak(specular, 4.0);
return LimitSpecularPeak(specular, peakLimit);
}
float3 ComputeSpecular(
float3 N,
float3 V,
float3 L,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo)
{
return ComputeSpecularShaped(
N,
V,
L,
lightColor,
lightIntensity,
atten,
shadow,
specularAlbedo,
-0.04,
0.12,
0.82,
1.0,
1.42,
4.8);
}
float EstimateSkeletalSubsurfaceMask(float3 baseAlbedo, float3 specularAlbedo)
{
// Doom 3 characters have no authored SSS/thickness maps. Use a conservative
// skin/flesh proxy from the visible albedo and damp it on shiny/metal-like
// pieces so armor and gear do not glow like wax.
baseAlbedo = saturate(baseAlbedo);
float luma = dot(baseAlbedo, float3(0.299, 0.587, 0.114));
float warm = saturate((baseAlbedo.r - baseAlbedo.b * 0.55) * 2.35);
float redBias = saturate((baseAlbedo.r - max(baseAlbedo.g, baseAlbedo.b)) * 3.25 + 0.18);
float fleshRange = smoothstep(0.035, 0.18, luma) * (1.0 - smoothstep(0.82, 1.0, luma));
float matte = 1.0 - saturate(SpecularPeak3(specularAlbedo) * 0.85);
return saturate((0.08 + warm * 0.42 + redBias * 0.38) * fleshRange * lerp(0.55, 1.0, matte));
}
float3 ComputeSkeletalSpecularAlbedo(float3 baseAlbedo, float3 specularAlbedo)
{
float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo);
// Old character spec maps were authored for Doom 3's stylized interaction
// lights. In the path tracer they behave like wet plastic unless we make the
// BRDF much softer and cap the F0 on skin/flesh-like areas.
float3 spec = saturate(specularAlbedo);
float3 softSpec = spec * lerp(0.48, 0.24, subsurfaceMask);
float cap = lerp(0.16, 0.075, subsurfaceMask);
return min(softSpec, float3(cap, cap, cap));
}
float3 ComputeSkeletalSpecular(
float3 N,
float3 V,
float3 L,
float3 baseAlbedo,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float3 specularAlbedo)
{
float subsurfaceMask = EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo);
float3 characterSpec = ComputeSkeletalSpecularAlbedo(baseAlbedo, specularAlbedo);
// Character MD5 specular maps often encode art-directed shine, not material
// F0. Shape the whole BRDF, including grazing Fresnel, so faces and bodies
// keep a soft oily skin response instead of reflecting like sealed plastic.
return ComputeSpecularShaped(
N,
V,
L,
lightColor,
lightIntensity,
atten,
shadow,
characterSpec,
lerp(0.10, 0.20, subsurfaceMask),
0.0,
0.58,
lerp(0.32, 0.14, subsurfaceMask),
lerp(0.40, 0.24, subsurfaceMask),
lerp(0.55, 0.18, subsurfaceMask));
}
float ComputeSubsurfaceShape(float3 N, float3 V, float3 L)
{
N = normalize(N);
V = normalize(V);
L = normalize(L);
float NoL = dot(N, L);
float wrap = saturate((NoL + 0.58) / 1.58);
wrap = wrap * wrap;
// Transmission becomes visible around the terminator and when the light is
// behind the viewed surface, which is the bit missing most painfully on MD5s.
float back = saturate((-NoL + 0.22) / 1.22);
float forward = pow(saturate(dot(-L, V)), 2.25);
float rim = pow(saturate(1.0 - abs(dot(N, V))), 1.60);
return saturate(wrap * 0.42 + back * (0.55 + forward * 0.85) + rim * wrap * 0.18);
}
float3 ComputeSkeletalSubsurfaceRadiance(
float3 N,
float3 V,
float3 L,
float3 baseAlbedo,
float3 lightColor,
float lightIntensity,
float atten,
float shadow,
float subsurfaceMask)
{
if (subsurfaceMask <= 0.001 || atten <= 0.0 || lightIntensity <= 0.0)
return 0.0;
float shape = ComputeSubsurfaceShape(N, V, L);
if (shape <= 0.001)
return 0.0;
float softShadow = lerp(shadow, 1.0, 0.18 * subsurfaceMask);
float3 scatterTint = saturate(lerp(
float3(1.0, 0.30, 0.18),
saturate(baseAlbedo) * float3(1.22, 0.68, 0.52) + float3(0.10, 0.03, 0.02),
0.58));
const float SUBSURFACE_DIRECT_SCALE = 0.34;
return clamp(
lightColor *
lightIntensity *
atten *
softShadow *
scatterTint *
shape *
(subsurfaceMask * SUBSURFACE_DIRECT_SCALE),
0.0,
2.25);
}
)"
R"(
@@ -4255,10 +4410,12 @@ float3 EstimatePathTracedSky(float3 worldPos, float3 N, inout uint rng)
accum /= (float)bounceSamples;
return accum * 0.55;
}
float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut)
)"
R"(
float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toCenter = Lgt.position - worldPos;
float centerDist = length(toCenter);
@@ -4284,6 +4441,7 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3
float3 diffuseAccum = 0.0;
float3 specAccum = 0.0;
float3 sssAccum = 0.0;
[loop]
for (uint s = 0u; s < sampleCount; ++s)
@@ -4303,24 +4461,28 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && diffuseNoL > 0.0001)
if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001))
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
if (Lgt.pointRadiusPad <= 0.5)
specAccum += ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
specAccum += isSkeletal
? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo)
: ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
sssAccum += ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float invSamples = 1.0 / (float)sampleCount;
specularOut = specAccum * invSamples;
subsurfaceOut = sssAccum * invSamples;
return diffuseAccum * invSamples;
}
)"
R"(
float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut)
float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toLight = Lgt.position - worldPos;
float dist = length(toLight);
@@ -4333,19 +4495,22 @@ float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 base
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && diffuseNoL > 0.0001 && atten > 0.0)
if (Lgt.samples != 0u && (diffuseNoL > 0.0001 || subsurfaceMask > 0.001) && 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);
specularOut = isSkeletal
? ComputeSkeletalSpecular(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo)
: ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
subsurfaceOut = ComputeSkeletalSubsurfaceRadiance(N, V, L, baseAlbedo, Lgt.color, Lgt.intensity, atten, shadow, subsurfaceMask);
return Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
)"
R"(
float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut)
float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, bool isSkeletal, float subsurfaceMask, Light Lgt, inout uint rng, out float3 specularOut, out float3 subsurfaceOut)
{
specularOut = 0.0;
subsurfaceOut = 0.0;
float3 toCenter = Lgt.position - worldPos;
float centerDist = length(toCenter);
@@ -4365,6 +4530,7 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
float3 diffuseAccum = 0.0;
float3 specAccum = 0.0;
float3 sssAccum = 0.0;
[loop]
for (uint s = 0u; s < sampleCount; ++s)
@@ -4385,7 +4551,8 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
float3 L = sampleVec / sampleDist;
float NdotL = ComputeDiffuseLightingTerm(N, L);
if (NdotL <= 0.0)
float sssShape = (subsurfaceMask > 0.001) ? ComputeSubsurfaceShape(N, V, L) : 0.0;
if (NdotL <= 0.0 && sssShape <= 0.001)
continue;
float faceTerm = (Lgt.twoSided != 0)
@@ -4399,9 +4566,18 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
if (Lgt.samples != 0u)
shadow = TraceVisibilityBiased(worldPos, N, L, sampleDist);
if (Lgt.pointRadiusPad <= 0.5)
{
specAccum += ComputeSpecular(
specAccum += (isSkeletal
? ComputeSkeletalSpecular(
N,
V,
L,
baseAlbedo,
Lgt.color,
Lgt.intensity * faceTerm,
1.0,
shadow,
specularAlbedo)
: ComputeSpecular(
N,
V,
L,
@@ -4409,14 +4585,25 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
Lgt.intensity * faceTerm,
1.0,
shadow,
specularAlbedo) * atten;
}
specularAlbedo)) * atten;
sssAccum += ComputeSkeletalSubsurfaceRadiance(
N,
V,
L,
baseAlbedo,
Lgt.color,
Lgt.intensity * faceTerm,
atten,
shadow,
subsurfaceMask);
diffuseAccum += clamp(Lgt.color * (Lgt.intensity * NdotL * faceTerm * atten * shadow), 0.0, 4.0);
}
float invSamples = 1.0 / (float)sampleCount;
specularOut = specAccum * invSamples;
subsurfaceOut = sssAccum * invSamples;
return diffuseAccum * invSamples;
}
@@ -4618,29 +4805,31 @@ float3 EstimateUnresolvedBounceRadiance(float3 hitPos, float3 hitN, float3 albed
return clamp(albedo * max(lighting, 0.0), 0.0, 6.0);
}
float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, Light Lgt, inout uint rng)
float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, bool hitIsSkeletal, Light Lgt, inout uint rng)
{
float3 spec = 0.0;
float3 sss = 0.0;
float3 diffuse = 0.0;
float3 hitSpecularAlbedo = LoadSceneSpecularAlbedo(hitPixel, hitAlbedo);
float subsurfaceMask = hitIsSkeletal ? EstimateSkeletalSubsurfaceMask(hitAlbedo, hitSpecularAlbedo) * 0.45 : 0.0;
// Use the same visibility-capable direct-light samplers as the primary hit.
// This supplies proper next-event estimation at secondary hits instead of the
// old unoccluded light-list approximation.
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
diffuse = PathTraceDirectPointLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec);
diffuse = PathTraceDirectPointLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
diffuse = PathTraceDirectSpotLight(hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec);
diffuse = PathTraceDirectSpotLight(hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
diffuse = PathTraceDirectRectLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, Lgt, rng, spec);
diffuse = PathTraceDirectRectLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitSpecularAlbedo, hitIsSkeletal, subsurfaceMask, Lgt, rng, spec, sss);
}
return clamp(diffuse, 0.0, 12.0);
return clamp(diffuse + sss * 0.35, 0.0, 12.0);
}
float3 EstimateBounceSkyLighting(float3 hitPos, float3 hitN, inout uint rng)
@@ -4731,7 +4920,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
uint lightIndex = (start + c * stride) % gLightCount;
Light Lgt = gLights[lightIndex];
float3 fast = EstimateFastBounceLight(hitPos, hitN, Lgt);
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, lightingRng);
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, hitIsSkeletal, Lgt, lightingRng);
lighting += shadowed - fast;
}
}
@@ -5056,7 +5245,10 @@ float3 ApplyPrimarySpecularPost(float3 specularAccum, float ao, bool isSkeletal)
// surfaces. Keep a mild occlusion tint, but let direct-light specular read.
float specAo = lerp(0.58, 1.0, saturate(ao));
if (isSkeletal)
{
specAo = lerp(specAo, 1.0, 0.50);
specularAccum *= 0.42;
}
specularAccum *= specAo;
//if (isSkeletal)
@@ -5077,9 +5269,11 @@ float3 PathTraceDeterministicLighting(
float ao,
float skyVis,
float ambientSkyVis,
out float3 specularAccum)
out float3 specularAccum,
out float3 subsurfaceAccum)
{
specularAccum = 0.0;
subsurfaceAccum = 0.0;
float microShadow = lerp(0.75, 1.0, cavity);
@@ -5104,32 +5298,37 @@ float3 PathTraceDeterministicLighting(
// argument only because the functions share the same signature as stochastic
// helpers; it is not consumed by PathTraceDirect* in the current shader.
uint directRng = InitRng(pixel, 0u, 0xD17EC7u);
float subsurfaceMask = isSkeletal ? EstimateSkeletalSubsurfaceMask(baseAlbedo, specularAlbedo) : 0.0;
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
Light Lgt = gLights[i];
float3 spec = 0.0;
float3 sss = 0.0;
float3 diffuse = 0.0;
if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_POINT)
{
diffuse = PathTraceDirectPointLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec);
diffuse = PathTraceDirectPointLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
{
diffuse = PathTraceDirectSpotLight(worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec);
diffuse = PathTraceDirectSpotLight(worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
{
diffuse = PathTraceDirectRectLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, Lgt, directRng, spec);
diffuse = PathTraceDirectRectLight(pixel, worldPos, N, V, baseAlbedo, specularAlbedo, isSkeletal, subsurfaceMask, Lgt, directRng, spec, sss);
}
lightingAccum += diffuse;
specularAccum += spec;
subsurfaceAccum += sss;
}
specularAccum = ApplyPrimarySpecularPost(specularAccum, ao, isSkeletal);
if (isSkeletal)
subsurfaceAccum *= lerp(0.72, 1.0, saturate(ao));
return ApplyPrimaryDiffusePost(lightingAccum, ao, microShadow, isSkeletal);
}
@@ -5193,7 +5392,7 @@ float ComputeSpecularReflectionLocalEnergy(float3 worldPos, float3 N)
continue;
float3 toLight = SafeNormalizeOr(Lgt.position - worldPos, N);
float facing = saturate(dot(N, toLight));
float facing = saturate((dot(N, toLight) + 0.18) / 1.18);
float lightPeak = max(max(Lgt.color.r, Lgt.color.g), Lgt.color.b);
energy += atten * facing * Lgt.intensity * lightPeak;
@@ -5201,7 +5400,7 @@ float ComputeSpecularReflectionLocalEnergy(float3 worldPos, float3 N)
// Keep reflections tied to nearby light contribution instead of becoming
// a global mirror pass.
return saturate(energy * 0.08);
return saturate(energy * 0.12);
}
float ComputeSpecularReflectionMaxDistance(float3 worldPos)
@@ -5264,11 +5463,18 @@ float3 EstimateRayTracedSpecularReflection(
float3 baseAlbedo,
float3 specularAlbedo,
float cavity,
bool isSkeletal,
inout uint rng)
{
if (gEnableSpecular == 0u || gMaxBounces <= 1u)
return 0.0;
// Character materials should get soft direct specular, not mirror-like local
// reflections. The G-buffer material model has no skeletal roughness map, so
// reflected TLAS hits make skin/armor read much wetter than intended.
if (isSkeletal)
return 0.0;
N = SafeNormalizeOr(N, float3(0.0, 0.0, 1.0));
V = SafeNormalizeOr(V, -N);
@@ -5280,7 +5486,7 @@ float3 EstimateRayTracedSpecularReflection(
bool hasSpecularMap = LooksLikeAuthoredSpecularSample(rawSpecular, baseAlbedo);
float specPeak = SpecularPeak3(specularAlbedo);
if (specPeak <= 0.015)
if (specPeak <= 0.006)
return 0.0;
float NoV = saturate(dot(N, V));
@@ -5319,8 +5525,8 @@ float3 EstimateRayTracedSpecularReflection(
{
// 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;
reflectedRadiance = GetSkyRadiance(R) * 0.055;
distanceFade = 0.28;
}
else
{
@@ -5373,13 +5579,13 @@ float3 EstimateRayTracedSpecularReflection(
}
reflectedRadiance *= reflectedSurfaceAverageColor;
reflectedRadiance = CompressEmissiveRadiance(reflectedRadiance, hasSpecularMap ? 5.0 : 3.5);
reflectedRadiance = CompressEmissiveRadiance(reflectedRadiance, hasSpecularMap ? 3.5 : 2.5);
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;
? saturate(specPeak * 1.30)
: saturate((specPeak - 0.08) * 0.70);
float reflectionScale = hasSpecularMap ? 0.85 : 0.52;
if (reflectionStrength <= 0.001)
return 0.0;
@@ -5393,7 +5599,7 @@ float3 EstimateRayTracedSpecularReflection(
localReflectionEnergy *
distanceFade,
0.0,
3.5);
2.6);
}
)"
R"(
@@ -5488,6 +5694,7 @@ void RayGen()
}
float3 specularAccum = 0.0;
float3 subsurfaceAccum = 0.0;
float3 lightingAccum = PathTraceDeterministicLighting(
pixel,
worldPos,
@@ -5500,7 +5707,8 @@ void RayGen()
ao,
skyVis,
ambientSkyVis,
specularAccum);
specularAccum,
subsurfaceAccum);
lightingAccum += EstimateScreenSpaceEmissiveParticleLighting(pixel, worldPos, N);
// Only the indirect bounce path uses per-SPP randomness now. Direct lights,
@@ -5540,6 +5748,7 @@ void RayGen()
baseAlbedo,
specularAlbedo,
cavity,
isSkeletal,
reflectionRng);
float3 albedo = baseAlbedo * cavity;
@@ -5554,6 +5763,7 @@ void RayGen()
float outputAo = isSkeletal ? lerp(ao, 1.0, 0.55) : ao;
finalColor *= clamp(outputAo + 0.3, 0.0, 1.0);
finalColor += subsurfaceAccum;
// Volumetric light scattering is radiance in the camera ray, not surface
// reflectance, so add it after surface albedo/specular composition. Because
+250 -104
View File
@@ -2971,7 +2971,7 @@ cbuffer DrawCB : register(b0)
// Pixel-shader POM depth in UV space. Keep this conservative: legacy idTech
// normal maps often do not have a true height channel, so the POM path below
// gates fallback height by local detail before applying any offset.
#define gParallaxScale (clamp(gNormalMapStrength, 0.0, 4.0) * 0.020)
#define gParallaxScale (clamp(gNormalMapStrength, 0.0, 4.0) * 0.028)
#define gCameraWorldPos gCameraPomPad.xyz
#define gCameraPomValid gCameraPomPad.w
#define gUseNeuralPOM gNeuralPomPad.x
@@ -3002,6 +3002,7 @@ cbuffer DrawCB : register(b0)
#define QD3D12_TESS_TARGET_EDGE_PIXELS 28.0
#define QD3D12_TESS_DISTANCE_NEAR 512.0
#define QD3D12_TESS_DISTANCE_FAR 2200.0
#define QD3D12_GEOMETRY_FLAG_SKELETAL 1u
#define QD3D12_POM_DISTANCE_NEAR 384.0
#define QD3D12_POM_DISTANCE_FAR 1800.0
@@ -3443,6 +3444,43 @@ float QD3D12_GetPomDepth(float2 uv)
return saturate(1.0 - QD3D12_GetPomHeightFromSamples(uv));
}
float QD3D12_GetPomDepthLOD(float2 uv, float lod)
{
float4 nm = gNormalMap.SampleLevel(gSamp2, uv, lod);
float authored = QD3D12_AuthoredPomAlphaWeight(nm.a);
if (authored > 0.5)
return saturate(1.0 - nm.a);
float3 decoded = nm.xyz * 2.0 - 1.0;
decoded.y *= gNormalMapYSign;
float slope = saturate(length(decoded.xy));
float normalHeight = saturate(0.5 + (pow(slope, 0.80) - 0.35) * 0.42);
if (gUseTex0 > 0.5)
{
float normalDetailGate = saturate((slope - 0.040) * 8.0);
if (normalDetailGate > 0.001)
{
float mipScale = exp2(lod);
float2 texel = QD3D12_NormalMapTexelSize() * mipScale;
float lumC = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv, lod).rgb);
float lumL = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv - float2(texel.x, 0.0), lod).rgb);
float lumR = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv + float2(texel.x, 0.0), lod).rgb);
float lumU = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv - float2(0.0, texel.y), lod).rgb);
float lumD = QD3D12_Luma(gTex0.SampleLevel(gSamp0, uv + float2(0.0, texel.y), lod).rgb);
float lumAvg = (lumL + lumR + lumU + lumD) * 0.25;
float localContrast = abs(lumC - lumAvg);
float diffuseWeight = saturate((localContrast - 0.018) * 16.0) * normalDetailGate;
float diffuseHeight = saturate(0.5 + (lumC - lumAvg) * 1.85);
normalHeight = lerp(normalHeight, diffuseHeight, diffuseWeight * 0.28);
}
}
return saturate(1.0 - normalHeight);
}
float QD3D12_GetPomConfidence(float2 uv)
{
float minHeight = 0.5;
@@ -3484,6 +3522,13 @@ float QD3D12_GetRegularPomFade(VSOut i, float2 baseUv)
return saturate(confidence * distanceFade * grazingFade);
}
struct QD3D12PomTraceResult
{
float2 uv;
float confidence;
float visibility;
};
// Keep the runtime network bounded. Most payloads do not need the full 128-wide
// MLP in a fixed-function material pass; clamping evaluation here prevents a
@@ -3750,106 +3795,145 @@ QD3D12NeuralPOMResult QD3D12_EvaluateNeuralPOM(VSOut i, float2 baseUv)
r.active = 1.0;
return r;
}
)HLSL"
R"HLSL(
QD3D12PomTraceResult QD3D12_TraceReliefPOM(VSOut i, float2 baseUv)
{
QD3D12PomTraceResult r;
r.uv = baseUv;
r.confidence = 0.0;
r.visibility = 1.0;
if (gUseNormalMap < 0.5)
return r;
if (gAlphaBlendPass > 0.5)
return r;
if (gUseTex0 > 0.5)
{
float baseAlpha = gTex0.SampleLevel(gSamp0, baseUv, 0.0).a;
if (baseAlpha < 0.985)
return r;
}
float minHeight = 0.5;
float maxHeight = 0.5;
float authoredWeight = 0.0;
QD3D12_GetPomHeightStats(baseUv, minHeight, maxHeight, authoredWeight);
float heightRange = maxHeight - minHeight;
float threshold = lerp(0.055, 0.014, authoredWeight);
float confidence = saturate((heightRange - threshold) / max(0.18 - threshold, 0.001));
confidence = confidence * confidence * (3.0 - 2.0 * confidence);
if (confidence <= 0.035)
return r;
float3 n, t, b;
QD3D12_BuildPixelTBN(i, n, t, b);
float cameraConfidence = (gCameraPomValid >= 0.5) ? 1.0 : 0.35;
float3 rawViewWS = (gCameraPomValid >= 0.5) ? (gCameraWorldPos - i.worldPos) : (-i.worldPos);
float3 viewWS = QD3D12_SafeNormalize(rawViewWS, n);
float NoV = dot(n, viewWS);
if (NoV <= 0.025)
return r;
float3 viewTS = QD3D12_SafeNormalize(float3(dot(viewWS, t), dot(viewWS, b), NoV), float3(0.0, 0.0, 1.0));
float ndotv = saturate(viewTS.z);
float viewDistance = (gCameraPomValid >= 0.5) ? max(length(gCameraWorldPos - i.worldPos), 1.0) : max(abs(i.currClip.w), 1.0);
float distanceFade = 1.0 - smoothstep(QD3D12_POM_DISTANCE_NEAR, QD3D12_POM_DISTANCE_FAR, viewDistance);
float grazingFade = smoothstep(0.055, 0.18, ndotv);
float fade = saturate(confidence * distanceFade * grazingFade * cameraConfidence);
if (fade <= 0.001)
return r;
float2 texel = QD3D12_NormalMapTexelSize();
float2 uvGradX = ddx(baseUv);
float2 uvGradY = ddy(baseUv);
float footprint = max(length(uvGradX / max(texel, float2(1.0e-6, 1.0e-6))), length(uvGradY / max(texel, float2(1.0e-6, 1.0e-6))));
float lod = clamp(log2(max(footprint, 1.0)), 0.0, 5.0);
float depthScale = gParallaxScale * lerp(0.68, 1.18, authoredWeight) * confidence * distanceFade * cameraConfidence;
float vz = max(ndotv, lerp(0.18, 0.10, authoredWeight));
float2 parallaxVector = (viewTS.xy / vz) * depthScale;
float parallaxLen = length(parallaxVector);
float maxParallaxShift = lerp(0.014, 0.046, authoredWeight) * lerp(0.85, 1.18, confidence);
if (parallaxLen > maxParallaxShift && parallaxLen > 1.0e-6)
parallaxVector *= maxParallaxShift / parallaxLen;
float layerCountF = lerp(14.0, 46.0, saturate(1.0 - ndotv));
layerCountF = lerp(10.0, layerCountF, distanceFade);
layerCountF = lerp(layerCountF * 0.70, layerCountF, authoredWeight);
uint layerCount = (uint)clamp(layerCountF + 0.5, 10.0, 48.0);
float invLayerCount = rcp((float)layerCount);
float2 deltaUv = parallaxVector * invLayerCount;
float2 prevUv = baseUv;
float2 uv = baseUv;
float prevRayDepth = 0.0;
float rayDepth = 0.0;
float prevSurfaceDepth = QD3D12_GetPomDepthLOD(baseUv, lod);
float surfaceDepth = prevSurfaceDepth;
[loop]
for (uint layer = 0u; layer < 48u; ++layer)
{
if (layer >= layerCount || rayDepth >= surfaceDepth)
break;
prevUv = uv;
prevRayDepth = rayDepth;
prevSurfaceDepth = surfaceDepth;
uv -= deltaUv;
rayDepth += invLayerCount;
surfaceDepth = QD3D12_GetPomDepthLOD(uv, lod);
}
float after = surfaceDepth - rayDepth;
float before = prevSurfaceDepth - prevRayDepth;
float denom = after - before;
float w = (abs(denom) > 1.0e-5) ? saturate(after / denom) : 0.0;
float2 refinedUv = lerp(uv, prevUv, w);
float2 loUv = uv;
float2 hiUv = prevUv;
[unroll]
for (uint refine = 0u; refine < 5u; ++refine)
{
float2 midUv = (loUv + hiUv) * 0.5;
float midT = dot(baseUv - midUv, parallaxVector) / max(dot(parallaxVector, parallaxVector), 1.0e-8);
float midDepth = saturate(midT);
float midSurface = QD3D12_GetPomDepthLOD(midUv, lod);
if (midDepth < midSurface)
hiUv = midUv;
else
loUv = midUv;
}
refinedUv = lerp(refinedUv, (loUv + hiUv) * 0.5, 0.65);
float2 finalOffset = refinedUv - baseUv;
float maxOffset = maxParallaxShift * 1.05;
float finalLen = length(finalOffset);
if (finalLen > maxOffset && finalLen > 1.0e-6)
refinedUv = baseUv + finalOffset * (maxOffset / finalLen);
r.uv = lerp(baseUv, refinedUv, fade);
r.confidence = fade;
r.visibility = saturate(1.0 - length(r.uv - baseUv) / max(maxParallaxShift, 1.0e-5) * 0.18);
return r;
}
float2 QD3D12_ComputeParallaxUVWithNeural(VSOut i, float2 baseUv, QD3D12NeuralPOMResult nr)
{
if (nr.active > 0.5)
return nr.uv;
if (gUseNormalMap < 0.5)
return baseUv;
float confidence = QD3D12_GetPomConfidence(baseUv);
if (confidence <= 0.05)
return baseUv;
float parallaxScale = gParallaxScale * confidence;
if (abs(parallaxScale) < 1e-6)
return baseUv;
float3 n, t, b;
QD3D12_BuildPixelTBN(i, n, t, b);
// Correct view vector. The old code used -worldPos, which only works when
// the camera is exactly at world origin and causes the obvious offset bugs
// as soon as the camera moves. Keep -worldPos only as a reduced compatibility
// fallback for older paths that cannot derive a perspective camera.
float cameraConfidence = (gCameraPomValid >= 0.5) ? 1.0 : 0.35;
float3 rawViewWS = (gCameraPomValid >= 0.5) ? (gCameraWorldPos - i.worldPos) : (-i.worldPos);
float3 viewWS = QD3D12_SafeNormalize(rawViewWS, n);
float NoV = dot(n, viewWS);
if (NoV <= 0.035)
return baseUv;
float3 viewTS = QD3D12_SafeNormalize(float3(
dot(viewWS, t),
dot(viewWS, b),
NoV
), float3(0.0, 0.0, 1.0));
float ndotv = saturate(viewTS.z);
float vz = max(ndotv, 0.22);
// Fade out before far surfaces shimmer or when the viewing angle is too
// grazing for estimated height maps.
float viewDistance = (gCameraPomValid >= 0.5) ? max(length(gCameraWorldPos - i.worldPos), 1.0) : max(abs(i.currClip.w), 1.0);
float distanceFade = 1.0 - smoothstep(QD3D12_POM_DISTANCE_NEAR, QD3D12_POM_DISTANCE_FAR, viewDistance);
float grazingFade = smoothstep(0.08, 0.22, ndotv);
float scale = parallaxScale * distanceFade * grazingFade * cameraConfidence;
if (scale <= 0.00045)
return baseUv;
float layerCountF = lerp(10.0, 30.0, saturate(1.0 - ndotv));
layerCountF = lerp(8.0, layerCountF, saturate(distanceFade));
uint layerCount = (uint)clamp(layerCountF + 0.5, 8.0, 32.0);
// Offset-limited POM. Divide by a softened z term and clamp the max UV walk;
// this removes the extreme stretched-offset artifacts on steep angles.
// Standard POM walks opposite the view vector in tangent space. The shift
// cap is intentionally tight for generated RGB-only height, looser for real
// authored alpha height.
float2 parallaxVector = (viewTS.xy / vz) * scale;
float parallaxLen = length(parallaxVector);
float maxParallaxShift = lerp(0.016, 0.040, confidence);
if (parallaxLen > maxParallaxShift && parallaxLen > 1e-6)
parallaxVector *= maxParallaxShift / parallaxLen;
float invLayerCount = rcp((float)layerCount);
float2 deltaUv = parallaxVector * invLayerCount;
float2 uv = baseUv;
float2 prevUv = uv;
float currentLayerDepth = 0.0;
float prevLayerDepth = 0.0;
float currentDepth = QD3D12_GetPomDepth(uv);
float prevDepth = currentDepth;
[loop]
for (uint layer = 0u; layer < 32u; ++layer)
{
if (layer >= layerCount || currentLayerDepth >= currentDepth)
break;
prevUv = uv;
prevLayerDepth = currentLayerDepth;
prevDepth = currentDepth;
uv -= deltaUv;
currentLayerDepth += invLayerCount;
currentDepth = QD3D12_GetPomDepth(uv);
}
float afterDepth = currentDepth - currentLayerDepth;
float beforeDepth = prevDepth - prevLayerDepth;
float denom = afterDepth - beforeDepth;
float weight = (abs(denom) > 1e-5) ? saturate(afterDepth / denom) : 0.0;
float2 refinedUv = lerp(uv, prevUv, weight);
// Blend the final result in instead of applying a manual half-vector center
// correction. The center correction was the source of several texture-offset
// bugs on flat/low-confidence areas.
return lerp(baseUv, refinedUv, saturate(distanceFade * grazingFade * confidence));
QD3D12PomTraceResult trace = QD3D12_TraceReliefPOM(i, baseUv);
return trace.uv;
}
)HLSL"
R"HLSL(
@@ -4006,10 +4090,7 @@ float4 BuildSpecularAlbedoCached(VSOut i, QD3D12MaterialEval m)
{
if (gUseSpecularMap > 0.0)
{
float regularPomFade = (gUseNeuralPOM <= 0.5 || m.neural.active <= 0.5)
? QD3D12_GetRegularPomFade(i, i.uv0)
: 0.0;
float2 specUv = lerp(i.uv0, m.uv0, regularPomFade);
float2 specUv = m.uv0;
float4 spec = gSpecularMap.Sample(gSamp4, specUv);
float strength = max(gSpecularMapStrength, 0.0);
@@ -4021,8 +4102,6 @@ float4 BuildSpecularAlbedoCached(VSOut i, QD3D12MaterialEval m)
bool alphaLooksForcedOpaque = (spec.a >= 0.999);
float alphaMask = alphaLooksForcedOpaque ? 1.0 : saturate(spec.a);
float3 specRgb = saturate(spec.rgb) * alphaMask * strength;
if (regularPomFade > 0.0)
specRgb = min(specRgb, float3(0.82, 0.82, 0.82));
return float4(specRgb, 1.0);
}
@@ -4235,11 +4314,42 @@ float QD3D12_TessellationPatchEdgeFade(float3 bary)
return smoothstep(0.0, 0.08, edgeDistance);
}
uint QD3D12_DecodeGeometryFlag(float flag)
{
return (uint)floor(max(flag, 0.0) + 0.5);
}
bool QD3D12_IsSkeletalGeometry(float flag)
{
return (QD3D12_DecodeGeometryFlag(flag) & QD3D12_GEOMETRY_FLAG_SKELETAL) != 0u;
}
float QD3D12_ComputeCharacterTessEdgeFactor(VSOut a, VSOut b)
{
float distanceFade = QD3D12_TessellationEdgeDistanceFade(a, b);
if (distanceFade <= 0.001)
return 1.0;
float edgePixels = QD3D12_EdgeLengthPixels(a.currClip, b.currClip);
float screenTerm = sqrt(max(edgePixels, 1.0) / 24.0);
float strength = clamp(max(gNormalMapStrength, 0.0), 0.0, 4.0);
// Character tessellation is for silhouette/deformation smoothness, not
// normal-map relief. Keep it bounded so animated MD5s do not turn rubbery.
float nearFactor = 1.0 + screenTerm * 2.65 + strength * 0.22;
nearFactor = clamp(nearFactor, 1.0, 8.0);
return clamp(lerp(1.0, nearFactor, distanceFade), QD3D12_TESS_MIN_FACTOR, 8.0);
}
float QD3D12_ComputeNormalMapTessEdgeFactor(VSOut a, VSOut b)
{
if (gUseNormalMap <= 0.5)
return 1.0;
if (QD3D12_IsSkeletalGeometry(a.attr.x) || QD3D12_IsSkeletalGeometry(b.attr.x))
return QD3D12_ComputeCharacterTessEdgeFactor(a, b);
float distanceFade = QD3D12_TessellationEdgeDistanceFade(a, b);
if (distanceFade <= 0.001)
return 1.0;
@@ -4331,6 +4441,38 @@ float4 QD3D12_Interp4(float4 a, float4 b, float4 c, float3 w)
return a * w.x + b * w.y + c * w.z;
}
float3 QD3D12_ProjectPointToTangentPlane(float3 p, float3 planePoint, float3 planeNormal)
{
planeNormal = QD3D12_SafeNormalize(planeNormal, float3(0.0, 0.0, 1.0));
return p - planeNormal * dot(p - planePoint, planeNormal);
}
float3 QD3D12_CharacterPhongTessellate(const OutputPatch<TessCP, 3> patch, float3 bary, float3 linearObjPos, float distanceFade)
{
float3 n0 = QD3D12_SafeNormalize(patch[0].objNormal, float3(0.0, 0.0, 1.0));
float3 n1 = QD3D12_SafeNormalize(patch[1].objNormal, n0);
float3 n2 = QD3D12_SafeNormalize(patch[2].objNormal, n0);
float3 q0 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[0].objPos, n0);
float3 q1 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[1].objPos, n1);
float3 q2 = QD3D12_ProjectPointToTangentPlane(linearObjPos, patch[2].objPos, n2);
float3 phongObjPos = q0 * bary.x + q1 * bary.y + q2 * bary.z;
float normalAgreement = saturate((dot(n0, n1) + dot(n1, n2) + dot(n2, n0)) * 0.1667 + 0.5);
float smoothAmount = 0.78 * distanceFade * smoothstep(0.10, 0.82, normalAgreement);
float3 delta = phongObjPos - linearObjPos;
float maxEdgeLen = max(
length(patch[0].objPos - patch[1].objPos),
max(length(patch[1].objPos - patch[2].objPos), length(patch[2].objPos - patch[0].objPos)));
float maxDelta = max(maxEdgeLen * 0.075, 0.01);
float deltaLen = length(delta);
if (deltaLen > maxDelta)
delta *= maxDelta / max(deltaLen, 1.0e-5);
return linearObjPos + delta * smoothAmount;
}
[domain("tri")]
VSOut DSMain(HSConstOut tessFactors, float3 bary : SV_DomainLocation, const OutputPatch<TessCP, 3> patch)
{
@@ -4353,13 +4495,17 @@ VSOut DSMain(HSConstOut tessFactors, float3 bary : SV_DomainLocation, const Outp
VSOut o;
float3 objNormal = QD3D12_SafeNormalize(i.objNormal, float3(0.0, 0.0, 1.0));
bool isSkeletal = QD3D12_IsSkeletalGeometry(i.attr.x);
float distanceFade = QD3D12_TessellationDisplacementFade(i.currClip);
float3 baseObjPos = isSkeletal
? QD3D12_CharacterPhongTessellate(patch, bary, i.objPos, distanceFade)
: i.objPos;
float height = QD3D12_GetFilteredTessHeight(i.uv0);
float centeredHeight = QD3D12_CleanCenteredTessHeight(height);
float displacementFade = QD3D12_TessellationDisplacementFade(i.currClip);
displacementFade *= QD3D12_TessellationPatchEdgeFade(bary);
float displacementFade = isSkeletal ? 0.0 : distanceFade * QD3D12_TessellationPatchEdgeFade(bary);
float reliefConfidence = QD3D12_GetPomConfidence(i.uv0);
float displacement = centeredHeight * gTessellationDisplacement * displacementFade * reliefConfidence;
float3 displacedObjPos = i.objPos + objNormal * displacement;
float3 displacedObjPos = baseObjPos + objNormal * displacement;
float4 worldPos = mul(gModelMatrix, float4(displacedObjPos, 1.0));
float4 currClip = mul(gMVP, float4(displacedObjPos, 1.0));