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 }