#include "world/terrain.h" #include "world/site.h" #include "render/texture.h" #include #include #include #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; }