#include "twn_draw_c.h" #include "twn_draw.h" #include "twn_engine_context_c.h" #include "twn_camera_c.h" #include "twn_types.h" #include "twn_util.h" #include "twn_vec.h" #include "twn_deferred_commands.h" #include #include #include #include #include DeferredCommand *deferred_commands; /* TODO: have a default initialized one */ /* TODO: with buffered render, don't we use camera of wrong frame right now ? */ Matrix4 camera_projection_matrix; Matrix4 camera_look_at_matrix; double depth_range_low, depth_range_high; void render_queue_clear(void) { text_cache_reset_arena(&ctx.text_cache); /* 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_2d, 0); /* TODO: free memory if it isn't used for a while */ for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0); for (size_t i = 0; i < hmlenu(ctx.billboard_batches); ++i) arrsetlen(ctx.billboard_batches[i].value.primitives, 0); } void draw_nine_slice(const char *texture, Vec2 corners, Rect rect, float border_thickness, Color color) { const float bt = border_thickness; const float bt2 = bt * 2; /* combined size of the two borders in an axis */ Rect top_left = { .x = rect.x, .y = rect.y, .w = bt, .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, top_left), m_opt(texture_region, ((Rect) { 0, 0, bt, bt })), m_opt(color, color), ); Rect top_center = { .x = rect.x + bt, .y = rect.y, .w = rect.w - bt2, /* here bt2 represents the top left and right corners */ .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, top_center), m_opt(texture_region, ((Rect) { bt, 0, corners.x - bt2, bt })), m_opt(color, color), ); Rect top_right = { .x = rect.x + (rect.w - bt), .y = rect.y, .w = bt, .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, top_right), m_opt(texture_region, ((Rect) { corners.x - bt, 0, bt, bt })), m_opt(color, color), ); Rect center_left = { .x = rect.x, .y = rect.y + bt, .w = bt, .h = rect.h - bt2, /* here bt2 represents the top and bottom left corners */ }; m_sprite( m_set(texture, texture), m_set(rect, center_left), m_opt(texture_region, ((Rect) { 0, bt, bt, corners.y - bt2 })), m_opt(color, color), ); Rect center_right = { .x = rect.x + (rect.w - bt), .y = rect.y + bt, .w = bt, .h = rect.h - bt2, /* here bt2 represents the top and bottom right corners */ }; m_sprite( m_set(texture, texture), m_set(rect, center_right), m_opt(texture_region, ((Rect) { corners.x - bt, bt, bt, corners.y - bt2 })), m_opt(color, color), ); Rect bottom_left = { .x = rect.x, .y = rect.y + (rect.h - bt), .w = bt, .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, bottom_left), m_opt(texture_region, ((Rect) { 0, corners.y - bt, bt, bt })), m_opt(color, color), ); Rect bottom_center = { .x = rect.x + bt, .y = rect.y + (rect.h - bt), .w = rect.w - bt2, /* here bt2 represents the bottom left and right corners */ .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, bottom_center), m_opt(texture_region, ((Rect) { bt, corners.y - bt, corners.x - bt2, bt })), m_opt(color, color), ); Rect bottom_right = { .x = rect.x + (rect.w - bt), .y = rect.y + (rect.h - bt), .w = bt, .h = bt, }; m_sprite( m_set(texture, texture), m_set(rect, bottom_right), m_opt(texture_region, ((Rect) { corners.x - bt, corners.y - bt, bt, bt })), m_opt(color, color), ); Rect center = { .x = rect.x + bt, .y = rect.y + bt, .w = rect.w - bt2, .h = rect.h - bt2, }; m_sprite( m_set(texture, texture), m_set(rect, center), m_opt(texture_region, ((Rect) { bt, bt, corners.x - bt2, corners.y - bt2 })), m_opt(color, color), ); } TWN_API void draw_quad(char const *texture, Vec3 v0, /* upper-left */ Vec3 v1, /* bottom-left */ Vec3 v2, /* bottom-right */ Vec3 v3, /* upper-right */ Rect texture_region, Color color) { Vec2 const uv0 = { texture_region.x, texture_region.y }; Vec2 const uv1 = { texture_region.x, texture_region.y + texture_region.h }; Vec2 const uv2 = { texture_region.x + texture_region.w, texture_region.y + texture_region.h }; Vec2 const uv3 = { texture_region.x + texture_region.w, texture_region.y }; draw_triangle(texture, v0, v1, v3, uv0, uv1, uv3, color, color, color); draw_triangle(texture, v3, v1, v2, uv3, uv1, uv2, color, color, color); } static void render_2d(void) { const size_t render_queue_len = arrlenu(ctx.render_queue_2d); struct Render2DInvocation { Primitive2D const *primitive; double layer; union { struct QuadBatch quad_batch; }; }; /* first, collect all invocations, while merging into batches where applicable */ /* we separate into opaque and transparent ones, as it presents optimization opportunities */ struct Render2DInvocation *opaque_invocations = NULL; struct Render2DInvocation *ghostly_invocations = NULL; arrsetcap(opaque_invocations, render_queue_len); arrsetcap(ghostly_invocations, render_queue_len); for (size_t i = 0; i < render_queue_len; ++i) { const Primitive2D *current = &ctx.render_queue_2d[i]; // TODO: https://gamedev.stackexchange.com/questions/101136/using-full-resolution-of-depth-buffer-for-2d-rendering double const layer = ((double)((render_queue_len + 1) - i) / (double)(render_queue_len + 1)) * 0.75; switch (current->type) { case PRIMITIVE_2D_SPRITE: { const struct QuadBatch batch = collect_sprite_batch(current, render_queue_len - i); struct Render2DInvocation const invocation = { .primitive = current, .quad_batch = batch, .layer = layer, }; if (batch.mode == TEXTURE_MODE_GHOSTLY) arrput(ghostly_invocations, invocation); else arrput(opaque_invocations, invocation); i += batch.size - 1; break; } case PRIMITIVE_2D_RECT: { const struct QuadBatch batch = collect_rect_batch(current, render_queue_len - i); struct Render2DInvocation const invocation = { .primitive = current, .quad_batch = batch, .layer = layer, }; if (batch.mode == TEXTURE_MODE_GHOSTLY) arrput(ghostly_invocations, invocation); else arrput(opaque_invocations, invocation); i += batch.size - 1; break; } case PRIMITIVE_2D_CIRCLE: { struct Render2DInvocation const invocation = { .primitive = current, .layer = layer, }; if (current->circle.color.a != 255) arrput(ghostly_invocations, invocation); else arrput(opaque_invocations, invocation); break; } /* TODO: batching */ case PRIMITIVE_2D_LINE: { struct Render2DInvocation const invocation = { .primitive = current, .layer = layer, }; if (current->line.color.a != 255) arrput(ghostly_invocations, invocation); else arrput(opaque_invocations, invocation); break; } case PRIMITIVE_2D_TEXT: { struct Render2DInvocation const invocation = { .primitive = current, .layer = layer, }; arrput(ghostly_invocations, invocation); break; } default: SDL_assert(false); } } /* first issue all opaque primitives, front-to-back */ for (size_t i = 0; i < arrlenu(opaque_invocations); ++i) { struct Render2DInvocation const invocation = opaque_invocations[arrlenu(opaque_invocations) - 1 - i]; /* idea here is to set constant z write that moves further and further along */ /* with that every batch can early z reject against the previous */ /* additionally, it will also apply for future transparent passes, sandwitching in-between */ set_depth_range(invocation.layer, 1.0); switch (invocation.primitive->type) { case PRIMITIVE_2D_SPRITE: { render_sprite_batch(invocation.primitive, invocation.quad_batch); break; } case PRIMITIVE_2D_RECT: { render_rect_batch(invocation.primitive, invocation.quad_batch); break; } /* TODO: circle batching */ case PRIMITIVE_2D_CIRCLE: render_circle(&invocation.primitive->circle); break; case PRIMITIVE_2D_LINE: render_line(&invocation.primitive->line); break; case PRIMITIVE_2D_TEXT: default: SDL_assert(false); } } /* then issue all transparent primitives, back-to-front */ for (size_t i = 0; i < arrlenu(ghostly_invocations); ++i) { struct Render2DInvocation const invocation = ghostly_invocations[i]; /* now we use it not for writing layers, but inferring ordering */ set_depth_range(invocation.layer, 1.0); switch (invocation.primitive->type) { case PRIMITIVE_2D_SPRITE: { render_sprite_batch(invocation.primitive, invocation.quad_batch); break; } case PRIMITIVE_2D_RECT: { render_rect_batch(invocation.primitive, invocation.quad_batch); break; } /* TODO: circle batching */ case PRIMITIVE_2D_CIRCLE: render_circle(&invocation.primitive->circle); break; case PRIMITIVE_2D_TEXT: render_text(&invocation.primitive->text); break; case PRIMITIVE_2D_LINE: render_line(&invocation.primitive->line); break; default: SDL_assert(false); } } arrfree(opaque_invocations); arrfree(ghostly_invocations); } static void render_space(void) { /* nothing to do, abort */ /* as space pipeline isn't used we can have fewer changes and initialization costs */ if (hmlenu(ctx.uncolored_mesh_batches) != 0 || hmlenu(ctx.billboard_batches) != 0) { for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) { finally_draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value, ctx.uncolored_mesh_batches[i].key); } for (size_t i = 0; i < hmlenu(ctx.billboard_batches); ++i) { finally_draw_billboard_batch(&ctx.billboard_batches[i].value, ctx.billboard_batches[i].key); } } render_skybox(); /* after everything else, as to use depth buffer for early z rejection */ } void render(void) { textures_update_atlas(&ctx.texture_cache); /* fit rendering context onto the resizable screen */ if (ctx.window_size_has_changed) { setup_viewport((int)ctx.viewport_rect.x, (int)ctx.viewport_rect.y, (int)ctx.viewport_rect.w, (int)ctx.viewport_rect.h); } start_render_frame(); { render_space(); render_2d(); } end_render_frame(); } void draw_camera(Vec3 position, float fov, Vec3 up, Vec3 direction) { if (fabsf(0.0f - fov) < 0.00001f || fov >= M_PIf) log_warn("Invalid fov given (%f)", (double)fov); Camera const camera = { .fov = fov, .pos = position, .target = direction, .up = up, }; camera_projection_matrix = camera_perspective(&camera); camera_look_at_matrix = camera_look_at(&camera); } /* TODO: https://stackoverflow.com/questions/62493770/how-to-add-roll-in-camera-class */ DrawCameraFromPrincipalAxesResult draw_camera_from_principal_axes(Vec3 position, float fov, float roll, float pitch, float yaw) { if (fabsf(0.0f - fov) < 0.00001f || fov >= M_PIf) log_warn("Invalid fov given (%f)", (double)fov); (void)roll; float yawc, yaws, pitchc, pitchs; sincosf(yaw, &yaws, &yawc); sincosf(pitch, &pitchs, &pitchc); Camera const camera = { .fov = fov, .pos = position, .target = m_vec_norm(((Vec3){ yawc * pitchc, pitchs, yaws * pitchc, })), .up = (Vec3){0, 1, 0}, }; camera_projection_matrix = camera_perspective(&camera); camera_look_at_matrix = camera_look_at(&camera); return (DrawCameraFromPrincipalAxesResult) { .direction = camera.target, .up = camera.up, }; } void set_depth_range(double low, double high) { depth_range_low = low; depth_range_high = high; } void clear_draw_buffer(void) { /* TODO: we can optimize a rectangle drawn over whole window to a clear color call*/ DeferredCommand command = { .type = DEFERRED_COMMAND_TYPE_CLEAR, .clear = (DeferredCommandClear) { .clear_color = true, .clear_depth = true, .clear_stencil = true, .color = (Color) { 230, 230, 230, 1 } } }; arrpush(deferred_commands, command); } void issue_deferred_draw_commands(void) { for (size_t i = 0; i < arrlenu(deferred_commands); ++i) { switch (deferred_commands[i].type) { case DEFERRED_COMMAND_TYPE_CLEAR: { finally_clear_draw_buffer(deferred_commands[i].clear); break; } case DEFERRED_COMMAND_TYPE_DRAW: { finally_draw_command(deferred_commands[i].draw); break; } case DEFERRED_COMMAND_TYPE_DRAW_SKYBOX: { finally_render_skybox(deferred_commands[i].draw_skybox); break; } default: SDL_assert(false); } } } /* TODO: Support thickness */ void draw_line(Vec2 start, Vec2 finish, float thickness, Color color) { if (fabsf(1.0f - thickness) >= 0.00001f) log_warn("Thickness isn't yet implemented for line drawing (got %f)", (double)thickness); LinePrimitive line = { .start = start, .finish = finish, .thickness = thickness, .color = color, }; Primitive2D primitive = { .type = PRIMITIVE_2D_LINE, .line = line, }; arrput(ctx.render_queue_2d, primitive); } void draw_box(Rect rect, float thickness, Color color) { draw_line((Vec2){rect.x, rect.y}, (Vec2){rect.x + rect.w, rect.y}, thickness, color); draw_line((Vec2){rect.x + rect.w, rect.y}, (Vec2){rect.x + rect.w, rect.y + rect.h}, thickness, color); draw_line((Vec2){rect.x + rect.w, rect.y + rect.h}, (Vec2){rect.x, rect.y + rect.h}, thickness, color); draw_line((Vec2){rect.x, rect.y + rect.h}, (Vec2){rect.x, rect.y}, thickness, color); }