Files
townengine/src/rendering/twn_draw.c
veclavtalica 3bfa86066e billboards!
2025-01-05 19:46:05 +03:00

535 lines
15 KiB
C

#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_c.h"
#include "twn_vec.h"
#include "twn_deferred_commands.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <stddef.h>
#include <tgmath.h>
DeferredCommand *deferred_commands;
/* TODO: have a default initialized one */
Matrix4 camera_projection_matrix;
Matrix4 camera_look_at_matrix;
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),
);
}
static void render_2d(void) {
use_2d_pipeline();
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;
}
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_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;
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) {
use_space_pipeline();
apply_fog();
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);
}
pop_fog();
}
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) {
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) {
(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) {
DeferredCommand const command = {
.type = DEFERRED_COMMAND_TYPE_DEPTH_RANGE,
.depth_range = {
.low = low,
.high = high
}
};
arrpush(deferred_commands, command);
}
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 use_texture_mode(TextureMode mode) {
DeferredCommand const command = {
.type = DEFERRED_COMMAND_TYPE_USE_TEXTURE_MODE,
.use_texture_mode = { mode }
};
arrpush(deferred_commands, command);
}
void use_2d_pipeline(void) {
DeferredCommand const command = {
.type = DEFERRED_COMMAND_TYPE_USE_PIPIELINE,
.use_pipeline = { PIPELINE_2D }
};
arrpush(deferred_commands, command);
}
void use_space_pipeline(void) {
DeferredCommand const command = {
.type = DEFERRED_COMMAND_TYPE_USE_PIPIELINE,
.use_pipeline = { PIPELINE_SPACE }
};
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_DEPTH_RANGE: {
finally_set_depth_range(deferred_commands[i].depth_range);
break;
}
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;
}
case DEFERRED_COMMAND_TYPE_USE_PIPIELINE: {
switch (deferred_commands[i].use_pipeline.pipeline) {
case PIPELINE_2D:
finally_use_2d_pipeline();
break;
case PIPELINE_SPACE:
finally_use_space_pipeline();
break;
case PIPELINE_NO:
default:
SDL_assert(false);
}
break;
}
case DEFERRED_COMMAND_TYPE_USE_TEXTURE_MODE: {
finally_use_texture_mode(deferred_commands[i].use_texture_mode.mode);
break;
}
case DEFERRED_COMMAND_TYPE_APPLY_FOG: {
finally_apply_fog(deferred_commands[i].apply_fog);
break;
}
case DEFERRED_COMMAND_TYPE_POP_FOG: {
finally_pop_fog();
break;
}
default:
SDL_assert(false);
}
}
}