283 lines
8.8 KiB
C++
283 lines
8.8 KiB
C++
|
|
#include "VoxelWorld.h"
|
||
|
|
#include <cmath>
|
||
|
|
#include <algorithm>
|
||
|
|
|
||
|
|
namespace voxel {
|
||
|
|
|
||
|
|
VoxelWorld::VoxelWorld() {
|
||
|
|
setupDefaultMaterials();
|
||
|
|
}
|
||
|
|
|
||
|
|
VoxelWorld::~VoxelWorld() = default;
|
||
|
|
|
||
|
|
void VoxelWorld::setupDefaultMaterials() {
|
||
|
|
// Material 0: Air (empty, never rendered)
|
||
|
|
// Material 1: Grass
|
||
|
|
materials[1].albedoTextureIndex = 0;
|
||
|
|
materials[1].roughness = 200;
|
||
|
|
materials[1].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||
|
|
// Material 2: Dirt
|
||
|
|
materials[2].albedoTextureIndex = 1;
|
||
|
|
materials[2].roughness = 220;
|
||
|
|
materials[2].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||
|
|
// Material 3: Stone
|
||
|
|
materials[3].albedoTextureIndex = 2;
|
||
|
|
materials[3].roughness = 180;
|
||
|
|
materials[3].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||
|
|
// Material 4: Sand
|
||
|
|
materials[4].albedoTextureIndex = 3;
|
||
|
|
materials[4].roughness = 230;
|
||
|
|
materials[4].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||
|
|
// Material 5: Snow
|
||
|
|
materials[5].albedoTextureIndex = 4;
|
||
|
|
materials[5].roughness = 150;
|
||
|
|
materials[5].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Permutation-based noise (no external dependency) ────────────
|
||
|
|
|
||
|
|
static constexpr int PERM_SIZE = 256;
|
||
|
|
static uint8_t perm[512];
|
||
|
|
static uint32_t permSeed = 0;
|
||
|
|
static bool permInitialized = false;
|
||
|
|
|
||
|
|
static void initPerm(uint32_t seed) {
|
||
|
|
if (permInitialized && permSeed == seed) return;
|
||
|
|
for (int i = 0; i < PERM_SIZE; i++) perm[i] = (uint8_t)i;
|
||
|
|
// Fisher-Yates shuffle with seed
|
||
|
|
uint32_t s = seed;
|
||
|
|
for (int i = PERM_SIZE - 1; i > 0; i--) {
|
||
|
|
s = s * 1664525u + 1013904223u; // LCG
|
||
|
|
int j = s % (i + 1);
|
||
|
|
uint8_t tmp = perm[i];
|
||
|
|
perm[i] = perm[j];
|
||
|
|
perm[j] = tmp;
|
||
|
|
}
|
||
|
|
for (int i = 0; i < 256; i++) perm[i + 256] = perm[i];
|
||
|
|
permSeed = seed;
|
||
|
|
permInitialized = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
static float fade(float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }
|
||
|
|
static float lerp(float a, float b, float t) { return a + t * (b - a); }
|
||
|
|
|
||
|
|
static float grad(int hash, float x, float y, float z) {
|
||
|
|
int h = hash & 15;
|
||
|
|
float u = h < 8 ? x : y;
|
||
|
|
float v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
|
||
|
|
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
|
||
|
|
}
|
||
|
|
|
||
|
|
float VoxelWorld::noise3D(float x, float y, float z) const {
|
||
|
|
initPerm(seed_);
|
||
|
|
int X = (int)std::floor(x) & 255;
|
||
|
|
int Y = (int)std::floor(y) & 255;
|
||
|
|
int Z = (int)std::floor(z) & 255;
|
||
|
|
x -= std::floor(x);
|
||
|
|
y -= std::floor(y);
|
||
|
|
z -= std::floor(z);
|
||
|
|
float u = fade(x), v = fade(y), w = fade(z);
|
||
|
|
|
||
|
|
int A = perm[X] + Y;
|
||
|
|
int AA = perm[A] + Z;
|
||
|
|
int AB = perm[A + 1] + Z;
|
||
|
|
int B = perm[X + 1] + Y;
|
||
|
|
int BA = perm[B] + Z;
|
||
|
|
int BB = perm[B + 1] + Z;
|
||
|
|
|
||
|
|
return lerp(
|
||
|
|
lerp(lerp(grad(perm[AA], x, y, z), grad(perm[BA], x-1, y, z), u),
|
||
|
|
lerp(grad(perm[AB], x, y-1, z), grad(perm[BB], x-1, y-1, z), u), v),
|
||
|
|
lerp(lerp(grad(perm[AA+1], x, y, z-1), grad(perm[BA+1], x-1, y, z-1), u),
|
||
|
|
lerp(grad(perm[AB+1], x, y-1, z-1), grad(perm[BB+1], x-1, y-1, z-1), u), v),
|
||
|
|
w);
|
||
|
|
}
|
||
|
|
|
||
|
|
float VoxelWorld::fbm(float x, float y, float z, int octaves) const {
|
||
|
|
float value = 0.0f;
|
||
|
|
float amplitude = 1.0f;
|
||
|
|
float frequency = 1.0f;
|
||
|
|
float maxVal = 0.0f;
|
||
|
|
for (int i = 0; i < octaves; i++) {
|
||
|
|
value += amplitude * noise3D(x * frequency, y * frequency, z * frequency);
|
||
|
|
maxVal += amplitude;
|
||
|
|
amplitude *= 0.5f;
|
||
|
|
frequency *= 2.0f;
|
||
|
|
}
|
||
|
|
return value / maxVal;
|
||
|
|
}
|
||
|
|
|
||
|
|
void VoxelWorld::generateChunk(Chunk& chunk) {
|
||
|
|
const float scale = 0.02f; // terrain horizontal scale
|
||
|
|
const float heightScale = 64.0f;
|
||
|
|
const float baseHeight = 40.0f;
|
||
|
|
const float caveScale = 0.05f;
|
||
|
|
const float caveThreshold = 0.3f;
|
||
|
|
|
||
|
|
for (int z = 0; z < CHUNK_SIZE; z++) {
|
||
|
|
for (int x = 0; x < CHUNK_SIZE; x++) {
|
||
|
|
// World-space coordinates
|
||
|
|
float wx = (float)(chunk.pos.x * CHUNK_SIZE + x);
|
||
|
|
float wz = (float)(chunk.pos.z * CHUNK_SIZE + z);
|
||
|
|
|
||
|
|
// Heightmap using fBm
|
||
|
|
float height = baseHeight + heightScale * fbm(wx * scale, 0.0f, wz * scale, 5);
|
||
|
|
|
||
|
|
for (int y = 0; y < CHUNK_SIZE; y++) {
|
||
|
|
float wy = (float)(chunk.pos.y * CHUNK_SIZE + y);
|
||
|
|
VoxelData v;
|
||
|
|
|
||
|
|
if (wy > height) {
|
||
|
|
// Air above terrain
|
||
|
|
v = VoxelData();
|
||
|
|
} else {
|
||
|
|
// Cave generation
|
||
|
|
float cave = fbm(wx * caveScale, wy * caveScale, wz * caveScale, 3);
|
||
|
|
if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) {
|
||
|
|
v = VoxelData(); // Cave
|
||
|
|
} else if (wy > height - 1.0f) {
|
||
|
|
// Surface layer: material depends on height
|
||
|
|
if (wy > 90.0f) {
|
||
|
|
v = VoxelData(5); // Snow
|
||
|
|
} else if (wy > 70.0f) {
|
||
|
|
v = VoxelData(3); // Stone
|
||
|
|
} else if (wy < 25.0f) {
|
||
|
|
v = VoxelData(4); // Sand
|
||
|
|
} else {
|
||
|
|
v = VoxelData(1); // Grass
|
||
|
|
}
|
||
|
|
} else if (wy > height - 4.0f) {
|
||
|
|
v = VoxelData(2); // Dirt
|
||
|
|
} else {
|
||
|
|
v = VoxelData(3); // Stone
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
chunk.at(x, y, z) = v;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
chunk.dirty = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void VoxelWorld::generateAround(float cx, float cy, float cz, int radiusChunks) {
|
||
|
|
int ccx = (int)std::floor(cx / CHUNK_SIZE);
|
||
|
|
int ccy = (int)std::floor(cy / CHUNK_SIZE);
|
||
|
|
int ccz = (int)std::floor(cz / CHUNK_SIZE);
|
||
|
|
|
||
|
|
for (int dz = -radiusChunks; dz <= radiusChunks; dz++) {
|
||
|
|
for (int dx = -radiusChunks; dx <= radiusChunks; dx++) {
|
||
|
|
// Y range: only generate chunks that could contain terrain (0 to ~4 chunks high)
|
||
|
|
for (int dy = 0; dy < 8; dy++) {
|
||
|
|
ChunkPos pos = { ccx + dx, dy, ccz + dz };
|
||
|
|
if (chunks_.find(pos) == chunks_.end()) {
|
||
|
|
auto chunk = std::make_unique<Chunk>();
|
||
|
|
chunk->pos = pos;
|
||
|
|
generateChunk(*chunk);
|
||
|
|
chunks_[pos] = std::move(chunk);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Chunk* VoxelWorld::getChunk(const ChunkPos& pos) {
|
||
|
|
auto it = chunks_.find(pos);
|
||
|
|
return it != chunks_.end() ? it->second.get() : nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Chunk* VoxelWorld::getChunk(const ChunkPos& pos) const {
|
||
|
|
auto it = chunks_.find(pos);
|
||
|
|
return it != chunks_.end() ? it->second.get() : nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
VoxelData VoxelWorld::getVoxel(int wx, int wy, int wz) const {
|
||
|
|
// Integer floor division that works for negatives
|
||
|
|
auto floorDiv = [](int a, int b) -> int {
|
||
|
|
return (a >= 0) ? (a / b) : ((a - b + 1) / b);
|
||
|
|
};
|
||
|
|
auto floorMod = [](int a, int b) -> int {
|
||
|
|
int r = a % b;
|
||
|
|
return (r < 0) ? r + b : r;
|
||
|
|
};
|
||
|
|
|
||
|
|
ChunkPos cp = {
|
||
|
|
floorDiv(wx, CHUNK_SIZE),
|
||
|
|
floorDiv(wy, CHUNK_SIZE),
|
||
|
|
floorDiv(wz, CHUNK_SIZE)
|
||
|
|
};
|
||
|
|
const Chunk* chunk = getChunk(cp);
|
||
|
|
if (!chunk) return VoxelData();
|
||
|
|
|
||
|
|
return chunk->at(
|
||
|
|
floorMod(wx, CHUNK_SIZE),
|
||
|
|
floorMod(wy, CHUNK_SIZE),
|
||
|
|
floorMod(wz, CHUNK_SIZE)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void VoxelWorld::setVoxel(int wx, int wy, int wz, VoxelData v) {
|
||
|
|
auto floorDiv = [](int a, int b) -> int {
|
||
|
|
return (a >= 0) ? (a / b) : ((a - b + 1) / b);
|
||
|
|
};
|
||
|
|
auto floorMod = [](int a, int b) -> int {
|
||
|
|
int r = a % b;
|
||
|
|
return (r < 0) ? r + b : r;
|
||
|
|
};
|
||
|
|
|
||
|
|
ChunkPos cp = {
|
||
|
|
floorDiv(wx, CHUNK_SIZE),
|
||
|
|
floorDiv(wy, CHUNK_SIZE),
|
||
|
|
floorDiv(wz, CHUNK_SIZE)
|
||
|
|
};
|
||
|
|
Chunk* chunk = getChunk(cp);
|
||
|
|
if (!chunk) return;
|
||
|
|
|
||
|
|
chunk->at(
|
||
|
|
floorMod(wx, CHUNK_SIZE),
|
||
|
|
floorMod(wy, CHUNK_SIZE),
|
||
|
|
floorMod(wz, CHUNK_SIZE)
|
||
|
|
) = v;
|
||
|
|
chunk->dirty = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void VoxelWorld::generateDebug() {
|
||
|
|
chunks_.clear();
|
||
|
|
|
||
|
|
// Create a single chunk at origin
|
||
|
|
ChunkPos cp = {0, 0, 0};
|
||
|
|
auto chunk = std::make_unique<Chunk>();
|
||
|
|
chunk->pos = cp;
|
||
|
|
std::memset(chunk->voxels, 0, sizeof(chunk->voxels));
|
||
|
|
|
||
|
|
VoxelData stone(3); // material 3 = stone
|
||
|
|
|
||
|
|
// Block 1: single isolated block at (5, 5, 5)
|
||
|
|
// → should show all 6 faces
|
||
|
|
chunk->at(5, 5, 5) = stone;
|
||
|
|
|
||
|
|
// Block 2: 2x1x1 bar at (12, 5, 5) along X
|
||
|
|
// → internal faces should be culled
|
||
|
|
chunk->at(12, 5, 5) = stone;
|
||
|
|
chunk->at(13, 5, 5) = stone;
|
||
|
|
|
||
|
|
// Block 3: L-shape at (5, 5, 12)
|
||
|
|
chunk->at(5, 5, 12) = stone;
|
||
|
|
chunk->at(6, 5, 12) = stone;
|
||
|
|
chunk->at(5, 5, 13) = stone;
|
||
|
|
|
||
|
|
// Block 4: 3-high column at (12, 5, 12)
|
||
|
|
chunk->at(12, 5, 12) = stone;
|
||
|
|
chunk->at(12, 6, 12) = stone;
|
||
|
|
chunk->at(12, 7, 12) = stone;
|
||
|
|
|
||
|
|
// Block 5: single block at (20, 5, 5) with material 1 (grass)
|
||
|
|
chunk->at(20, 5, 5) = VoxelData(1);
|
||
|
|
|
||
|
|
chunk->dirty = true;
|
||
|
|
chunks_[cp] = std::move(chunk);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace voxel
|