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 }