using System.Collections.Generic; using UnityEngine.Experimental.Rendering; namespace UnityEngine.Rendering { /// /// Texture atlas with rectangular power of two size. /// public class PowerOfTwoTextureAtlas : Texture2DAtlas { readonly int m_MipPadding; const float k_MipmapFactorApprox = 1.33f; private Dictionary m_RequestedTextures = new Dictionary(); /// /// Create a new texture atlas, must have power of two size. /// /// The size of the atlas in pixels. Must be power of two. /// Amount of mip padding in power of two. /// Atlas texture format /// Atlas texture filter mode. /// Name of the atlas /// Use mip maps public PowerOfTwoTextureAtlas(int size, int mipPadding, GraphicsFormat format, FilterMode filterMode = FilterMode.Point, string name = "", bool useMipMap = true) : base(size, size, format, filterMode, true, name, useMipMap) { this.m_MipPadding = mipPadding; // Check if size is a power of two if ((size & (size - 1)) != 0) Debug.Assert(false, "Power of two atlas was constructed with non power of two size: " + size); } /// /// Used mipmap padding size in power of two. /// public int mipPadding => m_MipPadding; int GetTexturePadding() => (int)Mathf.Pow(2, m_MipPadding) * 2; /// /// Get location of the actual texture data without padding in the atlas. /// /// The source texture cached in the atlas. /// Cached atlas location (scale and offset) for the source texture. /// Scale and offset for the source texture without padding. public Vector4 GetPayloadScaleOffset(Texture texture, in Vector4 scaleOffset) { int pixelPadding = GetTexturePadding(); Vector2 paddingSize = Vector2.one * pixelPadding; Vector2 textureSize = GetPowerOfTwoTextureSize(texture); return GetPayloadScaleOffset(textureSize, paddingSize, scaleOffset); } /// /// Get location of the actual texture data without padding in the atlas. /// /// Size of the source texture /// Padding size used for the source texture. /// Cached atlas location (scale and offset) for the source texture. /// Scale and offset for the source texture without padding. static public Vector4 GetPayloadScaleOffset(in Vector2 textureSize, in Vector2 paddingSize, in Vector4 scaleOffset) { // Scale, Offset is a padded atlas sub-texture rectangle. // Actual texture data (payload) is inset, i.e. padded inwards. Vector2 subTexScale = new Vector2(scaleOffset.x, scaleOffset.y); Vector2 subTexOffset = new Vector2(scaleOffset.z, scaleOffset.w); // NOTE: Should match Blit() padding calculations. Vector2 scalePadding = ((textureSize + paddingSize) / textureSize); // Size of padding (sampling) rectangle relative to the payload texture. Vector2 offsetPadding = (paddingSize / 2.0f) / (textureSize + paddingSize); // Padding offset in the padding rectangle Vector2 insetScale = subTexScale / scalePadding; // Size of payload rectangle in sub-tex Vector2 insetOffset = subTexOffset + subTexScale * offsetPadding; // Offset of payload rectangle in sub-tex return new Vector4(insetScale.x, insetScale.y, insetOffset.x, insetOffset.y); } private enum BlitType { Padding, PaddingMultiply, OctahedralPadding, OctahedralPaddingMultiply, } private void Blit2DTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips, BlitType blitType) { int mipCount = GetTextureMipmapCount(texture.width, texture.height); int pixelPadding = GetTexturePadding(); Vector2 textureSize = GetPowerOfTwoTextureSize(texture); bool bilinear = texture.filterMode != FilterMode.Point; if (!blitMips) mipCount = 1; using (new ProfilingScope(cmd, ProfilingSampler.Get(CoreProfileId.BlitTextureInPotAtlas))) { for (int mipLevel = 0; mipLevel < mipCount; mipLevel++) { cmd.SetRenderTarget(m_AtlasTexture, mipLevel); switch (blitType) { case BlitType.Padding: Blitter.BlitQuadWithPadding(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break; case BlitType.PaddingMultiply: Blitter.BlitQuadWithPaddingMultiply(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break; case BlitType.OctahedralPadding: Blitter.BlitOctahedralWithPadding(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break; case BlitType.OctahedralPaddingMultiply: Blitter.BlitOctahedralWithPaddingMultiply(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break; } } } } /// /// Blit texture into the atlas with padding. /// /// Target command buffer for graphics commands. /// Destination scale (.xy) and offset (.zw) /// Source Texture /// Source scale (.xy) and offset(.zw). /// Blit mip maps. /// Override texture instance ID. public override void BlitTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1) { // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering if (Is2D(texture)) { Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.Padding); MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips); } } /// /// Blit texture into the atlas with padding and blending. /// /// Target command buffer for graphics commands. /// Destination scale (.xy) and offset (.zw) /// Source Texture /// Source scale (.xy) and offset(.zw). /// Blit mip maps. /// Override texture instance ID. public void BlitTextureMultiply(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1) { // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering if (Is2D(texture)) { Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.PaddingMultiply); MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips); } } /// /// Blit octahedral texture into the atlas with padding. /// /// Target command buffer for graphics commands. /// Destination scale (.xy) and offset (.zw) /// Source Texture /// Source scale (.xy) and offset(.zw). /// Blit mip maps. /// Override texture instance ID. public override void BlitOctahedralTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1) { // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering if (Is2D(texture)) { Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.OctahedralPadding); MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips); } } /// /// Blit octahedral texture into the atlas with padding. /// /// Target command buffer for graphics commands. /// Destination scale (.xy) and offset (.zw) /// Source Texture /// Source scale (.xy) and offset(.zw). /// Blit mip maps. /// Override texture instance ID. public void BlitOctahedralTextureMultiply(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1) { // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering if (Is2D(texture)) { Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.OctahedralPaddingMultiply); MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips); } } void TextureSizeToPowerOfTwo(Texture texture, ref int width, ref int height) { // Change the width and height of the texture to be power of two width = Mathf.NextPowerOfTwo(width); height = Mathf.NextPowerOfTwo(height); } Vector2 GetPowerOfTwoTextureSize(Texture texture) { int width = texture.width, height = texture.height; TextureSizeToPowerOfTwo(texture, ref width, ref height); return new Vector2(width, height); } // Override the behavior when we add a texture so all non-pot textures are blitted to a pot target zone /// /// Allocate space from the atlas for a texture and copy texture contents into the atlas. /// /// Target command buffer for graphics commands. /// Allocated scale (.xy) and offset (.zw) /// Source Texture /// Request width in pixels. /// Request height in pixels. /// Override texture instance ID. /// True on success, false otherwise. public override bool AllocateTexture(CommandBuffer cmd, ref Vector4 scaleOffset, Texture texture, int width, int height, int overrideInstanceID = -1) { // This atlas only supports square textures if (height != width) { Debug.LogError("Can't place " + texture + " in the atlas " + m_AtlasTexture.name + ": Only squared texture are allowed in this atlas."); return false; } TextureSizeToPowerOfTwo(texture, ref height, ref width); return base.AllocateTexture(cmd, ref scaleOffset, texture, width, height); } /// /// Clear tracked requested textures. /// public void ResetRequestedTexture() => m_RequestedTextures.Clear(); /// /// Reserves the space on the texture atlas /// /// The source texture /// True if the space is reserved public bool ReserveSpace(Texture texture) => ReserveSpace(texture, texture.width, texture.height); /// /// Reserves the space on the texture atlas /// /// The source texture /// The width /// The height /// True if the space is reserved public bool ReserveSpace(Texture texture, int width, int height) => ReserveSpace(GetTextureID(texture), width, height); /// /// Reserves the space on the texture atlas /// /// The source texture A /// The source texture B /// The width /// The height /// True if the space is reserved public bool ReserveSpace(Texture textureA, Texture textureB, int width, int height) => ReserveSpace(GetTextureID(textureA, textureB), width, height); /// /// Reserves the space on the texture atlas /// /// The id /// The width /// The height /// True if the space is reserved bool ReserveSpace(int id, int width, int height) { m_RequestedTextures[id] = new Vector2Int(width, height); // Cookie texture resolution changing between frame is a special case, so we handle it here. // The texture will be re-allocated and may cause holes in the atlas texture, which is fine // because when it doesn't have any more space, it will re-layout the texture correctly. var cachedSize = GetCachedTextureSize(id); if (!IsCached(out _, id) || cachedSize.x != width || cachedSize.y != height) { Vector4 scaleBias = Vector4.zero; if (!AllocateTextureWithoutBlit(id, width, height, ref scaleBias)) return false; } return true; } /// /// sort all the requested allocation from biggest to smallest and re-insert them. /// This function does not moves the textures in the atlas, it only changes their coordinates /// /// True if all textures have successfully been re-inserted in the atlas public bool RelayoutEntries() { var entries = new List<(int instanceId, Vector2Int size)>(); foreach (var entry in m_RequestedTextures) entries.Add((entry.Key, entry.Value)); ResetAllocator(); // Sort entries from biggest to smallest entries.Sort((c1, c2) => { return c2.size.magnitude.CompareTo(c1.size.magnitude); }); bool success = true; Vector4 newScaleOffset = Vector4.zero; foreach (var e in entries) success &= AllocateTextureWithoutBlit(e.instanceId, e.size.x, e.size.y, ref newScaleOffset); return success; } /// /// Get cache size in bytes. /// /// /// Atlas resolution (square). /// Atlas uses mip maps. /// Atlas format. /// public static long GetApproxCacheSizeInByte(int nbElement, int resolution, bool hasMipmap, GraphicsFormat format) => (long)(nbElement * resolution * resolution * (double)((hasMipmap ? k_MipmapFactorApprox : 1.0f) * GraphicsFormatUtility.GetBlockSize(format))); /// /// Compute the max size of a power of two atlas for a given size in byte (weight). /// /// Atlas size in bytes. /// Atlas uses mip maps. /// Atlas format. /// public static int GetMaxCacheSizeForWeightInByte(int weight, bool hasMipmap, GraphicsFormat format) { float bytePerPixel = (float)GraphicsFormatUtility.GetBlockSize(format) * (hasMipmap ? k_MipmapFactorApprox : 1.0f); var maxAtlasSquareSize = Mathf.Sqrt((float)weight / bytePerPixel); return CoreUtils.PreviousPowerOfTwo((int)maxAtlasSquareSize); } } }