Nessuna descrizione
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.

FrustumPlanes.cs 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. using Unity.Collections;
  2. using Unity.Mathematics;
  3. namespace UnityEngine.Rendering
  4. {
  5. // 6-component representation of a (infinite length) line in 3D space
  6. internal struct Line
  7. {
  8. // for the line to be valid, dot(m, t) == 0
  9. public float3 m;
  10. public float3 t;
  11. internal static Line LineOfPlaneIntersectingPlane(float4 a, float4 b)
  12. {
  13. // planes do not need to have a unit length normal
  14. return new Line {
  15. m = a.w*b.xyz - b.w*a.xyz,
  16. t = math.cross(a.xyz, b.xyz),
  17. };
  18. }
  19. internal static float4 PlaneContainingLineAndPoint(Line a, float3 b)
  20. {
  21. // the resulting plane will not have a unit length normal (and the normal will be approximately zero when no plane exists)
  22. return new float4(a.m + math.cross(a.t, b), -math.dot(a.m, b));
  23. }
  24. internal static float4 PlaneContainingLineWithNormalPerpendicularToVector(Line a, float3 b)
  25. {
  26. // the resulting plane will not have a unit length normal (and the normal will be approximately zero when no plane exists)
  27. return new float4(math.cross(a.t, b), -math.dot(a.m, b));
  28. }
  29. }
  30. internal struct ReceiverPlanes
  31. {
  32. public NativeList<Plane> planes;
  33. public int lightFacingPlaneCount;
  34. private static bool IsSignBitSet(float x)
  35. {
  36. uint i = math.asuint(x);
  37. return (i >> 31) != 0;
  38. }
  39. internal NativeArray<Plane> SilhouettePlaneSubArray()
  40. {
  41. return planes.AsArray().GetSubArray(lightFacingPlaneCount, planes.Length - lightFacingPlaneCount);
  42. }
  43. internal NativeArray<Plane> CopyLightFacingFrustumPlanes(Allocator allocator)
  44. {
  45. var facingPlanes = new NativeArray<Plane>(lightFacingPlaneCount, allocator, NativeArrayOptions.UninitializedMemory);
  46. for (int i = 0; i < lightFacingPlaneCount; ++i)
  47. facingPlanes[i] = planes[i];
  48. return facingPlanes;
  49. }
  50. internal static ReceiverPlanes CreateEmptyForTesting(Allocator allocator)
  51. {
  52. return new ReceiverPlanes()
  53. {
  54. planes = new NativeList<Plane>(allocator),
  55. lightFacingPlaneCount = 0,
  56. };
  57. }
  58. internal void Dispose()
  59. {
  60. planes.Dispose();
  61. }
  62. internal static ReceiverPlanes Create(in BatchCullingContext cc, Allocator allocator)
  63. {
  64. var result = new ReceiverPlanes()
  65. {
  66. planes = new NativeList<Plane>(allocator),
  67. lightFacingPlaneCount = 0,
  68. };
  69. if (cc.viewType == BatchCullingViewType.Light && cc.receiverPlaneCount != 0)
  70. {
  71. bool isLightOrthographic = false;
  72. if (cc.cullingSplits.Length > 0)
  73. {
  74. Matrix4x4 m = cc.cullingSplits[0].cullingMatrix;
  75. isLightOrthographic = m[15] == 1.0f && m[11] == 0.0f && m[7] == 0.0f && m[3] == 0.0f;
  76. }
  77. if (isLightOrthographic)
  78. {
  79. Vector3 lightDir = -cc.localToWorldMatrix.GetColumn(2);
  80. // cache result for each plane, add planes facing towards the light
  81. int planeSignBits = 0;
  82. for (int i = 0; i < cc.receiverPlaneCount; ++i)
  83. {
  84. var plane = cc.cullingPlanes[cc.receiverPlaneOffset + i];
  85. float facingTerm = Vector3.Dot(plane.normal, lightDir);
  86. if (IsSignBitSet(facingTerm))
  87. planeSignBits |= (1 << i);
  88. else
  89. result.planes.Add(plane);
  90. }
  91. result.lightFacingPlaneCount = result.planes.Length;
  92. // assume ordering +/-x, +/-y, +/-z for frustum planes, test pairs for silhouette edges
  93. if (cc.receiverPlaneCount == 6)
  94. {
  95. for (int i = 0; i < cc.receiverPlaneCount; ++i)
  96. {
  97. for (int j = i + 1; j < cc.receiverPlaneCount; ++j)
  98. {
  99. // skip pairs that are from the same frustum axis (i.e. both xs, both ys or both zs)
  100. if ((i / 2) == (j / 2))
  101. continue;
  102. // silhouette edges occur when the planes have opposing signs
  103. int signCheck = ((planeSignBits >> i) ^ (planeSignBits >> j)) & 1;
  104. if (signCheck == 0)
  105. continue;
  106. // process in consistent order for consistent plane normal in the result
  107. var (indexA, indexB) = (((planeSignBits >> i) & 1) == 0) ? (i, j) : (j, i);
  108. var planeA = cc.cullingPlanes[cc.receiverPlaneOffset + indexA];
  109. var planeB = cc.cullingPlanes[cc.receiverPlaneOffset + indexB];
  110. // construct a plane that contains the light origin and this silhouette edge
  111. var planeEqA = new float4(planeA.normal, planeA.distance);
  112. var planeEqB = new float4(planeB.normal, planeB.distance);
  113. var silhouetteEdge = Line.LineOfPlaneIntersectingPlane(planeEqA, planeEqB);
  114. var silhouettePlaneEq = Line.PlaneContainingLineWithNormalPerpendicularToVector(silhouetteEdge, lightDir);
  115. // try to normalize
  116. silhouettePlaneEq = silhouettePlaneEq / math.length(silhouettePlaneEq.xyz);
  117. if (!math.any(math.isnan(silhouettePlaneEq)))
  118. result.planes.Add(new Plane(silhouettePlaneEq.xyz, silhouettePlaneEq.w));
  119. }
  120. }
  121. }
  122. }
  123. else
  124. {
  125. var lightPos = cc.localToWorldMatrix.GetPosition();
  126. // cache result for each plane, add planes facing towards the light
  127. int planeSignBits = 0;
  128. for (int i = 0; i < cc.receiverPlaneCount; ++i)
  129. {
  130. var plane = cc.cullingPlanes[cc.receiverPlaneOffset + i];
  131. float distance = plane.GetDistanceToPoint(lightPos);
  132. if (IsSignBitSet(distance))
  133. planeSignBits |= (1 << i);
  134. else
  135. result.planes.Add(plane);
  136. }
  137. result.lightFacingPlaneCount = result.planes.Length;
  138. // assume ordering +/-x, +/-y, +/-z for frustum planes, test pairs for silhouette edges
  139. if (cc.receiverPlaneCount == 6)
  140. {
  141. for (int i = 0; i < cc.receiverPlaneCount; ++i)
  142. {
  143. for (int j = i + 1; j < cc.receiverPlaneCount; ++j)
  144. {
  145. // skip pairs that are from the same frustum axis (i.e. both xs, both ys or both zs)
  146. if ((i / 2) == (j / 2))
  147. continue;
  148. // silhouette edges occur when the planes have opposing signs
  149. int signCheck = ((planeSignBits >> i) ^ (planeSignBits >> j)) & 1;
  150. if (signCheck == 0)
  151. continue;
  152. // process in consistent order for consistent plane normal in the result
  153. var (indexA, indexB) = (((planeSignBits >> i) & 1) == 0) ? (i, j) : (j, i);
  154. var planeA = cc.cullingPlanes[cc.receiverPlaneOffset + indexA];
  155. var planeB = cc.cullingPlanes[cc.receiverPlaneOffset + indexB];
  156. // construct a plane that contains the light origin and this silhouette edge
  157. var planeEqA = new float4(planeA.normal, planeA.distance);
  158. var planeEqB = new float4(planeB.normal, planeB.distance);
  159. var silhouetteEdge = Line.LineOfPlaneIntersectingPlane(planeEqA, planeEqB);
  160. var silhouettePlaneEq = Line.PlaneContainingLineAndPoint(silhouetteEdge, lightPos);
  161. // try to normalize
  162. silhouettePlaneEq = silhouettePlaneEq / math.length(silhouettePlaneEq.xyz);
  163. if (!math.any(math.isnan(silhouettePlaneEq)))
  164. result.planes.Add(new Plane(silhouettePlaneEq.xyz, silhouettePlaneEq.w));
  165. }
  166. }
  167. }
  168. }
  169. }
  170. return result;
  171. }
  172. }
  173. internal struct FrustumPlaneCuller
  174. {
  175. internal struct PlanePacket4
  176. {
  177. public float4 nx;
  178. public float4 ny;
  179. public float4 nz;
  180. public float4 d;
  181. // Store absolute values of plane normals to avoid recalculating per instance
  182. public float4 nxAbs;
  183. public float4 nyAbs;
  184. public float4 nzAbs;
  185. public PlanePacket4(NativeArray<Plane> planes, int offset, int limit)
  186. {
  187. Plane p0 = planes[Mathf.Min(offset + 0, limit)];
  188. Plane p1 = planes[Mathf.Min(offset + 1, limit)];
  189. Plane p2 = planes[Mathf.Min(offset + 2, limit)];
  190. Plane p3 = planes[Mathf.Min(offset + 3, limit)];
  191. nx = new float4(p0.normal.x, p1.normal.x, p2.normal.x, p3.normal.x);
  192. ny = new float4(p0.normal.y, p1.normal.y, p2.normal.y, p3.normal.y);
  193. nz = new float4(p0.normal.z, p1.normal.z, p2.normal.z, p3.normal.z);
  194. d = new float4(p0.distance, p1.distance, p2.distance, p3.distance);
  195. nxAbs = math.abs(nx);
  196. nyAbs = math.abs(ny);
  197. nzAbs = math.abs(nz);
  198. }
  199. }
  200. internal struct SplitInfo
  201. {
  202. public int packetCount;
  203. }
  204. public NativeArray<PlanePacket4> planePackets;
  205. public NativeArray<SplitInfo> splitInfos;
  206. internal static FrustumPlaneCuller Create(in BatchCullingContext cc, NativeArray<Plane> receiverPlanes, in ReceiverSphereCuller receiverSphereCuller, Allocator allocator)
  207. {
  208. int splitCount = cc.cullingSplits.Length;
  209. int totalPacketCount = 0;
  210. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  211. {
  212. int planeCount = receiverPlanes.Length + cc.cullingSplits[splitIndex].cullingPlaneCount;
  213. totalPacketCount += (planeCount + 3)/4;
  214. }
  215. FrustumPlaneCuller result = new FrustumPlaneCuller()
  216. {
  217. planePackets = new NativeArray<PlanePacket4>(totalPacketCount, allocator, NativeArrayOptions.UninitializedMemory),
  218. splitInfos = new NativeArray<SplitInfo>(splitCount, allocator, NativeArrayOptions.UninitializedMemory),
  219. };
  220. var tmpPlanes = new NativeList<Plane>(Allocator.Temp);
  221. int packetBase = 0;
  222. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  223. {
  224. CullingSplit split = cc.cullingSplits[splitIndex];
  225. tmpPlanes.Clear();
  226. // use all culling planes
  227. for (int i = 0; i < split.cullingPlaneCount; ++i)
  228. tmpPlanes.Add(cc.cullingPlanes[split.cullingPlaneOffset + i]);
  229. // conditionally use receiver planes
  230. if (receiverSphereCuller.UseReceiverPlanes())
  231. tmpPlanes.AddRange(receiverPlanes);
  232. int packetCount = (tmpPlanes.Length + 3)/4;
  233. result.splitInfos[splitIndex] = new SplitInfo()
  234. {
  235. packetCount = packetCount,
  236. };
  237. for (int i = 0; i < packetCount; ++i)
  238. result.planePackets[packetBase + i] = new PlanePacket4(tmpPlanes.AsArray(), 4*i, tmpPlanes.Length - 1);
  239. packetBase += packetCount;
  240. }
  241. tmpPlanes.Dispose();
  242. return result;
  243. }
  244. internal static uint ComputeSplitVisibilityMask(NativeArray<PlanePacket4> planePackets, NativeArray<SplitInfo> splitInfos, in AABB bounds)
  245. {
  246. float4 cx = bounds.center.xxxx;
  247. float4 cy = bounds.center.yyyy;
  248. float4 cz = bounds.center.zzzz;
  249. float4 ex = bounds.extents.xxxx;
  250. float4 ey = bounds.extents.yyyy;
  251. float4 ez = bounds.extents.zzzz;
  252. uint splitVisibilityMask = 0;
  253. int packetBase = 0;
  254. int splitCount = splitInfos.Length;
  255. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  256. {
  257. SplitInfo splitInfo = splitInfos[splitIndex];
  258. bool4 isCulled = new bool4(false);
  259. for (int i = 0; i < splitInfo.packetCount; ++i)
  260. {
  261. PlanePacket4 p = planePackets[packetBase + i];
  262. float4 distances = p.nx*cx + p.ny*cy + p.nz*cz + p.d;
  263. float4 radii = p.nxAbs*ex + p.nyAbs*ey + p.nzAbs*ez;
  264. isCulled = isCulled | (distances + radii < float4.zero);
  265. }
  266. if (!math.any(isCulled))
  267. splitVisibilityMask |= 1U << splitIndex;
  268. packetBase += splitInfo.packetCount;
  269. }
  270. return splitVisibilityMask;
  271. }
  272. }
  273. internal struct ReceiverSphereCuller
  274. {
  275. internal struct SplitInfo
  276. {
  277. public float4 receiverSphereLightSpace;
  278. public float cascadeBlendCullingFactor;
  279. }
  280. public NativeArray<SplitInfo> splitInfos;
  281. public float3x3 worldToLightSpaceRotation;
  282. internal static ReceiverSphereCuller CreateEmptyForTesting(Allocator allocator)
  283. {
  284. return new ReceiverSphereCuller()
  285. {
  286. splitInfos = new NativeArray<SplitInfo>(0, allocator),
  287. worldToLightSpaceRotation = float3x3.identity,
  288. };
  289. }
  290. internal void Dispose()
  291. {
  292. splitInfos.Dispose();
  293. }
  294. internal bool UseReceiverPlanes()
  295. {
  296. // only use receiver planes if there are no receiver spheres
  297. // (if spheres are present, then this is directional light cascades and Unity has already added receiver planes to the culling planes)
  298. return splitInfos.Length == 0;
  299. }
  300. internal static ReceiverSphereCuller Create(in BatchCullingContext cc, Allocator allocator)
  301. {
  302. int splitCount = cc.cullingSplits.Length;
  303. // only set up sphere culling when there are multiple splits and all splits have valid spheres
  304. bool allSpheresValid = (splitCount > 1);
  305. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  306. {
  307. // ensure that NaN is not considered as valid
  308. if (!(cc.cullingSplits[splitIndex].sphereRadius > 0.0f))
  309. allSpheresValid = false;
  310. }
  311. if (!allSpheresValid)
  312. splitCount = 0;
  313. float3x3 lightToWorldSpaceRotation = (float3x3)(float4x4)cc.localToWorldMatrix;
  314. ReceiverSphereCuller result = new ReceiverSphereCuller()
  315. {
  316. splitInfos = new NativeArray<SplitInfo>(splitCount, allocator, NativeArrayOptions.UninitializedMemory),
  317. worldToLightSpaceRotation = math.transpose(lightToWorldSpaceRotation),
  318. };
  319. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  320. {
  321. var cullingSplit = cc.cullingSplits[splitIndex];
  322. float4 receiverSphereLightSpace = new float4(
  323. math.mul(result.worldToLightSpaceRotation, cullingSplit.sphereCenter),
  324. cullingSplit.sphereRadius);
  325. result.splitInfos[splitIndex] = new SplitInfo()
  326. {
  327. receiverSphereLightSpace = receiverSphereLightSpace,
  328. cascadeBlendCullingFactor = cullingSplit.cascadeBlendCullingFactor,
  329. };
  330. }
  331. return result;
  332. }
  333. internal static float DistanceUntilCylinderFullyCrossesPlane(
  334. float3 cylinderCenter,
  335. float3 cylinderDirection,
  336. float cylinderRadius,
  337. Plane plane)
  338. {
  339. float cosEpsilon = 0.001f; // clamp the cosine of glancing angles
  340. // compute the distance until the center intersects the plane
  341. float cosTheta = math.max(math.abs(math.dot(plane.normal, cylinderDirection)), cosEpsilon);
  342. float heightAbovePlane = math.dot(plane.normal, cylinderCenter) + plane.distance;
  343. float centerDistanceToPlane = heightAbovePlane/cosTheta;
  344. // compute the additional distance until the edge of the cylinder intersects the plane
  345. float sinTheta = math.sqrt(math.max(1.0f - cosTheta*cosTheta, 0.0f));
  346. float edgeDistanceToPlane = cylinderRadius*sinTheta/cosTheta;
  347. return centerDistanceToPlane + edgeDistanceToPlane;
  348. }
  349. internal static uint ComputeSplitVisibilityMask(
  350. NativeArray<Plane> lightFacingFrustumPlanes,
  351. NativeArray<SplitInfo> splitInfos,
  352. float3x3 worldToLightSpaceRotation,
  353. in AABB bounds)
  354. {
  355. float3 casterCenterWorldSpace = bounds.center;
  356. float3 casterCenterLightSpace = math.mul(worldToLightSpaceRotation, bounds.center);
  357. float casterRadius = math.length(bounds.extents);
  358. // push the (light-facing) frustum planes back by the caster radius, then intersect with a line through the caster capsule center,
  359. // to compute the length of the shadow that will cover all possible receivers within the whole frustum (not just this split)
  360. float3 shadowDirection = math.transpose(worldToLightSpaceRotation).c2;
  361. float shadowLength = math.INFINITY;
  362. for (int i = 0; i < lightFacingFrustumPlanes.Length; ++i)
  363. {
  364. shadowLength = math.min(shadowLength, DistanceUntilCylinderFullyCrossesPlane(
  365. casterCenterWorldSpace,
  366. shadowDirection,
  367. casterRadius,
  368. lightFacingFrustumPlanes[i]));
  369. }
  370. shadowLength = math.max(shadowLength, 0.0f);
  371. uint splitVisibilityMask = 0;
  372. int splitCount = splitInfos.Length;
  373. for (int splitIndex = 0; splitIndex < splitCount; ++splitIndex)
  374. {
  375. SplitInfo splitInfo = splitInfos[splitIndex];
  376. float3 receiverCenterLightSpace = splitInfo.receiverSphereLightSpace.xyz;
  377. float receiverRadius = splitInfo.receiverSphereLightSpace.w;
  378. float3 receiverToCasterLightSpace = casterCenterLightSpace - receiverCenterLightSpace;
  379. // compute the light space z coordinate where the caster sphere and receiver sphere just intersect
  380. float sphereIntersectionMaxDistance = casterRadius + receiverRadius;
  381. float zSqAtSphereIntersection = math.lengthsq(sphereIntersectionMaxDistance) - math.lengthsq(receiverToCasterLightSpace.xy);
  382. // if this is negative, the spheres do not overlap as circles in the XY plane, so cull the caster
  383. if (zSqAtSphereIntersection < 0.0f)
  384. continue;
  385. // if the caster is outside of the receiver sphere in the light direction, it cannot cast a shadow on it, so cull it
  386. if (receiverToCasterLightSpace.z > 0.0f && math.lengthsq(receiverToCasterLightSpace.z) > zSqAtSphereIntersection)
  387. continue;
  388. // render the caster in this split
  389. splitVisibilityMask |= 1U << splitIndex;
  390. // culling assumes that shaders will always sample from the cascade with the lowest index,
  391. // so if the caster capsule is fully contained within the "core" sphere where only this split index is sampled,
  392. // then cull this caster from all the larger index splits (break from this loop)
  393. // (it is sufficient to test that only the capsule start and end spheres are within the "core" receiver sphere)
  394. float coreRadius = receiverRadius * splitInfo.cascadeBlendCullingFactor;
  395. float3 receiverToShadowEndLightSpace = receiverToCasterLightSpace + new float3(0.0f, 0.0f, shadowLength);
  396. float capsuleMaxDistance = coreRadius - casterRadius;
  397. float capsuleDistanceSq = math.max(math.lengthsq(receiverToCasterLightSpace), math.lengthsq(receiverToShadowEndLightSpace));
  398. if (capsuleMaxDistance > 0.0f && capsuleDistanceSq < math.lengthsq(capsuleMaxDistance))
  399. break;
  400. }
  401. return splitVisibilityMask;
  402. }
  403. }
  404. }