diff --git a/tests/worldbox.cpp b/tests/worldbox.cpp index fe7e61d..1cbc1b3 100644 --- a/tests/worldbox.cpp +++ b/tests/worldbox.cpp @@ -80,6 +80,7 @@ public: // Update simulation objects like the Star sim.updateStar(deltaTime); + sim.updateWeatherAndPhysics(deltaTime); ImGui::Begin("WorldBox Simulation"); if (ImGui::BeginTable("MainLayout", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter)) { @@ -140,6 +141,34 @@ public: } } + if (ImGui::CollapsingHeader("Weather & Physics", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox("Enable Gravity (Terrain)", &sim.config.enableGravity); + ImGui::DragFloat3("Gravity", sim.config.gravity.data()); + ImGui::DragFloat3("Wind", sim.config.wind.data()); + ImGui::DragFloat("Physics Step (sec)", &sim.config.physicsStep, 0.01f, 0.01f, 1.0f); + + ImGui::Separator(); + ImGui::Text("Clouds & Rain"); + ImGui::DragInt("Cloud Count", &sim.config.cloudCount, 1, 0, 100); + ImGui::DragFloat("Cloud Height", &sim.config.cloudHeight, 5.0f, 10.0f, 1000.0f); + ImGui::DragFloat("Rain Spawn Rate", &sim.config.rainSpawnRate, 0.1f, 0.0f, 50.0f); + ImGui::ColorEdit3("Cloud Color", sim.config.cloudColor.data()); + ImGui::ColorEdit3("Rain Color", sim.config.rainColor.data()); + + if (ImGui::Button("Generate Clouds", ImVec2(-1, 40))) { + sim.generateClouds(); + applyDebugColorMode(); + statsNeedUpdate = true; + } + if (ImGui::Button("Clear Weather", ImVec2(-1, 30))) { + for (auto& c : sim.clouds) sim.grid.remove(c.pos); + for (auto& r : sim.rainDrops) sim.grid.remove(r.pos); + sim.clouds.clear(); + sim.rainDrops.clear(); + statsNeedUpdate = true; + } + } + if (ImGui::CollapsingHeader("Environment & Celestial", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("Star Settings"); ImGui::Checkbox("Enable Star Rotation", &sim.config.enableStarRotation); @@ -246,10 +275,21 @@ public: } case DebugColorMode::BASE: default: - if (p->data.type == 1) color = sim.config.baseRockColor; - else if (p->data.type == 2) color = sim.config.grassColorBase; - else if (p->data.type == 3) color = sim.config.starColor; - else color = sim.config.baseDirtColor; + if (p->data.type == 0) { + v3 darkDirt = sim.config.baseDirtColor * 0.4f; + color = sim.config.baseDirtColor * (1.0f - p->data.moisture) + darkDirt * p->data.moisture; + } else if (p->data.type == 1) { + color = sim.config.baseRockColor; + } else if (p->data.type == 2) { + v3 lushGrass = sim.config.grassColorBase * 1.5f; + color = sim.config.grassColorBase * (1.0f - p->data.moisture) + lushGrass * p->data.moisture; + } else if (p->data.type == 3) { + color = sim.config.starColor; + } else if (p->data.type == 4) { + color = sim.config.cloudColor; + } else if (p->data.type == 5) { + color = sim.config.rainColor; + } break; } diff --git a/util/grid/grid3eigen.hpp b/util/grid/grid3eigen.hpp index 324b53a..665ca38 100644 --- a/util/grid/grid3eigen.hpp +++ b/util/grid/grid3eigen.hpp @@ -89,6 +89,13 @@ public: } }; + struct RaycastHit { + std::shared_ptr node; + float distance; + PointType normal; + PointType hitPoint; + }; + struct OctreeNode { BoundingBox bounds; std::vector> points; @@ -634,6 +641,89 @@ private: } } + void insertHit(std::vector& hits, size_t maxHits, const std::shared_ptr& node, + float t, const PointType& normal, const PointType& hitPoint, float& maxDist) const { + for (const auto& h : hits) { + if (h.node == node) return; + } + + auto it = std::lower_bound(hits.begin(), hits.end(), t, + [](const RaycastHit& a, float val) { + return a.distance < val; + }); + + hits.insert(it, {node, t, normal, hitPoint}); + + if (hits.size() > maxHits) { + hits.pop_back(); + } + + if (hits.size() == maxHits) { + maxDist = std::min(maxDist, hits.back().distance); + } + } + + void voxelTraverseMultipleRecursive(OctreeNode* node, float tMin, float tMax, float& maxDist, bool enableLOD, + const Ray& ray, std::vector& hits, size_t maxHits, float invLodf) const { + if (enableLOD && !node->isLeaf) { + float dist = (node->center - ray.origin).norm(); + float ratio = dist / node->nodeSize; + + if (dist > lodMinDistance_ && ratio > invLodf && node->lodData) { + float t; + PointType n; + PointType h; + if (rayCubeIntersect(ray, node->lodData.get(), t, n, h)) { + if (t >= 0 && t <= maxDist) { + insertHit(hits, maxHits, node->lodData, t, n, h, maxDist); + } + } + return; + } + } + + for (const auto& pointData : node->points) { + if (!pointData->active) continue; + + float t; + PointType normal, hitPoint; + if (rayCubeIntersect(ray, pointData.get(), t, normal, hitPoint)) { + if (t >= 0 && t <= maxDist && t <= tMax + 0.001f) { + insertHit(hits, maxHits, pointData, t, normal, hitPoint, maxDist); + } + } + } + + if (node->isLeaf) return; + + // DDA Traversal + PointType center = node->center; + Eigen::Vector3f ttt = (center - ray.origin).cwiseProduct(ray.invDir); + + int currIdx = 0; + currIdx = ((tMin >= ttt.x()) ? 1 : 0) | ((tMin >= ttt.y()) ? 2 : 0) | ((tMin >= ttt.z()) ? 4 : 0); + + float tNext; + + while(tMin < tMax && tMin <= maxDist) { + Eigen::Vector3f next_t; + next_t[0] = (currIdx & 1) ? tMax : ttt[0]; + next_t[1] = (currIdx & 2) ? tMax : ttt[1]; + next_t[2] = (currIdx & 4) ? tMax : ttt[2]; + + tNext = next_t.minCoeff(); + + int physIdx = currIdx ^ ray.signMask; + + if (node->children[physIdx]) { + voxelTraverseMultipleRecursive(node->children[physIdx].get(), tMin, tNext, maxDist, enableLOD, ray, hits, maxHits, invLodf); + } + + tMin = tNext; + currIdx |= ((next_t[0] <= tNext) ? 1 : 0) | ((next_t[1] <= tNext) ? 2 : 0) | ((next_t[2] <= tNext) ? 4 : 0); + } + } + PointType sampleGGX(const PointType& n, float roughness, uint32_t& state) const { float alpha = std::max(EPSILON, roughness * roughness); float r1 = float(rand_r(&state)) / float(RAND_MAX); @@ -2280,6 +2370,11 @@ public: size = 0; } + + void collectNodesByObjectId(int id, std::vector>& results) const { + std::unordered_set> seen; + collectNodesByObjectIdRecursive(root_.get(), id, results, seen); + } }; #endif diff --git a/util/sim/worldbox.hpp b/util/sim/worldbox.hpp index d0a2f0f..65cb370 100644 --- a/util/sim/worldbox.hpp +++ b/util/sim/worldbox.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../grid/grid3eigen.hpp" #include "../timing_decorator.cpp" @@ -17,7 +18,7 @@ using v3 = Eigen::Vector3f; struct WorldVoxel { float nutrients = 1.0f; float moisture = 0.5f; - int type = 0; + int type = 0; // 0=Dirt, 1=Rock, 2=Grass, 3=Star, 4=Cloud, 5=Rain WorldVoxel() = default; @@ -48,6 +49,32 @@ struct WorldBoxConfig { v3 starColor = v3(1.0f, 0.95f, 0.8f); float starSpeed = 0.2f; // Radians per second float starAngle = 0.0f; + + // Weather Config + int cloudCount = 15; + float cloudHeight = 150.0f; + v3 cloudColor = v3(0.9f, 0.9f, 0.95f); + float cloudBaseSize = 6.0f; + + v3 rainColor = v3(0.2f, 0.4f, 0.9f); + float rainDropSize = 0.5f; + float rainSpawnRate = 1.0f; + + // Physics Config + bool enableGravity = true; + v3 gravity = v3(0.0f, -60.0f, 0.0f); + v3 wind = v3(20.0f, 0.0f, 10.0f); + float physicsStep = 0.1f; +}; + +struct CloudVoxel { + v3 pos; + float size; +}; + +struct RainDrop { + v3 pos; + v3 vel; }; class worldboxsim { @@ -56,6 +83,10 @@ public: Octree grid; std::mt19937 rng; std::vector starVoxelPositions; + + std::vector clouds; + std::vector rainDrops; + float physicsTimer = 0.0f; worldboxsim() : rng(42) { config = WorldBoxConfig(); @@ -64,12 +95,10 @@ public: } void updateStar(float dt) { - // If turned off, despawn current star if it exists if (!config.enableStarRotation) { if (!starVoxelPositions.empty()) { - WorldVoxel emptyVoxel; for(const auto& pos : starVoxelPositions) { - grid.set(emptyVoxel, pos, false, v3(0,0,0), config.starVoxelSize, false, 0, 0); + grid.remove(pos); } starVoxelPositions.clear(); } @@ -80,17 +109,10 @@ public: config.starAngle += dt * config.starSpeed; if (config.starAngle > 2 * M_PI) config.starAngle -= 2 * M_PI; - // 1. Erase the old star voxels - WorldVoxel emptyVoxel; - for(const auto& pos : starVoxelPositions) { - grid.set(emptyVoxel, pos, false, v3(0,0,0), config.starVoxelSize, false, 0, 0); - } - starVoxelPositions.clear(); - - // 2. Calculate new center of star (orbiting on the X/Y plane) + // Calculate new center of star (orbiting on the X/Y plane) v3 starCenter(cos(config.starAngle) * config.starOrbitRadius, sin(config.starAngle) * config.starOrbitRadius, 0.0f); - // 3. Create a flat panel facing the origin + // Create a flat panel facing the origin v3 n = -starCenter.normalized(); v3 worldUp(0, 1, 0); if (std::abs(n.dot(worldUp)) > 0.99f) worldUp = v3(0, 0, 1); @@ -100,12 +122,181 @@ public: int halfGrid = std::max(1, static_cast((config.starPanelSize / config.starVoxelSize) / 2.0f)); WorldVoxel starVoxel(0.0f, 0.0f, 3); // Type 3 = Star + // Calculate the new ideal positions for this frame + std::vector newPositions; + newPositions.reserve((2 * halfGrid + 1) * (2 * halfGrid + 1)); + for (int i = -halfGrid; i <= halfGrid; ++i) { for (int j = -halfGrid; j <= halfGrid; ++j) { - v3 pos = starCenter + (right * (i * config.starVoxelSize)) + (up * (j * config.starVoxelSize)); - // Add star voxel with high material/emission flag - grid.set(starVoxel, pos, true, config.starColor, config.starVoxelSize, true, 1, 1); - starVoxelPositions.push_back(pos); + newPositions.push_back(starCenter + (right * (i * config.starVoxelSize)) + (up * (j * config.starVoxelSize))); + } + } + + // Apply grid changes + if (starVoxelPositions.empty()) { + // Creation: Spawn voxels into the grid for the first time + for (const auto& pos : newPositions) { + // Injecting a high emittance factor (15.0f) to make it a bright emissive light source + grid.set(starVoxel, pos, true, config.starColor, config.starVoxelSize, true, 1, 1, 15.0f); + } + starVoxelPositions = newPositions; + } else if (starVoxelPositions.size() == newPositions.size()) { + // Moving: Using grid.move() to smoothly transfer nodes in the Octree + for (size_t i = 0; i < starVoxelPositions.size(); ++i) { + grid.move(starVoxelPositions[i], newPositions[i]); + } + starVoxelPositions = newPositions; + } + } + + void generateClouds() { + std::uniform_real_distribution randX(-config.worldSizeX/2, config.worldSizeX/2); + std::uniform_real_distribution randZ(-config.worldSizeZ/2, config.worldSizeZ/2); + std::uniform_real_distribution randY(config.cloudHeight - 10.0f, config.cloudHeight + 10.0f); + + for (int i=0; i nextClouds; + for (auto& c : clouds) { + v3 nextPos = c.pos + config.wind * dt; + + // Screen wrap logic for wind drift + if (nextPos.x() > halfX) nextPos.x() -= config.worldSizeX; + if (nextPos.x() < -halfX) nextPos.x() += config.worldSizeX; + if (nextPos.z() > halfZ) nextPos.z() -= config.worldSizeZ; + if (nextPos.z() < -halfZ) nextPos.z() += config.worldSizeZ; + + if (grid.move(c.pos, nextPos)) { + c.pos = nextPos; + } else { + WorldVoxel vox(0.0f, 1.0f, 4); + grid.set(vox, nextPos, true, config.cloudColor, c.size, true, 4, 0, 0.0f, 1.0f, 0.0f, 0.4f); + c.pos = nextPos; + } + nextClouds.push_back(c); + + // Spawn Rain + std::uniform_real_distribution dist(0, 1); + if (dist(rng) < (config.rainSpawnRate * dt * 0.1f)) { + RainDrop r = {c.pos - v3(0, c.size, 0), config.wind}; + rainDrops.push_back(r); + WorldVoxel rv(0.0f, 1.0f, 5); // Type 5 = Rain + grid.set(rv, r.pos, true, config.rainColor, config.rainDropSize, true, 5); + } + } + clouds = nextClouds; + + // 2. Rain Update + std::vector nextRain; + for (auto& r : rainDrops) { + r.vel += config.gravity * dt; + v3 nextPos = r.pos + r.vel * dt; + + v3 dir = (nextPos - r.pos); + float distMag = dir.norm(); + + if (distMag > 0) { + dir.normalize(); + auto hit = grid.voxelTraverse(r.pos, dir, distMag, false); + + // If it hits solid terrain + if (hit && hit->data.type != 4 && hit->data.type != 5) { + if (hit->data.type == 0) { // Hit Dirt + hit->data.moisture = std::min(1.0f, hit->data.moisture + 0.15f); + v3 darkDirt = config.baseDirtColor * 0.4f; + v3 wetColor = config.baseDirtColor * (1.0f - hit->data.moisture) + darkDirt * hit->data.moisture; + grid.setColor(hit->position, wetColor); + } else if (hit->data.type == 2) { // Hit Grass + hit->data.moisture = std::min(1.0f, hit->data.moisture + 0.15f); + v3 lushGrass = config.grassColorBase * 1.5f; + v3 wetColor = config.grassColorBase * (1.0f - hit->data.moisture) + lushGrass * hit->data.moisture; + grid.setColor(hit->position, wetColor); + } + + grid.remove(r.pos); + continue; + } + } + + // Delete if falls out of bounds + if (nextPos.y() < -config.worldDepth - 20.0f) { + grid.remove(r.pos); + continue; + } + + if (grid.move(r.pos, nextPos)) { + r.pos = nextPos; + nextRain.push_back(r); + } else { + WorldVoxel rv(0.0f, 1.0f, 5); + grid.set(rv, nextPos, true, config.rainColor, config.rainDropSize, true, 5); + r.pos = nextPos; + nextRain.push_back(r); + } + } + rainDrops = nextRain; + + // 3. Apply Block Gravity + if (config.enableGravity) { + physicsTimer += dt; + if (physicsTimer >= config.physicsStep) { + applyTerrainGravity(); + physicsTimer = 0.0f; + } + } + } + + void applyTerrainGravity() { + std::vector::NodeData>> nodes; + grid.collectNodesByObjectId( -1, nodes); + + std::vector::NodeData>> terrain; + terrain.reserve(nodes.size()); + for (auto& n : nodes) { + // Include Dirt, Rock, and Grass in gravity sweep + if (n->data.type == 0 || n->data.type == 1 || n->data.type == 2) { + terrain.push_back(n); + } + } + + // Process Bottom-Up + std::sort(terrain.begin(), terrain.end(), [](const auto& a, const auto& b) { + return a->position.y() < b->position.y(); + }); + + for (auto& n : terrain) { + v3 belowPos = n->position + v3(0, -config.voxelSize, 0); + + // Bounds check so voxels don't fall infinitely + if (belowPos.y() < -config.worldDepth) continue; + + auto hit = grid.find(belowPos, config.voxelSize * 0.1f); + if (!hit) { + grid.move(n->position, belowPos); } } } @@ -179,11 +370,6 @@ public: int nodeCount = 0; - std::random_device rd; - std::mt19937 local_rng(rd()); - std::uniform_real_distribution colorVar(-0.03f, 0.03f); - std::uniform_real_distribution nutrientVar(0.8f, 1.2f); - #pragma omp parallel for schedule(static) collapse(3) for (int i = 0; i < stepsX; ++i) { for (int j = 0; j < stepsZ; ++j) { @@ -226,6 +412,9 @@ public: void clearWorld() { grid.clear(); + clouds.clear(); + rainDrops.clear(); + starVoxelPositions.clear(); } };