Nenhuma descrição
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

LightCookieManager.cs 37KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. using System;
  2. using System.Runtime.InteropServices;
  3. using UnityEngine.Experimental.Rendering;
  4. using Unity.Mathematics;
  5. namespace UnityEngine.Rendering.Universal
  6. {
  7. internal class LightCookieManager : IDisposable
  8. {
  9. static class ShaderProperty
  10. {
  11. public static readonly int mainLightTexture = Shader.PropertyToID("_MainLightCookieTexture");
  12. public static readonly int mainLightWorldToLight = Shader.PropertyToID("_MainLightWorldToLight");
  13. public static readonly int mainLightCookieTextureFormat = Shader.PropertyToID("_MainLightCookieTextureFormat");
  14. public static readonly int additionalLightsCookieAtlasTexture = Shader.PropertyToID("_AdditionalLightsCookieAtlasTexture");
  15. public static readonly int additionalLightsCookieAtlasTextureFormat = Shader.PropertyToID("_AdditionalLightsCookieAtlasTextureFormat");
  16. public static readonly int additionalLightsCookieEnableBits = Shader.PropertyToID("_AdditionalLightsCookieEnableBits");
  17. public static readonly int additionalLightsCookieAtlasUVRectBuffer = Shader.PropertyToID("_AdditionalLightsCookieAtlasUVRectBuffer");
  18. public static readonly int additionalLightsCookieAtlasUVRects = Shader.PropertyToID("_AdditionalLightsCookieAtlasUVRects");
  19. // TODO: these should be generic light property
  20. public static readonly int additionalLightsWorldToLightBuffer = Shader.PropertyToID("_AdditionalLightsWorldToLightBuffer");
  21. public static readonly int additionalLightsLightTypeBuffer = Shader.PropertyToID("_AdditionalLightsLightTypeBuffer");
  22. public static readonly int additionalLightsWorldToLights = Shader.PropertyToID("_AdditionalLightsWorldToLights");
  23. public static readonly int additionalLightsLightTypes = Shader.PropertyToID("_AdditionalLightsLightTypes");
  24. }
  25. private enum LightCookieShaderFormat
  26. {
  27. None = -1,
  28. RGB = 0,
  29. Alpha = 1,
  30. Red = 2
  31. }
  32. public struct Settings
  33. {
  34. public struct AtlasSettings
  35. {
  36. public Vector2Int resolution;
  37. public GraphicsFormat format;
  38. public bool isPow2 => Mathf.IsPowerOfTwo(resolution.x) && Mathf.IsPowerOfTwo(resolution.y);
  39. public bool isSquare => resolution.x == resolution.y;
  40. }
  41. public AtlasSettings atlas;
  42. public int maxAdditionalLights; // UniversalRenderPipeline.maxVisibleAdditionalLights;
  43. public float cubeOctahedralSizeScale; // Cube octahedral projection size scale.
  44. public bool useStructuredBuffer; // RenderingUtils.useStructuredBuffer
  45. public static Settings Create()
  46. {
  47. Settings s;
  48. s.atlas.resolution = new Vector2Int(1024, 1024);
  49. s.atlas.format = GraphicsFormat.R8G8B8A8_SRGB;
  50. s.maxAdditionalLights = UniversalRenderPipeline.maxVisibleAdditionalLights;
  51. // (Scale * W * Scale * H) / (6 * WH) == (Scale^2 / 6)
  52. // 1: 1/6 = 16%, 2: 4/6 = 66%, 4: 16/6 == 266% of cube pixels
  53. // 100% cube pixels == sqrt(6) ~= 2.45f --> 2.5;
  54. s.cubeOctahedralSizeScale = 2.5f;
  55. s.useStructuredBuffer = RenderingUtils.useStructuredBuffer;
  56. return s;
  57. }
  58. }
  59. private struct LightCookieMapping
  60. {
  61. public ushort visibleLightIndex; // Index into visible light (src)
  62. public ushort lightBufferIndex; // Index into light shader data buffer(s) (dst) (matches ForwardLights.SetupAdditionalLightConstants())
  63. public Light light; // Cached built-in light for the visibleLightIndex. Avoids multiple copies on all the gets from native array.
  64. public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByCookieSize = (LightCookieMapping a, LightCookieMapping b) =>
  65. {
  66. var alc = a.light.cookie;
  67. var blc = b.light.cookie;
  68. int a2 = alc.width * alc.height;
  69. int b2 = blc.width * blc.height;
  70. int d = b2 - a2;
  71. if (d == 0)
  72. {
  73. // Sort by texture ID if "undecided" to batch fetches to the same cookie texture.
  74. int ai = alc.GetInstanceID();
  75. int bi = blc.GetInstanceID();
  76. return ai - bi;
  77. }
  78. return d;
  79. };
  80. public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByBufferIndex = (LightCookieMapping a, LightCookieMapping b) =>
  81. {
  82. return a.lightBufferIndex - b.lightBufferIndex;
  83. };
  84. }
  85. private readonly struct WorkSlice<T>
  86. {
  87. private readonly T[] m_Data;
  88. private readonly int m_Start;
  89. private readonly int m_Length;
  90. public WorkSlice(T[] src, int srcLen = -1) : this(src, 0, srcLen) { }
  91. public WorkSlice(T[] src, int srcStart, int srcLen = -1)
  92. {
  93. m_Data = src;
  94. m_Start = srcStart;
  95. m_Length = (srcLen < 0) ? src.Length : Math.Min(srcLen, src.Length);
  96. Assertions.Assert.IsTrue(m_Start + m_Length <= capacity);
  97. }
  98. public T this[int index]
  99. {
  100. get => m_Data[m_Start + index];
  101. set => m_Data[m_Start + index] = value;
  102. }
  103. public int length => m_Length;
  104. public int capacity => m_Data.Length;
  105. public void Sort(Func<T, T, int> compare)
  106. {
  107. if (m_Length > 1)
  108. Sorting.QuickSort(m_Data, m_Start, m_Start + m_Length - 1, compare);
  109. }
  110. }
  111. // Persistent work/temp memory of [] data.
  112. private class WorkMemory
  113. {
  114. public LightCookieMapping[] lightMappings;
  115. public Vector4[] uvRects;
  116. public void Resize(int size)
  117. {
  118. if (size <= lightMappings?.Length)
  119. return;
  120. // Avoid allocs on every tiny size change.
  121. size = Math.Max(size, ((size + 15) / 16) * 16);
  122. lightMappings = new LightCookieMapping[size];
  123. uvRects = new Vector4[size];
  124. }
  125. }
  126. /// Must match light data layout.
  127. private class LightCookieShaderData : IDisposable
  128. {
  129. int m_Size = 0;
  130. bool m_UseStructuredBuffer;
  131. // Shader data CPU arrays, used to upload the data to GPU
  132. Matrix4x4[] m_WorldToLightCpuData;
  133. Vector4[] m_AtlasUVRectCpuData;
  134. float[] m_LightTypeCpuData;
  135. ShaderBitArray m_CookieEnableBitsCpuData;
  136. // Compute buffer counterparts for the CPU data
  137. ComputeBuffer m_WorldToLightBuffer; // TODO: WorldToLight matrices should be general property of lights!!
  138. ComputeBuffer m_AtlasUVRectBuffer;
  139. ComputeBuffer m_LightTypeBuffer;
  140. public Matrix4x4[] worldToLights => m_WorldToLightCpuData;
  141. public ShaderBitArray cookieEnableBits => m_CookieEnableBitsCpuData;
  142. public Vector4[] atlasUVRects => m_AtlasUVRectCpuData;
  143. public float[] lightTypes => m_LightTypeCpuData;
  144. public bool isUploaded { get; set; }
  145. public LightCookieShaderData(int size, bool useStructuredBuffer)
  146. {
  147. m_UseStructuredBuffer = useStructuredBuffer;
  148. Resize(size);
  149. }
  150. public void Dispose()
  151. {
  152. if (m_UseStructuredBuffer)
  153. {
  154. m_WorldToLightBuffer?.Dispose();
  155. m_AtlasUVRectBuffer?.Dispose();
  156. m_LightTypeBuffer?.Dispose();
  157. }
  158. }
  159. public void Resize(int size)
  160. {
  161. if (size <= m_Size)
  162. return;
  163. if (m_Size > 0)
  164. Dispose();
  165. m_WorldToLightCpuData = new Matrix4x4[size];
  166. m_AtlasUVRectCpuData = new Vector4[size];
  167. m_LightTypeCpuData = new float[size];
  168. m_CookieEnableBitsCpuData.Resize(size);
  169. if (m_UseStructuredBuffer)
  170. {
  171. m_WorldToLightBuffer = new ComputeBuffer(size, Marshal.SizeOf<Matrix4x4>());
  172. m_AtlasUVRectBuffer = new ComputeBuffer(size, Marshal.SizeOf<Vector4>());
  173. m_LightTypeBuffer = new ComputeBuffer(size, Marshal.SizeOf<float>());
  174. }
  175. m_Size = size;
  176. }
  177. public void Upload(CommandBuffer cmd)
  178. {
  179. if (m_UseStructuredBuffer)
  180. {
  181. m_WorldToLightBuffer.SetData(m_WorldToLightCpuData);
  182. m_AtlasUVRectBuffer.SetData(m_AtlasUVRectCpuData);
  183. m_LightTypeBuffer.SetData(m_LightTypeCpuData);
  184. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsWorldToLightBuffer, m_WorldToLightBuffer);
  185. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsCookieAtlasUVRectBuffer, m_AtlasUVRectBuffer);
  186. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsLightTypeBuffer, m_LightTypeBuffer);
  187. }
  188. else
  189. {
  190. cmd.SetGlobalMatrixArray(ShaderProperty.additionalLightsWorldToLights, m_WorldToLightCpuData);
  191. cmd.SetGlobalVectorArray(ShaderProperty.additionalLightsCookieAtlasUVRects, m_AtlasUVRectCpuData);
  192. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsLightTypes, m_LightTypeCpuData);
  193. }
  194. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
  195. isUploaded = true;
  196. }
  197. public void Clear(CommandBuffer cmd)
  198. {
  199. if (isUploaded)
  200. {
  201. // Set all lights to disabled/invalid state
  202. m_CookieEnableBitsCpuData.Clear();
  203. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
  204. isUploaded = false;
  205. }
  206. }
  207. }
  208. // Unity defines directional light UVs over a unit box centered at light.
  209. // i.e. (0, 1) uv == (-0.5, 0.5) world area instead of the (0,1) world area.
  210. static readonly Matrix4x4 s_DirLightProj = Matrix4x4.Ortho(-0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f);
  211. Texture2DAtlas m_AdditionalLightsCookieAtlas;
  212. LightCookieShaderData m_AdditionalLightsCookieShaderData;
  213. readonly Settings m_Settings;
  214. WorkMemory m_WorkMem;
  215. // Mapping: map[visibleLightIndex] = ShaderDataIndex
  216. // Mostly used by deferred rendering.
  217. int[] m_VisibleLightIndexToShaderDataIndex;
  218. // Parameters for rescaling cookies to fit into the atlas.
  219. const int k_MaxCookieSizeDivisor = 16;
  220. int m_CookieSizeDivisor = 1;
  221. uint m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
  222. // TODO: replace with a proper error system
  223. // Frame "timestamp" of last warning to throttle warn messages.
  224. int m_PrevWarnFrame = -1;
  225. internal bool IsKeywordLightCookieEnabled { get; private set; }
  226. internal RTHandle AdditionalLightsCookieAtlasTexture => m_AdditionalLightsCookieAtlas?.AtlasTexture;
  227. public LightCookieManager(ref Settings settings)
  228. {
  229. m_Settings = settings;
  230. m_WorkMem = new WorkMemory();
  231. }
  232. void InitAdditionalLights(int size)
  233. {
  234. // No mip padding support.
  235. m_AdditionalLightsCookieAtlas = new Texture2DAtlas(
  236. m_Settings.atlas.resolution.x,
  237. m_Settings.atlas.resolution.y,
  238. m_Settings.atlas.format,
  239. FilterMode.Bilinear,
  240. false,
  241. "Universal Light Cookie Atlas",
  242. false); // to support mips, use Pow2Atlas
  243. m_AdditionalLightsCookieShaderData = new LightCookieShaderData(size, m_Settings.useStructuredBuffer);
  244. const int mainLightCount = 1;
  245. m_VisibleLightIndexToShaderDataIndex = new int[m_Settings.maxAdditionalLights + mainLightCount];
  246. m_CookieSizeDivisor = 1;
  247. m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
  248. }
  249. public bool isInitialized() => m_AdditionalLightsCookieAtlas != null && m_AdditionalLightsCookieShaderData != null;
  250. /// <summary>
  251. /// Release LightCookieManager resources.
  252. /// </summary>
  253. public void Dispose()
  254. {
  255. m_AdditionalLightsCookieAtlas?.Release();
  256. m_AdditionalLightsCookieShaderData?.Dispose();
  257. }
  258. // -1 on invalid/disabled cookie.
  259. public int GetLightCookieShaderDataIndex(int visibleLightIndex)
  260. {
  261. if (!isInitialized())
  262. return -1;
  263. return m_VisibleLightIndexToShaderDataIndex[visibleLightIndex];
  264. }
  265. public void Setup(CommandBuffer cmd, UniversalLightData lightData)
  266. {
  267. using var profScope = new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.LightCookies));
  268. // Main light, 1 directional, bound directly
  269. bool isMainLightAvailable = lightData.mainLightIndex >= 0;
  270. if (isMainLightAvailable)
  271. {
  272. var mainLight = lightData.visibleLights[lightData.mainLightIndex];
  273. isMainLightAvailable = SetupMainLight(cmd, ref mainLight);
  274. }
  275. // Additional lights, N spot and point lights in atlas
  276. bool isAdditionalLightsAvailable = lightData.additionalLightsCount > 0;
  277. if (isAdditionalLightsAvailable)
  278. {
  279. isAdditionalLightsAvailable = SetupAdditionalLights(cmd, lightData);
  280. }
  281. // Ensure cookies are disabled if no cookies are available.
  282. if (!isAdditionalLightsAvailable)
  283. {
  284. // ..on the CPU (for deferred)
  285. if (m_VisibleLightIndexToShaderDataIndex != null &&
  286. m_AdditionalLightsCookieShaderData.isUploaded)
  287. {
  288. int len = m_VisibleLightIndexToShaderDataIndex.Length;
  289. for (int i = 0; i < len; i++)
  290. m_VisibleLightIndexToShaderDataIndex[i] = -1;
  291. }
  292. // ..on the GPU
  293. m_AdditionalLightsCookieShaderData?.Clear(cmd);
  294. }
  295. // Main and additional lights are merged into one keyword to reduce variants.
  296. IsKeywordLightCookieEnabled = isMainLightAvailable || isAdditionalLightsAvailable;
  297. cmd.SetKeyword(ShaderGlobalKeywords.LightCookies, IsKeywordLightCookieEnabled);
  298. }
  299. bool SetupMainLight(CommandBuffer cmd, ref VisibleLight visibleMainLight)
  300. {
  301. var mainLight = visibleMainLight.light;
  302. var cookieTexture = mainLight.cookie;
  303. bool isMainLightCookieEnabled = cookieTexture != null;
  304. if (isMainLightCookieEnabled)
  305. {
  306. Matrix4x4 cookieUVTransform = Matrix4x4.identity;
  307. float cookieFormat = (float)GetLightCookieShaderFormat(cookieTexture.graphicsFormat);
  308. if (mainLight.TryGetComponent(out UniversalAdditionalLightData additionalLightData))
  309. GetLightUVScaleOffset(ref additionalLightData, ref cookieUVTransform);
  310. Matrix4x4 cookieMatrix = s_DirLightProj * cookieUVTransform *
  311. visibleMainLight.localToWorldMatrix.inverse;
  312. cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, cookieTexture);
  313. cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, cookieMatrix);
  314. cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, cookieFormat);
  315. }
  316. else
  317. {
  318. // Make sure we erase stale data in case the main light is disabled but cookie system is enabled (for additional lights).
  319. cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, Texture2D.whiteTexture);
  320. cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, Matrix4x4.identity);
  321. cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, (float)LightCookieShaderFormat.None);
  322. }
  323. return isMainLightCookieEnabled;
  324. }
  325. private LightCookieShaderFormat GetLightCookieShaderFormat(GraphicsFormat cookieFormat)
  326. {
  327. // TODO: convert this to use GraphicsFormatUtility
  328. switch (cookieFormat)
  329. {
  330. default:
  331. return LightCookieShaderFormat.RGB;
  332. // A8, A16 GraphicsFormat does not expose yet.
  333. case (GraphicsFormat)54:
  334. case (GraphicsFormat)55:
  335. return LightCookieShaderFormat.Alpha;
  336. case GraphicsFormat.R8_SRGB:
  337. case GraphicsFormat.R8_UNorm:
  338. case GraphicsFormat.R8_UInt:
  339. case GraphicsFormat.R8_SNorm:
  340. case GraphicsFormat.R8_SInt:
  341. case GraphicsFormat.R16_UNorm:
  342. case GraphicsFormat.R16_UInt:
  343. case GraphicsFormat.R16_SNorm:
  344. case GraphicsFormat.R16_SInt:
  345. case GraphicsFormat.R16_SFloat:
  346. case GraphicsFormat.R32_UInt:
  347. case GraphicsFormat.R32_SInt:
  348. case GraphicsFormat.R32_SFloat:
  349. case GraphicsFormat.R_BC4_SNorm:
  350. case GraphicsFormat.R_BC4_UNorm:
  351. case GraphicsFormat.R_EAC_SNorm:
  352. case GraphicsFormat.R_EAC_UNorm:
  353. return LightCookieShaderFormat.Red;
  354. }
  355. }
  356. private void GetLightUVScaleOffset(ref UniversalAdditionalLightData additionalLightData, ref Matrix4x4 uvTransform)
  357. {
  358. Vector2 uvScale = Vector2.one / additionalLightData.lightCookieSize;
  359. Vector2 uvOffset = additionalLightData.lightCookieOffset;
  360. if (Mathf.Abs(uvScale.x) < half.MinValue)
  361. uvScale.x = Mathf.Sign(uvScale.x) * half.MinValue;
  362. if (Mathf.Abs(uvScale.y) < half.MinValue)
  363. uvScale.y = Mathf.Sign(uvScale.y) * half.MinValue;
  364. uvTransform = Matrix4x4.Scale(new Vector3(uvScale.x, uvScale.y, 1));
  365. uvTransform.SetColumn(3, new Vector4(-uvOffset.x * uvScale.x, -uvOffset.y * uvScale.y, 0, 1));
  366. }
  367. bool SetupAdditionalLights(CommandBuffer cmd, UniversalLightData lightData)
  368. {
  369. int maxLightCount = Math.Min(m_Settings.maxAdditionalLights, lightData.visibleLights.Length);
  370. m_WorkMem.Resize(maxLightCount);
  371. int validLightCount = FilterAndValidateAdditionalLights(lightData, m_WorkMem.lightMappings);
  372. // Early exit if no valid cookie lights
  373. if (validLightCount <= 0)
  374. return false;
  375. // Lazy init GPU resources
  376. if (!isInitialized())
  377. InitAdditionalLights(validLightCount);
  378. // Update Atlas
  379. var validLights = new WorkSlice<LightCookieMapping>(m_WorkMem.lightMappings, validLightCount);
  380. int validUVRectCount = UpdateAdditionalLightsAtlas(cmd, ref validLights, m_WorkMem.uvRects);
  381. // Upload shader data
  382. var validUvRects = new WorkSlice<Vector4>(m_WorkMem.uvRects, validUVRectCount);
  383. UploadAdditionalLights(cmd, lightData, ref validLights, ref validUvRects);
  384. bool isAdditionalLightsEnabled = validUvRects.length > 0;
  385. return isAdditionalLightsEnabled;
  386. }
  387. int FilterAndValidateAdditionalLights(UniversalLightData lightData, LightCookieMapping[] validLightMappings)
  388. {
  389. int skipMainLightIndex = lightData.mainLightIndex;
  390. int lightBufferOffset = 0;
  391. int validLightCount = 0;
  392. int visibleLightCount = lightData.visibleLights.Length;
  393. for (int i = 0; i < visibleLightCount; i++)
  394. {
  395. // Drop main light from additional lights buffer.
  396. if (i == skipMainLightIndex)
  397. {
  398. lightBufferOffset -= 1;
  399. continue;
  400. }
  401. ref var visLight = ref lightData.visibleLights.UnsafeElementAtMutable(i);
  402. Light light = visLight.light;
  403. // Skip lights without a cookie texture
  404. if (light.cookie == null)
  405. continue;
  406. // Only spot, point and directional lights are supported.
  407. // Warn on dropped lights
  408. var lightType = visLight.lightType;
  409. if (!(lightType == LightType.Spot ||
  410. lightType == LightType.Point ||
  411. lightType == LightType.Directional))
  412. {
  413. Debug.LogWarning($"Additional {lightType.ToString()} light called '{light.name}' has a light cookie which will not be visible.", light);
  414. continue;
  415. }
  416. Assertions.Assert.IsTrue(i < ushort.MaxValue);
  417. LightCookieMapping lp;
  418. lp.visibleLightIndex = (ushort)i;
  419. lp.lightBufferIndex = (ushort)(i + lightBufferOffset); // Matching FowardLights.SetupAdditionalLightConstants
  420. lp.light = light;
  421. // Drop lights if we have too many lights or too many cookies to fit ForwardLight data.
  422. if (lp.lightBufferIndex >= validLightMappings.Length || validLightCount + 1 >= validLightMappings.Length)
  423. {
  424. // TODO: Better error system
  425. if (visibleLightCount > m_Settings.maxAdditionalLights &&
  426. Time.frameCount - m_PrevWarnFrame > 60 * 60) // warn throttling: ~60 FPS * 60 secs ~= 1 min
  427. {
  428. m_PrevWarnFrame = Time.frameCount;
  429. Debug.LogWarning($"Max light cookies ({validLightMappings.Length.ToString()}) reached. Some visible lights ({(visibleLightCount - i - 1).ToString()}) might skip light cookie rendering.");
  430. }
  431. // Always break, buffer full.
  432. break;
  433. }
  434. validLightMappings[validLightCount++] = lp;
  435. }
  436. return validLightCount;
  437. }
  438. int UpdateAdditionalLightsAtlas(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects)
  439. {
  440. // Sort in-place by cookie size for better atlas allocation efficiency (and deduplication)
  441. validLightMappings.Sort(LightCookieMapping.s_CompareByCookieSize);
  442. uint cookieRequestPixelCount = ComputeCookieRequestPixelCount(ref validLightMappings);
  443. var atlasSize = m_AdditionalLightsCookieAtlas.AtlasTexture.referenceSize;
  444. float requestAtlasRatio = cookieRequestPixelCount / (float)(atlasSize.x * atlasSize.y);
  445. int cookieSizeDivisorApprox = ApproximateCookieSizeDivisor(requestAtlasRatio);
  446. // Try to recover resolution and scale the cookies back up.
  447. // If the cookies "should fit" and
  448. // If we have less requested pixels than the last time we found the correct divisor (a guard against retrying every frame).
  449. if (cookieSizeDivisorApprox < m_CookieSizeDivisor &&
  450. cookieRequestPixelCount < m_PrevCookieRequestPixelCount)
  451. {
  452. m_AdditionalLightsCookieAtlas.ResetAllocator();
  453. m_CookieSizeDivisor = cookieSizeDivisorApprox;
  454. }
  455. // Get cached atlas uv rectangles.
  456. // If there's new cookies, first try to add at current scaling level.
  457. // (This can result in suboptimal packing & scaling (additions aren't sorted), but reduces rebuilds.)
  458. // If it doesn't fit, scale down and rebuild the atlas until it fits.
  459. int uvRectCount = 0;
  460. while (uvRectCount <= 0)
  461. {
  462. uvRectCount = FetchUVRects(cmd, ref validLightMappings, textureAtlasUVRects, m_CookieSizeDivisor);
  463. if (uvRectCount <= 0)
  464. {
  465. // Uv rect fetching failed, reset and try again.
  466. m_AdditionalLightsCookieAtlas.ResetAllocator();
  467. // Reduce cookie size to approximate value try to rebuild the atlas.
  468. m_CookieSizeDivisor = Mathf.Max(m_CookieSizeDivisor + 1, cookieSizeDivisorApprox);
  469. m_PrevCookieRequestPixelCount = cookieRequestPixelCount;
  470. }
  471. }
  472. return uvRectCount;
  473. }
  474. int FetchUVRects(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects, int cookieSizeDivisor)
  475. {
  476. int uvRectCount = 0;
  477. for (int i = 0; i < validLightMappings.length; i++)
  478. {
  479. var lcm = validLightMappings[i];
  480. Light light = lcm.light;
  481. Texture cookie = light.cookie;
  482. // NOTE: Currently we blit directly on addition (on atlas fetch cache miss).
  483. // This can be costly if there are many resize rebuilds (in case "out-of-space", which shouldn't be a common case).
  484. // If rebuilds become a problem, we could try to just allocate and blit only when we have a fully valid allocation.
  485. // It would also make sense to do atlas operations only for unique textures and then reuse the results for similar cookies.
  486. Vector4 uvScaleOffset = Vector4.zero;
  487. if (cookie.dimension == TextureDimension.Cube)
  488. {
  489. Assertions.Assert.IsTrue(light.type == LightType.Point);
  490. uvScaleOffset = FetchCube(cmd, cookie, cookieSizeDivisor);
  491. }
  492. else
  493. {
  494. Assertions.Assert.IsTrue(light.type == LightType.Spot || light.type == LightType.Directional, "Light type needs 2D texture!");
  495. uvScaleOffset = Fetch2D(cmd, cookie, cookieSizeDivisor);
  496. }
  497. bool isCached = uvScaleOffset != Vector4.zero;
  498. if (!isCached)
  499. {
  500. if (cookieSizeDivisor > k_MaxCookieSizeDivisor)
  501. {
  502. Debug.LogWarning($"Light cookies atlas is extremely full! Some of the light cookies were discarded. Increase light cookie atlas space or reduce the amount of unique light cookies.");
  503. // Complete fail, return what we have.
  504. return uvRectCount;
  505. }
  506. // Failed to get uv rect for each cookie, fail and try again.
  507. return 0;
  508. }
  509. // Adjust atlas UVs for OpenGL
  510. if (!SystemInfo.graphicsUVStartsAtTop)
  511. uvScaleOffset.w = 1.0f - uvScaleOffset.w - uvScaleOffset.y;
  512. textureAtlasUVRects[uvRectCount++] = uvScaleOffset;
  513. }
  514. return uvRectCount;
  515. }
  516. uint ComputeCookieRequestPixelCount(ref WorkSlice<LightCookieMapping> validLightMappings)
  517. {
  518. uint requestPixelCount = 0;
  519. int prevCookieID = 0;
  520. for (int i = 0; i < validLightMappings.length; i++)
  521. {
  522. var lcm = validLightMappings[i];
  523. Texture cookie = lcm.light.cookie;
  524. int cookieID = cookie.GetInstanceID();
  525. // Consider only unique textures as atlas request pixels
  526. // NOTE: relies on same cookies being sorted together
  527. // (we need sorting for good atlas packing anyway)
  528. if (cookieID == prevCookieID)
  529. {
  530. continue;
  531. }
  532. prevCookieID = cookieID;
  533. int pixelCookieCount = cookie.width * cookie.height;
  534. requestPixelCount += (uint)pixelCookieCount;
  535. }
  536. return requestPixelCount;
  537. }
  538. int ApproximateCookieSizeDivisor(float requestAtlasRatio)
  539. {
  540. // (Edge / N)^2 == 1/N^2 of area.
  541. // Ratio/N^2 == 1, sqrt(Ratio) == N, for "1:1" ratio.
  542. return (int)Mathf.Max(Mathf.Ceil(Mathf.Sqrt(requestAtlasRatio)), 1);
  543. }
  544. Vector4 Fetch2D(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
  545. {
  546. Assertions.Assert.IsTrue(cookie != null);
  547. Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Tex2D);
  548. Vector4 uvScaleOffset = Vector4.zero;
  549. var scaledWidth = Mathf.Max(cookie.width / cookieSizeDivisor, 4);
  550. var scaledHeight = Mathf.Max(cookie.height / cookieSizeDivisor, 4);
  551. Vector2 scaledCookieSize = new Vector2(scaledWidth, scaledHeight);
  552. bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
  553. if (isCached)
  554. {
  555. // Update contents IF required
  556. m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
  557. }
  558. else
  559. {
  560. m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledWidth, scaledHeight);
  561. }
  562. AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
  563. return uvScaleOffset;
  564. }
  565. Vector4 FetchCube(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
  566. {
  567. Assertions.Assert.IsTrue(cookie != null);
  568. Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Cube);
  569. Vector4 uvScaleOffset = Vector4.zero;
  570. // Scale octahedral projection, so that cube -> oct2D pixel count match better.
  571. int scaledOctCookieSize = Mathf.Max(ComputeOctahedralCookieSize(cookie) / cookieSizeDivisor, 4);
  572. bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
  573. if (isCached)
  574. {
  575. // Update contents IF required
  576. m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
  577. }
  578. else
  579. {
  580. m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledOctCookieSize, scaledOctCookieSize);
  581. }
  582. // Cookie size in the atlas might not match CookieTexture size.
  583. // UVRect adjustment must be done with size in atlas.
  584. var scaledCookieSize = Vector2.one * scaledOctCookieSize;
  585. AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
  586. return uvScaleOffset;
  587. }
  588. int ComputeOctahedralCookieSize(Texture cookie)
  589. {
  590. // Map 6*WxH pixels into 2W*2H pixels, so 4/6 ratio or 66% of cube pixels.
  591. int octCookieSize = Math.Max(cookie.width, cookie.height);
  592. if (m_Settings.atlas.isPow2)
  593. octCookieSize = octCookieSize * Mathf.NextPowerOfTwo((int)m_Settings.cubeOctahedralSizeScale);
  594. else
  595. octCookieSize = (int)(octCookieSize * m_Settings.cubeOctahedralSizeScale + 0.5f);
  596. return octCookieSize;
  597. }
  598. private void AdjustUVRect(ref Vector4 uvScaleOffset, Texture cookie, ref Vector2 cookieSize)
  599. {
  600. if (uvScaleOffset != Vector4.zero)
  601. {
  602. // Shrink by 0.5px to clamp the bilinear sampling to exclude atlas neighbors (no padding)
  603. ShrinkUVRect(ref uvScaleOffset, 0.5f, ref cookieSize);
  604. }
  605. }
  606. private void ShrinkUVRect(ref Vector4 uvScaleOffset, float amountPixels, ref Vector2 cookieSize)
  607. {
  608. var shrinkOffset = Vector2.one * amountPixels / cookieSize;
  609. var shrinkScale = (cookieSize - Vector2.one * (amountPixels * 2)) / cookieSize;
  610. uvScaleOffset.z += uvScaleOffset.x * shrinkOffset.x;
  611. uvScaleOffset.w += uvScaleOffset.y * shrinkOffset.y;
  612. uvScaleOffset.x *= shrinkScale.x;
  613. uvScaleOffset.y *= shrinkScale.y;
  614. }
  615. void UploadAdditionalLights(CommandBuffer cmd, UniversalLightData lightData, ref WorkSlice<LightCookieMapping> validLightMappings, ref WorkSlice<Vector4> validUvRects)
  616. {
  617. Assertions.Assert.IsTrue(m_AdditionalLightsCookieAtlas != null);
  618. Assertions.Assert.IsTrue(m_AdditionalLightsCookieShaderData != null);
  619. cmd.SetGlobalTexture(ShaderProperty.additionalLightsCookieAtlasTexture, m_AdditionalLightsCookieAtlas.AtlasTexture);
  620. cmd.SetGlobalFloat(ShaderProperty.additionalLightsCookieAtlasTextureFormat, (float)GetLightCookieShaderFormat(m_AdditionalLightsCookieAtlas.AtlasTexture.rt.graphicsFormat));
  621. // Resize and clear visible light to shader data mapping
  622. if (m_VisibleLightIndexToShaderDataIndex.Length < lightData.visibleLights.Length)
  623. m_VisibleLightIndexToShaderDataIndex = new int[lightData.visibleLights.Length];
  624. // Clear
  625. int len = Math.Min(m_VisibleLightIndexToShaderDataIndex.Length, lightData.visibleLights.Length);
  626. for (int i = 0; i < len; i++)
  627. m_VisibleLightIndexToShaderDataIndex[i] = -1;
  628. // Resize or init shader data.
  629. m_AdditionalLightsCookieShaderData.Resize(m_Settings.maxAdditionalLights);
  630. var worldToLights = m_AdditionalLightsCookieShaderData.worldToLights;
  631. var cookieEnableBits = m_AdditionalLightsCookieShaderData.cookieEnableBits;
  632. var atlasUVRects = m_AdditionalLightsCookieShaderData.atlasUVRects;
  633. var lightTypes = m_AdditionalLightsCookieShaderData.lightTypes;
  634. // Set all rects to "Invalid" zero area (Vector4.zero), just in case they're accessed.
  635. Array.Clear(atlasUVRects, 0, atlasUVRects.Length);
  636. // Set all cookies disabled
  637. cookieEnableBits.Clear();
  638. // NOTE: technically, we don't need to upload constants again if we knew the lights, atlas (rects) or visible order haven't changed.
  639. // But detecting that, might be as time consuming as just doing the work.
  640. // Fill shader data. Layout should match primary light data for additional lights.
  641. // Currently it's the same as visible lights, but main light(s) dropped.
  642. for (int i = 0; i < validUvRects.length; i++)
  643. {
  644. int visIndex = validLightMappings[i].visibleLightIndex;
  645. int bufIndex = validLightMappings[i].lightBufferIndex;
  646. // Update the mapping
  647. m_VisibleLightIndexToShaderDataIndex[visIndex] = bufIndex;
  648. ref var visLight = ref lightData.visibleLights.UnsafeElementAtMutable(visIndex);
  649. // Update the (cpu) data
  650. lightTypes[bufIndex] = (int)visLight.lightType;
  651. worldToLights[bufIndex] = visLight.localToWorldMatrix.inverse;
  652. atlasUVRects[bufIndex] = validUvRects[i];
  653. cookieEnableBits[bufIndex] = true;
  654. // Spot projection
  655. if (visLight.lightType == LightType.Spot)
  656. {
  657. // VisibleLight.localToWorldMatrix only contains position & rotation.
  658. // Multiply projection for spot light.
  659. float spotAngle = visLight.spotAngle;
  660. float spotRange = visLight.range;
  661. var perp = Matrix4x4.Perspective(spotAngle, 1, 0.001f, spotRange);
  662. // Cancel embedded camera view axis flip (https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Matrix4x4.Perspective.html)
  663. perp.SetColumn(2, perp.GetColumn(2) * -1);
  664. // world -> light local -> light perspective
  665. worldToLights[bufIndex] = perp * worldToLights[bufIndex];
  666. }
  667. // Directional projection
  668. else if (visLight.lightType == LightType.Directional)
  669. {
  670. Light light = visLight.light;
  671. light.TryGetComponent<UniversalAdditionalLightData>(out var additionalLightData);
  672. {
  673. Matrix4x4 cookieUVTransform = Matrix4x4.identity;
  674. GetLightUVScaleOffset(ref additionalLightData, ref cookieUVTransform);
  675. Matrix4x4 cookieMatrix = s_DirLightProj * cookieUVTransform *
  676. visLight.localToWorldMatrix.inverse;
  677. worldToLights[bufIndex] = cookieMatrix;
  678. }
  679. }
  680. }
  681. // Apply changes and upload to GPU
  682. m_AdditionalLightsCookieShaderData.Upload(cmd);
  683. }
  684. }
  685. }