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 }