Added lit particle emissive support.

This commit is contained in:
Justin Marshall
2026-05-24 14:07:55 -07:00
parent 56bb2f004f
commit 4e6d4a281f
49 changed files with 34640 additions and 87 deletions
+138 -28
View File
@@ -3899,45 +3899,55 @@ float3 LoadEmissiveRadianceClamped(int2 p)
{
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
p = clamp(p, int2(0, 0), maxPixel);
return CompressEmissiveRadiance(gEmissiveTex.Load(int3(p, 0)).rgb, 7.50);
float3 radiance = CompressEmissiveRadiance(gEmissiveTex.Load(int3(p, 0)).rgb, 18.0);
return min(radiance + sqrt(max(radiance, 0.0)) * 0.35, 22.0);
}
float3 EstimateEmissiveBloomAtPixel(uint2 pixel)
{
int2 p = int2(pixel);
// Strong threshold-free bloom. This stays direct/bloom-only: no extra
// TraceRay calls, and no emissive lighting contribution. The goal is to make
// visible glow maps read as hot emissive surfaces in the DXR output.
// Strong threshold-free bloom in the ray output. Particle emissive lighting
// is injected separately below; this is the visible camera halo.
float3 bloom = 0.0;
bloom += LoadEmissiveRadianceClamped(p) * 0.180;
bloom += LoadEmissiveRadianceClamped(p) * 0.320;
bloom += LoadEmissiveRadianceClamped(p + int2( 1, 0)) * 0.220;
bloom += LoadEmissiveRadianceClamped(p + int2(-1, 0)) * 0.220;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 1)) * 0.220;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -1)) * 0.220;
bloom += LoadEmissiveRadianceClamped(p + int2( 1, 0)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2(-1, 0)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 1)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -1)) * 0.420;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, 2)) * 0.135;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, 2)) * 0.135;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, -2)) * 0.135;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, -2)) * 0.135;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, 2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, 2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2( 2, -2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2(-2, -2)) * 0.260;
bloom += LoadEmissiveRadianceClamped(p + int2( 4, 0)) * 0.090;
bloom += LoadEmissiveRadianceClamped(p + int2(-4, 0)) * 0.090;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 4)) * 0.090;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -4)) * 0.090;
bloom += LoadEmissiveRadianceClamped(p + int2( 4, 0)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2(-4, 0)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 4)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -4)) * 0.180;
bloom += LoadEmissiveRadianceClamped(p + int2( 8, 0)) * 0.060;
bloom += LoadEmissiveRadianceClamped(p + int2(-8, 0)) * 0.060;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 8)) * 0.060;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -8)) * 0.060;
bloom += LoadEmissiveRadianceClamped(p + int2( 8, 0)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2(-8, 0)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 8)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -8)) * 0.120;
bloom += LoadEmissiveRadianceClamped(p + int2( 14, 0)) * 0.035;
bloom += LoadEmissiveRadianceClamped(p + int2(-14, 0)) * 0.035;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 14)) * 0.035;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -14)) * 0.035;
bloom += LoadEmissiveRadianceClamped(p + int2( 14, 0)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2(-14, 0)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 14)) * 0.075;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -14)) * 0.075;
return bloom * 0.78;
bloom += LoadEmissiveRadianceClamped(p + int2( 24, 0)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2(-24, 0)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 24)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -24)) * 0.045;
bloom += LoadEmissiveRadianceClamped(p + int2( 42, 0)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2(-42, 0)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, 42)) * 0.025;
bloom += LoadEmissiveRadianceClamped(p + int2( 0, -42)) * 0.025;
return min(bloom * 1.45, 28.0);
}
float3 SafeNormalizeOr(float3 v, float3 fallback)
@@ -3948,6 +3958,103 @@ float3 SafeNormalizeOr(float3 v, float3 fallback)
return v * rsqrt(lenSq);
}
bool TryScreenParticleWorldCandidate(float4 clip, float3 receiverPos, inout float bestDistSq, inout float3 bestWorld)
{
float4 worldA = mul(clip, gInvViewProj);
if (abs(worldA.w) > 1e-6)
{
float3 candidate = worldA.xyz / worldA.w;
float distSq = dot(candidate - receiverPos, candidate - receiverPos);
if (distSq < bestDistSq && distSq > 1e-4 && distSq < 147456.0)
{
bestDistSq = distSq;
bestWorld = candidate;
}
}
float4 worldB = mul(gInvViewProj, clip);
if (abs(worldB.w) > 1e-6)
{
float3 candidate = worldB.xyz / worldB.w;
float distSq = dot(candidate - receiverPos, candidate - receiverPos);
if (distSq < bestDistSq && distSq > 1e-4 && distSq < 147456.0)
{
bestDistSq = distSq;
bestWorld = candidate;
}
}
return bestDistSq < 147456.0;
}
bool TryReconstructScreenParticleWorld(uint2 particlePixel, float particleDepth, float3 receiverPos, out float3 particleWorld)
{
float2 uv = ((float2)particlePixel + 0.5) / max(gScreenSize.xy, float2(1.0, 1.0));
float x = uv.x * 2.0 - 1.0;
float yUp = 1.0 - uv.y * 2.0;
float yDown = uv.y * 2.0 - 1.0;
float bestDistSq = 1.0e30;
particleWorld = receiverPos;
TryScreenParticleWorldCandidate(float4(x, yUp, particleDepth, 1.0), receiverPos, bestDistSq, particleWorld);
TryScreenParticleWorldCandidate(float4(x, yDown, particleDepth, 1.0), receiverPos, bestDistSq, particleWorld);
return bestDistSq < 147456.0;
}
float3 EstimateScreenSpaceEmissiveParticleLighting(uint2 pixel, float3 worldPos, float3 N)
{
static const int2 kParticleLightTaps[17] =
{
int2( 0, 0),
int2( 7, 0), int2( -7, 0), int2( 0, 7), int2( 0, -7),
int2( 10, 10), int2(-10, 10), int2( 10, -10), int2(-10, -10),
int2( 18, 4), int2(-18, -4), int2( 4, 18), int2( -4, -18),
int2( 30, 0), int2(-30, 0), int2( 0, 30), int2( 0, -30)
};
int2 maxPixel = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
float pixelScale = max(1.0, round(min(gScreenSize.x, gScreenSize.y) / 720.0));
float3 lighting = 0.0;
[unroll]
for (uint i = 0u; i < 17u; ++i)
{
int2 sp = int2(pixel) + int2(round((float2)kParticleLightTaps[i] * pixelScale));
sp = clamp(sp, int2(0, 0), maxPixel);
float4 particleEmission = gEmissiveTex.Load(int3(sp, 0));
if (particleEmission.a <= 1.5)
continue;
float particleDepth = saturate(particleEmission.a - 2.0);
float3 particleWorld;
if (!TryReconstructScreenParticleWorld(uint2(sp), particleDepth, worldPos, particleWorld))
continue;
float3 toParticle = particleWorld - worldPos;
float distSq = dot(toParticle, toParticle);
if (distSq <= 1e-4 || distSq >= 147456.0)
continue;
float dist = sqrt(distSq);
float3 L = toParticle / dist;
float NoL = saturate(dot(N, L));
if (NoL <= 0.01)
continue;
float visibility = TraceVisibilityBiased(worldPos, N, L, dist);
if (visibility <= 0.0)
continue;
float distanceFade = saturate(1.0 - dist / 640.0);
distanceFade *= distanceFade;
float attenuation = distanceFade / (1.0 + distSq * 0.000075);
lighting += CompressEmissiveRadiance(particleEmission.rgb, 24.0) * (NoL * visibility * attenuation);
}
return lighting * 9.0;
}
bool ProjectClipToGBufferCandidate(
float4 clipPos,
bool flipY,
@@ -4151,7 +4258,8 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3
specularOut = specAccum * 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)
{
specularOut = 0.0;
@@ -4175,7 +4283,8 @@ float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 base
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)
{
specularOut = 0.0;
@@ -5304,6 +5413,7 @@ void RayGen()
skyVis,
ambientSkyVis,
specularAccum);
lightingAccum += EstimateScreenSpaceEmissiveParticleLighting(pixel, worldPos, N);
// Only the indirect bounce path uses per-SPP randomness now. Direct lights,
// AO, sky visibility, cavity, and final gather are deterministic and should