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 }