townengine/src/rendering.c

552 lines
17 KiB
C
Raw Normal View History

2024-07-08 00:44:20 +00:00
#include "private/rendering.h"
#include "context.h"
#include "textures.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <glad/glad.h>
2024-07-08 00:44:20 +00:00
#include <stddef.h>
2024-07-08 00:44:20 +00:00
#include <stdlib.h>
#include <tgmath.h>
/* http://www.swiftless.com/opengltuts.html */
2024-07-08 00:44:20 +00:00
void render_queue_clear(void) {
/* since i don't intend to free the queues, */
/* it's faster and simpler to just "start over" */
/* and start overwriting the existing data */
arrsetlen(ctx.render_queue_sprites, 0);
arrsetlen(ctx.render_queue_rectangles, 0);
arrsetlen(ctx.render_queue_circles, 0);
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i)
arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0);
2024-07-08 00:44:20 +00:00
}
/*
* 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
*/
/* sprite */
void push_sprite(char *path, t_frect rect) {
struct sprite_primitive sprite = {
.rect = rect,
.color = (t_color) { 255, 255, 255, 255 },
.rotation = 0.0,
.blend_mode = SDL_BLENDMODE_BLEND,
.texture_key = textures_get_key(&ctx.texture_cache, path),
2024-07-08 00:44:20 +00:00
.layer = 0,
.flip_x = false,
.flip_y = false,
};
arrput(ctx.render_queue_sprites, sprite);
}
void push_sprite_ex(t_frect rect, t_push_sprite_args args) {
struct sprite_primitive sprite = {
.rect = rect,
.color = args.color,
.rotation = args.rotation,
.blend_mode = args.blend_mode,
.texture_key = textures_get_key(&ctx.texture_cache, args.path),
2024-07-08 00:44:20 +00:00
.layer = args.layer,
.flip_x = args.flip_x,
.flip_y = args.flip_y,
};
arrput(ctx.render_queue_sprites, sprite);
}
/* rectangle */
void push_rectangle(t_frect rect, t_color color) {
struct rect_primitive rectangle = {
.rect = rect,
.color = color,
};
arrput(ctx.render_queue_rectangles, rectangle);
}
/* circle */
void push_circle(t_fvec2 position, float radius, t_color color) {
struct circle_primitive circle = {
.radius = radius,
.color = color,
.position = position,
};
arrput(ctx.render_queue_circles, circle);
}
/* TODO: automatic handling of repeating textures */
/* for that we could allocate a loner texture */
void unfurl_triangle(const char *path,
t_fvec3 v0,
t_fvec3 v1,
t_fvec3 v2,
t_shvec2 uv0,
t_shvec2 uv1,
t_shvec2 uv2)
{
const t_texture_key texture_key = textures_get_key(&ctx.texture_cache, path);
struct mesh_batch_item *batch_p = hmgetp_null(ctx.uncolored_mesh_batches, texture_key);
if (!batch_p) {
struct mesh_batch item = {0};
hmput(ctx.uncolored_mesh_batches, texture_key, item);
batch_p = &ctx.uncolored_mesh_batches[hmlenu(ctx.uncolored_mesh_batches) - 1]; /* TODO: can last index be used? */
}
union uncolored_space_triangle triangle = { .primitive = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.uv1 = m_to_fvec2(uv1),
.uv0 = m_to_fvec2(uv0),
.uv2 = m_to_fvec2(uv2),
}};
union uncolored_space_triangle *triangles =
(union uncolored_space_triangle *)batch_p->value.primitives;
arrpush(triangles, triangle);
batch_p->value.primitives = (uint8_t *)triangles;
}
2024-07-08 00:44:20 +00:00
/* compare functions for the sort in render_sprites */
static int cmp_atlases(const void *a, const void *b) {
int index_a = ((const struct sprite_primitive *)a)->texture_key.id;
int index_b = ((const struct sprite_primitive *)b)->texture_key.id;
2024-07-08 00:44:20 +00:00
return (index_a > index_b) - (index_a < index_b);
}
static int cmp_blend_modes(const void *a, const void *b) {
SDL_BlendMode mode_a = ((const struct sprite_primitive *)a)->blend_mode;
SDL_BlendMode mode_b = ((const struct sprite_primitive *)b)->blend_mode;
return (mode_a > mode_b) - (mode_a < mode_b);
}
static int cmp_colors(const void *a, const void *b) {
t_color color_a = ((const struct sprite_primitive *)a)->color;
t_color color_b = ((const struct sprite_primitive *)b)->color;
/* check reds */
if (color_a.r < color_b.r)
return -1;
else if (color_a.r > color_b.r)
return 1;
/* reds were equal, check greens */
else if (color_a.g < color_b.g)
return -1;
else if (color_a.g > color_b.g)
return 1;
/* greens were equal, check blues */
else if (color_a.b < color_b.b)
return -1;
else if (color_a.b > color_b.b)
return 1;
/* blues were equal, check alphas */
else if (color_a.a < color_b.a)
return -1;
else if (color_a.a > color_b.a)
return 1;
/* entirely equal */
else
return 0;
}
static int cmp_layers(const void *a, const void *b) {
int layer_a = ((const struct sprite_primitive *)a)->layer;
int layer_b = ((const struct sprite_primitive *)b)->layer;
return (layer_a > layer_b) - (layer_a < layer_b);
}
/* TODO: not that we're SDL free we need to implement batching ourselves */
2024-07-08 00:44:20 +00:00
/* necessary to allow the renderer to draw in batches in the best case */
static void sort_sprites(struct sprite_primitive *sprites) {
size_t sprites_len = arrlenu(sprites);
qsort(sprites, sprites_len, sizeof *sprites, cmp_atlases);
qsort(sprites, sprites_len, sizeof *sprites, cmp_blend_modes);
qsort(sprites, sprites_len, sizeof *sprites, cmp_colors);
qsort(sprites, sprites_len, sizeof *sprites, cmp_layers);
}
static void upload_quad_vertices(t_frect rect) {
/* client memory needs to be reachable on glDraw*, so */
static float vertices[6 * 2];
vertices[0] = rect.x; vertices[1] = rect.y;
vertices[2] = rect.x; vertices[3] = rect.y + rect.h;
vertices[4] = rect.x + rect.w; vertices[5] = rect.y + rect.h;
vertices[6] = rect.x + rect.w; vertices[7] = rect.y + rect.h;
vertices[8] = rect.x + rect.w; vertices[9] = rect.y;
vertices[10] = rect.x; vertices[11] = rect.y;
glVertexPointer(2, GL_FLOAT, 0, (void *)&vertices);
}
/* TODO: texture flipping */
/* assumes that orthogonal matrix setup is done already */
2024-07-08 00:44:20 +00:00
static void render_sprite(struct sprite_primitive *sprite) {
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glClientActiveTexture(GL_TEXTURE0);
2024-07-08 00:44:20 +00:00
textures_bind(&ctx.texture_cache, sprite->texture_key, GL_TEXTURE_2D);
t_rect srcrect = textures_get_srcrect(&ctx.texture_cache, sprite->texture_key);
t_rect dims = textures_get_dims(&ctx.texture_cache, sprite->texture_key);
glTexCoordPointer(2,
GL_FLOAT,
0,
/* TODO: try using shorts */
(void *)(float[6 * 2]) {
(float)srcrect.x / (float)dims.w,
(float)srcrect.y / (float)dims.h,
(float)srcrect.x / (float)dims.w,
(float)(srcrect.y + srcrect.h) / (float)dims.h,
(float)(srcrect.x + srcrect.w) / (float)dims.w,
(float)(srcrect.y + srcrect.h) / (float)dims.h,
(float)(srcrect.x + srcrect.w) / (float)dims.w,
(float)(srcrect.y + srcrect.h) / (float)dims.h,
(float)(srcrect.x + srcrect.w) / (float)dims.w,
(float)srcrect.y / (float)dims.h,
(float)srcrect.x / (float)dims.w,
(float)srcrect.y / (float)dims.h });
2024-07-08 00:44:20 +00:00
glColor4ub(sprite->color.r, sprite->color.g, sprite->color.b, sprite->color.a);
// SDL_SetTextureBlendMode(texture, sprite->blend_mode);
2024-07-08 00:44:20 +00:00
glEnableClientState(GL_VERTEX_ARRAY);
upload_quad_vertices(sprite->rect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glBindTexture(GL_TEXTURE_2D, 0);
2024-07-08 00:44:20 +00:00
}
static void render_rectangle(struct rect_primitive *rectangle) {
glColor4ub(rectangle->color.r, rectangle->color.g,
rectangle->color.b, rectangle->color.a);
glEnableClientState(GL_VERTEX_ARRAY);
upload_quad_vertices(rectangle->rect);
2024-07-08 00:44:20 +00:00
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableClientState(GL_VERTEX_ARRAY);
2024-07-08 00:44:20 +00:00
}
/* vertices_out and indices_out MUST BE FREED */
static void create_circle_geometry(t_fvec2 position,
t_color color,
float radius,
size_t num_vertices,
SDL_Vertex **vertices_out,
int **indices_out)
{
SDL_Vertex *vertices = cmalloc(sizeof *vertices * (num_vertices + 1));
int *indices = cmalloc(sizeof *indices * (num_vertices * 3));
/* the angle (in radians) to rotate by on each iteration */
float seg_rotation_angle = (360.0f / (float)num_vertices) * ((float)M_PI / 180);
vertices[0].position.x = (float)position.x;
vertices[0].position.y = (float)position.y;
vertices[0].color.r = color.r;
vertices[0].color.g = color.g;
vertices[0].color.b = color.b;
vertices[0].color.a = color.a;
vertices[0].tex_coord = (SDL_FPoint){ 0, 0 };
/* this point will rotate around the center */
float start_x = 0.0f - radius;
float start_y = 0.0f;
for (size_t i = 1; i < num_vertices + 1; ++i) {
float final_seg_rotation_angle = (float)i * seg_rotation_angle;
vertices[i].position.x =
cos(final_seg_rotation_angle) * start_x -
sin(final_seg_rotation_angle) * start_y;
vertices[i].position.y =
cos(final_seg_rotation_angle) * start_y +
sin(final_seg_rotation_angle) * start_x;
vertices[i].position.x += position.x;
vertices[i].position.y += position.y;
vertices[i].color.r = color.r;
vertices[i].color.g = color.g;
vertices[i].color.b = color.b;
vertices[i].color.a = color.a;
vertices[i].tex_coord = (SDL_FPoint){ 0, 0 };
size_t triangle_offset = 3 * (i - 1);
/* center point index */
indices[triangle_offset] = 0;
/* generated point index */
indices[triangle_offset + 1] = (int)i;
size_t index = (i + 1) % num_vertices;
if (index == 0)
index = num_vertices;
indices[triangle_offset + 2] = (int)index;
}
*vertices_out = vertices;
*indices_out = indices;
}
static void render_circle(struct circle_primitive *circle) {
SDL_Vertex *vertices = NULL;
int *indices = NULL;
int num_vertices = (int)circle->radius;
create_circle_geometry(circle->position,
circle->color,
circle->radius,
num_vertices,
&vertices,
&indices);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(2,
GL_FLOAT,
sizeof (SDL_Vertex),
&vertices->position);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(4,
GL_UNSIGNED_BYTE,
sizeof (SDL_Vertex),
&vertices->color);
glDrawElements(GL_TRIANGLES,
num_vertices * 3,
GL_UNSIGNED_INT,
indices);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
2024-07-08 00:44:20 +00:00
free(vertices);
free(indices);
}
static void render_sprites(void) {
sort_sprites(ctx.render_queue_sprites);
for (size_t i = 0; i < arrlenu(ctx.render_queue_sprites); ++i) {
render_sprite(&ctx.render_queue_sprites[i]);
}
}
static void render_rectangles(void) {
for (size_t i = 0; i < arrlenu(ctx.render_queue_rectangles); ++i) {
render_rectangle(&ctx.render_queue_rectangles[i]);
}
}
static void render_circles(void) {
for (size_t i = 0; i < arrlenu(ctx.render_queue_circles); ++i) {
render_circle(&ctx.render_queue_circles[i]);
}
}
static void draw_uncolored_space_traingle_batch(struct mesh_batch *batch,
t_texture_key texture_key)
{
size_t primitives_len = arrlenu(batch->primitives);
if (primitives_len == 0)
return;
/* create vertex array object */
if (batch->buffer == 0)
glGenBuffers(1, &batch->buffer);
/* TODO: try using mapped buffers while building batches instead? */
/* this way we could skip client side copy that is kept until commitment */
/* alternatively we could commit glBufferSubData based on a threshold */
/* update pixel-based uvs to correspond with texture atlases */
for (size_t i = 0; i < primitives_len; ++i) {
struct uncolored_space_triangle_payload *payload =
&((union uncolored_space_triangle *)batch->primitives)[i].payload;
t_rect srcrect = textures_get_srcrect(&ctx.texture_cache, texture_key);
t_rect dims = textures_get_dims(&ctx.texture_cache, 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;
payload->uv0.x = xr + ((float)payload->uv0.x / (float)srcrect.w) * wr;
payload->uv0.y = yr + ((float)payload->uv0.y / (float)srcrect.h) * hr;
payload->uv1.x = xr + ((float)payload->uv1.x / (float)srcrect.w) * wr;
payload->uv1.y = yr + ((float)payload->uv1.y / (float)srcrect.h) * hr;
payload->uv2.x = xr + ((float)payload->uv2.x / (float)srcrect.w) * wr;
payload->uv2.y = yr + ((float)payload->uv2.y / (float)srcrect.h) * hr;
}
textures_bind(&ctx.texture_cache, texture_key, GL_TEXTURE_2D);
glBindBuffer(GL_ARRAY_BUFFER, batch->buffer);
/* upload batched data */
glBufferData(GL_ARRAY_BUFFER,
primitives_len * sizeof (struct uncolored_space_triangle_payload),
batch->primitives,
GL_STREAM_DRAW);
/* vertex specification*/
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3,
GL_FLOAT,
offsetof(struct uncolored_space_triangle_payload, v1),
(void *)offsetof(struct uncolored_space_triangle_payload, v0));
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glClientActiveTexture(GL_TEXTURE0);
glTexCoordPointer(2,
GL_FLOAT,
offsetof(struct uncolored_space_triangle_payload, v1),
(void *)offsetof(struct uncolored_space_triangle_payload, uv0));
/* commit for drawing */
glDrawArrays(GL_TRIANGLES, 0, 3 * (int)primitives_len);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
/* invalidate the buffer immediately */
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
static void render_space(void) {
glEnable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0);
/* solid white, no modulation */
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) {
draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value,
ctx.uncolored_mesh_batches[i].key);
}
}
2024-07-08 00:44:20 +00:00
void render(void) {
textures_update_atlas(&ctx.texture_cache);
/* fit rendering context onto the resizable screen */
if ((float)ctx.window_w / (float)ctx.window_h > RENDER_BASE_RATIO) {
float ratio = (float)ctx.window_h / (float)RENDER_BASE_HEIGHT;
int w = (int)((float)RENDER_BASE_WIDTH * ratio);
glViewport(
ctx.window_w / 2 - w / 2,
0,
w,
ctx.window_h
);
} else {
float ratio = (float)ctx.window_w / (float)RENDER_BASE_WIDTH;
int h = (int)((float)RENDER_BASE_HEIGHT * ratio);
glViewport(
0,
ctx.window_h / 2 - h / 2,
ctx.window_w,
h
);
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_ALPHA_TEST);
glClearColor((1.0f / 255) * 230,
(1.0f / 255) * 230,
(1.0f / 255) * 230, 1);
glClear(GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT);
2024-07-08 00:44:20 +00:00
{
glDisable(GL_CULL_FACE);
glDepthFunc(GL_ALWAYS); /* fill depth buffer with ones, 2d view is always in front */
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glEnable(GL_TEXTURE_2D);
render_sprites();
render_rectangles();
render_circles();
}
2024-07-08 00:44:20 +00:00
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glEnable(GL_CULL_FACE);
glDepthFunc(GL_LESS);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/* TODO: use depth test to optimize gui regions away */
render_space();
}
SDL_GL_SwapWindow(ctx.window);
2024-07-08 00:44:20 +00:00
}