Nimbin[12]?Games / Entropie / entropie/src/world/terrain.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/world/terrain.c 18.4 KB · 453 lines raw
#include "world/terrain.h"
#include "world/site.h"
#include "render/texture.h"

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

#define TERRAIN_GRID      256  /* height grid quads per side */
#define TERRAIN_WEAR_RES  512  /* wear map cells per side */
#define HALF              (TERRAIN_SIZE * 0.5f)
#define GRID_CELL         (TERRAIN_SIZE / (float)TERRAIN_GRID)
#define TILE_CELLS        (TERRAIN_GRID / TERRAIN_TILES)
#define WEAR_CELL         (TERRAIN_SIZE / (float)TERRAIN_WEAR_RES)

#define HILL_HEIGHT       1.0f   /* gentle rolling hills */
#define HILL_SCALE        22.0f
#define BUMP_HEIGHT       0.15f  /* small lumps */
#define BUMP_SCALE        4.0f
#define FLAT_MARGIN       1.5f   /* level ground around static objects */
#define FLAT_FALLOFF      5.0f
#define SKIRT_DEPTH       3.0f   /* hides the underside at the terrain edge */
#define OUTSIDE_HEIGHT    -1000.0f

#define TRAMPLE_RADIUS    0.6f
#define TRAMPLE_RATE      2.0f   /* about ten walks over the same spot make a path */
#define SECONDS_PER_DAY   86400.0f

static const Vec3 COLOR_GRASS     = { 0.47f, 0.62f, 0.28f };
static const Vec3 COLOR_DRY_GRASS = { 0.62f, 0.66f, 0.33f };
static const Vec3 COLOR_EARTH     = { 0.55f, 0.40f, 0.24f };

/* ---------- Noise ---------- */

static float hash2(int x, int z) {
    unsigned h = (unsigned)x * 374761393u + (unsigned)z * 668265263u;
    h = (h ^ (h >> 13)) * 1274126177u;
    h ^= h >> 16;
    return (float)(h & 0xffffffu) / 16777215.0f;
}

/* Smooth value noise in 0..1 */
static float valueNoise(float x, float z) {
    float fx = floorf(x), fz = floorf(z);
    int   ix = (int)fx, iz = (int)fz;
    float u = x - fx, v = z - fz;
    u = u * u * (3.0f - 2.0f * u);
    v = v * v * (3.0f - 2.0f * v);
    return lerpf(lerpf(hash2(ix, iz),     hash2(ix + 1, iz),     u),
                 lerpf(hash2(ix, iz + 1), hash2(ix + 1, iz + 1), u), v);
}

static float fbm(float x, float z) {
    float sum = 0.0f, amp = 0.5f;
    for (int i = 0; i < 3; i++) {
        sum += amp * valueNoise(x, z);
        x = x * 2.03f + 17.1f;
        z = z * 2.03f + 5.3f;
        amp *= 0.5f;
    }
    return sum / 0.875f;
}

/* ---------- Flat zones around static objects ---------- */

typedef struct {
    float min_x, max_x, min_z, max_z;
} Rect;

typedef struct {
    Rect *rects;
    int   count;
} FlatZones;

static FlatZones collectFlatZones(Object *const *objects, int count) {
    FlatZones z = { 0 };
    int total = 0;
    for (int i = 0; i < count; i++)
        if (objects[i]->is_static && objects[i]->type != OBJ_TREE && objects[i]->type != OBJ_BUSH) total += objects[i]->collider_count;
    if (total == 0 || !(z.rects = malloc((size_t)total * sizeof(Rect)))) return z;

    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (!o->is_static || o->type == OBJ_TREE || o->type == OBJ_BUSH) continue; /* natural ground stays as it is */
        for (int c = 0; c < o->collider_count; c++) {
            const Collider *col = &o->colliders[c];
            Vec3 center = Object_colliderCenter(o, c);
            float ex = 0.0f, ez = 0.0f;
            for (int k = 0; k < 3; k++) {
                float h = k == 0 ? col->half_extents.x : (k == 1 ? col->half_extents.y
                                                                 : col->half_extents.z);
                ex += fabsf(col->axes[k].x) * h;
                ez += fabsf(col->axes[k].z) * h;
            }
            z.rects[z.count++] = (Rect){ center.x - ex, center.x + ex, center.z - ez, center.z + ez };
        }
    }
    return z;
}

/* 0 on and near static objects, 1 in open meadow. */
static float openness(const FlatZones *zones, float x, float z) {
    float mask = 1.0f;
    for (int i = 0; i < zones->count; i++) {
        const Rect *r = &zones->rects[i];
        float dx = fmaxf(fmaxf(r->min_x - x, x - r->max_x), 0.0f);
        float dz = fmaxf(fmaxf(r->min_z - z, z - r->max_z), 0.0f);
        float d = sqrtf(dx * dx + dz * dz);
        mask = fminf(mask, smoothstepf(FLAT_MARGIN, FLAT_MARGIN + FLAT_FALLOFF, d));
    }
    return mask;
}

/* Rolling hills; `fine` (0..1) keeps their smallest ripples (paved roads leave them out). */
static float hills(float x, float z, float fine) {
    float sum = 0.0f, amp = 0.5f;
    for (int i = 0; i < 3; i++) {
        float n = valueNoise(x, z);
        if (i == 2) n = lerpf(0.5f, n, fine);
        sum += amp * n;
        x = x * 2.03f + 17.1f;
        z = z * 2.03f + 5.3f;
        amp *= 0.5f;
    }
    return sum / 0.875f;
}

static float rawHeight(float x, float z) {
    float smooth = Site_paved(x, z);
    return (1.0f - Site_flat(x, z)) * HILL_HEIGHT * (hills(x / HILL_SCALE, z / HILL_SCALE, 1.0f - smooth) - 0.5f) * 2.0f
         + BUMP_HEIGHT * (valueNoise(x / BUMP_SCALE + 100.0f, z / BUMP_SCALE) - 0.5f) * 2.0f
                       * (1.0f - Site_road(x, z)); /* roads are smooth; the yard keeps its lumps */
}

/* ---------- Mesh ---------- */

static int gridIndex(int ix, int iz) { return iz * (TERRAIN_GRID + 1) + ix; }

static float gridX(int i) { return -HALF + (float)i * GRID_CELL; }

static Vec3 gridNormal(const Terrain *t, int ix, int iz) {
    int x0 = ix > 0 ? ix - 1 : ix, x1 = ix < TERRAIN_GRID ? ix + 1 : ix;
    int z0 = iz > 0 ? iz - 1 : iz, z1 = iz < TERRAIN_GRID ? iz + 1 : iz;
    float dx = (t->heights[gridIndex(x1, iz)] - t->heights[gridIndex(x0, iz)]) / ((float)(x1 - x0) * GRID_CELL);
    float dz = (t->heights[gridIndex(ix, z1)] - t->heights[gridIndex(ix, z0)]) / ((float)(z1 - z0) * GRID_CELL);
    return vec3_normalize(vec3(-dx, 1.0f, -dz));
}

static Vec3 groundColor(float x, float z, float open) {
    Vec3 color = vec3_lerp(COLOR_GRASS, COLOR_DRY_GRASS, valueNoise(x / 9.0f, z / 9.0f));
    float bare = smoothstepf(0.62f, 0.72f, fbm(x / 12.0f + 50.0f, z / 12.0f)); /* natural earth spots */
    bare = fmaxf(bare, (1.0f - open) * 0.6f);                                    /* yards */
    return Site_groundColor(x, z, vec3_lerp(color, COLOR_EARTH, bare));
}

/* A skirt strip hanging down from a terrain edge (grid cells from..to). */
static void addSkirt(MeshBuilder *b, const Terrain *t, bool along_x, int fixed, Vec3 normal, int from, int to) {
    for (int i = from; i < to; i++) {
        GLuint idx[4];
        for (int k = 0; k < 2; k++) {
            int ix = along_x ? i + k : fixed, iz = along_x ? fixed : i + k;
            Vec3 top = vec3(gridX(ix), t->heights[gridIndex(ix, iz)], gridX(iz));
            idx[k]     = MeshBuilder_vertex(b, top, normal, COLOR_EARTH);
            idx[3 - k] = MeshBuilder_vertex(b, vec3(top.x, top.y - SKIRT_DEPTH, top.z), normal, COLOR_EARTH);
        }
        MeshBuilder_quad(b, idx[0], idx[1], idx[2], idx[3]);
    }
}

/* One tile of the ground (TILE_CELLS square): tiles out of view aren't drawn. */
static Mesh buildTile(const Terrain *t, const FlatZones *zones, int tx, int tz) {
    MeshBuilder b;
    MeshBuilder_init(&b);
    const int x0 = tx * TILE_CELLS, z0 = tz * TILE_CELLS, n = TILE_CELLS + 1;

    for (int iz = z0; iz <= z0 + TILE_CELLS; iz++) {
        for (int ix = x0; ix <= x0 + TILE_CELLS; ix++) {
            float x = gridX(ix), z = gridX(iz);
            Vec3 p = vec3(x, t->heights[gridIndex(ix, iz)], z);
            MeshBuilder_vertex(&b, p, gridNormal(t, ix, iz), groundColor(x, z, openness(zones, x, z)));
        }
    }
    for (int iz = 0; iz < TILE_CELLS; iz++) {
        for (int ix = 0; ix < TILE_CELLS; ix++) {
            GLuint a = (GLuint)(iz * n + ix), c = (GLuint)((iz + 1) * n + ix);
            MeshBuilder_quad(&b, a, a + 1, c + 1, c);
        }
    }

    /* the edge of the world: a skirt hanging down */
    if (tz == 0)                 addSkirt(&b, t, true,  0,            vec3(0, 0, -1), x0, x0 + TILE_CELLS);
    if (tz == TERRAIN_TILES - 1) addSkirt(&b, t, true,  TERRAIN_GRID, vec3(0, 0,  1), x0, x0 + TILE_CELLS);
    if (tx == 0)                 addSkirt(&b, t, false, 0,            vec3(-1, 0, 0), z0, z0 + TILE_CELLS);
    if (tx == TERRAIN_TILES - 1) addSkirt(&b, t, false, TERRAIN_GRID, vec3( 1, 0, 0), z0, z0 + TILE_CELLS);

    return Mesh_fromBuilder(&b);
}

/* ---------- Surfaces ---------- */

#define SURFACE_CELL (TERRAIN_SIZE / (float)TERRAIN_SURFACE_RES)

static unsigned char byte01(float v) { return (unsigned char)(clampf(v, 0.0f, 1.0f) * 255.0f + 0.5f); }

/* One cell of the surface map, and how hard it is underfoot. */
static void surfaceCell(Terrain *t, unsigned char *rgba, int ix, int iz) {
    float x = -HALF + ((float)ix + 0.5f) * SURFACE_CELL, z = -HALF + ((float)iz + 0.5f) * SURFACE_CELL;
    SiteSurface s = Site_surface(x, z);
    size_t i = (size_t)iz * TERRAIN_SURFACE_RES + (size_t)ix;
    unsigned char *p = &rgba[i * 4];
    p[0] = byte01(s.asphalt);
    p[1] = byte01(s.concrete);
    p[2] = byte01(s.gravel);
    p[3] = byte01(s.earth);
    t->hard[i] = byte01(s.asphalt + s.concrete + 0.7f * s.gravel);
}

/* The surface map (roads, gravel, tracks) for the shader, and how hard it is for the feet. */
static bool buildSurface(Terrain *t) {
    enum { BLOCK = 16 }; /* most of it is meadow: whole blocks of it are skipped */
    const size_t n = (size_t)TERRAIN_SURFACE_RES * TERRAIN_SURFACE_RES;
    unsigned char *rgba = calloc(n * 4, 1);
    t->hard = calloc(n, 1);
    if (!rgba || !t->hard) {
        free(rgba);
        return false;
    }
    for (int bz = 0; bz < TERRAIN_SURFACE_RES; bz += BLOCK) {
        for (int bx = 0; bx < TERRAIN_SURFACE_RES; bx += BLOCK) {
            float cx = -HALF + ((float)bx + 0.5f * BLOCK) * SURFACE_CELL, cz = -HALF + ((float)bz + 0.5f * BLOCK) * SURFACE_CELL;
            if (!Site_surfaceNear(cx, cz, 0.75f * BLOCK * SURFACE_CELL)) continue;
            for (int iz = bz; iz < bz + BLOCK; iz++)
                for (int ix = bx; ix < bx + BLOCK; ix++) surfaceCell(t, rgba, ix, iz);
        }
    }
    t->surface_texture = Texture_createRGBA8(TERRAIN_SURFACE_RES, TERRAIN_SURFACE_RES);
    Texture_updateRGBA8(t->surface_texture, 0, 0, TERRAIN_SURFACE_RES, TERRAIN_SURFACE_RES, rgba);
    free(rgba);
    return t->surface_texture != 0;
}

float Terrain_hardness(const Terrain *t, float x, float z) {
    if (!t->hard) return 0.0f;
    int ix = (int)((x + HALF) / SURFACE_CELL), iz = (int)((z + HALF) / SURFACE_CELL);
    if (ix < 0 || iz < 0 || ix >= TERRAIN_SURFACE_RES || iz >= TERRAIN_SURFACE_RES) return 0.0f;
    return (float)t->hard[(size_t)iz * TERRAIN_SURFACE_RES + (size_t)ix] / 255.0f;
}

/* ---------- Public ---------- */

bool Terrain_addPit(Terrain *t, Vec3 center, float yaw, float half_x, float half_z, float floor_y) {
    if (t->pit_count >= TERRAIN_PITS) return false;
    t->pits[t->pit_count++] = (TerrainPit){ center.x, center.z, cosf(yaw), sinf(yaw), half_x, half_z, floor_y };
    return true;
}

bool Terrain_inPit(const Terrain *t, float x, float z, float *floor_y) {
    for (int i = 0; i < t->pit_count; i++) {
        const TerrainPit *p = &t->pits[i];
        float dx = x - p->x, dz = z - p->z;
        float u = dx * p->cos_yaw + dz * p->sin_yaw, v = -dx * p->sin_yaw + dz * p->cos_yaw;
        if (fabsf(u) <= p->half_x && fabsf(v) <= p->half_z) {
            if (floor_y) *floor_y = p->floor;
            return true;
        }
    }
    return false;
}

bool Terrain_init(Terrain *t, Object *const *objects, int count) {
    TerrainPit pits[TERRAIN_PITS]; /* (dug before: kept) */
    const int pit_count = t->pit_count;
    memcpy(pits, t->pits, sizeof(pits));
    memset(t, 0, sizeof(*t));
    memcpy(t->pits, pits, sizeof(pits));
    t->pit_count = pit_count;
    const size_t grid_n = (size_t)(TERRAIN_GRID + 1) * (TERRAIN_GRID + 1);
    const size_t wear_n = (size_t)TERRAIN_WEAR_RES * TERRAIN_WEAR_RES;

    t->heights    = malloc(grid_n * sizeof(float));
    t->wear       = calloc(wear_n, sizeof(float));
    t->wear_bytes = calloc(wear_n, 1);
    FlatZones zones = collectFlatZones(objects, count);
    if (!t->heights || !t->wear || !t->wear_bytes) {
        free(zones.rects);
        Terrain_destroy(t);
        return false;
    }

    for (int iz = 0; iz <= TERRAIN_GRID; iz++) {
        for (int ix = 0; ix <= TERRAIN_GRID; ix++) {
            float x = gridX(ix), z = gridX(iz);
            t->heights[gridIndex(ix, iz)] = rawHeight(x, z) * openness(&zones, x, z) + Site_surface(x, z).lift + Site_rise(x, z);
        }
    }
    if (!buildSurface(t)) {
        free(zones.rects);
        Terrain_destroy(t);
        return false;
    }

    bool ok = true;
    for (int tz = 0; tz < TERRAIN_TILES; tz++)
        for (int tx = 0; tx < TERRAIN_TILES; tx++) {
            Mesh *m = &t->tiles[tz * TERRAIN_TILES + tx];
            *m = buildTile(t, &zones, tx, tz);
            ok = ok && m->vao;
        }
    free(zones.rects);
    if (!ok) {
        Terrain_destroy(t);
        return false;
    }

    t->wear_texture = Texture_createR8(TERRAIN_WEAR_RES, TERRAIN_WEAR_RES);
    if (!Wetness_init(&t->wet)) {
        Terrain_destroy(t);
        return false;
    }
    t->wear_dirty = true;
    Terrain_syncTexture(t);
    return true;
}

void Terrain_destroy(Terrain *t) {
    for (int i = 0; i < TERRAIN_TILES * TERRAIN_TILES; i++) Mesh_destroy(&t->tiles[i]);
    Texture_destroy(&t->wear_texture);
    Texture_destroy(&t->surface_texture);
    Wetness_destroy(&t->wet);
    free(t->hard);
    free(t->heights);
    free(t->wear);
    free(t->wear_bytes);
    memset(t, 0, sizeof(*t));
}

bool Terrain_writeWear(const Terrain *t, FILE *f) {
    unsigned res = TERRAIN_WEAR_RES;
    return fwrite(&res, 4, 1, f) == 1 && fwrite(t->wear, sizeof(float), (size_t)res * res, f) == (size_t)res * res;
}

bool Terrain_readWear(Terrain *t, FILE *f) {
    unsigned res;
    if (fread(&res, 4, 1, f) != 1 || res != TERRAIN_WEAR_RES) return false;
    if (fread(t->wear, sizeof(float), (size_t)res * res, f) != (size_t)res * res) return false;
    t->wear_dirty = true;
    return true;
}

/* Bilinear sample of the wear map, matching GPU linear filtering. */
static float wearAt(const Terrain *t, float x, float z) {
    float gx = clampf((x + HALF) / WEAR_CELL - 0.5f, 0.0f, (float)(TERRAIN_WEAR_RES - 1));
    float gz = clampf((z + HALF) / WEAR_CELL - 0.5f, 0.0f, (float)(TERRAIN_WEAR_RES - 1));
    int ix = (int)gx, iz = (int)gz;
    int ix1 = ix < TERRAIN_WEAR_RES - 1 ? ix + 1 : ix;
    int iz1 = iz < TERRAIN_WEAR_RES - 1 ? iz + 1 : iz;
    float u = gx - (float)ix, v = gz - (float)iz;
    const float *w = t->wear;
    return lerpf(lerpf(w[iz * TERRAIN_WEAR_RES + ix],  w[iz * TERRAIN_WEAR_RES + ix1],  u),
                 lerpf(w[iz1 * TERRAIN_WEAR_RES + ix], w[iz1 * TERRAIN_WEAR_RES + ix1], u), v);
}

float Terrain_height(const Terrain *t, float x, float z) {
    float pit;
    if (t->pit_count && x >= -HALF && x <= HALF && z >= -HALF && z <= HALF && Terrain_inPit(t, x, z, &pit)) return pit;
    return Terrain_naturalHeight(t, x, z);
}

float Terrain_naturalHeight(const Terrain *t, float x, float z) {
    if (!(x >= -HALF && x <= HALF && z >= -HALF && z <= HALF)) return OUTSIDE_HEIGHT; /* (written so a NaN is outside too: never an index from it) */
    float gx = fminf((x + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
    float gz = fminf((z + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f);
    int ix = (int)gx, iz = (int)gz;
    float u = gx - (float)ix, v = gz - (float)iz;

    /* Same diagonal split as the mesh quads: (a, a+1, c+1) and (c+1, c, a) */
    float h00 = t->heights[gridIndex(ix, iz)],     h10 = t->heights[gridIndex(ix + 1, iz)];
    float h01 = t->heights[gridIndex(ix, iz + 1)], h11 = t->heights[gridIndex(ix + 1, iz + 1)];
    float h = u >= v ? h00 + (h10 - h00) * u + (h11 - h10) * v
                     : h00 + (h11 - h01) * u + (h01 - h00) * v;

    return h - wearAt(t, x, z) * TERRAIN_GRASS_DEPTH;
}

float Terrain_baseHeight(const Terrain *t, float x, float z) {
    return Terrain_height(t, x, z) + wearAt(t, x, z) * TERRAIN_GRASS_DEPTH;
}

float Terrain_wear(const Terrain *t, float x, float z) {
    return wearAt(t, x, z);
}

Vec3 Terrain_grassColor(float x, float z) {
    return groundColor(x, z, 1.0f);
}

void Terrain_trample(Terrain *t, float x, float z, float dt) {
    dt *= (1.0f + 3.0f * Wetness_softness(&t->wet, x, z)) * (1.0f - Terrain_hardness(t, x, z)); /* wet soft ground gives way, tar doesn't */
    if (dt <= 0.0f) return;
    int x0 = (int)floorf((x - TRAMPLE_RADIUS + HALF) / WEAR_CELL);
    int x1 = (int)floorf((x + TRAMPLE_RADIUS + HALF) / WEAR_CELL);
    int z0 = (int)floorf((z - TRAMPLE_RADIUS + HALF) / WEAR_CELL);
    int z1 = (int)floorf((z + TRAMPLE_RADIUS + HALF) / WEAR_CELL);

    for (int iz = z0 > 0 ? z0 : 0; iz <= z1 && iz < TERRAIN_WEAR_RES; iz++) {
        for (int ix = x0 > 0 ? x0 : 0; ix <= x1 && ix < TERRAIN_WEAR_RES; ix++) {
            float cx = -HALF + ((float)ix + 0.5f) * WEAR_CELL;
            float cz = -HALF + ((float)iz + 0.5f) * WEAR_CELL;
            float d = sqrtf((cx - x) * (cx - x) + (cz - z) * (cz - z));
            if (d >= TRAMPLE_RADIUS) continue;

            float *w = &t->wear[iz * TERRAIN_WEAR_RES + ix];
            *w += TRAMPLE_RATE * dt * (1.0f - d / TRAMPLE_RADIUS) * (1.0f - *w);
            t->wear_dirty = true;
        }
    }
}

void Terrain_soak(Terrain *t, float x, float z, float radius) {
    int x0 = (int)floorf((x - radius + HALF) / WEAR_CELL), x1 = (int)floorf((x + radius + HALF) / WEAR_CELL);
    int z0 = (int)floorf((z - radius + HALF) / WEAR_CELL), z1 = (int)floorf((z + radius + HALF) / WEAR_CELL);
    for (int iz = z0 > 0 ? z0 : 0; iz <= z1 && iz < TERRAIN_WEAR_RES; iz++) {
        for (int ix = x0 > 0 ? x0 : 0; ix <= x1 && ix < TERRAIN_WEAR_RES; ix++) {
            float cx = -HALF + ((float)ix + 0.5f) * WEAR_CELL, cz = -HALF + ((float)iz + 0.5f) * WEAR_CELL;
            float d = sqrtf((cx - x) * (cx - x) + (cz - z) * (cz - z));
            if (d >= radius) continue;
            float *w = &t->wear[iz * TERRAIN_WEAR_RES + ix];
            *w = fmaxf(*w, 0.55f * (1.0f - 0.5f * d / radius) * (1.0f - Terrain_hardness(t, cx, cz)));
            t->wear_dirty = true;
        }
    }
}

void Terrain_recover(Terrain *t, float game_dt) {
    const float amount = game_dt / (TERRAIN_RECOVERY_DAYS * SECONDS_PER_DAY);
    const int n = TERRAIN_WEAR_RES * TERRAIN_WEAR_RES;
    for (int i = 0; i < n; i++) {
        if (t->wear[i] > 0.0f) {
            t->wear[i] = fmaxf(0.0f, t->wear[i] - amount);
            t->wear_dirty = true;
        }
    }
}

void Terrain_syncTexture(Terrain *t) {
    Wetness_sync(&t->wet);
    if (!t->wear_dirty) return;
    const int n = TERRAIN_WEAR_RES * TERRAIN_WEAR_RES;
    for (int i = 0; i < n; i++)
        t->wear_bytes[i] = (unsigned char)(clampf(t->wear[i], 0.0f, 1.0f) * 255.0f + 0.5f);
    Texture_updateR8(t->wear_texture, TERRAIN_WEAR_RES, TERRAIN_WEAR_RES, t->wear_bytes);
    t->wear_dirty = false;
}