#include "VoxelWorld.h" #include "wiJobSystem.h" #include #include 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, float timeOffset) { 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; // Animation mode: fewer octaves + skip caves (much faster for 20Hz regen) const bool animating = (timeOffset != 0.0f); const int heightOctaves = animating ? 2 : 5; 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 — timeOffset shifts the Y coord of the noise // to create a rolling wave effect across the terrain float height = baseHeight + heightScale * fbm(wx * scale, timeOffset, wz * scale, heightOctaves); 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 if (!animating) { // Cave generation (only for initial generation, too costly for animation) 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) { if (wy > 90.0f) v = VoxelData(5); else if (wy > 70.0f) v = VoxelData(3); else if (wy < 25.0f) v = VoxelData(4); else v = VoxelData(1); } else if (wy > height - 4.0f) { v = VoxelData(2); } else { v = VoxelData(3); } } else { // Animation path: simplified material assignment (no caves) if (wy > height - 1.0f) { if (wy > 90.0f) v = VoxelData(5); else if (wy > 70.0f) v = VoxelData(3); else if (wy < 25.0f) v = VoxelData(4); else v = VoxelData(1); } else if (wy > height - 4.0f) { v = VoxelData(2); } else { v = VoxelData(3); } } chunk.at(x, y, z) = v; } } } chunk.dirty = true; } void VoxelWorld::regenerateAnimated(float time, uint32_t* packDst, uint32_t packDstCapacity) { // Regenerate all existing chunks with time-shifted noise (wave effect) // Parallelized across all CPU cores via wi::jobsystem float timeOffset = time * 0.1f; // Collect chunk pointers for indexed access (hashmap isn't index-friendly) std::vector chunkPtrs; chunkPtrs.reserve(chunks_.size()); for (auto& [pos, chunk] : chunks_) { chunkPtrs.push_back(chunk.get()); } const uint32_t wordsPerChunk = CHUNK_VOLUME / 2; // 16384 wi::jobsystem::context ctx; wi::jobsystem::Dispatch(ctx, (uint32_t)chunkPtrs.size(), 1, [&chunkPtrs, timeOffset, packDst, packDstCapacity, wordsPerChunk, this](wi::jobsystem::JobArgs args) { generateChunk(*chunkPtrs[args.jobIndex], timeOffset); // Fused pack: memcpy voxel data into GPU staging cache if (packDst) { uint32_t offset = args.jobIndex * wordsPerChunk; if (offset + wordsPerChunk <= packDstCapacity) { std::memcpy(packDst + offset, chunkPtrs[args.jobIndex]->voxels, wordsPerChunk * sizeof(uint32_t)); } } }); wi::jobsystem::Wait(ctx); } 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->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->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