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 
60     void addVertexData(vec2 position, vec2 uvs, vec4 color) {
61         data[dataOffset..dataOffset+DataLength] = [position.x, position.y, uvs.x, uvs.y, color.x, color.y, color.z, color.w];
62         dataOffset += DataLength;
63     }
64 
65 public:
66 
67     /**
68         Constructor
69     */
70     this() {
71         data = new float[DataSize*EntryCount];
72 
73         glGenVertexArrays(1, &vao);
74         glBindVertexArray(vao);
75 
76         glGenBuffers(1, &buffer);
77         glBindBuffer(GL_ARRAY_BUFFER, buffer);
78         glBufferData(GL_ARRAY_BUFFER, float.sizeof*data.length, data.ptr, GL_DYNAMIC_DRAW);
79 
80         batchCamera = new Camera2D();
81         spriteBatchShader = new Shader(import("shaders/batch.vert"), import("shaders/batch.frag"));
82         vp = spriteBatchShader.getUniformLocation("vp");
83     }
84 
85     /**
86         Draws texture from atlas
87     */
88     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)) {
89         auto index = GameAtlas[item];
90         draw(index, position, cutout, origin, rotation, flip, color);
91     }
92 
93     /**
94         Draws cached atlas index
95     */
96     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)) {
97         
98         vec4 fCutout = index.area;
99         if (cutout.isFinite) {
100 
101             // Clamp the cutout to fit within the texture's area
102             vec4 cutoutClamped = vec4(
103                 clamp(cutout.x, 0, index.area.z),
104                 clamp(cutout.y, 0, index.area.w),
105                 clamp(cutout.z, 0, index.area.z),
106                 clamp(cutout.w, 0, index.area.w),
107             );
108             
109             // Cut in to the area
110             fCutout = vec4(
111                 index.area.x+cutoutClamped.x,
112                 index.area.y+cutoutClamped.y,
113                 cutoutClamped.z,
114                 cutoutClamped.w,
115             );
116         }
117         
118         draw(index.texture, position, fCutout, origin, rotation, flip, color);
119     }
120 
121     /**
122         Draws the texture
123 
124         Remember to call flush after drawing all the textures you want
125 
126         Flush will automatically be called if your draws exceed the max count
127         Flush will automatically be called if you queue an other texture
128     */
129     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)) {
130 
131         // Flush if neccesary
132         if (dataOffset == DataSize*EntryCount) flush();
133         if (texture != currentTexture) flush();
134 
135         // Update current texture
136         currentTexture = texture;
137 
138         mat4 transform =
139             mat4.translation(-origin.x, -origin.y, 0) *
140             mat4.translation(position.x, position.y, 0) *
141             mat4.translation(origin.x, origin.y, 0) *
142             mat4.zrotation(rotation) * 
143             mat4.translation(-origin.x, -origin.y, 0) *
144             mat4.scaling(position.z, position.w, 0);
145 
146         if (!cutout.isFinite) {
147             cutout = vec4(0, 0, texture.width, texture.height);
148         }
149 
150         vec4 uvArea = vec4(
151             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x+cutout.z)-1.5 : (cutout.x)+0.8,
152             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y+cutout.w)-1.5 : (cutout.y)+0.8,
153             (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x)+0.8 : (cutout.x+cutout.z)-1.5,
154             (flip & SpriteFlip.Vertical)   > 0 ? (cutout.y)+0.8 : (cutout.y+cutout.w)-1.5,
155         );
156 
157         // Triangle 1
158         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
159         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
160         addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color);
161         
162         // Triangle 2
163         addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color);
164         addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color);
165         addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color);
166 
167         tris += 2;
168     }
169 
170     /**
171         Flush the buffer
172     */
173     void flush() {
174 
175         // Disable depth testing for the batcher
176         glDisable(GL_DEPTH_TEST);
177 
178         // Don't draw empty textures
179         if (currentTexture is null) return;
180 
181         // Bind VAO
182         glBindVertexArray(vao);
183 
184         // Bind just in case some shennanigans happen
185         glBindBuffer(GL_ARRAY_BUFFER, buffer);
186 
187         // Update with this draw round's data
188         glBufferSubData(GL_ARRAY_BUFFER, 0, dataOffset*float.sizeof, data.ptr);
189 
190         // Bind the texture
191         currentTexture.bind();
192 
193         // Use our sprite batcher shader
194         spriteBatchShader.use();
195         spriteBatchShader.setUniform(vp, batchCamera.matrix);
196 
197         // Vertex Buffer
198         glEnableVertexAttribArray(0);
199         glVertexAttribPointer(
200             0,
201             VecSize,
202             GL_FLOAT,
203             GL_FALSE,
204             DataLength*GLfloat.sizeof,
205             null,
206         );
207 
208         // UV Buffer
209         glEnableVertexAttribArray(1);
210         glVertexAttribPointer(
211             1,
212             UVSize,
213             GL_FLOAT,
214             GL_FALSE,
215             DataLength*GLfloat.sizeof,
216             cast(GLvoid*)(UVSize*GLfloat.sizeof),
217         );
218 
219         // UV Buffer
220         glEnableVertexAttribArray(2);
221         glVertexAttribPointer(
222             2,
223             ColorSize,
224             GL_FLOAT,
225             GL_FALSE,
226             DataLength*GLfloat.sizeof,
227             cast(GLvoid*)((VecSize+UVSize)*GLfloat.sizeof),
228         );
229 
230         // Draw the triangles
231         glDrawArrays(GL_TRIANGLES, 0, cast(int)(tris*3));
232 
233         // Reset the batcher's state
234         glDisableVertexAttribArray(0);
235         glDisableVertexAttribArray(1);
236         glDisableVertexAttribArray(2);
237         currentTexture = null;
238         dataOffset = 0;
239         tris = 0;
240 
241         // Re-enable depth test Clear depth buffer
242         glEnable(GL_DEPTH_TEST);
243     }
244 }