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, 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 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( 260 GL_TEXTURE_2D, 261 GL_TEXTURE_MIN_FILTER, 262 filtering == Filtering.Linear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST 263 ); 264 glTexParameteri( 265 GL_TEXTURE_2D, 266 GL_TEXTURE_MAG_FILTER, 267 filtering == Filtering.Linear ? GL_LINEAR : GL_NEAREST 268 ); 269 270 glBindTexture(GL_TEXTURE_2D, id); 271 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 16); 272 } 273 274 /** 275 Set the wrapping mode used for the texture 276 */ 277 void setWrapping(Wrapping wrapping) { 278 this.bind(); 279 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapping); 280 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapping); 281 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, [0f, 0f, 0f, 0f].ptr); 282 } 283 284 /** 285 Sets the data of the texture 286 */ 287 void setData(ubyte[] data) { 288 this.bind(); 289 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 290 glTexImage2D(GL_TEXTURE_2D, 0, colorMode, width_, height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.ptr); 291 292 this.genMipmap(); 293 } 294 295 /** 296 Generate mipmaps 297 */ 298 void genMipmap() { 299 this.bind(); 300 glGenerateMipmap(GL_TEXTURE_2D); 301 } 302 303 /** 304 Sets a region of a texture to new data 305 */ 306 void setDataRegion(ubyte[] data, int x, int y, int width, int height) { 307 this.bind(); 308 309 // Make sure we don't try to change the texture in an out of bounds area. 310 enforce( x >= 0 && x+width <= this.width_, "x offset is out of bounds (xoffset=%s, xbound=%s)".format(x+width, this.width_)); 311 enforce( y >= 0 && y+height <= this.height_, "y offset is out of bounds (yoffset=%s, ybound=%s)".format(y+height, this.height_)); 312 313 // Update the texture 314 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 315 glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, this.colorMode, GL_UNSIGNED_BYTE, data.ptr); 316 317 this.genMipmap(); 318 } 319 320 /** 321 Bind this texture 322 323 Notes 324 - In release mode the unit value is clamped to 31 (The max OpenGL texture unit value) 325 - In debug mode unit values over 31 will assert. 326 */ 327 void bind(uint unit = 0) { 328 assert(unit <= 31u, "Outside maximum OpenGL texture unit value"); 329 glActiveTexture(GL_TEXTURE0+(unit <= 31u ? unit : 31u)); 330 glBindTexture(GL_TEXTURE_2D, id); 331 } 332 333 /** 334 Saves the texture to file 335 */ 336 void save(string file) { 337 write_image(file, width, height, getTextureData(), 4); 338 } 339 340 /** 341 Gets the texture data for the texture 342 */ 343 ubyte[] getTextureData() { 344 ubyte[] buf = new ubyte[width*height*4]; 345 bind(); 346 glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf.ptr); 347 return buf; 348 } 349 350 /** 351 Gets this texture's texture id 352 */ 353 GLuint getTextureId() { 354 return id; 355 } 356 } 357 358 private { 359 Texture[] textureBindings; 360 bool started = false; 361 } 362 363 /** 364 Begins a texture loading pass 365 */ 366 void inBeginTextureLoading() { 367 enforce(!started, "Texture loading pass already started!"); 368 started = true; 369 } 370 371 /** 372 Returns a texture from the internal texture list 373 */ 374 Texture inGetTextureFromId(uint id) { 375 enforce(started, "Texture loading pass not started!"); 376 return textureBindings[cast(size_t)id]; 377 } 378 379 /** 380 Gets the latest texture from the internal texture list 381 */ 382 Texture inGetLatestTexture() { 383 return textureBindings[$-1]; 384 } 385 386 /** 387 Adds binary texture 388 */ 389 void inAddTextureBinary(ShallowTexture data) { 390 textureBindings ~= new Texture(data); 391 } 392 393 /** 394 Ends a texture loading pass 395 */ 396 void inEndTextureLoading() { 397 enforce(started, "Texture loading pass not started!"); 398 started = false; 399 textureBindings.length = 0; 400 } 401 402 void inTexPremultiply(ref ubyte[] data) { 403 foreach(i; 0..data.length/4) { 404 data[((i*4)+0)] = cast(ubyte)((cast(int)data[((i*4)+0)] * cast(int)data[((i*4)+3)])/255); 405 data[((i*4)+1)] = cast(ubyte)((cast(int)data[((i*4)+1)] * cast(int)data[((i*4)+3)])/255); 406 data[((i*4)+2)] = cast(ubyte)((cast(int)data[((i*4)+2)] * cast(int)data[((i*4)+3)])/255); 407 } 408 } 409 410 void inTexUnPremuliply(ref ubyte[] data) { 411 foreach(i; 0..data.length/4) { 412 if (data[((i*4)+3)] == 0) continue; 413 414 data[((i*4)+0)] = cast(ubyte)(cast(int)data[((i*4)+0)] * 255 / cast(int)data[((i*4)+3)]); 415 data[((i*4)+1)] = cast(ubyte)(cast(int)data[((i*4)+1)] * 255 / cast(int)data[((i*4)+3)]); 416 data[((i*4)+2)] = cast(ubyte)(cast(int)data[((i*4)+2)] * 255 / cast(int)data[((i*4)+3)]); 417 } 418 }