2021-09-09 20:42:29 -04:00

403 lines
16 KiB
Plaintext

// #pragma enable_d3d11_debug_symbols
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
#pragma kernel MAIN_1 main=MAIN_1 SINGLE_SCATTERING
#pragma kernel MAIN_S main=MAIN_S MULTIPLE_SCATTERING_GATHER SRC_SS
#pragma kernel MAIN_M main=MAIN_M MULTIPLE_SCATTERING_GATHER SRC_MS
#pragma kernel MAIN_A main=MAIN_A MULTIPLE_SCATTERING_ACCUMULATE
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Sky/PhysicallyBasedSky/PhysicallyBasedSkyCommon.hlsl"
#define TABLE_SIZE uint3(PBRSKYCONFIG_IN_SCATTERED_RADIANCE_TABLE_SIZE_X, \
PBRSKYCONFIG_IN_SCATTERED_RADIANCE_TABLE_SIZE_Y, \
PBRSKYCONFIG_IN_SCATTERED_RADIANCE_TABLE_SIZE_Z)
// Emulate a 4D texture with a "deep" 3D texture.
RW_TEXTURE3D(float4, _AirSingleScatteringTable);
RW_TEXTURE3D(float4, _AerosolSingleScatteringTable);
RW_TEXTURE3D(float4, _MultipleScatteringTable);
RW_TEXTURE3D(float4, _MultipleScatteringTableOrder);
[numthreads(4, 4, 4)]
void main(uint3 dispatchThreadId : SV_DispatchThreadID)
{
const float A = _AtmosphericRadius;
const float R = _PlanetaryRadius;
const uint zTexSize = PBRSKYCONFIG_IN_SCATTERED_RADIANCE_TABLE_SIZE_Z;
const uint zTexCnt = PBRSKYCONFIG_IN_SCATTERED_RADIANCE_TABLE_SIZE_W;
// We don't care about the extremal points for XY, but need the full range of Z values.
const float3 scale = rcp(float3(TABLE_SIZE.xy, 1));
const float3 bias = float3(0.5 * scale.xy, 0);
// Let the hardware and the driver handle the ordering of the computation.
uint3 tableCoord = dispatchThreadId;
uint texId = tableCoord.z / zTexSize; // [0, zTexCnt - 1]
uint texCoord = tableCoord.z & (zTexSize - 1); // [0, zTexSize - 1]
float3 uvw = tableCoord * scale + bias;
// Convention:
// V points towards the camera.
// The normal vector N points upwards (local Z).
// The view vector V spans the local XZ plane.
// The light vector is represented as {phiL, cosThataL} w.r.t. the XZ plane.
float cosChi = UnmapAerialPerspective(uvw.xy).x; // [-1, 1]
float height = UnmapAerialPerspective(uvw.xy).y; // [0, _AtmosphericDepth]
float phiL = PI * saturate(texCoord * rcp(zTexSize - 1)); // [-Pi, Pi]
float NdotL = UnmapCosineOfZenithAngle(saturate(texId * rcp(zTexCnt - 1))); // [-0.5, 1]
float NdotV = -cosChi;
float r = height + R;
float cosHor = ComputeCosineOfHorizonAngle(r);
bool lookAboveHorizon = (cosChi >= cosHor);
bool viewAboveHorizon = (NdotV >= cosHor);
float3 N = float3(0, 0, 1);
float3 V = SphericalToCartesian(0, NdotV);
float3 L = SphericalToCartesian(phiL, NdotL);
float LdotV = dot(L, V);
// float LdotV = SphericalDot(NdotL, phiL, NdotV, 0);
// Set up the ray...
float h = height;
float3 O = r * N;
// Determine the region of integration.
float tMax;
if (lookAboveHorizon)
{
tMax = IntersectSphere(A, cosChi, r).y; // Max root
}
else
{
tMax = IntersectSphere(R, cosChi, r).x; // Min root
}
#ifdef SINGLE_SCATTERING
float4 airTableEntry = 0;
float4 aerosolTableEntry = 0;
// For single scattering with the directional light assumption, planet's shadow forms a shadow volume
// which is shaped like a cylinder with the planet's radius, aligned with the light direction.
// We can use it to optimize the integration process, as the shadowed region has 0 contribution.
float tMin = 0;
float2 tCyl = IntersectRayCylinder(L, R, r, -V);
if (tCyl.y >= 0) // Will exit?
{
// We found an intersection with the shadow volume.
// It could be either the entry or the exit.
if (tCyl.x >= 0) // Will enter?
{
float zHit = dot(L, O + tCyl.x * -V);
if (zHit < 0) // Below?
{
// Stop the ray at the entry.
// We assume that the ray does not exit the shadow volume before exiting the atmosphere.
// The error of that assumption should be reasonably low.
tMax = min(tMax, tCyl.x);
}
}
else // Already entered?
{
bool inShadow = (L.z < 0); // Inside the shadow volume?
if (inShadow)
{
// Start the ray at the exit.
tMin = max(tMin, tCyl.y);
}
}
}
else // Failed to intersect?
{
bool inShadow = (L.z < 0) && ((1 - Sq(L.z)) < (R * rcp(r)));
// If we are inside the shadow volume, we should terminate the ray.
if (inShadow)
{
tMax = -tMin;
}
}
airTableEntry.a = tMax - tMin;
aerosolTableEntry.a = tMax - tMin;
if (tMin >= tMax)
{
_AirSingleScatteringTable[tableCoord] = 0; // One order
_AerosolSingleScatteringTable[tableCoord] = 0; // One order
_MultipleScatteringTable[tableCoord] = 0; // MS orders
return;
}
// Integrate in-scattered radiance along -V.
// Note that we have to evaluate the transmittance integral along -V as well.
// The transmittance integral is pretty smooth (I plotted it in Mathematica).
// However, using a non-linear distribution of samples is still a good idea both
// when looking up (due to the exponential falloff of the coefficients)
// and for horizontal rays (due to the exponential transmittance term).
// It's easy enough to use a simple quadratic remap.
float t0 = tMin;
float3 viewODepthX = ComputeAtmosphericOpticalDepth(r, NdotV, viewAboveHorizon); // Back to start
float3 viewODepth = 0; // Sample to start
if (tMin > 0) // Start changed?
{
float3 P = O + tMin * -V;
// Update these for the step along the ray...
r = max(length(P), R);
height = r - R;
NdotV = dot(normalize(P), V);
NdotL = dot(normalize(P), L);
viewODepth = ComputeAtmosphericOpticalDepth(r, NdotV, viewAboveHorizon) - viewODepthX;
}
float3 lightODepth = ComputeAtmosphericOpticalDepth1(r, NdotL);
// Apply the phase function at runtime.
float3 transm0 = TransmittanceFromOpticalDepth(viewODepth + lightODepth);
float3 airInScatterTerm0 = transm0 * AirScatter(height);
float3 aerosolInScatterTerm0 = transm0 * AerosolScatter(height);
// Eye-balled number of samples.
const int numSamples = 256;
// const int numSamples = 4096;
const float startNdotV = NdotV;
for (int i = 0; i < numSamples; i++)
{
// Use (n = 0.5) when looking down, (n = 1) when looking horizontally,
// and (n = 2) when looking up.
// TODO: use exponential media importance sampling...
float n = pow(2, -startNdotV);
float x = pow(abs((i + 1) * rcp(numSamples)), n); // TODO: Cranley-Patterson Rotation
float t1 = tMin + (tMax - tMin) * x;
float dt = t1 - t0;
float3 P = O + t1 * -V;
// Update these for the step along the ray...
r = max(length(P), R);
height = r - R;
NdotV = dot(normalize(P), V);
NdotL = dot(normalize(P), L);
viewODepth = ComputeAtmosphericOpticalDepth(r, NdotV, viewAboveHorizon) - viewODepthX;
lightODepth = ComputeAtmosphericOpticalDepth1(r, NdotL);
// Compute the amount of in-scattered radiance.
// Evaluate the transmittance integral from 't0' to 't1' using the trapezoid rule.
// Integral{a, b}{Transmittance(0, t) dt} ≈
// (Transmittance(0, a) + Transmittance(0, b)) * (b - a) / 2 =
// (Transmittance(0, a) + Transmittance(0, tMax) / Transmittance(b, tMax)) * (b - a) / 2.
// We handle other terms in a similar way (by pre-multiplying transmittance).
float3 transm1 = TransmittanceFromOpticalDepth(viewODepth + lightODepth);
float3 airInScatterTerm1 = transm1 * AirScatter(height);
float3 aerosolInScatterTerm1 = transm1 * AerosolScatter(height);
// Apply the phase function at runtime.
airTableEntry.rgb += (airInScatterTerm0 + airInScatterTerm1 ) * (0.5 * dt);
aerosolTableEntry.rgb += (aerosolInScatterTerm0 + aerosolInScatterTerm1) * (0.5 * dt);
// Carry these over to the next iteration...
t0 = t1;
airInScatterTerm0 = airInScatterTerm1;
aerosolInScatterTerm0 = aerosolInScatterTerm1;
}
// TODO: deep compositing.
// Note: ground reflection is computed at runtime.
_AirSingleScatteringTable[tableCoord] = airTableEntry; // One order
_AerosolSingleScatteringTable[tableCoord] = aerosolTableEntry; // One order
_MultipleScatteringTable[tableCoord] = float4(0, 0, 0, airTableEntry.a); // MS orders
#elif MULTIPLE_SCATTERING_GATHER
float4 tableEntry;
tableEntry.rgb = 0;
tableEntry.a = tMax;
// Gather the ground contribution.
// The ground is a Lambertian reflector. It is basically a textured sphere light.
// TODO: 50% of the ground is black! And most of the time it's too far away!
// Also, anisotropic Mie scattering will make sure the contribution is near-zero...
{
float cosAperture = -cosHor; // Cosine of the half-angle
// Arbitrary number of samples...
const int numGroundSamples = 89;
for (int i = 0; i < numGroundSamples; i++)
{
float2 f = Fibonacci2d(i, numGroundSamples); // TODO: Cranley-Patterson Rotation
// Construct a direction around the up vector.
float3 dir; float rcpPdf;
SampleCone(f, cosAperture, dir, rcpPdf);
// Flip it upside-down to point towards the sphere light (planet).
float3 gL = -dir;
float t = IntersectSphere(R, gL.z, r).x;
float3 gP = O + t * gL;
float3 gN = normalize(gP);
// Shade the ground.
const float3 gBrdf = INV_PI * _GroundAlbedo.rgb;
// TODO: we compute the same thing for many voxels,
// the only real difference is the phase function...
float3 phase = AtmospherePhaseScatter(dot(gL, V), height);
float3 oDepth = ComputeAtmosphericOpticalDepth1(r, gL.z);
float3 transm = TransmittanceFromOpticalDepth(oDepth);
float weight = rcpPdf * rcp(numGroundSamples);
tableEntry.rgb += weight * phase * transm * gBrdf * SampleGroundIrradianceTexture(dot(gN, L));
}
}
// Gather the volume contribution.
{
// Arbitrary number of samples...
#if defined(SHADER_API_XBOXONE)
// This forces a different code path, the xbox compiler flusters with the path coming from 89 samples.
// We don't care too much about perf as it is pre-computation, so we simply get more samples.
const int numVolumeSamples = 144;
#else
const int numVolumeSamples = 89;
#endif
for (int i = 0; i < numVolumeSamples; i++)
{
float2 f = Fibonacci2d(i, numVolumeSamples); // TODO: Cranley-Patterson Rotation
// TODO: anisotropy support without doubling the cost?
float cosTheta = ImportanceSampleRayleighPhase(f.x);
float phi = TWO_PI * f.y;
// Construct a direction around the V vector.
float3x3 frame = GetLocalFrame(V); // Orientation around V is arbitrary, so maybe Cranley-Patterson Rotation is not needed
// Note that this direction is likely no longer in the V-N plane.
// Must add a fudge factor here, otherwise Unity's shader compiler does not work correctly...
float3 V1 = SphericalToCartesian(phi, cosTheta + FLT_EPS);
V1 = mul(V1, frame);
// TODO: solve in spherical coords?
float NdotV1 = V1.z;
float phiL1 = acos(clamp(dot(L.xy, V1.xy) * rsqrt(max(dot(L.xy, L.xy) * dot(V1.xy, V1.xy), FLT_EPS)), -1, 1));
TexCoord4D tc = ConvertPositionAndOrientationToTexCoords(height, NdotV1, NdotL, phiL1);
float3 radiance = 0;
#ifdef SRC_SS
// Single scattering does not contain the phase function.
float LdotV1 = dot(L, V1);
radiance += lerp(SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb,
SAMPLE_TEXTURE3D_LOD(_AirSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb,
tc.a) * AirPhase(LdotV1);
radiance += lerp(SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb,
SAMPLE_TEXTURE3D_LOD(_AerosolSingleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb,
tc.a) * AerosolPhase(LdotV1);
#else // SRC_MS
radiance += lerp(SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb,
SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w1), 0).rgb,
tc.a);
#endif // SRC_MS
// AtmospherPhaseScatter / AirPhase.
// Assume that, after multiple bounces, the effect of anisotropy is lost.
// The view direction is the opposite of the incoming direction.
float3 phase = AirScatter(height) + AerosolScatter(height);
float weight = rcp(numVolumeSamples);
tableEntry.rgb += weight * phase * radiance;
}
}
// TODO: deep compositing.
_MultipleScatteringTable[tableCoord] = tableEntry;
#else // MULTIPLE_SCATTERING_ACCUMULATE
float4 tableEntry;
tableEntry.rgb = 0;
tableEntry.a = tMax;
// Integrate in-scattered radiance along -V.
float t0 = 0;
float3 viewODepthX = ComputeAtmosphericOpticalDepth(r, NdotV, viewAboveHorizon); // Back to start
float3 viewODepth = 0; // Sample to start
float3 inScatteredRadiance = LOAD_TEXTURE3D(_MultipleScatteringTexture, tableCoord).rgb;
float3 inScatterTerm0 = TransmittanceFromOpticalDepth(viewODepth) * inScatteredRadiance;
// Eye-balled number of samples.
const int numSamples = 256;
// const int numSamples = 4096;
const float startNdotV = NdotV;
for (int i = 0; i < numSamples; i++)
{
// Use (n = 0.5) when looking down, (n = 1) when looking horizontally,
// and (n = 2) when looking up.
// TODO: use exponential media importance sampling...
float n = pow(2, -startNdotV);
float x = pow(abs((i + 1) * rcp(numSamples)), n); // TODO: Cranley-Patterson Rotation
float t1 = tMax * x;
float dt = t1 - t0;
float3 P = O + t1 * -V;
// Update these for the step along the ray...
r = max(length(P), R);
height = r - R;
NdotV = dot(normalize(P), V);
NdotL = dot(normalize(P), L);
viewODepth = ComputeAtmosphericOpticalDepth(r, NdotV, viewAboveHorizon) - viewODepthX;
// 'phiL' is unchanged, so quadrilinear interpolation is actually unnecessary.
TexCoord4D tc = ConvertPositionAndOrientationToTexCoords(height, NdotV, NdotL, phiL);
inScatteredRadiance = SAMPLE_TEXTURE3D_LOD(_MultipleScatteringTexture, s_linear_clamp_sampler, float3(tc.u, tc.v, tc.w0), 0).rgb;
float3 inScatterTerm1 = TransmittanceFromOpticalDepth(viewODepth) * inScatteredRadiance;
// Apply the phase function at runtime.
tableEntry.rgb += (inScatterTerm0 + inScatterTerm1) * (0.5 * dt);
// Carry these over to the next iteration...
t0 = t1;
inScatterTerm0 = inScatterTerm1;
}
// TODO: deep compositing.
_MultipleScatteringTableOrder[tableCoord] = tableEntry; // One order
_MultipleScatteringTable[tableCoord] += float4(tableEntry.rgb, 0); // MS orders
#endif // MULTIPLE_SCATTERING_ACCUMULATE
}