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/wetness.c raw
#include "world/wetness.h"
#include "world/terrain.h"
#include "world/site.h"
#include "render/texture.h"
#include <stdlib.h>
#include <string.h>
#define WET_RES 2048 /* cells per side of the whole ground */
#define WET_CHUNK 32 /* cells per side of a chunk */
#define WET_CHUNKS (WET_RES / WET_CHUNK)
#define CHUNK_CELLS (WET_CHUNK * WET_CHUNK)
#define HALF (TERRAIN_SIZE * 0.5f)
#define CELL (TERRAIN_SIZE / (float)WET_RES) /* meters */
#define CELL_AREA (CELL * CELL) /* m^2: 1 liter on it is 1 / CELL_AREA mm */
#define SOIL_BLOCK 4 /* soil and grass looked up per 4 x 4 cells */
#define FLOW_STEP (1.0f / 30.0f) /* real seconds per step of running water */
#define FLOW_MAX 0.45f /* most of a cell's water that may leave in one step */
#define SLOW_STEP 0.25f /* real seconds between soaking / drying steps */
#define EVAPORATE (1.5f / 3600.0f) /* mm of standing water per game second (paved: twice) */
#define DRAIN_TIME 1200.0f /* game seconds: soaked soil drains down to field capacity */
#define FIELD_CAP 0.7f /* ... which is this much of what it can hold */
#define PEE_FADE (2.0f * 3600.0f) /* game seconds: a pee stain / smell fades */
#define MUD_FORMS 60.0f /* game seconds for soaked bare ground to turn to mud */
#define MUD_DRIES (4.0f * 3600.0f) /* ... and to dry up again */
#define WATER_SHOWN 25.0f /* mm of standing water at full texture value */
#define PEE_SHOWN 2.0f /* mm of pee at a full stain */
/* holds: mm of water the top soil takes; soak: mm per game second into dry soil;
dry: game hours to dry out; mud: how readily bare ground turns to mud;
speed: how fast water runs over it (m/s at a slope of 1); keep: mm that stays put. */
static const struct { float holds, soak, dry, mud, speed, keep; } SOIL[SOIL_COUNT] = {
[SOIL_MEADOW] = { 14.0f, 0.10f, 5.0f, 0.8f, 0.8f, 2.0f },
[SOIL_YARD] = { 8.0f, 0.03f, 4.0f, 1.0f, 1.6f, 1.5f },
[SOIL_TRACK] = { 8.0f, 0.02f, 4.0f, 1.0f, 1.6f, 1.5f },
[SOIL_FIELD] = { 20.0f, 0.15f, 6.0f, 1.0f, 1.2f, 2.0f },
[SOIL_FOREST] = { 22.0f, 0.25f, 8.0f, 0.3f, 0.6f, 2.5f },
[SOIL_GRAVEL] = { 4.0f, 1.50f, 1.5f, 0.0f, 1.2f, 0.5f },
[SOIL_PAVED] = { 0.0f, 0.00f, 1.0f, 0.0f, 3.0f, 0.8f },
};
typedef struct WetChunk {
int cx, cz;
bool running; /* standing water above what stays put: runs every flow step */
bool dirty; /* texture out of date */
bool dead; /* all dry: freed once the texture is cleared */
float ground[CHUNK_CELLS]; /* mm, ground height incl. trampled paths */
float water[CHUNK_CELLS]; /* mm standing on top */
float soil[CHUNK_CELLS]; /* mm held in the soil */
float pee[CHUNK_CELLS]; /* mm of pee in it all */
float mud[CHUNK_CELLS]; /* 0..1 */
float grass[CHUNK_CELLS]; /* how much grass grows here (0..1) */
unsigned char kind[CHUNK_CELLS];
float dw[CHUNK_CELLS], dp[CHUNK_CELLS]; /* flow in / out this step */
unsigned char texels[CHUNK_CELLS * 4];
} WetChunk;
/* ---------- Cells and chunks ---------- */
static float cellCenter(int g) { return -HALF + ((float)g + 0.5f) * CELL; }
static WetChunk *chunkAt(const Wetness *w, int cx, int cz) {
if (cx < 0 || cz < 0 || cx >= WET_CHUNKS || cz >= WET_CHUNKS) return NULL;
return w->chunks[cz * WET_CHUNKS + cx];
}
static WetChunk *newChunk(Terrain *t, int cx, int cz) {
Wetness *w = &t->wet;
WetChunk *c = calloc(1, sizeof(WetChunk));
if (!c) return NULL;
c->cx = cx;
c->cz = cz;
for (int bz = 0; bz < WET_CHUNK; bz += SOIL_BLOCK)
for (int bx = 0; bx < WET_CHUNK; bx += SOIL_BLOCK) {
float x = cellCenter(cx * WET_CHUNK + bx) + CELL * (SOIL_BLOCK - 1) * 0.5f;
float z = cellCenter(cz * WET_CHUNK + bz) + CELL * (SOIL_BLOCK - 1) * 0.5f;
Soil kind = Site_soil(x, z);
float grass = Site_grass(x, z);
for (int k = 0; k < SOIL_BLOCK * SOIL_BLOCK; k++) {
int i = (bz + k / SOIL_BLOCK) * WET_CHUNK + bx + k % SOIL_BLOCK;
c->kind[i] = (unsigned char)kind;
c->grass[i] = grass;
}
}
for (int i = 0; i < CHUNK_CELLS; i++)
c->ground[i] = Terrain_height(t, cellCenter(cx * WET_CHUNK + i % WET_CHUNK), cellCenter(cz * WET_CHUNK + i / WET_CHUNK)) * 1000.0f;
w->chunks[cz * WET_CHUNKS + cx] = c;
w->live[w->live_count++] = cz * WET_CHUNKS + cx;
return c;
}
#define WET_SAVE_TAG 0x57455431u /* "WET1" */
bool Wetness_write(const Wetness *w, FILE *f) {
unsigned tag = WET_SAVE_TAG, n = 0, res = WET_RES, chunk = WET_CHUNK;
for (int i = 0; i < w->live_count; i++) { const WetChunk *c = w->chunks[w->live[i]]; if (c && !c->dead) n++; }
if (fwrite(&tag, 4, 1, f) != 1 || fwrite(&res, 4, 1, f) != 1 || fwrite(&chunk, 4, 1, f) != 1 || fwrite(&n, 4, 1, f) != 1) return false;
for (int i = 0; i < w->live_count; i++) {
const WetChunk *c = w->chunks[w->live[i]];
if (!c || c->dead) continue;
int head[2] = { c->cx, c->cz };
if (fwrite(head, sizeof head, 1, f) != 1 || fwrite(c->water, sizeof c->water, 1, f) != 1 || fwrite(c->soil, sizeof c->soil, 1, f) != 1 ||
fwrite(c->pee, sizeof c->pee, 1, f) != 1 || fwrite(c->mud, sizeof c->mud, 1, f) != 1) return false;
}
return true;
}
bool Wetness_read(Terrain *t, FILE *f) {
unsigned tag, n, res, chunk;
if (fread(&tag, 4, 1, f) != 1 || tag != WET_SAVE_TAG || fread(&res, 4, 1, f) != 1 || fread(&chunk, 4, 1, f) != 1 || fread(&n, 4, 1, f) != 1) return false;
if (res != WET_RES || chunk != WET_CHUNK) return false; /* (another grid: the ground starts dry) */
for (unsigned k = 0; k < n; k++) {
int head[2];
WetChunk tmp;
if (fread(head, sizeof head, 1, f) != 1 || fread(tmp.water, sizeof tmp.water, 1, f) != 1 || fread(tmp.soil, sizeof tmp.soil, 1, f) != 1 ||
fread(tmp.pee, sizeof tmp.pee, 1, f) != 1 || fread(tmp.mud, sizeof tmp.mud, 1, f) != 1) return false;
if (head[0] < 0 || head[1] < 0 || head[0] >= WET_CHUNKS || head[1] >= WET_CHUNKS) continue;
WetChunk *c = chunkAt(&t->wet, head[0], head[1]);
if (!c) c = newChunk(t, head[0], head[1]);
if (!c) return false;
memcpy(c->water, tmp.water, sizeof c->water);
memcpy(c->soil, tmp.soil, sizeof c->soil);
memcpy(c->pee, tmp.pee, sizeof c->pee);
memcpy(c->mud, tmp.mud, sizeof c->mud);
c->running = true;
c->dirty = true;
c->dead = false;
}
return true;
}
/* The chunk and index of cell (gx, gz), made if missing and make is set. */
static WetChunk *cellAt(Terrain *t, int gx, int gz, bool make, int *index) {
if (gx < 0 || gz < 0 || gx >= WET_RES || gz >= WET_RES) return NULL;
WetChunk *c = chunkAt(&t->wet, gx / WET_CHUNK, gz / WET_CHUNK);
if (!c && make) c = newChunk(t, gx / WET_CHUNK, gz / WET_CHUNK);
if (c) *index = (gz % WET_CHUNK) * WET_CHUNK + gx % WET_CHUNK;
return c;
}
static const WetChunk *cellAtConst(const Wetness *w, float x, float z, int *index) {
int gx = (int)floorf((x + HALF) / CELL), gz = (int)floorf((z + HALF) / CELL);
if (gx < 0 || gz < 0 || gx >= WET_RES || gz >= WET_RES) return NULL;
const WetChunk *c = chunkAt(w, gx / WET_CHUNK, gz / WET_CHUNK);
if (c) *index = (gz % WET_CHUNK) * WET_CHUNK + gx % WET_CHUNK;
return c;
}
static float saturation(const WetChunk *c, int i) {
float holds = SOIL[c->kind[i]].holds;
return holds > 0.0f ? clampf(c->soil[i] / holds, 0.0f, 1.0f) : 0.0f;
}
/* ---------- Public: setup ---------- */
bool Wetness_init(Wetness *w) {
memset(w, 0, sizeof(*w));
w->chunks = calloc((size_t)WET_CHUNKS * WET_CHUNKS, sizeof(WetChunk *));
w->live = calloc((size_t)WET_CHUNKS * WET_CHUNKS, sizeof(int));
w->texture = Texture_createRGBA8(WET_RES, WET_RES);
if (!w->chunks || !w->live || !w->texture) {
Wetness_destroy(w);
return false;
}
return true;
}
void Wetness_destroy(Wetness *w) {
if (w->chunks)
for (int i = 0; i < WET_CHUNKS * WET_CHUNKS; i++) free(w->chunks[i]);
free(w->chunks);
free(w->live);
Texture_destroy(&w->texture);
memset(w, 0, sizeof(*w));
}
/* ---------- Public: water coming down ---------- */
void Wetness_pour(Terrain *t, float x, float z, float liters, float radius, float pee) {
if (liters <= 0.0f) return;
radius = fmaxf(radius, CELL * 0.5f);
int g0x = (int)floorf((x - radius + HALF) / CELL), g1x = (int)floorf((x + radius + HALF) / CELL);
int g0z = (int)floorf((z - radius + HALF) / CELL), g1z = (int)floorf((z + radius + HALF) / CELL);
/* weights fall off from the middle; at least the cell it hits */
float total = 0.0f;
for (int pass = 0; pass < 2; pass++) {
for (int gz = g0z; gz <= g1z; gz++)
for (int gx = g0x; gx <= g1x; gx++) {
float dx = cellCenter(gx) - x, dz = cellCenter(gz) - z;
float weight = fmaxf(0.0f, 1.0f - sqrtf(dx * dx + dz * dz) / radius);
if (g0x == g1x && g0z == g1z) weight = 1.0f;
if (weight <= 0.0f) continue;
if (pass == 0) { total += weight; continue; }
int i;
WetChunk *c = cellAt(t, gx, gz, true, &i);
if (!c) continue;
float mm = liters / CELL_AREA * weight / total;
c->water[i] += mm;
c->pee[i] += mm * pee;
c->running = c->dirty = true;
c->dead = false; /* wet again before it was cleared */
}
if (total <= 0.0f) { /* smaller than a cell and between centers: the cell it hits */
int i;
WetChunk *c = cellAt(t, (int)floorf((x + HALF) / CELL), (int)floorf((z + HALF) / CELL), true, &i);
if (!c) return;
c->water[i] += liters / CELL_AREA;
c->pee[i] += liters / CELL_AREA * pee;
c->running = c->dirty = true;
c->dead = false;
return;
}
}
}
/* ---------- Running water ---------- */
static const int NEIGHBOR[4][2] = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
/* Standing water runs to lower neighbors (ground + water level), the faster
the steeper and the deeper; pee goes along with it. */
static void flowChunk(Terrain *t, WetChunk *c) {
bool running = false;
for (int i = 0; i < CHUNK_CELLS; i++) {
const float keep = SOIL[c->kind[i]].keep;
float free_mm = c->water[i] - keep;
if (free_mm <= 0.0f) continue;
running = true;
float level = c->ground[i] + c->water[i];
int gx = c->cx * WET_CHUNK + i % WET_CHUNK, gz = c->cz * WET_CHUNK + i / WET_CHUNK;
float depth = clampf(cbrtf((free_mm / 4.0f) * (free_mm / 4.0f)), 0.15f, 2.0f); /* shallow water creeps */
WetChunk *to[4];
int at[4];
float share[4], drop[4], sum = 0.0f;
for (int k = 0; k < 4; k++) {
to[k] = NULL;
share[k] = 0.0f;
int nx = gx + NEIGHBOR[k][0], nz = gz + NEIGHBOR[k][1], j;
WetChunk *n = cellAt(t, nx, nz, false, &j);
float other;
if (n) other = n->ground[j] + n->water[j];
else if (nx < 0 || nz < 0 || nx >= WET_RES || nz >= WET_RES) continue;
else other = Terrain_height(t, cellCenter(nx), cellCenter(nz)) * 1000.0f;
drop[k] = level - other;
if (drop[k] <= 0.0f) continue;
float slope = drop[k] / (CELL * 1000.0f);
share[k] = SOIL[c->kind[i]].speed * sqrtf(slope) * depth * FLOW_STEP / CELL;
if (!n) n = cellAt(t, nx, nz, true, &j);
if (!n) { share[k] = 0.0f; continue; }
to[k] = n;
at[k] = j;
sum += share[k];
}
if (sum <= 0.0f) continue;
float scale = sum > FLOW_MAX ? FLOW_MAX / sum : 1.0f;
float body = c->water[i] + c->soil[i] + 1e-3f;
for (int k = 0; k < 4; k++) {
if (!to[k]) continue;
float moved = fminf(free_mm * share[k] * scale, drop[k] * 0.3f);
float pee = c->pee[i] * moved / body;
c->dw[i] -= moved;
c->dp[i] -= pee;
to[k]->dw[at[k]] += moved;
to[k]->dp[at[k]] += pee;
to[k]->running = true;
to[k]->dead = false;
}
}
c->running = running;
}
static void flowStep(Terrain *t) {
Wetness *w = &t->wet;
for (int n = 0; n < w->live_count; n++) {
WetChunk *c = w->chunks[w->live[n]];
memset(c->dw, 0, sizeof(c->dw));
memset(c->dp, 0, sizeof(c->dp));
}
int count = w->live_count; /* chunks made while flowing start with nothing to give */
for (int n = 0; n < count; n++) {
WetChunk *c = w->chunks[w->live[n]];
if (c->running) flowChunk(t, c);
}
for (int n = 0; n < w->live_count; n++) {
WetChunk *c = w->chunks[w->live[n]];
bool changed = false;
for (int i = 0; i < CHUNK_CELLS; i++) {
if (c->dw[i] == 0.0f && c->dp[i] == 0.0f) continue;
c->water[i] = fmaxf(0.0f, c->water[i] + c->dw[i]);
c->pee[i] = fmaxf(0.0f, c->pee[i] + c->dp[i]);
changed = true;
}
if (changed) c->dirty = true;
}
}
/* ---------- Soaking in, drying, mud ---------- */
static void slowChunk(Terrain *t, WetChunk *c, float gdt) {
float most = 0.0f;
for (int i = 0; i < CHUNK_CELLS; i++) {
const int kind = c->kind[i];
const float holds = SOIL[kind].holds;
float x = cellCenter(c->cx * WET_CHUNK + i % WET_CHUNK), z = cellCenter(c->cz * WET_CHUNK + i / WET_CHUNK);
c->ground[i] = Terrain_height(t, x, z) * 1000.0f; /* paths get trodden in */
/* soaking in: dry soil drinks fastest */
float s = saturation(c, i);
float soak = fminf(c->water[i], fminf(SOIL[kind].soak * (0.3f + 2.0f * (1.0f - s)) * gdt, holds - c->soil[i]));
if (soak > 0.0f) {
c->water[i] -= soak;
c->soil[i] += soak;
}
c->water[i] = fmaxf(0.0f, c->water[i] - EVAPORATE * (kind == SOIL_PAVED ? 2.0f : 1.0f) * gdt);
/* draining down (fast while soaked) and drying out (slow) */
float drained = 0.0f;
if (holds > 0.0f) {
float field = FIELD_CAP * holds;
drained = c->soil[i] > field ? (c->soil[i] - field) * fminf(1.0f, gdt / DRAIN_TIME)
: fminf(c->soil[i], holds * (0.3f + 0.7f * s) * gdt / (SOIL[kind].dry * 3600.0f));
c->soil[i] -= drained;
}
/* pee: washed down with the water, and fading */
float body = c->water[i] + c->soil[i] + drained + 1e-3f;
c->pee[i] -= c->pee[i] * fminf(1.0f, drained / body + gdt / PEE_FADE);
if (c->water[i] + c->soil[i] < 0.05f) c->pee[i] *= 1.0f - fminf(1.0f, 3.0f * gdt / PEE_FADE);
/* mud: soaked bare soft ground */
s = saturation(c, i);
float bare = 1.0f - c->grass[i] * (1.0f - Terrain_wear(t, x, z));
float target = SOIL[kind].mud * bare * smoothstepf(0.55f, 0.95f, s);
if (target > c->mud[i]) c->mud[i] += (target - c->mud[i]) * fminf(1.0f, gdt / MUD_FORMS);
else c->mud[i] -= (c->mud[i] - target) * fminf(1.0f, gdt / MUD_DRIES);
/* what can't be seen any more is gone */
if (c->water[i] < 0.01f) c->water[i] = 0.0f;
if (c->water[i] == 0.0f && c->soil[i] < 0.02f) c->soil[i] = 0.0f;
if (c->water[i] + c->soil[i] == 0.0f && c->pee[i] < 0.02f) c->pee[i] = 0.0f;
if (c->mud[i] < 0.1f && s < 0.3f) c->mud[i] = 0.0f;
if (c->water[i] > SOIL[kind].keep) c->running = true;
most = fmaxf(most, c->water[i] + c->soil[i] + c->pee[i] + c->mud[i]);
}
c->dirty = true;
if (most <= 0.0f) c->dead = true;
}
void Wetness_update(Terrain *t, float dt, float game_dt) {
Wetness *w = &t->wet;
if (w->live_count == 0) {
w->flow_time = w->slow_time = w->slow_game = 0.0f;
return;
}
w->flow_time = fminf(w->flow_time + dt, FLOW_STEP * 4.0f); /* falls behind rather than stalls */
while (w->flow_time >= FLOW_STEP) {
w->flow_time -= FLOW_STEP;
flowStep(t);
}
w->slow_time += dt;
w->slow_game += game_dt;
if (w->slow_time >= SLOW_STEP) {
for (int n = 0; n < w->live_count; n++) slowChunk(t, w->chunks[w->live[n]], w->slow_game);
w->slow_time = w->slow_game = 0.0f;
}
}
/* ---------- Texture ---------- */
static unsigned char byte(float v) { return (unsigned char)(clampf(v, 0.0f, 1.0f) * 255.0f + 0.5f); }
void Wetness_sync(Wetness *w) {
for (int n = 0; n < w->live_count; n++) {
WetChunk *c = w->chunks[w->live[n]];
if (!c->dirty) continue;
for (int i = 0; i < CHUNK_CELLS; i++) {
unsigned char *p = &c->texels[i * 4];
if (c->dead) { p[0] = p[1] = p[2] = p[3] = 0; continue; }
p[0] = byte(c->kind[i] == SOIL_PAVED ? fminf(c->water[i] * 0.5f, 1.0f) : saturation(c, i));
p[1] = byte(sqrtf(c->water[i] / WATER_SHOWN));
p[2] = byte(c->pee[i] / PEE_SHOWN);
p[3] = byte(c->mud[i]);
}
Texture_updateRGBA8(w->texture, c->cx * WET_CHUNK, c->cz * WET_CHUNK, WET_CHUNK, WET_CHUNK, c->texels);
c->dirty = false;
if (c->dead) { /* cleared on the GPU: gone */
w->chunks[w->live[n]] = NULL;
free(c);
w->live[n--] = w->live[--w->live_count];
}
}
}
/* ---------- Queries ---------- */
float Wetness_water(const Wetness *w, float x, float z) {
int i;
const WetChunk *c = cellAtConst(w, x, z, &i);
return c ? c->water[i] : 0.0f;
}
float Wetness_mud(const Wetness *w, float x, float z) {
int i;
const WetChunk *c = cellAtConst(w, x, z, &i);
return c ? c->mud[i] : 0.0f;
}
float Wetness_softness(const Wetness *w, float x, float z) {
int i;
const WetChunk *c = cellAtConst(w, x, z, &i);
return c ? SOIL[c->kind[i]].mud * smoothstepf(0.4f, 0.9f, saturation(c, i)) : 0.0f;
}
float Wetness_pee(const Wetness *w, float x, float z, float radius) {
float most = 0.0f;
const int n = (int)(radius / CELL);
for (int kz = -n; kz <= n; kz++)
for (int kx = -n; kx <= n; kx++) {
if (kx * kx + kz * kz > n * n) continue;
int i;
const WetChunk *c = cellAtConst(w, x + (float)kx * CELL, z + (float)kz * CELL, &i);
if (c) most = fmaxf(most, c->pee[i]);
}
return clampf(most / (PEE_SHOWN * 0.5f), 0.0f, 1.0f);
}
#define STEP_RADIUS 0.12f /* a shoe */
float Wetness_step(Wetness *w, float x, float z) {
float soft = Wetness_softness(w, x, z), mud = Wetness_mud(w, x, z);
if (soft <= 0.05f) return mud;
const int n = (int)(STEP_RADIUS / CELL + 0.5f);
for (int kz = -n; kz <= n; kz++)
for (int kx = -n; kx <= n; kx++) {
int i;
WetChunk *c = (WetChunk *)cellAtConst(w, x + (float)kx * CELL, z + (float)kz * CELL, &i);
if (!c) continue;
c->mud[i] = fminf(1.0f, c->mud[i] + 0.06f * soft);
c->dirty = true;
c->dead = false;
}
return mud;
}