1 /* 2 Copyright © 2020, Inochi2D Project 3 Distributed under the 2-Clause BSD License, see LICENSE file. 4 5 Authors: Luna Nielsen 6 */ 7 module inochi2d.core.texture; 8 import inochi2d.math; 9 import std.exception; 10 import std.format; 11 import bindbc.opengl; 12 import imagefmt; 13 import std.stdio; 14 15 /** 16 Filtering mode for texture 17 */ 18 enum Filtering { 19 /** 20 Linear filtering will try to smooth out textures 21 */ 22 Linear = GL_LINEAR_MIPMAP_LINEAR, 23 24 /** 25 Point filtering will try to preserve pixel edges. 26 Due to texture sampling being float based this is imprecise. 27 */ 28 Point = GL_POINT 29 } 30 31 /** 32 Texture wrapping modes 33 */ 34 enum Wrapping { 35 /** 36 Clamp texture sampling to be within the texture 37 */ 38 Clamp = GL_CLAMP_TO_BORDER, 39 40 /** 41 Wrap the texture in every direction idefinitely 42 */ 43 Repeat = GL_REPEAT, 44 45 /** 46 Wrap the texture mirrored in every direction indefinitely 47 */ 48 Mirror = GL_MIRRORED_REPEAT 49 } 50 51 /** 52 A texture which is not bound to an OpenGL context 53 Used for texture atlassing 54 */ 55 struct ShallowTexture { 56 public: 57 /** 58 8-bit RGBA color data 59 */ 60 ubyte[] data; 61 62 /** 63 Width of texture 64 */ 65 int width; 66 67 /** 68 Height of texture 69 */ 70 int height; 71 72 /** 73 Loads a shallow texture from image file 74 Supported file types: 75 * PNG 8-bit 76 * BMP 8-bit 77 * TGA 8-bit non-palleted 78 * JPEG baseline 79 */ 80 this(string file) { 81 import std.file : read; 82 83 // Ensure we keep this ref alive until we're done with it 84 ubyte[] fData = cast(ubyte[])read(file); 85 86 // Load image from disk, as RGBA 8-bit 87 IFImage image = read_image(fData, 4, 8); 88 enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 89 scope(exit) image.free(); 90 91 // Copy data from IFImage to this ShallowTexture 92 this.data = new ubyte[image.buf8.length]; 93 this.data[] = image.buf8; 94 95 // Set the width/height data 96 this.width = image.w; 97 this.height = image.h; 98 } 99 100 /** 101 Loads a shallow texture from image buffer 102 Supported file types: 103 * PNG 8-bit 104 * BMP 8-bit 105 * TGA 8-bit non-palleted 106 * JPEG baseline 107 */ 108 this(ubyte[] buffer) { 109 110 // Load image from disk, as RGBA 8-bit 111 IFImage image = read_image(buffer, 4, 8); 112 enforce( image.e == 0, "%s".format(IF_ERROR[image.e])); 113 scope(exit) image.free(); 114 115 // Copy data from IFImage to this ShallowTexture 116 this.data = new ubyte[image.buf8.length]; 117 this.data[] = image.buf8; 118 119 // Set the width/height data 120 this.width = image.w; 121 this.height = image.h; 122 } 123 124 /** 125 Loads uncompressed texture from memory 126 */ 127 this(ubyte[] buffer, int w, int h, int channels = 4) { 128 this.data = buffer; 129 130 // Set the width/height data 131 this.width = w; 132 this.height = h; 133 } 134 135 /** 136 Saves image 137 */ 138 void save(string file) { 139 import std.file : write; 140 import core.stdc.stdlib : free; 141 int e; 142 ubyte[] sData = write_image_mem(IF_PNG, this.width, this.height, this.data, 4, e); 143 enforce(!e, "%s".format(IF_ERROR[e])); 144 145 write(file, sData); 146 147 // Make sure we free the buffer 148 free(sData.ptr); 149 } 150 } 151 152 /** 153 A texture, only format supported is unsigned 8 bit RGBA 154 */ 155 class Texture { 156 private: 157 GLuint id; 158 int width_; 159 int height_; 160 161 GLuint colorMode; 162 int alignment; 163 164 public: 165 166 /** 167 Loads texture from image file 168 Supported file types: 169 * PNG 8-bit 170 * BMP 8-bit 171 * TGA 8-bit non-palleted 172 * JPEG baseline 173 */ 174 this(string file) { 175 import std.file : read; 176 177 // Ensure we keep this ref alive until we're done with it 178 ubyte[] fData = cast(ubyte[])read(file); 179 180 // Load image from disk, as RGBA 8-bit 181 IFImage image = read_image(fData, 4, 8); 182 enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 183 scope(exit) image.free(); 184 185 // Load in image data to OpenGL 186 this(image.buf8, image.w, image.h); 187 } 188 189 /** 190 Creates a texture from a ShallowTexture 191 */ 192 this(ShallowTexture shallow) { 193 this(shallow.data, shallow.width, shallow.height); 194 } 195 196 /** 197 Creates a new empty texture 198 */ 199 this(int width, int height, GLuint mode = GL_RGBA, int alignment = 4) { 200 201 // Create an empty texture array with no data 202 ubyte[] empty = new ubyte[width_*height_*alignment]; 203 204 // Pass it on to the other texturing 205 this(empty, width, height, mode, alignment); 206 } 207 208 /** 209 Creates a new texture from specified data 210 */ 211 this(ubyte[] data, int width, int height, GLuint mode = GL_RGBA, int alignment = 4) { 212 this.colorMode = mode; 213 this.alignment = alignment; 214 this.width_ = width; 215 this.height_ = height; 216 217 // Generate OpenGL texture 218 glGenTextures(1, &id); 219 this.setData(data); 220 221 // Set default filtering and wrapping 222 this.setFiltering(Filtering.Linear); 223 this.setWrapping(Wrapping.Clamp); 224 } 225 226 /** 227 Width of texture 228 */ 229 int width() { 230 return width_; 231 } 232 233 /** 234 Height of texture 235 */ 236 int height() { 237 return height_; 238 } 239 240 /** 241 Center of texture 242 */ 243 vec2i center() { 244 return vec2i(width_/2, height_/2); 245 } 246 247 /** 248 Gets the size of the texture 249 */ 250 vec2i size() { 251 return vec2i(width_, height_); 252 } 253 254 /** 255 Set the filtering mode used for the texture 256 */ 257 void setFiltering(Filtering filtering) { 258 this.bind(); 259 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filtering); 260 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filtering); 261 262 glBindTexture(GL_TEXTURE_2D, id); 263 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 16); 264 } 265 266 /** 267 Set the wrapping mode used for the texture 268 */ 269 void setWrapping(Wrapping wrapping) { 270 this.bind(); 271 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapping); 272 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapping); 273 274 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, [0f, 0f, 0f, 0f].ptr); 275 } 276 277 /** 278 Sets the data of the texture 279 */ 280 void setData(ubyte[] data) { 281 this.bind(); 282 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 283 glTexImage2D(GL_TEXTURE_2D, 0, colorMode, width_, height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.ptr); 284 285 this.genMipmap(); 286 } 287 288 /** 289 Generate mipmaps 290 */ 291 void genMipmap() { 292 this.bind(); 293 glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST); 294 glGenerateMipmap(GL_TEXTURE_2D); 295 } 296 297 /** 298 Sets a region of a texture to new data 299 */ 300 void setDataRegion(ubyte[] data, int x, int y, int width, int height) { 301 this.bind(); 302 303 // Make sure we don't try to change the texture in an out of bounds area. 304 enforce( x >= 0 && x+width <= this.width_, "x offset is out of bounds (xoffset=%s, xbound=%s)".format(x+width, this.width_)); 305 enforce( y >= 0 && y+height <= this.height_, "y offset is out of bounds (yoffset=%s, ybound=%s)".format(y+height, this.height_)); 306 307 // Update the texture 308 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 309 glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, this.colorMode, GL_UNSIGNED_BYTE, data.ptr); 310 311 this.genMipmap(); 312 } 313 314 /** 315 Bind this texture 316 317 Notes 318 - In release mode the unit value is clamped to 31 (The max OpenGL texture unit value) 319 - In debug mode unit values over 31 will assert. 320 */ 321 void bind(uint unit = 0) { 322 assert(unit <= 31u, "Outside maximum OpenGL texture unit value"); 323 glActiveTexture(GL_TEXTURE0+(unit <= 31u ? unit : 31u)); 324 glBindTexture(GL_TEXTURE_2D, id); 325 } 326 327 /** 328 Saves the texture to file 329 */ 330 void save(string file) { 331 write_image(file, width, height, getTextureData(), 4); 332 } 333 334 /** 335 Gets the texture data for the texture 336 */ 337 ubyte[] getTextureData() { 338 ubyte[] buf = new ubyte[width*height*4]; 339 bind(); 340 glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf.ptr); 341 return buf; 342 } 343 344 /** 345 Gets this texture's texture id 346 */ 347 GLuint getTextureId() { 348 return id; 349 } 350 } 351 352 private { 353 Texture[] textureBindings; 354 bool started = false; 355 } 356 357 /** 358 Begins a texture loading pass 359 */ 360 void inBeginTextureLoading() { 361 enforce(!started, "Texture loading pass already started!"); 362 started = true; 363 } 364 365 /** 366 Returns a texture from the internal texture list 367 */ 368 Texture inGetTextureFromId(uint id) { 369 enforce(started, "Texture loading pass not started!"); 370 return textureBindings[cast(size_t)id]; 371 } 372 373 /** 374 Gets the latest texture from the internal texture list 375 */ 376 Texture inGetLatestTexture() { 377 return textureBindings[$-1]; 378 } 379 380 /** 381 Adds binary texture 382 */ 383 void inAddTextureBinary(ShallowTexture data) { 384 textureBindings ~= new Texture(data); 385 } 386 387 /** 388 Ends a texture loading pass 389 */ 390 void inEndTextureLoading() { 391 enforce(started, "Texture loading pass not started!"); 392 started = false; 393 textureBindings.length = 0; 394 } 395 396 void inTexPremultiply(ref ubyte[] data) { 397 foreach(i; 0..data.length/4) { 398 data[((i*4)+0)] = cast(ubyte)((cast(int)data[((i*4)+0)] * cast(int)data[((i*4)+3)])/255); 399 data[((i*4)+1)] = cast(ubyte)((cast(int)data[((i*4)+1)] * cast(int)data[((i*4)+3)])/255); 400 data[((i*4)+2)] = cast(ubyte)((cast(int)data[((i*4)+2)] * cast(int)data[((i*4)+3)])/255); 401 } 402 } 403 404 void inTexUnPremuliply(ref ubyte[] data) { 405 foreach(i; 0..data.length/4) { 406 if (data[((i*4)+3)] == 0) continue; 407 408 data[((i*4)+0)] = cast(ubyte)(cast(int)data[((i*4)+0)] * 255 / cast(int)data[((i*4)+3)]); 409 data[((i*4)+1)] = cast(ubyte)(cast(int)data[((i*4)+1)] * 255 / cast(int)data[((i*4)+3)]); 410 data[((i*4)+2)] = cast(ubyte)(cast(int)data[((i*4)+2)] * 255 / cast(int)data[((i*4)+3)]); 411 } 412 }