using System; using System.Linq; using System.Collections.Generic; using UnityEngine.Experimental.Rendering; namespace UnityEngine.Rendering.HighDefinition { /// /// Texture 3D atlas. It can only stores power of two cubic textures. /// In this atlas, texture are guaranteed to be aligned with a power of two grid. /// class Texture3DAtlas { // For the packing in 3D, we use an algorithm that packs the 3D textures in an octree // where the top level elements are divided into piece of maxElementSize size. // Due to the hardware limitation of max 2048 pixels in one dimension of a Texture3D, // the atlas uses first the x axis and then y to place the volumes. // The z dimension of the atlas will always have the size of maxElementSize. // Here's a 2D representation of a possible atlas layout with 5 volumes: // +-----+-----+-----+-----+ // | |B |C | | | // | A +-----+ E | | // | |D | | | | // +-----+--+--+-----+-----+ // As you can see the second cell is divided to place smaller POT elements in the atlas. // When an element is removed, the cell is marked as free. When the last leaf of a cell // is removed, all the leaves are removed to form a cell of maxElementSize again. class AtlasElement { public Vector3Int position; public int size; public Texture texture; public int hash; public AtlasElement[] children = null; public AtlasElement parent = null; // If the texture is null, then it means this space is free public bool IsFree() => texture == null && children == null; public AtlasElement(Vector3Int position, int size, Texture texture = null) { this.position = position; this.size = size; this.texture = texture; this.hash = 0; } // Subdivide the current cell in 8 cubes of equal size public void PopulateChildren() { children = new AtlasElement[8]; int halfSize = size / 2; // Down Front left corner children[0] = new AtlasElement(position + new Vector3Int(0, 0, 0), halfSize); // Down Front right corner children[1] = new AtlasElement(position + new Vector3Int(halfSize, 0, 0), halfSize); // Down Back left corner children[2] = new AtlasElement(position + new Vector3Int(0, 0, halfSize), halfSize); // Down Back right corner children[3] = new AtlasElement(position + new Vector3Int(halfSize, 0, halfSize), halfSize); // Up Front left corner children[4] = new AtlasElement(position + new Vector3Int(0, halfSize, 0), halfSize); // Up Front right corner children[5] = new AtlasElement(position + new Vector3Int(halfSize, halfSize, 0), halfSize); // Up Back left corner children[6] = new AtlasElement(position + new Vector3Int(0, halfSize, halfSize), halfSize); // Up Back right corner children[7] = new AtlasElement(position + new Vector3Int(halfSize, halfSize, halfSize), halfSize); foreach (var child in children) child.parent = this; } public void RemoveChildrenIfEmpty() { bool remove = true; foreach (var child in children) if (child.texture != null) remove = false; if (remove) children = null; } public override string ToString() => $"3D Atlas Element, pos: {position}, size: {size}, texture:{texture}, children: {children != null}"; } List m_Elements = new List(); // We keep track of cached texture in a map because it's faster to traverse than the element tree when looking for a texture Dictionary m_TextureElementsMap = new Dictionary(); RenderTexture m_Atlas; RenderTexture m_MipMapGenerationTemp; GraphicsFormat m_format; ComputeShader m_Texture3DAtlasCompute; int m_CopyKernel; int m_GenerateMipKernel; Vector3Int m_KernelGroupSize; int m_MaxElementSize = 0; int m_MaxElementCount = 0; bool m_HasMipMaps = false; const float k_MipmapFactorApprox = 1.33f; public Texture3DAtlas(GraphicsFormat format, int maxElementSize, int maxElementCount, bool hasMipMaps = true) { m_format = format; m_MaxElementSize = maxElementSize; m_MaxElementCount = maxElementCount; m_HasMipMaps = hasMipMaps; // Texture 3D are limited to 2048 resolution in every axis, so wen need to create the atlas in x and y axis: const int maxTexture3DSize = 2048; // TODO replace this by SystemInfo.maxTexture3DSize when it will be available. int maxElementCountPerDimension = maxTexture3DSize / maxElementSize; int xElementCount = Mathf.Min(maxElementCount, maxElementCountPerDimension); int yElementCount = maxElementCount < maxElementCountPerDimension ? 1 : Mathf.CeilToInt(maxElementCount / maxElementCountPerDimension); m_Atlas = new RenderTexture(xElementCount * maxElementSize, yElementCount * maxElementSize, 0, format) { volumeDepth = maxElementSize, dimension = TextureDimension.Tex3D, hideFlags = HideFlags.HideAndDontSave, enableRandomWrite = true, useMipMap = hasMipMaps, autoGenerateMips = false, name = $"Texture 3D Atlas - {xElementCount * maxElementSize}x{yElementCount * maxElementSize}x{maxElementSize}", }; m_Atlas.Create(); // Quarter res temp texture used for the mip generation m_MipMapGenerationTemp = new RenderTexture(maxElementSize / 4, maxElementSize / 4, 0, format) { volumeDepth = maxElementSize / 4, dimension = TextureDimension.Tex3D, hideFlags = HideFlags.HideAndDontSave, enableRandomWrite = true, useMipMap = hasMipMaps, autoGenerateMips = false, name = $"Texture 3D MipMap Temp - {maxElementSize / 4}x{maxElementSize / 4}x{maxElementSize / 4}", }; m_MipMapGenerationTemp.Create(); // Fill the atlas with empty elements: for (int i = 0; i < maxElementCount; i++) { Vector3Int pos = new Vector3Int((i % xElementCount) * maxElementSize, (int)(Mathf.FloorToInt(i / (float)xElementCount) * maxElementSize), 0); var elem = new AtlasElement(pos, maxElementSize); m_Elements.Add(elem); } var shaders = GraphicsSettings.GetRenderPipelineSettings(); m_Texture3DAtlasCompute = shaders.texture3DAtlasCS; m_CopyKernel = m_Texture3DAtlasCompute.FindKernel("Copy"); m_GenerateMipKernel = m_Texture3DAtlasCompute.FindKernel("GenerateMipMap"); m_Texture3DAtlasCompute.GetKernelThreadGroupSizes(m_CopyKernel, out var groupThreadX, out var groupThreadY, out var groupThreadZ); m_KernelGroupSize = new Vector3Int((int)groupThreadX, (int)groupThreadY, (int)groupThreadZ); } int GetTextureDepth(Texture t) { if (t is Texture3D volume) return volume.depth; else if (t is RenderTexture rt) return rt.volumeDepth; return 0; } protected int GetTextureHash(Texture texture) { int hash = texture.GetHashCode(); unchecked { #if UNITY_EDITOR hash = 23 * hash + texture.imageContentsHash.GetHashCode(); #endif hash = 23 * hash + texture.GetInstanceID().GetHashCode(); hash = 23 * hash + texture.graphicsFormat.GetHashCode(); hash = 23 * hash + texture.width.GetHashCode(); hash = 23 * hash + texture.height.GetHashCode(); hash = 23 * hash + texture.updateCount.GetHashCode(); } return hash; } public bool IsTextureValid(Texture tex) { if (tex.width != tex.height || tex.height != GetTextureDepth(tex)) { Debug.LogError($"3D Texture Atlas: Added texture {tex} is not doesn't have a cubic size {tex.width}x{tex.height}x{GetTextureDepth(tex)}."); return false; } if (tex.width > m_MaxElementSize) { Debug.LogError($"3D Texture Atlas: Added texture {tex} size {tex.width} is bigger than the max element atlas size {m_MaxElementSize}."); return false; } if (tex.width < 1) { Debug.LogError($"3D Texture Atlas: Added texture {tex} size {tex.width} is smaller than 1."); return false; } if (!Mathf.IsPowerOfTwo(tex.width)) { Debug.LogError($"3D Texture Atlas: Added texture {tex} size {tex.width} is not power of two."); return false; } return true; } public bool AddTexture(Texture tex) { if (m_TextureElementsMap.ContainsKey(tex)) return true; if (!IsTextureValid(tex)) return false; if (!TryAddTextureToTree(tex)) return false; return true; } bool TryAddTextureToTree(Texture tex) { // For texture that have the max size in the atlas, we just have to find the first empty element. if (tex.width == m_MaxElementSize) { var freeElem = m_Elements.FirstOrDefault(e => e.IsFree()); if (freeElem != null) { SetTextureToElem(freeElem, tex); return true; } } else // Otherwise, we traverse the tree in depth to find a suitable position { // Find free element by looking at children var freeElem = FindFreeElementWithSize(tex.width); if (freeElem != null) { SetTextureToElem(freeElem, tex); return true; } else { // If we didn't found any empty element of the same size as the texture, then we have to create a new one freeElem = m_Elements.FirstOrDefault(e => e.IsFree()); // No more space in the atlas if (freeElem == null) return true; while (freeElem.size > tex.width) { freeElem.PopulateChildren(); freeElem = freeElem.children[0]; } SetTextureToElem(freeElem, tex); return true; } } void SetTextureToElem(AtlasElement element, Texture texture) { element.texture = texture; m_TextureElementsMap.Add(texture, element); } return false; } AtlasElement FindFreeElementWithSize(int size) { static AtlasElement FindFreeElement(int size, AtlasElement elem) { if (elem.size == size) { if (elem.IsFree()) return elem; else return null; } if (elem.children == null) return null; foreach (var child in elem.children) { if (child.children != null && child.size >= size) { var cell = FindFreeElement(size, child); if (cell != null) return cell; } else if (child.IsFree()) return child; } return null; } foreach (var elem in m_Elements) { var result = FindFreeElement(size, elem); if (result != null) return result; } return null; } public void RemoveTexture(Texture tex) { if (m_TextureElementsMap.TryGetValue(tex, out var element)) { element.texture = null; if (element.parent != null) element.parent.RemoveChildrenIfEmpty(); m_TextureElementsMap.Remove(tex); } } public void ClearTextures() { foreach (var elem in m_Elements) { elem.texture = null; elem.children = null; } m_TextureElementsMap.Clear(); } public Vector3 GetTextureOffset(Texture tex) { if (tex != null && m_TextureElementsMap.TryGetValue(tex, out var element)) return (Vector3)element.position; else return -Vector3.one; } public void Update(CommandBuffer cmd) { if (m_TextureElementsMap.Count == 0) return; // First pass to remove / add textures that changed resolution, it can happens if a 3D render texture is resized foreach (var element in m_Elements) { var texture = element.texture; if (texture == null) continue; if (texture.width != element.size) { RemoveTexture(texture); AddTexture(texture); continue; } } // Second pass to update elements where the texture content have changed foreach (var element in m_TextureElementsMap.Values) { if (element.texture == null) continue; int newHash = GetTextureHash(element.texture); if (element.hash != newHash) { element.hash = newHash; CopyTexture(cmd, element); } } } struct MipGenerationSwapData { public RenderTexture target; public Vector3Int offset; public int mipOffset; } void CopyTexture(CommandBuffer cmd, AtlasElement element) { // Copy mip 0 of the texture CopyMip(cmd, element.texture, 0, m_Atlas, element.position, 0); // If we need mip maps, we either copy them from the source if it has mip maps or we generate them. if (m_HasMipMaps) { int mipMapCount = m_HasMipMaps ? CoreUtils.GetMipCount(element.texture.width) : 1; bool sourceHasMipMaps = element.texture.mipmapCount > 1; // If the source 3D texture has mipmaps, we can just copy them if (sourceHasMipMaps) CopyMips(cmd, element.texture, m_Atlas, element.position); else // Otherwise, we need to generate them { // TODO: handle texture that are smaller than m_MipMapGenerationTemp! // Generating the first mip from the source texture into the atlas to save a copy. GenerateMip(cmd, element.texture, Vector3Int.zero, 0, m_Atlas, element.position, 1); MipGenerationSwapData source = new MipGenerationSwapData { target = m_Atlas, offset = element.position, mipOffset = 0 }; // m_MipMapGenerationTemp is allocated in quater res to save memory so we need to apply a mip offset when writing to it. int tempMipOffset = (int)Mathf.Log((m_MipMapGenerationTemp.width / (element.size >> 2)), 2); MipGenerationSwapData destination = new MipGenerationSwapData { target = m_MipMapGenerationTemp, offset = Vector3Int.zero, mipOffset = tempMipOffset - 2 }; for (int i = 2; i < mipMapCount; i++) { GenerateMip(cmd, source.target, source.offset, i + source.mipOffset - 1, destination.target, destination.offset, i + destination.mipOffset); // Swap rt settings var temp = source; source = destination; destination = temp; } // Copy back the mips from the temp target to the atlas for (int i = 2; i < mipMapCount; i += 2) { var mipPos = new Vector3Int((int)element.position.x >> i, (int)element.position.y >> i, (int)element.position.z >> i); CopyMip(cmd, m_MipMapGenerationTemp, i - 2 + tempMipOffset, m_Atlas, mipPos, i); } } } } void CopyMips(CommandBuffer cmd, Texture source, Texture destination, Vector3Int destinationOffset) { int mipMapCount = CoreUtils.GetMipCount(source.width); for (int i = 1; i < mipMapCount; i++) { var mipPos = new Vector3Int((int)destinationOffset.x >> i, (int)destinationOffset.y >> i, (int)destinationOffset.z >> i); CopyMip(cmd, source, i, destination, mipPos, i); } } void CopyMip(CommandBuffer cmd, Texture source, int sourceMip, Texture destination, Vector3Int destinationOffset, int destinationMip) { cmd.SetComputeTextureParam(m_Texture3DAtlasCompute, m_CopyKernel, HDShaderIDs._Src3DTexture, source); cmd.SetComputeFloatParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcMip, sourceMip); cmd.SetComputeTextureParam(m_Texture3DAtlasCompute, m_CopyKernel, HDShaderIDs._Dst3DTexture, destination, destinationMip); cmd.SetComputeVectorParam(m_Texture3DAtlasCompute, HDShaderIDs._DstOffset, (Vector3)destinationOffset); // Previous volume texture only used the alpha channel so when we copy them, we put a white color to avoid having a black texture bool alphaOnly = (source is Texture3D t) && t.format == TextureFormat.Alpha8; cmd.SetComputeFloatParam(m_Texture3DAtlasCompute, HDShaderIDs._AlphaOnlyTexture, alphaOnly ? 1 : 0); int mipMapSize = (int)source.width >> sourceMip; // We assume that the texture is POT cmd.SetComputeIntParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcSize, mipMapSize); cmd.DispatchCompute( m_Texture3DAtlasCompute, m_CopyKernel, Mathf.Max(mipMapSize / m_KernelGroupSize.x, 1), Mathf.Max(mipMapSize / m_KernelGroupSize.y, 1), Mathf.Max(mipMapSize / m_KernelGroupSize.z, 1) ); } void GenerateMip(CommandBuffer cmd, Texture source, Vector3Int sourceOffset, int sourceMip, Texture destination, Vector3Int destinationOffset, int destinationMip) { // Compute the source scale and offset in UV space: Vector3 offset = new Vector3(sourceOffset.x / (float)source.width, sourceOffset.y / (float)source.height, sourceOffset.z / (float)GetTextureDepth(source)); Vector3Int dstOffset = new Vector3Int(destinationOffset.x >> destinationMip, destinationOffset.y >> destinationMip, destinationOffset.z >> destinationMip); Vector3Int minSourceSize = new Vector3Int(Mathf.Min(source.width, destination.width), Mathf.Min(source.height, destination.height), Mathf.Min(GetTextureDepth(source), GetTextureDepth(destination))); // Vector3Int sourceTextureMipSize = new Vector3Int(minSourceSize.x >> sourceMip, minSourceSize.y >> sourceMip, minSourceSize.z >> sourceMip); Vector3Int destinationTextureMipSize = new Vector3Int(destination.width >> destinationMip, destination.height >> destinationMip, GetTextureDepth(destination) >> destinationMip); Vector3 scale = Vector3.one; Vector3Int sourceMipSize = new Vector3Int(source.width >> (sourceMip + 1), source.height >> (sourceMip + 1), GetTextureDepth(source) >> (sourceMip + 1)); Vector3Int destinationMipSize = new Vector3Int(destination.width >> destinationMip, destination.height >> destinationMip, GetTextureDepth(destination) >> destinationMip); // if (source.width > destination.width) // { scale = new Vector3( Mathf.Min((float)destinationMipSize.x / sourceMipSize.x, 1), Mathf.Min((float)destinationMipSize.y / sourceMipSize.y, 1), Mathf.Min((float)destinationMipSize.z / sourceMipSize.z, 1) ); // } cmd.SetComputeTextureParam(m_Texture3DAtlasCompute, m_GenerateMipKernel, HDShaderIDs._Src3DTexture, source); cmd.SetComputeVectorParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcScale, scale); cmd.SetComputeVectorParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcOffset, offset); cmd.SetComputeFloatParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcMip, sourceMip); cmd.SetComputeTextureParam(m_Texture3DAtlasCompute, m_GenerateMipKernel, HDShaderIDs._Dst3DTexture, destination, destinationMip); cmd.SetComputeVectorParam(m_Texture3DAtlasCompute, HDShaderIDs._DstOffset, (Vector3)dstOffset); // This is not correct when the atlas is the destination, we can compute it using the min of mip source size and dest mip side. int mipMapSize = Mathf.Min(GetTextureDepth(source) >> (sourceMip + 1), GetTextureDepth(destination) >> (destinationMip)); cmd.SetComputeIntParam(m_Texture3DAtlasCompute, HDShaderIDs._SrcSize, mipMapSize); bool alphaOnly = (source is Texture3D t) && t.format == TextureFormat.Alpha8; cmd.SetComputeFloatParam(m_Texture3DAtlasCompute, HDShaderIDs._AlphaOnlyTexture, alphaOnly ? 1 : 0); cmd.DispatchCompute( m_Texture3DAtlasCompute, m_GenerateMipKernel, Mathf.Max(mipMapSize / m_KernelGroupSize.x, 1), Mathf.Max(mipMapSize / m_KernelGroupSize.y, 1), Mathf.Max(mipMapSize / m_KernelGroupSize.z, 1) ); } public RenderTexture GetAtlas() => m_Atlas; public void Release() { ClearTextures(); CoreUtils.Destroy(m_Atlas); CoreUtils.Destroy(m_MipMapGenerationTemp); } public static long GetApproxCacheSizeInByte(int elementSize, int elementCount, GraphicsFormat format, bool hasMipMaps) { int formatInBytes = HDUtils.GetFormatSizeInBytes(format); long elementSizeInBytes = (long)(elementSize * elementSize * elementSize * formatInBytes * (hasMipMaps ? k_MipmapFactorApprox : 1.0f)); return elementSizeInBytes * elementCount; } public static int GetMaxElementCountForWeightInByte(long weight, int elementSize, int elementCount, GraphicsFormat format, bool hasMipMaps) { long elementSizeInByte = (long)((long)elementSize * elementSize * elementSize * HDUtils.GetFormatSizeInBytes(format) * (hasMipMaps ? k_MipmapFactorApprox : 1.0f)); return (int)Mathf.Clamp(weight / elementSizeInByte, 1, elementCount); } } }