Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/world/terrain.c 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;
}