1 /*
2     Copyright © 2020, Luna Nielsen
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module engine.render.batcher;
8 import engine;
9 
10 private {
11 
12     /// How many entries in a SpriteBatch
13     enum EntryCount = 10_000;
14 
15     // Various variables that make it easier to reference sizes
16     enum VecSize = 2;
17     enum UVSize = 2;
18     enum ColorSize = 4;
19     enum VertsCount = 6;
20     enum DataLength = VecSize+UVSize+ColorSize;
21     enum DataSize = DataLength*VertsCount;
22 
23     Shader spriteBatchShader;
24     Camera2D batchCamera;
25 
26     vec2 transformVerts(vec2 position, mat4 matrix) {
27         return vec2(matrix*vec4(position.x, position.y, 0, 1));
28     }
29 }
30 
31 /**
32     Global game sprite batcher
33 */
34 static SpriteBatch GameBatch;
35 
36 /**
37     Sprite flipping
38 */
39 enum SpriteFlip {
40     None = 0,
41     Horizontal = 1,
42     Vertical = 2
43 }
44 
45 /**
46     Batches Texture objects for 2D drawing
47 */
48 class SpriteBatch {
49 private:
50     float[DataSize*EntryCount] data;
51     size_t dataOffset;
52     size_t tris;
53 
54     GLuint vao;
55     GLuint buffer;
56     GLint vp;
57 
58     Texture currentTexture;
59     Framebuffer currentFboTex;
60 
61     void addVertexData(vec2 position, vec2 uvs, vec4 color) {
62         data[dataOffset..dataOffset+DataLength] = [position.x, position.y, uvs.x, uvs.y, color.x, color.y, color.z, color.w];
63         dataOffset += DataLength;
64     }
65 
66 public:
67 
68     /**
69         Constructor
70     */
71     this() {
72         data = new float[DataSize*EntryCount];
73 
74         glGenVertexArrays(1, &vao);
75         glBindVertexArray(vao);
76 
77         glGenBuffers(1, &buffer);
78         glBindBuffer(GL_ARRAY_BUFFER, buffer);
79         glBufferData(GL_ARRAY_BUFFER, float.sizeof*data.length, data.ptr, GL_DYNAMIC_DRAW);
80 
81         batchCamera = new Camera2D();
82         spriteBatchShader = new Shader(import("shaders/batch.vert"), import("shaders/batch.frag"));
83         vp = spriteBatchShader.getUniformLocation("vp");
84     }
85 
86     /**
87         Draws texture from atlas
88     */
89     void draw(string item, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1, 1, 1, 1)) {
90         auto index = GameAtlas[item];
91         draw(index, position, cutout, origin, rotation, flip, color);
92     }
93 
94     /**
95         Draws cached atlas index
96     */
97     void draw(AtlasIndex index, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1)) {
98         
99         vec4 fCutout = index.area;
100         if (cutout.isFinite) {
101 
102             // Clamp the cutout to fit within the texture's area
103             vec4 cutoutClamped = vec4(
104                 clamp(cutout.x, 0, index.area.z),
105                 clamp(cutout.y, 0, index.area.w),
106                 clamp(cutout.z, 0, index.area.z),
107                 clamp(cutout.w, 0, index.area.w),
108             );
109             
110             // Cut in to the area
111             fCutout = vec4(
112                 index.area.x+cutoutClamped.x,
113                 index.area.y+cutoutClamped.y,
114                 cutoutClamped.z,
115                 cutoutClamped.w,
116             );
117         }
118         
119         draw(index.texture, position, fCutout, origin, rotation, flip, color);
120     }
121 
122     /**
123         Draws the texture
124 
125         Remember to call flush after drawing all the textures you want
126 
127         Flush will automatically be called if your draws exceed the max count
128         Flush will automatically be called if you queue an other texture
129     */
130     void draw(Texture texture, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1)) {
131 
132         // Flush if neccesary
133         if (dataOffset == DataSize*EntryCount) flush();
134         if (texture != currentTexture) flush();
135 
136         // Update current texture
137         currentTexture = texture;
138 
139         // Calculate rotation, position and scaling.
140         mat4 transform =
141             mat4.translation(-origin.x, -origin.y, 0) *
142             mat4.translation(position.x, position.y, 0) *
143             mat4.translation(origin.x, origin.y, 0) *
144             mat4.zrotation(rotation) * 
145             mat4.translation(-origin.x, -origin.y, 0) *
146             mat4.scaling(position.z, position.w, 0);
147 
148         // If cutout has not been set (all values are NaN or infinity) we set it to use the entire texture
149         if (!cutout.isFinite) {
150             cutout = vec4(0, 0, texture.width, texture.height);
151         }
152 
153         // Get the area of the texture with a tiny bit cut off to avoid textures bleeding in to each other
154         // TODO: add a 1x1 px transparent border around textures instead?
155         enum cutoffOffset = 0.8;
156         enum cutoffAmount = cutoffOffset*2;
157 
158         vec4 uvArea = vec4(
159             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x+cutout.z)-cutoffAmount : (cutout.x)+cutoffOffset,
160             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y+cutout.w)-cutoffAmount : (cutout.y)+cutoffOffset,
161             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x)+cutoffOffset : (cutout.x+cutout.z)-cutoffAmount,
162             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y)+cutoffOffset : (cutout.y+cutout.w)-cutoffAmount,
163         );
164 
165         // Triangle 1
166         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
167         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
168         addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color);
169         
170         // Triangle 2
171         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
172         addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color);
173         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
174 
175         tris += 2;
176     }
177 
178     /**
179         Draws a framebuffer texture
180 
181         Automatically flushes after draw
182     */
183     void draw(Framebuffer fbo, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1, 1, 1, 1)) {
184 
185         // Flush if neccesary
186         if (dataOffset == DataSize*EntryCount) flush();
187         if (currentTexture !is null) flush();
188 
189         // Update current texture
190         currentFboTex = fbo;
191 
192         // Calculate rotation, position and scaling.
193         mat4 transform =
194             mat4.translation(-origin.x, -origin.y, 0) *
195             mat4.translation(position.x, position.y, 0) *
196             mat4.translation(origin.x, origin.y, 0) *
197             mat4.zrotation(rotation) * 
198             mat4.translation(-origin.x, -origin.y, 0) *
199             mat4.scaling(position.z, position.w, 0);
200 
201         // If cutout has not been set (all values are NaN or infinity) we set it to use the entire texture
202         if (!cutout.isFinite) {
203             cutout = vec4(0, fbo.realHeight, fbo.realWidth, -fbo.realHeight);
204         }
205 
206         // Get the area of the texture with a tiny bit cut off to avoid textures bleeding in to each other
207         // TODO: add a 1x1 px transparent border around textures instead?
208         enum cutoffOffset = 0.8;
209         enum cutoffAmount = cutoffOffset*2;
210 
211         vec4 uvArea = vec4(
212             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x+cutout.z)-cutoffAmount : (cutout.x)+cutoffOffset,
213             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y+cutout.w)-cutoffAmount : (cutout.y)+cutoffOffset,
214             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x)+cutoffOffset : (cutout.x+cutout.z)-cutoffAmount,
215             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y)+cutoffOffset : (cutout.y+cutout.w)-cutoffAmount,
216         );
217 
218         // Triangle 1
219         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
220         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
221         addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color);
222         
223         // Triangle 2
224         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
225         addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color);
226         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
227 
228         tris += 2;
229 
230         // Auto flush
231         this.flush!true();
232     }
233 
234     /**
235         Flush the buffer
236     */
237     void flush(bool isFbo=false)() {
238 
239         // Disable depth testing for the batcher
240         glDisable(GL_DEPTH_TEST);
241 
242         // Don't draw empty textures
243         static if (!isFbo) {
244             if (currentTexture is null) return;
245         }
246 
247         // Bind VAO
248         glBindVertexArray(vao);
249 
250         // Bind just in case some shennanigans happen
251         glBindBuffer(GL_ARRAY_BUFFER, buffer);
252 
253         // Update with this draw round's data
254         glBufferSubData(GL_ARRAY_BUFFER, 0, dataOffset*float.sizeof, data.ptr);
255 
256         // Bind the texture
257         static if (!isFbo) {
258             currentTexture.bind();
259         } else {
260             glActiveTexture(GL_TEXTURE0);
261             glBindTexture(GL_TEXTURE_2D, currentFboTex.getTexId());
262         }
263 
264         // Use our sprite batcher shader and bind our camera matrix
265         spriteBatchShader.use();
266         spriteBatchShader.setUniform(vp, batchCamera.matrix);
267 
268         // Vertex buffer
269         glEnableVertexAttribArray(0);
270         glVertexAttribPointer(
271             0,
272             VecSize,
273             GL_FLOAT,
274             GL_FALSE,
275             DataLength*GLfloat.sizeof,
276             null,
277         );
278 
279         // UV buffer
280         glEnableVertexAttribArray(1);
281         glVertexAttribPointer(
282             1,
283             UVSize,
284             GL_FLOAT,
285             GL_FALSE,
286             DataLength*GLfloat.sizeof,
287             cast(GLvoid*)(UVSize*GLfloat.sizeof),
288         );
289 
290         // Color buffer
291         glEnableVertexAttribArray(2);
292         glVertexAttribPointer(
293             2,
294             ColorSize,
295             GL_FLOAT,
296             GL_FALSE,
297             DataLength*GLfloat.sizeof,
298             cast(GLvoid*)((VecSize+UVSize)*GLfloat.sizeof),
299         );
300 
301         // Draw the triangles
302         glDrawArrays(GL_TRIANGLES, 0, cast(int)(tris*3));
303 
304         // Reset the batcher's state
305         glDisableVertexAttribArray(0);
306         glDisableVertexAttribArray(1);
307         glDisableVertexAttribArray(2);
308         currentTexture = null;
309         dataOffset = 0;
310         tris = 0;
311 
312         // Re-enable depth testing for 3D rendering
313         glEnable(GL_DEPTH_TEST);
314     }
315 }