Compare commits

...

2 Commits

Author SHA1 Message Date
2707748843 refactor: increase BLEND_RADIUS to 12 2026-04-23 15:40:40 +08:00
e90b0ce2f4 refactor: chunk generation logic 2026-04-23 15:19:34 +08:00
7 changed files with 399 additions and 45 deletions

View File

@@ -1,4 +1,5 @@
#pragma once
#include <array>
namespace Cubed {
@@ -13,7 +14,7 @@ constexpr int MAX_CHARACTER = 128;
constexpr float NORMAL_FOV = 70.0f;
constexpr int MAX_BIOME_SUM = 4;
using HeightMapArray = std::array<std::array<float, CHUCK_SIZE>, CHUCK_SIZE>;
constexpr float VERTICES_POS[6][6][3] = {
// ===== front (z = +1) =====
0.0f, 0.0f, 1.0f, // bottom left

View File

@@ -1,6 +1,7 @@
#pragma once
#include <array>
#include <string>
#include <vector>
namespace Cubed {
@@ -15,7 +16,8 @@ enum class Biome {
PLAIN = 0,
FOREST,
DESERT,
MOUNTAIN
MOUNTAIN,
NONE
};
struct BiomeHeightRange {
@@ -23,11 +25,16 @@ struct BiomeHeightRange {
int amplitude;
};
struct BiomeNonAdjacent {
Biome first;
std::vector<Biome> second;
Biome replace;
};
std::string get_biome_str(Biome biome);
Biome get_biome_from_noise(float temp, float humid);
std::array<float, 3> get_noise_frequencies_for_biome(Biome biome);
BiomeHeightRange get_biome_height_range(Biome biome);
Biome safe_int_to_biome(int x);
int get_interpolated_height(float world_x, float world_z, float temp, float humid);
}

View File

@@ -15,18 +15,28 @@ class World;
// if want to use, do init_chunk(), gen_vertex_data() and
class Chunk {
private:
static constexpr int SIZE_X = CHUCK_SIZE;
static constexpr int SIZE_Y = WORLD_SIZE_Y;
static constexpr int SIZE_Z = CHUCK_SIZE;
static inline const std::vector<BiomeNonAdjacent> NON_ADJACENT {{
{Biome::PLAIN, {Biome::NONE}, Biome::PLAIN},
{Biome::FOREST, {Biome::DESERT}, Biome::PLAIN},
{Biome::DESERT, {Biome::MOUNTAIN, Biome::FOREST}, Biome::PLAIN},
{Biome::MOUNTAIN, {Biome::DESERT}, Biome::PLAIN}
}
};
using HeightMapArray = std::array<std::array<float, SIZE_Z>, SIZE_X>;
std::atomic<bool> m_dirty {false};
std::atomic<bool> m_need_upload{true};
std::atomic<bool> m_is_on_gen_vertex_data {false};
std::atomic<size_t> m_vertex_sum = 0;
std::mutex m_vertexs_data_mutex;
static constexpr int SIZE_X = CHUCK_SIZE;
static constexpr int SIZE_Y = WORLD_SIZE_Y;
static constexpr int SIZE_Z = CHUCK_SIZE;
Biome m_biome = Biome::PLAIN;
std::atomic<Biome> m_biome = Biome::PLAIN;
ChunkPos m_chunk_pos;
World& m_world;
HeightMapArray m_heightmap;
// the index is a array of block id
std::vector<uint8_t> m_blocks;
GLuint m_vbo = 0;
@@ -51,10 +61,22 @@ public:
Biome get_biome() const;
const std::vector<uint8_t>& get_chunk_blocks() const;
HeightMapArray get_heightmap() const;
static int get_index(int x, int y, int z);
static int get_index(const glm::vec3& pos);
void init_chunk();
// Generate Biome
void gen_phase_one();
// Adjust Biome
void gen_phase_two(const std::array<const Chunk*, 4>& adj_chunks);
// Generate Heightmap
void gen_phase_three();
// Adjust Height
void gen_phase_four(const std::array<std::optional<HeightMapArray>, 4>& neighbor_heightmap);
// Generate Block
void gen_phase_five();
// Generate Structure
void gen_phase_six();
//void gen_vertex_data();
// 0 : (1, 0)
// 1 : (-1, 0)

View File

@@ -27,6 +27,9 @@ private:
using ChunkUpdateList = std::vector<std::pair<ChunkPos, Chunk>>;
using ConstChunkMap = std::unordered_map<ChunkPos, const Chunk*, ChunkPos::Hash>;
using ChunkPosSet = std::unordered_set<ChunkPos, ChunkPos::Hash>;
bool m_could_gen = true;
glm::vec3 m_gen_player_pos{0.0f, 0.0f, 0.0f};
std::unordered_map<ChunkPos , Chunk, ChunkPos::Hash> m_chunks;
std::unordered_map<std::size_t, Player> m_players;
@@ -48,6 +51,8 @@ private:
std::vector<std::pair<ChunkPos, Chunk>> m_new_chunk;
std::vector<std::pair<ChunkPos, Chunk>> m_new_chunk_queue;
void init_chunks();
void gen_chunks_internal();
void sync_player_pos(glm::vec3& player_pos);
void compute_required_chunks(ChunkPosSet& required_chunks);

View File

@@ -24,10 +24,13 @@ std::string get_biome_str(Biome biome) {
case MOUNTAIN:
str = "Mountain";
break;
case NONE:
str = "Unknown";
break;
}
return str;
};
/*
Biome get_biome_from_noise(float temp, float humid) {
auto weight = [](float t, float h, float ct, float ch) -> float {
float dt = t - ct;
@@ -45,7 +48,14 @@ Biome get_biome_from_noise(float temp, float humid) {
if (w_d >= w_m && w_d >= w_p && w_d >= w_f) return Biome::DESERT;
return Biome::FOREST;
}
*/
Biome get_biome_from_noise(float temp, float humid) {
using enum Biome;
if (temp < 0.5f && humid < 0.5f) return PLAIN;
if (temp < 0.5f && humid >= 0.5f) return FOREST;
if (temp >= 0.5f && humid < 0.5f) return DESERT;
return MOUNTAIN;
}
std::array<float, 3> get_noise_frequencies_for_biome(Biome biome) {
using enum Biome;
switch (biome) {
@@ -57,6 +67,9 @@ std::array<float, 3> get_noise_frequencies_for_biome(Biome biome) {
return {0.003f, 0.010f, 0.020f};
case MOUNTAIN:
return {0.006f, 0.015f, 0.030f};
case NONE:
ASSERT_MSG(false, "Chunk Biome is None");
throw std::invalid_argument{"Chunk Biome is None"};
}
Logger::warn("Unknown Biome");
return {0.003f, 0.015f, 0.06f};
@@ -73,6 +86,9 @@ BiomeHeightRange get_biome_height_range(Biome biome) {
return {61, 12};
case MOUNTAIN:
return {70, 70};
case NONE:
ASSERT_MSG(false, "Chunk Biome is None");
throw std::invalid_argument{"Chunk Biome is None"};
}
Logger::warn("Unknown Biome");
return {62, 4};

View File

@@ -31,9 +31,10 @@ Chunk::Chunk(Chunk&& other) noexcept :
m_need_upload(other.m_need_upload.load()),
m_is_on_gen_vertex_data(other.m_is_on_gen_vertex_data.load()),
m_vertex_sum(other.m_vertex_sum.load()),
m_biome(other.m_biome),
m_biome(other.m_biome.load()),
m_chunk_pos(std::move(other.m_chunk_pos)),
m_world(other.m_world),
m_heightmap(std::move(other.m_heightmap)),
m_blocks(std::move(other.m_blocks)),
m_vbo(other.m_vbo),
m_vertexs_data(std::move(other.m_vertexs_data))
@@ -42,13 +43,15 @@ Chunk::Chunk(Chunk&& other) noexcept :
}
Chunk& Chunk::operator=(Chunk&& other) noexcept {
//Logger::info("other Chunk pos {} {} in Chunk& Chunk::operator=(Chunk&& other) this {}", other.m_chunk_pos.x, other.m_chunk_pos.z, static_cast<const void*>(&other));
m_vbo = other.m_vbo;
other.m_vbo = 0;
m_chunk_pos = std::move(other.m_chunk_pos);
m_heightmap = std::move(other.m_heightmap);
m_blocks = std::move(other.m_blocks);
m_dirty = other.is_dirty();
m_vertexs_data = std::move(other.m_vertexs_data);
m_biome = other.m_biome;
m_biome = other.m_biome.load();
m_is_on_gen_vertex_data = other.m_is_on_gen_vertex_data.load();
m_need_upload = other.m_need_upload.load();
m_vertex_sum = other.m_vertex_sum.load();
@@ -56,13 +59,18 @@ Chunk& Chunk::operator=(Chunk&& other) noexcept {
}
Biome Chunk::get_biome() const {
return m_biome;
return m_biome.load();
}
const std::vector<uint8_t>& Chunk::get_chunk_blocks() const{
return m_blocks;
}
HeightMapArray Chunk::get_heightmap() const {
//Logger::info("Chunk pos {} {} in get_heightmap this {}", m_chunk_pos.x, m_chunk_pos.z, static_cast<const void*>(this));
return m_heightmap;
}
int Chunk::get_index(int x, int y, int z) {
ASSERT(!(x < 0 || y < 0 || z < 0 || x >= CHUCK_SIZE || y >= WORLD_SIZE_Y || z >= CHUCK_SIZE));
if ((x * WORLD_SIZE_Y + y) * CHUCK_SIZE + z < 0 || (x * WORLD_SIZE_Y + y) * CHUCK_SIZE + z >= CHUCK_SIZE * CHUCK_SIZE * WORLD_SIZE_Y) {
@@ -209,6 +217,187 @@ void Chunk::init_chunk() {
resolve_blocks();
}
void Chunk::gen_phase_one() {
resolve_biome();
}
void Chunk::gen_phase_two(const std::array<const Chunk*, 4>& adj_chunks) {
for (auto& chunk : adj_chunks) {
if (chunk == nullptr) {
continue;
}
Biome biome = chunk->get_biome();
for (const auto& non : NON_ADJACENT) {
if (m_biome != non.first) {
continue;
}
for (auto b : non.second) {
if (b == biome) {
m_biome = non.replace;
return;
}
}
}
}
}
void Chunk::gen_phase_three() {
for (int x = 0; x < CHUCK_SIZE; x++) {
for (int z = 0; z < CHUCK_SIZE; z++) {
float world_x = static_cast<float>(x + m_chunk_pos.x * CHUCK_SIZE);
float world_z = static_cast<float>(z + m_chunk_pos.z * CHUCK_SIZE);
auto sample_height = [&](Biome b) -> float {
auto range = get_biome_height_range(b);
auto [f1, f2, f3] = get_noise_frequencies_for_biome(b);
float n =
1.00f * PerlinNoise::noise(world_x * f1, 0.5f, world_z * f1) +
0.50f * PerlinNoise::noise(world_x * f2, 0.5f, world_z * f2) +
0.25f * PerlinNoise::noise(world_x * f3, 0.5f, world_z * f3);
n /= 1.75f;
return range.base_y + n * range.amplitude;
};
m_heightmap[x][z] = sample_height(m_biome);
}
}
}
void Chunk::gen_phase_four(const std::array<std::optional<HeightMapArray>, 4>& neighbor_heightmap) {
// Width of interpolation influence (in number of cells)
constexpr int BLEND_RADIUS = 12;
for (int x = 0; x < SIZE_X; x++) {
for (int z = 0; z < SIZE_Z; z++) {
float h = static_cast<float>(m_heightmap[x][z]);
float total_weight = 1.0f;
float blended = h;
// --- Right neighbor neighbor[0]: (1, 0) ---
// Blend when x is close to SIZE_X-1
if (neighbor_heightmap[0] != std::nullopt) {
int dist = (SIZE_X - 1) - x; // distance from right border
if (dist < BLEND_RADIUS) {
// Neighbor's boundary row is its x=0 column
float neighbor_h = static_cast<float>((*neighbor_heightmap[0])[0][z]);
float t = 1.0f - static_cast<float>(dist) / BLEND_RADIUS; // larger weight when closer
// Use smoothstep for a more natural transition
t = t * t * (3.0f - 2.0f * t);
blended += t * neighbor_h;
total_weight += t;
}
}
// --- Left neighbor neighbor[1]: (-1, 0) ---
if (neighbor_heightmap[1] != std::nullopt) {
int dist = x; // distance from left border
if (dist < BLEND_RADIUS) {
float neighbor_h = static_cast<float>((*neighbor_heightmap[1])[SIZE_X - 1][z]);
float t = 1.0f - static_cast<float>(dist) / BLEND_RADIUS;
t = t * t * (3.0f - 2.0f * t);
blended += t * neighbor_h;
total_weight += t;
}
}
// --- Front neighbor neighbor[2]: (0, 1) ---
if (neighbor_heightmap[2] != std::nullopt) {
int dist = (SIZE_Z - 1) - z;
if (dist < BLEND_RADIUS) {
float neighbor_h = static_cast<float>((*neighbor_heightmap[2])[x][0]);
float t = 1.0f - static_cast<float>(dist) / BLEND_RADIUS;
t = t * t * (3.0f - 2.0f * t);
blended += t * neighbor_h;
total_weight += t;
}
}
// --- Back neighbor neighbor[3]: (0, -1) ---
if (neighbor_heightmap[3] != std::nullopt) {
int dist = z;
if (dist < BLEND_RADIUS) {
float neighbor_h = static_cast<float>((*neighbor_heightmap[3])[x][SIZE_Z - 1]);
float t = 1.0f - static_cast<float>(dist) / BLEND_RADIUS;
t = t * t * (3.0f - 2.0f * t);
blended += t * neighbor_h;
total_weight += t;
}
}
m_heightmap[x][z] = static_cast<int>(blended / total_weight);
}
}
}
void Chunk::gen_phase_five() {
// bottom
m_blocks.assign(CHUCK_SIZE * CHUCK_SIZE * WORLD_SIZE_Y, 0);
for (int x = 0; x < CHUCK_SIZE; x++) {
for (int y = 0; y < 5; y++) {
for (int z = 0; z < CHUCK_SIZE; z++) {
m_blocks[get_index(x, y, z)] = 3;
}
}
}
for (int x = 0; x < CHUCK_SIZE; x++) {
for (int z = 0; z < CHUCK_SIZE; z++) {
int height = static_cast<int>(m_heightmap[x][z]);
for (int y = 5; y < height - 5; y++) {
m_blocks[get_index(x, y, z)] = 3;
}
if (m_biome == Biome::MOUNTAIN) {
for (int y = height - 5; y <= height - 1; y++) {
if (y > 110) {
m_blocks[get_index(x, y, z)] = 3;
} else {
m_blocks[get_index(x, y, z)] = 2;
}
}
if (height > 110) {
m_blocks[get_index(x, height - 1, z)] = 3;
} else {
m_blocks[get_index(x, height - 1, z)] = 1;
}
} else if (m_biome == Biome::DESERT) {
for (int y = height - 5; y <= height; y++) {
m_blocks[get_index(x, y, z)] = 4;
}
} else {
for (int y = height - 5; y <= height - 1; y++) {
m_blocks[get_index(x, y, z)] = 2;
}
for (int y = height; y <= height; y++) {
m_blocks[get_index(x, y, z)] = 1;
}
}
}
}
}
void Chunk::gen_phase_six() {
if (m_biome == Biome::FOREST) {
std::array<int, SIZE_X> x_arr;
std::iota(x_arr.begin(), x_arr.end(), 0);
std::shuffle(x_arr.begin(), x_arr.end(), Cubed::Random::get().engine());
std::array<int, SIZE_Z> z_arr;
std::iota(z_arr.begin(), z_arr.end(), 0);
std::shuffle(z_arr.begin(), z_arr.end(), Cubed::Random::get().engine());
for (auto x : x_arr) {
for (auto z : z_arr) {
if (Cubed::Random::get().random_bool(0.1)) {
build_tree(*this, {x, static_cast<int>(m_heightmap[x][z]), z});
}
}
}
}
mark_dirty();
}
void Chunk::upload_to_gpu() {
ASSERT(is_need_upload());

View File

@@ -11,7 +11,6 @@
namespace Cubed {
static constexpr ChunkPos CHUNK_DIR[] {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
@@ -75,6 +74,7 @@ Player& World::get_player(const std::string& name){
}
void World::init_world() {
m_chunks.reserve(DISTANCE * DISTANCE);
auto t1 = std::chrono::system_clock::now();
for (int s = 0; s < DISTANCE; s++) {
for (int t = 0; t < DISTANCE; t++) {
@@ -86,28 +86,21 @@ void World::init_world() {
m_chunks.emplace(pos, Chunk(*this, pos));
}
}
Logger::info("Max Support Thread is {}", std::thread::hardware_concurrency());
init_chunks();
auto t2 = std::chrono::system_clock::now();
auto d = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);
Logger::info("Chunk Block Init Finish, Time Consuming: {}", d);
// init players
m_players.emplace(HASH::str("TestPlayer"), Player(*this, "TestPlayer"));
Logger::info("TestPlayer Create Finish");
start_gen_thread();
}
/*
for (auto& chunk_map : m_chunks) {
auto& [chunk_pos, chunk] = chunk_map;
chunk.init_chunk();
}
// After block gen fininshed
std::array<const std::vector<uint8_t>*, 4> neighbor_block;
for (auto& [pos, chunk] : m_chunks) {
for (int i = 0; i < 4; i++) {
auto it = m_chunks.find(pos + CHUNK_DIR[i]);
if (it != m_chunks.end()) {
neighbor_block[i] = &(it->second.get_chunk_blocks());
} else {
neighbor_block[i] = nullptr;
}
}
chunk.gen_vertex_data(neighbor_block);
}
*/
void World::init_chunks() {
std::vector<Chunk*> chunk_ptrs;
chunk_ptrs.reserve(m_chunks.size());
for (auto& [pos, chunk] : m_chunks) {
@@ -148,14 +141,81 @@ void World::init_world() {
chunk.upload_to_gpu();
}
auto t2 = std::chrono::system_clock::now();
auto d = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);
Logger::info("Chunk Block Init Finish, Time Consuming: {}", d);
// init players
m_players.emplace(HASH::str("TestPlayer"), Player(*this, "TestPlayer"));
Logger::info("TestPlayer Create Finish");
}
*/
start_gen_thread();
void World::init_chunks() {
for (auto& [pos, chunks] : m_chunks) {
chunks.gen_phase_one();
}
std::array<const Chunk*, 4> neighbor_chunks;
for (auto& [pos, chunks] : m_chunks) {
for (int i = 0; i < 4; i++) {
auto neighbor_pos = pos + CHUNK_DIR[i];
auto it = m_chunks.find(neighbor_pos);
if (it == m_chunks.end()) {
neighbor_chunks[i] = nullptr;
continue;
}
neighbor_chunks[i] = &it->second;
}
chunks.gen_phase_two(neighbor_chunks);
}
for (auto& [pos, chunks] : m_chunks) {
chunks.gen_phase_three();
}
std::array<std::optional<HeightMapArray>, 4> neighbor_chunk_heightmap;
for (auto& [pos, chunks] : m_chunks) {
for (int i = 0; i < 4; i++) {
auto neighbor_pos = pos + CHUNK_DIR[i];
auto it = m_chunks.find(neighbor_pos);
if (it == m_chunks.end()) {
neighbor_chunk_heightmap[i] = std::nullopt;
continue;
}
neighbor_chunk_heightmap[i] = it->second.get_heightmap();
}
chunks.gen_phase_four(neighbor_chunk_heightmap);
}
for (auto& [pos, chunks] : m_chunks) {
chunks.gen_phase_five();
chunks.gen_phase_six();
}
std::atomic<int> sync{0};
sync.store(1, std::memory_order_release);
sync.load(std::memory_order_acquire);
std::vector<ChunkRenderData> pending_gen_data;
pending_gen_data.reserve(m_chunks.size());
for (auto& [pos, chunk] : m_chunks) {
ChunkRenderData data;
data.chunk = &chunk;
for (int i = 0; i < 4; i++) {
auto it = m_chunks.find(pos + CHUNK_DIR[i]);
if (it != m_chunks.end()) {
data.neighbor_block[i] = &(it->second.get_chunk_blocks());
} else {
data.neighbor_block[i] = nullptr;
}
}
pending_gen_data.emplace_back(std::move(data));
}
std::for_each(std::execution::par, pending_gen_data.begin(), pending_gen_data.end(), [](ChunkRenderData& data){
if(!data.chunk) {
return ;
}
data.chunk->gen_vertex_data(data.neighbor_block);
});
for (auto& chunk_map : m_chunks) {
auto& [chunk_pos, chunk] = chunk_map;
chunk.upload_to_gpu();
}
}
@@ -215,6 +275,7 @@ void World::gen_chunks_internal() {
Logger::info("New Gen Chunks Sum: {}", need_gen_chunks_pos.size());
if (need_gen_chunks_pos.empty()) {
m_could_gen = true;
return;
}
ChunkUpdateList new_chunks;
@@ -229,8 +290,49 @@ void World::gen_chunks_internal() {
std::array<const std::vector<uint8_t>*, 4> neighbor_block;
// build new chunk, but the neighbor in m_chunks also need to re-build
for (auto& [pos, chunk] : new_chunks) {
chunk.init_chunk();
chunk.gen_phase_one();
}
std::array<const Chunk*, 4> neighbor_chunks;
for (auto& [pos, chunks] : new_chunks) {
for (int i = 0; i < 4; i++) {
auto neighbor_pos = pos + CHUNK_DIR[i];
auto it = new_chunks_neighbor.find(neighbor_pos);
if (it == new_chunks_neighbor.end()) {
neighbor_chunks[i] = nullptr;
continue;
}
neighbor_chunks[i] = it->second;
}
chunks.gen_phase_two(neighbor_chunks);
}
for (auto& [pos, chunks] : new_chunks) {
chunks.gen_phase_three();
}
std::array<std::optional<HeightMapArray>, 4> neighbor_chunk_heightmap;
for (auto& [pos, chunks] : new_chunks) {
{
//std::lock_guard lk(m_chunks_mutex);
for (int i = 0; i < 4; i++) {
auto neighbor_pos = pos + CHUNK_DIR[i];
auto it = new_chunks_neighbor.find(neighbor_pos);
if (it == new_chunks_neighbor.end()) {
neighbor_chunk_heightmap[i] = std::nullopt;
continue;
}
neighbor_chunk_heightmap[i] = it->second->get_heightmap();
}
}
chunks.gen_phase_four(neighbor_chunk_heightmap);
}
for (auto& [pos, chunks] : new_chunks) {
chunks.gen_phase_five();
chunks.gen_phase_six();
}
for (auto& [pos, chunk] : new_chunks) {
@@ -371,6 +473,11 @@ void World::stop_gen_thread() {
}
void World::need_gen() {
if (!m_could_gen) {
Logger::warn("It is generating or consuming new chunks");
return;
}
m_could_gen = false;
{
std::lock_guard lk(m_gen_player_pos_mutex);
m_gen_player_pos = get_player("TestPlayer").get_player_pos();
@@ -509,9 +616,16 @@ void World::update(float delta_time) {
// unified compute vertex data before rendering
{
std::lock_guard lk(m_chunks_mutex);
bool consumed = false;
for (auto& x : m_new_chunk) {
m_chunks.insert_or_assign(x.first, std::move(x.second));
consumed = true;
}
if (consumed) {
m_could_gen = true;
}
m_render_snapshots.clear();
for (auto& [pos, chunk] : m_chunks) {
if (chunk.is_dirty()) {