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 }