You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1220 lines
42 KiB

#ifndef UNITY_PATH_TRACING_LIGHT_INCLUDED
#define UNITY_PATH_TRACING_LIGHT_INCLUDED
// This is just because it need to be defined, shadow maps are not used.
#define SHADOW_LOW
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/CookieSampling.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/ShaderVariablesRaytracingLightLoop.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/Shadows/SphericalQuad.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/Common/AtmosphericScatteringRayTracing.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/PathTracing/Shaders/PathTracingSampling.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/PathTracing/Shaders/PathTracingSkySampling.hlsl"
// Define this to use the Ray Tracing light cluster
#define USE_LIGHT_CLUSTER
#ifdef USE_LIGHT_CLUSTER
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/RayTracingLightCluster.hlsl"
#endif
// How many lights (at most) do we support at one given shading point
// FIXME: hardcoded limits are evil, this LightList should instead be put together in C#
#define MAX_LOCAL_LIGHT_COUNT SHADEROPTIONS_PATH_TRACING_MAX_LIGHT_COUNT
#define MAX_DISTANT_LIGHT_COUNT 4
#define DELTA_PDF 1000000.0
#define DOT_PRODUCT_EPSILON 0.001
#define SAMPLE_SOLID_ANGLE
// Supports punctual, spot, rect area and directional lights, in addition to one sky (aka environment)
struct LightList
{
uint localCount;
uint localPointCount;
uint localIndex[MAX_LOCAL_LIGHT_COUNT];
float localWeight;
uint distantCount;
uint distantIndex[MAX_DISTANT_LIGHT_COUNT];
float distantWeight;
uint skyCount; // 0 or 1
float skyWeight;
#ifdef USE_LIGHT_CLUSTER
uint cellIndex;
#endif
};
bool IsAreaLightActive(LightData lightData, float3 position, float3 normal)
{
float3 lightToPosition = position - lightData.positionRWS;
#ifndef USE_LIGHT_CLUSTER
// Check light range first
if (Length2(lightToPosition) > Sq(lightData.range))
return false;
#endif
// If this is tube light, we're done
if (lightData.lightType == GPULIGHTTYPE_TUBE)
return true;
// Check that the shading position is in front of the light
float lightCos = dot(lightToPosition, lightData.forward);
if (lightCos < 0.0)
return false;
// Check that at least part of the light is above the tangent plane
float lightTangentDist = dot(normal, lightToPosition);
if (4.0 * lightTangentDist * abs(lightTangentDist) > Sq(lightData.size.x) + Sq(lightData.size.y))
return false;
return true;
}
bool IsPointLightActive(LightData lightData, float3 position, float3 normal)
{
float3 lightToPosition = position - lightData.positionRWS;
if(lightData.lightType != GPULIGHTTYPE_PROJECTOR_BOX)
{
#ifndef USE_LIGHT_CLUSTER
// Check light range first
if (Length2(lightToPosition) > Sq(lightData.range))
return false;
#endif
// Check that at least part of the light is above the tangent plane
float lightTangentDist = dot(normal, lightToPosition);
if (lightTangentDist * abs(lightTangentDist) > lightData.size.x)
return false;
}
// If this is an omni-directional point light, we're done
if (lightData.lightType == GPULIGHTTYPE_POINT)
return true;
// Check that we are on the right side of the light plane
float z = dot(lightToPosition, lightData.forward);
if (z < 0.0)
return false;
if (lightData.lightType == GPULIGHTTYPE_SPOT)
{
// Offset the light position towards the back, to account for the radius,
// then check whether we are still within the dilated cone angle
float sinTheta2 = 1.0 - Sq(lightData.angleOffset / lightData.angleScale);
float3 lightRadiusOffset = sqrt(lightData.size.x / sinTheta2) * lightData.forward;
float lightCos = dot(normalize(lightToPosition + lightRadiusOffset), lightData.forward);
return lightCos * lightData.angleScale + lightData.angleOffset > 0.0;
}
// Our light type is either BOX or PYRAMID
float x = abs(dot(lightToPosition, lightData.right));
float y = abs(dot(lightToPosition, lightData.up));
return (lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX) ?
x < 1.0 && y < 1.0 : // BOX
x < z && y < z; // PYRAMID
}
bool IsDistantLightActive(DirectionalLightData lightData, float3 normal)
{
return dot(normal, lightData.forward) <= sin(lightData.angularDiameter * 0.5);
}
LightList CreateLightList(float3 position, float3 normal, uint lightLayers = RENDERING_LAYERS_MASK,
bool withPoint = true, bool withArea = true, bool withDistant = true,
float3 lightPosition = FLT_MAX)
{
LightList list = (LightList)0;
uint i;
// First take care of local lights (point, area)
if (withPoint || withArea)
{
uint localPointCount, localCount;
#ifdef USE_LIGHT_CLUSTER
if (PointInsideCluster(position))
{
list.cellIndex = GetClusterCellIndex(position);
localPointCount = GetPunctualLightEndIndexInClusterCell(list.cellIndex);
localCount = GetAreaLightEndIndexInClusterCell(list.cellIndex);
}
else
{
localPointCount = 0;
localCount = 0;
}
#else
localPointCount = _WorldPunctualLightCount;
localCount = _WorldPunctualLightCount + _WorldAreaLightCount;
#endif
// Do we have an imposed local light (identificed by position), for volumetric scattering?
bool forceLightPosition = (lightPosition.x != FLT_MAX);
// First point lights (including spot lights)
if (withPoint)
{
for (i = 0; i < localPointCount && list.localPointCount < MAX_LOCAL_LIGHT_COUNT; i++)
{
#ifdef USE_LIGHT_CLUSTER
const LightData lightData = FetchClusterLightIndex(list.cellIndex, i);
#else
const LightData lightData = _WorldLightDatas[i];
#endif
if (forceLightPosition && any(lightPosition - lightData.positionRWS))
continue;
if (IsMatchingLightLayer(lightData.lightLayers, lightLayers) && IsPointLightActive(lightData, position, normal))
list.localIndex[list.localPointCount++] = i;
}
list.localCount = list.localPointCount;
}
// Then rect area lights
if (withArea)
{
for (i = localPointCount; i < localCount && list.localCount < MAX_LOCAL_LIGHT_COUNT; i++)
{
#ifdef USE_LIGHT_CLUSTER
const LightData lightData = FetchClusterLightIndex(list.cellIndex, i);
#else
const LightData lightData = _WorldLightDatas[i];
#endif
if (forceLightPosition && any(lightPosition - lightData.positionRWS))
continue;
if (IsMatchingLightLayer(lightData.lightLayers, lightLayers) && IsAreaLightActive(lightData, position, normal))
list.localIndex[list.localCount++] = i;
}
}
}
// Then filter the active distant lights (directional)
list.distantCount = 0;
if (withDistant)
{
for (i = 0; i < _DirectionalLightCount && list.distantCount < MAX_DISTANT_LIGHT_COUNT; i++)
{
if (IsMatchingLightLayer(_DirectionalLightDatas[i].lightLayers, lightLayers) && IsDistantLightActive(_DirectionalLightDatas[i], normal))
list.distantIndex[list.distantCount++] = i;
}
}
// And finally the sky light
list.skyCount = withDistant && IsSkyEnabled() && IsSkySamplingEnabled() ? 1 : 0;
// Compute the weights, used for the lights PDF (we split 50/50 between local and distant+sky)
list.localWeight = list.localCount ? (list.distantCount + list.skyCount ? 0.5 : 1.0) : 0.0;
float nonLocalWeight = 1.0 - list.localWeight;
list.distantWeight = list.distantCount ? (list.skyCount ? 0.5 * nonLocalWeight : nonLocalWeight) : 0.0;
list.skyWeight = nonLocalWeight - list.distantWeight;
return list;
}
uint GetLightCount(LightList list)
{
return list.localCount + list.distantCount + list.skyCount;
}
LightData GetLocalLightData(LightList list, uint i)
{
#ifdef USE_LIGHT_CLUSTER
return FetchClusterLightIndex(list.cellIndex, list.localIndex[i]);
#else
return _WorldLightDatas[list.localIndex[i]];
#endif
}
LightData GetLocalLightData(LightList list, float inputSample)
{
return GetLocalLightData(list, (uint)(inputSample * list.localCount));
}
DirectionalLightData GetDistantLightData(LightList list, uint i)
{
return _DirectionalLightDatas[list.distantIndex[i]];
}
DirectionalLightData GetDistantLightData(LightList list, float inputSample)
{
return GetDistantLightData(list, (uint)(inputSample * list.distantCount));
}
float GetLocalLightWeight(LightList list)
{
return list.localWeight / list.localCount;
}
float GetDistantLightWeight(LightList list)
{
return list.distantWeight / list.distantCount;
}
float GetSkyLightWeight(LightList list)
{
return list.skyWeight;
}
#define PTLIGHT_LOCAL 0
#define PTLIGHT_DISTANT 1
#define PTLIGHT_SKY 2
uint PickLightType(LightList list, inout float theSample)
{
if (theSample < list.localWeight)
{
// We pick local lighting
theSample /= list.localWeight;
return PTLIGHT_LOCAL;
}
if (theSample < list.localWeight + list.distantWeight)
{
// We pick distant lighting
theSample = (theSample - list.localWeight) / list.distantWeight;
return PTLIGHT_DISTANT;
}
// Otherwise, sky lighting
theSample = (theSample - list.distantWeight - list.localWeight) / list.skyWeight;
return PTLIGHT_SKY;
}
float3 GetPunctualEmission(LightData lightData, float3 position, float3 outgoingDir, float dist)
{
float3 emission = lightData.color;
// Punctual attenuation
float4 distances = float4(dist, Sq(dist), rcp(dist), -dist * dot(outgoingDir, lightData.forward));
if(lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX)
{
//Note that we won't use GetPunctualLightVectors for non-Box cases to avoid calling ModifyDistancesForFillLighting
//We ensure the distances z and w coordinates are the same as for GetPunctualLightVectors for Box lights
distances.z = 1.0;
distances.w = dist;
}
emission *= PunctualLightAttenuation(distances, lightData.rangeAttenuationScale, lightData.rangeAttenuationBias, lightData.angleScale, lightData.angleOffset);
#ifndef LIGHT_EVALUATION_NO_COOKIE
if (lightData.cookieMode != COOKIEMODE_NONE)
{
LightLoopContext context;
float3 lightToSample = - dist * outgoingDir;
if(lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX)
{
lightToSample = position - lightData.positionRWS;
}
emission *= EvaluateCookie_Punctual(context, lightData, lightToSample).rgb;
}
#endif
return emission;
}
float3 GetDirectionalEmission(DirectionalLightData lightData, float3 positionRWS)
{
float3 emission = lightData.color;
#if SHADEROPTIONS_PRECOMPUTED_ATMOSPHERIC_ATTENUATION
// Nothing to do here
#else
// Physical sky emission color code, adapted from EvaluateLight_Directional()
if (asint(lightData.distanceFromCamera) >= 0)
emission *= EvaluateSunColorAttenuation(positionRWS - _PlanetCenterPosition, -lightData.forward);
#endif
#ifndef LIGHT_EVALUATION_NO_COOKIE
if (lightData.cookieMode != COOKIEMODE_NONE)
{
LightLoopContext context;
float3 lightToSample = positionRWS - lightData.positionRWS;
emission *= EvaluateCookie_Directional(context, lightData, lightToSample);
}
#endif
return emission;
}
float3 GetAreaEmission(LightData lightData, float centerU, float centerV, float sqDist)
{
float3 emission = lightData.color;
// Range windowing (see LightLoop.cs to understand why it is written this way)
if (lightData.rangeAttenuationBias == 1.0)
emission *= SmoothDistanceWindowing(sqDist, rcp(Sq(lightData.range)), lightData.rangeAttenuationBias);
#ifndef LIGHT_EVALUATION_NO_COOKIE
if (lightData.cookieMode != COOKIEMODE_NONE)
{
float2 uv = float2(0.5 - centerU, 0.5 + centerV);
emission *= SampleCookie2D(uv, lightData.cookieScaleOffset);
}
#endif
return emission;
}
float3 GetLightTransmission(float3 transmission, float shadowOpacity)
{
return lerp(float3(1.0, 1.0, 1.0), transmission, shadowOpacity);
}
bool SampleRectAreaLight(LightList lightList, LightData lightData,
float3 inputSample,
float3 position,
float3 normal,
bool isSpherical,
out float3 outgoingDir,
out float3 value,
out float pdf,
out float dist)
{
// The lights have already been filtered for "IsActive" in CreateLightList
/*
if (!IsAreaLightActive(lightData, position, normal))
return false;
*/
// Initialize out values
outgoingDir = 0;
value = 0;
pdf = 0;
dist = 0;
float3 lightCenter = lightData.positionRWS;
#ifndef SAMPLE_SOLID_ANGLE
// Generate a point on the surface of the light
float centerU = inputSample.x - 0.5;
float centerV = inputSample.y - 0.5;
float3 lightSamplePos = lightCenter + centerU * lightData.size.x * lightData.right + centerV * lightData.size.y * lightData.up;
// And the corresponding direction
outgoingDir = lightSamplePos - position;
float sqDist = Length2(outgoingDir);
dist = sqrt(sqDist);
outgoingDir /= dist;
if (!isSpherical && dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON)
return false;
float cosTheta = -dot(outgoingDir, lightData.forward);
if (cosTheta < DOT_PRODUCT_EPSILON)
return false;
float lightArea = length(cross(lightData.size.x * lightData.right, lightData.size.y * lightData.up));
value = GetAreaEmission(lightData, centerU, centerV, sqDist);
pdf = GetLocalLightWeight(lightList) * sqDist / (lightArea * cosTheta);
#else
// Solid angle sampling
float u = inputSample.x;
float v = inputSample.y;
SphQuad squad;
lightCenter = lightCenter - 0.5 * lightData.size.x * lightData.right;
lightCenter = lightCenter - 0.5 * lightData.size.y * lightData.up;
SphQuadInit(lightCenter, lightData.size.x * lightData.right, lightData.size.y * lightData.up, position, squad);
// Generate sample
// TODO: Move this validity check into the common quad initialization function
if (squad.S < 0.00001 || isnan(squad.S))
return false;
// 1. compute ’cu’
float au = u * squad.S + squad.k;
float fu = (cos(au) * squad.b0 - squad.b1) / sin(au);
float cu = 1 / sqrt(fu * fu + squad.b0sq);// *(fu > 0 ? +1 : -1);
cu = (fu > 0.0f) ? cu : -cu;
cu = clamp(cu, -1, 1); // avoid NaNs
// 2. compute ’xu’
float xu = -(cu * squad.z0) / sqrt(1 - cu * cu);
xu = clamp(xu, squad.x0, squad.x1); // avoid Infs
// 3. compute ’yv’
float d = sqrt(xu * xu + squad.z0sq);
float h0 = squad.y0 / sqrt(d * d + squad.y0sq);
float h1 = squad.y1 / sqrt(d * d + squad.y1sq);
float hv = h0 + v * (h1 - h0);
float hv2 = hv * hv;
float eps = 0.0001;
float yv = (hv2 < 1.0 - eps) ? (hv * d) / sqrt(1.0 - hv2) : squad.y1;
// 4. transform (xu,yv,z0) to world coords
float3 lightSamplePos = (squad.o + xu * squad.x + yv * squad.y + squad.z0 * squad.z);
// TODO: We should use this function, but we need xu and yv below for cookie evaluation
// float3 lightSamplePos = SphQuadSample(squad, u, v);
outgoingDir = lightSamplePos - position;
float sqDist = Length2(outgoingDir);
dist = sqrt(sqDist);
outgoingDir /= dist;
u = (xu - squad.x0)/(squad.x1 - squad.x0) - 0.5;
v = (yv - squad.y0)/(squad.y1 - squad.y0) - 0.5;
value = GetAreaEmission(lightData, u, v, sqDist); // TODO: add rcpPdf term here from lightlist when that PR lands
pdf = GetLocalLightWeight(lightList) / squad.S;
if (!isSpherical && dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON)
return false;
float cosTheta = -dot(outgoingDir, lightData.forward);
if (cosTheta < DOT_PRODUCT_EPSILON)
return false;
#endif
return true;
}
bool SampleTubeAreaLight(LightList lightList, LightData lightData,
float3 inputSample,
float3 position,
float3 normal,
bool isSpherical,
out float3 outgoingDir,
out float3 value,
out float pdf,
out float dist)
{
// initialize out values to avoid warnings
outgoingDir = 0;
value = 0;
pdf = 0;
dist = 0;
float3 lightCenter = lightData.positionRWS;
float lightLength = lightData.size.x;
// Generate a point on the line
// TODO : equiangular sampling might be better than just uniformly sampling along the line.
float centerU = inputSample.x - 0.5;
float3 lightSamplePos = lightCenter + centerU * lightLength * lightData.right;
// And the corresponding direction
outgoingDir = lightSamplePos - position;
float sqDist = Length2(outgoingDir);
dist = sqrt(sqDist);
outgoingDir /= dist;
if (!isSpherical && dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON)
return false;
float sinTheta = abs(dot(outgoingDir, lightData.right));
if (sinTheta > sqrt(1.0-DOT_PRODUCT_EPSILON*DOT_PRODUCT_EPSILON))
return false;
float cosTheta = sqrt(1.0-sinTheta*sinTheta);
// The multiplication by 2 is explained by the fact that we are dealing with a tube, although with radius -> 0
// So the energy coming to a point is actually coming from the half arc of the tube facing the point.
// This means the total intensity is multiplied by Integral{-PI/2 to PI/2, cos phi} = 2, phi being the
// angle of the tube surface normal along that half arc. This is empirically verified by comparing visual
// lighting intensities between the rasterized and path traced versions of the tube area light.
value = GetAreaEmission(lightData, centerU, 0, sqDist) * 2 * cosTheta / sqDist;
pdf = GetLocalLightWeight(lightList) / lightLength;
// Multiply both value and pdf by DELTA_PDF to give more MIS weight for the light,
// as the line light is ignored in EvaluateLight due to its inifinitesimal surface.
value *= DELTA_PDF;
pdf *= DELTA_PDF;
return true;
}
float2 GetDiscAreaLightCookieUV(LightData lightData, float3 posOnDisk)
{
float lightDiameter = 2*lightData.size.x;
float centerU = dot(posOnDisk, lightData.right) / (lightDiameter * Length2(lightData.right));
float centerV = dot(posOnDisk, lightData.up) / (lightDiameter * Length2(lightData.up));
return float2(centerU, centerV);
}
bool SampleDiscAreaLight(LightList lightList, LightData lightData,
float3 inputSample,
float3 position,
float3 normal,
bool isSpherical,
out float3 outgoingDir,
out float3 value,
out float pdf,
out float dist)
{
float3 lightCenter = lightData.positionRWS;
float3 lightSamplePos;
float3 lightNormal; // This is ignored
float4x4 lightToWorld = float4x4(float4(lightData.right, 0.0), float4(lightData.up, 0.0), float4(lightData.forward, 0.0), float4(lightCenter, 1.0));
float lightRadius = lightData.size.x;
SampleDisk(inputSample.xy, lightToWorld, lightRadius, pdf, lightSamplePos, lightNormal);
// And the corresponding direction
outgoingDir = lightSamplePos - position;
float sqDist = Length2(outgoingDir);
dist = sqrt(sqDist);
outgoingDir /= dist;
if (!isSpherical && dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON)
return false;
float cosTheta = -dot(outgoingDir, lightData.forward);
if (cosTheta < DOT_PRODUCT_EPSILON)
return false;
float3 diskPos = lightSamplePos-lightCenter;
float2 centerUV = GetDiscAreaLightCookieUV(lightData, diskPos);
value = GetAreaEmission(lightData, centerUV.x, centerUV.y, sqDist);
// 1/(Light area) has already been taken into account in the pdf returned by SampleDisk
pdf *= GetLocalLightWeight(lightList) * sqDist / cosTheta;
return true;
}
bool SamplePunctualLight(LightList lightList, LightData lightData,
float3 inputSample,
float3 position,
float3 normal,
bool isSpherical,
out float3 outgoingDir,
out float3 value,
out float pdf,
out float dist)
{
// Direction from shading point to light position
outgoingDir = lightData.positionRWS - position;
float sqDist = Length2(outgoingDir);
dist = sqrt(sqDist);
outgoingDir /= dist;
if (lightData.size.x > 0.0) // Stores the square radius
{
float3x3 localFrame = GetLocalFrame(outgoingDir);
SampleCone(inputSample.xy, sqrt(1.0 / (1.0 + lightData.size.x / sqDist)), outgoingDir, pdf); // computes rcpPdf
outgoingDir = outgoingDir.x * localFrame[0] + outgoingDir.y * localFrame[1] + outgoingDir.z * localFrame[2];
pdf = min(rcp(pdf), DELTA_PDF);
}
else
{
// DELTA_PDF represents 1 / area, where the area is infinitesimal
pdf = DELTA_PDF;
}
if(lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX)
{
outgoingDir = -lightData.forward;
dist = dot(outgoingDir, lightData.positionRWS - position);
}
if (!isSpherical && dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON)
return false;
value = GetPunctualEmission(lightData, position, outgoingDir, dist) * pdf;
pdf = GetLocalLightWeight(lightList) * pdf;
return true;
}
void SampleDistanceLight(LightList lightList, DirectionalLightData lightData,
float3 inputSample,
float3 position,
out float3 outgoingDir,
out float3 value,
out float pdf)
{
if (lightData.angularDiameter > 0.0)
{
SampleCone(inputSample.xy, cos(lightData.angularDiameter * 0.5), outgoingDir, pdf); // computes rcpPdf
value = GetDirectionalEmission(lightData, position) / pdf;
pdf = GetDistantLightWeight(lightList) / pdf;
outgoingDir = normalize(outgoingDir.x * normalize(lightData.right) + outgoingDir.y * normalize(lightData.up) - outgoingDir.z * lightData.forward);
}
else
{
value = GetDirectionalEmission(lightData, position) * DELTA_PDF;
pdf = GetDistantLightWeight(lightList) * DELTA_PDF;
outgoingDir = -lightData.forward;
}
}
bool SampleLights(LightList lightList,
float3 inputSample,
float3 position,
float3 normal,
bool isVolume,
out float3 outgoingDir,
out float3 value,
out float pdf,
out float dist,
out float shadowOpacity)
{
// initialize out values to avoid warnings on early return calls
outgoingDir = 0;
value = 0;
pdf = 0;
dist = 0;
shadowOpacity = 0;
if (!GetLightCount(lightList))
return false;
// Are we lighting a spherical (e.g. volume) or a hemi-spherical distribution (e.g. opaque surface)?
const bool isSpherical = isVolume || !any(normal);
// Stochastically pick one type of light to sample
const uint lightType = PickLightType(lightList, inputSample.z);
if (lightType == PTLIGHT_LOCAL)
{
// Pick a local light from the list
LightData lightData = GetLocalLightData(lightList, inputSample.z);
switch (lightData.lightType)
{
case GPULIGHTTYPE_RECTANGLE:
if (!SampleRectAreaLight(lightList, lightData, inputSample, position, normal, isSpherical, outgoingDir, value, pdf, dist))
return false;
break;
case GPULIGHTTYPE_TUBE:
if (!SampleTubeAreaLight(lightList, lightData, inputSample, position, normal, isSpherical, outgoingDir, value, pdf, dist))
return false;
break;
case GPULIGHTTYPE_DISC:
if (!SampleDiscAreaLight(lightList, lightData, inputSample, position, normal, isSpherical, outgoingDir, value, pdf, dist))
return false;
break;
default:
if (!SamplePunctualLight(lightList, lightData, inputSample, position, normal, isSpherical, outgoingDir, value, pdf, dist))
return false;
break;
}
if (isVolume)
{
value *= lightData.volumetricLightDimmer;
shadowOpacity = lightData.volumetricShadowDimmer;
}
else
{
value *= lightData.lightDimmer;
shadowOpacity = lightData.shadowDimmer;
}
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(position, outgoingDir, dist, value);
#endif
}
else // Distant or Sky light
{
if (lightType == PTLIGHT_DISTANT)
{
// Pick a distant light from the list
DirectionalLightData lightData = GetDistantLightData(lightList, inputSample.z);
SampleDistanceLight(lightList, lightData, inputSample, position, outgoingDir, value, pdf);
if (isVolume)
{
value *= lightData.volumetricLightDimmer;
shadowOpacity = lightData.volumetricShadowDimmer;
}
else
{
value *= lightData.lightDimmer;
shadowOpacity = lightData.shadowDimmer;
}
}
else // lightType == PTLIGHT_SKY
{
float2 uv = SampleSky(inputSample.xy);
outgoingDir = MapUVToSkyDirection(uv);
value = GetSkyValue(outgoingDir);
pdf = GetSkyLightWeight(lightList) * GetSkyPDFFromValue(value);
shadowOpacity = 1.0;
}
if (!isSpherical && (dot(normal, outgoingDir) < DOT_PRODUCT_EPSILON))
return false;
dist = FLT_INF;
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(position, outgoingDir, value);
#endif
}
return any(value) && any(pdf);
}
void EvaluateRectAreaLight(LightList lightList,
LightData lightData,
float3 rayOrigin,
float3 rayDirection,
float t,
float cosTheta,
float3 hitPosition, // Hit position is relative to lightCenter
inout float3 value,
inout float pdf)
{
// Then check if we are within the rectangle bounds
float centerU = dot(hitPosition, lightData.right) / (lightData.size.x * Length2(lightData.right));
float centerV = dot(hitPosition, lightData.up) / (lightData.size.y * Length2(lightData.up));
if (abs(centerU) < 0.5 && abs(centerV) < 0.5)
{
float3 lightCenter = lightData.positionRWS;
float t2 = Sq(t);
float3 lightValue = GetAreaEmission(lightData, centerU, centerV, t2);
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(rayOrigin, rayDirection, t, lightValue);
#endif
#ifndef SAMPLE_SOLID_ANGLE
float lightArea = length(cross(lightData.size.x * lightData.right, lightData.size.y * lightData.up));
value += lightValue;
pdf += GetLocalLightWeight(lightList) * t2 / (lightArea * cosTheta);
#else
float3 position = rayOrigin;
SphQuad squad;
lightCenter = lightCenter - 0.5 * lightData.size.x * lightData.right;
lightCenter = lightCenter - 0.5 * lightData.size.y * lightData.up;
SphQuadInit(lightCenter, lightData.size.x * lightData.right, lightData.size.y * lightData.up, position, squad);
// TODO: Move this validity check into the common quad initialization function
if (!(squad.S < 0.00001 || isnan(squad.S)))
{
value += lightValue;
pdf += GetLocalLightWeight(lightList) / squad.S;
}
#endif
}
}
void EvaluateDiscAreaLight(LightList lightList,
LightData lightData,
float3 rayOrigin,
float3 rayDirection,
float t,
float cosTheta,
float3 hitPosition, // Hit position is relative to lightCenter
inout float3 value,
inout float pdf)
{
float lightRadius = lightData.size.x;
float lightRadiusSquared = lightRadius*lightRadius;
// Then check if we are within the disc bounds
if (Length2(hitPosition) < lightRadiusSquared)
{
float2 centerUV = GetDiscAreaLightCookieUV(lightData, hitPosition);
float t2 = Sq(t);
float3 lightValue = GetAreaEmission(lightData, centerUV.x, centerUV.y, t2);
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(rayOrigin, rayDirection, t, lightValue);
#endif
float lightArea = PI * lightRadiusSquared;
value += lightValue;
pdf += GetLocalLightWeight(lightList) * t2 / (lightArea * cosTheta);
}
}
void EvaluateLights(LightList lightList,
float3 rayOrigin,
float3 rayDirection,
float rayHit,
out float3 value,
out float pdf)
{
value = 0.0;
pdf = 0.0;
uint i;
// First local lights (area lights only, as we consider the probability of hitting a point light neglectable)
for (i = lightList.localPointCount; i < lightList.localCount; i++)
{
LightData lightData = GetLocalLightData(lightList, i);
// Similarly, tube lights are line shaped so have no surface so neglect them as well
if (lightData.lightType == GPULIGHTTYPE_TUBE)
continue;
float t = rayHit;
float cosTheta = -dot(rayDirection, lightData.forward);
float3 lightCenter = lightData.positionRWS;
// Check if we hit the light plane, at a distance below our tMax (coming from indirect computation)
if (cosTheta > 0.0 && IntersectPlane(rayOrigin, rayDirection, lightCenter, lightData.forward, t))
{
if (t < rayHit)
{
float3 hitVec = rayOrigin + t * rayDirection - lightCenter;
if (lightData.lightType == GPULIGHTTYPE_RECTANGLE)
{
EvaluateRectAreaLight(lightList, lightData, rayOrigin, rayDirection, t, cosTheta, hitVec, value, pdf);
}
else
{
EvaluateDiscAreaLight(lightList, lightData, rayOrigin, rayDirection, t, cosTheta, hitVec, value, pdf);
}
}
}
}
// Then distant lights
for (i = 0; i < lightList.distantCount; i++)
{
DirectionalLightData lightData = GetDistantLightData(lightList, i);
if (lightData.angularDiameter > 0.0 && rayHit >= FLT_INF)
{
float cosHalfAngle = cos(lightData.angularDiameter * 0.5);
float cosTheta = -dot(rayDirection, lightData.forward);
if (cosTheta >= cosHalfAngle)
{
float3 lightValue = GetDirectionalEmission(lightData, rayDirection);
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(rayOrigin, rayDirection, lightValue);
#endif
float rcpPdf = TWO_PI * (1.0 - cosHalfAngle);
value += lightValue / rcpPdf;
pdf += GetDistantLightWeight(lightList) / rcpPdf;
}
}
}
// Then sky light
if (lightList.skyCount && rayHit >= FLT_INF)
{
float3 skyValue = GetSkyValue(rayDirection);
pdf += GetSkyLightWeight(lightList) * GetSkyPDFFromValue(skyValue);
#ifndef LIGHT_EVALUATION_NO_HEIGHT_FOG
ApplyFogAttenuation(rayOrigin, rayDirection, skyValue);
#endif
value += skyValue;
}
}
// Functions used by volumetric sampling
bool GetSphereInterval(float3 lightToRayOrigin, float radius, float3 rayDirection, out float tMin, out float tMax)
{
// initialize out values to avoid warnings on early return
tMin = 0;
tMax = 0;
// We consider Direction to be normalized => a = 1
float b = 2.0 * dot(rayDirection, lightToRayOrigin);
float c = Length2(lightToRayOrigin) - Sq(radius);
float2 t;
if (!SolveQuadraticEquation(1.0, b, c, t))
return false;
tMin = max(t.x, 0.0);
tMax = max(t.y, 0.0);
return tMin < tMax;
}
bool GetAreaLightInterval(LightData lightData, float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax)
{
// initialize out values to avoid warnings on early return
tMin = 0;
tMax = 0;
if (lightData.volumetricLightDimmer < 0.001)
return false;
float3 lightToRayOrigin = rayOrigin - lightData.positionRWS;
if (!GetSphereInterval(lightToRayOrigin, lightData.range, rayDirection, tMin, tMax))
return false;
// If this is tube light, we're done
if (lightData.lightType == GPULIGHTTYPE_TUBE)
return true;
float LdotD = dot(lightData.forward, rayDirection);
float t = -dot(lightData.forward, lightToRayOrigin) / LdotD;
if (LdotD > 0.0)
tMin = max(tMin, t);
else
tMax = min(tMax, t);
return tMin < tMax;
}
void Sort(inout float x, inout float y)
{
if (x > y) Swap(x, y);
}
void GetFrontInterval(float oz, float dz, float t1, float t2, inout float tMin, inout float tMax)
{
bool t1Valid = oz + t1 * dz > 0.0;
bool t2Valid = oz + t2 * dz > 0.0;
if (t1Valid)
{
if (t2Valid)
{
tMin = max(t1, tMin);
tMax = min(t2, tMax);
}
else
{
tMax = min(t1, tMax);
}
}
else
{
tMin = t2Valid ? max(t2, tMin) : tMax;
}
}
bool GetPointLightInterval(LightData lightData, float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax)
{
// initialize out values to avoid warnings on early return
tMin = 0;
tMax = 0;
if (lightData.volumetricLightDimmer < 0.001)
return false;
float3 lightToRayOrigin = rayOrigin - lightData.positionRWS;
if (lightData.lightType != GPULIGHTTYPE_PROJECTOR_BOX)
{
if (!GetSphereInterval(lightToRayOrigin, lightData.range, rayDirection, tMin, tMax))
return false;
}
// This is just a point light (no spot cone angle)
if (lightData.lightType == GPULIGHTTYPE_POINT)
return true;
// We are dealing with either a cone, a pyramid or a box
float3 localOrigin = float3(dot(lightToRayOrigin, lightData.right),
dot(lightToRayOrigin, lightData.up),
dot(lightToRayOrigin, lightData.forward));
float3 localDirection = float3(dot(rayDirection, lightData.right),
dot(rayDirection, lightData.up),
dot(rayDirection, lightData.forward));
if (lightData.lightType == GPULIGHTTYPE_PROJECTOR_BOX)
{
float tStart = 0;
float tEnd = rcp(FLT_EPS);
IntersectRayAABB(localOrigin, (localDirection), float3(-1, -1, 0), float3(1,1,lightData.range),
tStart, tEnd, tMin, tMax);
}
else if (lightData.lightType == GPULIGHTTYPE_PROJECTOR_PYRAMID)
{
// Compute intersections with planes x=-z and x=z
float tx1 = -(localOrigin.x - localOrigin.z) / (localDirection.x - localDirection.z);
float tx2 = -(localOrigin.x + localOrigin.z) / (localDirection.x + localDirection.z);
Sort(tx1, tx2);
// Check validity of the intersections (we want them only in front of the light)
GetFrontInterval(localOrigin.z, localDirection.z, tx1, tx2, tMin, tMax);
if (tMin < tMax)
{
// Compute intersections with planes y=-1 and y=1
float ty1 = -(localOrigin.y - localOrigin.z) / (localDirection.y - localDirection.z);
float ty2 = -(localOrigin.y + localOrigin.z) / (localDirection.y + localDirection.z);
Sort(ty1, ty2);
// Check validity of the intersections (we want them only in front of the light)
GetFrontInterval(localOrigin.z, localDirection.z, ty1, ty2, tMin, tMax);
}
}
else // lightData.lightType == GPULIGHTTYPE_SPOT
{
float cosTheta2 = Sq(lightData.angleOffset / lightData.angleScale);
// Offset light origin to account for light radius
localOrigin.z += sqrt(lightData.size.x / (1.0 - cosTheta2));
// Account for non-normalized local basis
float3 normalizedLocalOrigin = float3(localOrigin.x / Length2(lightData.right),
localOrigin.y / Length2(lightData.up),
localOrigin.z);
float a = Sq(localDirection.z) - cosTheta2;
float b = 2.0 * (localOrigin.z * localDirection.z - dot(normalizedLocalOrigin, localDirection) * cosTheta2);
float c = Sq(localOrigin.z) - dot(normalizedLocalOrigin, localOrigin) * cosTheta2;
float2 t;
if (!SolveQuadraticEquation(a, b, c, t))
return false;
// Check validity of the intersections (we want them only in front of the light)
GetFrontInterval(localOrigin.z, localDirection.z, t.x, t.y, tMin, tMax);
}
return tMin < tMax;
}
// This function has been deprecated in favor of PickLocalLightInterval() right below
// float GetLocalLightsInterval(float3 rayOrigin, float3 rayDirection, out float tMin, out float tMax)
// {
// tMin = FLT_MAX;
// tMax = 0.0;
// float tLightMin, tLightMax;
// // First process point lights
// uint i = 0, n = _WorldPunctualLightCount, localCount = 0;
// for (; i < n; i++)
// {
// if (GetPointLightInterval(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax))
// {
// tMin = min(tMin, tLightMin);
// tMax = max(tMax, tLightMax);
// localCount++;
// }
// }
// // Then area lights
// n += _WorldAreaLightCount;
// for (; i < n; i++)
// {
// if (GetAreaLightInterval(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax))
// {
// tMin = min(tMin, tLightMin);
// tMax = max(tMax, tLightMax);
// localCount++;
// }
// }
// uint lightCount = localCount + _DirectionalLightCount;
// return lightCount ? float(localCount) / lightCount : -1.0;
// }
float GetLocalLightWeight(LightData lightData, float3 rayOrigin, float3 rayDirection, float tMin, float tMax)
{
float tDist = clamp(dot(lightData.positionRWS - rayOrigin, rayDirection), tMin, tMax);
float3 vDist = rayOrigin + tDist * rayDirection - lightData.positionRWS;
// By offsetting the square distance by 1.0, we reduce the range of the weight to ]0.0, 1.0],
// while avoiding a singularity when distance goes towards 0.0.
float distSq = 1.0 + Length2(vDist);
return rcp(distSq);
}
float PickLocalLightInterval(float3 rayOrigin, float3 rayDirection, inout float inputSample, out float3 lightPosition, out float lightWeight, out float tMin, out float tMax)
{
tMin = FLT_MAX;
tMax = 0.0;
float tLightMin, tLightMax;
float wLight, wSum = 0.0;
// First process point lights
uint i = 0, n = 0, localCount = 0;
n += _WorldPunctualLightCount;
for (; i < n; i++)
{
if (GetPointLightInterval(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax))
{
wLight = GetLocalLightWeight(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax);
if (wLight > 0.0)
{
wSum += wLight;
wLight /= wSum;
if (inputSample < wLight)
{
lightPosition = _WorldLightDatas[i].positionRWS;
lightWeight = wLight;
tMin = tLightMin;
tMax = tLightMax;
inputSample = RescaleSampleUnder(inputSample, wLight);
}
else
{
lightWeight *= 1.0 - wLight;
inputSample = RescaleSampleOver(inputSample, wLight);
}
localCount++;
}
}
}
// Then area lights
n += _WorldAreaLightCount;
for (; i < n; i++)
{
if (GetAreaLightInterval(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax))
{
wLight = GetLocalLightWeight(_WorldLightDatas[i], rayOrigin, rayDirection, tLightMin, tLightMax);
if (wLight > 0.0)
{
wSum += wLight;
wLight /= wSum;
if (inputSample < wLight)
{
lightPosition = _WorldLightDatas[i].positionRWS;
lightWeight = wLight;
tMin = tLightMin;
tMax = tLightMax;
inputSample = RescaleSampleUnder(inputSample, wLight);
}
else
{
lightWeight *= 1.0 - wLight;
inputSample = RescaleSampleOver(inputSample, wLight);
}
localCount++;
}
}
}
uint lightCount = localCount + _DirectionalLightCount + (IsSkyEnabled() && IsSkySamplingEnabled() ? 1 : 0);
return lightCount ? float(localCount) / lightCount : -1.0;
}
LightList CreateLightList(float3 position, bool sampleLocalLights, float3 lightPosition = FLT_MAX)
{
return CreateLightList(position, 0.0, RENDERING_LAYERS_MASK, sampleLocalLights, sampleLocalLights, !sampleLocalLights, lightPosition);
}
#endif // UNITY_PATH_TRACING_LIGHT_INCLUDED