/* a rendering.c mixin */ #ifndef SPRITES_H #define SPRITES_H #include "../textures.h" #include "../rendering.h" #include "../context.h" #include "quad_element_buffer.h" #include #include #include /* * an implementation note: * try to avoid doing expensive work in the push functions, * because they will be called multiple times in the main loop * before anything is really rendered */ /* TODO: it might make sense to infer alpha channel presence / meaningfulness for textures in atlas */ /* so that they are rendered with no blend / batched in a way to reduce overdraw automatically */ void push_sprite(char *path, t_frect rect) { struct sprite_primitive sprite = { .rect = rect, .color = (t_color) { 255, 255, 255, 255 }, .rotation = 0.0, .texture_key = textures_get_key(&ctx.texture_cache, path), .flip_x = false, .flip_y = false, .blend = true, }; struct primitive_2d primitive = { .type = PRIMITIVE_2D_SPRITE, .sprite = sprite, }; arrput(ctx.render_queue_2d, primitive); } void push_sprite_ex(t_frect rect, t_push_sprite_args args) { struct sprite_primitive sprite = { .rect = rect, .color = args.color, .rotation = args.rotation, .texture_key = textures_get_key(&ctx.texture_cache, args.path), .flip_x = args.flip_x, .flip_y = args.flip_y, .blend = args.blend, }; struct primitive_2d primitive = { .type = PRIMITIVE_2D_SPRITE, .sprite = sprite, }; arrput(ctx.render_queue_2d, primitive); } static struct sprite_batch { int atlas_id; size_t size; /* how many primitives are in current batch */ bool blend; /* whether it's blended or not */ } collect_sprite_batch(const struct primitive_2d *primitives, size_t len) { /* assumes that first primitive is already a sprite */ struct sprite_batch result = { .atlas_id = textures_get_atlas_id(&ctx.texture_cache, primitives[0].sprite.texture_key), .blend = primitives[0].sprite.blend, }; /* batch size is clamped so that reallocated short indices could be used */ if (len >= QUAD_ELEMENT_BUFFER_LENGTH) len = QUAD_ELEMENT_BUFFER_LENGTH; for (size_t i = 0; i < len; ++i) { const struct primitive_2d *const current = &primitives[i]; /* don't touch things other than sprites */ if (current->type != PRIMITIVE_2D_SPRITE) break; /* only collect the same blend modes */ if (current->sprite.blend != result.blend) break; /* only collect the same texture atlases */ if (textures_get_atlas_id(&ctx.texture_cache, current->sprite.texture_key) != result.atlas_id) break; ++result.size; } return result; } /* TODO: texture flipping */ /* assumes that orthogonal matrix setup is done already */ static void render_sprites(const struct primitive_2d primitives[], const size_t len, const bool reversed) { /* single vertex array is used for every batch with NULL glBufferData() trick at the end */ static GLuint vertex_array = 0; if (vertex_array == 0) glGenBuffers(1, &vertex_array); glBindBuffer(GL_ARRAY_BUFFER, vertex_array); glBufferData(GL_ARRAY_BUFFER, sizeof (struct sprite_primitive_payload) * len, NULL, GL_STREAM_DRAW); const t_rect srcrect = textures_get_srcrect(&ctx.texture_cache, primitives->sprite.texture_key); const t_rect dims = textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key); /* vertex population over a mapped buffer */ { /* TODO: check errors, ensure alignment ? */ struct sprite_primitive_payload *const payload = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); for (size_t i = 0; i < len; ++i) { const size_t cur = reversed ? len - i - 1: i; const struct sprite_primitive sprite = primitives[cur].sprite; const float wr = (float)srcrect.w / (float)dims.w; const float hr = (float)srcrect.h / (float)dims.h; const float xr = (float)srcrect.x / (float)dims.w; const float yr = (float)srcrect.y / (float)dims.h; payload[i] = (struct sprite_primitive_payload) { /* upper-left */ .v0 = { sprite.rect.x, sprite.rect.y }, .uv0 = { xr, yr, }, /* bottom-left */ .v1 = { (sprite.rect.x), (sprite.rect.y + sprite.rect.h) }, .uv1 = { xr, yr + hr, }, /* bottom-right */ .v2 = { (sprite.rect.x + sprite.rect.w), (sprite.rect.y + sprite.rect.h) }, .uv2 = { xr + wr, yr + hr, }, /* upper-right */ .v3 = { (sprite.rect.x + sprite.rect.w), (sprite.rect.y) }, .uv3 = { xr + wr, yr, }, /* equal for all (flat shaded) */ .c0 = sprite.color, .c1 = sprite.color, .c2 = sprite.color, .c3 = sprite.color, }; } glUnmapBuffer(GL_ARRAY_BUFFER); } /* vertex specification */ glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, offsetof(struct sprite_primitive_payload, v1), (void *)offsetof(struct sprite_primitive_payload, v0)); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTexture(GL_TEXTURE0); glTexCoordPointer(2, GL_FLOAT, offsetof(struct sprite_primitive_payload, v1), (void *)offsetof(struct sprite_primitive_payload, uv0)); glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_UNSIGNED_BYTE, offsetof(struct sprite_primitive_payload, v1), (void *)offsetof(struct sprite_primitive_payload, c0)); textures_bind(&ctx.texture_cache, primitives->sprite.texture_key, GL_TEXTURE_2D); bind_quad_element_buffer(); glDrawElements(GL_TRIANGLES, 6 * (GLsizei)len, GL_UNSIGNED_SHORT, NULL); /* clear the state */ glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } #endif