Nimbin[12]?Games / Entropie / entropie/src/render/renderer.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/render/renderer.c 28.4 KB · 631 lines raw
#include "render/renderer.h"
#include "render/shader.h"

#include <string.h>
#include <SDL3/SDL.h>

#define BOIL_FPS            12.0f   /* "animated on twos" */
#define SHADOW_MAP_SIZE     4096
/* Texture units: 0 is left free for uploads (binding a texture there to
   update it must never unbind what a shader samples). */
#define UNIT_WEAR           1
#define UNIT_SHADOW         2
#define UNIT_WATER          3
#define UNIT_SURFACE        4
#define SHADOW_HALF_EXTENT  40.0f   /* shadowed area around the focus point */
#define SHADOW_DEPTH_RANGE  80.0f   /* depth range in front of/behind the focus */
#define SHADOW_LIGHT_DIST   100.0f

/* ---------- Setup ---------- */

static bool loadShaders(Renderer *r) {
    SceneShader *s = &r->scene;
    s->program = Shader_load("shaders/scene.vert", "shaders/scene.frag");
    if (!s->program) return false;
    s->model           = glGetUniformLocation(s->program, "model");
    s->view            = glGetUniformLocation(s->program, "view");
    s->projection      = glGetUniformLocation(s->program, "projection");
    s->light_space     = glGetUniformLocation(s->program, "lightSpaceMatrix");
    s->boil_time       = glGetUniformLocation(s->program, "boilTime");
    s->view_pos        = glGetUniformLocation(s->program, "viewPos");
    s->light_dir       = glGetUniformLocation(s->program, "lightDirection");
    s->light_color     = glGetUniformLocation(s->program, "lightColor");
    s->shadow_color    = glGetUniformLocation(s->program, "shadowColor");
    s->sky_top         = glGetUniformLocation(s->program, "skyTop");
    s->sky_horizon     = glGetUniformLocation(s->program, "skyHorizon");
    s->shadow_strength = glGetUniformLocation(s->program, "shadowStrength");
    s->fire_light      = glGetUniformLocation(s->program, "fireLight");
    s->tint            = glGetUniformLocation(s->program, "tint");
    s->puff            = glGetUniformLocation(s->program, "puff");
    s->grass           = glGetUniformLocation(s->program, "grass");
    s->rain_wet        = glGetUniformLocation(s->program, "rainWet");
    s->shadow_map      = glGetUniformLocation(s->program, "shadowMap");
    s->terrain_size    = glGetUniformLocation(s->program, "terrainSize");
    s->grass_depth     = glGetUniformLocation(s->program, "grassDepth");
    s->decal_size      = glGetUniformLocation(s->program, "decalSize");
    s->wear_map        = glGetUniformLocation(s->program, "wearMap");
    s->water_map       = glGetUniformLocation(s->program, "waterMap");
    s->surface_map     = glGetUniformLocation(s->program, "surfaceMap");
    s->holes           = glGetUniformLocation(s->program, "holes");
    s->hole_size       = glGetUniformLocation(s->program, "holeSize");
    s->hole_count      = glGetUniformLocation(s->program, "holeCount");

    ShadowShader *d = &r->shadow;
    d->program = Shader_load("shaders/shadow.vert", "shaders/shadow.frag");
    if (!d->program) return false;
    d->model       = glGetUniformLocation(d->program, "model");
    d->light_space = glGetUniformLocation(d->program, "lightSpaceMatrix");
    d->boil_time   = glGetUniformLocation(d->program, "boilTime");

    InkShader *k = &r->ink;
    k->program = Shader_load("shaders/ink.vert", "shaders/ink.frag");
    if (!k->program) return false;
    k->color_tex        = glGetUniformLocation(k->program, "colorTex");
    k->normal_depth_tex = glGetUniformLocation(k->program, "normalDepthTex");
    k->texel            = glGetUniformLocation(k->program, "texel");
    k->boil_time        = glGetUniformLocation(k->program, "boilTime");
    k->time             = glGetUniformLocation(k->program, "time");
    k->drunk            = glGetUniformLocation(k->program, "drunk");
    k->dark             = glGetUniformLocation(k->program, "dark");

    FigureShader *f = &r->figure;
    f->program = Shader_load("shaders/figure.vert", "shaders/figure.frag");
    if (!f->program) return false;
    f->view_proj       = glGetUniformLocation(f->program, "viewProj");
    f->view            = glGetUniformLocation(f->program, "view");
    f->light_space     = glGetUniformLocation(f->program, "lightSpaceMatrix");
    f->shadow_pass     = glGetUniformLocation(f->program, "shadowPass");
    f->boil_time       = glGetUniformLocation(f->program, "boilTime");
    f->figure_pos      = glGetUniformLocation(f->program, "figurePos");
    f->card_center     = glGetUniformLocation(f->program, "cardCenter");
    f->card_u          = glGetUniformLocation(f->program, "cardU");
    f->card_v          = glGetUniformLocation(f->program, "cardV");
    f->facing          = glGetUniformLocation(f->program, "facing");
    f->card_min        = glGetUniformLocation(f->program, "cardMin");
    f->card_max        = glGetUniformLocation(f->program, "cardMax");
    f->joints          = glGetUniformLocation(f->program, "joints");
    f->skin            = glGetUniformLocation(f->program, "skinColor");
    f->hair            = glGetUniformLocation(f->program, "hairColor");
    f->shirt           = glGetUniformLocation(f->program, "shirtColor");
    f->jacket          = glGetUniformLocation(f->program, "jacketColor");
    f->pants           = glGetUniformLocation(f->program, "pantsColor");
    f->shoes           = glGetUniformLocation(f->program, "shoeColor");
    f->hair_style      = glGetUniformLocation(f->program, "hairStyle");
    f->long_sleeves    = glGetUniformLocation(f->program, "longSleeves");
    f->cast            = glGetUniformLocation(f->program, "castLimb");
    f->head_forward    = glGetUniformLocation(f->program, "headForward");
    f->head_up         = glGetUniformLocation(f->program, "headUp");
    f->chest_forward   = glGetUniformLocation(f->program, "chestForward");
    f->waist_bend      = glGetUniformLocation(f->program, "waistBend");
    f->face            = glGetUniformLocation(f->program, "face");
    f->expression      = glGetUniformLocation(f->program, "expression");
    f->stream          = glGetUniformLocation(f->program, "stream");
    f->stream_kind     = glGetUniformLocation(f->program, "streamKind");
    f->stream_width    = glGetUniformLocation(f->program, "streamWidth");
    f->garment         = glGetUniformLocation(f->program, "garment");
    f->undress         = glGetUniformLocation(f->program, "undress");
    f->top             = glGetUniformLocation(f->program, "top");
    f->jacket_tied     = glGetUniformLocation(f->program, "jacketTied");
    f->belly           = glGetUniformLocation(f->program, "belly");
    f->wet             = glGetUniformLocation(f->program, "soaked");
    f->hood            = glGetUniformLocation(f->program, "hood");
    f->hat             = glGetUniformLocation(f->program, "hat");
    f->hat_color       = glGetUniformLocation(f->program, "hatColor");
    f->cig_a           = glGetUniformLocation(f->program, "cigA");
    f->uri_a           = glGetUniformLocation(f->program, "uriA");
    f->uri_b           = glGetUniformLocation(f->program, "uriB");
    f->uri             = glGetUniformLocation(f->program, "uri");
    f->cig_tip         = glGetUniformLocation(f->program, "cigTip");
    f->cig_glow        = glGetUniformLocation(f->program, "cigGlow");
    f->species         = glGetUniformLocation(f->program, "species");
    f->tail_a          = glGetUniformLocation(f->program, "tailA");
    f->tail_b          = glGetUniformLocation(f->program, "tailB");
    f->view_pos        = glGetUniformLocation(f->program, "viewPos");
    f->light_dir       = glGetUniformLocation(f->program, "lightDirection");
    f->light_color     = glGetUniformLocation(f->program, "lightColor");
    f->shadow_color    = glGetUniformLocation(f->program, "shadowColor");
    f->sky_top         = glGetUniformLocation(f->program, "skyTop");
    f->sky_horizon     = glGetUniformLocation(f->program, "skyHorizon");
    f->shadow_strength = glGetUniformLocation(f->program, "shadowStrength");
    f->fire_light      = glGetUniformLocation(f->program, "fireLight");
    f->shadow_map      = glGetUniformLocation(f->program, "shadowMap");
    return true;
}

static GLuint createTexture(GLint internal, int w, int h, GLenum format, GLenum type, GLint filter) {
    GLuint tex;
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexImage2D(GL_TEXTURE_2D, 0, internal, w, h, 0, format, type, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_2D, 0);
    return tex;
}

static bool createShadowMap(Renderer *r) {
    r->shadow_size = SHADOW_MAP_SIZE;
    r->shadow_map = createTexture(GL_DEPTH_COMPONENT24, r->shadow_size, r->shadow_size,
                                  GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, GL_NEAREST);

    glGenFramebuffers(1, &r->shadow_fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, r->shadow_fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, r->shadow_map, 0);
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
    bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    if (!complete) SDL_Log("Shadow framebuffer incomplete");
    return complete;
}

static void destroyTarget(SceneTarget *t) {
    if (t->fbo)          glDeleteFramebuffers(1, &t->fbo);
    if (t->color)        glDeleteTextures(1, &t->color);
    if (t->normal_depth) glDeleteTextures(1, &t->normal_depth);
    if (t->depth)        glDeleteTextures(1, &t->depth);
    memset(t, 0, sizeof(*t));
}

static bool createTarget(SceneTarget *t, int w, int h) {
    destroyTarget(t);
    t->width  = w;
    t->height = h;
    t->color        = createTexture(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR);
    t->normal_depth = createTexture(GL_RGBA16F, w, h, GL_RGBA, GL_FLOAT, GL_NEAREST);
    t->depth        = createTexture(GL_DEPTH_COMPONENT24, w, h, GL_DEPTH_COMPONENT,
                                    GL_UNSIGNED_INT, GL_NEAREST);

    glGenFramebuffers(1, &t->fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, t->fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, t->color, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, t->normal_depth, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, t->depth, 0);
    const GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, buffers);
    bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    if (!complete) SDL_Log("Scene framebuffer incomplete (%dx%d)", w, h);
    return complete;
}

bool Renderer_init(Renderer *r) {
    memset(r, 0, sizeof(*r));
    if (!loadShaders(r) || !createShadowMap(r)) {
        Renderer_shutdown(r);
        return false;
    }
    glGenVertexArrays(1, &r->fullscreen_vao); /* core profile needs a VAO */
    glEnable(GL_DEPTH_TEST);
    return true;
}

void Renderer_shutdown(Renderer *r) {
    Shader_destroy(&r->scene.program);
    Shader_destroy(&r->shadow.program);
    Shader_destroy(&r->ink.program);
    Shader_destroy(&r->figure.program);
    destroyTarget(&r->target);
    if (r->fullscreen_vao) glDeleteVertexArrays(1, &r->fullscreen_vao);
    if (r->shadow_fbo)     glDeleteFramebuffers(1, &r->shadow_fbo);
    if (r->shadow_map)     glDeleteTextures(1, &r->shadow_map);
    memset(r, 0, sizeof(*r));
}

/* ---------- Frame ---------- */

void Renderer_beginFrame(Renderer *r, int width, int height, float time) {
    r->width     = width;
    r->height    = height;
    r->time      = time;
    r->boil_time = floorf(time * BOIL_FPS) / BOIL_FPS;
    r->stats.drawn = r->stats.culled = 0;

    if (r->target.width != width || r->target.height != height)
        createTarget(&r->target, width, height);

    /* Nothing that we render into may stay bound for sampling. */
    for (int unit = UNIT_SHADOW; unit >= 0; unit--) {
        glActiveTexture(GL_TEXTURE0 + (GLenum)unit);
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    glBindFramebuffer(GL_FRAMEBUFFER, r->target.fbo);
    glViewport(0, 0, width, height);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

/* ---------- Shadow pass ---------- */

void Renderer_beginShadowPass(Renderer *r, const Lighting *light, Vec3 focus) {
    r->shadow_focus = focus;
    Vec3 forward = vec3_normalize(light->light_direction);
    Vec3 helper  = fabsf(forward.y) > 0.99f ? vec3(1, 0, 0) : vec3(0, 1, 0);
    Vec3 up      = vec3_cross(forward, vec3_normalize(vec3_cross(helper, forward)));
    Mat4 view    = mat4_look_at(vec3_scale(forward, SHADOW_LIGHT_DIST), vec3_zero(), up);

    /* Center the shadow area on the focus, snapped to whole shadow-map texels
       so shadow edges don't crawl while the camera moves. */
    const float texel = 2.0f * SHADOW_HALF_EXTENT / (float)SHADOW_MAP_SIZE;
    Vec3 f = mat4_mul_point(view, focus);
    float cx = floorf(f.x / texel) * texel;
    float cy = floorf(f.y / texel) * texel;
    Mat4 proj = mat4_orthographic(cx - SHADOW_HALF_EXTENT, cx + SHADOW_HALF_EXTENT,
                                  cy - SHADOW_HALF_EXTENT, cy + SHADOW_HALF_EXTENT,
                                  -f.z - SHADOW_DEPTH_RANGE, -f.z + SHADOW_DEPTH_RANGE);
    r->light_space = mat4_mul(proj, view);
    r->frustum = frustum_from_matrix(r->light_space); /* (only what falls into the shadow map) */

    glBindFramebuffer(GL_FRAMEBUFFER, r->shadow_fbo);
    glViewport(0, 0, r->shadow_size, r->shadow_size);
    glClear(GL_DEPTH_BUFFER_BIT);

    /* Faces towards the light cast the shadow, so it starts right at the
       base of whatever stands on the ground (back faces left a lit rim
       there). Receivers avoid acne with their normal offset instead. */
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glEnable(GL_POLYGON_OFFSET_FILL);
    glPolygonOffset(1.0f, 1.0f);

    /* Figures cut their shadow silhouette from a card facing the light. */
    const FigureShader *fs = &r->figure;
    const Mat4 identity = mat4_identity();
    glUseProgram(fs->program);
    glUniformMatrix4fv(fs->view_proj,   1, GL_FALSE, r->light_space.m);
    glUniformMatrix4fv(fs->view,        1, GL_FALSE, identity.m);
    glUniformMatrix4fv(fs->light_space, 1, GL_FALSE, identity.m);
    glUniform1i(fs->shadow_pass, 1);
    glUniform1f(fs->boil_time, r->boil_time);
    glUniform3f(fs->light_dir, forward.x, forward.y, forward.z);
    r->light_dir   = forward;
    r->shadow_pass = true;

    glUseProgram(r->shadow.program);
    glUniformMatrix4fv(r->shadow.light_space, 1, GL_FALSE, r->light_space.m);
    glUniform1f(r->shadow.boil_time, r->boil_time);
    r->pass_program = r->shadow.program;
}

/* ---------- Culling: only what's in view is drawn ---------- */

#define CULL_MARGIN 0.5f /* wobble, wind in the grass, the ground sinking under trampled things */

bool Renderer_inView(Renderer *r, Vec3 center, float radius) {
    bool in = frustum_sphere(&r->frustum, center, radius + CULL_MARGIN);
    if (in) r->stats.drawn++;
    else    r->stats.culled++;
    return in;
}

static bool meshInView(Renderer *r, const Mesh *mesh, Mat4 model) {
    if (!mesh->vao) return false;
    float scale = 0.0f;
    for (int c = 0; c < 3; c++)
        scale = fmaxf(scale, sqrtf(model.m[c * 4] * model.m[c * 4] + model.m[c * 4 + 1] * model.m[c * 4 + 1] +
                                   model.m[c * 4 + 2] * model.m[c * 4 + 2]));
    return Renderer_inView(r, mat4_mul_point(model, mesh->center), mesh->radius * scale);
}

void Renderer_drawShadowCaster(Renderer *r, const Mesh *mesh, Mat4 model) {
    if (!meshInView(r, mesh, model)) return;
    glUniformMatrix4fv(r->shadow.model, 1, GL_FALSE, model.m);
    Mesh_draw(mesh);
}

void Renderer_endShadowPass(Renderer *r) {
    glDisable(GL_POLYGON_OFFSET_FILL);
    glCullFace(GL_BACK);
    glBindFramebuffer(GL_FRAMEBUFFER, r->target.fbo);
    glViewport(0, 0, r->width, r->height);
}

/* ---------- Scene pass ---------- */

static void setVec3(GLint loc, Vec3 v) { glUniform3f(loc, v.x, v.y, v.z); }

void Renderer_beginScenePass(Renderer *r, const Camera *camera, const Lighting *light) {
    const SceneShader *s = &r->scene;
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glUseProgram(s->program);

    glUniformMatrix4fv(s->view,        1, GL_FALSE, camera->view.m);
    glUniformMatrix4fv(s->projection,  1, GL_FALSE, camera->projection.m);
    glUniformMatrix4fv(s->light_space, 1, GL_FALSE, r->light_space.m);
    glUniform1f(s->boil_time, r->boil_time);

    setVec3(s->view_pos,     camera->position);
    setVec3(s->light_dir,    light->light_direction);
    setVec3(s->light_color,  light->light_color);
    setVec3(s->shadow_color, light->shadow_color);
    setVec3(s->sky_top,      light->sky_top);
    setVec3(s->sky_horizon,  light->sky_horizon);
    glUniform1f(s->shadow_strength, light->shadow_strength);
    glUniform1f(s->rain_wet, light->wet);
    glUniform4f(s->fire_light, light->fire_position.x, light->fire_position.y, light->fire_position.z, light->fire_strength);

    glUniform1f(s->terrain_size, 0.0f); /* 0 = regular object */
    glUniform1f(s->decal_size, 0.0f);
    glUniform4f(s->tint, 1.0f, 1.0f, 1.0f, 0.0f);
    glUniform1f(s->grass, 0.0f);
    glUniform1f(s->puff, 0.0f);
    glUniform1i(s->wear_map, UNIT_WEAR);
    glUniform1i(s->water_map, UNIT_WATER);
    glUniform1i(s->surface_map, UNIT_SURFACE);

    glActiveTexture(GL_TEXTURE0 + UNIT_SHADOW);
    glBindTexture(GL_TEXTURE_2D, r->shadow_map);
    glActiveTexture(GL_TEXTURE0);
    glUniform1i(s->shadow_map, UNIT_SHADOW);

    /* Figures share camera and lighting */
    const FigureShader *f = &r->figure;
    const Mat4 view_proj = mat4_mul(camera->projection, camera->view);
    r->frustum = frustum_from_matrix(view_proj);
    glUseProgram(f->program);
    glUniformMatrix4fv(f->view_proj,   1, GL_FALSE, view_proj.m);
    glUniformMatrix4fv(f->view,        1, GL_FALSE, camera->view.m);
    glUniformMatrix4fv(f->light_space, 1, GL_FALSE, r->light_space.m);
    glUniform1i(f->shadow_pass, 0);
    glUniform1f(f->boil_time, r->boil_time);
    setVec3(f->view_pos,     camera->position);
    setVec3(f->light_dir,    light->light_direction);
    setVec3(f->light_color,  light->light_color);
    setVec3(f->shadow_color, light->shadow_color);
    setVec3(f->sky_top,      light->sky_top);
    setVec3(f->sky_horizon,  light->sky_horizon);
    glUniform1f(f->shadow_strength, light->shadow_strength);
    glUniform4f(f->fire_light, light->fire_position.x, light->fire_position.y, light->fire_position.z, light->fire_strength);
    glUniform1i(f->shadow_map, UNIT_SHADOW);
    r->viewer_pos  = camera->position;
    r->shadow_pass = false;

    glUseProgram(s->program);
    r->pass_program = s->program;
}

void Renderer_beginAirPass(Renderer *r) {
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glDepthFunc(GL_LESS);
    glUseProgram(r->scene.program);
}

void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float glow) {
    if (!meshInView(r, mesh, model)) return;
    glUniform4f(r->scene.tint, rgb.x, rgb.y, rgb.z, glow);
    glUniformMatrix4fv(r->scene.model, 1, GL_FALSE, model.m);
    Mesh_draw(mesh);
    glUniform4f(r->scene.tint, 1.0f, 1.0f, 1.0f, 0.0f);
}

void Renderer_drawSmoke(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb) {
    if (!meshInView(r, mesh, model)) return;
    glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* (no normal / depth for the ink: no outline) */
    glDepthMask(GL_FALSE);
    Renderer_drawMeshTinted(r, mesh, model, rgb, 0.0f);
    glDepthMask(GL_TRUE);
    glColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}

void Renderer_drawPuff(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float alpha) {
    if (!meshInView(r, mesh, model) || alpha <= 0.01f) return;
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* (no outline) */
    glDepthMask(GL_FALSE);
    glUniform1f(r->scene.puff, alpha);
    Renderer_drawMeshTinted(r, mesh, model, rgb, 0.0f);
    glUniform1f(r->scene.puff, 0.0f);
    glDepthMask(GL_TRUE);
    glColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDisable(GL_BLEND);
}

void Renderer_drawGrass(Renderer *r, const Mesh *mesh, float terrain_size, float grass_depth) {
    if (!meshInView(r, mesh, mat4_identity())) return;
    glUniform1f(r->scene.grass, terrain_size);
    glUniform1f(r->scene.grass_depth, grass_depth);
    glUniformMatrix4fv(r->scene.model, 1, GL_FALSE, mat4_identity().m);
    glDisable(GL_CULL_FACE); /* blades are seen from both sides */
    Mesh_draw(mesh);
    glEnable(GL_CULL_FACE);
    glUniform1f(r->scene.grass, 0.0f);
}

void Renderer_drawMesh(Renderer *r, const Mesh *mesh, Mat4 model) {
    if (!meshInView(r, mesh, model)) return;
    glUniformMatrix4fv(r->scene.model, 1, GL_FALSE, model.m);
    Mesh_draw(mesh);
}

void Renderer_drawDecal(Renderer *r, const Mesh *mesh, Mat4 model, float terrain_size) {
    if (!meshInView(r, mesh, model)) return;
    glUniform1f(r->scene.decal_size, terrain_size);
    glUniformMatrix4fv(r->scene.model, 1, GL_FALSE, model.m);
    Mesh_draw(mesh);
    glUniform1f(r->scene.decal_size, 0.0f);
}

void Renderer_setHoles(Renderer *r, const float (*rects)[6], int count) {
    float at[RENDER_HOLES * 4] = { 0 }, size[RENDER_HOLES * 2] = { 0 };
    if (count > RENDER_HOLES) count = RENDER_HOLES;
    for (int i = 0; i < count; i++) {
        for (int k = 0; k < 4; k++) at[i * 4 + k] = rects[i][k];
        size[i * 2] = rects[i][4];
        size[i * 2 + 1] = rects[i][5];
    }
    glUniform4fv(r->scene.holes, RENDER_HOLES, at);
    glUniform2fv(r->scene.hole_size, RENDER_HOLES, size);
    glUniform1i(r->scene.hole_count, count);
}

void Renderer_drawTerrain(Renderer *r, const Mesh *mesh, GLuint wear_map, GLuint water_map, GLuint surface_map,
                          float size, float grass_depth) {
    const SceneShader *s = &r->scene;
    glActiveTexture(GL_TEXTURE0 + UNIT_WEAR); /* (bound even out of view: grass and decals read them too) */
    glBindTexture(GL_TEXTURE_2D, wear_map);
    glActiveTexture(GL_TEXTURE0 + UNIT_WATER);
    glBindTexture(GL_TEXTURE_2D, water_map);
    glActiveTexture(GL_TEXTURE0 + UNIT_SURFACE);
    glBindTexture(GL_TEXTURE_2D, surface_map);
    glActiveTexture(GL_TEXTURE0);
    if (!meshInView(r, mesh, mat4_identity())) return;

    glUniform1f(s->terrain_size, size);
    glUniform1f(s->grass_depth, grass_depth);
    glUniformMatrix4fv(s->model, 1, GL_FALSE, mat4_identity().m);
    Mesh_draw(mesh);
    glUniform1f(s->terrain_size, 0.0f);
}

/* ---------- Figures ---------- */

#define FIGURE_BODY_RADIUS 0.25f /* how far flesh and hair reach beyond the joints */

void Renderer_drawFigure(Renderer *r, const FigurePose *pose) {
    const FigureShader *f = &r->figure;
    const Skeleton *sk = &pose->skeleton;

    /* Card through the middle of the body, facing the viewer:
       the camera, or the light in the shadow pass. */
    Vec3 center = vec3_zero();
    Vec3 world[JOINT_COUNT];
    for (int i = 0; i < JOINT_COUNT; i++) {
        world[i] = Figure_toWorld(pose->position, pose->facing, sk->joints[i]);
        center = vec3_add(center, world[i]);
    }
    center = vec3_scale(center, 1.0f / (float)JOINT_COUNT);
    {
        float reach = 0.0f;
        for (int i = 0; i < JOINT_COUNT; i++) reach = fmaxf(reach, vec3_length(vec3_sub(world[i], center)));
        if (sk->stream_kind != STREAM_NONE)
            for (int k = 0; k < 4; k++)
                reach = fmaxf(reach, vec3_length(vec3_sub(Figure_toWorld(pose->position, pose->facing, sk->stream[k]), center)));
        if (!Renderer_inView(r, center, reach + FIGURE_BODY_RADIUS)) return;
    }

    Vec3 to_viewer = r->shadow_pass ? r->light_dir : vec3_normalize(vec3_sub(r->viewer_pos, center));
    Vec3 u = vec3_cross(vec3(0, 1, 0), to_viewer);
    u = vec3_length(u) > 1e-3f ? vec3_normalize(u) : vec3_normalize(vec3_cross(pose->facing, vec3(0, 1, 0)));
    Vec3 v = vec3_normalize(vec3_cross(to_viewer, u));

    /* Bounds of the joints as the viewer sees them on the card: straight
       along the light, or in perspective from the camera eye. */
    float min_x = 1e9f, max_x = -1e9f, min_y = 1e9f, max_y = -1e9f;
    float card_dist = vec3_dot(vec3_sub(center, r->viewer_pos), vec3_scale(to_viewer, -1.0f));
    /* The card also covers a stream to the ground */
    Vec3 points[JOINT_COUNT + 4];
    int n = JOINT_COUNT;
    for (int i = 0; i < JOINT_COUNT; i++) points[i] = world[i];
    if (sk->stream_kind != STREAM_NONE)
        for (int k = 0; k < 4; k++) points[n++] = Figure_toWorld(pose->position, pose->facing, sk->stream[k]);
    for (int i = 0; i < n; i++) {
        Vec3 hit = points[i];
        float grow = 1.0f;
        if (!r->shadow_pass) {
            Vec3 from_eye = vec3_sub(points[i], r->viewer_pos);
            float depth = fmaxf(vec3_dot(from_eye, vec3_scale(to_viewer, -1.0f)), 0.05f);
            grow = card_dist / depth;
            hit = vec3_add(r->viewer_pos, vec3_scale(from_eye, grow));
        }
        Vec3 rel = vec3_sub(hit, center);
        float x = vec3_dot(rel, u), y = vec3_dot(rel, v);
        float reach = FIGURE_BODY_RADIUS * fmaxf(grow, 1.0f);
        min_x = fminf(min_x, x - reach); max_x = fmaxf(max_x, x + reach);
        min_y = fminf(min_y, y - reach); max_y = fmaxf(max_y, y + reach);
    }
    const float margin = 0.02f;

    glUseProgram(f->program);
    glDisable(GL_CULL_FACE);

    setVec3(f->figure_pos,  pose->position);
    setVec3(f->facing,      pose->facing);
    setVec3(f->card_center, center);
    setVec3(f->card_u,      u);
    setVec3(f->card_v,      v);
    glUniform2f(f->card_min, min_x - margin, min_y - margin);
    glUniform2f(f->card_max, max_x + margin, max_y + margin);
    glUniform3fv(f->joints, JOINT_COUNT, &sk->joints[0].x);
    setVec3(f->skin,  pose->look.skin);
    setVec3(f->hair,  pose->look.hair);
    setVec3(f->shirt, pose->look.shirt);
    setVec3(f->jacket, pose->look.jacket);
    setVec3(f->pants, pose->look.pants);
    setVec3(f->shoes, pose->look.shoes);
    glUniform1i(f->hair_style, (GLint)pose->look.hair_style);
    glUniform1i(f->long_sleeves, pose->look.long_sleeves ? 1 : 0);
    glUniform1i(f->cast, pose->look.cast);
    setVec3(f->head_forward,  sk->head_forward);
    setVec3(f->head_up,       sk->head_up);
    setVec3(f->chest_forward, sk->chest_forward);
    setVec3(f->waist_bend,    sk->waist_bend);
    setVec3(f->face, vec3(sk->face.mouth, sk->face.eyelids, sk->face.brow));
    glUniform2f(f->expression, sk->face.smile, sk->face.gaze);
    glUniform1i(f->stream_kind, sk->stream_kind);
    glUniform3fv(f->stream, 4, &sk->stream[0].x);
    glUniform1f(f->stream_width, sk->stream_width);
    glUniform1i(f->garment, pose->look.garment);
    glUniform1f(f->undress, sk->undress);
    glUniform1i(f->top, pose->look.top);
    glUniform1i(f->jacket_tied, pose->look.jacket_tied && pose->look.top != TOP_JACKET ? 1 : 0);
    glUniform1f(f->belly, pose->look.belly);
    glUniform1f(f->wet, pose->look.wet);
    glUniform1i(f->hood, pose->look.hoodie && pose->look.hood_up && pose->look.top == TOP_JACKET ? 1 : 0);
    glUniform1i(f->hat, pose->look.hat);
    glUniform3f(f->hat_color, pose->look.hat_color.x, pose->look.hat_color.y, pose->look.hat_color.z);
    setVec3(f->cig_a, sk->cig_a);
    setVec3(f->uri_a, sk->uri_a);
    setVec3(f->uri_b, sk->uri_b);
    glUniform1i(f->uri, sk->uri ? 1 : 0);
    setVec3(f->cig_tip, sk->cig_tip);
    glUniform1f(f->cig_glow, sk->cig_glow);
    glUniform1i(f->species, sk->species);
    setVec3(f->tail_a, sk->tail_a);
    setVec3(f->tail_b, sk->tail_b);

    glBindVertexArray(r->fullscreen_vao); /* no vertex data: the card is built from gl_VertexID */
    glDrawArrays(GL_TRIANGLES, 0, 6);
    glBindVertexArray(0);

    glEnable(GL_CULL_FACE);
    glUseProgram(r->pass_program);
}

/* ---------- Ink pass ---------- */

void Renderer_endFrame(Renderer *r) {
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0, 0, r->width, r->height);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    const InkShader *k = &r->ink;
    glUseProgram(k->program);
    glUniform2f(k->texel, 1.0f / (float)r->width, 1.0f / (float)r->height);
    glUniform1f(k->boil_time, r->boil_time);
    glUniform1f(k->time, r->time);
    glUniform1f(k->drunk, r->drunk);
    glUniform1f(k->dark, r->dark);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, r->target.color);
    glUniform1i(k->color_tex, 0);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, r->target.normal_depth);
    glUniform1i(k->normal_depth_tex, 1);

    glBindVertexArray(r->fullscreen_vao);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glBindVertexArray(0);

    glActiveTexture(GL_TEXTURE0);
    glEnable(GL_DEPTH_TEST);
}