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 /**
122     An area in a texture atlas
123 */
124 struct AtlasArea {
125     /**
126         The area in pixels
127     */
128     vec4 area;
129 
130     /**
131         The UV coordinates
132     */
133     vec4 uv;
134 }
135 
136 /**
137     A texture atlas
138 */
139 class TextureAtlas {
140 package(engine.render):
141     Texture texture;
142 
143 private:
144     TexturePacker packer;
145     AtlasArea[string] entries;
146 
147 public:
148 
149     /**
150         Creates a new texture atlas
151     */
152     this(vec2i textureSize) {
153         texture = new Texture(textureSize.x, textureSize.y);
154         packer = new TexturePacker(textureSize);
155     }
156 
157     /**
158         Gets the UV points for the index
159     */
160     AtlasArea opIndex(string name) {
161         return entries[name];
162     }
163 
164     /**
165         Bind the atlas texture
166     */
167     void bind(uint unit = 0) {
168         texture.bind(unit);
169     }
170 
171     /**
172         Set filtering used for the texture
173     */
174     void setFiltering(Filtering filtering) {
175         texture.setFiltering(filtering);
176     }
177 
178     /**
179         Add texture to the atlas from a file
180     */
181     AtlasArea add(string name, string file) {
182         return add(name, ShallowTexture(file));
183     }
184 
185     /**
186         Add texture to the atlas
187     */
188     AtlasArea add(string name, ShallowTexture shallowTexture) {
189         enforce(name !in entries, "Texture with name '%s' is already in the atlas".format(name));
190 
191         // Get packing position of texture
192         vec4i texpos = packer.packTexture(vec2i(shallowTexture.width, shallowTexture.height));
193 
194         // Texture does not fit in this atlas.
195         if (!texpos.isFinite) return AtlasArea(vec4.init, vec4.init);
196 
197         // Put it in to the texture and set its entry
198         texture.setDataRegion(shallowTexture.data, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height);
199 
200         debug {
201             AppLog.info("debug", "Packed texture %s in to region (%s, %s, %s, %s)", name, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height);
202         }
203 
204         // Calculate UV coordinates and put them in to the table
205         vec2 texSize = vec2(cast(float)texture.width, cast(float)texture.height);
206         vec4 texArea = vec4(
207             texpos.x, 
208             texpos.y, 
209             shallowTexture.width,
210             shallowTexture.height
211         );
212 
213         vec4 uvPoints = vec4(
214             texArea.x/texSize.x,
215             texArea.y/texSize.y, 
216             (texArea.x+texArea.z)/texSize.x, 
217             (texArea.y+texArea.w)/texSize.y
218         );
219 
220         // Adjust UV points to avoid oversampling
221         uvPoints.x += 0.25/texSize.x;
222         uvPoints.y += 0.25/texSize.y;
223         uvPoints.z -= 0.25/texSize.x;
224         uvPoints.w -= 0.25/texSize.y;
225         entries[name] = AtlasArea(texArea, uvPoints);
226         return entries[name];
227     }
228 
229     /**
230         Remove an entry from the atlas
231     */
232     void remove(string name) {
233         packer.remove(vec4i(
234                 cast(int)entries[name].area.x, 
235                 cast(int)entries[name].area.y, 
236                 cast(int)entries[name].area.z, 
237                 cast(int)entries[name].area.w
238             )
239         );
240         entries.remove(name);
241     }
242 
243     /**
244         Clears the texture atlas
245     */
246     void clear() {
247         packer.clear();
248         entries.clear();
249     }
250 }