설명 없음
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.

TilingJob.cs 54KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. using System;
  2. using Unity.Burst;
  3. using Unity.Collections;
  4. using Unity.Jobs;
  5. using Unity.Mathematics;
  6. namespace UnityEngine.Rendering.Universal
  7. {
  8. [BurstCompile(FloatMode = FloatMode.Default, DisableSafetyChecks = true, OptimizeFor = OptimizeFor.Performance)]
  9. struct TilingJob : IJobFor
  10. {
  11. [ReadOnly]
  12. public NativeArray<VisibleLight> lights;
  13. [ReadOnly]
  14. public NativeArray<VisibleReflectionProbe> reflectionProbes;
  15. [NativeDisableParallelForRestriction]
  16. public NativeArray<InclusiveRange> tileRanges;
  17. public int itemsPerTile;
  18. public int rangesPerItem;
  19. public Fixed2<float4x4> worldToViews;
  20. public float2 tileScale;
  21. public float2 tileScaleInv;
  22. public Fixed2<float> viewPlaneBottoms;
  23. public Fixed2<float> viewPlaneTops;
  24. public Fixed2<float4> viewToViewportScaleBiases;
  25. public int2 tileCount;
  26. public float near;
  27. public bool isOrthographic;
  28. InclusiveRange m_TileYRange;
  29. int m_Offset;
  30. int m_ViewIndex;
  31. float2 m_CenterOffset;
  32. public void Execute(int jobIndex)
  33. {
  34. var index = jobIndex % itemsPerTile;
  35. m_ViewIndex = jobIndex / itemsPerTile;
  36. m_Offset = jobIndex * rangesPerItem;
  37. m_TileYRange = new InclusiveRange(short.MaxValue, short.MinValue);
  38. for (var i = 0; i < rangesPerItem; i++)
  39. {
  40. tileRanges[m_Offset + i] = new InclusiveRange(short.MaxValue, short.MinValue);
  41. }
  42. if (index < lights.Length)
  43. {
  44. if (isOrthographic) { TileLightOrthographic(index); }
  45. else { TileLight(index); }
  46. }
  47. else { TileReflectionProbe(index); }
  48. }
  49. void TileLight(int lightIndex)
  50. {
  51. var light = lights[lightIndex];
  52. if (light.lightType != LightType.Point && light.lightType != LightType.Spot)
  53. {
  54. return;
  55. }
  56. var lightToWorld = (float4x4)light.localToWorldMatrix;
  57. var lightPositionVS = math.mul(worldToViews[m_ViewIndex], math.float4(lightToWorld.c3.xyz, 1)).xyz;
  58. lightPositionVS.z *= -1;
  59. if (lightPositionVS.z >= near) ExpandY(lightPositionVS);
  60. var lightDirectionVS = math.normalize(math.mul(worldToViews[m_ViewIndex], math.float4(lightToWorld.c2.xyz, 0)).xyz);
  61. lightDirectionVS.z *= -1;
  62. var halfAngle = math.radians(light.spotAngle * 0.5f);
  63. var range = light.range;
  64. var rangesq = square(range);
  65. var cosHalfAngle = math.cos(halfAngle);
  66. var coneHeight = cosHalfAngle * range;
  67. // Radius of circle formed by intersection of sphere and near plane.
  68. // Found using Pythagoras with a right triangle formed by three points:
  69. // (a) light position
  70. // (b) light position projected to near plane
  71. // (c) a point on the near plane at a distance `range` from the light position
  72. // (i.e. lies both on the sphere and the near plane)
  73. // Thus the hypotenuse is formed by (a) and (c) with length `range`, and the known side is formed
  74. // by (a) and (b) with length equal to the distance between the near plane and the light position.
  75. // The remaining unknown side is formed by (b) and (c) with length equal to the radius of the circle.
  76. // m_ClipCircleRadius = sqrt(sq(light.range) - sq(m_Near - m_LightPosition.z));
  77. var sphereClipRadius = math.sqrt(rangesq - square(near - lightPositionVS.z));
  78. // Assumes a point on the sphere, i.e. at distance `range` from the light position.
  79. // If spot light, we check the angle between the direction vector from the light position and the light direction vector.
  80. // Note that division by range is to normalize the vector, as we know that the resulting vector will have length `range`.
  81. bool SpherePointIsValid(float3 p) => light.lightType == LightType.Point ||
  82. math.dot(math.normalize(p - lightPositionVS), lightDirectionVS) >= cosHalfAngle;
  83. // Project light sphere onto YZ plane, find the horizon points, and re-construct view space position of found points.
  84. // CalculateSphereYBounds(lightPositionVS, range, near, sphereClipRadius, out var sphereBoundY0, out var sphereBoundY1);
  85. GetSphereHorizon(lightPositionVS.yz, range, near, sphereClipRadius, out var sphereBoundYZ0, out var sphereBoundYZ1);
  86. var sphereBoundY0 = math.float3(lightPositionVS.x, sphereBoundYZ0);
  87. var sphereBoundY1 = math.float3(lightPositionVS.x, sphereBoundYZ1);
  88. if (SpherePointIsValid(sphereBoundY0)) ExpandY(sphereBoundY0);
  89. if (SpherePointIsValid(sphereBoundY1)) ExpandY(sphereBoundY1);
  90. // Project light sphere onto XZ plane, find the horizon points, and re-construct view space position of found points.
  91. GetSphereHorizon(lightPositionVS.xz, range, near, sphereClipRadius, out var sphereBoundXZ0, out var sphereBoundXZ1);
  92. var sphereBoundX0 = math.float3(sphereBoundXZ0.x, lightPositionVS.y, sphereBoundXZ0.y);
  93. var sphereBoundX1 = math.float3(sphereBoundXZ1.x, lightPositionVS.y, sphereBoundXZ1.y);
  94. if (SpherePointIsValid(sphereBoundX0)) ExpandY(sphereBoundX0);
  95. if (SpherePointIsValid(sphereBoundX1)) ExpandY(sphereBoundX1);
  96. if (light.lightType == LightType.Spot)
  97. {
  98. // Cone base
  99. var baseRadius = math.sqrt(range * range - coneHeight * coneHeight);
  100. var baseCenter = lightPositionVS + lightDirectionVS * coneHeight;
  101. // Project cone base (a circle) into the YZ plane, find the horizon points, and re-construct view space position of found points.
  102. // When projecting a circle to a plane, it becomes an ellipse where the major axis is parallel to the line
  103. // of intersection of the projection plane and the circle plane. We can get this by taking the cross product
  104. // of the two plane normals, as the line of intersection will have to be a vector in both planes, and thus
  105. // orthogonal to both normals.
  106. // If the two plane normals are parallel, the cross product would return 0. In that case, the circle will
  107. // project to a line segment, so we pick a vector in the plane pointing in the direction we're interested
  108. // in finding horizon points in.
  109. var baseUY = math.abs(math.abs(lightDirectionVS.x) - 1) < 1e-6f ? math.float3(0, 1, 0) : math.normalize(math.cross(lightDirectionVS, math.float3(1, 0, 0)));
  110. var baseVY = math.cross(lightDirectionVS, baseUY);
  111. GetProjectedCircleHorizon(baseCenter.yz, baseRadius, baseUY.yz, baseVY.yz, out var baseY1UV, out var baseY2UV);
  112. var baseY1 = baseCenter + baseY1UV.x * baseUY + baseY1UV.y * baseVY;
  113. var baseY2 = baseCenter + baseY2UV.x * baseUY + baseY2UV.y * baseVY;
  114. if (baseY1.z >= near) ExpandY(baseY1);
  115. if (baseY2.z >= near) ExpandY(baseY2);
  116. // Project cone base into the XZ plane, find the horizon points, and re-construct view space position of found points.
  117. // See comment for YZ plane for details.
  118. var baseUX = math.abs(math.abs(lightDirectionVS.y) - 1) < 1e-6f ? math.float3(1, 0, 0) : math.normalize(math.cross(lightDirectionVS, math.float3(0, 1, 0)));
  119. var baseVX = math.cross(lightDirectionVS, baseUX);
  120. GetProjectedCircleHorizon(baseCenter.xz, baseRadius, baseUX.xz, baseVX.xz, out var baseX1UV, out var baseX2UV);
  121. var baseX1 = baseCenter + baseX1UV.x * baseUX + baseX1UV.y * baseVX;
  122. var baseX2 = baseCenter + baseX2UV.x * baseUX + baseX2UV.y * baseVX;
  123. if (baseX1.z >= near) ExpandY(baseX1);
  124. if (baseX2.z >= near) ExpandY(baseX2);
  125. // Handle base circle clipping by intersecting it with the near-plane if needed.
  126. if (GetCircleClipPoints(baseCenter, lightDirectionVS, baseRadius, near, out var baseClip0, out var baseClip1))
  127. {
  128. ExpandY(baseClip0);
  129. ExpandY(baseClip1);
  130. }
  131. bool ConicPointIsValid(float3 p) =>
  132. math.dot(math.normalize(p - lightPositionVS), lightDirectionVS) >= 0 &&
  133. math.dot(p - lightPositionVS, lightDirectionVS) <= coneHeight;
  134. // Calculate Z bounds of cone and check if it's overlapping with the near plane.
  135. // From https://www.iquilezles.org/www/articles/diskbbox/diskbbox.htm
  136. var baseExtentZ = baseRadius * math.sqrt(1.0f - square(lightDirectionVS.z));
  137. var coneIsClipping = near >= math.min(baseCenter.z - baseExtentZ, lightPositionVS.z) && near <= math.max(baseCenter.z + baseExtentZ, lightPositionVS.z);
  138. var coneU = math.cross(lightDirectionVS, lightPositionVS);
  139. // The cross product will be the 0-vector if the light-direction and camera-to-light-position vectors are parallel.
  140. // In that case, {1, 0, 0} is orthogonal to the light direction and we use that instead.
  141. coneU = math.csum(coneU) != 0f ? math.normalize(coneU) : math.float3(1, 0, 0);
  142. var coneV = math.cross(lightDirectionVS, coneU);
  143. if (coneIsClipping)
  144. {
  145. var r = baseRadius / coneHeight;
  146. // Find the Y bounds of the near-plane cone intersection, i.e. where y' = 0
  147. var thetaY = FindNearConicTangentTheta(lightPositionVS.yz, lightDirectionVS.yz, r, coneU.yz, coneV.yz);
  148. var p0Y = EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, thetaY.x);
  149. var p1Y = EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, thetaY.y);
  150. if (ConicPointIsValid(p0Y)) ExpandY(p0Y);
  151. if (ConicPointIsValid(p1Y)) ExpandY(p1Y);
  152. // Find the X bounds of the near-plane cone intersection, i.e. where x' = 0
  153. var thetaX = FindNearConicTangentTheta(lightPositionVS.xz, lightDirectionVS.xz, r, coneU.xz, coneV.xz);
  154. var p0X = EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, thetaX.x);
  155. var p1X = EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, thetaX.y);
  156. if (ConicPointIsValid(p0X)) ExpandY(p0X);
  157. if (ConicPointIsValid(p1X)) ExpandY(p1X);
  158. }
  159. // Calculate the lines making up the sides of the cone as seen from the camera. `l1` and `l2` form lines
  160. // from the light position.
  161. GetConeSideTangentPoints(lightPositionVS, lightDirectionVS, cosHalfAngle, baseRadius, coneHeight, range, coneU, coneV, out var l1, out var l2);
  162. {
  163. var planeNormal = math.float3(0, 1, viewPlaneBottoms[m_ViewIndex]);
  164. var l1t = math.dot(-lightPositionVS, planeNormal) / math.dot(l1, planeNormal);
  165. var l1x = lightPositionVS + l1 * l1t;
  166. if (l1t >= 0 && l1t <= 1 && l1x.z >= near) ExpandY(l1x);
  167. }
  168. {
  169. var planeNormal = math.float3(0, 1, viewPlaneTops[m_ViewIndex]);
  170. var l1t = math.dot(-lightPositionVS, planeNormal) / math.dot(l1, planeNormal);
  171. var l1x = lightPositionVS + l1 * l1t;
  172. if (l1t >= 0 && l1t <= 1 && l1x.z >= near) ExpandY(l1x);
  173. }
  174. m_TileYRange.Clamp(0, (short)(tileCount.y - 1));
  175. // Calculate tile plane ranges for cone.
  176. for (var planeIndex = m_TileYRange.start + 1; planeIndex <= m_TileYRange.end; planeIndex++)
  177. {
  178. var planeRange = InclusiveRange.empty;
  179. // Y-position on the view plane (Z=1)
  180. var planeY = math.lerp(viewPlaneBottoms[m_ViewIndex], viewPlaneTops[m_ViewIndex], planeIndex * tileScaleInv.y);
  181. var planeNormal = math.float3(0, 1, -planeY);
  182. // Intersect lines with y-plane and clip if needed.
  183. var l1t = math.dot(-lightPositionVS, planeNormal) / math.dot(l1, planeNormal);
  184. var l1x = lightPositionVS + l1 * l1t;
  185. if (l1t >= 0 && l1t <= 1 && l1x.z >= near) planeRange.Expand((short)math.clamp(ViewToTileSpace(l1x).x, 0, tileCount.x - 1));
  186. var l2t = math.dot(-lightPositionVS, planeNormal) / math.dot(l2, planeNormal);
  187. var l2x = lightPositionVS + l2 * l2t;
  188. if (l2t >= 0 && l2t <= 1 && l2x.z >= near) planeRange.Expand((short)math.clamp(ViewToTileSpace(l2x).x, 0, tileCount.x - 1));
  189. if (IntersectCircleYPlane(planeY, baseCenter, lightDirectionVS, baseUY, baseVY, baseRadius, out var circleTile0, out var circleTile1))
  190. {
  191. if (circleTile0.z >= near) planeRange.Expand((short)math.clamp(ViewToTileSpace(circleTile0).x, 0, tileCount.x - 1));
  192. if (circleTile1.z >= near) planeRange.Expand((short)math.clamp(ViewToTileSpace(circleTile1).x, 0, tileCount.x - 1));
  193. }
  194. if (coneIsClipping)
  195. {
  196. var y = planeY * near;
  197. var r = baseRadius / coneHeight;
  198. var theta = FindNearConicYTheta(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, y);
  199. var p0 = math.float3(EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, theta.x).x, y, near);
  200. var p1 = math.float3(EvaluateNearConic(near, lightPositionVS, lightDirectionVS, r, coneU, coneV, theta.y).x, y, near);
  201. if (ConicPointIsValid(p0)) planeRange.Expand((short)math.clamp(ViewToTileSpace(p0).x, 0, tileCount.x - 1));
  202. if (ConicPointIsValid(p1)) planeRange.Expand((short)math.clamp(ViewToTileSpace(p1).x, 0, tileCount.x - 1));
  203. }
  204. // Write to tile ranges above and below the plane. Note that at `m_Offset` we store Y-range.
  205. var tileIndex = m_Offset + 1 + planeIndex;
  206. tileRanges[tileIndex] = InclusiveRange.Merge(tileRanges[tileIndex], planeRange);
  207. tileRanges[tileIndex - 1] = InclusiveRange.Merge(tileRanges[tileIndex - 1], planeRange);
  208. }
  209. }
  210. m_TileYRange.Clamp(0, (short)(tileCount.y - 1));
  211. // Calculate tile plane ranges for sphere.
  212. for (var planeIndex = m_TileYRange.start + 1; planeIndex <= m_TileYRange.end; planeIndex++)
  213. {
  214. var planeRange = InclusiveRange.empty;
  215. var planeY = math.lerp(viewPlaneBottoms[m_ViewIndex], viewPlaneTops[m_ViewIndex], planeIndex * tileScaleInv.y);
  216. GetSphereYPlaneHorizon(lightPositionVS, range, near, sphereClipRadius, planeY, out var sphereTile0, out var sphereTile1);
  217. if (SpherePointIsValid(sphereTile0)) planeRange.Expand((short)math.clamp(ViewToTileSpace(sphereTile0).x, 0, tileCount.x - 1));
  218. if (SpherePointIsValid(sphereTile1)) planeRange.Expand((short)math.clamp(ViewToTileSpace(sphereTile1).x, 0, tileCount.x - 1));
  219. var tileIndex = m_Offset + 1 + planeIndex;
  220. tileRanges[tileIndex] = InclusiveRange.Merge(tileRanges[tileIndex], planeRange);
  221. tileRanges[tileIndex - 1] = InclusiveRange.Merge(tileRanges[tileIndex - 1], planeRange);
  222. }
  223. tileRanges[m_Offset] = m_TileYRange;
  224. }
  225. void TileLightOrthographic(int lightIndex)
  226. {
  227. var light = lights[lightIndex];
  228. var lightToWorld = (float4x4)light.localToWorldMatrix;
  229. var lightPosVS = math.mul(worldToViews[m_ViewIndex], math.float4(lightToWorld.c3.xyz, 1)).xyz;
  230. lightPosVS.z *= -1;
  231. ExpandOrthographic(lightPosVS);
  232. var lightDirVS = math.mul(worldToViews[m_ViewIndex], math.float4(lightToWorld.c2.xyz, 0)).xyz;
  233. lightDirVS.z *= -1;
  234. lightDirVS = math.normalize(lightDirVS);
  235. var halfAngle = math.radians(light.spotAngle * 0.5f);
  236. var range = light.range;
  237. var rangeSq = square(range);
  238. var cosHalfAngle = math.cos(halfAngle);
  239. var coneHeight = cosHalfAngle * range;
  240. var coneHeightSq = square(coneHeight);
  241. var coneHeightInv = 1f / coneHeight;
  242. var coneHeightInvSq = square(coneHeightInv);
  243. bool SpherePointIsValid(float3 p) => light.lightType == LightType.Point ||
  244. math.dot(math.normalize(p - lightPosVS), lightDirVS) >= cosHalfAngle;
  245. var sphereBoundY0 = lightPosVS - math.float3(0, range, 0);
  246. var sphereBoundY1 = lightPosVS + math.float3(0, range, 0);
  247. var sphereBoundX0 = lightPosVS - math.float3(range, 0, 0);
  248. var sphereBoundX1 = lightPosVS + math.float3(range, 0, 0);
  249. if (SpherePointIsValid(sphereBoundY0)) ExpandOrthographic(sphereBoundY0);
  250. if (SpherePointIsValid(sphereBoundY1)) ExpandOrthographic(sphereBoundY1);
  251. if (SpherePointIsValid(sphereBoundX0)) ExpandOrthographic(sphereBoundX0);
  252. if (SpherePointIsValid(sphereBoundX1)) ExpandOrthographic(sphereBoundX1);
  253. var circleCenter = lightPosVS + lightDirVS * coneHeight;
  254. var circleRadius = math.sqrt(rangeSq - coneHeightSq);
  255. var circleRadiusSq = square(circleRadius);
  256. var circleUp = math.normalize(math.float3(0, 1, 0) - lightDirVS * lightDirVS.y);
  257. var circleRight = math.normalize(math.float3(1, 0, 0) - lightDirVS * lightDirVS.x);
  258. var circleBoundY0 = circleCenter - circleUp * circleRadius;
  259. var circleBoundY1 = circleCenter + circleUp * circleRadius;
  260. if (light.lightType == LightType.Spot)
  261. {
  262. var circleBoundX0 = circleCenter - circleRight * circleRadius;
  263. var circleBoundX1 = circleCenter + circleRight * circleRadius;
  264. ExpandOrthographic(circleBoundY0);
  265. ExpandOrthographic(circleBoundY1);
  266. ExpandOrthographic(circleBoundX0);
  267. ExpandOrthographic(circleBoundX1);
  268. }
  269. m_TileYRange.Clamp(0, (short)(tileCount.y - 1));
  270. // Find two lines in screen-space for the cone if the light is a spot.
  271. float coneDir0X = 0, coneDir0YInv = 0, coneDir1X = 0, coneDir1YInv = 0;
  272. if (light.lightType == LightType.Spot)
  273. {
  274. // Distance from light position to and radius of sphere fitted to the end of the cone.
  275. var sphereDistance = coneHeight + circleRadiusSq * coneHeightInv;
  276. var sphereRadius = math.sqrt(square(circleRadiusSq) * coneHeightInvSq + circleRadiusSq);
  277. var directionXYSqInv = math.rcp(math.lengthsq(lightDirVS.xy));
  278. var polarIntersection = -circleRadiusSq * coneHeightInv * directionXYSqInv * lightDirVS.xy;
  279. var polarDir = math.sqrt((square(sphereRadius) - math.lengthsq(polarIntersection)) * directionXYSqInv) * math.float2(lightDirVS.y, -lightDirVS.x);
  280. var conePBase = lightPosVS.xy + sphereDistance * lightDirVS.xy + polarIntersection;
  281. var coneP0 = conePBase - polarDir;
  282. var coneP1 = conePBase + polarDir;
  283. coneDir0X = coneP0.x - lightPosVS.x;
  284. coneDir0YInv = math.rcp(coneP0.y - lightPosVS.y);
  285. coneDir1X = coneP1.x - lightPosVS.x;
  286. coneDir1YInv = math.rcp(coneP1.y - lightPosVS.y);
  287. }
  288. // Tile plane ranges
  289. for (var planeIndex = m_TileYRange.start + 1; planeIndex <= m_TileYRange.end; planeIndex++)
  290. {
  291. var planeRange = InclusiveRange.empty;
  292. // Sphere
  293. var planeY = math.lerp(viewPlaneBottoms[m_ViewIndex], viewPlaneTops[m_ViewIndex], planeIndex * tileScaleInv.y);
  294. var sphereX = math.sqrt(rangeSq - square(planeY - lightPosVS.y));
  295. var sphereX0 = math.float3(lightPosVS.x - sphereX, planeY, lightPosVS.z);
  296. var sphereX1 = math.float3(lightPosVS.x + sphereX, planeY, lightPosVS.z);
  297. if (SpherePointIsValid(sphereX0)) { ExpandRangeOrthographic(ref planeRange, sphereX0.x); }
  298. if (SpherePointIsValid(sphereX1)) { ExpandRangeOrthographic(ref planeRange, sphereX1.x); }
  299. if (light.lightType == LightType.Spot)
  300. {
  301. // Circle
  302. if (planeY >= circleBoundY0.y && planeY <= circleBoundY1.y)
  303. {
  304. var intersectionDistance = (planeY - circleCenter.y) / circleUp.y;
  305. var closestPointX = circleCenter.x + intersectionDistance * circleUp.x;
  306. var intersectionDirX = -lightDirVS.z / math.length(math.float3(-lightDirVS.z, 0, lightDirVS.x));
  307. var sideDistance = math.sqrt(square(circleRadius) - square(intersectionDistance));
  308. var circleX0 = closestPointX - sideDistance * intersectionDirX;
  309. var circleX1 = closestPointX + sideDistance * intersectionDirX;
  310. ExpandRangeOrthographic(ref planeRange, circleX0);
  311. ExpandRangeOrthographic(ref planeRange, circleX1);
  312. }
  313. // Cone
  314. var deltaY = planeY - lightPosVS.y;
  315. var coneT0 = deltaY * coneDir0YInv;
  316. var coneT1 = deltaY * coneDir1YInv;
  317. if (coneT0 >= 0 && coneT0 <= 1) { ExpandRangeOrthographic(ref planeRange, lightPosVS.x + coneT0 * coneDir0X); }
  318. if (coneT1 >= 0 && coneT1 <= 1) { ExpandRangeOrthographic(ref planeRange, lightPosVS.x + coneT1 * coneDir1X); }
  319. }
  320. var tileIndex = m_Offset + 1 + planeIndex;
  321. tileRanges[tileIndex] = InclusiveRange.Merge(tileRanges[tileIndex], planeRange);
  322. tileRanges[tileIndex - 1] = InclusiveRange.Merge(tileRanges[tileIndex - 1], planeRange);
  323. }
  324. tileRanges[m_Offset] = m_TileYRange;
  325. }
  326. static readonly float3[] k_CubePoints =
  327. {
  328. new(-1, -1, -1),
  329. new(-1, -1, +1),
  330. new(-1, +1, -1),
  331. new(-1, +1, +1),
  332. new(+1, -1, -1),
  333. new(+1, -1, +1),
  334. new(+1, +1, -1),
  335. new(+1, +1, +1),
  336. };
  337. // Each item represents 3 lines, with x being the start index and yzw the end indices.
  338. static readonly int4[] k_CubeLineIndices =
  339. {
  340. // (-1, -1, -1) -> {(+1, -1, -1), (-1, +1, -1), (-1, -1, +1)}
  341. new(0, 4, 2, 1),
  342. // (-1, +1, +1) -> {(+1, +1, +1), (-1, -1, +1), (-1, +1, -1)}
  343. new(3, 7, 1, 2),
  344. // (+1, -1, +1) -> {(-1, -1, +1), (+1, +1, +1), (+1, -1, -1)}
  345. new(5, 1, 7, 4),
  346. // (+1, +1, -1) -> {(-1, +1, -1), (+1, -1, -1), (+1, +1, +1)}
  347. new(6, 2, 4, 7),
  348. };
  349. void TileReflectionProbe(int index)
  350. {
  351. // The algorithm used here works by clipping all the lines of the cube against the near-plane, and then
  352. // projects the resulting points to the view plane. These points are then used to construct a 2D convex
  353. // hull, which we can iterate linearly to get the lines on screen making up the cube.
  354. var reflectionProbe = reflectionProbes[index - lights.Length];
  355. var centerWS = (float3)reflectionProbe.bounds.center;
  356. var extentsWS = (float3)reflectionProbe.bounds.extents;
  357. // The vertices of the cube in view space.
  358. var points = new NativeArray<float3>(k_CubePoints.Length, Allocator.Temp);
  359. // This is initially filled with just the cube vertices that lie in front of the near plane.
  360. var clippedPoints = new NativeArray<float2>(k_CubePoints.Length + k_CubeLineIndices.Length * 3, Allocator.Temp);
  361. var clippedPointsCount = 0;
  362. var leftmostIndex = 0;
  363. for (var i = 0; i < k_CubePoints.Length; i++)
  364. {
  365. var point = math.mul(worldToViews[m_ViewIndex], math.float4(centerWS + extentsWS * k_CubePoints[i], 1)).xyz;
  366. point.z *= -1;
  367. points[i] = point;
  368. if (point.z >= near)
  369. {
  370. var clippedPoint = isOrthographic ? point.xy : point.xy/point.z;
  371. var clippedIndex = clippedPointsCount++;
  372. clippedPoints[clippedIndex] = clippedPoint;
  373. if (clippedPoint.x < clippedPoints[leftmostIndex].x) leftmostIndex = clippedIndex;
  374. }
  375. }
  376. // Clip the cube's line segments with the near plane, and add the new vertices to clippedPoints. Only lines
  377. // that are clipped will generate new vertices.
  378. for (var i = 0; i < k_CubeLineIndices.Length; i++)
  379. {
  380. var indices = k_CubeLineIndices[i];
  381. var p0 = points[indices.x];
  382. for (var j = 0; j < 3; j++)
  383. {
  384. var p1 = points[indices[j+1]];
  385. // The entire line is in front of the near plane.
  386. if (p0.z < near && p1.z < near) continue;
  387. // Check whether the line needs clipping.
  388. if (p0.z < near || p1.z < near)
  389. {
  390. var d = (near - p0.z) / (p1.z - p0.z);
  391. var p = math.lerp(p0, p1, d);
  392. var clippedPoint = isOrthographic ? p.xy : p.xy/p.z;
  393. var clippedIndex = clippedPointsCount++;
  394. clippedPoints[clippedIndex] = clippedPoint;
  395. if (clippedPoint.x < clippedPoints[leftmostIndex].x) leftmostIndex = clippedIndex;
  396. }
  397. }
  398. }
  399. // Construct the convex hull. It is formed by the line loop consisting of the points in the array.
  400. var hullPoints = new NativeArray<float2>(clippedPointsCount, Allocator.Temp);
  401. var hullPointsCount = 0;
  402. if (clippedPointsCount > 0)
  403. {
  404. // Start with the leftmost point, as that is guaranteed to be on the hull.
  405. var hullPointIndex = leftmostIndex;
  406. // Find the remaining hull points until we end up back at the leftmost point.
  407. do
  408. {
  409. var hullPoint = clippedPoints[hullPointIndex];
  410. ExpandY(math.float3(hullPoint, 1));
  411. hullPoints[hullPointsCount++] = hullPoint;
  412. // Find the endpoint resulting in the leftmost turning line. This line will be a part of the hull.
  413. var endpointIndex = 0;
  414. var endpointLine = clippedPoints[endpointIndex] - hullPoint;
  415. for (var i = 0; i < clippedPointsCount; i++)
  416. {
  417. var candidateLine = clippedPoints[i] - hullPoint;
  418. var det = math.determinant(math.float2x2(endpointLine, candidateLine));
  419. // Check if point i lies on the left side of the line to the current endpoint, or if it lies
  420. // collinear to the current endpoint but farther away.
  421. if (endpointIndex == hullPointIndex || det > 0 || (det == 0.0f && math.lengthsq(candidateLine) > math.lengthsq(endpointLine)))
  422. {
  423. endpointIndex = i;
  424. endpointLine = candidateLine;
  425. }
  426. }
  427. hullPointIndex = endpointIndex;
  428. } while (hullPointIndex != leftmostIndex && hullPointsCount < clippedPointsCount);
  429. m_TileYRange.Clamp(0, (short)(tileCount.y - 1));
  430. // Calculate tile plane ranges for sphere.
  431. for (var planeIndex = m_TileYRange.start + 1; planeIndex <= m_TileYRange.end; planeIndex++)
  432. {
  433. var planeRange = InclusiveRange.empty;
  434. var planeY = math.lerp(viewPlaneBottoms[m_ViewIndex], viewPlaneTops[m_ViewIndex], planeIndex * tileScaleInv.y);
  435. for (var i = 0; i < hullPointsCount; i++)
  436. {
  437. var hp0 = hullPoints[i];
  438. var hp1 = hullPoints[(i + 1) % hullPointsCount];
  439. // planeY = hp0 + t * (hp1 - hp0) => planeY - hp0 = t * (hp1 - hp0) => (planeY - hp0) / (hp1 - hp0) = t
  440. var t = (planeY - hp0.y) / (hp1.y - hp0.y);
  441. if (t < 0 || t > 1) continue;
  442. var x = math.lerp(hp0.x, hp1.x, t);
  443. var p = math.float3(x, planeY, 1);
  444. var pTS = isOrthographic ? ViewToTileSpaceOrthographic(p) : ViewToTileSpace(p);
  445. planeRange.Expand((short)math.clamp(pTS.x, 0, tileCount.x - 1));
  446. }
  447. var tileIndex = m_Offset + 1 + planeIndex;
  448. tileRanges[tileIndex] = InclusiveRange.Merge(tileRanges[tileIndex], planeRange);
  449. tileRanges[tileIndex - 1] = InclusiveRange.Merge(tileRanges[tileIndex - 1], planeRange);
  450. }
  451. tileRanges[m_Offset] = m_TileYRange;
  452. }
  453. hullPoints.Dispose();
  454. clippedPoints.Dispose();
  455. points.Dispose();
  456. }
  457. /// <summary>
  458. /// Project onto Z=1, scale and offset into [0, tileCount]
  459. /// </summary>
  460. float2 ViewToTileSpace(float3 positionVS)
  461. {
  462. return (positionVS.xy / positionVS.z * viewToViewportScaleBiases[m_ViewIndex].xy + viewToViewportScaleBiases[m_ViewIndex].zw) * tileScale;
  463. }
  464. /// <summary>
  465. /// Project onto Z=1, scale and offset into [0, tileCount]
  466. /// </summary>
  467. float2 ViewToTileSpaceOrthographic(float3 positionVS)
  468. {
  469. return (positionVS.xy * viewToViewportScaleBiases[m_ViewIndex].xy + viewToViewportScaleBiases[m_ViewIndex].zw) * tileScale;
  470. }
  471. /// <summary>
  472. /// Expands the tile Y range and the X range in the row containing the position.
  473. /// </summary>
  474. void ExpandY(float3 positionVS)
  475. {
  476. // var positionTS = math.clamp(ViewToTileSpace(positionVS), 0, tileCount - 1);
  477. var positionTS = ViewToTileSpace(positionVS);
  478. var tileY = (int)positionTS.y;
  479. var tileX = (int)positionTS.x;
  480. m_TileYRange.Expand((short)math.clamp(tileY, 0, tileCount.y - 1));
  481. if (tileY >= 0 && tileY < tileCount.y && tileX >= 0 && tileX < tileCount.x)
  482. {
  483. var rowXRange = tileRanges[m_Offset + 1 + tileY];
  484. rowXRange.Expand((short)tileX);
  485. tileRanges[m_Offset + 1 + tileY] = rowXRange;
  486. }
  487. }
  488. /// <summary>
  489. /// Expands the tile Y range and the X range in the row containing the position.
  490. /// </summary>
  491. void ExpandOrthographic(float3 positionVS)
  492. {
  493. // var positionTS = math.clamp(ViewToTileSpace(positionVS), 0, tileCount - 1);
  494. var positionTS = ViewToTileSpaceOrthographic(positionVS);
  495. var tileY = (int)positionTS.y;
  496. var tileX = (int)positionTS.x;
  497. m_TileYRange.Expand((short)math.clamp(tileY, 0, tileCount.y - 1));
  498. if (tileY >= 0 && tileY < tileCount.y && tileX >= 0 && tileX < tileCount.x)
  499. {
  500. var rowXRange = tileRanges[m_Offset + 1 + tileY];
  501. rowXRange.Expand((short)tileX);
  502. tileRanges[m_Offset + 1 + tileY] = rowXRange;
  503. }
  504. }
  505. void ExpandRangeOrthographic(ref InclusiveRange range, float xVS)
  506. {
  507. range.Expand((short)math.clamp(ViewToTileSpaceOrthographic(xVS).x, 0, tileCount.x - 1));
  508. }
  509. static float square(float x) => x * x;
  510. /// <summary>
  511. /// Finds the two horizon points seen from (0, 0) of a sphere projected onto either XZ or YZ. Takes clipping into account.
  512. /// </summary>
  513. static void GetSphereHorizon(float2 center, float radius, float near, float clipRadius, out float2 p0, out float2 p1)
  514. {
  515. var direction = math.normalize(center);
  516. // Distance from camera to center of sphere
  517. var d = math.length(center);
  518. // Distance from camera to sphere horizon edge
  519. var l = math.sqrt(d * d - radius * radius);
  520. // Height of circle horizon
  521. var h = l * radius / d;
  522. // Center of circle horizon
  523. var c = direction * (l * h / radius);
  524. p0 = math.float2(float.MinValue, 1f);
  525. p1 = math.float2(float.MaxValue, 1f);
  526. // Handle clipping
  527. if (center.y - radius < near)
  528. {
  529. p0 = math.float2(center.x + clipRadius, near);
  530. p1 = math.float2(center.x - clipRadius, near);
  531. }
  532. // Circle horizon points
  533. var c0 = c + math.float2(-direction.y, direction.x) * h;
  534. if (square(d) >= square(radius) && c0.y >= near)
  535. {
  536. if (c0.x > p0.x) { p0 = c0; }
  537. if (c0.x < p1.x) { p1 = c0; }
  538. }
  539. var c1 = c + math.float2(direction.y, -direction.x) * h;
  540. if (square(d) >= square(radius) && c1.y >= near)
  541. {
  542. if (c1.x > p0.x) { p0 = c1; }
  543. if (c1.x < p1.x) { p1 = c1; }
  544. }
  545. }
  546. static void GetSphereYPlaneHorizon(float3 center, float sphereRadius, float near, float clipRadius, float y, out float3 left, out float3 right)
  547. {
  548. // Note: The y-plane is the plane that is determined by `y` in that it contains the vector (1, 0, 0)
  549. // and goes through the points (0, y, 1) and (0, 0, 0). This would become a straight line in screen-space, and so it
  550. // represents the boundary between two rows of tiles.
  551. // Near-plane clipping - will get overwritten if no clipping is needed.
  552. // `y` is given for the view plane (Z=1), scale it so that it is on the near plane instead.
  553. var yNear = y * near;
  554. // Find the two points of intersection between the clip circle of the sphere and the y-plane.
  555. // Found using Pythagoras with a right triangle formed by three points:
  556. // (a) center of the clip circle
  557. // (b) a point straight above the clip circle center on the y-plane
  558. // (c) a point that is both on the circle and the y-plane (this is the point we want to find in the end)
  559. // The hypotenuse is formed by (a) and (c) with length equal to the clip radius. The known side is
  560. // formed by (a) and (b) and is simply the distance from the center to the y-plane along the y-axis.
  561. // The remaining side gives us the x-displacement needed to find the intersection points.
  562. var clipHalfWidth = math.sqrt(square(clipRadius) - square(yNear - center.y));
  563. left = math.float3(center.x - clipHalfWidth, yNear, near);
  564. right = math.float3(center.x + clipHalfWidth, yNear, near);
  565. // Basis vectors in the y-plane for being able to parameterize the plane.
  566. var planeU = math.normalize(math.float3(0, y, 1));
  567. var planeV = math.float3(1, 0, 0);
  568. // Calculate the normal of the y-plane. Found from: (0, y, 1) × (1, 0, 0) = (0, 1, -y)
  569. // This is used to represent the plane along with the origin, which is just 0 and thus doesn't show up
  570. // in the calculations.
  571. var normal = math.normalize(math.float3(0, 1, -y));
  572. // We want to first find the circle from the intersection of the y-plane and the sphere.
  573. // The shortest distance from the sphere center and the y-plane. The sign determines which side of the plane
  574. // the center is on.
  575. var signedDistance = math.dot(normal, center);
  576. // Unsigned shortest distance from the sphere center to the plane.
  577. var distanceToPlane = math.abs(signedDistance);
  578. // The center of the intersection circle in the y-plane, which is the point on the plane closest to the
  579. // sphere center. I.e. this is at `distanceToPlane` from the center.
  580. var centerOnPlane = math.float2(math.dot(center, planeU), math.dot(center, planeV));
  581. // Distance from origin to the circle center.
  582. var distanceInPlane = math.length(centerOnPlane);
  583. // Direction from origin to the circle center.
  584. var directionPS = centerOnPlane / distanceInPlane;
  585. // Calculate the radius of the circle using Pythagoras. We know that any point on the circle is a point on
  586. // the sphere. Thus we can construct a triangle with the sphere center, circle center, and a point on the
  587. // circle. We then want to find its distance to the circle center, as that will be equal to the radius. As
  588. // the point is on the sphere, it must be `sphereRadius` from the sphere center, forming the hypotenuse. The
  589. // other side is between the sphere and circle centers, which we've already calculated to be
  590. // `distanceToPlane`.
  591. var circleRadius = math.sqrt(square(sphereRadius) - square(distanceToPlane));
  592. // Now that we have the circle, we can find the horizon points. Since we've parametrized the plane, we can
  593. // just do this in 2D.
  594. // Any of these conditions will yield NaN due to negative square roots. They are signs that clipping is needed,
  595. // so we fallback on the already calculated values in that case.
  596. if (square(distanceToPlane) <= square(sphereRadius) && square(circleRadius) <= square(distanceInPlane))
  597. {
  598. // Distance from origin to circle horizon edge.
  599. var l = math.sqrt(square(distanceInPlane) - square(circleRadius));
  600. // Height of circle horizon.
  601. var h = l * circleRadius / distanceInPlane;
  602. // Center of circle horizon.
  603. var c = directionPS * (l * h / circleRadius);
  604. // Calculate the horizon points in the plane.
  605. var leftOnPlane = c + math.float2(directionPS.y, -directionPS.x) * h;
  606. var rightOnPlane = c + math.float2(-directionPS.y, directionPS.x) * h;
  607. // Transform horizon points to view space and use if not clipped.
  608. var leftCandidate = leftOnPlane.x * planeU + leftOnPlane.y * planeV;
  609. if (leftCandidate.z >= near) left = leftCandidate;
  610. var rightCandidate = rightOnPlane.x * planeU + rightOnPlane.y * planeV;
  611. if (rightCandidate.z >= near) right = rightCandidate;
  612. }
  613. }
  614. /// <summary>
  615. /// Finds the two points of intersection of a 3D circle and the near plane.
  616. /// </summary>
  617. static bool GetCircleClipPoints(float3 circleCenter, float3 circleNormal, float circleRadius, float near, out float3 p0, out float3 p1)
  618. {
  619. // The intersection of two planes is a line where the direction is the cross product of the two plane normals.
  620. // In this case, it is the plane containing the circle, and the near plane.
  621. var lineDirection = math.normalize(math.cross(circleNormal, math.float3(0, 0, 1)));
  622. // Find a direction on the circle plane towards the nearest point on the intersection line.
  623. // It has to be perpendicular to the circle normal to be in the circle plane. The direction to the closest
  624. // point on a line is perpendicular to the line direction. Thus this is given by the cross product of the
  625. // line direction and the circle normal, as this gives us a vector that is perpendicular to both of those.
  626. var nearestDirection = math.cross(lineDirection, circleNormal);
  627. // Distance from circle center to the intersection line along `nearestDirection`.
  628. // This is done using a ray-plane intersection, where the plane is the near plane.
  629. // ({0, 0, near} - circleCenter) . {0, 0, 1} / (nearestDirection . {0, 0, 1})
  630. var distance = (near - circleCenter.z) / nearestDirection.z;
  631. // The point on the line nearest to the circle center when traveling only in the circle plane.
  632. var nearestPoint = circleCenter + nearestDirection * distance;
  633. // Any line through a circle makes a chord where the endpoints are the intersections with the circle.
  634. // The half length of the circle chord can be found by constructing a right triangle from three points:
  635. // (a) The circle center.
  636. // (b) The nearest point.
  637. // (c) A point that is on circle and the intersection line.
  638. // The hypotenuse is formed by (a) and (c) and will have length `circleRadius` as it is on the circle.
  639. // The known side if formed by (a) and (b), which we have already calculated the distance of in `distance`.
  640. // The unknown side formed by (b) and (c) is then found using Pythagoras.
  641. var chordHalfLength = math.sqrt(square(circleRadius) - square(distance));
  642. p0 = nearestPoint + lineDirection * chordHalfLength;
  643. p1 = nearestPoint - lineDirection * chordHalfLength;
  644. return math.abs(distance) <= circleRadius;
  645. }
  646. static (float, float) IntersectEllipseLine(float a, float b, float3 line)
  647. {
  648. // The line is represented as a homogenous 2D line {u, v, w} such that ux + vy + w = 0.
  649. // The ellipse is represented by the implicit equation x^2/a^2 + y^2/b^2 = 1.
  650. // We solve the line equation for y: y = (ux + w) / v
  651. // We then substitute this into the ellipse equation and expand and re-arrange a bit:
  652. // x^2/a^2 + ((ux + w) / v)^2/b^2 = 1 =>
  653. // x^2/a^2 + ((ux + w)^2 / v^2)/b^2 = 1 =>
  654. // x^2/a^2 + (ux + w)^2/(v^2 b^2) = 1 =>
  655. // x^2/a^2 + (u^2 x^2 + w^2 + 2 u x w)/(v^2 b^2) = 1 =>
  656. // x^2/a^2 + x^2 u^2 / (v^2 b^2) + w^2/(v^2 b^2) + x 2 u w / (v^2 b^2) = 1 =>
  657. // x^2 (1/a^2 + u^2 / (v^2 b^2)) + x 2 u w / (v^2 b^2) + w^2 / (v^2 b^2) - 1 = 0
  658. // We now have a quadratic equation with:
  659. // a = 1/a^2 + u^2 / (v^2 b^2)
  660. // b = 2 u w / (v^2 b^2)
  661. // c = w^2 / (v^2 b^2) - 1
  662. var div = math.rcp(square(line.y) * square(b));
  663. var qa = 1f / square(a) + square(line.x) * div;
  664. var qb = 2f * line.x * line.z * div;
  665. var qc = square(line.z) * div - 1f;
  666. var sqrtD = math.sqrt(qb * qb - 4f * qa * qc);
  667. var x1 = (-qb + sqrtD) / (2f * qa);
  668. var x2 = (-qb - sqrtD) / (2f * qa);
  669. return (x1, x2);
  670. }
  671. /// <summary>
  672. /// Calculates the horizon of a circle orthogonally projected to a plane as seen from the origin on the plane.
  673. /// </summary>
  674. /// <param name="center">The center of the circle projected onto the plane.</param>
  675. /// <param name="radius">The radius of the circle.</param>
  676. /// <param name="U">The major axis of the ellipse formed by the projection of the circle.</param>
  677. /// <param name="V">The minor axis of the ellipse formed by the projection of the circle.</param>
  678. /// <param name="uv1">The first horizon point expressed as factors of <paramref name="U"/> and <paramref name="V"/>.</param>
  679. /// <param name="uv2">The second horizon point expressed as factors of <paramref name="U"/> and <paramref name="V"/>.</param>
  680. static void GetProjectedCircleHorizon(float2 center, float radius, float2 U, float2 V, out float2 uv1, out float2 uv2)
  681. {
  682. // U is assumed to be constructed such that it is never 0, but V can be if the circle projects to a line segment.
  683. // In that case, the solution can be trivially found using U only.
  684. var vl = math.length(V);
  685. if (vl < 1e-6f)
  686. {
  687. uv1 = math.float2(radius, 0);
  688. uv2 = math.float2(-radius, 0);
  689. }
  690. else
  691. {
  692. var ul = math.length(U);
  693. var ulinv = math.rcp(ul);
  694. var vlinv = math.rcp(vl);
  695. // Normalize U and V in the plane.
  696. var u = U * ulinv;
  697. var v = V * vlinv;
  698. // Major and minor axis of the ellipse.
  699. var a = ul * radius;
  700. var b = vl * radius;
  701. // Project the camera position into a 2D coordinate system with the circle at (0, 0) and
  702. // the ellipse major and minor axes as the coordinate system axes. This allows us to use the standard
  703. // form of the ellipse equation, greatly simplifying the calculations.
  704. var cameraUV = math.float2(math.dot(-center, u), math.dot(-center, v));
  705. // Find the polar line of the camera position in the normalized UV coordinate system.
  706. var polar = math.float3(cameraUV.x / square(a), cameraUV.y / square(b), -1);
  707. var (t1, t2) = IntersectEllipseLine(a, b, polar);
  708. // Find Y by putting polar into line equation and solving. Denormalize by dividing by U and V lengths.
  709. uv1 = math.float2(t1 * ulinv, (-polar.x / polar.y * t1 - polar.z / polar.y) * vlinv);
  710. uv2 = math.float2(t2 * ulinv, (-polar.x / polar.y * t2 - polar.z / polar.y) * vlinv);
  711. }
  712. }
  713. static bool IntersectCircleYPlane(
  714. float y, float3 circleCenter, float3 circleNormal, float3 circleU, float3 circleV, float circleRadius,
  715. out float3 p1, out float3 p2)
  716. {
  717. p1 = p2 = 0;
  718. // Intersecting a circle with a plane yields 2 points, or the whole circle if the plane and the plane of the
  719. // circle are the same, or nothing if the planes are parallel but offset. We're only interested in the first
  720. // case. Our other tests will catch the other cases.
  721. // The two points will be on the line of intersection of the two planes. Thus we first have to find that line.
  722. // Shoot 2 rays along the y-plane and intersect the circle plane. We then transform them into the circle
  723. // plane, so that we can work in 2D.
  724. var CdotN = math.dot(circleCenter, circleNormal);
  725. var h1v = math.float3(1, y, 1) * CdotN / math.dot(math.float3(1, y, 1), circleNormal) - circleCenter;
  726. var h1 = math.float2(math.dot(h1v, circleU), math.dot(h1v, circleV));
  727. var h2v = math.float3(-1, y, 1) * CdotN / math.dot(math.float3(-1, y, 1), circleNormal) - circleCenter;
  728. var h2 = math.float2(math.dot(h2v, circleU), math.dot(h2v, circleV));
  729. var lineDirection = math.normalize(h2 - h1);
  730. // We now have the direction of the line, and would like to find the point on it that is closest to the
  731. // circle center. A line in 2D is similar to a plane in 3D. So we can calculate a normal, which is just a
  732. // perpendicular/orthogonal direction, and then take the dot product to find the distance. This is similar
  733. // to when calculating the d-term for a plane in 3D, which is also just calculating the closest distance
  734. // from the origin to the plane.
  735. var lineNormal = math.float2(lineDirection.y, -lineDirection.x);
  736. var distToLine = math.dot(h1, lineNormal);
  737. // We can then get that point on the line by following our normal with the distance we just calculated.
  738. var lineCenter = lineNormal * distToLine;
  739. // Avoid negative square roots, as this means we've hit one of the cases that we do not care about.
  740. if (distToLine > circleRadius) return false;
  741. // What's left now is to intersect the line with the circle. We can do so with Pythagoras. Our triangle
  742. // is made up of `lineCenter`, the circle center and one of the intersection points.
  743. // We know the distance from `lineCenter` to the circle center (`distToLine`), and the distance from
  744. // the circle center to one of the intersection points must be the circle radius, as it lies on the
  745. // circle, forming the hypotenuse.
  746. var l = math.sqrt(circleRadius * circleRadius - distToLine * distToLine);
  747. // What we found above is the distance from `lineCenter` to each of the intersection points. So we just
  748. // scrub along the line in both directions using the found distance, and then transform back into view
  749. // space.
  750. var x1 = lineCenter + l * lineDirection;
  751. var x2 = lineCenter - l * lineDirection;
  752. p1 = circleCenter + x1.x * circleU + x1.y * circleV;
  753. p2 = circleCenter + x2.x * circleU + x2.y * circleV;
  754. return true;
  755. }
  756. static void GetConeSideTangentPoints(float3 vertex, float3 axis, float cosHalfAngle, float circleRadius, float coneHeight, float range, float3 circleU, float3 circleV, out float3 l1, out float3 l2)
  757. {
  758. l1 = l2 = 0;
  759. if (math.dot(math.normalize(-vertex), axis) >= cosHalfAngle)
  760. {
  761. return;
  762. }
  763. var d = -math.dot(vertex, axis);
  764. // If d is zero, this leads to a numerical instability in the code later on. This is why we make the value
  765. // an epsilon if it is zero.
  766. if (d == 0f) d = 1e-6f;
  767. var sign = d < 0 ? -1f : 1f;
  768. // sign *= vertex.z < 0 ? -1f : 1f;
  769. // `origin` is the center of the circular slice we're about to calculate at distance `d` from the `vertex`.
  770. var origin = vertex + axis * d;
  771. // Get the radius of the circular slice of the cone at the `origin`.
  772. var radius = math.abs(d) * circleRadius / coneHeight;
  773. // `circleU` and `circleV` are the two vectors perpendicular to the cone's axis. `cameraUV` is thus the
  774. // position of the camera projected onto the plane of the circular slice. This basically creates a new
  775. // 2D coordinate space, with (0, 0) located at the center of the circular slice, which why this variable
  776. // is called `origin`.
  777. var cameraUV = math.float2(math.dot(circleU, -origin), math.dot(circleV, -origin));
  778. // Use homogeneous coordinates to find the tangents.
  779. var polar = math.float3(cameraUV, -square(radius));
  780. var p1 = math.float2(-1, -polar.x / polar.y * (-1) - polar.z / polar.y);
  781. var p2 = math.float2(1, -polar.x / polar.y * 1 - polar.z / polar.y);
  782. var lineDirection = math.normalize(p2 - p1);
  783. var lineNormal = math.float2(lineDirection.y, -lineDirection.x);
  784. var distToLine = math.dot(p1, lineNormal);
  785. var lineCenter = lineNormal * distToLine;
  786. var l = math.sqrt(radius * radius - distToLine * distToLine);
  787. var x1UV = lineCenter + l * lineDirection;
  788. var x2UV = lineCenter - l * lineDirection;
  789. var dir1 = math.normalize((origin + x1UV.x * circleU + x1UV.y * circleV) - vertex) * sign;
  790. var dir2 = math.normalize((origin + x2UV.x * circleU + x2UV.y * circleV) - vertex) * sign;
  791. l1 = dir1 * range;
  792. l2 = dir2 * range;
  793. }
  794. static float3 EvaluateNearConic(float near, float3 o, float3 d, float r, float3 u, float3 v, float theta)
  795. {
  796. var h = (near - o.z) / (d.z + r * u.z * math.cos(theta) + r * v.z * math.sin(theta));
  797. return math.float3(o.xy + h * (d.xy + r * u.xy * math.cos(theta) + r * v.xy * math.sin(theta)), near);
  798. }
  799. // o, d, u and v are expected to contain {x or y, z}. I.e. pass in x values to find tangents where x' = 0
  800. // Returns the two theta values as a float2.
  801. static float2 FindNearConicTangentTheta(float2 o, float2 d, float r, float2 u, float2 v)
  802. {
  803. var sqrt = math.sqrt(square(d.x) * square(u.y) + square(d.x) * square(v.y) - 2f * d.x * d.y * u.x * u.y - 2f * d.x * d.y * v.x * v.y + square(d.y) * square(u.x) + square(d.y) * square(v.x) - square(r) * square(u.x) * square(v.y) + 2f * square(r) * u.x * u.y * v.x * v.y - square(r) * square(u.y) * square(v.x));
  804. var denom = d.x * v.y - d.y * v.x - r * u.x * v.y + r * u.y * v.x;
  805. return 2 * math.atan((-d.x * u.y + d.y * u.x + math.float2(1, -1) * sqrt) / denom);
  806. }
  807. static float2 FindNearConicYTheta(float near, float3 o, float3 d, float r, float3 u, float3 v, float y)
  808. {
  809. var sqrt = math.sqrt(-square(d.y) * square(o.z) + 2 * square(d.y) * o.z * near - square(d.y) * square(near) + 2 * d.y * d.z * o.y * o.z - 2 * d.y * d.z * o.y * near - 2 * d.y * d.z * o.z * y + 2 * d.y * d.z * y * near - square(d.z) * square(o.y) + 2 * square(d.z) * o.y * y - square(d.z) * square(y) + square(o.y) * square(r) * square(u.z) + square(o.y) * square(r) * square(v.z) - 2 * o.y * o.z * square(r) * u.y * u.z - 2 * o.y * o.z * square(r) * v.y * v.z - 2 * o.y * y * square(r) * square(u.z) - 2 * o.y * y * square(r) * square(v.z) + 2 * o.y * square(r) * u.y * u.z * near + 2 * o.y * square(r) * v.y * v.z * near + square(o.z) * square(r) * square(u.y) + square(o.z) * square(r) * square(v.y) + 2 * o.z * y * square(r) * u.y * u.z + 2 * o.z * y * square(r) * v.y * v.z - 2 * o.z * square(r) * square(u.y) * near - 2 * o.z * square(r) * square(v.y) * near + square(y) * square(r) * square(u.z) + square(y) * square(r) * square(v.z) - 2 * y * square(r) * u.y * u.z * near - 2 * y * square(r) * v.y * v.z * near + square(r) * square(u.y) * square(near) + square(r) * square(v.y) * square(near));
  810. var denom = d.y * o.z - d.y * near - d.z * o.y + d.z * y + o.y * r * u.z - o.z * r * u.y - y * r * u.z + r * u.y * near;
  811. return 2 * math.atan((r * (o.y * v.z - o.z * v.y - y * v.z + v.y * near) + math.float2(1, -1) * sqrt) / denom);
  812. }
  813. }
  814. }