1 module isodi.resources.loader; 2 3 import raylib; 4 import std.algorithm; 5 6 import isodi.chunk; 7 import isodi.utils; 8 import isodi.skeleton; 9 10 11 @safe: 12 13 14 /// Interface for resource loading, can be specified in the `Properties` of each object. 15 /// 16 /// Use `Pack` and `getPack` to use Isodi's default pack loader. 17 /// 18 /// The loader should be responsible for allocating, caching and freeing the textures. 19 interface ResourceLoader { 20 21 /// Get options for the given resource. 22 const(ResourceOptions)* options(ResourceType resource, string name); 23 24 /// Load texture for a chunk. 25 Texture2D blockTexture(string[] names, out BlockUV[BlockType] uv) 26 in (isSorted(names)); 27 28 ///// Load texture for the given bone sets. 29 Texture2D boneSetTexture(string[] names, out BoneUV[BoneType] uv) 30 in (isSorted(names)); 31 32 /// Load bones for a skeleton, using the given bone set. 33 Bone[] skeleton(string name, string boneSet); 34 35 // TODO packImages wrapper to support variably sized images. 36 37 /// Combine the given images to create an atlas map. Each image must be square with dimensions equal to a power of 38 /// two, eg. 32×32 or 128x128. All images must be of equal size. 39 /// 40 /// UV type must be a `RectangleI` or struct containing `RectangleI`s. The new mapping will offset the positions 41 /// of those rectangles to match those of the newly made image. 42 /// 43 /// If given only one image, returns it unchanged. 44 static Image packImages(UV, Type)(return scope UV[Type][] mapping, return scope Image[] images, 45 out UV[Type] newMapping) 46 in (images.length != 0, 47 "No images given") 48 in (images.all!"a.width == a.height && !(a.width & (a.width-1))", 49 "All images are required to be square with dimensions equal to a power of two, eg. 32x32") 50 in (images.isSorted!"a.width > b.width", 51 "Given images must be sorted descending by size") 52 in (mapping.length == images.length, 53 "Map and image count must be equal") 54 out (r; r.width == r.height, 55 "Output image must be square") 56 out (r; !(r.width & (r.width - 1)), 57 "Output image must have dimensions equal to a power of two") 58 do { 59 60 import std.math, std.range, std.traits; 61 62 static assert(is(UV == struct), "Mapping type must be a struct"); 63 64 T offsetUV(T)(T uv, int x, int y) { 65 66 // Given a rectangle, offset it 67 static if (is(T == RectangleI)) { 68 69 return RectangleI(uv.x + x, uv.y + y, uv.width, uv.height); 70 71 } 72 73 // Something else! 74 else { 75 76 // Check each field of the struct 77 foreach (ref field; uv.tupleof) { 78 79 // Search for rectangles 80 static if (is(typeof(field) == RectangleI)) { 81 82 // Apply offset to them 83 field = offsetUV(field, x, y); 84 85 } 86 87 } 88 89 return uv; 90 91 } 92 93 } 94 95 // Just one image 96 if (mapping.length == 1) { 97 98 // Nothing to do 99 newMapping = mapping[0]; 100 return images[0]; 101 102 } 103 104 // Allocate the new image 105 const imagesPerRow = cast(int) sqrt(cast(real) mapping.length).ceil; 106 const partSize = images[0].width; 107 const size = partSize * imagesPerRow; 108 109 Image result = { 110 data: &(new Color[size * size])[0], 111 width: size, 112 height: size, 113 mipmaps: 1, 114 format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 115 }; 116 117 int i, j; 118 119 foreach (map, image; zip(mapping, cast(const(Image)[]) images)) { 120 121 // Advance to the next spot 122 scope (success) { 123 i += 1; 124 j += i / imagesPerRow; 125 i %= imagesPerRow; 126 } 127 128 const offsetX = partSize*i; 129 const offsetY = partSize*j; 130 131 // Update the mapping 132 foreach (key, value; map) { 133 134 newMapping[key] = offsetUV(value, offsetX, offsetY); 135 136 } 137 138 // Place the image 139 () @trusted { 140 141 // Note: ImageDraw is very imprecise 142 143 // Copy the pixels over 144 foreach (y; 0 .. partSize) 145 foreach (x; 0 .. partSize) { 146 147 ImageDrawPixel(&result, offsetX + x, offsetY + y, GetImageColor(cast() image, x, y)); 148 149 } 150 151 }(); 152 153 } 154 155 return result; 156 157 } 158 159 unittest { 160 161 Color[2][2] colorsA = [ 162 [Color(1, 1, 1, 1), Color(2, 2, 2, 2)], 163 [Color(3, 3, 3, 3), Color(4, 4, 4, 4)], 164 ]; 165 Color[2][2] colorsB = [ 166 [Color(5, 5, 5, 5), Color(6, 6, 6, 6)], 167 [Color(7, 7, 7, 7), Color(8, 8, 8, 8)], 168 ]; 169 170 scope Image imageA = { 171 data: &colorsA, 172 width: 2, 173 height: 2, 174 mipmaps: 1, 175 format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 176 }; 177 scope Image imageB = { 178 data: &colorsB, 179 width: 2, 180 height: 2, 181 mipmaps: 1, 182 format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 183 }; 184 185 RectangleI[string] mapA = [ 186 "a": RectangleI(0, 0, 1, 2), 187 ]; 188 RectangleI[string] mapB = [ 189 "b": RectangleI(0, 0, 1, 2), 190 "c": RectangleI(1, 0, 1, 2), 191 ]; 192 RectangleI[string] map; 193 194 auto image = packImages([mapA, mapB], [imageA, imageB], map); 195 196 () @trusted { 197 198 assert(image.GetImageColor(0, 0) == Color(1, 1, 1, 1)); 199 assert(image.GetImageColor(1, 0) == Color(2, 2, 2, 2)); 200 assert(image.GetImageColor(2, 0) == Color(5, 5, 5, 5)); 201 assert(image.GetImageColor(3, 0) == Color(6, 6, 6, 6)); 202 assert(image.GetImageColor(3, 1) == Color(8, 8, 8, 8)); 203 assert(image.GetImageColor(3, 3) == Color(0, 0, 0, 0)); 204 205 }(); 206 207 assert(map == [ 208 "a": RectangleI(0, 0, 1, 2), 209 "b": RectangleI(2, 0, 1, 2), 210 "c": RectangleI(3, 0, 1, 2), 211 ]); 212 213 } 214 215 } 216 217 /// Type of the resource. 218 enum ResourceType { 219 220 block, 221 bone, 222 skeleton, 223 224 } 225 226 /// Resource options 227 struct ResourceOptions { 228 229 /// If true, a filter will be applied to smooth out the texture. This should be off for pixel art packs. 230 bool interpolate = true; 231 232 // TODO better docs 233 234 /// Size of the tile texture (both width and height). 235 /// 236 /// Required. 237 uint tileSize; 238 239 /// Side texture height. 240 uint sideSize; 241 242 /// Amount of angles each multi-directional texture will provide. All angles should be placed in a single 243 /// row in the image. 244 /// 245 /// 4 angles means the textures have a separate sprite for every 90 degrees, 8 angles — 45 degrees, 246 /// and so on. 247 /// 248 /// Defaults to `4`. 249 uint angles = 4; 250 251 int[4] tileArea; 252 int[4] sideArea; 253 254 auto blockUV() const => BlockUV( 255 cast(RectangleI) tileArea, 256 cast(RectangleI) sideArea, 257 tileSize, 258 sideSize, 259 ); 260 261 }