#include "twn_draw.h" #include "twn_draw_c.h" #include "twn_engine_context_c.h" #include "twn_util.h" #include "twn_util_c.h" #include "twn_textures_c.h" #include "twn_option.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 */ void draw_sprite(char const *path, Rect rect, Rect const *texture_region, /* optional, default: NULL */ Color color, /* optional, default: all 255 */ float rotation, /* optional, default: 0 */ bool flip_x, /* optional, default: false */ bool flip_y, /* optional, default: false */ bool stretch) { SpritePrimitive sprite = { .rect = rect, .color = color, .rotation = rotation, .texture_key = textures_get_key(&ctx.texture_cache, path), .flip_x = flip_x, .flip_y = flip_y, .repeat = !stretch, .texture_region_opt_set = texture_region != NULL, }; if (texture_region) sprite.texture_region_opt = *texture_region; Primitive2D primitive = { .type = PRIMITIVE_2D_SPRITE, .sprite = sprite, }; arrput(ctx.render_queue_2d, primitive); } void draw_sprite_args(const DrawSpriteArgs args) { Color const color = m_or(args, color, ((Color) { 255, 255, 255, 255 })); float const rotation = m_or(args, rotation, 0.0f); bool const flip_x = m_or(args, flip_x, false); bool const flip_y = m_or(args, flip_y, false); bool const stretch = m_or(args, stretch, false); Rect const *texture_region = m_is_set(args, texture_region) ? &args.texture_region_opt : NULL; draw_sprite(args.path, args.rect, texture_region, color, rotation, flip_x, flip_y, stretch); } struct SpriteBatch collect_sprite_batch(const Primitive2D primitives[], size_t len) { /* assumes that first primitive is already a sprite */ const uint16_t texture_key_id = primitives[0].sprite.texture_key.id; const int atlas_id = textures_get_atlas_id(&ctx.texture_cache, primitives[0].sprite.texture_key); struct SpriteBatch batch = { .mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key), .constant_colored = true, .repeat = primitives[0].sprite.repeat, }; 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 Primitive2D *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 TextureMode 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) != atlas_id) break; /* repeated textures require separate handling */ if (batch.repeat) { /* all must be repeated */ if (!current->sprite.repeat) break; /* all must be of same texture id, not just atlas id */ if (current->sprite.texture_key.id != texture_key_id) break; } /* if all are modulated the same we can skip sending the color data */ if (*(const uint32_t *)¤t->sprite.color != uniform_color) batch.constant_colored = false; ++batch.size; } return batch; } /* assumes that orthogonal matrix setup is done already */ void render_sprites(const Primitive2D primitives[], const struct SpriteBatch batch) { /* single vertex array is used for every batch with NULL glBufferData() trick at the end */ static VertexBuffer vertex_array = 0; if (vertex_array == 0) vertex_array = create_vertex_buffer(); use_texture_mode(batch.mode); const Rect dims = textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key); /* vertex population over a vertex buffer builder interface */ { VertexBufferBuilder payload = build_vertex_buffer(vertex_array, get_sprite_payload_size(batch) * batch.size); 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 SpritePrimitive sprite = primitives[cur].sprite; const Rect srcrect = textures_get_srcrect(&ctx.texture_cache, primitives[cur].sprite.texture_key); Vec2 uv0, uv1, uv2, uv3; if (!sprite.repeat) { const float wr = srcrect.w / dims.w; const float hr = srcrect.h / dims.h; const float xr = srcrect.x / dims.w; const float yr = srcrect.y / dims.h; if (!m_is_set(sprite, texture_region)) { uv0 = (Vec2){ xr + wr * sprite.flip_x, yr + hr * sprite.flip_y }; uv1 = (Vec2){ xr + wr * sprite.flip_x, yr + hr * !sprite.flip_y }; uv2 = (Vec2){ xr + wr * !sprite.flip_x, yr + hr * !sprite.flip_y }; uv3 = (Vec2){ xr + wr * !sprite.flip_x, yr + hr * sprite.flip_y }; } else { /* TODO: support for flipping */ uv0 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x) / dims.w, (srcrect.y + sprite.texture_region_opt.y) / dims.h }; uv1 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x) / dims.w, (srcrect.y + sprite.texture_region_opt.y + sprite.texture_region_opt.h) / dims.h }; uv2 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x + sprite.texture_region_opt.w) / dims.w, (srcrect.y + sprite.texture_region_opt.y + sprite.texture_region_opt.h) / dims.h }; uv3 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x + sprite.texture_region_opt.w) / dims.w, (srcrect.y + sprite.texture_region_opt.y) / dims.h }; } } else { /* try fitting texture into supplied destination rectangle */ const float rx = sprite.rect.w / srcrect.w; const float ry = sprite.rect.h / srcrect.h; uv0 = (Vec2){ rx * sprite.flip_x, ry * sprite.flip_y }; uv1 = (Vec2){ rx * sprite.flip_x, ry * !sprite.flip_y }; uv2 = (Vec2){ rx * !sprite.flip_x, ry * !sprite.flip_y }; uv3 = (Vec2){ rx * !sprite.flip_x, ry * sprite.flip_y }; if (m_is_set(sprite, texture_region)) { /* displace origin */ const float ax = sprite.texture_region_opt.x / srcrect.w; const float ay = sprite.texture_region_opt.y / srcrect.h; uv0.x += ax; uv1.x += ax; uv2.x += ax; uv3.x += ax; uv0.y += ay; uv1.y += ay; uv2.y += ay; uv3.y += ay; } } Vec2 v0, v1, v2, v3; /* todo: fast PI/2 degree divisible rotations? */ if (sprite.rotation == 0.0f) { /* non-rotated case */ v0 = (Vec2){ sprite.rect.x, sprite.rect.y }; v1 = (Vec2){ sprite.rect.x, sprite.rect.y + sprite.rect.h }; v2 = (Vec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y + sprite.rect.h }; v3 = (Vec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y }; } else if (sprite.rect.w == sprite.rect.h) { /* rotated square case */ const Vec2 c = frect_center(sprite.rect); const Vec2 t = fast_cossine(sprite.rotation + (float)M_PI_4); const Vec2 d = { .x = t.x * sprite.rect.w * (float)M_SQRT1_2, .y = t.y * sprite.rect.h * (float)M_SQRT1_2, }; v0 = (Vec2){ c.x - d.x, c.y - d.y }; v1 = (Vec2){ c.x - d.y, c.y + d.x }; v2 = (Vec2){ c.x + d.x, c.y + d.y }; v3 = (Vec2){ c.x + d.y, c.y - d.x }; } else { /* rotated non-square case*/ const Vec2 c = frect_center(sprite.rect); const Vec2 t = fast_cossine(sprite.rotation); const Vec2 h = { sprite.rect.w / 2, sprite.rect.h / 2 }; v0 = (Vec2){ c.x + t.x * -h.x - t.y * -h.y, c.y + t.y * -h.x + t.x * -h.y }; v1 = (Vec2){ c.x + t.x * -h.x - t.y * +h.y, c.y + t.y * -h.x + t.x * +h.y }; v2 = (Vec2){ c.x + t.x * +h.x - t.y * +h.y, c.y + t.y * +h.x + t.x * +h.y }; v3 = (Vec2){ c.x + t.x * +h.x - t.y * -h.y, c.y + t.y * +h.x + t.x * -h.y }; } push_sprite_payload_to_vertex_buffer_builder(batch, &payload, v0, v1, v2, v3, uv0, uv1, uv2, uv3, sprite.color); } } finally_render_sprites(primitives, batch, vertex_array); }