From 4a53e6a186d2a98e783bbbde17915e1000d0426a Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Sun, 10 May 2026 03:41:48 -0700 Subject: [PATCH] Quality enhacements. --- neo/engine/opengl/gl_d3d12raylight.cpp | 307 ++++++++++++++++++++++--- neo/engine/opengl/gl_d3d12shim.cpp | 2 +- 2 files changed, 282 insertions(+), 27 deletions(-) diff --git a/neo/engine/opengl/gl_d3d12raylight.cpp b/neo/engine/opengl/gl_d3d12raylight.cpp index 6bac4935..face37a4 100644 --- a/neo/engine/opengl/gl_d3d12raylight.cpp +++ b/neo/engine/opengl/gl_d3d12raylight.cpp @@ -2713,6 +2713,18 @@ void BounceClosestHit(inout BouncePayload payload, in BuiltInTriangleIntersectio payload.pad0 = 0; } +[shader("closesthit")] +void ReflectionClosestHit(inout BouncePayload payload, in BuiltInTriangleIntersectionAttributes attr) +{ + // Specular rays are view rays, not diffuse/visibility rays. They should + // report the first reflected surface even when that surface was tagged as + // glass/alpha, otherwise glass panes/sprites vanish from mirror-like hits. + payload.hit = 1; + payload.hitT = RayTCurrent(); + payload.materialFlags = DecodeInstanceMaterialFlags(); + payload.pad0 = 0; +} + float TraceShadow(float3 origin, float3 dir, float maxT) { RayDesc ray; @@ -2766,6 +2778,35 @@ bool TraceBounce(float3 origin, float3 dir, float maxT, out float hitT, out uint return payload.hit != 0; } +bool TraceSpecularReflection(float3 origin, float3 dir, float maxT, out float hitT, out uint materialFlags) +{ + RayDesc ray; + ray.Origin = origin; + ray.Direction = dir; + ray.TMin = 0.001; + ray.TMax = maxT; + + BouncePayload payload; + payload.hit = 0; + payload.hitT = 0.0; + payload.materialFlags = 0; + payload.pad0 = 0; + + TraceRay( + gSceneBVH, + RAY_FLAG_NONE, + 0xFF, + 2, + 0, + 1, + ray, + payload); + + hitT = payload.hitT; + materialFlags = payload.materialFlags; + return payload.hit != 0; +} + float Hash12(float2 p) { float3 p3 = frac(float3(p.xyx) * 0.1031); @@ -2889,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 @@ -3265,13 +3307,39 @@ float ComputeCavity(uint2 pixel, float3 worldPos, float3 N) float Doom3SpecularLookup(float x) { - // Doom 3 used a lookup table for specular falloff. - // This approximates the classic broad idTech4 highlight. + // 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); - // Broad enough to read like Doom 3 plastic/metal, - // not razor-sharp PBR. - return pow(x, 16.0); + 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); +} + +bool LooksLikeAuthoredSpecularSample(float4 specSample, float3 baseAlbedo) +{ + // Normal path: alpha is the raster G-buffer validity bit. + if (specSample.a > 0.5) + return true; + + // Tolerant path: some raster paths/specular inputs write RGB but leave the + // validity alpha at zero. Do not treat the fallback albedo descriptor as a + // spec map; when no specular texture is bound, specSample.rgb == baseAlbedo. + float3 specRgb = saturate(specSample.rgb); + float3 baseRgb = saturate(baseAlbedo); + float rgbPeak = SpecularPeak3(specRgb); + float rgbDiff = length(specRgb - baseRgb); + + return rgbPeak > 0.025 && rgbDiff > 0.035; } float3 Doom3PseudoSpecularMask(float3 baseAlbedo) @@ -3297,7 +3365,10 @@ float3 LoadSceneSpecularAlbedo(uint2 pixel, float3 baseAlbedo) // 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". - if (specSample.a > 0.5) + // 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); @@ -3323,18 +3394,25 @@ float3 ComputeSpecular( float NdotL = dot(N, L); float NdotV = dot(N, V); - // Doom 3 specular should not show on the wrong side of the surface, - // but once it passes this gate, do not multiply the final spec by NdotL again. - // The old code did that and made highlights collapse too aggressively. - if (NdotL <= 0.0 || NdotV <= 0.0 || atten <= 0.0 || shadow <= 0.0) + 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 = normalize(L + V); + float3 H = Doom3SafeNormalizeOr(L + V, N); float NdotH = saturate(dot(N, H)); - float specTerm = Doom3SpecularLookup(NdotH); + float specTerm = Doom3SpecularLookup(NdotH) * lightFacing * viewFacing; + if (specTerm <= 1.0e-5) + return 0.0; float3 specMask = saturate(specularAlbedo); @@ -4391,7 +4469,11 @@ float3 ApplyPrimaryDiffusePost(float3 lightingAccum, float ao, float microShadow float3 ApplyPrimarySpecularPost(float3 specularAccum, float ao, bool isSkeletal) { - specularAccum *= ao; + // AO is a diffuse/ambient visibility term here. Multiplying specular by AO + // directly made highlights disappear on creases, props, and normal-mapped + // surfaces. Keep a mild occlusion tint, but let direct-light specular read. + float specAo = lerp(0.58, 1.0, saturate(ao)); + specularAccum *= specAo; //if (isSkeletal) // specularAccum *= 1.15; @@ -4467,6 +4549,148 @@ float3 PathTraceDeterministicLighting( return ApplyPrimaryDiffusePost(lightingAccum, ao, microShadow, isSkeletal); } + +float3 EstimateFallbackReflectionHitRadiance(float3 hitPos, float3 hitNormal, float3 incomingViewDir) +{ + // Reflection rays can hit off-screen or camera-hidden TLAS geometry. In that + // case this pass has no per-triangle material/normal table, so use a neutral + // lit card instead of returning black. This keeps reflected objects visible + // without pretending every unknown hit is a perfect emissive surface. + incomingViewDir = SafeNormalizeOr(incomingViewDir, -hitNormal); + hitNormal = SafeNormalizeOr(hitNormal, incomingViewDir); + + if (dot(hitNormal, incomingViewDir) < 0.0) + hitNormal = -hitNormal; + + float upness = saturate(hitNormal.z * 0.5 + 0.5); + float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.035); + lighting += GetSkyRadiance(hitNormal) * (0.12 + 0.08 * upness); + lighting += GetSkyRadiance(SafeNormalizeOr(reflect(-incomingViewDir, hitNormal), hitNormal)) * 0.055; + + [loop] + for (uint i = 0; i < gLightCount; ++i) + { + lighting += EstimateFastBounceLight(hitPos, hitNormal, gLights[i]) * 0.78; + } + + const float3 NEUTRAL_UNKNOWN_ALBEDO = float3(0.58, 0.58, 0.58); + return clamp(NEUTRAL_UNKNOWN_ALBEDO * max(lighting, 0.0), 0.0, 10.0); +} + +float3 EstimateRayTracedSpecularReflection( + uint2 pixel, + float3 worldPos, + float3 N, + float3 V, + float3 baseAlbedo, + float3 specularAlbedo, + float cavity, + inout uint rng) +{ + if (gEnableSpecular == 0u || gMaxBounces <= 1u) + return 0.0; + + 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) + 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) + 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 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); + + float3 reflectedRadiance = 0.0; + + if (!hit) + { + reflectedRadiance = GetSkyRadiance(R); + } + else + { + float3 rayHitPos = reflectionOrigin + R * hitT; + + uint2 hitPixel = pixel; + float3 hitPos = rayHitPos; + float3 hitNormal = SafeNormalizeOr(-R, N); + float3 hitAlbedo = float3(0.55, 0.55, 0.55); + uint hitGeoFlag = GEOMETRY_FLAG_NONE; + + bool hasGBufferMaterial = TryFetchGBufferAtRayHit( + rayHitPos, + hitPixel, + hitPos, + hitNormal, + hitAlbedo, + hitGeoFlag); + + if (dot(hitNormal, -R) < 0.0) + hitNormal = -hitNormal; + + if (hasGBufferMaterial) + { + float3 hitV = SafeNormalizeOr(-R, V); + reflectedRadiance = EstimateDirectLightingForBounceHit( + hitPixel, + hitPos, + hitNormal, + hitV, + hitAlbedo, + hitGeoFlag, + rng); + + // Reflected glow maps should be visible in the reflection, but still + // remain direct radiance; this does not turn emissive into GI. + float3 hitEmissive = CompressEmissiveRadiance(gEmissiveTex.Load(int3(hitPixel, 0)).rgb, 6.50); + reflectedRadiance += hitEmissive; + } + else + { + // Off-screen/hidden TLAS hits lack material data in this G-buffer-only + // material path. Use a neutral lit fallback instead of a dim sky-only + // tint, otherwise many real reflected objects vanish completely. + reflectedRadiance = EstimateFallbackReflectionHitRadiance(rayHitPos, hitNormal, -R); + } + } + + 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 (reflectionStrength <= 0.001) + return 0.0; + + return clamp(reflectedRadiance * fresnel * reflectionStrength * cavityFade * reflectionScale, 0.0, 8.0); +} +)" +R"( [shader("raygeneration")] void RayGen() { @@ -4513,7 +4737,7 @@ 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 skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel); // jmarshall - fix me later float ambientSkyVis = TraceStraightUpToSky(worldPos, N); float microShadow = lerp(0.75, 1.0, cavity); @@ -4575,8 +4799,19 @@ void RayGen() lightingAccum += ApplyPrimaryDiffusePost(indirectLighting, ao, microShadow, isSkeletal); } + uint reflectionRng = InitRng(pixel, gFrameIndex, 0x5EECu); + float3 reflectedSpecular = EstimateRayTracedSpecularReflection( + pixel, + worldPos, + N, + V, + baseAlbedo, + specularAlbedo, + cavity, + reflectionRng); + float3 albedo = baseAlbedo * cavity; - float3 finalColor = (albedo * lightingAccum) + specularAccum; + float3 finalColor = (albedo * lightingAccum) + specularAccum + reflectedSpecular; if (gMaxBounces > 1u) { @@ -5447,7 +5682,7 @@ static int glRaytracingLightingCreateStateObject(void) if (!dxil) return 0; - D3D12_EXPORT_DESC exports[7] = {}; + D3D12_EXPORT_DESC exports[8] = {}; exports[0].Name = L"RayGen"; exports[1].Name = L"ShadowMiss"; exports[2].Name = L"ShadowAnyHit"; @@ -5455,6 +5690,7 @@ static int glRaytracingLightingCreateStateObject(void) exports[4].Name = L"BounceMiss"; exports[5].Name = L"BounceAnyHit"; exports[6].Name = L"BounceClosestHit"; + exports[7].Name = L"ReflectionClosestHit"; D3D12_DXIL_LIBRARY_DESC libDesc = {}; D3D12_SHADER_BYTECODE libBytecode = {}; @@ -5464,7 +5700,7 @@ static int glRaytracingLightingCreateStateObject(void) libDesc.NumExports = _countof(exports); libDesc.pExports = exports; - D3D12_HIT_GROUP_DESC hitGroups[2] = {}; + D3D12_HIT_GROUP_DESC hitGroups[3] = {}; hitGroups[0].HitGroupExport = L"ShadowHitGroup"; hitGroups[0].AnyHitShaderImport = L"ShadowAnyHit"; hitGroups[0].ClosestHitShaderImport = L"ShadowClosestHit"; @@ -5475,6 +5711,10 @@ static int glRaytracingLightingCreateStateObject(void) hitGroups[1].ClosestHitShaderImport = L"BounceClosestHit"; hitGroups[1].Type = D3D12_HIT_GROUP_TYPE_TRIANGLES; + hitGroups[2].HitGroupExport = L"ReflectionHitGroup"; + hitGroups[2].ClosestHitShaderImport = L"ReflectionClosestHit"; + hitGroups[2].Type = D3D12_HIT_GROUP_TYPE_TRIANGLES; + D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {}; shaderConfig.MaxPayloadSizeInBytes = 16; // BouncePayload: uint + float + uint + uint. shaderConfig.MaxAttributeSizeInBytes = 8; @@ -5485,7 +5725,7 @@ static int glRaytracingLightingCreateStateObject(void) D3D12_LOCAL_ROOT_SIGNATURE localRS = {}; localRS.pLocalRootSignature = g_glRaytracingLighting.localRootSig.Get(); - D3D12_STATE_SUBOBJECT subobjects[8] = {}; + D3D12_STATE_SUBOBJECT subobjects[9] = {}; UINT sub = 0; subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY; @@ -5500,6 +5740,10 @@ static int glRaytracingLightingCreateStateObject(void) subobjects[sub].pDesc = &hitGroups[1]; ++sub; + subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP; + subobjects[sub].pDesc = &hitGroups[2]; + ++sub; + subobjects[sub].Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG; subobjects[sub].pDesc = &shaderConfig; ++sub; @@ -5518,11 +5762,12 @@ static int glRaytracingLightingCreateStateObject(void) L"ShadowMiss", L"ShadowHitGroup", L"BounceMiss", - L"BounceHitGroup" + L"BounceHitGroup", + L"ReflectionHitGroup" }; D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION assoc = {}; - assoc.pSubobjectToAssociate = &subobjects[5]; + assoc.pSubobjectToAssociate = &subobjects[6]; assoc.NumExports = _countof(localExports); assoc.pExports = localExports; @@ -5554,8 +5799,9 @@ static int glRaytracingLightingCreateShaderTables(void) void* bounceMissId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"BounceMiss"); void* shadowHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"ShadowHitGroup"); void* bounceHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"BounceHitGroup"); + void* reflectionHitId = g_glRaytracingLighting.rtStateProps->GetShaderIdentifier(L"ReflectionHitGroup"); - if (!raygenId || !shadowMissId || !bounceMissId || !shadowHitId || !bounceHitId) + if (!raygenId || !shadowMissId || !bounceMissId || !shadowHitId || !bounceHitId || !reflectionHitId) { glRaytracingFatal("Failed to fetch shader identifiers"); return 0; @@ -5564,7 +5810,7 @@ static int glRaytracingLightingCreateShaderTables(void) const UINT shaderIdSize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES; const UINT recordSize = (UINT)glRaytracingAlignUp(shaderIdSize, D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT); const UINT missTableSize = recordSize * 2u; - const UINT hitTableSize = recordSize * 2u; + const UINT hitTableSize = recordSize * 3u; g_glRaytracingLighting.raygenTable = glRaytracingCreateBuffer( g_glRaytracingCmd.device.Get(), @@ -5609,6 +5855,7 @@ static int glRaytracingLightingCreateShaderTables(void) memset(temp.data(), 0, temp.size()); memcpy(temp.data(), shadowHitId, shaderIdSize); memcpy(temp.data() + recordSize, bounceHitId, shaderIdSize); + memcpy(temp.data() + recordSize * 2u, reflectionHitId, shaderIdSize); glRaytracingMapCopy(g_glRaytracingLighting.hitTable.resource.Get(), temp.data(), hitTableSize); return 1; @@ -5981,7 +6228,7 @@ static bool glRaytracingLightingExecuteInternal( rays.MissShaderTable.SizeInBytes = shaderRecordSize * 2u; rays.MissShaderTable.StrideInBytes = shaderRecordSize; rays.HitGroupTable.StartAddress = g_glRaytracingLighting.hitTable.gpuVA; - rays.HitGroupTable.SizeInBytes = shaderRecordSize * 2u; + rays.HitGroupTable.SizeInBytes = shaderRecordSize * 3u; rays.HitGroupTable.StrideInBytes = shaderRecordSize; rays.Width = pass->width; rays.Height = pass->height; @@ -6495,10 +6742,18 @@ void glRaytracingLightingSetSpecularInput(ID3D12Resource* texture, DXGI_FORMAT f { std::lock_guard lock(g_glRaytracingMutex); - g_glRaytracingLighting.specularTexture = texture; - g_glRaytracingLighting.specularFormat = (format == DXGI_FORMAT_UNKNOWN) + DXGI_FORMAT newFormat = (format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_R8G8B8A8_UNORM : format; + + if (g_glRaytracingLighting.specularTexture != texture || + g_glRaytracingLighting.specularFormat != newFormat) + { + glRaytracingLightingResetDenoiseHistory(); + } + + g_glRaytracingLighting.specularTexture = texture; + g_glRaytracingLighting.specularFormat = newFormat; } bool glRaytracingLightingExecuteForScene(const glRaytracingLightingPassDesc_t* pass, glRaytracingSceneHandle_t worldHandle) diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index 6fa9f46b..54bf9db8 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -12707,7 +12707,7 @@ void glLightScene(glRaytracingSceneHandle_t sceneHandle) raySpp, activeMaxBounces, useDLSSRayReconstruction ? 0 : 1, - useDLSSRayReconstruction ? 0.0f : 1.0f); + useDLSSRayReconstruction ? 0.0f : 0.45f); glRaytracingLightingSetEmissiveInput(sceneEmissive, QD3D12_EmissiveFormat); glRaytracingLightingSetSpecularInput(sceneSpecular, QD3D12_SpecularAlbedoFormat);