Files
UnrealEngine/Engine/Shaders/Private/IntersectionUtils.ush
2025-05-18 13:04:45 +08:00

262 lines
12 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
bool SphereIntersectCone(float4 SphereCenterAndRadius, float3 ConeVertex, float3 ConeAxis, float ConeAngleCos, float ConeAngleSin)
{
float3 U = ConeVertex - (SphereCenterAndRadius.w / ConeAngleSin) * ConeAxis;
float3 D = SphereCenterAndRadius.xyz - U;
float DSizeSq = dot(D, D);
float E = dot(ConeAxis, D);
if (E > 0 && E * E >= DSizeSq * ConeAngleCos * ConeAngleCos)
{
D = SphereCenterAndRadius.xyz - ConeVertex;
DSizeSq = dot(D, D);
E = -dot(ConeAxis, D);
if (E > 0 && E * E >= DSizeSq * ConeAngleSin * ConeAngleSin)
{
return DSizeSq <= SphereCenterAndRadius.w * SphereCenterAndRadius.w;
}
else
{
return true;
}
}
return false;
}
bool SphereIntersectConeWithMaxDistance(float4 SphereCenterAndRadius, float3 ConeVertex, float3 ConeAxis, float ConeAngleCos, float ConeAngleSin, float MaxDistanceAlongAxis)
{
if (SphereIntersectCone(SphereCenterAndRadius, ConeVertex, ConeAxis, ConeAngleCos, ConeAngleSin))
{
float ConeAxisDistance = dot(SphereCenterAndRadius.xyz - ConeVertex, ConeAxis);
float ConeAxisDistanceMax = ConeAxisDistance - SphereCenterAndRadius.w;
return ConeAxisDistanceMax < MaxDistanceAlongAxis;
}
return false;
}
bool SphereIntersectConeWithDepthRanges(float4 SphereCenterAndRadius, float3 ConeVertex, float3 ConeAxis, float ConeAngleCos, float ConeAngleSin, float4 ConeAxisDepthRanges)
{
if (SphereIntersectCone(SphereCenterAndRadius, ConeVertex, ConeAxis, ConeAngleCos, ConeAngleSin))
{
float ConeAxisDistance = dot(SphereCenterAndRadius.xyz - ConeVertex, ConeAxis);
float2 ConeAxisDistanceMinMax = float2(ConeAxisDistance + SphereCenterAndRadius.w, ConeAxisDistance - SphereCenterAndRadius.w);
if ((ConeAxisDistanceMinMax.x > ConeAxisDepthRanges.x && ConeAxisDistanceMinMax.y < ConeAxisDepthRanges.y)
|| (ConeAxisDistanceMinMax.x > ConeAxisDepthRanges.z && ConeAxisDistanceMinMax.y < ConeAxisDepthRanges.w))
{
return true;
}
}
return false;
}
bool SphereIntersectSphere(float4 SphereCenterAndRadius, float4 OtherSphereCenterAndRadius)
{
float CombinedRadii = SphereCenterAndRadius.w + OtherSphereCenterAndRadius.w;
float3 VectorBetweenCenters = SphereCenterAndRadius.xyz - OtherSphereCenterAndRadius.xyz;
return dot(VectorBetweenCenters, VectorBetweenCenters) < CombinedRadii * CombinedRadii;
}
// Given two ranges of distances along a ray - check if they overlap
// NOTE: This check ignores empty intervals and intervals which contain only a single point
bool CheckOverlap(float2 Ta, float2 Tb) {
return max(Ta.x, Tb.x) < min(Ta.y, Tb.y);
}
bool RayOverlapsAABB(float3 O, float3 D, float TMax, float3 Lo, float3 Hi)
{
float3 InvRayDir = rcp(D);
float3 T0 = (Lo - O) * InvRayDir;
float3 T1 = (Hi - O) * InvRayDir;
float3 TNear = min(T0, T1), TFar = max(T0, T1);
return max(max(TNear.x, TNear.y), max(TNear.z, 0.0)) <= min(min(TFar.x, TFar.y), min(TFar.z, TMax));
}
// Compute the overlap between an infinite ray and an infinite cone volume
// The Apex is the origin of the cone, the normal is the unit vector , CosTheta is the half-angle describing the cone's opening
float2 RayInfiniteConeVolumeOverlap(
float3 RayOrigin, float3 RayDirection,
float3 Apex, float3 Axis,
float CosTheta)
{
// Adapted from: https://www.iquilezles.org/www/articles/intersectors/intersectors.htm
const float3 ba = Axis * CosTheta;
const float3 oa = RayOrigin - Apex;
const float3 rd = RayDirection;
const float m0 = dot(ba, ba);
const float m1 = dot(oa, ba);
const float m3 = dot(rd, ba);
const float m4 = dot(rd, oa);
const float m5 = dot(oa, oa);
const float k2 = m0 * m0 - m3 * m3;
const float k1 = m0 * m0 * m4 - m1 * m3;
const float k0 = m0 * m0 * m5 - m1 * m1;
const float h = k1 * k1 - k2 * k0;
if (h > 0)
{
const float s = sign(k2);
const float2 t = (-k1 + float2(-s, s) * sqrt(h)) / k2;
const float2 y = m1 + t * m3;
// check which hits are valid:
const int c = ((y.x > 0) ? 1 : 0) + (y.y > 0 ? 2 : 0);
if (c == 1)
{
// only smaller root is valid, far root is invalid
return float2(0.0, t.x);
}
if (c == 2)
{
// only larger root is valid, must have started outside the cone
return float2(t.y, POSITIVE_INFINITY);
}
if (c == 3)
{
// both roots are valid
return t;
}
}
return -1.0;
}
float2 RaySphereOverlap(float3 Ro, float3 Rd, float3 Center, float Radius, float TMin, float TMax)
{
float3 oc = Ro - Center;
float b = dot(oc, Rd);
float h = Radius * Radius - length2(oc - b * Rd);
if (h > 0)
{
float2 t = -b + float2(-1, +1) * sqrt(h);
return float2(max(t.x, TMin), min(t.y, TMax));
}
return -1.0;
}
// Check for overlap between an AABB and a Cone (the portion of the cone inside a sphere of the given Radius)
// NOTE: This test can be expensive, so one should check for AABB vs sphere overlap first
// NOTE: The last argument can be used to do a cheaper test using the AABB of the rounded cone
bool AABBOverlapsCurvedCone(float3 Lo, float3 Hi, float3 Apex, float3 Axis, float CosTheta, float Radius, bool bUseApproxTest)
{
if (bUseApproxTest)
{
// Compute AABB of the cone, and check for overlap of the two AABBs
// box around ray from light center to tip of the cone
float3 Tip = Apex + Axis * Radius;
float3 LightBoundLo = min(Apex, Tip);
float3 LightBoundHi = max(Apex, Tip);
// expand by disc around the farthest part of the cone
float SinTheta2 = 1 - CosTheta * CosTheta;
float3 Disc = sqrt(saturate(SinTheta2 * (1.0 - Axis * Axis)));
LightBoundLo = min(LightBoundLo, Apex + Radius * (Axis * CosTheta - Disc));
LightBoundHi = max(LightBoundHi, Apex + Radius * (Axis * CosTheta + Disc));
float3 F = select(abs(Axis) > CosTheta, Apex + Radius * sign(Axis), Apex); // include far point along axis if it lies inside the cone
LightBoundLo = min(LightBoundLo, F);
LightBoundHi = max(LightBoundHi, F);
// intersect bounds and see if we have anything left
return all(max(Lo, LightBoundLo) < min(Hi, LightBoundHi));
}
else
{
return
// Does the Apex line inside the AABB?
all((Lo <= Apex) & (Apex <= Hi)) ||
// Does the central axis of the cone pass overlap the AABB?
RayOverlapsAABB(Apex, Axis, Radius, Lo, Hi) ||
// Test each of the 12 AABB edges against cone -- if any of them overlap, we can stop
// NOTE: this looks expensive, but there is a huge amount of simplification and common terms between each of these calls
// NOTE: This is a volumetric test, so edges entirely _inside_ the cone, will pass the test
// Check X edges
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Lo.y, Lo.z), float3(1, 0, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Lo.y, Lo.z), float3(1, 0, 0), Apex, Radius, 0, Hi.x - Lo.x)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Lo.y, Hi.z), float3(1, 0, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Lo.y, Hi.z), float3(1, 0, 0), Apex, Radius, 0, Hi.x - Lo.x)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Hi.y, Lo.z), float3(1, 0, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Hi.y, Lo.z), float3(1, 0, 0), Apex, Radius, 0, Hi.x - Lo.x)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Hi.y, Hi.z), float3(1, 0, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Hi.y, Hi.z), float3(1, 0, 0), Apex, Radius, 0, Hi.x - Lo.x)) ||
// Check Y edges
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Lo.y, Lo.z), float3(0, 1, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Lo.y, Lo.z), float3(0, 1, 0), Apex, Radius, 0, Hi.y - Lo.y)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Lo.y, Hi.z), float3(0, 1, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Lo.y, Hi.z), float3(0, 1, 0), Apex, Radius, 0, Hi.y - Lo.y)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Hi.x, Lo.y, Lo.z), float3(0, 1, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Hi.x, Lo.y, Lo.z), float3(0, 1, 0), Apex, Radius, 0, Hi.y - Lo.y)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Hi.x, Lo.y, Hi.z), float3(0, 1, 0), Apex, Axis, CosTheta), RaySphereOverlap(float3(Hi.x, Lo.y, Hi.z), float3(0, 1, 0), Apex, Radius, 0, Hi.y - Lo.y)) ||
// Check Z edges
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Lo.y, Lo.z), float3(0, 0, 1), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Lo.y, Lo.z), float3(0, 0, 1), Apex, Radius, 0, Hi.z - Lo.z)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Lo.x, Hi.y, Lo.z), float3(0, 0, 1), Apex, Axis, CosTheta), RaySphereOverlap(float3(Lo.x, Hi.y, Lo.z), float3(0, 0, 1), Apex, Radius, 0, Hi.z - Lo.z)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Hi.x, Lo.y, Lo.z), float3(0, 0, 1), Apex, Axis, CosTheta), RaySphereOverlap(float3(Hi.x, Lo.y, Lo.z), float3(0, 0, 1), Apex, Radius, 0, Hi.z - Lo.z)) ||
CheckOverlap(RayInfiniteConeVolumeOverlap(float3(Hi.x, Hi.y, Lo.z), float3(0, 0, 1), Apex, Axis, CosTheta), RaySphereOverlap(float3(Hi.x, Hi.y, Lo.z), float3(0, 0, 1), Apex, Radius, 0, Hi.z - Lo.z));
}
}
// Test AABB against solid sphere
bool AABBOverlapsSphere(float3 Lo, float3 Hi, float3 Center, float Radius)
{
// James Arvo - Graphics Gems (p. 335)
return length2(max(Lo - Center, 0) + max(Center - Hi, 0)) < Radius * Radius;
}
// Does the AABB lie fully "behind" the plane defined by Center and Normal
// NOTE: Normal vector does not need to be normalized
bool IsBoxBehindPlane(float3 Lo, float3 Hi, float3 Normal, float3 Center)
{
return dot(select(Normal > 0, Hi, Lo) - Center, Normal) < 0;
}
// Test AABB against the penumbra frustum defined by a rectlight's barndoors
// This test is expensive, so should only be used if the barndoors are active
// It is assumed the AABB already overlaps the sphere of influence of the rect light and is at least partially "in front" of the light
// The last portion of the test is a bit more expensive, so skip it optionally (at the risk of more false-positives)
bool AABBOverlapsRectLightFrustum(float3 Lo, float3 Hi, float3 Center, float3 Normal, float3 Tangent, float HalfWidth, float HalfHeight, float Radius, float BarnCos, float BarnLen, bool bImprovedFrustumTest)
{
const float3 dPdv = Tangent;
const float3 dPdu = cross(Normal, dPdv);
const float BarnSin = sqrt(1 - BarnCos * BarnCos);
float3 BoundingPlane = float3(
2 * HalfWidth + BarnLen * BarnSin,
2 * HalfHeight + BarnLen * BarnSin,
BarnLen * BarnCos
);
// Test if box is outside any of the 4 barndoor penumbra planes:
if (IsBoxBehindPlane(Lo, Hi, BoundingPlane.x * Normal + BoundingPlane.z * dPdu, Center - dPdu * HalfWidth) ||
IsBoxBehindPlane(Lo, Hi, BoundingPlane.x * Normal - BoundingPlane.z * dPdu, Center + dPdu * HalfWidth) ||
IsBoxBehindPlane(Lo, Hi, BoundingPlane.y * Normal + BoundingPlane.z * dPdv, Center - dPdv * HalfHeight) ||
IsBoxBehindPlane(Lo, Hi, BoundingPlane.y * Normal - BoundingPlane.z * dPdv, Center + dPdv * HalfHeight))
{
return false;
}
if (!bImprovedFrustumTest)
{
return true;
}
// Checking the AABB against the 4 planes is not sufficient, and some cases which still lie outside could still pass through
// See description here: https://iquilezles.org/articles/frustumcorrect/
// Normalize the penumbra vector and scale it to the back of the sphere of influence
BoundingPlane = normalize(BoundingPlane);
BoundingPlane.xy *= Radius * rcp(BoundingPlane.z);
// Get 8 corners of the penumbra frustum in world space
const float3 C00 = Center - HalfWidth * dPdu - HalfHeight * dPdv, F00 = C00 - BoundingPlane.x * dPdu - BoundingPlane.y * dPdv + Radius * Normal;
const float3 C01 = Center + HalfWidth * dPdu + HalfHeight * dPdv, F01 = C01 - BoundingPlane.x * dPdu + BoundingPlane.y * dPdv + Radius * Normal;
const float3 C10 = Center - HalfWidth * dPdu - HalfHeight * dPdv, F10 = C10 + BoundingPlane.x * dPdu - BoundingPlane.y * dPdv + Radius * Normal;
const float3 C11 = Center + HalfWidth * dPdu + HalfHeight * dPdv, F11 = C11 + BoundingPlane.x * dPdu + BoundingPlane.y * dPdv + Radius * Normal;
// Get an AABB around the 8 frustum points and check if it overlaps the query AABB
if (any(max(max(max(C00, F00), max(C01, F01)), max(max(C10, F10), max(C11, F11))) < Lo) ||
any(min(min(min(C00, F00), min(C01, F01)), min(min(C10, F10), min(C11, F11))) > Hi))
{
return false;
}
return true;
}