using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Rendering { // 6-component representation of a (infinite length) line in 3D space internal struct Line { // for the line to be valid, dot(m, t) == 0 public float3 m; public float3 t; internal static Line LineOfPlaneIntersectingPlane(float4 a, float4 b) { // planes do not need to have a unit length normal return new Line { m = a.w*b.xyz - b.w*a.xyz, t = math.cross(a.xyz, b.xyz), }; } internal static float4 PlaneContainingLineAndPoint(Line a, float3 b) { // the resulting plane will not have a unit length normal (and the normal will be approximately zero when no plane exists) return new float4(a.m + math.cross(a.t, b), -math.dot(a.m, b)); } internal static float4 PlaneContainingLineWithNormalPerpendicularToVector(Line a, float3 b) { // the resulting plane will not have a unit length normal (and the normal will be approximately zero when no plane exists) return new float4(math.cross(a.t, b), -math.dot(a.m, b)); } } internal struct ReceiverPlanes { public NativeList planes; public int lightFacingPlaneCount; private static bool IsSignBitSet(float x) { uint i = math.asuint(x); return (i >> 31) != 0; } internal NativeArray SilhouettePlaneSubArray() { return planes.AsArray().GetSubArray(lightFacingPlaneCount, planes.Length - lightFacingPlaneCount); } internal NativeArray CopyLightFacingFrustumPlanes(Allocator allocator) { var facingPlanes = new NativeArray(lightFacingPlaneCount, allocator, NativeArrayOptions.UninitializedMemory); for (int i = 0; i < lightFacingPlaneCount; ++i) facingPlanes[i] = planes[i]; return facingPlanes; } internal static ReceiverPlanes CreateEmptyForTesting(Allocator allocator) { return new ReceiverPlanes() { planes = new NativeList(allocator), lightFacingPlaneCount = 0, }; } internal void Dispose() { planes.Dispose(); } internal static ReceiverPlanes Create(in BatchCullingContext cc, Allocator allocator) { var result = new ReceiverPlanes() { planes = new NativeList(allocator), lightFacingPlaneCount = 0, }; if (cc.viewType == BatchCullingViewType.Light && cc.receiverPlaneCount != 0) { bool isLightOrthographic = false; if (cc.cullingSplits.Length > 0) { Matrix4x4 m = cc.cullingSplits[0].cullingMatrix; isLightOrthographic = m[15] == 1.0f && m[11] == 0.0f && m[7] == 0.0f && m[3] == 0.0f; } if (isLightOrthographic) { Vector3 lightDir = -cc.localToWorldMatrix.GetColumn(2); // cache result for each plane, add planes facing towards the light int planeSignBits = 0; for (int i = 0; i < cc.receiverPlaneCount; ++i) { var plane = cc.cullingPlanes[cc.receiverPlaneOffset + i]; float facingTerm = Vector3.Dot(plane.normal, lightDir); if (IsSignBitSet(facingTerm)) planeSignBits |= (1 << i); else result.planes.Add(plane); } result.lightFacingPlaneCount = result.planes.Length; // assume ordering +/-x, +/-y, +/-z for frustum planes, test pairs for silhouette edges if (cc.receiverPlaneCount == 6) { for (int i = 0; i < cc.receiverPlaneCount; ++i) { for (int j = i + 1; j < cc.receiverPlaneCount; ++j) { // skip pairs that are from the same frustum axis (i.e. both xs, both ys or both zs) if ((i / 2) == (j / 2)) continue; // silhouette edges occur when the planes have opposing signs int signCheck = ((planeSignBits >> i) ^ (planeSignBits >> j)) & 1; if (signCheck == 0) continue; // process in consistent order for consistent plane normal in the result var (indexA, indexB) = (((planeSignBits >> i) & 1) == 0) ? (i, j) : (j, i); var planeA = cc.cullingPlanes[cc.receiverPlaneOffset + indexA]; var planeB = cc.cullingPlanes[cc.receiverPlaneOffset + indexB]; // construct a plane that contains the light origin and this silhouette edge var planeEqA = new float4(planeA.normal, planeA.distance); var planeEqB = new float4(planeB.normal, planeB.distance); var silhouetteEdge = Line.LineOfPlaneIntersectingPlane(planeEqA, planeEqB); var silhouettePlaneEq = Line.PlaneContainingLineWithNormalPerpendicularToVector(silhouetteEdge, lightDir); // try to normalize silhouettePlaneEq = silhouettePlaneEq / math.length(silhouettePlaneEq.xyz); if (!math.any(math.isnan(silhouettePlaneEq))) result.planes.Add(new Plane(silhouettePlaneEq.xyz, silhouettePlaneEq.w)); } } } } else { var lightPos = cc.localToWorldMatrix.GetPosition(); // cache result for each plane, add planes facing towards the light int planeSignBits = 0; for (int i = 0; i < cc.receiverPlaneCount; ++i) { var plane = cc.cullingPlanes[cc.receiverPlaneOffset + i]; float distance = plane.GetDistanceToPoint(lightPos); if (IsSignBitSet(distance)) planeSignBits |= (1 << i); else result.planes.Add(plane); } result.lightFacingPlaneCount = result.planes.Length; // assume ordering +/-x, +/-y, +/-z for frustum planes, test pairs for silhouette edges if (cc.receiverPlaneCount == 6) { for (int i = 0; i < cc.receiverPlaneCount; ++i) { for (int j = i + 1; j < cc.receiverPlaneCount; ++j) { // skip pairs that are from the same frustum axis (i.e. both xs, both ys or both zs) if ((i / 2) == (j / 2)) continue; // silhouette edges occur when the planes have opposing signs int signCheck = ((planeSignBits >> i) ^ (planeSignBits >> j)) & 1; if (signCheck == 0) continue; // process in consistent order for consistent plane normal in the result var (indexA, indexB) = (((planeSignBits >> i) & 1) == 0) ? (i, j) : (j, i); var planeA = cc.cullingPlanes[cc.receiverPlaneOffset + indexA]; var planeB = cc.cullingPlanes[cc.receiverPlaneOffset + indexB]; // construct a plane that contains the light origin and this silhouette edge var planeEqA = new float4(planeA.normal, planeA.distance); var planeEqB = new float4(planeB.normal, planeB.distance); var silhouetteEdge = Line.LineOfPlaneIntersectingPlane(planeEqA, planeEqB); var silhouettePlaneEq = Line.PlaneContainingLineAndPoint(silhouetteEdge, lightPos); // try to normalize silhouettePlaneEq = silhouettePlaneEq / math.length(silhouettePlaneEq.xyz); if (!math.any(math.isnan(silhouettePlaneEq))) result.planes.Add(new Plane(silhouettePlaneEq.xyz, silhouettePlaneEq.w)); } } } } } return result; } } internal struct FrustumPlaneCuller { internal struct PlanePacket4 { public float4 nx; public float4 ny; public float4 nz; public float4 d; // Store absolute values of plane normals to avoid recalculating per instance public float4 nxAbs; public float4 nyAbs; public float4 nzAbs; public PlanePacket4(NativeArray planes, int offset, int limit) { Plane p0 = planes[Mathf.Min(offset + 0, limit)]; Plane p1 = planes[Mathf.Min(offset + 1, limit)]; Plane p2 = planes[Mathf.Min(offset + 2, limit)]; Plane p3 = planes[Mathf.Min(offset + 3, limit)]; nx = new float4(p0.normal.x, p1.normal.x, p2.normal.x, p3.normal.x); ny = new float4(p0.normal.y, p1.normal.y, p2.normal.y, p3.normal.y); nz = new float4(p0.normal.z, p1.normal.z, p2.normal.z, p3.normal.z); d = new float4(p0.distance, p1.distance, p2.distance, p3.distance); nxAbs = math.abs(nx); nyAbs = math.abs(ny); nzAbs = math.abs(nz); } } internal struct SplitInfo { public int packetCount; } public NativeArray planePackets; public NativeArray splitInfos; internal static FrustumPlaneCuller Create(in BatchCullingContext cc, NativeArray receiverPlanes, in ReceiverSphereCuller receiverSphereCuller, Allocator allocator) { int splitCount = cc.cullingSplits.Length; int totalPacketCount = 0; for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { int planeCount = receiverPlanes.Length + cc.cullingSplits[splitIndex].cullingPlaneCount; totalPacketCount += (planeCount + 3)/4; } FrustumPlaneCuller result = new FrustumPlaneCuller() { planePackets = new NativeArray(totalPacketCount, allocator, NativeArrayOptions.UninitializedMemory), splitInfos = new NativeArray(splitCount, allocator, NativeArrayOptions.UninitializedMemory), }; var tmpPlanes = new NativeList(Allocator.Temp); int packetBase = 0; for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { CullingSplit split = cc.cullingSplits[splitIndex]; tmpPlanes.Clear(); // use all culling planes for (int i = 0; i < split.cullingPlaneCount; ++i) tmpPlanes.Add(cc.cullingPlanes[split.cullingPlaneOffset + i]); // conditionally use receiver planes if (receiverSphereCuller.UseReceiverPlanes()) tmpPlanes.AddRange(receiverPlanes); int packetCount = (tmpPlanes.Length + 3)/4; result.splitInfos[splitIndex] = new SplitInfo() { packetCount = packetCount, }; for (int i = 0; i < packetCount; ++i) result.planePackets[packetBase + i] = new PlanePacket4(tmpPlanes.AsArray(), 4*i, tmpPlanes.Length - 1); packetBase += packetCount; } tmpPlanes.Dispose(); return result; } internal static uint ComputeSplitVisibilityMask(NativeArray planePackets, NativeArray splitInfos, in AABB bounds) { float4 cx = bounds.center.xxxx; float4 cy = bounds.center.yyyy; float4 cz = bounds.center.zzzz; float4 ex = bounds.extents.xxxx; float4 ey = bounds.extents.yyyy; float4 ez = bounds.extents.zzzz; uint splitVisibilityMask = 0; int packetBase = 0; int splitCount = splitInfos.Length; for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { SplitInfo splitInfo = splitInfos[splitIndex]; bool4 isCulled = new bool4(false); for (int i = 0; i < splitInfo.packetCount; ++i) { PlanePacket4 p = planePackets[packetBase + i]; float4 distances = p.nx*cx + p.ny*cy + p.nz*cz + p.d; float4 radii = p.nxAbs*ex + p.nyAbs*ey + p.nzAbs*ez; isCulled = isCulled | (distances + radii < float4.zero); } if (!math.any(isCulled)) splitVisibilityMask |= 1U << splitIndex; packetBase += splitInfo.packetCount; } return splitVisibilityMask; } } internal struct ReceiverSphereCuller { internal struct SplitInfo { public float4 receiverSphereLightSpace; public float cascadeBlendCullingFactor; } public NativeArray splitInfos; public float3x3 worldToLightSpaceRotation; internal static ReceiverSphereCuller CreateEmptyForTesting(Allocator allocator) { return new ReceiverSphereCuller() { splitInfos = new NativeArray(0, allocator), worldToLightSpaceRotation = float3x3.identity, }; } internal void Dispose() { splitInfos.Dispose(); } internal bool UseReceiverPlanes() { // only use receiver planes if there are no receiver spheres // (if spheres are present, then this is directional light cascades and Unity has already added receiver planes to the culling planes) return splitInfos.Length == 0; } internal static ReceiverSphereCuller Create(in BatchCullingContext cc, Allocator allocator) { int splitCount = cc.cullingSplits.Length; // only set up sphere culling when there are multiple splits and all splits have valid spheres bool allSpheresValid = (splitCount > 1); for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { // ensure that NaN is not considered as valid if (!(cc.cullingSplits[splitIndex].sphereRadius > 0.0f)) allSpheresValid = false; } if (!allSpheresValid) splitCount = 0; float3x3 lightToWorldSpaceRotation = (float3x3)(float4x4)cc.localToWorldMatrix; ReceiverSphereCuller result = new ReceiverSphereCuller() { splitInfos = new NativeArray(splitCount, allocator, NativeArrayOptions.UninitializedMemory), worldToLightSpaceRotation = math.transpose(lightToWorldSpaceRotation), }; for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { var cullingSplit = cc.cullingSplits[splitIndex]; float4 receiverSphereLightSpace = new float4( math.mul(result.worldToLightSpaceRotation, cullingSplit.sphereCenter), cullingSplit.sphereRadius); result.splitInfos[splitIndex] = new SplitInfo() { receiverSphereLightSpace = receiverSphereLightSpace, cascadeBlendCullingFactor = cullingSplit.cascadeBlendCullingFactor, }; } return result; } internal static float DistanceUntilCylinderFullyCrossesPlane( float3 cylinderCenter, float3 cylinderDirection, float cylinderRadius, Plane plane) { float cosEpsilon = 0.001f; // clamp the cosine of glancing angles // compute the distance until the center intersects the plane float cosTheta = math.max(math.abs(math.dot(plane.normal, cylinderDirection)), cosEpsilon); float heightAbovePlane = math.dot(plane.normal, cylinderCenter) + plane.distance; float centerDistanceToPlane = heightAbovePlane/cosTheta; // compute the additional distance until the edge of the cylinder intersects the plane float sinTheta = math.sqrt(math.max(1.0f - cosTheta*cosTheta, 0.0f)); float edgeDistanceToPlane = cylinderRadius*sinTheta/cosTheta; return centerDistanceToPlane + edgeDistanceToPlane; } internal static uint ComputeSplitVisibilityMask( NativeArray lightFacingFrustumPlanes, NativeArray splitInfos, float3x3 worldToLightSpaceRotation, in AABB bounds) { float3 casterCenterWorldSpace = bounds.center; float3 casterCenterLightSpace = math.mul(worldToLightSpaceRotation, bounds.center); float casterRadius = math.length(bounds.extents); // push the (light-facing) frustum planes back by the caster radius, then intersect with a line through the caster capsule center, // to compute the length of the shadow that will cover all possible receivers within the whole frustum (not just this split) float3 shadowDirection = math.transpose(worldToLightSpaceRotation).c2; float shadowLength = math.INFINITY; for (int i = 0; i < lightFacingFrustumPlanes.Length; ++i) { shadowLength = math.min(shadowLength, DistanceUntilCylinderFullyCrossesPlane( casterCenterWorldSpace, shadowDirection, casterRadius, lightFacingFrustumPlanes[i])); } shadowLength = math.max(shadowLength, 0.0f); uint splitVisibilityMask = 0; int splitCount = splitInfos.Length; for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex) { SplitInfo splitInfo = splitInfos[splitIndex]; float3 receiverCenterLightSpace = splitInfo.receiverSphereLightSpace.xyz; float receiverRadius = splitInfo.receiverSphereLightSpace.w; float3 receiverToCasterLightSpace = casterCenterLightSpace - receiverCenterLightSpace; // compute the light space z coordinate where the caster sphere and receiver sphere just intersect float sphereIntersectionMaxDistance = casterRadius + receiverRadius; float zSqAtSphereIntersection = math.lengthsq(sphereIntersectionMaxDistance) - math.lengthsq(receiverToCasterLightSpace.xy); // if this is negative, the spheres do not overlap as circles in the XY plane, so cull the caster if (zSqAtSphereIntersection < 0.0f) continue; // if the caster is outside of the receiver sphere in the light direction, it cannot cast a shadow on it, so cull it if (receiverToCasterLightSpace.z > 0.0f && math.lengthsq(receiverToCasterLightSpace.z) > zSqAtSphereIntersection) continue; // render the caster in this split splitVisibilityMask |= 1U << splitIndex; // culling assumes that shaders will always sample from the cascade with the lowest index, // so if the caster capsule is fully contained within the "core" sphere where only this split index is sampled, // then cull this caster from all the larger index splits (break from this loop) // (it is sufficient to test that only the capsule start and end spheres are within the "core" receiver sphere) float coreRadius = receiverRadius * splitInfo.cascadeBlendCullingFactor; float3 receiverToShadowEndLightSpace = receiverToCasterLightSpace + new float3(0.0f, 0.0f, shadowLength); float capsuleMaxDistance = coreRadius - casterRadius; float capsuleDistanceSq = math.max(math.lengthsq(receiverToCasterLightSpace), math.lengthsq(receiverToShadowEndLightSpace)); if (capsuleMaxDistance > 0.0f && capsuleDistanceSq < math.lengthsq(capsuleMaxDistance)) break; } return splitVisibilityMask; } } }