#include "private/rendering.h" #include "context.h" #include "textures.h" #include #include #include #include #include #include /* http://www.swiftless.com/opengltuts.html */ 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); } /* * 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), .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), .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; } /* 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; 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 */ /* 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 */ static void render_sprite(struct sprite_primitive *sprite) { glEnableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTexture(GL_TEXTURE0); 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 }); glColor4ub(sprite->color.r, sprite->color.g, sprite->color.b, sprite->color.a); // SDL_SetTextureBlendMode(texture, sprite->blend_mode); 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); } 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); glDrawArrays(GL_TRIANGLES, 0, 6); glDisableClientState(GL_VERTEX_ARRAY); } /* 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); 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); } } 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); { 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(); } { 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); }