123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011 |
- using System;
- using System.Runtime.InteropServices;
- using UnityEngine.Experimental.Rendering;
- using Unity.Mathematics;
- namespace UnityEngine.Rendering.Universal
- {
- internal class LightCookieManager : IDisposable
- {
- static class ShaderProperty
- {
- public static readonly int mainLightTexture = Shader.PropertyToID("_MainLightCookieTexture");
- public static readonly int mainLightWorldToLight = Shader.PropertyToID("_MainLightWorldToLight");
- public static readonly int mainLightCookieTextureFormat = Shader.PropertyToID("_MainLightCookieTextureFormat");
- public static readonly int additionalLightsCookieAtlasTexture = Shader.PropertyToID("_AdditionalLightsCookieAtlasTexture");
- public static readonly int additionalLightsCookieAtlasTextureFormat = Shader.PropertyToID("_AdditionalLightsCookieAtlasTextureFormat");
- public static readonly int additionalLightsCookieEnableBits = Shader.PropertyToID("_AdditionalLightsCookieEnableBits");
- public static readonly int additionalLightsCookieAtlasUVRectBuffer = Shader.PropertyToID("_AdditionalLightsCookieAtlasUVRectBuffer");
- public static readonly int additionalLightsCookieAtlasUVRects = Shader.PropertyToID("_AdditionalLightsCookieAtlasUVRects");
- // TODO: these should be generic light property
- public static readonly int additionalLightsWorldToLightBuffer = Shader.PropertyToID("_AdditionalLightsWorldToLightBuffer");
- public static readonly int additionalLightsLightTypeBuffer = Shader.PropertyToID("_AdditionalLightsLightTypeBuffer");
- public static readonly int additionalLightsWorldToLights = Shader.PropertyToID("_AdditionalLightsWorldToLights");
- public static readonly int additionalLightsLightTypes = Shader.PropertyToID("_AdditionalLightsLightTypes");
- }
- private enum LightCookieShaderFormat
- {
- None = -1,
- RGB = 0,
- Alpha = 1,
- Red = 2
- }
- public struct Settings
- {
- public struct AtlasSettings
- {
- public Vector2Int resolution;
- public GraphicsFormat format;
- public bool useMips;
- public bool isPow2 => Mathf.IsPowerOfTwo(resolution.x) && Mathf.IsPowerOfTwo(resolution.y);
- public bool isSquare => resolution.x == resolution.y;
- }
- public AtlasSettings atlas;
- public int maxAdditionalLights; // UniversalRenderPipeline.maxVisibleAdditionalLights;
- public float cubeOctahedralSizeScale; // Cube octahedral projection size scale.
- public bool useStructuredBuffer; // RenderingUtils.useStructuredBuffer
- public static Settings GetDefault()
- {
- Settings s;
- s.atlas.resolution = new Vector2Int(1024, 1024);
- s.atlas.format = GraphicsFormat.R8G8B8A8_SRGB;
- s.atlas.useMips = false; // TODO: set to true, make sure they work proper first! Disable them for now...
- s.maxAdditionalLights = UniversalRenderPipeline.maxVisibleAdditionalLights;
- // (Scale * W * Scale * H) / (6 * WH) == (Scale^2 / 6)
- // 1: 1/6 = 16%, 2: 4/6 = 66%, 4: 16/6 == 266% of cube pixels
- // 100% cube pixels == sqrt(6) ~= 2.45f --> 2.5;
- s.cubeOctahedralSizeScale = s.atlas.useMips && s.atlas.isPow2 ? 2.0f : 2.5f;
- s.useStructuredBuffer = RenderingUtils.useStructuredBuffer;
- return s;
- }
- }
- private struct Sorting
- {
- public static void QuickSort<T>(T[] data, Func<T, T, int> compare)
- {
- QuickSort<T>(data, 0, data.Length - 1, compare);
- }
- // A non-allocating predicated sub-array quick sort.
- // NOTE: Similar to UnityEngine.Rendering.CoreUnsafeUtils.QuickSort in CoreUnsafeUtils.cs,
- // we should see if these could be merged in the future.
- // For example: Sorting.QuickSort(test, 0, test.Length - 1, (int a, int b) => a - b);
- public static void QuickSort<T>(T[] data, int start, int end, Func<T, T, int> compare)
- {
- int diff = end - start;
- if (diff < 1)
- return;
- if (diff < 8)
- {
- InsertionSort(data, start, end, compare);
- return;
- }
- Assertions.Assert.IsTrue((uint)start < data.Length);
- Assertions.Assert.IsTrue((uint)end < data.Length); // end == inclusive
- if (start < end)
- {
- int pivot = Partition<T>(data, start, end, compare);
- if (pivot >= 1)
- QuickSort<T>(data, start, pivot, compare);
- if (pivot + 1 < end)
- QuickSort<T>(data, pivot + 1, end, compare);
- }
- }
- static T Median3Pivot<T>(T[] data, int start, int pivot, int end, Func<T, T, int> compare)
- {
- void Swap(int a, int b)
- {
- var tmp = data[a];
- data[a] = data[b];
- data[b] = tmp;
- }
- if (compare(data[end], data[start]) < 0) Swap(start, end);
- if (compare(data[pivot], data[start]) < 0) Swap(start, pivot);
- if (compare(data[end], data[pivot]) < 0) Swap(pivot, end);
- return data[pivot];
- }
- static int Partition<T>(T[] data, int start, int end, Func<T, T, int> compare)
- {
- int diff = end - start;
- int pivot = start + diff / 2;
- var pivotValue = Median3Pivot(data, start, pivot, end, compare);
- while (true)
- {
- while (compare(data[start], pivotValue) < 0) ++start;
- while (compare(data[end], pivotValue) > 0) --end;
- if (start >= end)
- {
- return end;
- }
- var tmp = data[start];
- data[start++] = data[end];
- data[end--] = tmp;
- }
- }
- // A non-allocating predicated sub-array insertion sort.
- static public void InsertionSort<T>(T[] data, int start, int end, Func<T, T, int> compare)
- {
- Assertions.Assert.IsTrue((uint)start < data.Length);
- Assertions.Assert.IsTrue((uint)end < data.Length);
- for (int i = start + 1; i < end + 1; i++)
- {
- var iData = data[i];
- int j = i - 1;
- while (j >= 0 && compare(iData, data[j]) < 0)
- {
- data[j + 1] = data[j];
- j--;
- }
- data[j + 1] = iData;
- }
- }
- }
- private struct LightCookieMapping
- {
- public ushort visibleLightIndex; // Index into visible light (src)
- public ushort lightBufferIndex; // Index into light shader data buffer (dst)
- public Light light; // Cached built-in light for the visibleLightIndex. Avoids multiple copies on all the gets from native array.
- public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByCookieSize = (LightCookieMapping a, LightCookieMapping b) =>
- {
- var alc = a.light.cookie;
- var blc = b.light.cookie;
- int a2 = alc.width * alc.height;
- int b2 = blc.width * blc.height;
- int d = b2 - a2;
- if (d == 0)
- {
- // Sort by texture ID if "undecided" to batch fetches to the same cookie texture.
- int ai = alc.GetInstanceID();
- int bi = blc.GetInstanceID();
- return ai - bi;
- }
- return d;
- };
- public static Func<LightCookieMapping, LightCookieMapping, int> s_CompareByBufferIndex = (LightCookieMapping a, LightCookieMapping b) =>
- {
- return a.lightBufferIndex - b.lightBufferIndex;
- };
- }
- private readonly struct WorkSlice<T>
- {
- private readonly T[] m_Data;
- private readonly int m_Start;
- private readonly int m_Length;
- public WorkSlice(T[] src, int srcLen = -1) : this(src, 0, srcLen) { }
- public WorkSlice(T[] src, int srcStart, int srcLen = -1)
- {
- m_Data = src;
- m_Start = srcStart;
- m_Length = (srcLen < 0) ? src.Length : Math.Min(srcLen, src.Length);
- Assertions.Assert.IsTrue(m_Start + m_Length <= capacity);
- }
- public T this[int index]
- {
- get => m_Data[m_Start + index];
- set => m_Data[m_Start + index] = value;
- }
- public int length => m_Length;
- public int capacity => m_Data.Length;
- public void Sort(Func<T, T, int> compare)
- {
- if (m_Length > 1)
- Sorting.QuickSort(m_Data, m_Start, m_Start + m_Length - 1, compare);
- }
- }
- // Persistent work/temp memory of [] data.
- private class WorkMemory
- {
- public LightCookieMapping[] lightMappings;
- public Vector4[] uvRects;
- public void Resize(int size)
- {
- if (size <= lightMappings?.Length)
- return;
- // Avoid allocs on every tiny size change.
- size = Math.Max(size, ((size + 15) / 16) * 16);
- lightMappings = new LightCookieMapping[size];
- uvRects = new Vector4[size];
- }
- }
- private struct ShaderBitArray
- {
- const int k_BitsPerElement = 32;
- const int k_ElementShift = 5;
- const int k_ElementMask = (1 << k_ElementShift) - 1;
- private float[] m_Data;
- public int elemLength => m_Data == null ? 0 : m_Data.Length;
- public int bitCapacity => elemLength * k_BitsPerElement;
- public float[] data => m_Data;
- public void Resize(int bitCount)
- {
- if (bitCapacity > bitCount)
- return;
- int newElemCount = ((bitCount + (k_BitsPerElement - 1)) / k_BitsPerElement);
- if (newElemCount == m_Data?.Length)
- return;
- var newData = new float[newElemCount];
- if (m_Data != null)
- {
- for (int i = 0; i < m_Data.Length; i++)
- newData[i] = m_Data[i];
- }
- m_Data = newData;
- }
- public void Clear()
- {
- for (int i = 0; i < m_Data.Length; i++)
- m_Data[i] = 0;
- }
- private void GetElementIndexAndBitOffset(int index, out int elemIndex, out int bitOffset)
- {
- elemIndex = index >> k_ElementShift;
- bitOffset = index & k_ElementMask;
- }
- public bool this[int index]
- {
- get
- {
- GetElementIndexAndBitOffset(index, out var elemIndex, out var bitOffset);
- unsafe
- {
- fixed (float* floatData = m_Data)
- {
- uint* uintElem = (uint*)&floatData[elemIndex];
- bool val = ((*uintElem) & (1u << bitOffset)) != 0u;
- return val;
- }
- }
- }
- set
- {
- GetElementIndexAndBitOffset(index, out var elemIndex, out var bitOffset);
- unsafe
- {
- fixed (float* floatData = m_Data)
- {
- uint* uintElem = (uint*)&floatData[elemIndex];
- if (value == true)
- *uintElem = (*uintElem) | (1u << bitOffset);
- else
- *uintElem = (*uintElem) & ~(1u << bitOffset);
- }
- }
- }
- }
- public override string ToString()
- {
- unsafe
- {
- Debug.Assert(bitCapacity < 4096, "Bit string too long! It was truncated!");
- int len = Math.Min(bitCapacity, 4096);
- byte* buf = stackalloc byte[len];
- for (int i = 0; i < len; i++)
- {
- buf[i] = (byte)(this[i] ? '1' : '0');
- }
- return new string((sbyte*)buf, 0, len, System.Text.Encoding.UTF8);
- }
- }
- }
- /// Must match light data layout.
- private class LightCookieShaderData : IDisposable
- {
- int m_Size = 0;
- bool m_UseStructuredBuffer;
- // Shader data CPU arrays, used to upload the data to GPU
- Matrix4x4[] m_WorldToLightCpuData;
- Vector4[] m_AtlasUVRectCpuData;
- float[] m_LightTypeCpuData;
- ShaderBitArray m_CookieEnableBitsCpuData;
- // Compute buffer counterparts for the CPU data
- ComputeBuffer m_WorldToLightBuffer; // TODO: WorldToLight matrices should be general property of lights!!
- ComputeBuffer m_AtlasUVRectBuffer;
- ComputeBuffer m_LightTypeBuffer;
- public Matrix4x4[] worldToLights => m_WorldToLightCpuData;
- public ShaderBitArray cookieEnableBits => m_CookieEnableBitsCpuData;
- public Vector4[] atlasUVRects => m_AtlasUVRectCpuData;
- public float[] lightTypes => m_LightTypeCpuData;
- public bool isUploaded { get; set; }
- public LightCookieShaderData(int size, bool useStructuredBuffer)
- {
- m_UseStructuredBuffer = useStructuredBuffer;
- Resize(size);
- }
- public void Dispose()
- {
- if (m_UseStructuredBuffer)
- {
- m_WorldToLightBuffer?.Dispose();
- m_AtlasUVRectBuffer?.Dispose();
- m_LightTypeBuffer?.Dispose();
- }
- }
- public void Resize(int size)
- {
- if (size <= m_Size)
- return;
- if (m_Size > 0)
- Dispose();
- m_WorldToLightCpuData = new Matrix4x4[size];
- m_AtlasUVRectCpuData = new Vector4[size];
- m_LightTypeCpuData = new float[size];
- m_CookieEnableBitsCpuData.Resize(size);
- if (m_UseStructuredBuffer)
- {
- m_WorldToLightBuffer = new ComputeBuffer(size, Marshal.SizeOf<Matrix4x4>());
- m_AtlasUVRectBuffer = new ComputeBuffer(size, Marshal.SizeOf<Vector4>());
- m_LightTypeBuffer = new ComputeBuffer(size, Marshal.SizeOf<float>());
- }
- m_Size = size;
- }
- public void Upload(CommandBuffer cmd)
- {
- if (m_UseStructuredBuffer)
- {
- m_WorldToLightBuffer.SetData(m_WorldToLightCpuData);
- m_AtlasUVRectBuffer.SetData(m_AtlasUVRectCpuData);
- m_LightTypeBuffer.SetData(m_LightTypeCpuData);
- cmd.SetGlobalBuffer(ShaderProperty.additionalLightsWorldToLightBuffer, m_WorldToLightBuffer);
- cmd.SetGlobalBuffer(ShaderProperty.additionalLightsCookieAtlasUVRectBuffer, m_AtlasUVRectBuffer);
- cmd.SetGlobalBuffer(ShaderProperty.additionalLightsLightTypeBuffer, m_LightTypeBuffer);
- }
- else
- {
- cmd.SetGlobalMatrixArray(ShaderProperty.additionalLightsWorldToLights, m_WorldToLightCpuData);
- cmd.SetGlobalVectorArray(ShaderProperty.additionalLightsCookieAtlasUVRects, m_AtlasUVRectCpuData);
- cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsLightTypes, m_LightTypeCpuData);
- }
- cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
- isUploaded = true;
- }
- public void Clear(CommandBuffer cmd)
- {
- if (isUploaded)
- {
- // Set all lights to disabled/invalid state
- m_CookieEnableBitsCpuData.Clear();
- cmd.SetGlobalFloatArray(ShaderProperty.additionalLightsCookieEnableBits, m_CookieEnableBitsCpuData.data);
- isUploaded = false;
- }
- }
- }
- // Unity defines directional light UVs over a unit box centered at light.
- // i.e. (0, 1) uv == (-0.5, 0.5) world area instead of the (0,1) world area.
- static readonly Matrix4x4 s_DirLightProj = Matrix4x4.Ortho(-0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f);
- Texture2DAtlas m_AdditionalLightsCookieAtlas;
- LightCookieShaderData m_AdditionalLightsCookieShaderData;
- readonly Settings m_Settings;
- WorkMemory m_WorkMem;
- // Mapping: map[visibleLightIndex] = ShaderDataIndex
- // Mostly used by deferred rendering.
- int[] m_VisibleLightIndexToShaderDataIndex;
- // Parameters for rescaling cookies to fit into the atlas.
- const int k_MaxCookieSizeDivisor = 16;
- int m_CookieSizeDivisor = 1;
- uint m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
- internal bool IsKeywordLightCookieEnabled { get; private set; }
- public LightCookieManager(ref Settings settings)
- {
- m_Settings = settings;
- m_WorkMem = new WorkMemory();
- }
- void InitAdditionalLights(int size)
- {
- if (m_Settings.atlas.useMips && m_Settings.atlas.isPow2)
- {
- // TODO: MipMaps still have sampling artifacts. FIX FIX
- // Supports mip padding for correct filtering at the edges.
- m_AdditionalLightsCookieAtlas = new PowerOfTwoTextureAtlas(
- m_Settings.atlas.resolution.x,
- 4,
- m_Settings.atlas.format,
- FilterMode.Bilinear,
- "Universal Light Cookie Pow2 Atlas",
- true);
- }
- else
- {
- // No mip padding support.
- m_AdditionalLightsCookieAtlas = new Texture2DAtlas(
- m_Settings.atlas.resolution.x,
- m_Settings.atlas.resolution.y,
- m_Settings.atlas.format,
- FilterMode.Bilinear,
- false,
- "Universal Light Cookie Atlas",
- false); // to support mips, use Pow2Atlas
- }
- m_AdditionalLightsCookieShaderData = new LightCookieShaderData(size, m_Settings.useStructuredBuffer);
- const int mainLightCount = 1;
- m_VisibleLightIndexToShaderDataIndex = new int[m_Settings.maxAdditionalLights + mainLightCount];
- m_CookieSizeDivisor = 1;
- m_PrevCookieRequestPixelCount = 0xFFFFFFFF;
- }
- public bool isInitialized() => m_AdditionalLightsCookieAtlas != null && m_AdditionalLightsCookieShaderData != null;
- /// <summary>
- /// Release LightCookieManager resources.
- /// </summary>
- public void Dispose()
- {
- m_AdditionalLightsCookieAtlas?.Release();
- m_AdditionalLightsCookieShaderData?.Dispose();
- }
- // -1 on invalid/disabled cookie.
- public int GetLightCookieShaderDataIndex(int visibleLightIndex)
- {
- if (!isInitialized())
- return -1;
- return m_VisibleLightIndexToShaderDataIndex[visibleLightIndex];
- }
- public void Setup(ScriptableRenderContext ctx, CommandBuffer cmd, ref LightData lightData)
- {
- using var profScope = new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.LightCookies));
- // Main light, 1 directional, bound directly
- bool isMainLightAvailable = lightData.mainLightIndex >= 0;
- if (isMainLightAvailable)
- {
- var mainLight = lightData.visibleLights[lightData.mainLightIndex];
- isMainLightAvailable = SetupMainLight(cmd, ref mainLight);
- }
- // Additional lights, N spot and point lights in atlas
- bool isAdditionalLightsAvailable = lightData.additionalLightsCount > 0;
- if (isAdditionalLightsAvailable)
- {
- isAdditionalLightsAvailable = SetupAdditionalLights(cmd, ref lightData);
- }
- // Ensure cookies are disabled if no cookies are available.
- if (!isAdditionalLightsAvailable)
- {
- // ..on the CPU (for deferred)
- if (m_VisibleLightIndexToShaderDataIndex != null &&
- m_AdditionalLightsCookieShaderData.isUploaded)
- {
- int len = Math.Min(m_VisibleLightIndexToShaderDataIndex.Length, lightData.visibleLights.Length);
- for (int i = 0; i < len; i++)
- m_VisibleLightIndexToShaderDataIndex[i] = -1;
- }
- // ..on the GPU
- m_AdditionalLightsCookieShaderData?.Clear(cmd);
- }
- // Main and additional lights are merged into one keyword to reduce variants.
- IsKeywordLightCookieEnabled = isMainLightAvailable || isAdditionalLightsAvailable;
- CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.LightCookies, IsKeywordLightCookieEnabled);
- }
- bool SetupMainLight(CommandBuffer cmd, ref VisibleLight visibleMainLight)
- {
- var mainLight = visibleMainLight.light;
- var cookieTexture = mainLight.cookie;
- bool isMainLightCookieEnabled = cookieTexture != null;
- if (isMainLightCookieEnabled)
- {
- Matrix4x4 cookieUVTransform = Matrix4x4.identity;
- float cookieFormat = (float)GetLightCookieShaderFormat(cookieTexture.graphicsFormat);
- if (mainLight.TryGetComponent(out UniversalAdditionalLightData additionalLightData))
- GetLightUVScaleOffset(ref additionalLightData, ref cookieUVTransform);
- Matrix4x4 cookieMatrix = s_DirLightProj * cookieUVTransform *
- visibleMainLight.localToWorldMatrix.inverse;
- cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, cookieTexture);
- cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, cookieMatrix);
- cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, cookieFormat);
- }
- else
- {
- // Make sure we erase stale data in case the main light is disabled but cookie system is enabled (for additional lights).
- cmd.SetGlobalTexture(ShaderProperty.mainLightTexture, Texture2D.whiteTexture);
- cmd.SetGlobalMatrix(ShaderProperty.mainLightWorldToLight, Matrix4x4.identity);
- cmd.SetGlobalFloat(ShaderProperty.mainLightCookieTextureFormat, (float)LightCookieShaderFormat.None);
- }
- return isMainLightCookieEnabled;
- }
- private LightCookieShaderFormat GetLightCookieShaderFormat(GraphicsFormat cookieFormat)
- {
- // TODO: convert this to use GraphicsFormatUtility
- switch (cookieFormat)
- {
- default:
- return LightCookieShaderFormat.RGB;
- // A8, A16 GraphicsFormat does not expose yet.
- case (GraphicsFormat)54:
- case (GraphicsFormat)55:
- return LightCookieShaderFormat.Alpha;
- case GraphicsFormat.R8_SRGB:
- case GraphicsFormat.R8_UNorm:
- case GraphicsFormat.R8_UInt:
- case GraphicsFormat.R8_SNorm:
- case GraphicsFormat.R8_SInt:
- case GraphicsFormat.R16_UNorm:
- case GraphicsFormat.R16_UInt:
- case GraphicsFormat.R16_SNorm:
- case GraphicsFormat.R16_SInt:
- case GraphicsFormat.R16_SFloat:
- case GraphicsFormat.R32_UInt:
- case GraphicsFormat.R32_SInt:
- case GraphicsFormat.R32_SFloat:
- case GraphicsFormat.R_BC4_SNorm:
- case GraphicsFormat.R_BC4_UNorm:
- case GraphicsFormat.R_EAC_SNorm:
- case GraphicsFormat.R_EAC_UNorm:
- return LightCookieShaderFormat.Red;
- }
- }
- private void GetLightUVScaleOffset(ref UniversalAdditionalLightData additionalLightData, ref Matrix4x4 uvTransform)
- {
- Vector2 uvScale = Vector2.one / additionalLightData.lightCookieSize;
- Vector2 uvOffset = additionalLightData.lightCookieOffset;
- if (Mathf.Abs(uvScale.x) < half.MinValue)
- uvScale.x = Mathf.Sign(uvScale.x) * half.MinValue;
- if (Mathf.Abs(uvScale.y) < half.MinValue)
- uvScale.y = Mathf.Sign(uvScale.y) * half.MinValue;
- uvTransform = Matrix4x4.Scale(new Vector3(uvScale.x, uvScale.y, 1));
- uvTransform.SetColumn(3, new Vector4(-uvOffset.x * uvScale.x, -uvOffset.y * uvScale.y, 0, 1));
- }
- bool SetupAdditionalLights(CommandBuffer cmd, ref LightData lightData)
- {
- int maxLightCount = Math.Min(m_Settings.maxAdditionalLights, lightData.visibleLights.Length);
- m_WorkMem.Resize(maxLightCount);
- int validLightCount = FilterAndValidateAdditionalLights(ref lightData, m_WorkMem.lightMappings);
- // Early exit if no valid cookie lights
- if (validLightCount <= 0)
- return false;
- // Lazy init GPU resources
- if (!isInitialized())
- InitAdditionalLights(validLightCount);
- // Update Atlas
- var validLights = new WorkSlice<LightCookieMapping>(m_WorkMem.lightMappings, validLightCount);
- int validUVRectCount = UpdateAdditionalLightsAtlas(cmd, ref validLights, m_WorkMem.uvRects);
- // Upload shader data
- var validUvRects = new WorkSlice<Vector4>(m_WorkMem.uvRects, validUVRectCount);
- UploadAdditionalLights(cmd, ref lightData, ref validLights, ref validUvRects);
- bool isAdditionalLightsEnabled = validUvRects.length > 0;
- return isAdditionalLightsEnabled;
- }
- int FilterAndValidateAdditionalLights(ref LightData lightData, LightCookieMapping[] validLightMappings)
- {
- int skipMainLightIndex = lightData.mainLightIndex;
- int lightBufferOffset = 0;
- int validLightCount = 0;
- // Warn on dropped lights
- int maxLights = Math.Min(lightData.visibleLights.Length, validLightMappings.Length);
- for (int i = 0; i < maxLights; i++)
- {
- if (i == skipMainLightIndex)
- {
- lightBufferOffset -= 1;
- continue;
- }
- Light light = lightData.visibleLights[i].light;
- // Skip lights without a cookie texture
- if (light.cookie == null)
- continue;
- // Only spot and point lights are supported.
- // Directional lights basically work,
- // but would require a lot of constants for the uv transform parameters
- // and there are very few use cases for multiple global cookies.
- var lightType = lightData.visibleLights[i].lightType;
- if (!(lightType == LightType.Spot ||
- lightType == LightType.Point))
- {
- Debug.LogWarning($"Additional {lightType.ToString()} light called '{light.name}' has a light cookie which will not be visible.", light);
- continue;
- }
- Assertions.Assert.IsTrue(i < ushort.MaxValue);
- LightCookieMapping lp;
- lp.visibleLightIndex = (ushort)i;
- lp.lightBufferIndex = (ushort)(i + lightBufferOffset);
- lp.light = light;
- validLightMappings[validLightCount++] = lp;
- }
- return validLightCount;
- }
- int UpdateAdditionalLightsAtlas(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects)
- {
- // Sort in-place by cookie size for better atlas allocation efficiency (and deduplication)
- validLightMappings.Sort(LightCookieMapping.s_CompareByCookieSize);
- uint cookieRequestPixelCount = ComputeCookieRequestPixelCount(ref validLightMappings);
- var atlasSize = m_AdditionalLightsCookieAtlas.AtlasTexture.referenceSize;
- float requestAtlasRatio = cookieRequestPixelCount / (float)(atlasSize.x * atlasSize.y);
- int cookieSizeDivisorApprox = ApproximateCookieSizeDivisor(requestAtlasRatio);
- // Try to recover resolution and scale the cookies back up.
- // If the cookies "should fit" and
- // If we have less requested pixels than the last time we found the correct divisor (a guard against retrying every frame).
- if (cookieSizeDivisorApprox < m_CookieSizeDivisor &&
- cookieRequestPixelCount < m_PrevCookieRequestPixelCount)
- {
- m_AdditionalLightsCookieAtlas.ResetAllocator();
- m_CookieSizeDivisor = cookieSizeDivisorApprox;
- }
- // Get cached atlas uv rectangles.
- // If there's new cookies, first try to add at current scaling level.
- // (This can result in suboptimal packing & scaling (additions aren't sorted), but reduces rebuilds.)
- // If it doesn't fit, scale down and rebuild the atlas until it fits.
- int uvRectCount = 0;
- while (uvRectCount <= 0)
- {
- uvRectCount = FetchUVRects(cmd, ref validLightMappings, textureAtlasUVRects, m_CookieSizeDivisor);
- if (uvRectCount <= 0)
- {
- // Uv rect fetching failed, reset and try again.
- m_AdditionalLightsCookieAtlas.ResetAllocator();
- // Reduce cookie size to approximate value try to rebuild the atlas.
- m_CookieSizeDivisor = Mathf.Max(m_CookieSizeDivisor + 1, cookieSizeDivisorApprox);
- m_PrevCookieRequestPixelCount = cookieRequestPixelCount;
- }
- }
- return uvRectCount;
- }
- int FetchUVRects(CommandBuffer cmd, ref WorkSlice<LightCookieMapping> validLightMappings, Vector4[] textureAtlasUVRects, int cookieSizeDivisor)
- {
- int uvRectCount = 0;
- for (int i = 0; i < validLightMappings.length; i++)
- {
- var lcm = validLightMappings[i];
- Light light = lcm.light;
- Texture cookie = light.cookie;
- // NOTE: Currently we blit directly on addition (on atlas fetch cache miss).
- // This can be costly if there are many resize rebuilds (in case "out-of-space", which shouldn't be a common case).
- // If rebuilds become a problem, we could try to just allocate and blit only when we have a fully valid allocation.
- // It would also make sense to do atlas operations only for unique textures and then reuse the results for similar cookies.
- Vector4 uvScaleOffset = Vector4.zero;
- if (cookie.dimension == TextureDimension.Cube)
- {
- Assertions.Assert.IsTrue(light.type == LightType.Point);
- uvScaleOffset = FetchCube(cmd, cookie, cookieSizeDivisor);
- }
- else
- {
- Assertions.Assert.IsTrue(light.type == LightType.Spot || light.type == LightType.Directional, "Light type needs 2D texture!");
- uvScaleOffset = Fetch2D(cmd, cookie, cookieSizeDivisor);
- }
- bool isCached = uvScaleOffset != Vector4.zero;
- if (!isCached)
- {
- if (cookieSizeDivisor > k_MaxCookieSizeDivisor)
- {
- 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.");
- // Complete fail, return what we have.
- return uvRectCount;
- }
- // Failed to get uv rect for each cookie, fail and try again.
- return 0;
- }
- // Adjust atlas UVs for OpenGL
- if (!SystemInfo.graphicsUVStartsAtTop)
- uvScaleOffset.w = 1.0f - uvScaleOffset.w - uvScaleOffset.y;
- textureAtlasUVRects[uvRectCount++] = uvScaleOffset;
- }
- return uvRectCount;
- }
- uint ComputeCookieRequestPixelCount(ref WorkSlice<LightCookieMapping> validLightMappings)
- {
- uint requestPixelCount = 0;
- int prevCookieID = 0;
- for (int i = 0; i < validLightMappings.length; i++)
- {
- var lcm = validLightMappings[i];
- Texture cookie = lcm.light.cookie;
- int cookieID = cookie.GetInstanceID();
- // Consider only unique textures as atlas request pixels
- // NOTE: relies on same cookies being sorted together
- // (we need sorting for good atlas packing anyway)
- if (cookieID == prevCookieID)
- {
- continue;
- }
- prevCookieID = cookieID;
- int pixelCookieCount = cookie.width * cookie.height;
- requestPixelCount += (uint)pixelCookieCount;
- }
- return requestPixelCount;
- }
- int ApproximateCookieSizeDivisor(float requestAtlasRatio)
- {
- // (Edge / N)^2 == 1/N^2 of area.
- // Ratio/N^2 == 1, sqrt(Ratio) == N, for "1:1" ratio.
- return (int)Mathf.Max(Mathf.Ceil(Mathf.Sqrt(requestAtlasRatio)), 1);
- }
- Vector4 Fetch2D(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
- {
- Assertions.Assert.IsTrue(cookie != null);
- Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Tex2D);
- Vector4 uvScaleOffset = Vector4.zero;
- var scaledWidth = Mathf.Max(cookie.width / cookieSizeDivisor, 4);
- var scaledHeight = Mathf.Max(cookie.height / cookieSizeDivisor, 4);
- Vector2 scaledCookieSize = new Vector2(scaledWidth, scaledHeight);
- bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
- if (isCached)
- {
- // Update contents IF required
- m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
- }
- else
- {
- m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledWidth, scaledHeight);
- }
- AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
- return uvScaleOffset;
- }
- Vector4 FetchCube(CommandBuffer cmd, Texture cookie, int cookieSizeDivisor = 1)
- {
- Assertions.Assert.IsTrue(cookie != null);
- Assertions.Assert.IsTrue(cookie.dimension == TextureDimension.Cube);
- Vector4 uvScaleOffset = Vector4.zero;
- // Scale octahedral projection, so that cube -> oct2D pixel count match better.
- int scaledOctCookieSize = Mathf.Max(ComputeOctahedralCookieSize(cookie) / cookieSizeDivisor, 4);
- bool isCached = m_AdditionalLightsCookieAtlas.IsCached(out uvScaleOffset, cookie);
- if (isCached)
- {
- // Update contents IF required
- m_AdditionalLightsCookieAtlas.UpdateTexture(cmd, cookie, ref uvScaleOffset);
- }
- else
- {
- m_AdditionalLightsCookieAtlas.AllocateTexture(cmd, ref uvScaleOffset, cookie, scaledOctCookieSize, scaledOctCookieSize);
- }
- // Cookie size in the atlas might not match CookieTexture size.
- // UVRect adjustment must be done with size in atlas.
- var scaledCookieSize = Vector2.one * scaledOctCookieSize;
- AdjustUVRect(ref uvScaleOffset, cookie, ref scaledCookieSize);
- return uvScaleOffset;
- }
- int ComputeOctahedralCookieSize(Texture cookie)
- {
- // Map 6*WxH pixels into 2W*2H pixels, so 4/6 ratio or 66% of cube pixels.
- int octCookieSize = Math.Max(cookie.width, cookie.height);
- if (m_Settings.atlas.isPow2)
- octCookieSize = octCookieSize * Mathf.NextPowerOfTwo((int)m_Settings.cubeOctahedralSizeScale);
- else
- octCookieSize = (int)(octCookieSize * m_Settings.cubeOctahedralSizeScale + 0.5f);
- return octCookieSize;
- }
- private void AdjustUVRect(ref Vector4 uvScaleOffset, Texture cookie, ref Vector2 cookieSize)
- {
- if (uvScaleOffset != Vector4.zero)
- {
- if (m_Settings.atlas.useMips)
- {
- // Payload texture is inset
- var potAtlas = (m_AdditionalLightsCookieAtlas as PowerOfTwoTextureAtlas);
- var mipPadding = potAtlas == null ? 1 : potAtlas.mipPadding;
- var paddingSize = Vector2.one * (int)Mathf.Pow(2, mipPadding) * 2;
- uvScaleOffset = PowerOfTwoTextureAtlas.GetPayloadScaleOffset(cookieSize, paddingSize, uvScaleOffset);
- }
- else
- {
- // Shrink by 0.5px to clamp sampling atlas neighbors (no padding)
- ShrinkUVRect(ref uvScaleOffset, 0.5f, ref cookieSize);
- }
- }
- }
- private void ShrinkUVRect(ref Vector4 uvScaleOffset, float amountPixels, ref Vector2 cookieSize)
- {
- var shrinkOffset = Vector2.one * amountPixels / cookieSize;
- var shrinkScale = (cookieSize - Vector2.one * (amountPixels * 2)) / cookieSize;
- uvScaleOffset.z += uvScaleOffset.x * shrinkOffset.x;
- uvScaleOffset.w += uvScaleOffset.y * shrinkOffset.y;
- uvScaleOffset.x *= shrinkScale.x;
- uvScaleOffset.y *= shrinkScale.y;
- }
- void UploadAdditionalLights(CommandBuffer cmd, ref LightData lightData, ref WorkSlice<LightCookieMapping> validLightMappings, ref WorkSlice<Vector4> validUvRects)
- {
- Assertions.Assert.IsTrue(m_AdditionalLightsCookieAtlas != null);
- Assertions.Assert.IsTrue(m_AdditionalLightsCookieShaderData != null);
- cmd.SetGlobalTexture(ShaderProperty.additionalLightsCookieAtlasTexture, m_AdditionalLightsCookieAtlas.AtlasTexture);
- cmd.SetGlobalFloat(ShaderProperty.additionalLightsCookieAtlasTextureFormat, (float)GetLightCookieShaderFormat(m_AdditionalLightsCookieAtlas.AtlasTexture.rt.graphicsFormat));
- // Resize and clear visible light to shader data mapping
- if (m_VisibleLightIndexToShaderDataIndex.Length < lightData.visibleLights.Length)
- m_VisibleLightIndexToShaderDataIndex = new int[lightData.visibleLights.Length];
- // Clear
- int len = Math.Min(m_VisibleLightIndexToShaderDataIndex.Length, lightData.visibleLights.Length);
- for (int i = 0; i < len; i++)
- m_VisibleLightIndexToShaderDataIndex[i] = -1;
- // Resize or init shader data.
- m_AdditionalLightsCookieShaderData.Resize(m_Settings.maxAdditionalLights);
- var worldToLights = m_AdditionalLightsCookieShaderData.worldToLights;
- var cookieEnableBits = m_AdditionalLightsCookieShaderData.cookieEnableBits;
- var atlasUVRects = m_AdditionalLightsCookieShaderData.atlasUVRects;
- var lightTypes = m_AdditionalLightsCookieShaderData.lightTypes;
- // Set all rects to "Invalid" zero area (Vector4.zero), just in case they're accessed.
- Array.Clear(atlasUVRects, 0, atlasUVRects.Length);
- // Set all cookies disabled
- cookieEnableBits.Clear();
- // NOTE: technically, we don't need to upload constants again if we knew the lights, atlas (rects) or visible order haven't changed.
- // But detecting that, might be as time consuming as just doing the work.
- // Fill shader data. Layout should match primary light data for additional lights.
- // Currently it's the same as visible lights, but main light(s) dropped.
- for (int i = 0; i < validUvRects.length; i++)
- {
- int visIndex = validLightMappings[i].visibleLightIndex;
- int bufIndex = validLightMappings[i].lightBufferIndex;
- // Update the mapping
- m_VisibleLightIndexToShaderDataIndex[visIndex] = bufIndex;
- var visLight = lightData.visibleLights[visIndex];
- // Update the (cpu) data
- lightTypes[bufIndex] = (int)visLight.lightType;
- worldToLights[bufIndex] = visLight.localToWorldMatrix.inverse;
- atlasUVRects[bufIndex] = validUvRects[i];
- cookieEnableBits[bufIndex] = true;
- // Spot projection
- if (visLight.lightType == LightType.Spot)
- {
- // VisibleLight.localToWorldMatrix only contains position & rotation.
- // Multiply projection for spot light.
- float spotAngle = visLight.spotAngle;
- float spotRange = visLight.range;
- var perp = Matrix4x4.Perspective(spotAngle, 1, 0.001f, spotRange);
- // Cancel embedded camera view axis flip (https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Matrix4x4.Perspective.html)
- perp.SetColumn(2, perp.GetColumn(2) * -1);
- // world -> light local -> light perspective
- worldToLights[bufIndex] = perp * worldToLights[bufIndex];
- }
- }
- // Apply changes and upload to GPU
- m_AdditionalLightsCookieShaderData.Upload(cmd);
- }
- }
- }
|