diff --git a/tests/plant.cpp b/tests/plant.cpp new file mode 100644 index 0000000..41565ff --- /dev/null +++ b/tests/plant.cpp @@ -0,0 +1,264 @@ +#ifndef PLANT_CPP +#define PLANT_CPP + +#include "../util/sim/plant.hpp" +#include "../util/grid/camera.hpp" + +// Assuming ImGui headers are available via ptest.cpp or similar include paths +#include "imgui.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" + +class PlantSimUI { +private: + PlantSim sim; + Camera cam; + + // Rendering / Texture vars + GLuint textu = 0; + std::mutex PreviewMutex; + bool textureInitialized = false; + frame currentPreviewFrame; + + // Render Settings + int outWidth = 512; + int outHeight = 512; + bool slowRender = false; + bool globalIllumination = true; // Default to true to see sun emission + int reflectCount = 2; + float maxDist = 200.0f; + float framerate = 10.0f; + + // Input state + std::map keyStates; + float deltaTime = 0.016f; // approx 30fps + + const char* getSeasonName(float season, float latitude) { + bool north = latitude >= 0; + if (season < 0.25f) return north ? "Spring" : "Autumn"; + if (season < 0.50f) return north ? "Summer" : "Winter"; + if (season < 0.75f) return north ? "Autumn" : "Spring"; + return north ? "Winter" : "Summer"; + } + + const char* getWeatherName(PlantSim::WeatherState state) { + switch(state) { + case PlantSim::WeatherState::RAIN: return "Raining"; + case PlantSim::WeatherState::SNOW: return "Snowing"; + default: return "Clear"; + } + } +public: + PlantSimUI() { + // Position camera to look at the dirt + cam.origin = v3(0, 5, 30); + cam.lookAt(v3(0, 2, 0)); + cam.fov = 45; + + // Init the simulation + sim.initWorld(); + v3 bg = v3(0.511f, 0.625f, 0.868f); + sim.grid.setBackgroundColor(bg); + sim.grid.setSkylight(bg); + } + + ~PlantSimUI() { + if (textu != 0) glDeleteTextures(1, &textu); + } + + void renderUI(GLFWwindow* window) { + handleCameraControls(window); + + ImGui::Begin("Plant Growth Lab"); + + if (ImGui::BeginTable("PlantLayout", 2, ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch, 0.3f); + ImGui::TableSetupColumn("Viewport", ImGuiTableColumnFlags_WidthStretch, 0.7f); + ImGui::TableNextColumn(); + + renderControls(); + + ImGui::TableNextColumn(); + + renderPreview(); + + ImGui::EndTable(); + } + sim.update(deltaTime); + + ImGui::End(); + } + +private: + void handleCameraControls(GLFWwindow* window) { + glfwPollEvents(); + for (int i = GLFW_KEY_SPACE; i <= GLFW_KEY_LAST; i++) { + keyStates[i] = (glfwGetKey(window, i) == GLFW_PRESS); + } + + float speed = 10.0f * deltaTime; + if (keyStates[GLFW_KEY_W]) cam.moveForward(deltaTime * 10.0f); + if (keyStates[GLFW_KEY_S]) cam.moveBackward(deltaTime * 10.0f); + if (keyStates[GLFW_KEY_A]) cam.moveLeft(deltaTime * 10.0f); + if (keyStates[GLFW_KEY_D]) cam.moveRight(deltaTime * 10.0f); + if (keyStates[GLFW_KEY_Q]) cam.rotateYaw(deltaTime); + if (keyStates[GLFW_KEY_E]) cam.rotateYaw(-deltaTime); + } + + void renderControls() { + if (ImGui::CollapsingHeader("World State", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Day: %d / %d", sim.config.currentDay + 1, sim.config.daysPerYear); + ImGui::Text("Season: %s", getSeasonName(sim.config.season, sim.config.latitude)); + ImGui::Text("Global Temperature: %.1f °C", sim.currentTemperature); + + ImVec4 weatherColor = ImVec4(1, 1, 1, 1); + if (sim.currentWeather == PlantSim::WeatherState::RAIN) weatherColor = ImVec4(0.3f, 0.5f, 1.0f, 1.0f); + else if (sim.currentWeather == PlantSim::WeatherState::SNOW) weatherColor = ImVec4(0.8f, 0.9f, 1.0f, 1.0f); + + ImGui::TextColored(weatherColor, "Current Weather: %s", getWeatherName(sim.currentWeather)); + ImGui::TextColored(weatherColor, "(Time Remaining: %.1fs)", sim.weatherTimer); + + ImGui::Separator(); + ImGui::Text("Atmospheric Moisture: %.1f", sim.atmosphericMoisture); + float localCo2 = 0.0f; + float localAirTemp = sim.currentTemperature; + auto airNodes = sim.grid.findInRadius(v3(0, sim.config.voxelSize / 2.0f, 0), sim.config.voxelSize, 5); + if (!airNodes.empty()) { + auto ap = std::static_pointer_cast(airNodes[0]->data); + localCo2 = ap->co2; + localAirTemp = ap->temperature; + } + ImGui::Text("Local Air Temp: %.1f °C", localAirTemp); + ImGui::Text("Ambient CO2: %.1f ppm", localCo2); + + if (sim.extendedHeatTimer > 0.0f) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.0f, 1.0f), "Heat Wave Timer: %.1f days", sim.extendedHeatTimer / std::max(1.0f, sim.config.dayDuration)); + } else { + ImGui::Text("Heat Wave Timer: 0.0 days"); + } + } + + if (ImGui::CollapsingHeader("Plant Health & Structure", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Active Plant Cells:"); + ImGui::BulletText("Leaves: %d", sim.leafCount); + ImGui::BulletText("Roots: %d", sim.rootCount); + ImGui::Text("Total Organism Resources:"); + ImGui::ProgressBar(std::min(sim.totalPlantEnergy / (std::max(1, sim.leafCount + sim.rootCount) * 20.0f), 1.0f), ImVec2(-1, 0), + ("Energy: " + std::to_string((int)sim.totalPlantEnergy)).c_str()); + ImGui::ProgressBar(std::min(sim.totalPlantWater / (std::max(1, sim.leafCount + sim.rootCount) * 20.0f), 1.0f), ImVec2(-1, 0), + ("Water: " + std::to_string((int)sim.totalPlantWater)).c_str()); + } + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Soil Environment")) { + float currentHydration = 0.0f; + float currentTemp = 0.0f; + float n = 0; + float p = 0; + float k = 0; + float c = 0; + float mg = 0; + + auto dirtNodes = sim.grid.findInRadius(v3(0, -sim.config.voxelSize / 2.0f, 0), sim.config.voxelSize, 0); + if (!dirtNodes.empty()) { + auto dp = std::static_pointer_cast(dirtNodes[0]->data); + currentHydration = dp->hydration; + currentTemp = dp->temperature; + n = dp->nitrogen; + p = dp->phosphorus; + k = dp->potassium; + c = dp->carbon; + mg = dp->magnesium; + } + ImGui::Text("Soil Temp: %.1f °C", currentTemp); + ImGui::ProgressBar(std::min(currentHydration / 500.0f, 1.0f), ImVec2(-1, 0), + ("Water: " + std::to_string((int)currentHydration)).c_str()); + + ImGui::Text("Nitrogen (N): %.1f", n); + ImGui::Text("Phosphorus (P): %.1f", p); + ImGui::Text("Potassium (K): %.1f", k); + ImGui::Text("Carbon (C): %.1f", c); + ImGui::Text("Magnesium (Mg): %.1f", mg); + } + + if (ImGui::CollapsingHeader("Sun & Seasons", ImGuiTreeNodeFlags_DefaultOpen)) { + bool rebuildSun = false; + + ImGui::Text("Time & Season"); + ImGui::SliderFloat("Time of Day", &sim.config.timeOfDay, 0.0f, 1.0f); + int prevDay = sim.config.currentDay; + if (ImGui::SliderInt("Current Day", &sim.config.currentDay, 0, sim.config.daysPerYear - 1)) { + sim.config.season = (static_cast(sim.config.currentDay) + sim.config.timeOfDay) / sim.config.daysPerYear; + } + ImGui::SliderFloat("Day Duration (s)", &sim.config.dayDuration, 1.0f, 600.0f); + if (ImGui::SliderInt("Days per Year", &sim.config.daysPerYear, 4, 365)) { + if (sim.config.currentDay >= sim.config.daysPerYear) sim.config.currentDay = 0; + } + + ImGui::Separator(); + ImGui::Text("Geography"); + ImGui::SliderFloat("Latitude", &sim.config.latitude, -90.0f, 90.0f); + ImGui::SliderFloat("Axial Tilt", &sim.config.axialTilt, 0.0f, 90.0f); + + ImGui::Separator(); + ImGui::Text("Sun Appearance"); + rebuildSun |= ImGui::SliderFloat("Distance", &sim.config.sunDistance, 10.0f, 100.0f); + rebuildSun |= ImGui::ColorEdit3("Color", sim.config.sunColor.data()); + rebuildSun |= ImGui::DragFloat("Intensity", &sim.config.sunIntensity, 0.1f, 0.0f, 100.0f); + + ImGui::Text("Weather Constraints"); + ImGui::SliderFloat("Precipitation Rate", &sim.config.precipRate, 10.0f, 500.0f); + } + + if (ImGui::CollapsingHeader("Simulation")) { + if (ImGui::Button("Reset World", ImVec2(-1, 0))) { + sim.initWorld(); + } + } + + if (ImGui::CollapsingHeader("Render Settings")) { + ImGui::Checkbox("High Quality (Slow)", &slowRender); + ImGui::Checkbox("Global Illumination", &globalIllumination); + ImGui::DragInt("Bounces", &reflectCount, 1, 0, 8); + } + } + + void renderPreview() { + livePreview(); + + if (textureInitialized) { + float availW = ImGui::GetContentRegionAvail().x; + float aspect = (float)outWidth / (float)outHeight; + ImGui::Image((void*)(intptr_t)textu, ImVec2(availW, availW / aspect)); + } + } + + void livePreview() { + std::lock_guard lock(PreviewMutex); + + // Update Grid settings based on UI + sim.grid.setMaxDistance(maxDist); + + // Render + if (slowRender) { + currentPreviewFrame = sim.grid.renderFrame(cam, outHeight, outWidth, frame::colormap::RGB, 10, reflectCount, globalIllumination, true); + } else { + currentPreviewFrame = sim.grid.fastRenderFrame(cam, outHeight, outWidth, frame::colormap::RGB); + } + + // Upload to GPU + if (textu == 0) glGenTextures(1, &textu); + + glBindTexture(GL_TEXTURE_2D, textu); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentPreviewFrame.getWidth(), currentPreviewFrame.getHeight(), + 0, GL_RGB, GL_UNSIGNED_BYTE, currentPreviewFrame.getData().data()); + + textureInitialized = true; + } +}; + +#endif \ No newline at end of file diff --git a/tests/ptest.cpp b/tests/ptest.cpp index ce0f716..6fe4400 100644 --- a/tests/ptest.cpp +++ b/tests/ptest.cpp @@ -10,6 +10,7 @@ #include "../util/noise/pnoise.cpp" #include "planet.cpp" +#include "plant.cpp" #include "../util/basicdefines.hpp" void framebuffer_size_callback(GLFWwindow* window, int width, int height) { @@ -80,6 +81,7 @@ int main() { planetSimUI planetApp; NoisePreviewState noiseState; + PlantSimUI plantApp; if (noiseState.layers.empty()) { NoiseLayer defaultLayer; @@ -101,6 +103,7 @@ int main() { ImGui::GetMainViewport(); drawNoiseLab(noiseState); planetApp.renderUI(window); + plantApp.renderUI(window); ImGui::Begin("Integration Control"); ImGui::Text("Bridge: Noise Lab -> Planet Sim"); diff --git a/util/grid/grid3eigen.hpp b/util/grid/grid3eigen.hpp index 99c0155..6ce7a31 100644 --- a/util/grid/grid3eigen.hpp +++ b/util/grid/grid3eigen.hpp @@ -598,7 +598,7 @@ private: float t; PointType normal, hitPoint; - if (rayCubeIntersect(ray, pointData.get(), t, normal, hitPoint)) { + if (rayCubeIntersect(ray, pointData.get(), t, normal, hitPoint) && pointData->visible) { if (t >= 0 && t <= maxDist && t <= tMax + 0.001f) { maxDist = t; hit = pointData; diff --git a/util/sim/plant.hpp b/util/sim/plant.hpp new file mode 100644 index 0000000..595bdf7 --- /dev/null +++ b/util/sim/plant.hpp @@ -0,0 +1,1023 @@ +#ifndef PLANT_HPP +#define PLANT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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> 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 dirtColorPalette; + + // Plant Stats + int leafCount = 0; + int rootCount = 0; + float totalPlantEnergy = 0.0f; + float totalPlantWater = 0.0f; + + PlantSim() : rng(std::random_device{}()) { + grid = Octree>(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(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(); + 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(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(400.0f, 20.0f); + grid.set(air, pos, false, v3(0,0,0), vSize, true, 5, 0); + } + } + } + + auto seed = std::make_shared(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 xzDist(-config.groundSize / 2.0f, config.groundSize / 2.0f); + std::uniform_real_distribution yDist(config.groundSize, config.groundSize * 1.5f); + + v3 pos(xzDist(rng), yDist(rng), xzDist(rng)); + + if (currentWeather == WeatherState::RAIN) { + auto rain = std::make_shared(); + 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(); + 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(0.2f, 0.8f)(rng) * config.dayDuration; + } else { + float r = std::uniform_real_distribution(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(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(0.02f, 0.08f)(rng); // Short summer showers + } else if (currentTemperature < 0.0f) { + durationDays = std::uniform_real_distribution(0.2f, 0.6f)(rng); // Long winter snows + } else { + durationDays = std::uniform_real_distribution(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(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>::NodeData>> leaves; + std::vector>::NodeData>> roots; + + for (auto& node : plantNodes) { + auto p = std::static_pointer_cast(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(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(leafNode->data); + + int res = std::clamp(static_cast((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(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(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(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 dWater(plantNodes.size(), 0.0f); + std::vector dEnergy(plantNodes.size(), 0.0f); + + for (size_t i = 0; i < plantNodes.size(); ++i) { + auto p1 = std::static_pointer_cast(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(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 newPlants; + std::vector 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(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(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(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(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(dryHeat * 10.0f), 0, 10); + int wIdx = std::clamp(static_cast(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>::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(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 toRemove; + std::vector> toMove; + + #pragma omp parallel + { + std::vector localRemove; + std::vector> 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 dirtToHydrate; + auto rainNodes = grid.findInRadius(v3(0,0,0), searchRadius, 3); + if (!rainNodes.empty()) { + float rainRadius = config.voxelSize * 0.1f; + + std::vector rainToRemove; + std::vector> rainToMove; + + #pragma omp parallel + { + std::vector localRemove; + std::vector> localMove; + std::vector localHydrate; + + static thread_local std::mt19937 local_rng(std::random_device{}()); + std::uniform_real_distribution bounceDist(-1.5f, 1.5f); + std::uniform_real_distribution 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 snowToRemove; + std::vector> snowToMove; + + #pragma omp parallel + { + std::vector localRemove; + std::vector> localMove; + std::vector localHydrate; + + #pragma omp for + for (size_t i = 0; i < snowNodes.size(); ++i) { + auto& node = snowNodes[i]; + auto p = std::static_pointer_cast(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 highestDirt = nullptr; + + for (auto& dirtNode : localDirtNodes) { + if (dirtNode->position.y() > highestY) { + highestY = dirtNode->position.y(); + highestDirt = std::static_pointer_cast(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(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(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(), 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 \ No newline at end of file