optimize world generation

This commit is contained in:
p2r3
2025-08-30 16:56:00 +03:00
parent 4a7f9554a8
commit 759399dac8
2 changed files with 145 additions and 74 deletions

View File

@@ -21,10 +21,17 @@ typedef struct {
uint8_t biome;
} ChunkAnchor;
typedef struct {
short x;
uint8_t y;
short z;
uint8_t variant;
} ChunkFeature;
uint32_t getChunkHash (short x, short z);
uint8_t getChunkBiome (short x, short z);
int getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome);
int getHeightAt (int x, int z);
uint8_t getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome);
uint8_t getHeightAt (int x, int z);
uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor);
uint8_t getBlockAt (int x, int y, int z);

View File

@@ -48,12 +48,12 @@ uint8_t getChunkBiome (short x, short z) {
}
int getCornerHeight (uint32_t hash, uint8_t biome) {
uint8_t getCornerHeight (uint32_t hash, uint8_t biome) {
// When calculating the height, parts of the hash are used as random values.
// Often, multiple values are stacked to stabilize the distribution while
// allowing for occasionally larger variances.
int height = TERRAIN_BASE_HEIGHT;
uint8_t height = TERRAIN_BASE_HEIGHT;
switch (biome) {
@@ -114,13 +114,32 @@ int getCornerHeight (uint32_t hash, uint8_t biome) {
}
int interpolate (int a, int b, int c, int d, int x, int z) {
int top = a * (CHUNK_SIZE - x) + b * x;
int bottom = c * (CHUNK_SIZE - x) + d * x;
uint8_t interpolate (uint8_t a, uint8_t b, uint8_t c, uint8_t d, int x, int z) {
uint16_t top = a * (CHUNK_SIZE - x) + b * x;
uint16_t bottom = c * (CHUNK_SIZE - x) + d * x;
return (top * (CHUNK_SIZE - z) + bottom * z) / (CHUNK_SIZE * CHUNK_SIZE);
}
int getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome) {
// Calculates terrain height using a pointer to an array of anchors
// The pointer should point towards the minichunk containing the desired
// coordinates, with available neighbors on +X and +Z.
uint8_t getHeightAtFromAnchors (int rx, int rz, ChunkAnchor *anchor_ptr) {
if (rx == 0 && rz == 0) {
int height = getCornerHeight(anchor_ptr[0].hash, anchor_ptr[0].biome);
if (height > 67) return height - 1;
}
return interpolate(
getCornerHeight(anchor_ptr[0].hash, anchor_ptr[0].biome),
getCornerHeight(anchor_ptr[1].hash, anchor_ptr[1].biome),
getCornerHeight(anchor_ptr[16 / CHUNK_SIZE + 1].hash, anchor_ptr[16 / CHUNK_SIZE + 1].biome),
getCornerHeight(anchor_ptr[16 / CHUNK_SIZE + 2].hash, anchor_ptr[16 / CHUNK_SIZE + 2].biome),
rx, rz
);
}
uint8_t getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome) {
if (rx == 0 && rz == 0) {
int height = getCornerHeight(chunk_hash, biome);
@@ -136,7 +155,9 @@ int getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, ui
}
int getHeightAt (int x, int z) {
// Get terrain height at the given coordinates
// Does *not* account for block changes
uint8_t getHeightAt (int x, int z) {
int _x = div_floor(x, CHUNK_SIZE);
int _z = div_floor(z, CHUNK_SIZE);
@@ -149,74 +170,47 @@ int getHeightAt (int x, int z) {
}
uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) {
if (y > 80) return B_air;
int rx = x % CHUNK_SIZE;
int rz = z % CHUNK_SIZE;
if (rx < 0) rx += CHUNK_SIZE;
if (rz < 0) rz += CHUNK_SIZE;
int height = getHeightAtFromHash(rx, rz, anchor.x, anchor.z, anchor.hash, anchor.biome);
uint8_t getTerrainAtFromCache (int x, int y, int z, int rx, int rz, ChunkAnchor anchor, ChunkFeature feature, uint8_t height) {
if (y < 64 || y < height) goto skip_feature;
uint8_t feature_position = anchor.hash % (CHUNK_SIZE * CHUNK_SIZE);
short feature_x = feature_position % CHUNK_SIZE;
short feature_z = feature_position / CHUNK_SIZE;
// The following check does two things:
// firstly, it ensures that trees don't cross chunk boundaries;
// secondly, it reduces overall feature count. This is favorable
// everywhere except for swamps, which are otherwise very boring.
if (anchor.biome != W_mangrove_swamp) {
if (feature_x < 3 || feature_x > CHUNK_SIZE - 3) goto skip_feature;
if (feature_z < 3 || feature_z > CHUNK_SIZE - 3) goto skip_feature;
}
uint8_t feature_variant = (anchor.hash >> (feature_x + feature_z)) & 1;
feature_x += anchor.x * CHUNK_SIZE;
feature_z += anchor.z * CHUNK_SIZE;
switch (anchor.biome) {
case W_plains: { // Generate trees in the plains biome
uint8_t feature_y = getHeightAtFromHash(
feature_x < 0 ? feature_x % CHUNK_SIZE + CHUNK_SIZE : feature_x % CHUNK_SIZE,
feature_z < 0 ? feature_z % CHUNK_SIZE + CHUNK_SIZE : feature_z % CHUNK_SIZE,
anchor.x, anchor.z, anchor.hash, anchor.biome
) + 1;
if (feature_y < 64) break;
// Don't generate trees underwater
if (feature.y < 64) break;
if (x == feature_x && z == feature_z) {
if (y == feature_y - 1) return B_dirt;
if (y >= feature_y && y < feature_y - feature_variant + 6) return B_oak_log;
// Handle tree stem and the dirt under it
if (x == feature.x && z == feature.z) {
if (y == feature.y - 1) return B_dirt;
if (y >= feature.y && y < feature.y - feature.variant + 6) return B_oak_log;
}
uint8_t dx = x > feature_x ? x - feature_x : feature_x - x;
uint8_t dz = z > feature_z ? z - feature_z : feature_z - z;
// Get X/Z distance from center of tree
uint8_t dx = x > feature.x ? x - feature.x : feature.x - x;
uint8_t dz = z > feature.z ? z - feature.z : feature.z - z;
if (dx < 3 && dz < 3 && y > feature_y - feature_variant + 2 && y < feature_y - feature_variant + 5) {
if (y == feature_y - feature_variant + 4 && dx == 2 && dz == 2) break;
// Generate leaf clusters
if (dx < 3 && dz < 3 && y > feature.y - feature.variant + 2 && y < feature.y - feature.variant + 5) {
if (y == feature.y - feature.variant + 4 && dx == 2 && dz == 2) break;
return B_oak_leaves;
}
if (dx < 2 && dz < 2 && y >= feature_y - feature_variant + 5 && y <= feature_y - feature_variant + 6) {
if (y == feature_y - feature_variant + 6 && dx == 1 && dz == 1) break;
if (dx < 2 && dz < 2 && y >= feature.y - feature.variant + 5 && y <= feature.y - feature.variant + 6) {
if (y == feature.y - feature.variant + 6 && dx == 1 && dz == 1) break;
return B_oak_leaves;
}
// Since we're sure that we're above sea level and in a plains biome,
// there's no need to drop down to decide the surrounding blocks.
if (y == height) return B_grass_block;
return B_air;
}
case W_desert: { // Generate dead bushes and cacti in deserts
if (x != feature_x || z != feature_z) break;
if (x != feature.x || z != feature.z) break;
if (feature_variant == 0) {
if (feature.variant == 0) {
if (y == height + 1) return B_dead_bush;
} else if (y > height) {
// The size of the cactus is determined based on whether the terrain
@@ -231,13 +225,13 @@ uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) {
case W_mangrove_swamp: { // Generate lilypads and moss carpets in swamps
if (x == feature_x && z == feature_z && y == 64 && height < 63) {
if (x == feature.x && z == feature.z && y == 64 && height < 63) {
return B_lily_pad;
}
if (y == height + 1) {
uint8_t dx = x > feature_x ? x - feature_x : feature_x - x;
uint8_t dz = z > feature_z ? z - feature_z : feature_z - z;
uint8_t dx = x > feature.x ? x - feature.x : feature.x - x;
uint8_t dz = z > feature.z ? z - feature.z : feature.z - z;
if (dx + dz < 4) return B_moss_carpet;
}
@@ -246,7 +240,7 @@ uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) {
case W_snowy_plains: { // Generate grass stubs in snowy plains
if (x == feature_x && z == feature_z && y == height + 1 && height >= 64) {
if (x == feature.x && z == feature.z && y == height + 1 && height >= 64) {
return B_short_grass;
}
@@ -316,35 +310,88 @@ skip_feature:
}
ChunkFeature getFeatureFromAnchor (ChunkAnchor anchor) {
ChunkFeature feature;
uint8_t feature_position = anchor.hash % (CHUNK_SIZE * CHUNK_SIZE);
feature.x = feature_position % CHUNK_SIZE;
feature.z = feature_position / CHUNK_SIZE;
uint8_t skip_feature = false;
// The following check does two things:
// firstly, it ensures that trees don't cross chunk boundaries;
// secondly, it reduces overall feature count. This is favorable
// everywhere except for swamps, which are otherwise very boring.
if (anchor.biome != W_mangrove_swamp) {
if (feature.x < 3 || feature.x > CHUNK_SIZE - 3) skip_feature = true;
else if (feature.z < 3 || feature.z > CHUNK_SIZE - 3) skip_feature = true;
}
if (skip_feature) {
// Skipped features are indicated by a Y coordinate of 0xFF (255)
feature.y = 0xFF;
} else {
feature.x += anchor.x * CHUNK_SIZE;
feature.z += anchor.z * CHUNK_SIZE;
feature.y = getHeightAtFromHash(
mod_abs(feature.x, CHUNK_SIZE), mod_abs(feature.z, CHUNK_SIZE),
anchor.x, anchor.z, anchor.hash, anchor.biome
) + 1;
feature.variant = (anchor.hash >> (feature.x + feature.z)) & 1;
}
return feature;
}
uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) {
if (y > 80) return B_air;
int rx = x % CHUNK_SIZE;
int rz = z % CHUNK_SIZE;
if (rx < 0) rx += CHUNK_SIZE;
if (rz < 0) rz += CHUNK_SIZE;
ChunkFeature feature = getFeatureFromAnchor(anchor);
uint8_t height = getHeightAtFromHash(rx, rz, anchor.x, anchor.z, anchor.hash, anchor.biome);
return getTerrainAtFromCache(x, y, z, rx, rz, anchor, feature, height);
}
uint8_t getBlockAt (int x, int y, int z) {
uint8_t block_change = getBlockChange(x, y, z);
if (block_change != 0xFF) return block_change;
short anchor_x = div_floor(x, CHUNK_SIZE);
short anchor_z = div_floor(z, CHUNK_SIZE);
ChunkAnchor anchor = {
x / CHUNK_SIZE,
z / CHUNK_SIZE
.x = anchor_x,
.z = anchor_z,
.hash = getChunkHash(anchor_x, anchor_z),
.biome = getChunkBiome(anchor_x, anchor_z)
};
if (x % CHUNK_SIZE < 0) anchor.x --;
if (z % CHUNK_SIZE < 0) anchor.z --;
anchor.hash = getChunkHash(anchor.x, anchor.z);
anchor.biome = getChunkBiome(anchor.x, anchor.z);
return getTerrainAt(x, y, z, anchor);
}
uint8_t chunk_section[4096];
ChunkAnchor chunk_anchors[256 / (CHUNK_SIZE * CHUNK_SIZE)];
ChunkAnchor chunk_anchors[(16 / CHUNK_SIZE + 1) * (16 / CHUNK_SIZE + 1)];
ChunkFeature chunk_features[256 / (CHUNK_SIZE * CHUNK_SIZE)];
uint8_t chunk_section_height[16][16];
// Builds a 16x16x16 chunk of blocks and writes it to `chunk_section`
// Returns the biome at the origin corner of the chunk
uint8_t buildChunkSection (int cx, int cy, int cz) {
// Precompute the hashes and anchors for each minichunk
int anchor_index = 0;
for (int i = cz; i < cz + 16; i += CHUNK_SIZE) {
for (int j = cx; j < cx + 16; j += CHUNK_SIZE) {
// Precompute hashes, anchors and features for each relevant minichunk
int anchor_index = 0, feature_index = 0;
for (int i = cz; i < cz + 16 + CHUNK_SIZE; i += CHUNK_SIZE) {
for (int j = cx; j < cx + 16 + CHUNK_SIZE; j += CHUNK_SIZE) {
ChunkAnchor *anchor = chunk_anchors + anchor_index;
@@ -353,24 +400,41 @@ uint8_t buildChunkSection (int cx, int cy, int cz) {
anchor->hash = getChunkHash(anchor->x, anchor->z);
anchor->biome = getChunkBiome(anchor->x, anchor->z);
// Compute chunk features for the minichunks within this section
if (i != cz + 16 && j != cx + 16) {
chunk_features[feature_index] = getFeatureFromAnchor(*anchor);
feature_index ++;
}
anchor_index ++;
}
}
// Precompute terrain height for entire chunk section
for (int i = 0; i < 16; i ++) {
for (int j = 0; j < 16; j ++) {
anchor_index = (j / CHUNK_SIZE) + (i / CHUNK_SIZE) * (16 / CHUNK_SIZE + 1);
ChunkAnchor *anchor_ptr = chunk_anchors + anchor_index;
chunk_section_height[j][i] = getHeightAtFromAnchors(j % 8, i % 8, anchor_ptr);
}
}
// Generate 4096 blocks in one buffer to reduce overhead
for (int j = 0; j < 4096; j += 8) {
// These values don't change in the lower array,
// since all of the operations are on multiples of 8
int y = j / 256 + cy;
int z = j / 16 % 16 + cz;
int rz = j / 16 % 16;
feature_index = (j % 16) / CHUNK_SIZE + (j / 16 % 16) / CHUNK_SIZE * (16 / CHUNK_SIZE);
anchor_index = (j % 16) / CHUNK_SIZE + (j / 16 % 16) / CHUNK_SIZE * (16 / CHUNK_SIZE + 1);
// The client expects "big-endian longs", which in our
// case means reversing the order in which we store/send
// each 8 block sequence.
anchor_index = (j % 16) / CHUNK_SIZE + (j / 16 % 16) / CHUNK_SIZE * 2;
for (int offset = 7; offset >= 0; offset--) {
int k = j + offset;
int x = k % 16 + cx;
chunk_section[j + 7 - offset] = getTerrainAt(x, y, z, chunk_anchors[anchor_index]);
int rx = k % 16;
uint8_t height = chunk_section_height[rx][rz];
chunk_section[j + 7 - offset] = getTerrainAtFromCache(rx + cx, y, rz + cz, rx, rz, chunk_anchors[anchor_index], chunk_features[feature_index], height);
}
}