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 }