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 import inochi2d.core.nodes : inCreateUUID; 15 16 /** 17 Filtering mode for texture 18 */ 19 enum Filtering { 20 /** 21 Linear filtering will try to smooth out textures 22 */ 23 Linear, 24 25 /** 26 Point filtering will try to preserve pixel edges. 27 Due to texture sampling being float based this is imprecise. 28 */ 29 Point 30 } 31 32 /** 33 Texture wrapping modes 34 */ 35 enum Wrapping { 36 /** 37 Clamp texture sampling to be within the texture 38 */ 39 Clamp = GL_CLAMP_TO_BORDER, 40 41 /** 42 Wrap the texture in every direction idefinitely 43 */ 44 Repeat = GL_REPEAT, 45 46 /** 47 Wrap the texture mirrored in every direction indefinitely 48 */ 49 Mirror = GL_MIRRORED_REPEAT 50 } 51 52 /** 53 A texture which is not bound to an OpenGL context 54 Used for texture atlassing 55 */ 56 struct ShallowTexture { 57 public: 58 /** 59 8-bit RGBA color data 60 */ 61 ubyte[] data; 62 63 /** 64 Width of texture 65 */ 66 int width; 67 68 /** 69 Height of texture 70 */ 71 int height; 72 73 /** 74 Amount of color channels 75 */ 76 int channels; 77 78 /** 79 Amount of channels to conver to when passed to OpenGL 80 */ 81 int convChannels; 82 83 /** 84 Loads a shallow texture from image file 85 Supported file types: 86 * PNG 8-bit 87 * BMP 8-bit 88 * TGA 8-bit non-palleted 89 * JPEG baseline 90 */ 91 this(string file, int channels = 0) { 92 import std.file : read; 93 94 // Ensure we keep this ref alive until we're done with it 95 ubyte[] fData = cast(ubyte[])read(file); 96 97 // Load image from disk, as <channels> 8-bit 98 IFImage image = read_image(fData, 0, 8); 99 enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 100 scope(exit) image.free(); 101 102 // Copy data from IFImage to this ShallowTexture 103 this.data = new ubyte[image.buf8.length]; 104 this.data[] = image.buf8; 105 106 // Set the width/height data 107 this.width = image.w; 108 this.height = image.h; 109 this.channels = image.c; 110 this.convChannels = channels == 0 ? image.c : channels; 111 } 112 113 /** 114 Loads a shallow texture from image buffer 115 Supported file types: 116 * PNG 8-bit 117 * BMP 8-bit 118 * TGA 8-bit non-palleted 119 * JPEG baseline 120 121 By setting channels to a specific value you can force a specific color mode 122 */ 123 this(ubyte[] buffer, int channels = 0) { 124 125 // Load image from disk, as <channels> 8-bit 126 IFImage image = read_image(buffer, 0, 8); 127 enforce( image.e == 0, "%s".format(IF_ERROR[image.e])); 128 scope(exit) image.free(); 129 130 // Copy data from IFImage to this ShallowTexture 131 this.data = new ubyte[image.buf8.length]; 132 this.data[] = image.buf8; 133 134 // Set the width/height data 135 this.width = image.w; 136 this.height = image.h; 137 this.channels = image.c; 138 this.convChannels = channels == 0 ? image.c : channels; 139 } 140 141 /** 142 Loads uncompressed texture from memory 143 */ 144 this(ubyte[] buffer, int w, int h, int channels = 4) { 145 this.data = buffer; 146 147 // Set the width/height data 148 this.width = w; 149 this.height = h; 150 this.channels = channels; 151 this.convChannels = channels; 152 } 153 154 /** 155 Loads uncompressed texture from memory 156 */ 157 this(ubyte[] buffer, int w, int h, int channels = 4, int convChannels = 4) { 158 this.data = buffer; 159 160 // Set the width/height data 161 this.width = w; 162 this.height = h; 163 this.channels = channels; 164 this.convChannels = convChannels; 165 } 166 167 /** 168 Saves image 169 */ 170 void save(string file) { 171 import std.file : write; 172 import core.stdc.stdlib : free; 173 int e; 174 ubyte[] sData = write_image_mem(IF_PNG, this.width, this.height, this.data, channels, e); 175 enforce(!e, "%s".format(IF_ERROR[e])); 176 177 write(file, sData); 178 179 // Make sure we free the buffer 180 free(sData.ptr); 181 } 182 } 183 184 /** 185 A texture, only format supported is unsigned 8 bit RGBA 186 */ 187 class Texture { 188 private: 189 GLuint id; 190 int width_; 191 int height_; 192 193 GLuint inColorMode_; 194 GLuint outColorMode_; 195 int channels_; 196 197 uint uuid; 198 199 public: 200 201 /** 202 Loads texture from image file 203 Supported file types: 204 * PNG 8-bit 205 * BMP 8-bit 206 * TGA 8-bit non-palleted 207 * JPEG baseline 208 */ 209 this(string file, int channels = 0) { 210 import std.file : read; 211 212 // Ensure we keep this ref alive until we're done with it 213 ubyte[] fData = cast(ubyte[])read(file); 214 215 // Load image from disk, as RGBA 8-bit 216 IFImage image = read_image(fData, 0, 8); 217 enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 218 scope(exit) image.free(); 219 220 // Load in image data to OpenGL 221 this(image.buf8, image.w, image.h, image.c, channels == 0 ? image.c : channels); 222 uuid = inCreateUUID(); 223 } 224 225 /** 226 Creates a texture from a ShallowTexture 227 */ 228 this(ShallowTexture shallow) { 229 this(shallow.data, shallow.width, shallow.height, shallow.channels, shallow.convChannels); 230 } 231 232 /** 233 Creates a new empty texture 234 */ 235 this(int width, int height, int channels = 4) { 236 237 // Create an empty texture array with no data 238 ubyte[] empty = new ubyte[width_*height_*channels]; 239 240 // Pass it on to the other texturing 241 this(empty, width, height, channels, channels); 242 } 243 244 /** 245 Creates a new texture from specified data 246 */ 247 this(ubyte[] data, int width, int height, int inChannels = 4, int outChannels = 4) { 248 this.width_ = width; 249 this.height_ = height; 250 this.channels_ = outChannels; 251 252 this.inColorMode_ = GL_RGBA; 253 this.outColorMode_ = GL_RGBA; 254 if (inChannels == 1) this.inColorMode_ = GL_RED; 255 else if (inChannels == 2) this.inColorMode_ = GL_RG; 256 else if (inChannels == 3) this.inColorMode_ = GL_RGB; 257 if (outChannels == 1) this.outColorMode_ = GL_RED; 258 else if (outChannels == 2) this.outColorMode_ = GL_RG; 259 else if (outChannels == 3) this.outColorMode_ = GL_RGB; 260 261 // Generate OpenGL texture 262 glGenTextures(1, &id); 263 this.setData(data); 264 265 // Set default filtering and wrapping 266 this.setFiltering(Filtering.Linear); 267 this.setWrapping(Wrapping.Clamp); 268 this.setAnisotropy(incGetMaxAnisotropy()/2.0f); 269 uuid = inCreateUUID(); 270 } 271 272 ~this() { 273 dispose(); 274 } 275 276 /** 277 Width of texture 278 */ 279 int width() { 280 return width_; 281 } 282 283 /** 284 Height of texture 285 */ 286 int height() { 287 return height_; 288 } 289 290 /** 291 Gets the OpenGL color mode 292 */ 293 GLuint colorMode() { 294 return outColorMode_; 295 } 296 297 /** 298 Gets the channel count 299 */ 300 int channels() { 301 return channels_; 302 } 303 304 /** 305 Center of texture 306 */ 307 vec2i center() { 308 return vec2i(width_/2, height_/2); 309 } 310 311 /** 312 Gets the size of the texture 313 */ 314 vec2i size() { 315 return vec2i(width_, height_); 316 } 317 318 /** 319 Returns runtime UUID for texture 320 */ 321 uint getRuntimeUUID() { 322 return uuid; 323 } 324 325 /** 326 Set the filtering mode used for the texture 327 */ 328 void setFiltering(Filtering filtering) { 329 this.bind(); 330 glTexParameteri( 331 GL_TEXTURE_2D, 332 GL_TEXTURE_MIN_FILTER, 333 filtering == Filtering.Linear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST 334 ); 335 336 glTexParameteri( 337 GL_TEXTURE_2D, 338 GL_TEXTURE_MAG_FILTER, 339 filtering == Filtering.Linear ? GL_LINEAR : GL_NEAREST 340 ); 341 } 342 343 void setAnisotropy(float value) { 344 this.bind(); 345 glTexParameterf( 346 GL_TEXTURE_2D, 347 GL_TEXTURE_MAX_ANISOTROPY, 348 clamp(value, 1, incGetMaxAnisotropy()) 349 ); 350 } 351 352 /** 353 Set the wrapping mode used for the texture 354 */ 355 void setWrapping(Wrapping wrapping) { 356 this.bind(); 357 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapping); 358 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapping); 359 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, [0f, 0f, 0f, 0f].ptr); 360 } 361 362 /** 363 Sets the data of the texture 364 */ 365 void setData(ubyte[] data) { 366 this.bind(); 367 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 368 glPixelStorei(GL_PACK_ALIGNMENT, 1); 369 glTexImage2D(GL_TEXTURE_2D, 0, outColorMode_, width_, height_, 0, inColorMode_, GL_UNSIGNED_BYTE, data.ptr); 370 371 this.genMipmap(); 372 } 373 374 /** 375 Generate mipmaps 376 */ 377 void genMipmap() { 378 this.bind(); 379 glGenerateMipmap(GL_TEXTURE_2D); 380 } 381 382 /** 383 Sets a region of a texture to new data 384 */ 385 void setDataRegion(ubyte[] data, int x, int y, int width, int height, int channels = 4) { 386 this.bind(); 387 388 // Make sure we don't try to change the texture in an out of bounds area. 389 enforce( x >= 0 && x+width <= this.width_, "x offset is out of bounds (xoffset=%s, xbound=%s)".format(x+width, this.width_)); 390 enforce( y >= 0 && y+height <= this.height_, "y offset is out of bounds (yoffset=%s, ybound=%s)".format(y+height, this.height_)); 391 392 GLuint inChannelMode = GL_RGBA; 393 if (channels == 1) inChannelMode = GL_RED; 394 else if (channels == 2) inChannelMode = GL_RG; 395 else if (channels == 3) inChannelMode = GL_RGB; 396 397 // Update the texture 398 glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, inChannelMode, GL_UNSIGNED_BYTE, data.ptr); 399 400 this.genMipmap(); 401 } 402 403 /** 404 Bind this texture 405 406 Notes 407 - In release mode the unit value is clamped to 31 (The max OpenGL texture unit value) 408 - In debug mode unit values over 31 will assert. 409 */ 410 void bind(uint unit = 0) { 411 assert(unit <= 31u, "Outside maximum OpenGL texture unit value"); 412 glActiveTexture(GL_TEXTURE0+(unit <= 31u ? unit : 31u)); 413 glBindTexture(GL_TEXTURE_2D, id); 414 } 415 416 /** 417 Saves the texture to file 418 */ 419 void save(string file) { 420 write_image(file, width, height, getTextureData(true), channels_); 421 } 422 423 /** 424 Gets the texture data for the texture 425 */ 426 ubyte[] getTextureData(bool unmultiply=false) { 427 ubyte[] buf = new ubyte[width*height*channels_]; 428 bind(); 429 glGetTexImage(GL_TEXTURE_2D, 0, outColorMode_, GL_UNSIGNED_BYTE, buf.ptr); 430 if (unmultiply && channels == 4) { 431 inTexUnPremuliply(buf); 432 } 433 return buf; 434 } 435 436 /** 437 Gets this texture's texture id 438 */ 439 GLuint getTextureId() { 440 return id; 441 } 442 443 /** 444 Disposes texture from GL 445 */ 446 void dispose() { 447 glDeleteTextures(1, &id); 448 id = 0; 449 } 450 } 451 452 private { 453 Texture[] textureBindings; 454 bool started = false; 455 } 456 457 /** 458 Gets the maximum level of anisotropy 459 */ 460 float incGetMaxAnisotropy() { 461 float max; 462 glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &max); 463 return max; 464 } 465 466 /** 467 Begins a texture loading pass 468 */ 469 void inBeginTextureLoading() { 470 enforce(!started, "Texture loading pass already started!"); 471 started = true; 472 } 473 474 /** 475 Returns a texture from the internal texture list 476 */ 477 Texture inGetTextureFromId(uint id) { 478 enforce(started, "Texture loading pass not started!"); 479 return textureBindings[cast(size_t)id]; 480 } 481 482 /** 483 Gets the latest texture from the internal texture list 484 */ 485 Texture inGetLatestTexture() { 486 return textureBindings[$-1]; 487 } 488 489 /** 490 Adds binary texture 491 */ 492 void inAddTextureBinary(ShallowTexture data) { 493 textureBindings ~= new Texture(data); 494 } 495 496 /** 497 Ends a texture loading pass 498 */ 499 void inEndTextureLoading(bool checkErrors=true)() { 500 static if (checkErrors) enforce(started, "Texture loading pass not started!"); 501 started = false; 502 textureBindings.length = 0; 503 } 504 505 void inTexPremultiply(ref ubyte[] data, int channels = 4) { 506 if (channels < 4) return; 507 508 foreach(i; 0..data.length/channels) { 509 510 size_t offsetPixel = (i*channels); 511 data[offsetPixel+0] = cast(ubyte)((cast(int)data[offsetPixel+0] * cast(int)data[offsetPixel+3])/255); 512 data[offsetPixel+1] = cast(ubyte)((cast(int)data[offsetPixel+1] * cast(int)data[offsetPixel+3])/255); 513 data[offsetPixel+2] = cast(ubyte)((cast(int)data[offsetPixel+2] * cast(int)data[offsetPixel+3])/255); 514 } 515 } 516 517 void inTexUnPremuliply(ref ubyte[] data) { 518 foreach(i; 0..data.length/4) { 519 if (data[((i*4)+3)] == 0) continue; 520 521 data[((i*4)+0)] = cast(ubyte)(cast(int)data[((i*4)+0)] * 255 / cast(int)data[((i*4)+3)]); 522 data[((i*4)+1)] = cast(ubyte)(cast(int)data[((i*4)+1)] * 255 / cast(int)data[((i*4)+3)]); 523 data[((i*4)+2)] = cast(ubyte)(cast(int)data[((i*4)+2)] * 255 / cast(int)data[((i*4)+3)]); 524 } 525 }