LightCookieManager.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  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 useMips;
  39. public bool isPow2 => Mathf.IsPowerOfTwo(resolution.x) && Mathf.IsPowerOfTwo(resolution.y);
  40. public bool isSquare => resolution.x == resolution.y;
  41. }
  42. public AtlasSettings atlas;
  43. public int maxAdditionalLights; // UniversalRenderPipeline.maxVisibleAdditionalLights;
  44. public float cubeOctahedralSizeScale; // Cube octahedral projection size scale.
  45. public bool useStructuredBuffer; // RenderingUtils.useStructuredBuffer
  46. public static Settings GetDefault()
  47. {
  48. Settings s;
  49. s.atlas.resolution = new Vector2Int(1024, 1024);
  50. s.atlas.format = GraphicsFormat.R8G8B8A8_SRGB;
  51. s.atlas.useMips = false; // TODO: set to true, make sure they work proper first! Disable them for now...
  52. s.maxAdditionalLights = UniversalRenderPipeline.maxVisibleAdditionalLights;
  53. // (Scale * W * Scale * H) / (6 * WH) == (Scale^2 / 6)
  54. // 1: 1/6 = 16%, 2: 4/6 = 66%, 4: 16/6 == 266% of cube pixels
  55. // 100% cube pixels == sqrt(6) ~= 2.45f --> 2.5;
  56. s.cubeOctahedralSizeScale = s.atlas.useMips && s.atlas.isPow2 ? 2.0f : 2.5f;
  57. s.useStructuredBuffer = RenderingUtils.useStructuredBuffer;
  58. return s;
  59. }
  60. }
  61. private struct Sorting
  62. {
  63. public static void QuickSort<T>(T[] data, Func<T, T, int> compare)
  64. {
  65. QuickSort<T>(data, 0, data.Length - 1, compare);
  66. }
  67. // A non-allocating predicated sub-array quick sort.
  68. // NOTE: Similar to UnityEngine.Rendering.CoreUnsafeUtils.QuickSort in CoreUnsafeUtils.cs,
  69. // we should see if these could be merged in the future.
  70. // For example: Sorting.QuickSort(test, 0, test.Length - 1, (int a, int b) => a - b);
  71. public static void QuickSort<T>(T[] data, int start, int end, Func<T, T, int> compare)
  72. {
  73. int diff = end - start;
  74. if (diff < 1)
  75. return;
  76. if (diff < 8)
  77. {
  78. InsertionSort(data, start, end, compare);
  79. return;
  80. }
  81. Assertions.Assert.IsTrue((uint)start < data.Length);
  82. Assertions.Assert.IsTrue((uint)end < data.Length); // end == inclusive
  83. if (start < end)
  84. {
  85. int pivot = Partition<T>(data, start, end, compare);
  86. if (pivot >= 1)
  87. QuickSort<T>(data, start, pivot, compare);
  88. if (pivot + 1 < end)
  89. QuickSort<T>(data, pivot + 1, end, compare);
  90. }
  91. }
  92. static T Median3Pivot<T>(T[] data, int start, int pivot, int end, Func<T, T, int> compare)
  93. {
  94. void Swap(int a, int b)
  95. {
  96. var tmp = data[a];
  97. data[a] = data[b];
  98. data[b] = tmp;
  99. }
  100. if (compare(data[end], data[start]) < 0) Swap(start, end);
  101. if (compare(data[pivot], data[start]) < 0) Swap(start, pivot);
  102. if (compare(data[end], data[pivot]) < 0) Swap(pivot, end);
  103. return data[pivot];
  104. }
  105. static int Partition<T>(T[] data, int start, int end, Func<T, T, int> compare)
  106. {
  107. int diff = end - start;
  108. int pivot = start + diff / 2;
  109. var pivotValue = Median3Pivot(data, start, pivot, end, compare);
  110. while (true)
  111. {
  112. while (compare(data[start], pivotValue) < 0) ++start;
  113. while (compare(data[end], pivotValue) > 0) --end;
  114. if (start >= end)
  115. {
  116. return end;
  117. }
  118. var tmp = data[start];
  119. data[start++] = data[end];
  120. data[end--] = tmp;
  121. }
  122. }
  123. // A non-allocating predicated sub-array insertion sort.
  124. static public void InsertionSort<T>(T[] data, int start, int end, Func<T, T, int> compare)
  125. {
  126. Assertions.Assert.IsTrue((uint)start < data.Length);
  127. Assertions.Assert.IsTrue((uint)end < data.Length);
  128. for (int i = start + 1; i < end + 1; i++)
  129. {
  130. var iData = data[i];
  131. int j = i - 1;
  132. while (j >= 0 && compare(iData, data[j]) < 0)
  133. {
  134. data[j + 1] = data[j];
  135. j--;
  136. }
  137. data[j + 1] = iData;
  138. }
  139. }
  140. }
  141. private struct LightCookieMapping
  142. {
  143. public ushort visibleLightIndex; // Index into visible light (src)
  144. public ushort lightBufferIndex; // Index into light shader data buffer (dst)
  145. public Light light; // Cached built-in light for the visibleLightIndex. Avoids multiple copies on all the gets from native array.
  146. public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByCookieSize = (LightCookieMapping a, LightCookieMapping b) =>
  147. {
  148. var alc = a.light.cookie;
  149. var blc = b.light.cookie;
  150. int a2 = alc.width * alc.height;
  151. int b2 = blc.width * blc.height;
  152. int d = b2 - a2;
  153. if (d == 0)
  154. {
  155. // Sort by texture ID if "undecided" to batch fetches to the same cookie texture.
  156. int ai = alc.GetInstanceID();
  157. int bi = blc.GetInstanceID();
  158. return ai - bi;
  159. }
  160. return d;
  161. };
  162. public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByBufferIndex = (LightCookieMapping a, LightCookieMapping b) =>
  163. {
  164. return a.lightBufferIndex - b.lightBufferIndex;
  165. };
  166. }
  167. private readonly struct WorkSlice<T>
  168. {
  169. private readonly T[] m_Data;
  170. private readonly int m_Start;
  171. private readonly int m_Length;
  172. public WorkSlice(T[] src, int srcLen = -1) : this(src, 0, srcLen) { }
  173. public WorkSlice(T[] src, int srcStart, int srcLen = -1)
  174. {
  175. m_Data = src;
  176. m_Start = srcStart;
  177. m_Length = (srcLen < 0) ? src.Length : Math.Min(srcLen, src.Length);
  178. Assertions.Assert.IsTrue(m_Start + m_Length <= capacity);
  179. }
  180. public T this[int index]
  181. {
  182. get => m_Data[m_Start + index];
  183. set => m_Data[m_Start + index] = value;
  184. }
  185. public int length => m_Length;
  186. public int capacity => m_Data.Length;
  187. public void Sort(Func<T, T, int> compare)
  188. {
  189. if (m_Length > 1)
  190. Sorting.QuickSort(m_Data, m_Start, m_Start + m_Length - 1, compare);
  191. }
  192. }
  193. // Persistent work/temp memory of [] data.
  194. private class WorkMemory
  195. {
  196. public LightCookieMapping[] lightMappings;
  197. public Vector4[] uvRects;
  198. public void Resize(int size)
  199. {
  200. if (size <= lightMappings?.Length)
  201. return;
  202. // Avoid allocs on every tiny size change.
  203. size = Math.Max(size, ((size + 15) / 16) * 16);
  204. lightMappings = new LightCookieMapping[size];
  205. uvRects = new Vector4[size];
  206. }
  207. }
  208. private struct ShaderBitArray
  209. {
  210. const int k_BitsPerElement = 32;
  211. const int k_ElementShift = 5;
  212. const int k_ElementMask = (1 << k_ElementShift) - 1;
  213. private float[] m_Data;
  214. public int elemLength => m_Data == null ? 0 : m_Data.Length;
  215. public int bitCapacity => elemLength * k_BitsPerElement;
  216. public float[] data => m_Data;
  217. public void Resize(int bitCount)
  218. {
  219. if (bitCapacity > bitCount)
  220. return;
  221. int newElemCount = ((bitCount + (k_BitsPerElement - 1)) / k_BitsPerElement);
  222. if (newElemCount == m_Data?.Length)
  223. return;
  224. var newData = new float[newElemCount];
  225. if (m_Data != null)
  226. {
  227. for (int i = 0; i < m_Data.Length; i++)
  228. newData[i] = m_Data[i];
  229. }
  230. m_Data = newData;
  231. }
  232. public void Clear()
  233. {
  234. for (int i = 0; i < m_Data.Length; i++)
  235. m_Data[i] = 0;
  236. }
  237. private void GetElementIndexAndBitOffset(int index, out int elemIndex, out int bitOffset)
  238. {
  239. elemIndex = index >> k_ElementShift;
  240. bitOffset = index & k_ElementMask;
  241. }
  242. public bool this[int index]
  243. {
  244. get
  245. {
  246. GetElementIndexAndBitOffset(index, out var elemIndex, out var bitOffset);
  247. unsafe
  248. {
  249. fixed (float* floatData = m_Data)
  250. {
  251. uint* uintElem = (uint*)&floatData[elemIndex];
  252. bool val = ((*uintElem) & (1u << bitOffset)) != 0u;
  253. return val;
  254. }
  255. }
  256. }
  257. set
  258. {
  259. GetElementIndexAndBitOffset(index, out var elemIndex, out var bitOffset);
  260. unsafe
  261. {
  262. fixed (float* floatData = m_Data)
  263. {
  264. uint* uintElem = (uint*)&floatData[elemIndex];
  265. if (value == true)
  266. *uintElem = (*uintElem) | (1u << bitOffset);
  267. else
  268. *uintElem = (*uintElem) & ~(1u << bitOffset);
  269. }
  270. }
  271. }
  272. }
  273. public override string ToString()
  274. {
  275. unsafe
  276. {
  277. Debug.Assert(bitCapacity < 4096, "Bit string too long! It was truncated!");
  278. int len = Math.Min(bitCapacity, 4096);
  279. byte* buf = stackalloc byte[len];
  280. for (int i = 0; i < len; i++)
  281. {
  282. buf[i] = (byte)(this[i] ? '1' : '0');
  283. }
  284. return new string((sbyte*)buf, 0, len, System.Text.Encoding.UTF8);
  285. }
  286. }
  287. }
  288. /// Must match light data layout.
  289. private class LightCookieShaderData : IDisposable
  290. {
  291. int m_Size = 0;
  292. bool m_UseStructuredBuffer;
  293. // Shader data CPU arrays, used to upload the data to GPU
  294. Matrix4x4[] m_WorldToLightCpuData;
  295. Vector4[] m_AtlasUVRectCpuData;
  296. float[] m_LightTypeCpuData;
  297. ShaderBitArray m_CookieEnableBitsCpuData;
  298. // Compute buffer counterparts for the CPU data
  299. ComputeBuffer m_WorldToLightBuffer; // TODO: WorldToLight matrices should be general property of lights!!
  300. ComputeBuffer m_AtlasUVRectBuffer;
  301. ComputeBuffer m_LightTypeBuffer;
  302. public Matrix4x4[] worldToLights => m_WorldToLightCpuData;
  303. public ShaderBitArray cookieEnableBits => m_CookieEnableBitsCpuData;
  304. public Vector4[] atlasUVRects => m_AtlasUVRectCpuData;
  305. public float[] lightTypes => m_LightTypeCpuData;
  306. public bool isUploaded { get; set; }
  307. public LightCookieShaderData(int size, bool useStructuredBuffer)
  308. {
  309. m_UseStructuredBuffer = useStructuredBuffer;
  310. Resize(size);
  311. }
  312. public void Dispose()
  313. {
  314. if (m_UseStructuredBuffer)
  315. {
  316. m_WorldToLightBuffer?.Dispose();
  317. m_AtlasUVRectBuffer?.Dispose();
  318. m_LightTypeBuffer?.Dispose();
  319. }
  320. }
  321. public void Resize(int size)
  322. {
  323. if (size <= m_Size)
  324. return;
  325. if (m_Size > 0)
  326. Dispose();
  327. m_WorldToLightCpuData = new Matrix4x4[size];
  328. m_AtlasUVRectCpuData = new Vector4[size];
  329. m_LightTypeCpuData = new float[size];
  330. m_CookieEnableBitsCpuData.Resize(size);
  331. if (m_UseStructuredBuffer)
  332. {
  333. m_WorldToLightBuffer = new ComputeBuffer(size, Marshal.SizeOf<Matrix4x4>());
  334. m_AtlasUVRectBuffer = new ComputeBuffer(size, Marshal.SizeOf<Vector4>());
  335. m_LightTypeBuffer = new ComputeBuffer(size, Marshal.SizeOf<float>());
  336. }
  337. m_Size = size;
  338. }
  339. public void Upload(CommandBuffer cmd)
  340. {
  341. if (m_UseStructuredBuffer)
  342. {
  343. m_WorldToLightBuffer.SetData(m_WorldToLightCpuData);
  344. m_AtlasUVRectBuffer.SetData(m_AtlasUVRectCpuData);
  345. m_LightTypeBuffer.SetData(m_LightTypeCpuData);
  346. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsWorldToLightBuffer, m_WorldToLightBuffer);
  347. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsCookieAtlasUVRectBuffer, m_AtlasUVRectBuffer);
  348. cmd.SetGlobalBuffer(ShaderProperty.additionalLightsLightTypeBuffer, m_LightTypeBuffer);
  349. }
  350. else
  351. {
  352. cmd.SetGlobalMatrixArray(ShaderProperty.additionalLightsWorldToLights, m_WorldToLightCpuData);
  353. cmd.SetGlobalVectorArray(ShaderProperty.additionalLightsCookieAtlasUVRects, m_AtlasUVRectCpuData);
  354. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsLightTypes, m_LightTypeCpuData);
  355. }
  356. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
  357. isUploaded = true;
  358. }
  359. public void Clear(CommandBuffer cmd)
  360. {
  361. if (isUploaded)
  362. {
  363. // Set all lights to disabled/invalid state
  364. m_CookieEnableBitsCpuData.Clear();
  365. cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
  366. isUploaded = false;
  367. }
  368. }
  369. }
  370. // Unity defines directional light UVs over a unit box centered at light.
  371. // i.e. (0, 1) uv == (-0.5, 0.5) world area instead of the (0,1) world area.
  372. static readonly Matrix4x4 s_DirLightProj = Matrix4x4.Ortho(-0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f);
  373. Texture2DAtlas m_AdditionalLightsCookieAtlas;
  374. LightCookieShaderData m_AdditionalLightsCookieShaderData;
  375. readonly Settings m_Settings;
  376. WorkMemory m_WorkMem;
  377. // Mapping: map[visibleLightIndex] = ShaderDataIndex
  378. // Mostly used by deferred rendering.
  379. int[] m_VisibleLightIndexToShaderDataIndex;
  380. // Parameters for rescaling cookies to fit into the atlas.
  381. const int k_MaxCookieSizeDivisor = 16;
  382. int m_CookieSizeDivisor = 1;
  383. uint m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
  384. internal bool IsKeywordLightCookieEnabled { get; private set; }
  385. public LightCookieManager(ref Settings settings)
  386. {
  387. m_Settings = settings;
  388. m_WorkMem = new WorkMemory();
  389. }
  390. void InitAdditionalLights(int size)
  391. {
  392. if (m_Settings.atlas.useMips && m_Settings.atlas.isPow2)
  393. {
  394. // TODO: MipMaps still have sampling artifacts. FIX FIX
  395. // Supports mip padding for correct filtering at the edges.
  396. m_AdditionalLightsCookieAtlas = new PowerOfTwoTextureAtlas(
  397. m_Settings.atlas.resolution.x,
  398. 4,
  399. m_Settings.atlas.format,
  400. FilterMode.Bilinear,
  401. "Universal Light Cookie Pow2 Atlas",
  402. true);
  403. }
  404. else
  405. {
  406. // No mip padding support.
  407. m_AdditionalLightsCookieAtlas = new Texture2DAtlas(
  408. m_Settings.atlas.resolution.x,
  409. m_Settings.atlas.resolution.y,
  410. m_Settings.atlas.format,
  411. FilterMode.Bilinear,
  412. false,
  413. "Universal Light Cookie Atlas",
  414. false); // to support mips, use Pow2Atlas
  415. }
  416. m_AdditionalLightsCookieShaderData = new LightCookieShaderData(size, m_Settings.useStructuredBuffer);
  417. const int mainLightCount = 1;
  418. m_VisibleLightIndexToShaderDataIndex = new int[m_Settings.maxAdditionalLights + mainLightCount];
  419. m_CookieSizeDivisor = 1;
  420. m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
  421. }
  422. public bool isInitialized() => m_AdditionalLightsCookieAtlas != null && m_AdditionalLightsCookieShaderData != null;
  423. /// <summary>
  424. /// Release LightCookieManager resources.
  425. /// </summary>
  426. public void Dispose()
  427. {
  428. m_AdditionalLightsCookieAtlas?.Release();
  429. m_AdditionalLightsCookieShaderData?.Dispose();
  430. }
  431. // -1 on invalid/disabled cookie.
  432. public int GetLightCookieShaderDataIndex(int visibleLightIndex)
  433. {
  434. if (!isInitialized())
  435. return -1;
  436. return m_VisibleLightIndexToShaderDataIndex[visibleLightIndex];
  437. }
  438. public void Setup(ScriptableRenderContext ctx, CommandBuffer cmd, ref LightData lightData)
  439. {
  440. using var profScope = new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.LightCookies));
  441. // Main light, 1 directional, bound directly
  442. bool isMainLightAvailable = lightData.mainLightIndex >= 0;
  443. if (isMainLightAvailable)
  444. {
  445. var mainLight = lightData.visibleLights[lightData.mainLightIndex];
  446. isMainLightAvailable = SetupMainLight(cmd, ref mainLight);
  447. }
  448. // Additional lights, N spot and point lights in atlas
  449. bool isAdditionalLightsAvailable = lightData.additionalLightsCount > 0;
  450. if (isAdditionalLightsAvailable)
  451. {
  452. isAdditionalLightsAvailable = SetupAdditionalLights(cmd, ref lightData);
  453. }
  454. // Ensure cookies are disabled if no cookies are available.
  455. if (!isAdditionalLightsAvailable)
  456. {
  457. // ..on the CPU (for deferred)
  458. if (m_VisibleLightIndexToShaderDataIndex != null &&
  459. m_AdditionalLightsCookieShaderData.isUploaded)
  460. {
  461. int len = Math.Min(m_VisibleLightIndexToShaderDataIndex.Length, lightData.visibleLights.Length);
  462. for (int i = 0; i < len; i++)
  463. m_VisibleLightIndexToShaderDataIndex[i] = -1;
  464. }
  465. // ..on the GPU
  466. m_AdditionalLightsCookieShaderData?.Clear(cmd);
  467. }
  468. // Main and additional lights are merged into one keyword to reduce variants.
  469. IsKeywordLightCookieEnabled = isMainLightAvailable || isAdditionalLightsAvailable;
  470. CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.LightCookies, IsKeywordLightCookieEnabled);
  471. }
  472. bool SetupMainLight(CommandBuffer cmd, ref VisibleLight visibleMainLight)
  473. {
  474. var mainLight = visibleMainLight.light;
  475. var cookieTexture = mainLight.cookie;
  476. bool isMainLightCookieEnabled = cookieTexture != null;
  477. if (isMainLightCookieEnabled)
  478. {
  479. Matrix4x4 cookieUVTransform = Matrix4x4.identity;
  480. float cookieFormat = (float)GetLightCookieShaderFormat(cookieTexture.graphicsFormat);
  481. if (mainLight.TryGetComponent(out UniversalAdditionalLightData additionalLightData))
  482. GetLightUVScaleOffset(ref additionalLightData, ref cookieUVTransform);
  483. Matrix4x4 cookieMatrix = s_DirLightProj * cookieUVTransform *
  484. visibleMainLight.localToWorldMatrix.inverse;
  485. cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, cookieTexture);
  486. cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, cookieMatrix);
  487. cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, cookieFormat);
  488. }
  489. else
  490. {
  491. // Make sure we erase stale data in case the main light is disabled but cookie system is enabled (for additional lights).
  492. cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, Texture2D.whiteTexture);
  493. cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, Matrix4x4.identity);
  494. cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, (float)LightCookieShaderFormat.None);
  495. }
  496. return isMainLightCookieEnabled;
  497. }
  498. private LightCookieShaderFormat GetLightCookieShaderFormat(GraphicsFormat cookieFormat)
  499. {
  500. // TODO: convert this to use GraphicsFormatUtility
  501. switch (cookieFormat)
  502. {
  503. default:
  504. return LightCookieShaderFormat.RGB;
  505. // A8, A16 GraphicsFormat does not expose yet.
  506. case (GraphicsFormat)54:
  507. case (GraphicsFormat)55:
  508. return LightCookieShaderFormat.Alpha;
  509. case GraphicsFormat.R8_SRGB:
  510. case GraphicsFormat.R8_UNorm:
  511. case GraphicsFormat.R8_UInt:
  512. case GraphicsFormat.R8_SNorm:
  513. case GraphicsFormat.R8_SInt:
  514. case GraphicsFormat.R16_UNorm:
  515. case GraphicsFormat.R16_UInt:
  516. case GraphicsFormat.R16_SNorm:
  517. case GraphicsFormat.R16_SInt:
  518. case GraphicsFormat.R16_SFloat:
  519. case GraphicsFormat.R32_UInt:
  520. case GraphicsFormat.R32_SInt:
  521. case GraphicsFormat.R32_SFloat:
  522. case GraphicsFormat.R_BC4_SNorm:
  523. case GraphicsFormat.R_BC4_UNorm:
  524. case GraphicsFormat.R_EAC_SNorm:
  525. case GraphicsFormat.R_EAC_UNorm:
  526. return LightCookieShaderFormat.Red;
  527. }
  528. }
  529. private void GetLightUVScaleOffset(ref UniversalAdditionalLightData additionalLightData, ref Matrix4x4 uvTransform)
  530. {
  531. Vector2 uvScale = Vector2.one / additionalLightData.lightCookieSize;
  532. Vector2 uvOffset = additionalLightData.lightCookieOffset;
  533. if (Mathf.Abs(uvScale.x) < half.MinValue)
  534. uvScale.x = Mathf.Sign(uvScale.x) * half.MinValue;
  535. if (Mathf.Abs(uvScale.y) < half.MinValue)
  536. uvScale.y = Mathf.Sign(uvScale.y) * half.MinValue;
  537. uvTransform = Matrix4x4.Scale(new Vector3(uvScale.x, uvScale.y, 1));
  538. uvTransform.SetColumn(3, new Vector4(-uvOffset.x * uvScale.x, -uvOffset.y * uvScale.y, 0, 1));
  539. }
  540. bool SetupAdditionalLights(CommandBuffer cmd, ref LightData lightData)
  541. {
  542. int maxLightCount = Math.Min(m_Settings.maxAdditionalLights, lightData.visibleLights.Length);
  543. m_WorkMem.Resize(maxLightCount);
  544. int validLightCount = FilterAndValidateAdditionalLights(ref lightData, m_WorkMem.lightMappings);
  545. // Early exit if no valid cookie lights
  546. if (validLightCount <= 0)
  547. return false;
  548. // Lazy init GPU resources
  549. if (!isInitialized())
  550. InitAdditionalLights(validLightCount);
  551. // Update Atlas
  552. var validLights = new WorkSlice<LightCookieMapping>(m_WorkMem.lightMappings, validLightCount);
  553. int validUVRectCount = UpdateAdditionalLightsAtlas(cmd, ref validLights, m_WorkMem.uvRects);
  554. // Upload shader data
  555. var validUvRects = new WorkSlice<Vector4>(m_WorkMem.uvRects, validUVRectCount);
  556. UploadAdditionalLights(cmd, ref lightData, ref validLights, ref validUvRects);
  557. bool isAdditionalLightsEnabled = validUvRects.length > 0;
  558. return isAdditionalLightsEnabled;
  559. }
  560. int FilterAndValidateAdditionalLights(ref LightData lightData, LightCookieMapping[] validLightMappings)
  561. {
  562. int skipMainLightIndex = lightData.mainLightIndex;
  563. int lightBufferOffset = 0;
  564. int validLightCount = 0;
  565. // Warn on dropped lights
  566. int maxLights = Math.Min(lightData.visibleLights.Length, validLightMappings.Length);
  567. for (int i = 0; i < maxLights; i++)
  568. {
  569. if (i == skipMainLightIndex)
  570. {
  571. lightBufferOffset -= 1;
  572. continue;
  573. }
  574. Light light = lightData.visibleLights[i].light;
  575. // Skip lights without a cookie texture
  576. if (light.cookie == null)
  577. continue;
  578. // Only spot and point lights are supported.
  579. // Directional lights basically work,
  580. // but would require a lot of constants for the uv transform parameters
  581. // and there are very few use cases for multiple global cookies.
  582. var lightType = lightData.visibleLights[i].lightType;
  583. if (!(lightType == LightType.Spot ||
  584. lightType == LightType.Point))
  585. {
  586. Debug.LogWarning($"Additional {lightType.ToString()} light called '{light.name}' has a light cookie which will not be visible.", light);
  587. continue;
  588. }
  589. Assertions.Assert.IsTrue(i < ushort.MaxValue);
  590. LightCookieMapping lp;
  591. lp.visibleLightIndex = (ushort)i;
  592. lp.lightBufferIndex = (ushort)(i + lightBufferOffset);
  593. lp.light = light;
  594. validLightMappings[validLightCount++] = lp;
  595. }
  596. return validLightCount;
  597. }
  598. int UpdateAdditionalLightsAtlas(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects)
  599. {
  600. // Sort in-place by cookie size for better atlas allocation efficiency (and deduplication)
  601. validLightMappings.Sort(LightCookieMapping.s_CompareByCookieSize);
  602. uint cookieRequestPixelCount = ComputeCookieRequestPixelCount(ref validLightMappings);
  603. var atlasSize = m_AdditionalLightsCookieAtlas.AtlasTexture.referenceSize;
  604. float requestAtlasRatio = cookieRequestPixelCount / (float)(atlasSize.x * atlasSize.y);
  605. int cookieSizeDivisorApprox = ApproximateCookieSizeDivisor(requestAtlasRatio);
  606. // Try to recover resolution and scale the cookies back up.
  607. // If the cookies "should fit" and
  608. // If we have less requested pixels than the last time we found the correct divisor (a guard against retrying every frame).
  609. if (cookieSizeDivisorApprox < m_CookieSizeDivisor &&
  610. cookieRequestPixelCount < m_PrevCookieRequestPixelCount)
  611. {
  612. m_AdditionalLightsCookieAtlas.ResetAllocator();
  613. m_CookieSizeDivisor = cookieSizeDivisorApprox;
  614. }
  615. // Get cached atlas uv rectangles.
  616. // If there's new cookies, first try to add at current scaling level.
  617. // (This can result in suboptimal packing & scaling (additions aren't sorted), but reduces rebuilds.)
  618. // If it doesn't fit, scale down and rebuild the atlas until it fits.
  619. int uvRectCount = 0;
  620. while (uvRectCount <= 0)
  621. {
  622. uvRectCount = FetchUVRects(cmd, ref validLightMappings, textureAtlasUVRects, m_CookieSizeDivisor);
  623. if (uvRectCount <= 0)
  624. {
  625. // Uv rect fetching failed, reset and try again.
  626. m_AdditionalLightsCookieAtlas.ResetAllocator();
  627. // Reduce cookie size to approximate value try to rebuild the atlas.
  628. m_CookieSizeDivisor = Mathf.Max(m_CookieSizeDivisor + 1, cookieSizeDivisorApprox);
  629. m_PrevCookieRequestPixelCount = cookieRequestPixelCount;
  630. }
  631. }
  632. return uvRectCount;
  633. }
  634. int FetchUVRects(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects, int cookieSizeDivisor)
  635. {
  636. int uvRectCount = 0;
  637. for (int i = 0; i < validLightMappings.length; i++)
  638. {
  639. var lcm = validLightMappings[i];
  640. Light light = lcm.light;
  641. Texture cookie = light.cookie;
  642. // NOTE: Currently we blit directly on addition (on atlas fetch cache miss).
  643. // This can be costly if there are many resize rebuilds (in case "out-of-space", which shouldn't be a common case).
  644. // If rebuilds become a problem, we could try to just allocate and blit only when we have a fully valid allocation.
  645. // It would also make sense to do atlas operations only for unique textures and then reuse the results for similar cookies.
  646. Vector4 uvScaleOffset = Vector4.zero;
  647. if (cookie.dimension == TextureDimension.Cube)
  648. {
  649. Assertions.Assert.IsTrue(light.type == LightType.Point);
  650. uvScaleOffset = FetchCube(cmd, cookie, cookieSizeDivisor);
  651. }
  652. else
  653. {
  654. Assertions.Assert.IsTrue(light.type == LightType.Spot || light.type == LightType.Directional, "Light type needs 2D texture!");
  655. uvScaleOffset = Fetch2D(cmd, cookie, cookieSizeDivisor);
  656. }
  657. bool isCached = uvScaleOffset != Vector4.zero;
  658. if (!isCached)
  659. {
  660. if (cookieSizeDivisor > k_MaxCookieSizeDivisor)
  661. {
  662. 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.");
  663. // Complete fail, return what we have.
  664. return uvRectCount;
  665. }
  666. // Failed to get uv rect for each cookie, fail and try again.
  667. return 0;
  668. }
  669. // Adjust atlas UVs for OpenGL
  670. if (!SystemInfo.graphicsUVStartsAtTop)
  671. uvScaleOffset.w = 1.0f - uvScaleOffset.w - uvScaleOffset.y;
  672. textureAtlasUVRects[uvRectCount++] = uvScaleOffset;
  673. }
  674. return uvRectCount;
  675. }
  676. uint ComputeCookieRequestPixelCount(ref WorkSlice<LightCookieMapping> validLightMappings)
  677. {
  678. uint requestPixelCount = 0;
  679. int prevCookieID = 0;
  680. for (int i = 0; i < validLightMappings.length; i++)
  681. {
  682. var lcm = validLightMappings[i];
  683. Texture cookie = lcm.light.cookie;
  684. int cookieID = cookie.GetInstanceID();
  685. // Consider only unique textures as atlas request pixels
  686. // NOTE: relies on same cookies being sorted together
  687. // (we need sorting for good atlas packing anyway)
  688. if (cookieID == prevCookieID)
  689. {
  690. continue;
  691. }
  692. prevCookieID = cookieID;
  693. int pixelCookieCount = cookie.width * cookie.height;
  694. requestPixelCount += (uint)pixelCookieCount;
  695. }
  696. return requestPixelCount;
  697. }
  698. int ApproximateCookieSizeDivisor(float requestAtlasRatio)
  699. {
  700. // (Edge / N)^2 == 1/N^2 of area.
  701. // Ratio/N^2 == 1, sqrt(Ratio) == N, for "1:1" ratio.
  702. return (int)Mathf.Max(Mathf.Ceil(Mathf.Sqrt(requestAtlasRatio)), 1);
  703. }
  704. Vector4 Fetch2D(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
  705. {
  706. Assertions.Assert.IsTrue(cookie != null);
  707. Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Tex2D);
  708. Vector4 uvScaleOffset = Vector4.zero;
  709. var scaledWidth = Mathf.Max(cookie.width / cookieSizeDivisor, 4);
  710. var scaledHeight = Mathf.Max(cookie.height / cookieSizeDivisor, 4);
  711. Vector2 scaledCookieSize = new Vector2(scaledWidth, scaledHeight);
  712. bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
  713. if (isCached)
  714. {
  715. // Update contents IF required
  716. m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
  717. }
  718. else
  719. {
  720. m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledWidth, scaledHeight);
  721. }
  722. AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
  723. return uvScaleOffset;
  724. }
  725. Vector4 FetchCube(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
  726. {
  727. Assertions.Assert.IsTrue(cookie != null);
  728. Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Cube);
  729. Vector4 uvScaleOffset = Vector4.zero;
  730. // Scale octahedral projection, so that cube -> oct2D pixel count match better.
  731. int scaledOctCookieSize = Mathf.Max(ComputeOctahedralCookieSize(cookie) / cookieSizeDivisor, 4);
  732. bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
  733. if (isCached)
  734. {
  735. // Update contents IF required
  736. m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
  737. }
  738. else
  739. {
  740. m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledOctCookieSize, scaledOctCookieSize);
  741. }
  742. // Cookie size in the atlas might not match CookieTexture size.
  743. // UVRect adjustment must be done with size in atlas.
  744. var scaledCookieSize = Vector2.one * scaledOctCookieSize;
  745. AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
  746. return uvScaleOffset;
  747. }
  748. int ComputeOctahedralCookieSize(Texture cookie)
  749. {
  750. // Map 6*WxH pixels into 2W*2H pixels, so 4/6 ratio or 66% of cube pixels.
  751. int octCookieSize = Math.Max(cookie.width, cookie.height);
  752. if (m_Settings.atlas.isPow2)
  753. octCookieSize = octCookieSize * Mathf.NextPowerOfTwo((int)m_Settings.cubeOctahedralSizeScale);
  754. else
  755. octCookieSize = (int)(octCookieSize * m_Settings.cubeOctahedralSizeScale + 0.5f);
  756. return octCookieSize;
  757. }
  758. private void AdjustUVRect(ref Vector4 uvScaleOffset, Texture cookie, ref Vector2 cookieSize)
  759. {
  760. if (uvScaleOffset != Vector4.zero)
  761. {
  762. if (m_Settings.atlas.useMips)
  763. {
  764. // Payload texture is inset
  765. var potAtlas = (m_AdditionalLightsCookieAtlas as PowerOfTwoTextureAtlas);
  766. var mipPadding = potAtlas == null ? 1 : potAtlas.mipPadding;
  767. var paddingSize = Vector2.one * (int)Mathf.Pow(2, mipPadding) * 2;
  768. uvScaleOffset = PowerOfTwoTextureAtlas.GetPayloadScaleOffset(cookieSize, paddingSize, uvScaleOffset);
  769. }
  770. else
  771. {
  772. // Shrink by 0.5px to clamp sampling atlas neighbors (no padding)
  773. ShrinkUVRect(ref uvScaleOffset, 0.5f, ref cookieSize);
  774. }
  775. }
  776. }
  777. private void ShrinkUVRect(ref Vector4 uvScaleOffset, float amountPixels, ref Vector2 cookieSize)
  778. {
  779. var shrinkOffset = Vector2.one * amountPixels / cookieSize;
  780. var shrinkScale = (cookieSize - Vector2.one * (amountPixels * 2)) / cookieSize;
  781. uvScaleOffset.z += uvScaleOffset.x * shrinkOffset.x;
  782. uvScaleOffset.w += uvScaleOffset.y * shrinkOffset.y;
  783. uvScaleOffset.x *= shrinkScale.x;
  784. uvScaleOffset.y *= shrinkScale.y;
  785. }
  786. void UploadAdditionalLights(CommandBuffer cmd, ref LightData lightData, ref WorkSlice<LightCookieMapping> validLightMappings, ref WorkSlice<Vector4> validUvRects)
  787. {
  788. Assertions.Assert.IsTrue(m_AdditionalLightsCookieAtlas != null);
  789. Assertions.Assert.IsTrue(m_AdditionalLightsCookieShaderData != null);
  790. cmd.SetGlobalTexture(ShaderProperty.additionalLightsCookieAtlasTexture, m_AdditionalLightsCookieAtlas.AtlasTexture);
  791. cmd.SetGlobalFloat(ShaderProperty.additionalLightsCookieAtlasTextureFormat, (float)GetLightCookieShaderFormat(m_AdditionalLightsCookieAtlas.AtlasTexture.rt.graphicsFormat));
  792. // Resize and clear visible light to shader data mapping
  793. if (m_VisibleLightIndexToShaderDataIndex.Length < lightData.visibleLights.Length)
  794. m_VisibleLightIndexToShaderDataIndex = new int[lightData.visibleLights.Length];
  795. // Clear
  796. int len = Math.Min(m_VisibleLightIndexToShaderDataIndex.Length, lightData.visibleLights.Length);
  797. for (int i = 0; i < len; i++)
  798. m_VisibleLightIndexToShaderDataIndex[i] = -1;
  799. // Resize or init shader data.
  800. m_AdditionalLightsCookieShaderData.Resize(m_Settings.maxAdditionalLights);
  801. var worldToLights = m_AdditionalLightsCookieShaderData.worldToLights;
  802. var cookieEnableBits = m_AdditionalLightsCookieShaderData.cookieEnableBits;
  803. var atlasUVRects = m_AdditionalLightsCookieShaderData.atlasUVRects;
  804. var lightTypes = m_AdditionalLightsCookieShaderData.lightTypes;
  805. // Set all rects to "Invalid" zero area (Vector4.zero), just in case they're accessed.
  806. Array.Clear(atlasUVRects, 0, atlasUVRects.Length);
  807. // Set all cookies disabled
  808. cookieEnableBits.Clear();
  809. // NOTE: technically, we don't need to upload constants again if we knew the lights, atlas (rects) or visible order haven't changed.
  810. // But detecting that, might be as time consuming as just doing the work.
  811. // Fill shader data. Layout should match primary light data for additional lights.
  812. // Currently it's the same as visible lights, but main light(s) dropped.
  813. for (int i = 0; i < validUvRects.length; i++)
  814. {
  815. int visIndex = validLightMappings[i].visibleLightIndex;
  816. int bufIndex = validLightMappings[i].lightBufferIndex;
  817. // Update the mapping
  818. m_VisibleLightIndexToShaderDataIndex[visIndex] = bufIndex;
  819. var visLight = lightData.visibleLights[visIndex];
  820. // Update the (cpu) data
  821. lightTypes[bufIndex] = (int)visLight.lightType;
  822. worldToLights[bufIndex] = visLight.localToWorldMatrix.inverse;
  823. atlasUVRects[bufIndex] = validUvRects[i];
  824. cookieEnableBits[bufIndex] = true;
  825. // Spot projection
  826. if (visLight.lightType == LightType.Spot)
  827. {
  828. // VisibleLight.localToWorldMatrix only contains position & rotation.
  829. // Multiply projection for spot light.
  830. float spotAngle = visLight.spotAngle;
  831. float spotRange = visLight.range;
  832. var perp = Matrix4x4.Perspective(spotAngle, 1, 0.001f, spotRange);
  833. // Cancel embedded camera view axis flip (https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Matrix4x4.Perspective.html)
  834. perp.SetColumn(2, perp.GetColumn(2) * -1);
  835. // world -> light local -> light perspective
  836. worldToLights[bufIndex] = perp * worldToLights[bufIndex];
  837. }
  838. }
  839. // Apply changes and upload to GPU
  840. m_AdditionalLightsCookieShaderData.Upload(cmd);
  841. }
  842. }
  843. }