Files
stupidsimcpp/util/sim/plant.hpp
yggdrasil75 3ff50cb43d plants
2026-03-09 05:38:27 -04:00

1023 lines
42 KiB
C++

#ifndef PLANT_HPP
#define PLANT_HPP
#include <vector>
#include <cmath>
#include <random>
#include <iostream>
#include <memory>
#include <queue>
#include <unordered_map>
#include <omp.h>
#include <algorithm>
#include <chrono>
#include "../grid/grid3eigen.hpp"
using v3 = Eigen::Vector3f;
enum class ParticleType {
AIR,
SUN,
DIRT,
PLANT,
WATER,
SNOW
};
enum class PlantPart {
SEED,
ROOT,
LEAF
};
struct PlantsimParticle {
ParticleType pt;
float energy;
v3 velocity = v3(0, 0, 0);
float timeOutOfBounds = 0.0f;
bool anchored = false;
float strength = 15.0f;
PlantsimParticle() : pt(ParticleType::AIR), energy(0.0) {}
PlantsimParticle(ParticleType t, float e) : pt(t), energy(e) {}
virtual ~PlantsimParticle() = default;
};
struct AirParticle : public PlantsimParticle {
float co2;
float temperature;
AirParticle(float c = 400.0f, float t = 20.0f)
: PlantsimParticle(ParticleType::AIR, 0.0f), co2(c), temperature(t) {}
};
struct SunParticle : public PlantsimParticle {
SunParticle() : PlantsimParticle(ParticleType::SUN, 1000.0f) {}
};
struct DirtParticle : public PlantsimParticle {
float nitrogen;
float phosphorus;
float potassium;
float carbon;
float magnesium;
float hydration;
float temperature;
DirtParticle(float n = 100.0f, float p = 100.0f, float k = 100.0f, float c = 100.0f, float mg = 100.0f)
: PlantsimParticle(ParticleType::DIRT, 0.0f), nitrogen(n), phosphorus(p), potassium(k), carbon(c), magnesium(mg),
hydration(0.0f), temperature(20.0f) {}
};
struct PlantParticle : public PlantsimParticle {
PlantPart part;
float age;
float water;
float nitrogen;
float phosphorus;
float potassium;
float carbon;
float magnesium;
PlantParticle(PlantPart p = PlantPart::SEED) : PlantsimParticle(ParticleType::PLANT, 10.0f), part(p), age(0.0f),
water(10.0f), nitrogen(0.0f), phosphorus(0.0f), potassium(0.0f), carbon(0.0f), magnesium(0.0f) {}
};
struct WaterParticle : public PlantsimParticle {
WaterParticle() : PlantsimParticle(ParticleType::WATER, 5.0f) {}
};
struct SnowParticle : public PlantsimParticle {
float meltProgress = 0.0f;
SnowParticle() : PlantsimParticle(ParticleType::SNOW, 2.0f) {}
};
struct PlantConfig {
// World Settings
float voxelSize = 0.5f;
float groundSize = 20.0f;
// Sun Settings
float dayDuration = 60.0f;
float sunInclination = 25.0f;
float timeOfDay = 0.3f;
float season = 0.0f;
int currentDay = 0;
int daysPerYear = 24;
float latitude = 45.0f;
float axialTilt = 23.5f;
float sunDistance = 60.0f;
float sunSize = 25.0f;
Eigen::Vector3f sunColor = Eigen::Vector3f(1.0f, 0.95f, 0.8f);
float sunIntensity = 50.0f;
float precipRate = 80.0f;
};
class PlantSim {
public:
enum class WeatherState { CLEAR, RAIN, SNOW };
PlantConfig config;
Octree<std::shared_ptr<PlantsimParticle>> grid;
std::mt19937 rng;
v3 currentSunPos = v3(0, 0, 0);
bool worldInitialized = false;
bool sunExists = false;
float precipAccumulator = 0.0f;
float currentTemperature = 20.0f;
float cloudCover = 0.0f;
WeatherState currentWeather = WeatherState::CLEAR;
float weatherTimer = 0.0f;
float atmosphericMoisture = 0.0f;
float extendedHeatTimer = 0.0f;
float hourlyTimer = 0.0f;
std::vector<uint16_t> dirtColorPalette;
// Plant Stats
int leafCount = 0;
int rootCount = 0;
float totalPlantEnergy = 0.0f;
float totalPlantWater = 0.0f;
PlantSim() : rng(std::random_device{}()) {
grid = Octree<std::shared_ptr<PlantsimParticle>>(v3(-100, -100, -100), v3(100, 100, 100), 8, 16);
// Default Sky settings for the octree renderer
grid.setSkylight(v3(0.05f, 0.05f, 0.1f));
grid.setBackgroundColor(v3(0.02f, 0.02f, 0.05f));
}
void buildDirtPalette() {
dirtColorPalette.resize(11 * 11);
for (int h = 0; h <= 10; ++h) {
for (int w = 0; w <= 10; ++w) {
float dryHeat = h / 10.0f;
float wetness = w / 10.0f;
v3 dryColor(0.36f, 0.25f, 0.20f);
v3 wetColor(0.15f, 0.10f, 0.08f);
v3 bakedColor(0.65f, 0.55f, 0.40f);
v3 baseColor = dryColor * (1.0f - dryHeat) + bakedColor * dryHeat;
v3 newColor = baseColor * (1.0f - wetness) + wetColor * wetness;
dirtColorPalette[h * 11 + w] = grid.getColorIndex(newColor);
}
}
}
void initWorld() {
grid.clear();
sunExists = false;
precipAccumulator = 0.0f;
weatherTimer = 0.0f;
currentWeather = WeatherState::CLEAR;
atmosphericMoisture = 0.0f;
extendedHeatTimer = 0.0f;
hourlyTimer = 0.0f;
buildDirtPalette();
float vSize = config.voxelSize;
int gridRadius = static_cast<int>(config.groundSize / (2.0f * vSize));
int depth = 4; // Generate multiple layers of dirt
v3 dirtColor(0.36f, 0.25f, 0.20f);
v3 seedPos(0, config.voxelSize / 2.0f + 0.01f, 0);
for (int x = -gridRadius; x <= gridRadius; ++x) {
for (int z = -gridRadius; z <= gridRadius; ++z) {
for (int y = 0; y < depth; ++y) {
auto dirt = std::make_shared<DirtParticle>();
dirt->hydration = 10.0f;
v3 pos(x * vSize, -(y + 0.5f) * vSize, z * vSize);
grid.set(dirt, pos, true, dirtColor, vSize, true, 0, 0, 0.0f, 1.0f);
}
// Air Layers (y >= 0)
int airHeight = static_cast<int>(config.groundSize / vSize);
for (int y = 0; y < airHeight; ++y) {
v3 pos(x * vSize, (y + 0.5f) * vSize, z * vSize);
// Don't spawn air directly on top of where the seed will be
if (std::abs(pos.x() - seedPos.x()) < 0.001f &&
std::abs(pos.y() - seedPos.y()) < 0.001f &&
std::abs(pos.z() - seedPos.z()) < 0.001f) {
continue;
}
auto air = std::make_shared<AirParticle>(400.0f, 20.0f);
grid.set(air, pos, false, v3(0,0,0), vSize, true, 5, 0);
}
}
}
auto seed = std::make_shared<PlantParticle>(PlantPart::SEED);
seed->energy = 25.0f;
seed->water = 25.0f;
v3 plantColor(0.8f, 0.7f, 0.2f);
grid.set(seed, seedPos, true, plantColor, config.voxelSize * 0.5f, true, 1, 0, 0.0f, 0.6f);
worldInitialized = true;
updateSunPosition(true);
updateEnvironment(0.016f);
}
void spawnPrecipitation() {
std::uniform_real_distribution<float> xzDist(-config.groundSize / 2.0f, config.groundSize / 2.0f);
std::uniform_real_distribution<float> yDist(config.groundSize, config.groundSize * 1.5f);
v3 pos(xzDist(rng), yDist(rng), xzDist(rng));
if (currentWeather == WeatherState::RAIN) {
auto rain = std::make_shared<WaterParticle>();
rain->velocity = v3(0, -2.0f, 0);
float rainSize = config.voxelSize * 0.2f;
v3 rainColor(0.3f, 0.5f, 0.9f);
grid.set(rain, pos, true, rainColor, rainSize, true, 3, 0, 0.0f, 0.6f);
} else if (currentWeather == WeatherState::SNOW) {
auto snow = std::make_shared<SnowParticle>();
snow->velocity = v3(0, -0.5f, 0);
float snowSize = config.voxelSize * 0.3f;
v3 snowColor(0.9f, 0.95f, 1.0f);
grid.set(snow, pos, true, snowColor, snowSize, true, 4, 0, 0.0f, 0.9f);
}
}
void updateWeather(float dt) {
weatherTimer -= dt;
if (weatherTimer <= 0.0f) {
if (currentWeather != WeatherState::CLEAR) {
// Stop Precipitating
currentWeather = WeatherState::CLEAR;
weatherTimer = std::uniform_real_distribution<float>(0.2f, 0.8f)(rng) * config.dayDuration;
} else {
float r = std::uniform_real_distribution<float>(0.0f, 1.0f)(rng);
float precipChance = 0.2f;
float durationDays = 0.1f;
float delta = config.axialTilt * M_PI / 180.0f * std::sin(config.season * 2.0f * M_PI);
if (std::abs(config.latitude) < 20.0f) {
float latRad = config.latitude * M_PI / 180.0f;
float diff = std::abs(latRad - delta);
precipChance = (diff < 0.2f) ? 0.6f : 0.1f;
durationDays = std::uniform_real_distribution<float>(0.1f, 0.4f)(rng);
} else {
float springFallFactor = std::abs(std::sin(config.season * 4.0f * M_PI));
precipChance = 0.15f + 0.25f * springFallFactor; // More precip in spring/autumn transition
if (currentTemperature > 25.0f) {
durationDays = std::uniform_real_distribution<float>(0.02f, 0.08f)(rng); // Short summer showers
} else if (currentTemperature < 0.0f) {
durationDays = std::uniform_real_distribution<float>(0.2f, 0.6f)(rng); // Long winter snows
} else {
durationDays = std::uniform_real_distribution<float>(0.1f, 0.3f)(rng); // Normal rain
}
}
float moistureBonus = std::min(atmosphericMoisture * 0.005f, 0.4f);
if (extendedHeatTimer > 3.0f * config.dayDuration) { // Heat wave
moistureBonus = 0.0f;
precipChance *= 0.3f; // Desertification stops rain severely
}
precipChance += moistureBonus;
if (r < precipChance) {
currentWeather = (currentTemperature < 0.0f) ? WeatherState::SNOW : WeatherState::RAIN;
weatherTimer = durationDays * config.dayDuration;
atmosphericMoisture *= 0.5f;
} else {
currentWeather = WeatherState::CLEAR;
weatherTimer = std::uniform_real_distribution<float>(0.2f, 0.8f)(rng) * config.dayDuration;
}
}
}
float targetCloudCover = (currentWeather != WeatherState::CLEAR) ? 0.8f : 0.0f;
cloudCover += (targetCloudCover - cloudCover) * dt * 0.1f; // Lerp clouds
}
void updatePlants(float dt) {
float searchRadius = std::max(config.groundSize, config.groundSize * 2.0f) * 2.0f;
auto plantNodes = grid.findInRadius(v3(0,0,0), searchRadius, 1); // objectId 1 = Plant
leafCount = 0;
rootCount = 0;
totalPlantEnergy = 0.0f;
totalPlantWater = 0.0f;
if (plantNodes.empty()) return;
std::vector<std::shared_ptr<Octree<std::shared_ptr<PlantsimParticle>>::NodeData>> leaves;
std::vector<std::shared_ptr<Octree<std::shared_ptr<PlantsimParticle>>::NodeData>> roots;
for (auto& node : plantNodes) {
auto p = std::static_pointer_cast<PlantParticle>(node->data);
if (p->part == PlantPart::LEAF) leaves.push_back(node);
else roots.push_back(node);
totalPlantEnergy += p->energy;
totalPlantWater += p->water;
p->age += dt;
}
leafCount = leaves.size();
rootCount = roots.size();
double timeBudgetPerLeaf = 0.0;
if (!leaves.empty()) {
timeBudgetPerLeaf = static_cast<double>(dt) / leaves.size();
}
// 1. Light gathering & Photosynthesis for leaves
#pragma omp parallel for
for (size_t i = 0; i < leaves.size(); ++i) {
auto& leafNode = leaves[i];
auto p = std::static_pointer_cast<PlantParticle>(leafNode->data);
int res = std::clamp(static_cast<int>((leafNode->size / config.voxelSize) * 16.0f), 4, 16);
Camera leafCam;
leafCam.origin = leafNode->position + v3(0, leafNode->size * 0.51f, 0);
v3 sunDir = currentSunPos.normalized();
if (sunDir.y() < 0.1f) sunDir.y() = 0.1f; // Avoid rendering straight into the ground
leafCam.direction = sunDir;
if (std::abs(leafCam.direction.y()) > 0.99f) {
leafCam.up = v3(1, 0, 0);
} else {
leafCam.up = v3(0, 1, 0);
}
leafCam.fov = 90.0f; // Broad FOV to collect ambient scattering
// Generate the small frame to detect incoming sunlight using grid3eigen's existing renderer
frame lightFrame = grid.renderFrameTimed(leafCam, res, res, frame::colormap::RGB, timeBudgetPerLeaf, 1, false, true);
float totalLight = 0.0f;
const auto& frameData = lightFrame.getData();
int pixelCount = res * res;
for (int p_idx = 0; p_idx < pixelCount * 3; p_idx += 3) {
float r = frameData[p_idx];
float g = frameData[p_idx+1];
float b = frameData[p_idx+2];
// Approximate perceived luminance representing energy capture
totalLight += (r * 0.299f + g * 0.587f + b * 0.114f);
}
float lightIntensity = totalLight / static_cast<float>(pixelCount);
// Photosynthesis: Convert water & light -> energy
float maxWaterUsable = dt * 10.0f;
float waterUsed = std::min(p->water, lightIntensity * maxWaterUsable);
p->water -= waterUsed;
p->energy += waterUsed * 2.5f; // Produce more energy than water consumed
// Passive drain to stay alive
p->water -= dt * 0.5f;
p->energy -= dt * 0.5f;
}
// 2. Roots absorb water from dirt
#pragma omp parallel for
for (size_t i = 0; i < roots.size(); ++i) {
auto& rootNode = roots[i];
auto p = std::static_pointer_cast<PlantParticle>(rootNode->data);
auto dirtNodes = grid.findInRadius(rootNode->position, config.voxelSize * 1.5f, 0);
float waterAbsorbed = 0.0f;
for (auto& dirt : dirtNodes) {
auto dp = std::static_pointer_cast<DirtParticle>(dirt->data);
if (dp->hydration > 0.5f) {
float absorb = std::min(dp->hydration, dt * 5.0f);
#pragma omp atomic
dp->hydration -= absorb;
waterAbsorbed += absorb;
}
}
p->water += waterAbsorbed;
// Passive root maintenance drain
p->water -= dt * 0.1f;
p->energy -= dt * 0.5f;
}
// 3. Resource Diffusion (Homogenizes resources, effectively carrying water up and energy down naturally)
std::vector<float> dWater(plantNodes.size(), 0.0f);
std::vector<float> dEnergy(plantNodes.size(), 0.0f);
for (size_t i = 0; i < plantNodes.size(); ++i) {
auto p1 = std::static_pointer_cast<PlantParticle>(plantNodes[i]->data);
auto neighbors = grid.findInRadius(plantNodes[i]->position, config.voxelSize * 1.2f, 1);
for (auto& n : neighbors) {
if (n == plantNodes[i]) continue;
auto p2 = std::static_pointer_cast<PlantParticle>(n->data);
float diffW = (p1->water - p2->water) * 2.0f * dt;
float diffE = (p1->energy - p2->energy) * 2.0f * dt;
dWater[i] -= diffW;
dEnergy[i] -= diffE;
}
}
// Apply diffusions & evaluate survival/growth
struct NewPlantData {
v3 pos;
PlantPart part;
float initW, initE;
};
std::vector<NewPlantData> newPlants;
std::vector<v3> toRemove;
v3 upDir(0, config.voxelSize, 0);
v3 downDir(0, -config.voxelSize, 0);
v3 sides[4] = { v3(config.voxelSize,0,0), v3(-config.voxelSize,0,0), v3(0,0,config.voxelSize), v3(0,0,-config.voxelSize) };
for (size_t i = 0; i < plantNodes.size(); ++i) {
auto p = std::static_pointer_cast<PlantParticle>(plantNodes[i]->data);
p->water = std::max(0.0f, p->water + dWater[i]);
p->energy = std::max(0.0f, p->energy + dEnergy[i]);
if (p->energy <= 0.0f && p->water <= 0.0f) {
toRemove.push_back(plantNodes[i]->position);
continue;
}
// Update Visuals depending on health
v3 newColor;
if (p->part == PlantPart::LEAF) {
v3 healthyGreen(0.1f, 0.8f, 0.1f);
v3 dryYellow(0.7f, 0.7f, 0.2f);
float health = std::clamp(p->water / 15.0f, 0.0f, 1.0f);
newColor = dryYellow * (1.0f - health) + healthyGreen * health;
} else if (p->part == PlantPart::ROOT) {
newColor = v3(0.6f, 0.5f, 0.4f);
} else { // SEED
newColor = v3(0.8f, 0.7f, 0.2f);
}
// --- Growth & Split Logic ---
float growThreshold = 20.0f;
if (p->part == PlantPart::SEED && p->energy > 15.0f && p->water > 15.0f) {
// Morph to root, spawn leaf directly above
p->part = PlantPart::ROOT;
p->energy -= 10.0f;
p->water -= 10.0f;
newPlants.push_back({plantNodes[i]->position + upDir, PlantPart::LEAF, 10.0f, 10.0f});
}
else if (p->energy > growThreshold && p->water > growThreshold) {
if (plantNodes[i]->size < config.voxelSize * 0.95f) {
// Grow in place
float growth = config.voxelSize * 0.2f * dt;
float newSize = std::min(config.voxelSize, plantNodes[i]->size + growth);
p->energy -= 5.0f * dt;
p->water -= 5.0f * dt;
grid.update(plantNodes[i]->position, plantNodes[i]->position, plantNodes[i]->data, true, newColor, newSize);
} else {
// Maximum size reached, try to replicate / split
bool split = false;
v3 basePos = plantNodes[i]->position;
if (p->part == PlantPart::LEAF) {
if (!grid.find(basePos + upDir, config.voxelSize * 0.1f)) {
newPlants.push_back({basePos + upDir, PlantPart::LEAF, 10.0f, 10.0f});
split = true;
} else {
for (auto& s : sides) {
if (!grid.find(basePos + s + upDir * 0.5f, config.voxelSize * 0.1f)) {
newPlants.push_back({basePos + s, PlantPart::LEAF, 10.0f, 10.0f});
split = true; break;
}
}
}
}
else if (p->part == PlantPart::ROOT) {
if (!grid.find(basePos + downDir, config.voxelSize * 0.1f)) {
newPlants.push_back({basePos + downDir, PlantPart::ROOT, 10.0f, 10.0f});
split = true;
} else {
for (auto& s : sides) {
if (!grid.find(basePos + s + downDir * 0.5f, config.voxelSize * 0.1f)) {
newPlants.push_back({basePos + s + downDir, PlantPart::ROOT, 10.0f, 10.0f});
split = true; break;
}
}
}
}
if (split) {
p->energy -= 15.0f;
p->water -= 15.0f;
}
}
} else {
// Just update color
grid.update(plantNodes[i]->position, plantNodes[i]->position, plantNodes[i]->data, true, newColor, plantNodes[i]->size);
}
}
// Apply structural changes
for (auto& pos : toRemove) grid.remove(pos);
for (auto& np : newPlants) {
auto plantCell = std::make_shared<PlantParticle>(np.part);
plantCell->water = np.initW;
plantCell->energy = np.initE;
v3 initColor = (np.part == PlantPart::LEAF) ? v3(0.1f, 0.8f, 0.1f) : v3(0.6f, 0.5f, 0.4f);
// Spawn at half size and grow into the space
grid.set(plantCell, np.pos, true, initColor, config.voxelSize * 0.5f, true, 1, 0, 0.0f, 0.8f);
}
}
void applyPhysics(float dt) {
float searchRadius = std::max(config.groundSize, config.groundSize * 2.0f) * 2.0f;
float boundMinX = -config.groundSize / 2.0f;
float boundMaxX = config.groundSize / 2.0f;
float boundMinZ = -config.groundSize / 2.0f;
float boundMaxZ = config.groundSize / 2.0f;
float boundMinY = -config.groundSize - 10.0f;
float boundMaxY = config.groundSize * 2.0f;
float floorY = 0.0f;
v3 gravity(0, -9.8f, 0);
auto dirtNodes = grid.findInRadius(v3(0,0,0), searchRadius, 0);
float totalEvaporation = 0.0f;
if (!dirtNodes.empty()) {
#pragma omp parallel
{
float localEvap = 0.0f;
#pragma omp for
for (size_t i = 0; i < dirtNodes.size(); ++i) {
auto& node = dirtNodes[i];
auto dp = std::static_pointer_cast<DirtParticle>(node->data);
// Depth logic (Layer 0 is at -config.voxelSize/2)
float depth = std::abs(node->position.y() + (config.voxelSize / 2.0f));
float depthFactor = std::max(0.0f, 1.0f - (depth / (config.voxelSize * 4.0f)));
// 1. Local Temperature
float targetTemp = currentTemperature;
if (depthFactor > 0.5f && cloudCover < 0.5f && currentSunPos.y() > 0.0f) { // Exposed Surface
float sunHeat = std::max(0.0f, currentSunPos.normalized().y()) * 15.0f * (1.0f - cloudCover);
targetTemp += sunHeat;
} else {
// Ground regulates to a cool average under the surface
targetTemp = 15.0f + (currentTemperature - 15.0f) * 0.2f;
}
dp->temperature += (targetTemp - dp->temperature) * dt * 0.05f;
// 2. Evaporation
if (dp->temperature > 20.0f && dp->hydration > 0.0f && depthFactor > 0.1f) {
float evap = (dp->temperature - 20.0f) * 0.1f * dt * depthFactor;
evap = std::min(evap, dp->hydration);
dp->hydration -= evap;
localEvap += evap;
}
// 3. Water Table Reduction (Flow downwards)
if (dp->hydration > 1.0f) {
v3 belowPos = node->position - v3(0, config.voxelSize, 0);
auto lowerNodes = grid.findInRadius(belowPos, config.voxelSize * 0.2f, 0);
for (auto& lowerNode : lowerNodes) {
if (lowerNode->position.y() < node->position.y() - config.voxelSize * 0.5f) {
auto lowerDp = std::static_pointer_cast<DirtParticle>(lowerNode->data);
if (dp->hydration > lowerDp->hydration) {
float flow = (dp->hydration - lowerDp->hydration) * 0.5f * dt;
float transfer = std::min(flow, dp->hydration);
#pragma omp atomic
dp->hydration -= transfer;
#pragma omp atomic
lowerDp->hydration += transfer;
}
break;
}
}
}
float wetness = std::clamp(dp->hydration / 300.0f, 0.0f, 1.0f);
float dryHeat = std::clamp((dp->temperature - 25.0f) / 15.0f, 0.0f, 1.0f);
int hIdx = std::clamp(static_cast<int>(dryHeat * 10.0f), 0, 10);
int wIdx = std::clamp(static_cast<int>(wetness * 10.0f), 0, 10);
node->colorIdx = dirtColorPalette[hIdx * 11 + wIdx];
}
#pragma omp atomic
totalEvaporation += localEvap;
}
// Desertification Logic
atmosphericMoisture += totalEvaporation;
if (currentTemperature > 28.0f && cloudCover < 0.3f) {
extendedHeatTimer += dt;
} else if (currentTemperature < 22.0f) {
extendedHeatTimer = std::max(0.0f, extendedHeatTimer - dt * 2.0f);
}
if (extendedHeatTimer > 3.0f * config.dayDuration) {
// Persistent heat scorches wind out of atmospheric moisture
atmosphericMoisture = std::max(0.0f, atmosphericMoisture - (atmosphericMoisture * 0.05f * dt));
}
}
updatePlants(dt);
auto plantNodes = grid.findInRadius(v3(0,0,0), searchRadius, 1);
if (!plantNodes.empty()) {
for (auto& node : plantNodes) node->data->anchored = false;
std::queue<std::pair<std::shared_ptr<Octree<std::shared_ptr<PlantsimParticle>>::NodeData>, float>> q;
float floorY = 0.0f;
float tol = config.voxelSize * 0.1f;
for (auto& node : plantNodes) {
if (node->position.y() <= floorY + config.voxelSize / 2.0f + tol || std::static_pointer_cast<PlantParticle>(node->data)->part == PlantPart::ROOT) {
node->data->anchored = true;
q.push({node, 0.0f});
}
}
while (!q.empty()) {
auto currPair = q.front();
auto curr = currPair.first;
float stress = currPair.second;
q.pop();
auto neighbors = grid.findInRadius(curr->position, config.voxelSize * 1.5f, 1);
for (auto& neighbor : neighbors) {
if (!neighbor->data->anchored && neighbor->objectId == 1) {
v3 diff = neighbor->position - curr->position;
float addedStress = (std::abs(diff.x()) > tol || std::abs(diff.z()) > tol) ? 2.5f : 1.0f;
float newStress = stress + addedStress;
if (newStress <= neighbor->data->strength) {
neighbor->data->anchored = true;
q.push({neighbor, newStress});
}
}
}
}
std::vector<v3> toRemove;
std::vector<std::pair<v3, v3>> toMove;
#pragma omp parallel
{
std::vector<v3> localRemove;
std::vector<std::pair<v3, v3>> localMove;
#pragma omp for
for (size_t i = 0; i < plantNodes.size(); ++i) {
auto& node = plantNodes[i];
auto p = node->data;
if (p->anchored) {
p->velocity = v3(0,0,0);
p->timeOutOfBounds = 0.0f;
continue;
}
p->velocity += gravity * dt;
v3 newPos = node->position + p->velocity * dt;
if (newPos.y() < boundMinY) {
localRemove.push_back(node->position);
continue;
}
if (newPos.y() - config.voxelSize / 2.0f <= floorY) {
newPos.y() = floorY + config.voxelSize / 2.0f;
p->velocity = v3(0,0,0);
}
localMove.push_back({node->position, newPos});
}
#pragma omp critical
{
toRemove.insert(toRemove.end(), localRemove.begin(), localRemove.end());
toMove.insert(toMove.end(), localMove.begin(), localMove.end());
}
}
for (auto& pos : toRemove) grid.remove(pos);
for (auto& movePair : toMove) grid.move(movePair.first, movePair.second);
}
std::vector<v3> dirtToHydrate;
auto rainNodes = grid.findInRadius(v3(0,0,0), searchRadius, 3);
if (!rainNodes.empty()) {
float rainRadius = config.voxelSize * 0.1f;
std::vector<v3> rainToRemove;
std::vector<std::pair<v3, v3>> rainToMove;
#pragma omp parallel
{
std::vector<v3> localRemove;
std::vector<std::pair<v3, v3>> localMove;
std::vector<v3> localHydrate;
static thread_local std::mt19937 local_rng(std::random_device{}());
std::uniform_real_distribution<float> bounceDist(-1.5f, 1.5f);
std::uniform_real_distribution<float> probDist(0.0f, 1.0f);
#pragma omp for
for (size_t i = 0; i < rainNodes.size(); ++i) {
auto& node = rainNodes[i];
auto p = node->data;
p->velocity += gravity * dt;
v3 newPos = node->position + p->velocity * dt;
bool overDirt = (newPos.x() >= boundMinX && newPos.x() <= boundMaxX &&
newPos.z() >= boundMinZ && newPos.z() <= boundMaxZ);
if (overDirt && newPos.y() - rainRadius <= floorY) {
newPos.y() = floorY + rainRadius;
// Bounce logic
if (p->velocity.y() < -1.5f) {
p->velocity.y() = -p->velocity.y() * 0.3f;
p->velocity.x() += bounceDist(local_rng);
p->velocity.z() += bounceDist(local_rng);
} else {
p->velocity.y() = 0.0f;
v3 centerDir = newPos; centerDir.y() = 0.0f;
if (centerDir.norm() > 0.1f) {
p->velocity += centerDir.normalized() * (dt * 3.0f);
}
p->velocity.x() *= 0.95f;
p->velocity.z() *= 0.95f;
}
if (probDist(local_rng) < dt * 1.5f) {
localRemove.push_back(node->position);
localHydrate.push_back(newPos);
continue;
}
}
if (newPos.y() < boundMinY) {
localRemove.push_back(node->position);
continue;
}
localMove.push_back({node->position, newPos});
}
#pragma omp critical
{
rainToRemove.insert(rainToRemove.end(), localRemove.begin(), localRemove.end());
rainToMove.insert(rainToMove.end(), localMove.begin(), localMove.end());
dirtToHydrate.insert(dirtToHydrate.end(), localHydrate.begin(), localHydrate.end());
}
}
for (auto& pos : rainToRemove) grid.remove(pos);
for (auto& movePair : rainToMove) grid.move(movePair.first, movePair.second);
}
auto snowNodes = grid.findInRadius(v3(0,0,0), searchRadius, 4);
if (!snowNodes.empty()) {
float snowRadius = config.voxelSize * 0.15f;
std::vector<v3> snowToRemove;
std::vector<std::pair<v3, v3>> snowToMove;
#pragma omp parallel
{
std::vector<v3> localRemove;
std::vector<std::pair<v3, v3>> localMove;
std::vector<v3> localHydrate;
#pragma omp for
for (size_t i = 0; i < snowNodes.size(); ++i) {
auto& node = snowNodes[i];
auto p = std::static_pointer_cast<SnowParticle>(node->data);
if (!p->anchored) {
p->velocity += gravity * dt * 0.15f; // Falls much slower
if (p->velocity.y() < -1.5f) p->velocity.y() = -1.5f; // Terminal velocity
v3 newPos = node->position + p->velocity * dt;
bool overDirt = (newPos.x() >= boundMinX && newPos.x() <= boundMaxX &&
newPos.z() >= boundMinZ && newPos.z() <= boundMaxZ);
if (overDirt && newPos.y() - snowRadius <= floorY + 0.02f) {
newPos.y() = floorY + snowRadius;
p->anchored = true;
p->velocity = v3(0,0,0);
} else if (newPos.y() < boundMinY) {
localRemove.push_back(node->position);
continue;
}
localMove.push_back({node->position, newPos});
} else {
// Melting logic for snow sitting on ground
if (currentTemperature > 0.0f) {
p->meltProgress += currentTemperature * dt;
if (p->meltProgress > 15.0f) {
localRemove.push_back(node->position);
localHydrate.push_back(node->position);
}
}
}
}
#pragma omp critical
{
snowToRemove.insert(snowToRemove.end(), localRemove.begin(), localRemove.end());
snowToMove.insert(snowToMove.end(), localMove.begin(), localMove.end());
dirtToHydrate.insert(dirtToHydrate.end(), localHydrate.begin(), localHydrate.end());
}
}
for (auto& pos : snowToRemove) grid.remove(pos);
for (auto& movePair : snowToMove) grid.move(movePair.first, movePair.second);
}
if (!dirtToHydrate.empty()) {
#pragma omp parallel for
for (size_t i = 0; i < dirtToHydrate.size(); ++i) {
auto localDirtNodes = grid.findInRadius(dirtToHydrate[i], config.voxelSize * 1.5f, 0);
float highestY = -9999.0f;
std::shared_ptr<DirtParticle> highestDirt = nullptr;
for (auto& dirtNode : localDirtNodes) {
if (dirtNode->position.y() > highestY) {
highestY = dirtNode->position.y();
highestDirt = std::static_pointer_cast<DirtParticle>(dirtNode->data);
}
}
if (highestDirt) {
#pragma omp atomic
highestDirt->hydration += 15.0f;
}
}
}
// Air Update
auto airNodes = grid.findInRadius(v3(0,0,0), searchRadius, 5);
if (!airNodes.empty()) {
#pragma omp parallel for
for (size_t i = 0; i < airNodes.size(); ++i) {
auto p = std::static_pointer_cast<AirParticle>(airNodes[i]->data);
// Slowly normalize air temperature to global temperature
p->temperature += (currentTemperature - p->temperature) * dt * 0.01f;
}
}
}
void update(float dt) {
if (!worldInitialized) return;
// Advance Time
if (config.dayDuration > 0.0f) {
float timeStep = dt / config.dayDuration;
config.timeOfDay += timeStep;
while (config.timeOfDay >= 1.0f) {
config.timeOfDay -= 1.0f;
config.currentDay++;
if (config.currentDay >= config.daysPerYear) {
config.currentDay = 0;
}
}
config.season = (static_cast<float>(config.currentDay) + config.timeOfDay) / config.daysPerYear;
}
updateEnvironment(dt);
hourlyTimer += dt;
float hourlyThreshold = config.dayDuration / 24.0f;
if (hourlyTimer >= hourlyThreshold) {
updateWeather(hourlyTimer);
hourlyTimer = std::fmod(hourlyTimer, hourlyThreshold);
}
if (currentWeather != WeatherState::CLEAR) {
precipAccumulator += config.precipRate * dt;
while (precipAccumulator >= 1.0f) {
precipAccumulator -= 1.0f;
spawnPrecipitation();
}
} else {
precipAccumulator = 0.0f;
}
updateSunPosition(false);
applyPhysics(dt);
}
void updateEnvironment(float dt) {
float delta = config.axialTilt * M_PI / 180.0f * std::sin(config.season * 2.0f * M_PI);
float phi = config.latitude * M_PI / 180.0f;
float H = (config.timeOfDay - 0.5f) * 2.0f * M_PI;
float sunHeight = std::sin(phi) * std::sin(delta) + std::cos(phi) * std::cos(delta) * std::cos(H);
// Compute local temperature
float baseTemp = 30.0f - (std::abs(config.latitude) / 90.0f) * 50.0f;
float seasonalSwing = (std::abs(config.latitude) / 90.0f) * 30.0f;
float latSign = (config.latitude >= 0) ? 1.0f : -1.0f;
float seasonOffset = (delta * 180.0f / M_PI) / 23.5f * latSign * seasonalSwing;
float diurnalOffset = (sunHeight - 0.0f) * 10.0f;
currentTemperature = baseTemp + seasonOffset + diurnalOffset;
float targetCloudCover = (currentWeather != WeatherState::CLEAR) ? 0.8f : 0.0f;
cloudCover += (targetCloudCover - cloudCover) * dt * 0.1f;
v3 nightColor(0.02f, 0.02f, 0.05f);
v3 dawnColor(0.8f, 0.4f, 0.2f);
v3 dayColor(0.53f, 0.81f, 0.92f);
v3 currentBg;
v3 currentLight;
float overcastFactor = 1.0f - cloudCover * 0.5f;
if (sunHeight > 0.2f) {
currentBg = dayColor * overcastFactor;
float brightness = (0.3f + (sunHeight * 0.7f)) * overcastFactor;
currentLight = v3(brightness, brightness, brightness);
} else if (sunHeight > 0.0f) {
float fade = sunHeight / 0.2f;
currentBg = ((1.0f - fade) * dawnColor + fade * dayColor) * overcastFactor;
float brightness = (0.1f + (fade * 0.2f)) * overcastFactor;
currentLight = v3(brightness, brightness, brightness);
} else if (sunHeight > -0.2f) {
float fade = (sunHeight + 0.2f) / 0.2f;
currentBg = (1.0f - fade) * nightColor + fade * dawnColor * overcastFactor;
currentLight = v3(0.02f, 0.02f, 0.05f) * (1.0f - fade) + v3(0.1f, 0.1f, 0.15f) * fade * overcastFactor;
} else {
currentBg = nightColor;
currentLight = v3(0.02f, 0.02f, 0.05f);
}
grid.setBackgroundColor(currentBg);
grid.setSkylight(currentLight);
}
void updateSunPosition(bool forceRebuild) {
float delta = config.axialTilt * M_PI / 180.0f * std::sin(config.season * 2.0f * M_PI);
float phi = config.latitude * M_PI / 180.0f;
float H = (config.timeOfDay - 0.5f) * 2.0f * M_PI;
float x = -std::cos(delta) * std::sin(H);
float y = std::sin(phi) * std::sin(delta) + std::cos(phi) * std::cos(delta) * std::cos(H);
float z = std::sin(phi) * std::cos(delta) * std::cos(H) - std::cos(phi) * std::sin(delta);
v3 newSunPos = v3(x, y, z).normalized() * config.sunDistance;
if (!forceRebuild && (newSunPos - currentSunPos).norm() < (config.sunSize * 0.1f)) {
return;
}
v3 deltaMove = newSunPos - currentSunPos;
currentSunPos = newSunPos;
if (sunExists && !forceRebuild) {
if (grid.moveObject(2, deltaMove)) {
return;
} else {
sunExists = false;
}
}
if (!sunExists || forceRebuild) {
int count = 25;
float step = config.sunSize / count;
float offset = config.sunSize / 2.0f;
v3 forward = -currentSunPos.normalized();
v3 right = v3(0,1,0).cross(forward).normalized();
v3 up = forward.cross(right).normalized();
float t = config.timeOfDay;
v3 sunColor(1.0f, 0.95f, 0.8f);
if (t > 0.7f || t < 0.3f) sunColor = v3(1.0f, 0.4f, 0.1f);
for(int i=0; i<count; ++i) {
for(int j=0; j<count; ++j) {
float u = (i * step) - offset;
float v = (j * step) - offset;
v3 pos = currentSunPos + (right * u) + (up * v);
grid.set(std::make_shared<SunParticle>(), pos, true, sunColor, step, true, 2, 0, config.sunIntensity, 0.0f, 0.0f, 1.0f, 1.0f);
}
}
sunExists = true;
}
}
void grow() {
std::cout << "Simulating growth step at day " << config.currentDay + 1 << ", time " << config.timeOfDay << std::endl;
}
};
#endif