594 lines
18 KiB
C
594 lines
18 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.h"
|
|
#include "twn_vec.h"
|
|
#include "twn_deferred_commands.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <stb_ds.h>
|
|
|
|
#include <stddef.h>
|
|
#include <math.h>
|
|
#include <tgmath.h>
|
|
|
|
|
|
DeferredCommand *deferred_commands;
|
|
|
|
/* TODO: with buffered render, don't we use camera of wrong frame right now ? */
|
|
Matrix4 camera_projection_matrix;
|
|
Matrix4 camera_look_at_matrix;
|
|
|
|
float camera_2d_rotation;
|
|
Vec2 camera_2d_position;
|
|
float camera_2d_zoom;
|
|
|
|
double depth_range_low, depth_range_high;
|
|
|
|
|
|
static void reset_camera_2d(void) {
|
|
camera_2d_position = (Vec2){0};
|
|
camera_2d_zoom = 1;
|
|
camera_2d_rotation = 0;
|
|
}
|
|
|
|
|
|
void render_clear(void) {
|
|
draw_camera((Vec3){0, 0, 0}, (Vec3){0, 0, 1}, (Vec3){0, 1, 0}, 1.57079632679f, 1);
|
|
reset_camera_2d();
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
/* TODO: benchmark which order works best for expected cases */
|
|
static void render_space(void) {
|
|
finally_draw_models();
|
|
|
|
/* 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) {
|
|
models_update_pre_textures();
|
|
textures_update_atlas(&ctx.texture_cache);
|
|
models_update_post_textures();
|
|
|
|
/* 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();
|
|
}
|
|
|
|
|
|
/* TODO: check for NaNs and alike */
|
|
TWN_API void draw_camera_2d(Vec2 position,
|
|
float rotation,
|
|
float zoom)
|
|
{
|
|
if (zoom <= 0) {
|
|
log_warn("Invalid zoom value given to draw_camera_2d()");
|
|
zoom = 0.1f;
|
|
}
|
|
|
|
camera_2d_position = position;
|
|
camera_2d_rotation = rotation;
|
|
camera_2d_zoom = zoom;
|
|
}
|
|
|
|
/* TODO: check for NaNs and alike */
|
|
void draw_camera(Vec3 position, Vec3 direction, Vec3 up, float fov, float zoom) {
|
|
bool const orthographic = fabsf(0.0f - fov) < 0.00001f;
|
|
if (!orthographic && fov >= (float)(M_PI))
|
|
log_warn("Invalid fov given (%f)", (double)fov);
|
|
|
|
Camera const camera = {
|
|
.fov = fov,
|
|
.pos = position,
|
|
.target = vec3_norm(direction),
|
|
.up = up,
|
|
.viewbox = {
|
|
(Vec2){ 1/-zoom, 1/zoom },
|
|
(Vec2){ 1/zoom, 1/-zoom }
|
|
},
|
|
};
|
|
|
|
if (!orthographic)
|
|
camera_projection_matrix = camera_perspective(&camera);
|
|
else
|
|
camera_projection_matrix = camera_orthographic(&camera);
|
|
|
|
camera_look_at_matrix = camera_look_at(&camera);
|
|
}
|
|
|
|
|
|
/* TODO: https://stackoverflow.com/questions/62493770/how-to-add-roll-in-camera-class */
|
|
/* TODOL call draw_camera() instead, to reuse the code */
|
|
DrawCameraFromPrincipalAxesResult draw_camera_from_principal_axes(Vec3 position,
|
|
float roll,
|
|
float pitch,
|
|
float yaw,
|
|
float fov,
|
|
float zoom)
|
|
{
|
|
bool const orthographic = fabsf(0.0f - fov) < 0.00001f;
|
|
if (!orthographic && fov >= (float)(M_PI))
|
|
log_warn("Invalid fov given (%f)", (double)fov);
|
|
|
|
(void)roll;
|
|
|
|
/* rotate so that yaw = 0 results in (0, 0, 1) target vector */
|
|
float yawc, yaws, pitchc, pitchs;
|
|
sincosf(yaw + (float)M_PI_2, &yaws, &yawc);
|
|
sincosf(pitch, &pitchs, &pitchc);
|
|
|
|
Vec3 const direction = vec3_norm(((Vec3){
|
|
yawc * pitchc,
|
|
pitchs,
|
|
yaws * pitchc,
|
|
}));
|
|
|
|
Vec3 const up = (Vec3){0, 1, 0};
|
|
|
|
draw_camera(position, direction, up, fov, zoom);
|
|
|
|
return (DrawCameraFromPrincipalAxesResult) {
|
|
.direction = direction,
|
|
.up = 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 = ctx.background_color,
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|