1 /*
2     Copyright © 2020, Luna Nielsen
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module engine.render.texture.atlas;
8 import engine.render.texture;
9 import gl3n.linalg;
10 import std.exception;
11 import std.format;
12 import engine;
13 
14 /**
15     The game's static texture atlas collection
16 */
17 static AtlasCollection GameAtlas;
18 
19 /**
20     An index in the atlas collection
21     
22     It is safe to keep this value around for caching
23 */
24 struct AtlasIndex {
25 private:
26     TextureAtlas parentAtlas;
27 
28 package(engine.render):
29     Texture texture() {
30         return parentAtlas.texture;
31     }
32 
33 public:
34     /**
35         The UV points of the texture
36     */
37     vec4 uv;
38 
39     /**
40         The area of the texture in the atlas
41     */
42     vec4 area;
43 
44     /**
45         Bind the atlas texture
46     */
47     void bind(uint unit = 0) {
48         parentAtlas.bind(unit);
49     }
50 }
51 
52 /***/
53 class AtlasCollection {
54 private:
55     TextureAtlas[] atlasses;
56     AtlasIndex[string] texTable;
57     Filtering defaultFilter = Filtering.Point;
58 
59 public:
60     /**
61         Gets the uv and atlas pointer for the index
62     */
63     AtlasIndex opIndex(string name) {
64         return texTable[name];
65     }
66 
67     /**
68         Gets whether the atlas has the specified name
69     */
70     bool has(string name) {
71         return (name in texTable) !is null;
72     }
73 
74     /**
75         Add texture to the atlas from a file
76     */
77     void add(string name, string file, size_t atlas=0) {
78         add(name, ShallowTexture(file), atlas);
79     }
80     
81     /**
82         Sets the filtering mode for the collection
83     */
84     void setFiltering(Filtering filtering) {
85         defaultFilter = filtering;
86         foreach(atlas; atlasses) {
87             atlas.setFiltering(filtering);
88         }
89     }
90 
91     /**
92         Add texture to the atlas collection
93     */
94     void add(string name, ShallowTexture shallowTexture, size_t atlas=0) {
95         enforce(name !in texTable, "Texture with name '%s' is already in the atlas collection".format(name));
96 
97         // Add new atlas
98         if (atlas >= atlasses.length) {
99             AppLog.info("AtlasCollection", "All atlases were out of space, creating new atlas %s...", atlasses.length);
100             atlasses ~= new TextureAtlas(vec2i(4096, 4096));
101             atlasses[$-1].setFiltering(defaultFilter);
102         }
103 
104         // Add to atlas and get uvs
105         AtlasArea area = atlasses[atlas].add(name, shallowTexture);
106 
107         // Height is 0 if it couldn't fit
108         if (!area.area.isFinite) {
109 
110             // Try the next atlas
111             add(name, shallowTexture, atlas+1);
112             return;
113         }
114 
115         // Put the texture and its uvs in to the table
116         texTable[name] = AtlasIndex(atlasses[atlas], area.uv, area.area);
117 
118     }
119 
120     /**
121         Remove named texture
122     */
123     void remove(string name) {
124         if (name in texTable) {
125             texTable[name].parentAtlas.remove(name);
126         }
127     }
128 }
129 
130 /**
131     An area in a texture atlas
132 */
133 struct AtlasArea {
134     /**
135         The area in pixels
136     */
137     vec4 area;
138 
139     /**
140         The UV coordinates
141     */
142     vec4 uv;
143 }
144 
145 /**
146     A texture atlas
147 */
148 class TextureAtlas {
149 package(engine.render):
150     Texture texture;
151 
152 private:
153     TexturePacker packer;
154     AtlasArea[string] entries;
155 
156 public:
157 
158     /**
159         Creates a new texture atlas
160     */
161     this(vec2i textureSize) {
162         texture = new Texture(textureSize.x, textureSize.y);
163         packer = new TexturePacker(textureSize);
164     }
165 
166     /**
167         Gets the UV points for the index
168     */
169     AtlasArea opIndex(string name) {
170         return entries[name];
171     }
172 
173     /**
174         Bind the atlas texture
175     */
176     void bind(uint unit = 0) {
177         texture.bind(unit);
178     }
179 
180     /**
181         Set filtering used for the texture
182     */
183     void setFiltering(Filtering filtering) {
184         texture.setFiltering(filtering);
185     }
186 
187     /**
188         Add texture to the atlas from a file
189     */
190     AtlasArea add(string name, string file) {
191         return add(name, ShallowTexture(file));
192     }
193 
194     /**
195         Add texture to the atlas
196     */
197     AtlasArea add(string name, ShallowTexture shallowTexture) {
198         enforce(name !in entries, "Texture with name '%s' is already in the atlas".format(name));
199 
200         // Get packing position of texture
201         vec4i texpos = packer.packTexture(vec2i(shallowTexture.width, shallowTexture.height));
202 
203         // Texture does not fit in this atlas.
204         if (!texpos.isFinite) return AtlasArea(vec4.init, vec4.init);
205 
206         // Put it in to the texture and set its entry
207         texture.setDataRegion(shallowTexture.data, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height);
208 
209         debug {
210             AppLog.info("debug", "Packed texture %s in to region (%s, %s, %s, %s)", name, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height);
211         }
212 
213         // Calculate UV coordinates and put them in to the table
214         vec2 texSize = vec2(cast(float)texture.width, cast(float)texture.height);
215         vec4 texArea = vec4(
216             texpos.x, 
217             texpos.y, 
218             shallowTexture.width,
219             shallowTexture.height
220         );
221 
222         vec4 uvPoints = vec4(
223             texArea.x/texSize.x,
224             texArea.y/texSize.y, 
225             (texArea.x+texArea.z)/texSize.x, 
226             (texArea.y+texArea.w)/texSize.y
227         );
228         
229         entries[name] = AtlasArea(texArea, uvPoints);
230         return entries[name];
231     }
232 
233     /**
234         Remove an entry from the atlas
235     */
236     void remove(string name) {
237         packer.remove(vec4i(
238                 cast(int)entries[name].area.x, 
239                 cast(int)entries[name].area.y, 
240                 cast(int)entries[name].area.z, 
241                 cast(int)entries[name].area.w
242             )
243         );
244         entries.remove(name);
245     }
246 
247     /**
248         Clears the texture atlas
249     */
250     void clear() {
251         packer.clear();
252         entries.clear();
253     }
254 }