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 }