// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================================= RectLight.usf: Light sampling functions for Rect light implementation ===============================================================================================*/ #pragma once #include "../../RectLight.ush" #include "PathTracingLightCommon.ush" #if USE_RECT_LIGHT_TEXTURES float3 EvaluateTexture(int LightId, float2 UV) { if (HasRectTexture(LightId)) { // TODO: could use path roughness / ray cone to lower the mip level here UV.y = 1 - UV.y; // match orientation with raster path // Transform local UV into rect. light atlas UV const float2 UVScale = GetRectLightAtlasUVScale(LightId); const float2 UVOffset = GetRectLightAtlasUVOffset(LightId); UV = UV * UVScale + UVOffset; return View.RectLightAtlasTexture.SampleLevel(View.RectLightAtlasSampler, UV, 0).xyz; } return 1.0; } #endif float3 BilinearQuadWarp(float2 uv, float W00, float W01, float W10, float W11) { // "Practical Product Sampling by Fitting and Composing Warps" - EGSR 2020 // https://casual-effects.com/research/Hart2020Sampling/index.html // https://www.shadertoy.com/view/wljyDz float a = lerp(W00, W01, .5); float b = lerp(W10, W11, .5); float u = a == b ? uv.x : (sqrt(lerp(a * a, b * b, uv.x)) - a) / (b - a); float c = lerp(W00, W10, u); float d = lerp(W01, W11, u); float v = c == d ? uv.y : (sqrt(lerp(c * c, d * d, uv.y)) - c) / (d - c); float area = lerp(a, b, .5); float pdf = lerp(c, d, v) / area; return float3(u, v, pdf); } // TODO: This method should be moved to RectLight.ush // TODO: Analyze the overall efficiency (MSE reduction vs. time) // Projected solid angle sampling converges faster at equal sample count, but is a bit slower // TODO: Unify with UniformSampleSphericalRect so both return the same struct, making them easier to interchange float4 SampleApproxProjectionSphericalRect(float2 Rand, FSphericalRect SphericalRect, float3 WorldNormal) { // Construct the quad vertices float3 S00 = float3(SphericalRect.x0, SphericalRect.y0, SphericalRect.z0); float3 S10 = float3(SphericalRect.x1, SphericalRect.y0, SphericalRect.z0); float3 S11 = float3(SphericalRect.x1, SphericalRect.y1, SphericalRect.z0); float3 S01 = float3(SphericalRect.x0, SphericalRect.y1, SphericalRect.z0); // Compute the cosine of the normal against each corner of the quad float3 LightSpaceNormal = mul(SphericalRect.Axis, WorldNormal); float W00 = saturate(dot(normalize(S00), LightSpaceNormal)); float W10 = saturate(dot(normalize(S10), LightSpaceNormal)); float W11 = saturate(dot(normalize(S11), LightSpaceNormal)); float W01 = saturate(dot(normalize(S01), LightSpaceNormal)); // Warp a 2D sample according to the cosine at each corner of the quad float3 WarpedRand = BilinearQuadWarp(Rand, W00, W01, W10, W11); // Proceed with uniform sampling of the spherical rectangle float3 Direction = UniformSampleSphericalRect(WarpedRand.xy, SphericalRect).Direction; float OutPdf = isfinite(SphericalRect.SolidAngle) ? 1.0 / SphericalRect.SolidAngle : 0.0; // Return Direction vector and adjusted PDF return float4(Direction, OutPdf * WarpedRand.z); } FLightHit RectLight_TraceLight(FRayDesc Ray, int LightId) { float3 TranslatedLightPosition = GetTranslatedPosition(LightId); float3 LightNormal = GetNormal(LightId); float3 LightDirection = TranslatedLightPosition - Ray.Origin; float DoN = dot(Ray.Direction, LightNormal); float t = dot(LightDirection, LightNormal) / DoN; // ray points toward the plane and intersect it? if (DoN < 0 && t > Ray.TMin && t < Ray.TMax) { float3 LightdPdu = GetdPdu(LightId); float3 LightdPdv = GetdPdv(LightId); float2 LightExtent = 0.5 * GetRectSize(LightId); float3 P = t * Ray.Direction - LightDirection; float2 UV = float2(dot(P, LightdPdu), dot(P, LightdPdv)); // test point against if (all(abs(UV) <= LightExtent)) { // Clip the Rectangle by the barndoors FRect Rect = GetRect(LightDirection, -LightNormal, LightdPdv, LightExtent.x, LightExtent.y, GetRectLightBarnCosAngle(LightId), GetRectLightBarnLength(LightId), true); P = t * Ray.Direction - Rect.Origin; // test again with the clipped extents float2 ClippedUV = float2(dot(P, LightdPdu), dot(P, LightdPdv)); if (all(abs(ClippedUV) <= Rect.Extent)) { // stored color is radiance float3 Radiance = GetColor(LightId) * ComputeIESAttenuation(LightId, Ray.Origin); Radiance *= ComputeAttenuationFalloff(dot(LightDirection, LightDirection), LightId); FSphericalRect SphericalRect = BuildSphericalRect(Rect); #if USE_RECT_LIGHT_TEXTURES Radiance *= EvaluateTexture(LightId, (UV + LightExtent) / (LightExtent * 2)); #endif float Pdf = rcp(GetSphericalRectInversePdf(Ray.Direction, t * t, SphericalRect)); return CreateLightHit(Radiance, Pdf, t); } } } return NullLightHit(); } FLightSample RectLight_SampleLight( int LightId, float2 RandSample, float3 TranslatedWorldPos, float3 WorldNormal ) { float3 TranslatedLightPosition = GetTranslatedPosition(LightId); float3 LightNormal = GetNormal(LightId); float3 LightdPdu = GetdPdu(LightId); float3 LightdPdv = GetdPdv(LightId); float LightWidth = GetWidth(LightId); float LightHeight = GetHeight(LightId); // Define rectangle and compute solid angle float3 LightDirection = TranslatedLightPosition - TranslatedWorldPos; FRect Rect = GetRect(LightDirection, -LightNormal, LightdPdv, 0.5 * LightWidth, 0.5 * LightHeight, GetRectLightBarnCosAngle(LightId), GetRectLightBarnLength(LightId), true /* bComputeVisibleRect */); if (!IsRectVisible(Rect) || dot(Rect.Axis[2], Rect.Origin) < 0) { return NullLightSample(); } FSphericalRect SphericalRect = BuildSphericalRect(Rect); float3 Radiance = GetColor(LightId) * ComputeIESAttenuation(LightId, TranslatedWorldPos); float DistanceSquared = length2(LightDirection); Radiance *= ComputeAttenuationFalloff(DistanceSquared, LightId); FSphericalRectSample Sample = UniformSampleSphericalRect(RandSample, SphericalRect); #if USE_RECT_LIGHT_TEXTURES float2 UV = 0.5 * ((2 * Sample.UV - 1) * Rect.Extent + Rect.Offset) / Rect.FullExtent + 0.5; Radiance *= EvaluateTexture(LightId, UV); #endif return CreateLightSample(Radiance * Sample.InvPdf, rcp(Sample.InvPdf), Sample.Direction, Sample.Distance); } float RectLight_EstimateLight( int LightId, float3 TranslatedWorldPos, float3 WorldNormal, bool IsTransmissiveMaterial ) { // Distance to centroid // #dxr_todo: UE-72533 Use closest point, instead float3 LightDirection = GetTranslatedPosition(LightId) - TranslatedWorldPos; float LightDistanceSquared = dot(LightDirection, LightDirection); float3 LightNormal = GetNormal(LightId); // Is the shading point behind the light? float LNoL = saturate(-dot(normalize(LightDirection), LightNormal)); if (LNoL <= 0.0) { return 0.0; } // Approximate geometric term float Width = GetWidth(LightId); float Height = GetHeight(LightId); // Don't bother trying to bound the N.L term as its in [0,1] and hard to estimate accurately and quickly float NoL = 1.0; float Area = Width * Height; float LightPower = Luminance(GetColor(LightId)); float Falloff = ComputeAttenuationFalloff(LightDistanceSquared, LightId); return LightPower * Falloff * Area * NoL * LNoL / LightDistanceSquared; } void ClipRayByPlane(float3 RayOrigin, float3 RayDirection, float3 N, float3 C, inout float TMin, inout float TMax) { const float DoN = dot(RayDirection, N); const float TPlane = dot(C - RayOrigin, N) * rcp(DoN); if (DoN > 0.0) { // plane is pointing in the same direction as the ray, clip TMin TMin = max(TMin, TPlane); } if (DoN < 0.0) { // plane is pointing towards the ray, clip TMax TMax = min(TMax, TPlane); } } FVolumeLightSampleSetup RectLight_PrepareLightVolumeSample( int LightId, float3 RayOrigin, float3 RayDirection, float TMin, float TMax ) { float3 Center = GetTranslatedPosition(LightId); float AttenuationRadius = rcp(GetAttenuation(LightId)); float2 T = RaySphereOverlap(RayOrigin, RayDirection, Center, AttenuationRadius, TMin, TMax); if (T.x < T.y) { // now clip the sphere of influence against the plane of the quad and adjust the near/far distance depending on the sign float3 N = GetNormal(LightId); // NOTE: push quad's plane slightly forward to avoid samples that would lie too close to the light ClipRayByPlane(RayOrigin, RayDirection, N, Center + N * 0.001, T.x, T.y); const float3 C = GetColor(LightId); const float Width = GetWidth(LightId); const float Height = GetHeight(LightId); #if 1 const float BarnCos = GetRectLightBarnCosAngle(LightId); const float BarnLen = GetRectLightBarnLength(LightId); if (BarnCos > 0.035) { // Clip ray against barndoor penumbra planes if barndoors are active (see threshold in GetRect) const float BarnSin = sqrt(1 - BarnCos * BarnCos); const float2 BoundingPlaneX = float2(Width + BarnLen * BarnSin, BarnLen * BarnCos); const float2 BoundingPlaneY = float2(Height + BarnLen * BarnSin, BarnLen * BarnCos); const float3 LightdPdu = GetdPdu(LightId); const float3 LightdPdv = GetdPdv(LightId); ClipRayByPlane(RayOrigin, RayDirection, BoundingPlaneX.x * N + BoundingPlaneX.y * LightdPdu, Center - LightdPdu * Width * 0.5, T.x, T.y); ClipRayByPlane(RayOrigin, RayDirection, BoundingPlaneX.x * N - BoundingPlaneX.y * LightdPdu, Center + LightdPdu * Width * 0.5, T.x, T.y); ClipRayByPlane(RayOrigin, RayDirection, BoundingPlaneY.x * N + BoundingPlaneY.y * LightdPdv, Center - LightdPdv * Height * 0.5, T.x, T.y); ClipRayByPlane(RayOrigin, RayDirection, BoundingPlaneY.x * N - BoundingPlaneY.y * LightdPdv, Center + LightdPdv * Height * 0.5, T.x, T.y); } #endif const float Area = Width * Height; const float LightImportance = max3(C.x, C.y, C.z) * Area; if ((SceneLights[LightId].Flags & PATHTRACER_FLAG_NON_INVERSE_SQUARE_FALLOFF_MASK) == 0) { // inverse square falloff requires equi-angular sampling return CreateEquiAngularSampler(LightImportance, Center, RayOrigin, RayDirection, T.x, T.y); } // otherwise, default to uniform sampling return CreateUniformSampler(LightImportance, T.x, T.y); } return NullVolumeLightSetup(); }