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);
}
}
}