// VoxelGrid.cpp - Dense 3D voxel array implementation // Part of McRogueFace 3D Extension - Milestones 9-11 #include "VoxelGrid.h" #include "VoxelMesher.h" #include "MeshLayer.h" // For MeshVertex #include #include #include // For memcpy, memcmp #include // For file I/O namespace mcrf { // Static air material for out-of-bounds or ID=0 queries static VoxelMaterial airMaterial{"air", sf::Color::Transparent, -1, true, 0.0f}; // ============================================================================= // Constructor // ============================================================================= VoxelGrid::VoxelGrid(int w, int h, int d, float cellSize) : width_(w), height_(h), depth_(d), cellSize_(cellSize), offset_(0, 0, 0), rotation_(0.0f) { if (w <= 0 || h <= 0 || d <= 0) { throw std::invalid_argument("VoxelGrid dimensions must be positive"); } if (cellSize <= 0.0f) { throw std::invalid_argument("VoxelGrid cell size must be positive"); } // Allocate dense array, initialized to air (0) size_t totalSize = static_cast(w) * h * d; data_.resize(totalSize, 0); } // ============================================================================= // Per-voxel access // ============================================================================= bool VoxelGrid::isValid(int x, int y, int z) const { return x >= 0 && x < width_ && y >= 0 && y < height_ && z >= 0 && z < depth_; } uint8_t VoxelGrid::get(int x, int y, int z) const { if (!isValid(x, y, z)) { return 0; // Out of bounds returns air } return data_[index(x, y, z)]; } void VoxelGrid::set(int x, int y, int z, uint8_t material) { if (!isValid(x, y, z)) { return; // Out of bounds is no-op } data_[index(x, y, z)] = material; meshDirty_ = true; } // ============================================================================= // Material palette // ============================================================================= uint8_t VoxelGrid::addMaterial(const VoxelMaterial& mat) { if (materials_.size() >= 255) { throw std::runtime_error("Material palette full (max 255 materials)"); } materials_.push_back(mat); return static_cast(materials_.size()); // 1-indexed } uint8_t VoxelGrid::addMaterial(const std::string& name, sf::Color color, int spriteIndex, bool transparent, float pathCost) { return addMaterial(VoxelMaterial(name, color, spriteIndex, transparent, pathCost)); } const VoxelMaterial& VoxelGrid::getMaterial(uint8_t id) const { if (id == 0 || id > materials_.size()) { return airMaterial; } return materials_[id - 1]; // 1-indexed, so ID 1 = materials_[0] } // ============================================================================= // Bulk operations // ============================================================================= void VoxelGrid::fill(uint8_t material) { std::fill(data_.begin(), data_.end(), material); meshDirty_ = true; } // ============================================================================= // Transform // ============================================================================= mat4 VoxelGrid::getModelMatrix() const { // Apply translation first, then rotation around Y axis mat4 translation = mat4::translate(offset_); mat4 rotation = mat4::rotateY(rotation_ * DEG_TO_RAD); return translation * rotation; } // ============================================================================= // Statistics // ============================================================================= size_t VoxelGrid::countNonAir() const { size_t count = 0; for (uint8_t v : data_) { if (v != 0) { count++; } } return count; } size_t VoxelGrid::countMaterial(uint8_t material) const { size_t count = 0; for (uint8_t v : data_) { if (v == material) { count++; } } return count; } // ============================================================================= // fillBox (Milestone 10) // ============================================================================= void VoxelGrid::fillBox(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material) { // Ensure proper ordering (min to max) if (x0 > x1) std::swap(x0, x1); if (y0 > y1) std::swap(y0, y1); if (z0 > z1) std::swap(z0, z1); // Clamp to valid range x0 = std::max(0, std::min(x0, width_ - 1)); x1 = std::max(0, std::min(x1, width_ - 1)); y0 = std::max(0, std::min(y0, height_ - 1)); y1 = std::max(0, std::min(y1, height_ - 1)); z0 = std::max(0, std::min(z0, depth_ - 1)); z1 = std::max(0, std::min(z1, depth_ - 1)); for (int z = z0; z <= z1; z++) { for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { data_[index(x, y, z)] = material; } } } meshDirty_ = true; } // ============================================================================= // Bulk Operations - Milestone 11 // ============================================================================= void VoxelGrid::fillBoxHollow(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material, int thickness) { // Ensure proper ordering (min to max) if (x0 > x1) std::swap(x0, x1); if (y0 > y1) std::swap(y0, y1); if (z0 > z1) std::swap(z0, z1); // Fill entire box with material fillBox(x0, y0, z0, x1, y1, z1, material); // Carve out interior (inset by thickness on all sides) int ix0 = x0 + thickness; int iy0 = y0 + thickness; int iz0 = z0 + thickness; int ix1 = x1 - thickness; int iy1 = y1 - thickness; int iz1 = z1 - thickness; // Only carve if there's interior space if (ix0 <= ix1 && iy0 <= iy1 && iz0 <= iz1) { fillBox(ix0, iy0, iz0, ix1, iy1, iz1, 0); // Air } // meshDirty_ already set by fillBox calls } void VoxelGrid::fillSphere(int cx, int cy, int cz, int radius, uint8_t material) { int r2 = radius * radius; for (int z = cz - radius; z <= cz + radius; z++) { for (int y = cy - radius; y <= cy + radius; y++) { for (int x = cx - radius; x <= cx + radius; x++) { int dx = x - cx; int dy = y - cy; int dz = z - cz; if (dx * dx + dy * dy + dz * dz <= r2) { if (isValid(x, y, z)) { data_[index(x, y, z)] = material; } } } } } meshDirty_ = true; } void VoxelGrid::fillCylinder(int cx, int cy, int cz, int radius, int height, uint8_t material) { int r2 = radius * radius; for (int y = cy; y < cy + height; y++) { for (int z = cz - radius; z <= cz + radius; z++) { for (int x = cx - radius; x <= cx + radius; x++) { int dx = x - cx; int dz = z - cz; if (dx * dx + dz * dz <= r2) { if (isValid(x, y, z)) { data_[index(x, y, z)] = material; } } } } } meshDirty_ = true; } // Simple 3D noise implementation (hash-based, similar to value noise) namespace { // Simple hash function for noise inline unsigned int hash3D(int x, int y, int z, unsigned int seed) { unsigned int h = seed; h ^= static_cast(x) * 374761393u; h ^= static_cast(y) * 668265263u; h ^= static_cast(z) * 2147483647u; h = (h ^ (h >> 13)) * 1274126177u; return h; } // Convert hash to 0-1 float inline float hashToFloat(unsigned int h) { return static_cast(h & 0xFFFFFF) / static_cast(0xFFFFFF); } // Linear interpolation inline float lerp(float a, float b, float t) { return a + t * (b - a); } // Smoothstep for smoother interpolation inline float smoothstep(float t) { return t * t * (3.0f - 2.0f * t); } // 3D value noise float noise3D(float x, float y, float z, unsigned int seed) { int xi = static_cast(std::floor(x)); int yi = static_cast(std::floor(y)); int zi = static_cast(std::floor(z)); float xf = x - xi; float yf = y - yi; float zf = z - zi; // Smoothstep the fractions float u = smoothstep(xf); float v = smoothstep(yf); float w = smoothstep(zf); // Hash corners of the unit cube float c000 = hashToFloat(hash3D(xi, yi, zi, seed)); float c100 = hashToFloat(hash3D(xi + 1, yi, zi, seed)); float c010 = hashToFloat(hash3D(xi, yi + 1, zi, seed)); float c110 = hashToFloat(hash3D(xi + 1, yi + 1, zi, seed)); float c001 = hashToFloat(hash3D(xi, yi, zi + 1, seed)); float c101 = hashToFloat(hash3D(xi + 1, yi, zi + 1, seed)); float c011 = hashToFloat(hash3D(xi, yi + 1, zi + 1, seed)); float c111 = hashToFloat(hash3D(xi + 1, yi + 1, zi + 1, seed)); // Trilinear interpolation float x00 = lerp(c000, c100, u); float x10 = lerp(c010, c110, u); float x01 = lerp(c001, c101, u); float x11 = lerp(c011, c111, u); float y0 = lerp(x00, x10, v); float y1 = lerp(x01, x11, v); return lerp(y0, y1, w); } } void VoxelGrid::fillNoise(int x0, int y0, int z0, int x1, int y1, int z1, uint8_t material, float threshold, float scale, unsigned int seed) { // Ensure proper ordering if (x0 > x1) std::swap(x0, x1); if (y0 > y1) std::swap(y0, y1); if (z0 > z1) std::swap(z0, z1); // Clamp to valid range x0 = std::max(0, std::min(x0, width_ - 1)); x1 = std::max(0, std::min(x1, width_ - 1)); y0 = std::max(0, std::min(y0, height_ - 1)); y1 = std::max(0, std::min(y1, height_ - 1)); z0 = std::max(0, std::min(z0, depth_ - 1)); z1 = std::max(0, std::min(z1, depth_ - 1)); for (int z = z0; z <= z1; z++) { for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { float n = noise3D(x * scale, y * scale, z * scale, seed); if (n > threshold) { data_[index(x, y, z)] = material; } } } } meshDirty_ = true; } // ============================================================================= // Copy/Paste Operations - Milestone 11 // ============================================================================= VoxelRegion VoxelGrid::copyRegion(int x0, int y0, int z0, int x1, int y1, int z1) const { // Ensure proper ordering if (x0 > x1) std::swap(x0, x1); if (y0 > y1) std::swap(y0, y1); if (z0 > z1) std::swap(z0, z1); // Clamp to valid range x0 = std::max(0, std::min(x0, width_ - 1)); x1 = std::max(0, std::min(x1, width_ - 1)); y0 = std::max(0, std::min(y0, height_ - 1)); y1 = std::max(0, std::min(y1, height_ - 1)); z0 = std::max(0, std::min(z0, depth_ - 1)); z1 = std::max(0, std::min(z1, depth_ - 1)); int rw = x1 - x0 + 1; int rh = y1 - y0 + 1; int rd = z1 - z0 + 1; VoxelRegion region(rw, rh, rd); for (int rz = 0; rz < rd; rz++) { for (int ry = 0; ry < rh; ry++) { for (int rx = 0; rx < rw; rx++) { int sx = x0 + rx; int sy = y0 + ry; int sz = z0 + rz; size_t ri = static_cast(rz) * (rw * rh) + static_cast(ry) * rw + rx; region.data[ri] = get(sx, sy, sz); } } } return region; } void VoxelGrid::pasteRegion(const VoxelRegion& region, int x, int y, int z, bool skipAir) { if (!region.isValid()) return; for (int rz = 0; rz < region.depth; rz++) { for (int ry = 0; ry < region.height; ry++) { for (int rx = 0; rx < region.width; rx++) { size_t ri = static_cast(rz) * (region.width * region.height) + static_cast(ry) * region.width + rx; uint8_t mat = region.data[ri]; if (skipAir && mat == 0) continue; int dx = x + rx; int dy = y + ry; int dz = z + rz; if (isValid(dx, dy, dz)) { data_[index(dx, dy, dz)] = mat; } } } } meshDirty_ = true; } // ============================================================================= // Navigation Projection - Milestone 12 // ============================================================================= VoxelGrid::NavInfo VoxelGrid::projectColumn(int x, int z, int headroom) const { NavInfo info; info.height = 0.0f; info.walkable = false; info.transparent = true; info.pathCost = 1.0f; // Out of bounds check if (x < 0 || x >= width_ || z < 0 || z >= depth_) { return info; } // Scan from top to bottom, find first solid with air above (floor) int floorY = -1; for (int y = height_ - 1; y >= 0; y--) { uint8_t mat = get(x, y, z); if (mat != 0) { // Found solid - check if it's a floor (air above) or ceiling bool hasAirAbove = (y == height_ - 1) || (get(x, y + 1, z) == 0); if (hasAirAbove) { floorY = y; break; } } } if (floorY >= 0) { // Found a floor info.height = (floorY + 1) * cellSize_; // Top of floor voxel info.walkable = true; // Check headroom (need enough air voxels above floor) int airCount = 0; for (int y = floorY + 1; y < height_; y++) { if (get(x, y, z) == 0) { airCount++; } else { break; } } if (airCount < headroom) { info.walkable = false; // Can't fit entity } // Get path cost from floor material uint8_t floorMat = get(x, floorY, z); info.pathCost = getMaterial(floorMat).pathCost; } // Check transparency: any non-transparent solid in column blocks FOV for (int y = 0; y < height_; y++) { uint8_t mat = get(x, y, z); if (mat != 0 && !getMaterial(mat).transparent) { info.transparent = false; break; } } return info; } // ============================================================================= // Mesh Caching (Milestone 10) // ============================================================================= const std::vector& VoxelGrid::getVertices() const { if (meshDirty_) { rebuildMesh(); } return cachedVertices_; } void VoxelGrid::rebuildMesh() const { cachedVertices_.clear(); if (greedyMeshing_) { VoxelMesher::generateGreedyMesh(*this, cachedVertices_); } else { VoxelMesher::generateMesh(*this, cachedVertices_); } meshDirty_ = false; } // ============================================================================= // Serialization - Milestone 14 // ============================================================================= // File format: // Magic "MCVG" (4 bytes) // Version (1 byte) - currently 1 // Width, Height, Depth (3 x int32 = 12 bytes) // Cell Size (float32 = 4 bytes) // Material count (uint8 = 1 byte) // For each material: // Name length (uint16) + name bytes // Color RGBA (4 bytes) // Sprite index (int32) // Transparent (uint8) // Path cost (float32) // Voxel data length (uint32) // Voxel data: RLE encoded (run_length: uint8, material: uint8) pairs // If run_length == 255, read extended_length: uint16 for longer runs namespace { const char MAGIC[4] = {'M', 'C', 'V', 'G'}; const uint8_t FORMAT_VERSION = 1; // Write helpers void writeU8(std::vector& buf, uint8_t v) { buf.push_back(v); } void writeU16(std::vector& buf, uint16_t v) { buf.push_back(static_cast(v & 0xFF)); buf.push_back(static_cast((v >> 8) & 0xFF)); } void writeI32(std::vector& buf, int32_t v) { buf.push_back(static_cast(v & 0xFF)); buf.push_back(static_cast((v >> 8) & 0xFF)); buf.push_back(static_cast((v >> 16) & 0xFF)); buf.push_back(static_cast((v >> 24) & 0xFF)); } void writeU32(std::vector& buf, uint32_t v) { buf.push_back(static_cast(v & 0xFF)); buf.push_back(static_cast((v >> 8) & 0xFF)); buf.push_back(static_cast((v >> 16) & 0xFF)); buf.push_back(static_cast((v >> 24) & 0xFF)); } void writeF32(std::vector& buf, float v) { static_assert(sizeof(float) == 4, "Expected 4-byte float"); const uint8_t* bytes = reinterpret_cast(&v); buf.insert(buf.end(), bytes, bytes + 4); } void writeString(std::vector& buf, const std::string& s) { uint16_t len = static_cast(std::min(s.size(), size_t(65535))); writeU16(buf, len); buf.insert(buf.end(), s.begin(), s.begin() + len); } // Read helpers class Reader { const uint8_t* data_; size_t size_; size_t pos_; public: Reader(const uint8_t* data, size_t size) : data_(data), size_(size), pos_(0) {} bool hasBytes(size_t n) const { return pos_ + n <= size_; } size_t position() const { return pos_; } bool readU8(uint8_t& v) { if (!hasBytes(1)) return false; v = data_[pos_++]; return true; } bool readU16(uint16_t& v) { if (!hasBytes(2)) return false; v = static_cast(data_[pos_]) | (static_cast(data_[pos_ + 1]) << 8); pos_ += 2; return true; } bool readI32(int32_t& v) { if (!hasBytes(4)) return false; v = static_cast(data_[pos_]) | (static_cast(data_[pos_ + 1]) << 8) | (static_cast(data_[pos_ + 2]) << 16) | (static_cast(data_[pos_ + 3]) << 24); pos_ += 4; return true; } bool readU32(uint32_t& v) { if (!hasBytes(4)) return false; v = static_cast(data_[pos_]) | (static_cast(data_[pos_ + 1]) << 8) | (static_cast(data_[pos_ + 2]) << 16) | (static_cast(data_[pos_ + 3]) << 24); pos_ += 4; return true; } bool readF32(float& v) { if (!hasBytes(4)) return false; static_assert(sizeof(float) == 4, "Expected 4-byte float"); std::memcpy(&v, data_ + pos_, 4); pos_ += 4; return true; } bool readString(std::string& s) { uint16_t len; if (!readU16(len)) return false; if (!hasBytes(len)) return false; s.assign(reinterpret_cast(data_ + pos_), len); pos_ += len; return true; } bool readBytes(uint8_t* out, size_t n) { if (!hasBytes(n)) return false; std::memcpy(out, data_ + pos_, n); pos_ += n; return true; } }; // RLE encode voxel data void rleEncode(const std::vector& data, std::vector& out) { if (data.empty()) return; size_t i = 0; while (i < data.size()) { uint8_t mat = data[i]; size_t runStart = i; // Count consecutive same materials while (i < data.size() && data[i] == mat && (i - runStart) < 65535 + 255) { i++; } size_t runLen = i - runStart; if (runLen < 255) { writeU8(out, static_cast(runLen)); } else { // Extended run: 255 marker + uint16 length writeU8(out, 255); writeU16(out, static_cast(runLen - 255)); } writeU8(out, mat); } } // RLE decode voxel data bool rleDecode(Reader& reader, std::vector& data, size_t expectedSize) { data.clear(); data.reserve(expectedSize); while (data.size() < expectedSize) { uint8_t runLen8; if (!reader.readU8(runLen8)) return false; size_t runLen = runLen8; if (runLen8 == 255) { uint16_t extLen; if (!reader.readU16(extLen)) return false; runLen = 255 + extLen; } uint8_t mat; if (!reader.readU8(mat)) return false; for (size_t j = 0; j < runLen && data.size() < expectedSize; j++) { data.push_back(mat); } } return data.size() == expectedSize; } } bool VoxelGrid::saveToBuffer(std::vector& buffer) const { buffer.clear(); buffer.reserve(1024 + data_.size()); // Rough estimate // Magic buffer.insert(buffer.end(), MAGIC, MAGIC + 4); // Version writeU8(buffer, FORMAT_VERSION); // Dimensions writeI32(buffer, width_); writeI32(buffer, height_); writeI32(buffer, depth_); // Cell size writeF32(buffer, cellSize_); // Materials writeU8(buffer, static_cast(materials_.size())); for (const auto& mat : materials_) { writeString(buffer, mat.name); writeU8(buffer, mat.color.r); writeU8(buffer, mat.color.g); writeU8(buffer, mat.color.b); writeU8(buffer, mat.color.a); writeI32(buffer, mat.spriteIndex); writeU8(buffer, mat.transparent ? 1 : 0); writeF32(buffer, mat.pathCost); } // RLE encode voxel data std::vector rleData; rleEncode(data_, rleData); // Write RLE data length and data writeU32(buffer, static_cast(rleData.size())); buffer.insert(buffer.end(), rleData.begin(), rleData.end()); return true; } bool VoxelGrid::loadFromBuffer(const uint8_t* data, size_t size) { Reader reader(data, size); // Check magic uint8_t magic[4]; if (!reader.readBytes(magic, 4)) return false; if (std::memcmp(magic, MAGIC, 4) != 0) return false; // Check version uint8_t version; if (!reader.readU8(version)) return false; if (version != FORMAT_VERSION) return false; // Read dimensions int32_t w, h, d; if (!reader.readI32(w) || !reader.readI32(h) || !reader.readI32(d)) return false; if (w <= 0 || h <= 0 || d <= 0) return false; // Read cell size float cs; if (!reader.readF32(cs)) return false; if (cs <= 0.0f) return false; // Read materials uint8_t matCount; if (!reader.readU8(matCount)) return false; std::vector newMaterials; newMaterials.reserve(matCount); for (uint8_t i = 0; i < matCount; i++) { VoxelMaterial mat; if (!reader.readString(mat.name)) return false; uint8_t r, g, b, a; if (!reader.readU8(r) || !reader.readU8(g) || !reader.readU8(b) || !reader.readU8(a)) return false; mat.color = sf::Color(r, g, b, a); int32_t sprite; if (!reader.readI32(sprite)) return false; mat.spriteIndex = sprite; uint8_t transp; if (!reader.readU8(transp)) return false; mat.transparent = (transp != 0); if (!reader.readF32(mat.pathCost)) return false; newMaterials.push_back(mat); } // Read RLE data length uint32_t rleLen; if (!reader.readU32(rleLen)) return false; // Decode voxel data size_t expectedVoxels = static_cast(w) * h * d; std::vector newData; if (!rleDecode(reader, newData, expectedVoxels)) return false; // Success - update the grid width_ = w; height_ = h; depth_ = d; cellSize_ = cs; materials_ = std::move(newMaterials); data_ = std::move(newData); meshDirty_ = true; return true; } bool VoxelGrid::save(const std::string& path) const { std::vector buffer; if (!saveToBuffer(buffer)) return false; std::ofstream file(path, std::ios::binary); if (!file) return false; file.write(reinterpret_cast(buffer.data()), buffer.size()); return file.good(); } bool VoxelGrid::load(const std::string& path) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return false; std::streamsize size = file.tellg(); if (size <= 0) return false; file.seekg(0, std::ios::beg); std::vector buffer(static_cast(size)); if (!file.read(reinterpret_cast(buffer.data()), size)) return false; return loadFromBuffer(buffer.data(), buffer.size()); } } // namespace mcrf