/* a rendering.c mixin */ #ifndef SPRITES_H #define SPRITES_H #include "../textures.h" #include "../rendering.h" #include "../context.h" #include "../util.h" #include "quad_element_buffer.h" #include #include #include /* interleaved vertex array data */ /* TODO: use int16_t for uvs */ /* TODO: use packed types? */ /* TODO: int16_t could be used for positioning, but we would need to have more CPU calcs */ struct sprite_primitive_payload { /* upper-left */ t_fvec2 v0; t_fvec2 uv0; t_color c0; /* bottom-left */ t_fvec2 v1; t_fvec2 uv1; t_color c1; /* bottom-right */ t_fvec2 v2; t_fvec2 uv2; t_color c2; /* upper-right */ t_fvec2 v3; t_fvec2 uv3; t_color c3; }; struct sprite_primitive_payload_without_color { /* upper-left */ t_fvec2 v0; t_fvec2 uv0; /* bottom-left */ t_fvec2 v1; t_fvec2 uv1; /* bottom-right */ t_fvec2 v2; t_fvec2 uv2; /* upper-right */ t_fvec2 v3; t_fvec2 uv3; }; /* * 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, }; 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, }; struct primitive_2d primitive = { .type = PRIMITIVE_2D_SPRITE, .sprite = sprite, }; arrput(ctx.render_queue_2d, primitive); } static struct sprite_batch { size_t size; /* how many primitives are in current batch */ int atlas_id; enum texture_mode mode; bool constant_colored; /* whether colored batch is uniformly colored */ } collect_sprite_batch(const struct primitive_2d *primitives, size_t len) { /* assumes that first primitive is already a sprite */ struct sprite_batch batch = { .atlas_id = textures_get_atlas_id(&ctx.texture_cache, primitives[0].sprite.texture_key), .mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key), .constant_colored = true, }; const uint32_t uniform_color = *(const uint32_t *)&primitives[0].sprite.color; /* 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 */ const enum texture_mode mode = textures_get_mode(&ctx.texture_cache, current->sprite.texture_key); if (mode != batch.mode) break; /* only collect the same texture atlases */ if (textures_get_atlas_id(&ctx.texture_cache, current->sprite.texture_key) != batch.atlas_id) break; /* if all are modulated the same we can skip sending the color data */ if (batch.constant_colored && *(const uint32_t *)¤t->sprite.color == uniform_color) batch.constant_colored = false; ++batch.size; } return batch; } /* assumes that orthogonal matrix setup is done already */ static void render_sprites(const struct primitive_2d primitives[], const struct sprite_batch batch) { /* 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); if (batch.mode == TEXTURE_MODE_GHOSTLY) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthFunc(GL_ALWAYS); glDepthMask(GL_FALSE); glDisable(GL_ALPHA_TEST); } else if (batch.mode == TEXTURE_MODE_SEETHROUGH) { glDisable(GL_BLEND); glDepthFunc(GL_LEQUAL); glDepthMask(GL_TRUE); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_EQUAL, 1.0f); } else { glDisable(GL_BLEND); glDepthFunc(GL_LESS); glDepthMask(GL_TRUE); glDisable(GL_ALPHA_TEST); } size_t payload_size; if (!batch.constant_colored) payload_size = sizeof (struct sprite_primitive_payload); else payload_size = sizeof (struct sprite_primitive_payload_without_color); glBindBuffer(GL_ARRAY_BUFFER, vertex_array); glBufferData(GL_ARRAY_BUFFER, payload_size * batch.size, NULL, GL_STREAM_DRAW); 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 ? */ void *const payload = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); for (size_t i = 0; i < batch.size; ++i) { /* render opaques front to back */ const size_t cur = batch.mode == TEXTURE_MODE_GHOSTLY ? i : batch.size - i - 1; const struct sprite_primitive sprite = primitives[cur].sprite; const t_rect srcrect = textures_get_srcrect(&ctx.texture_cache, primitives[cur].sprite.texture_key); 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; t_fvec2 uv0 = { xr + wr * sprite.flip_x, yr + hr * sprite.flip_y }; t_fvec2 uv1 = { xr + wr * sprite.flip_x, yr + hr * !sprite.flip_y }; t_fvec2 uv2 = { xr + wr * !sprite.flip_x, yr + hr * !sprite.flip_y }; t_fvec2 uv3 = { xr + wr * !sprite.flip_x, yr + hr * sprite.flip_y }; t_fvec2 v0, v1, v2, v3; /* todo: fast PI/2 degree divisible rotations? */ if (sprite.rotation == 0.0f) { /* non-rotated case */ v0 = (t_fvec2){ sprite.rect.x, sprite.rect.y }; v1 = (t_fvec2){ sprite.rect.x, sprite.rect.y + sprite.rect.h }; v2 = (t_fvec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y + sprite.rect.h }; v3 = (t_fvec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y }; } else if (sprite.rect.w == sprite.rect.h) { /* rotated square case */ const t_fvec2 c = frect_center(sprite.rect); const t_fvec2 t = fast_cossine(sprite.rotation + (float)M_PI_4); const t_fvec2 d = { .x = t.x * sprite.rect.w * (float)M_SQRT1_2, .y = t.y * sprite.rect.h * (float)M_SQRT1_2, }; v0 = (t_fvec2){ c.x - d.x, c.y - d.y }; v1 = (t_fvec2){ c.x - d.y, c.y + d.x }; v2 = (t_fvec2){ c.x + d.x, c.y + d.y }; v3 = (t_fvec2){ c.x + d.y, c.y - d.x }; } else { /* rotated non-square case*/ const t_fvec2 c = frect_center(sprite.rect); const t_fvec2 t = fast_cossine(sprite.rotation); const t_fvec2 h = { sprite.rect.w / 2, sprite.rect.h / 2 }; v0 = (t_fvec2){ c.x + t.x * -h.x - t.y * -h.y, c.y + t.y * -h.x + t.x * -h.y }; v1 = (t_fvec2){ c.x + t.x * -h.x - t.y * +h.y, c.y + t.y * -h.x + t.x * +h.y }; v2 = (t_fvec2){ c.x + t.x * +h.x - t.y * +h.y, c.y + t.y * +h.x + t.x * +h.y }; v3 = (t_fvec2){ c.x + t.x * +h.x - t.y * -h.y, c.y + t.y * +h.x + t.x * -h.y }; } if (!batch.constant_colored) ((struct sprite_primitive_payload *)payload)[i] = (struct sprite_primitive_payload) { .v0 = v0, .v1 = v1, .v2 = v2, .v3 = v3, .uv0 = uv0, .uv1 = uv1, .uv2 = uv2, .uv3 = uv3, /* equal for all (flat shaded) */ .c0 = sprite.color, .c1 = sprite.color, .c2 = sprite.color, .c3 = sprite.color, }; else ((struct sprite_primitive_payload_without_color *)payload)[i] = (struct sprite_primitive_payload_without_color) { .v0 = v0, .v1 = v1, .v2 = v2, .v3 = v3, .uv0 = uv0, .uv1 = uv1, .uv2 = uv2, .uv3 = uv3, }; } glUnmapBuffer(GL_ARRAY_BUFFER); } GLsizei off; GLsizei voff; GLsizei uvoff; if (!batch.constant_colored) { off = offsetof(struct sprite_primitive_payload, v1); voff = offsetof(struct sprite_primitive_payload, v0); uvoff = offsetof(struct sprite_primitive_payload, uv0); } else { off = offsetof(struct sprite_primitive_payload_without_color, v1); voff = offsetof(struct sprite_primitive_payload_without_color, v0); uvoff = offsetof(struct sprite_primitive_payload_without_color, uv0); } /* vertex specification */ glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(2, GL_FLOAT, off, (void *)(size_t)voff); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTexture(GL_TEXTURE0); glTexCoordPointer(2, GL_FLOAT, off, (void *)(size_t)uvoff); if (!batch.constant_colored) { glEnableClientState(GL_COLOR_ARRAY); glColorPointer(4, GL_UNSIGNED_BYTE, off, (void *)offsetof(struct sprite_primitive_payload, c0)); } else glColor4ub(primitives[0].sprite.color.r, primitives[0].sprite.color.g, primitives[0].sprite.color.b, primitives[0].sprite.color.a); textures_bind(&ctx.texture_cache, primitives->sprite.texture_key, GL_TEXTURE_2D); bind_quad_element_buffer(); glDrawElements(GL_TRIANGLES, 6 * (GLsizei)batch.size, 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