A game about forced loneliness, made by TACStudios
1using System.Collections.Generic;
2using UnityEngine.Experimental.Rendering;
3
4namespace UnityEngine.Rendering
5{
6 /// <summary>
7 /// Texture atlas with rectangular power of two size.
8 /// </summary>
9 public class PowerOfTwoTextureAtlas : Texture2DAtlas
10 {
11 readonly int m_MipPadding;
12 const float k_MipmapFactorApprox = 1.33f;
13
14 private Dictionary<int, Vector2Int> m_RequestedTextures = new Dictionary<int, Vector2Int>();
15
16 /// <summary>
17 /// Create a new texture atlas, must have power of two size.
18 /// </summary>
19 /// <param name="size">The size of the atlas in pixels. Must be power of two.</param>
20 /// <param name="mipPadding">Amount of mip padding in power of two.</param>
21 /// <param name="format">Atlas texture format</param>
22 /// <param name="filterMode">Atlas texture filter mode.</param>
23 /// <param name="name">Name of the atlas</param>
24 /// <param name="useMipMap">Use mip maps</param>
25 public PowerOfTwoTextureAtlas(int size, int mipPadding, GraphicsFormat format, FilterMode filterMode = FilterMode.Point, string name = "", bool useMipMap = true)
26 : base(size, size, format, filterMode, true, name, useMipMap)
27 {
28 this.m_MipPadding = mipPadding;
29
30 // Check if size is a power of two
31 if ((size & (size - 1)) != 0)
32 Debug.Assert(false, "Power of two atlas was constructed with non power of two size: " + size);
33 }
34
35 /// <summary>
36 /// Used mipmap padding size in power of two.
37 /// </summary>
38 public int mipPadding => m_MipPadding;
39
40 int GetTexturePadding() => (int)Mathf.Pow(2, m_MipPadding) * 2;
41
42 /// <summary>
43 /// Get location of the actual texture data without padding in the atlas.
44 /// </summary>
45 /// <param name="texture">The source texture cached in the atlas.</param>
46 /// <param name="scaleOffset">Cached atlas location (scale and offset) for the source texture.</param>
47 /// <returns>Scale and offset for the source texture without padding.</returns>
48 public Vector4 GetPayloadScaleOffset(Texture texture, in Vector4 scaleOffset)
49 {
50 int pixelPadding = GetTexturePadding();
51 Vector2 paddingSize = Vector2.one * pixelPadding;
52 Vector2 textureSize = GetPowerOfTwoTextureSize(texture);
53 return GetPayloadScaleOffset(textureSize, paddingSize, scaleOffset);
54 }
55
56 /// <summary>
57 /// Get location of the actual texture data without padding in the atlas.
58 /// </summary>
59 /// <param name="textureSize">Size of the source texture</param>
60 /// <param name="paddingSize">Padding size used for the source texture. </param>
61 /// <param name="scaleOffset">Cached atlas location (scale and offset) for the source texture.</param>
62 /// <returns>Scale and offset for the source texture without padding.</returns>
63 static public Vector4 GetPayloadScaleOffset(in Vector2 textureSize, in Vector2 paddingSize, in Vector4 scaleOffset)
64 {
65 // Scale, Offset is a padded atlas sub-texture rectangle.
66 // Actual texture data (payload) is inset, i.e. padded inwards.
67 Vector2 subTexScale = new Vector2(scaleOffset.x, scaleOffset.y);
68 Vector2 subTexOffset = new Vector2(scaleOffset.z, scaleOffset.w);
69
70 // NOTE: Should match Blit() padding calculations.
71 Vector2 scalePadding = ((textureSize + paddingSize) / textureSize); // Size of padding (sampling) rectangle relative to the payload texture.
72 Vector2 offsetPadding = (paddingSize / 2.0f) / (textureSize + paddingSize); // Padding offset in the padding rectangle
73
74 Vector2 insetScale = subTexScale / scalePadding; // Size of payload rectangle in sub-tex
75 Vector2 insetOffset = subTexOffset + subTexScale * offsetPadding; // Offset of payload rectangle in sub-tex
76
77 return new Vector4(insetScale.x, insetScale.y, insetOffset.x, insetOffset.y);
78 }
79
80 private enum BlitType
81 {
82 Padding,
83 PaddingMultiply,
84 OctahedralPadding,
85 OctahedralPaddingMultiply,
86 }
87
88 private void Blit2DTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips, BlitType blitType)
89 {
90 int mipCount = GetTextureMipmapCount(texture.width, texture.height);
91 int pixelPadding = GetTexturePadding();
92 Vector2 textureSize = GetPowerOfTwoTextureSize(texture);
93 bool bilinear = texture.filterMode != FilterMode.Point;
94
95 if (!blitMips)
96 mipCount = 1;
97
98 using (new ProfilingScope(cmd, ProfilingSampler.Get(CoreProfileId.BlitTextureInPotAtlas)))
99 {
100 for (int mipLevel = 0; mipLevel < mipCount; mipLevel++)
101 {
102 cmd.SetRenderTarget(m_AtlasTexture, mipLevel);
103 switch (blitType)
104 {
105 case BlitType.Padding: Blitter.BlitQuadWithPadding(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break;
106 case BlitType.PaddingMultiply: Blitter.BlitQuadWithPaddingMultiply(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break;
107 case BlitType.OctahedralPadding: Blitter.BlitOctahedralWithPadding(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break;
108 case BlitType.OctahedralPaddingMultiply: Blitter.BlitOctahedralWithPaddingMultiply(cmd, texture, textureSize, sourceScaleOffset, scaleOffset, mipLevel, bilinear, pixelPadding); break;
109 }
110 }
111 }
112 }
113
114 /// <summary>
115 /// Blit texture into the atlas with padding.
116 /// </summary>
117 /// <param name="cmd">Target command buffer for graphics commands.</param>
118 /// <param name="scaleOffset">Destination scale (.xy) and offset (.zw)</param>
119 /// <param name="texture">Source Texture</param>
120 /// <param name="sourceScaleOffset">Source scale (.xy) and offset(.zw).</param>
121 /// <param name="blitMips">Blit mip maps.</param>
122 /// <param name="overrideInstanceID">Override texture instance ID.</param>
123 public override void BlitTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1)
124 {
125 // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering
126 if (Is2D(texture))
127 {
128 Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.Padding);
129 MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips);
130 }
131 }
132
133 /// <summary>
134 /// Blit texture into the atlas with padding and blending.
135 /// </summary>
136 /// <param name="cmd">Target command buffer for graphics commands.</param>
137 /// <param name="scaleOffset">Destination scale (.xy) and offset (.zw)</param>
138 /// <param name="texture">Source Texture</param>
139 /// <param name="sourceScaleOffset">Source scale (.xy) and offset(.zw).</param>
140 /// <param name="blitMips">Blit mip maps.</param>
141 /// <param name="overrideInstanceID">Override texture instance ID.</param>
142 public void BlitTextureMultiply(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1)
143 {
144 // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering
145 if (Is2D(texture))
146 {
147 Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.PaddingMultiply);
148 MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips);
149 }
150 }
151
152 /// <summary>
153 /// Blit octahedral texture into the atlas with padding.
154 /// </summary>
155 /// <param name="cmd">Target command buffer for graphics commands.</param>
156 /// <param name="scaleOffset">Destination scale (.xy) and offset (.zw)</param>
157 /// <param name="texture">Source Texture</param>
158 /// <param name="sourceScaleOffset">Source scale (.xy) and offset(.zw).</param>
159 /// <param name="blitMips">Blit mip maps.</param>
160 /// <param name="overrideInstanceID">Override texture instance ID.</param>
161 public override void BlitOctahedralTexture(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1)
162 {
163 // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering
164 if (Is2D(texture))
165 {
166 Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.OctahedralPadding);
167 MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips);
168 }
169 }
170
171 /// <summary>
172 /// Blit octahedral texture into the atlas with padding.
173 /// </summary>
174 /// <param name="cmd">Target command buffer for graphics commands.</param>
175 /// <param name="scaleOffset">Destination scale (.xy) and offset (.zw)</param>
176 /// <param name="texture">Source Texture</param>
177 /// <param name="sourceScaleOffset">Source scale (.xy) and offset(.zw).</param>
178 /// <param name="blitMips">Blit mip maps.</param>
179 /// <param name="overrideInstanceID">Override texture instance ID.</param>
180 public void BlitOctahedralTextureMultiply(CommandBuffer cmd, Vector4 scaleOffset, Texture texture, Vector4 sourceScaleOffset, bool blitMips = true, int overrideInstanceID = -1)
181 {
182 // We handle ourself the 2D blit because cookies needs mipPadding for trilinear filtering
183 if (Is2D(texture))
184 {
185 Blit2DTexture(cmd, scaleOffset, texture, sourceScaleOffset, blitMips, BlitType.OctahedralPaddingMultiply);
186 MarkGPUTextureValid(overrideInstanceID != -1 ? overrideInstanceID : texture.GetInstanceID(), blitMips);
187 }
188 }
189
190 void TextureSizeToPowerOfTwo(Texture texture, ref int width, ref int height)
191 {
192 // Change the width and height of the texture to be power of two
193 width = Mathf.NextPowerOfTwo(width);
194 height = Mathf.NextPowerOfTwo(height);
195 }
196
197 Vector2 GetPowerOfTwoTextureSize(Texture texture)
198 {
199 int width = texture.width, height = texture.height;
200
201 TextureSizeToPowerOfTwo(texture, ref width, ref height);
202 return new Vector2(width, height);
203 }
204
205 // Override the behavior when we add a texture so all non-pot textures are blitted to a pot target zone
206 /// <summary>
207 /// Allocate space from the atlas for a texture and copy texture contents into the atlas.
208 /// </summary>
209 /// <param name="cmd">Target command buffer for graphics commands.</param>
210 /// <param name="scaleOffset">Allocated scale (.xy) and offset (.zw)</param>
211 /// <param name="texture">Source Texture</param>
212 /// <param name="width">Request width in pixels.</param>
213 /// <param name="height">Request height in pixels.</param>
214 /// <param name="overrideInstanceID">Override texture instance ID.</param>
215 /// <returns>True on success, false otherwise.</returns>
216 public override bool AllocateTexture(CommandBuffer cmd, ref Vector4 scaleOffset, Texture texture, int width, int height, int overrideInstanceID = -1)
217 {
218 // This atlas only supports square textures
219 if (height != width)
220 {
221 Debug.LogError("Can't place " + texture + " in the atlas " + m_AtlasTexture.name + ": Only squared texture are allowed in this atlas.");
222 return false;
223 }
224
225 TextureSizeToPowerOfTwo(texture, ref height, ref width);
226
227 return base.AllocateTexture(cmd, ref scaleOffset, texture, width, height);
228 }
229
230 /// <summary>
231 /// Clear tracked requested textures.
232 /// </summary>
233 public void ResetRequestedTexture() => m_RequestedTextures.Clear();
234
235 /// <summary>
236 /// Reserves the space on the texture atlas
237 /// </summary>
238 /// <param name="texture">The source texture</param>
239 /// <returns>True if the space is reserved</returns>
240 public bool ReserveSpace(Texture texture) => ReserveSpace(texture, texture.width, texture.height);
241
242 /// <summary>
243 /// Reserves the space on the texture atlas
244 /// </summary>
245 /// <param name="texture">The source texture</param>
246 /// <param name="width">The width</param>
247 /// <param name="height">The height</param>
248 /// <returns>True if the space is reserved</returns>
249 public bool ReserveSpace(Texture texture, int width, int height)
250 => ReserveSpace(GetTextureID(texture), width, height);
251
252 /// <summary>
253 /// Reserves the space on the texture atlas
254 /// </summary>
255 /// <param name="textureA">The source texture A</param>
256 /// <param name="textureB">The source texture B</param>
257 /// <param name="width">The width</param>
258 /// <param name="height">The height</param>
259 /// <returns>True if the space is reserved</returns>
260 public bool ReserveSpace(Texture textureA, Texture textureB, int width, int height)
261 => ReserveSpace(GetTextureID(textureA, textureB), width, height);
262
263 /// <summary>
264 /// Reserves the space on the texture atlas
265 /// </summary>
266 /// <param name="id">The id</param>
267 /// <param name="width">The width</param>
268 /// <param name="height">The height</param>
269 /// <returns>True if the space is reserved</returns>
270 public bool ReserveSpace(int id, int width, int height)
271 {
272 m_RequestedTextures[id] = new Vector2Int(width, height);
273
274 // Cookie texture resolution changing between frame is a special case, so we handle it here.
275 // The texture will be re-allocated and may cause holes in the atlas texture, which is fine
276 // because when it doesn't have any more space, it will re-layout the texture correctly.
277 var cachedSize = GetCachedTextureSize(id);
278 if (!IsCached(out _, id) || cachedSize.x != width || cachedSize.y != height)
279 {
280 Vector4 scaleBias = Vector4.zero;
281 if (!AllocateTextureWithoutBlit(id, width, height, ref scaleBias))
282 return false;
283 }
284 return true;
285 }
286
287 /// <summary>
288 /// sort all the requested allocation from biggest to smallest and re-insert them.
289 /// This function does not moves the textures in the atlas, it only changes their coordinates
290 /// </summary>
291 /// <returns>True if all textures have successfully been re-inserted in the atlas</returns>
292 public bool RelayoutEntries()
293 {
294 var entries = new List<(int instanceId, Vector2Int size)>();
295
296 foreach (var entry in m_RequestedTextures)
297 entries.Add((entry.Key, entry.Value));
298 ResetAllocator();
299
300 // Sort entries from biggest to smallest
301 entries.Sort((c1, c2) =>
302 {
303 return c2.size.magnitude.CompareTo(c1.size.magnitude);
304 });
305
306 bool success = true;
307 Vector4 newScaleOffset = Vector4.zero;
308 foreach (var e in entries)
309 success &= AllocateTextureWithoutBlit(e.instanceId, e.size.x, e.size.y, ref newScaleOffset);
310
311 return success;
312 }
313
314 /// <summary>
315 /// Get cache size in bytes.
316 /// </summary>
317 /// <param name="nbElement">The number of elements stored in the cache.</param>
318 /// <param name="resolution">Atlas resolution (square).</param>
319 /// <param name="hasMipmap">Atlas uses mip maps.</param>
320 /// <param name="format">Atlas format.</param>
321 /// <returns>The approximate size of the cache in bytes.</returns>
322 public static long GetApproxCacheSizeInByte(int nbElement, int resolution, bool hasMipmap, GraphicsFormat format)
323 => (long)(nbElement * resolution * resolution * (double)((hasMipmap ? k_MipmapFactorApprox : 1.0f) * GraphicsFormatUtility.GetBlockSize(format)));
324
325 /// <summary>
326 /// Compute the max size of a power of two atlas for a given size in byte (weight).
327 /// </summary>
328 /// <param name="weight">Atlas size in bytes.</param>
329 /// <param name="hasMipmap">Atlas uses mip maps.</param>
330 /// <param name="format">Atlas format.</param>
331 /// <returns>The largest possible resolution for a square atlas, constrained by weight, as a power of two value.</returns>
332 public static int GetMaxCacheSizeForWeightInByte(int weight, bool hasMipmap, GraphicsFormat format)
333 {
334 float bytePerPixel = (float)GraphicsFormatUtility.GetBlockSize(format) * (hasMipmap ? k_MipmapFactorApprox : 1.0f);
335 var maxAtlasSquareSize = Mathf.Sqrt((float)weight / bytePerPixel);
336 return CoreUtils.PreviousPowerOfTwo((int)maxAtlasSquareSize);
337 }
338 }
339}