1 Commits

Author SHA1 Message Date
yggdrasil75
35c8a71102 renderingbranch 2026-03-07 11:17:51 -05:00
9 changed files with 267 additions and 1012 deletions

View File

@@ -34,7 +34,7 @@ endif
CXXFLAGS = $(BASE_CXXFLAGS) $(SIMD_CXXFLAGS)
# Source files
SRC := $(SRC_DIR)/ptest.cpp
SRC := $(SRC_DIR)/materialtest.cpp
#SRC := $(SRC_DIR)/g2chromatic2.cpp
SRC += $(IMGUI_DIR)/imgui.cpp $(IMGUI_DIR)/imgui_demo.cpp $(IMGUI_DIR)/imgui_draw.cpp $(IMGUI_DIR)/imgui_tables.cpp $(IMGUI_DIR)/imgui_widgets.cpp
SRC += $(IMGUI_DIR)/backends/imgui_impl_glfw.cpp $(IMGUI_DIR)/backends/imgui_impl_opengl3.cpp

View File

@@ -10,6 +10,9 @@
#include "../util/grid/grid3eigen.hpp"
#include "../util/output/frame.hpp"
#include "../util/output/bmpwriter.hpp"
#include "../util/output/aviwriter.hpp"
#include "../util/timing_decorator.hpp"
#include "../util/timing_decorator.cpp"
// Helper function to create a solid volume of voxels with material properties
void createBox(Octree<int>& octree, const Eigen::Vector3f& center, const Eigen::Vector3f& size,
@@ -123,11 +126,16 @@ int main() {
octree.generateLODs();
octree.printStats();
// 3. Setup rendering loop
// 3. Setup video rendering
int width = 512;
int height = 512;
int samples = 400;
int bounces = 5;
// --- Video Animation Parameters ---
const float fps = 30.0f;
const float durationPerSegment = 10.0f; // Seconds to travel between each view
const int framesPerSegment = static_cast<int>(fps * durationPerSegment);
const int video_samples = 100; // Samples per pixel for each video frame
const int video_bounces = 5; // Ray bounces for each video frame
struct View {
std::string name;
@@ -135,61 +143,65 @@ int main() {
Eigen::Vector3f up;
};
// The walls are set perfectly at +/- 7.0 inner edges.
// Placing camera at +/- 6.8 will put it "just barely inside".
// Floor is at Z = -0.5, Wall top is at Z = 7.5
// Define the keyframe camera views for the animation
std::vector<View> views = {
{"+X", Eigen::Vector3f( 6.8f, 0.0f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"-X", Eigen::Vector3f(-6.8f, 0.0f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"+Y", Eigen::Vector3f( 0.0f, 6.8f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"-Y", Eigen::Vector3f( 0.0f, -6.8f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"+Z", Eigen::Vector3f( 0.0f, 0.0f, 7.3f), Eigen::Vector3f(0.0f, 1.0f, 0.0f)} // Looking down from just beneath wall top
{"+X", Eigen::Vector3f( 6.8f, 0.0f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"+Y", Eigen::Vector3f( 0.0f, 6.8f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"-X", Eigen::Vector3f(-6.8f, 0.0f, 1.0f), Eigen::Vector3f(0.0f, 0.0f, 1.0f)},
{"+Z", Eigen::Vector3f( 0.0f, 0.0f, 7.3f), Eigen::Vector3f(0.0f, 1.0f, 0.0f)} // Top-down view
};
Eigen::Vector3f target(0.0f, 0.0f, 0.5f);
Eigen::Vector3f target(0.0f, 0.0f, 0.5f); // The camera will always look at this point
for (const auto& view : views) {
std::cout << "\nRendering view from " << view.name << " direction (Fast Pass)..." << std::endl;
Camera cam;
cam.origin = view.origin;
cam.direction = (target - view.origin).normalized();
cam.up = view.up;
frame out = octree.fastRenderFrame(cam, height, width, frame::colormap::RGB);
std::string filename = "output/fast/render_" + view.name + ".bmp";
BMPWriter::saveBMP(filename, out);
}
for (const auto& view : views) {
std::cout << "\nRendering view from " << view.name << " direction (Medium 60s Pass)..." << std::endl;
Camera cam;
cam.origin = view.origin;
cam.direction = (target - view.origin).normalized();
cam.up = view.up;
frame out = octree.renderFrameTimed(cam, height, width, frame::colormap::RGB, 60, bounces, false, true);
std::string filename = "output/medium/render_" + view.name + ".bmp";
BMPWriter::saveBMP(filename, out);
}
for (const auto& view : views) {
std::cout << "\nRendering view from " << view.name << " direction (Slow 400 Samples Pass)..." << std::endl;
Camera cam;
cam.origin = view.origin;
cam.direction = (target - view.origin).normalized();
cam.up = view.up;
frame out = octree.renderFrame(cam, height, width, frame::colormap::RGB, samples, bounces, false, true);
std::string filename = "output/slow/render_" + view.name + ".bmp";
BMPWriter::saveBMP(filename, out);
// --- Main Animation and Rendering Loop ---
std::vector<frame> videoFrames;
const int totalFrames = framesPerSegment * views.size();
videoFrames.reserve(totalFrames);
int frameCounter = 0;
std::cout << "\nStarting video render..." << std::endl;
std::cout << "Total frames to render: " << totalFrames << std::endl;
for (size_t i = 0; i < views.size(); ++i) {
const View& startView = views[i];
const View& endView = views[(i + 1) % views.size()]; // Loop back to the first view at the end
std::cout << "\nAnimating segment: " << startView.name << " -> " << endView.name << std::endl;
for (int j = 0; j < framesPerSegment; ++j) {
frameCounter++;
float t = static_cast<float>(j) / static_cast<float>(framesPerSegment);
// Interpolate camera position (origin) linearly
Eigen::Vector3f currentOrigin = startView.origin * (1.0f - t) + endView.origin * t;
// Interpolate camera orientation (up vector) and normalize
Eigen::Vector3f currentUp = (startView.up * (1.0f - t) + endView.up * t).normalized();
Camera cam;
cam.origin = currentOrigin;
cam.up = currentUp;
cam.direction = (target - cam.origin).normalized();
std::cout << "Rendering video frame " << frameCounter << "/" << totalFrames << "..." << std::endl;
frame out = octree.renderFrame(cam, height, width, frame::colormap::RGB, video_samples, video_bounces, false, true);
videoFrames.push_back(std::move(out)); // Use std::move for efficiency
}
}
std::cout << "\nAll renders complete!" << std::endl;
// --- Save the final video ---
std::cout << "\nAll frames rendered. Saving video file..." << std::endl;
std::string videoFilename = "output/material_test_video.avi";
if (AVIWriter::saveAVIFromCompressedFrames(videoFilename, std::move(videoFrames), width, height, fps)) {
std::cout << "Video saved successfully to " << videoFilename << std::endl;
} else {
std::cerr << "Error: Failed to save video!" << std::endl;
}
std::cout << "\nRender complete!" << std::endl;
return 0;
}

View File

@@ -32,7 +32,6 @@ private:
std::map<int, bool> keyStates;
float deltaTime = 0.16f;
bool orbitEquator = false;
bool collapsed;
float rotationRadius = 2500;
float angle = 0.0f;
const float ω = (std::pow(M_PI, 2) / 30) / 10;
@@ -85,7 +84,7 @@ public:
void renderUI(GLFWwindow* window) {
handleCameraControls(window);
collapsed = ImGui::Begin("Planet Simulation");
ImGui::Begin("Planet Simulation");
if (ImGui::BeginTable("MainLayout", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter)) {
ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch, 0.3f);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch, 0.7f);
@@ -290,9 +289,8 @@ public:
void renderPreviewPanel() {
ImGui::BeginChild("PreviewChild", ImVec2(0, 0), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
if (!collapsed) {
livePreview();
}
livePreview();
if (textureInitialized) {
float aspect = (float)currentPreviewFrame.getWidth() / (float)currentPreviewFrame.getHeight();
float availWidth = ImGui::GetContentRegionAvail().x;

View File

@@ -10,7 +10,6 @@
#include "../util/noise/pnoise.cpp"
#include "planet.cpp"
#include "worldbox.cpp"
#include "../util/basicdefines.hpp"
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
@@ -81,7 +80,6 @@ int main() {
planetSimUI planetApp;
NoisePreviewState noiseState;
worldboxSimUI worldBox;
if (noiseState.layers.empty()) {
NoiseLayer defaultLayer;
@@ -103,7 +101,6 @@ int main() {
ImGui::GetMainViewport();
drawNoiseLab(noiseState);
planetApp.renderUI(window);
worldBox.renderUI(window);
ImGui::Begin("Integration Control");
ImGui::Text("Bridge: Noise Lab -> Planet Sim");

View File

@@ -1,347 +0,0 @@
#ifndef WORLDBOX_CPP
#define WORLDBOX_CPP
#include "../util/sim/worldbox.hpp"
#include "../util/grid/camera.hpp"
#include "../imgui/imgui.h"
#include "../imgui/backends/imgui_impl_glfw.h"
#include "../imgui/backends/imgui_impl_opengl3.h"
#include <GLFW/glfw3.h>
class worldboxSimUI {
private:
worldboxsim sim;
Camera cam;
// UI and Render State
GLuint textu = 0;
std::mutex PreviewMutex;
bool updatePreview = false;
bool textureInitialized = false;
frame currentPreviewFrame;
// Rendering Settings
int outWidth = 1024;
int outHeight = 1024;
int reflectCount = 2;
bool slowRender = false;
float lodDist = 1024.0f;
float lodDropoff = 0.05f;
float maxViewDistance = 4096.0f;
bool globalIllumination = false;
bool useLod = true;
float framerate = 60.0f;
// Input/Time
std::map<int, bool> keyStates;
std::chrono::steady_clock::time_point lastFrameTime;
float deltaTime = 0.016f;
// Stats tracking
std::chrono::steady_clock::time_point lastStatsUpdate;
std::string cachedStats;
bool statsNeedUpdate = true;
enum class DebugColorMode {
BASE,
NUTRIENTS,
MOISTURE
};
DebugColorMode currentColorMode = DebugColorMode::BASE;
public:
worldboxSimUI() {
// Position camera to look at the center of the world slightly from above
cam.origin = v3(0, 50, 80);
v3 target = v3(0, 0, 0);
cam.direction = (target - cam.origin).normalized();
cam.up = v3(0, 1, 0);
cam.fov = 60;
cam.rotationSpeed = M_1_PI;
cam.movementSpeed = 50.0f;
lastFrameTime = std::chrono::steady_clock::now();
}
~worldboxSimUI() {
if (textu != 0) {
glDeleteTextures(1, &textu);
}
sim.grid.clear();
}
void renderUI(GLFWwindow* window) {
// Compute delta time for consistent movement and sim stepping
auto now = std::chrono::steady_clock::now();
deltaTime = std::chrono::duration<float>(now - lastFrameTime).count();
lastFrameTime = now;
handleCameraControls(window);
// 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)) {
ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch, 0.3f);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch, 0.7f);
ImGui::TableNextColumn();
renderControlsPanel();
ImGui::TableNextColumn();
renderPreviewPanel();
ImGui::EndTable();
}
ImGui::End();
}
void handleCameraControls(GLFWwindow* window) {
glfwPollEvents();
for (int i = GLFW_KEY_SPACE; i <= GLFW_KEY_LAST; i++) {
keyStates[i] = (glfwGetKey(window, i) == GLFW_PRESS);
}
float currentSpeed = cam.movementSpeed * deltaTime;
if (keyStates[GLFW_KEY_LEFT_SHIFT]) currentSpeed *= 3.0f; // Sprint
if (keyStates[GLFW_KEY_W]) cam.moveForward(currentSpeed);
if (keyStates[GLFW_KEY_S]) cam.moveBackward(currentSpeed);
if (keyStates[GLFW_KEY_A]) cam.moveLeft(currentSpeed);
if (keyStates[GLFW_KEY_D]) cam.moveRight(currentSpeed);
if (keyStates[GLFW_KEY_E]) cam.moveUp(currentSpeed);
if (keyStates[GLFW_KEY_Q]) cam.moveDown(currentSpeed);
if (keyStates[GLFW_KEY_LEFT]) cam.rotateYaw(cam.rotationSpeed * deltaTime);
if (keyStates[GLFW_KEY_RIGHT]) cam.rotateYaw(-cam.rotationSpeed * deltaTime);
if (keyStates[GLFW_KEY_UP]) cam.rotatePitch(cam.rotationSpeed * deltaTime);
if (keyStates[GLFW_KEY_DOWN]) cam.rotatePitch(-cam.rotationSpeed * deltaTime);
}
void renderControlsPanel() {
ImGui::BeginChild("ControlsScroll", ImVec2(0, 0), true);
if (ImGui::CollapsingHeader("World Generation", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat("Width (X)", &sim.config.worldSizeX, 1.0f, 10.0f, 2000.0f);
ImGui::DragFloat("Length (Z)", &sim.config.worldSizeZ, 1.0f, 10.0f, 2000.0f);
ImGui::DragFloat("Depth (Y)", &sim.config.worldDepth, 1.0f, 1.0f, 500.0f);
ImGui::DragFloat("Voxel Size", &sim.config.voxelSize, 0.1f, 0.1f, 10.0f);
ImGui::ColorEdit3("Dirt Base Color", sim.config.baseDirtColor.data());
ImGui::Separator();
if (ImGui::Button("Generate Flat World", ImVec2(-1, 40))) {
sim.generateFlatWorld();
// applyDebugColorMode();
statsNeedUpdate = true;
}
if (ImGui::Button("Clear World", ImVec2(-1, 30))) {
sim.clearWorld();
statsNeedUpdate = true;
}
}
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);
ImGui::DragFloat("Orbit Radius", &sim.config.starOrbitRadius, 10.0f, 100.0f, 5000.0f);
ImGui::DragFloat("Star Speed", &sim.config.starSpeed, 0.01f, 0.0f, 5.0f);
ImGui::DragFloat("Panel Size", &sim.config.starPanelSize, 5.0f, 10.0f, 1000.0f);
ImGui::ColorEdit3("Star Color", sim.config.starColor.data());
ImGui::Separator();
ImGui::Text("Grass Settings");
ImGui::SliderFloat("Grass Density", &sim.config.grassDensity, 0.0f, 1.0f);
ImGui::ColorEdit3("Grass Color Base", sim.config.grassColorBase.data());
if (ImGui::Button("Generate Grass", ImVec2(-1, 40))) {
sim.generateGrass();
applyDebugColorMode();
statsNeedUpdate = true;
}
}
if (ImGui::CollapsingHeader("Debug Views")) {
ImGui::Text("Render Data Mode:");
bool colorChanged = false;
if (ImGui::RadioButton("Base Color", currentColorMode == DebugColorMode::BASE)) {
currentColorMode = DebugColorMode::BASE;
colorChanged = true;
}
if (ImGui::RadioButton("Nutrients", currentColorMode == DebugColorMode::NUTRIENTS)) {
currentColorMode = DebugColorMode::NUTRIENTS;
colorChanged = true;
}
if (ImGui::RadioButton("Moisture", currentColorMode == DebugColorMode::MOISTURE)) {
currentColorMode = DebugColorMode::MOISTURE;
colorChanged = true;
}
if (colorChanged) {
applyDebugColorMode();
}
}
if (ImGui::CollapsingHeader("Camera & Render Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat3("Origin", cam.origin.data());
ImGui::DragFloat3("Direction", cam.direction.data(), 0.0001f, -1.0f, 1.0f);
ImGui::DragFloat("Movement Speed", &cam.movementSpeed, 0.1f, 1.0f, 500.0f);
ImGui::InputFloat("Max Framerate", &framerate, 1, 10);
ImGui::Checkbox("Use PBR/Raytracing (Slow)", &slowRender);
if(slowRender) {
ImGui::DragInt("Bounces", &reflectCount, 1, 0, 10);
ImGui::Checkbox("Global Illumination", &globalIllumination);
}
ImGui::Checkbox("Use LODs", &useLod);
if (ImGui::Button("Reset Camera")) {
cam.origin = v3(0, sim.config.worldDepth * 2.0f, std::max(sim.config.worldSizeX, sim.config.worldSizeZ));
cam.direction = (v3(0, 0, 0) - cam.origin).normalized();
}
}
updateStatsCache();
ImGui::TextUnformatted(cachedStats.c_str());
ImGui::EndChild();
}
void renderPreviewPanel() {
ImGui::BeginChild("PreviewChild", ImVec2(0, 0), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
livePreview();
if (textureInitialized) {
float aspect = (float)currentPreviewFrame.getWidth() / (float)currentPreviewFrame.getHeight();
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::Image((void*)(intptr_t)textu, ImVec2(availWidth, availWidth / aspect));
}
ImGui::EndChild();
}
void applyDebugColorMode() {
if (sim.grid.empty()) return;
v3 boundsHalfExtent = v3(sim.config.worldSizeX, sim.config.worldDepth, sim.config.worldSizeZ);
float searchRadius = boundsHalfExtent.norm() * 2.0f;
auto allNodes = sim.grid.findInRadius(v3(0,0,0), searchRadius);
for (auto& p : allNodes) {
if (!p || !p->active) continue;
v3 color = sim.config.baseDirtColor;
switch (currentColorMode) {
case DebugColorMode::NUTRIENTS: {
float t = std::clamp(p->data.nutrients, 0.0f, 1.0f);
color = v3(1.0f - t, t, 0.0f);
break;
}
case DebugColorMode::MOISTURE: {
float t = std::clamp(p->data.moisture, 0.0f, 1.0f);
color = v3(1.0f - t, 1.0f - t, 1.0f);
break;
}
case DebugColorMode::BASE:
default:
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;
}
sim.grid.setColor(p->position, color);
}
}
void livePreview() {
std::lock_guard<std::mutex> lock(PreviewMutex);
updatePreview = true;
float invFrameRate = 1.0f / framerate;
if (!useLod) {
sim.grid.setLODFalloff(0.01);
sim.grid.setLODMinDistance(1000.0f);
} else {
sim.grid.setLODMinDistance(lodDist);
sim.grid.setLODFalloff(lodDropoff);
}
sim.grid.setMaxDistance(maxViewDistance);
if (slowRender) {
currentPreviewFrame = sim.grid.renderFrameTimed(cam, outHeight, outWidth, frame::colormap::RGB, invFrameRate, reflectCount, globalIllumination, useLod);
} else {
currentPreviewFrame = sim.grid.fastRenderFrame(cam, outHeight, outWidth, frame::colormap::RGB);
}
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());
updatePreview = false;
textureInitialized = true;
}
void updateStatsCache() {
auto now = std::chrono::steady_clock::now();
if (statsNeedUpdate || std::chrono::duration_cast<std::chrono::seconds>(now - lastStatsUpdate).count() >= 2) {
std::stringstream gridstats;
sim.grid.printStats(gridstats);
cachedStats = gridstats.str();
lastStatsUpdate = now;
statsNeedUpdate = false;
}
}
};
#endif

View File

@@ -29,7 +29,7 @@
constexpr int Dim = 3;
template<typename T, typename IndexType = uint32_t>
template<typename T, typename IndexType = uint16_t>
class Octree {
public:
using PointType = Eigen::Matrix<float, Dim, 1>;
@@ -71,14 +71,39 @@ public:
bool active;
bool visible;
// --- NEW ACCUMULATION FIELDS ---
Eigen::Vector3f accumColorSum; // Sum of all samples currently in the "bucket"
float accumWeight; // Current number of samples in the "bucket"
int lastUpdateFrame; // Frame number this node was last hit
mutable std::mutex lightMutex; // Thread safety
// -------------------------------
NodeData(const T& data, const PointType& pos, bool visible, IndexType colorIdx, float size = 0.01f,
bool active = true, int objectId = -1, int subId = 0, IndexType materialIdx = 0)
: data(data), position(pos), objectId(objectId), subId(subId), size(size),
colorIdx(colorIdx), materialIdx(materialIdx), active(active), visible(visible) {}
colorIdx(colorIdx), materialIdx(materialIdx), active(active), visible(visible),
accumColorSum(Eigen::Vector3f::Zero()), accumWeight(0.0f), lastUpdateFrame(-1) {}
NodeData() : objectId(-1), subId(0), size(0.0f), colorIdx(0), materialIdx(0),
active(false), visible(false) {}
active(false), visible(false),
accumColorSum(Eigen::Vector3f::Zero()), accumWeight(0.0f), lastUpdateFrame(-1) {}
// Manual copy constructor needed because std::mutex is not copyable
NodeData(const NodeData& other) {
data = other.data;
position = other.position;
objectId = other.objectId;
subId = other.subId;
size = other.size;
colorIdx = other.colorIdx;
materialIdx = other.materialIdx;
active = other.active;
visible = other.visible;
accumColorSum = Eigen::Vector3f::Zero();
accumWeight = 0.0f;
lastUpdateFrame = -1;
}
PointType getHalfSize() const {
return PointType(size * 0.5f, size * 0.5f, size * 0.5f);
}
@@ -89,13 +114,6 @@ public:
}
};
struct RaycastHit {
std::shared_ptr<NodeData> node;
float distance;
PointType normal;
PointType hitPoint;
};
struct OctreeNode {
BoundingBox bounds;
std::vector<std::shared_ptr<NodeData>> points;
@@ -134,6 +152,11 @@ private:
Eigen::Vector3f skylight_ = {0.1f, 0.1f, 0.1f};
Eigen::Vector3f backgroundColor_ = {0.53f, 0.81f, 0.92f};
// Parallel Lightmap (Sun) settings
Eigen::Vector3f sunDirection_ = {0.0f, 1.0f, 0.0f}; // Default straight up
Eigen::Vector3f sunColor_ = {1.0f, 1.0f, 0.9f};
float sunIntensity_ = 1.0f; // 0.0 = disabled
// Addressable Maps
std::unique_ptr<std::mutex> mapMutex_;
@@ -146,6 +169,9 @@ private:
std::map<std::pair<int, int>, std::shared_ptr<Mesh>> meshCache_;
std::set<std::pair<int, int>> dirtyMeshes_;
int nextSubIdGenerator_ = 1;
// Temporal Coherence
int frameCount_ = 0;
public:
inline IndexType getColorIndex(const Eigen::Vector3f& color) {
@@ -242,7 +268,7 @@ private:
float lodFalloffRate_ = 0.1f; // Lower = better, higher = worse. 0-1
float lodMinDistance_ = 100.0f;
float maxDistance_ = 100000;
float maxDistance_ = size * size;
struct Ray {
PointType origin;
@@ -261,11 +287,6 @@ private:
uint8_t getOctant(const PointType& point, const PointType& center) const {
return (point[0] >= center[0]) | ((point[1] >= center[1]) << 1) | ((point[2] >= center[2]) << 2);
// uint8_t octant = 0;
// if (point[0] >= center[0]) octant |= 1;
// if (point[1] >= center[1]) octant |= 2;
// if (point[2] >= center[2]) octant |= 4;
// return octant;
}
BoundingBox createChildBounds(const OctreeNode* node, uint8_t octant) const {
@@ -641,89 +662,6 @@ private:
}
}
void insertHit(std::vector<RaycastHit>& hits, size_t maxHits, const std::shared_ptr<NodeData>& 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<RaycastHit>& 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);
@@ -767,26 +705,52 @@ private:
int maxBounces, bool globalIllumination, bool useLod) {
if (bounces > maxBounces) return globalIllumination ? skylight_ : Eigen::Vector3f::Zero();
auto hit = voxelTraverse(rayOrig, rayDir, std::numeric_limits<float>::max(), useLod);
PointType currOrig = rayOrig;
std::shared_ptr<NodeData> hit = nullptr;
PointType hitPoint;
PointType normal;
float t = 0.0f;
// Loop to skip internal boundaries of transmissive objects
int safety = 0;
while(safety++ < 5000) {
hit = voxelTraverse(currOrig, rayDir, std::numeric_limits<float>::max(), useLod);
if (!hit) break;
Ray ray(currOrig, rayDir);
rayCubeIntersect(ray, hit.get(), t, normal, hitPoint);
Material objMat = getMaterial(hit->materialIdx);
if (objMat.transmission > 0.0f && rayDir.dot(normal) > 0.0f) {
float coordMax = hitPoint.cwiseAbs().maxCoeff();
float rayOffset = std::max(1e-4f, 1e-5f * coordMax);
PointType checkPos = hitPoint + rayDir * (rayOffset * 2.0f);
auto nextNode = find(checkPos);
if (nextNode && nextNode->active && nextNode->materialIdx == hit->materialIdx) {
currOrig = checkPos;
continue;
}
}
break;
}
if (!hit) {
if (bounces == 0) return backgroundColor_;
return globalIllumination ? skylight_ : Eigen::Vector3f::Zero();
}
auto obj = hit;
PointType hitPoint;
PointType normal;
float t = 0.0f;
Ray ray(rayOrig, rayDir);
rayCubeIntersect(ray, obj.get(), t, normal, hitPoint);
Eigen::Vector3f calculatedColor = Eigen::Vector3f::Zero();
Eigen::Vector3f objColor = getColor(obj->colorIdx);
Material objMat = getMaterial(obj->materialIdx);
Eigen::Vector3f finalColor = globalIllumination ? skylight_ : Eigen::Vector3f::Zero();
Eigen::Vector3f lighting = globalIllumination ? skylight_ : Eigen::Vector3f::Zero();
if (objMat.emittance > 0.0f) {
finalColor += objColor * objMat.emittance;
lighting += objColor * objMat.emittance;
}
float roughness = std::clamp(objMat.roughness, 0.01f, 1.0f);
@@ -804,6 +768,36 @@ private:
Eigen::Vector3f F0 = Eigen::Vector3f::Constant(0.04f);
F0 = F0 * (1.0f - metallic) + objColor * metallic;
if (sunIntensity_ > 0.0f) {
PointType L = sunDirection_.normalized();
PointType shadowOrig = hitPoint + n_eff * rayOffset;
auto shadowHit = voxelTraverse(shadowOrig, L, std::numeric_limits<float>::max(), useLod);
if (!shadowHit) {
float NdotL = std::max(0.001f, n_eff.dot(L));
PointType H_sun = (V + L).normalized();
float VdotH_sun = std::max(0.001f, V.dot(H_sun));
float NdotH_sun = std::max(0.001f, n_eff.dot(H_sun));
Eigen::Vector3f F_sun = F0 + (Eigen::Vector3f::Constant(1.0f) - F0) * std::pow(std::max(0.0f, 1.0f - VdotH_sun), 5.0f);
float alpha2 = roughness * roughness * roughness * roughness;
float denom = (NdotH_sun * NdotH_sun * (alpha2 - 1.0f) + 1.0f);
float D = alpha2 / (M_PI * denom * denom);
float k = (roughness + 1.0f) * (roughness + 1.0f) / 8.0f;
float G = (NdotL / (NdotL * (1.0f - k) + k)) * (cosThetaI / (cosThetaI * (1.0f - k) + k));
Eigen::Vector3f spec = (F_sun * D * G) / (4.0f * NdotL * cosThetaI + EPSILON);
Eigen::Vector3f kD = (Eigen::Vector3f::Constant(1.0f) - F_sun) * (1.0f - metallic);
Eigen::Vector3f diff = kD.cwiseProduct(objColor) / M_PI;
lighting += (diff + spec).cwiseProduct(sunColor_) * sunIntensity_ * NdotL;
}
}
PointType H = sampleGGX(n_eff, roughness, rngState);
float VdotH = std::max(0.001f, V.dot(H));
@@ -870,22 +864,74 @@ private:
secondColor = W_second.cwiseProduct(traceRay(secondOrigin, secondDir, bounces + 1, rngState, maxBounces, globalIllumination, useLod));
}
return finalColor + specColor + secondColor;
calculatedColor = lighting + specColor + secondColor;
} else {
float totalLum = lumSpec + lumSecond;
if (totalLum < 0.0001f) return finalColor;
float pSpec = lumSpec / totalLum;
float roll = float(rand_r(&rngState)) / float(RAND_MAX);
if (roll < pSpec) {
Eigen::Vector3f sample = traceRay(hitPoint + n_eff * rayOffset, specDir, bounces + 1, rngState, maxBounces, globalIllumination, useLod);
return finalColor + (W_spec / std::max(EPSILON, pSpec)).cwiseProduct(sample);
} else {
Eigen::Vector3f sample = traceRay(secondOrigin, secondDir, bounces + 1, rngState, maxBounces, globalIllumination, useLod);
return finalColor + (W_second / std::max(EPSILON, 1.0f - pSpec)).cwiseProduct(sample);
if (totalLum < 0.0001f) calculatedColor = lighting;
else {
float pSpec = lumSpec / totalLum;
float roll = float(rand_r(&rngState)) / float(RAND_MAX);
if (roll < pSpec) {
Eigen::Vector3f sample = traceRay(hitPoint + n_eff * rayOffset, specDir, bounces + 1, rngState, maxBounces, globalIllumination, useLod);
calculatedColor = lighting + (W_spec / std::max(EPSILON, pSpec)).cwiseProduct(sample);
} else {
Eigen::Vector3f sample = traceRay(secondOrigin, secondDir, bounces + 1, rngState, maxBounces, globalIllumination, useLod);
calculatedColor = lighting + (W_second / std::max(EPSILON, 1.0f - pSpec)).cwiseProduct(sample);
}
}
}
std::lock_guard<std::mutex> lock(obj->lightMutex);
// 1. Calculate Capacity based on Material Properties
// How many frames do we keep in the "bucket" before removing the oldest?
float capacity = 60.0f; // Base capacity (e.g. Diffuse surfaces)
// REFLECTIONS: Fade out VERY fast (low capacity) to prevent ghosting on moving objects
// If it's metallic and smooth, we might only keep the last 2-5 frames.
float reflectionIntensity = metallic * (1.0f - roughness);
if (reflectionIntensity > 0.0f) {
// Reduces capacity down to a minimum of 2.0 frames for perfect mirrors
capacity = std::max(2.0f, capacity * (1.0f - reflectionIntensity));
}
// REFRACTIONS: Keep longer (high capacity) to stabilize noise
if (transmission > 0.0f) {
capacity += transmission * 40.0f;
}
// 2. Handle Time Gaps (Age Limit)
// If this node wasn't hit last frame, the object or camera moved.
// Decay aggressively.
if (obj->lastUpdateFrame != -1) {
int framesMissed = frameCount_ - obj->lastUpdateFrame;
if (framesMissed > 1) {
// Divide accumulated weight by 2 for every missed frame
float decay = std::pow(0.5f, (float)framesMissed);
obj->accumColorSum *= decay;
obj->accumWeight *= decay;
}
}
// 3. The "Leaky Bucket" Eviction
// If the bucket is full (weight >= capacity), we "pour out" the average old value
// before adding the new one.
if (obj->accumWeight >= capacity) {
// Scale down the sum and weight so that adding 1.0 brings it back to capacity
float keepRatio = (capacity - 1.0f) / obj->accumWeight;
obj->accumColorSum *= keepRatio;
obj->accumWeight *= keepRatio;
}
// 4. Accumulate
obj->accumColorSum += calculatedColor;
obj->accumWeight += 1.0f;
obj->lastUpdateFrame = frameCount_;
// 5. Return the Average
// This average represents the last 'capacity' frames moving average
return obj->accumColorSum / obj->accumWeight;
}
void clearNode(OctreeNode* node) {
@@ -1013,6 +1059,7 @@ private:
writeVal(out, pt->size);
writeVal(out, pt->colorIdx);
writeVal(out, pt->materialIdx);
// accumulatedLight is transient cache, do not serialize
}
if (!node->isLeaf) {
@@ -1054,6 +1101,8 @@ private:
readVal(in, pt->size);
readVal(in, pt->colorIdx);
readVal(in, pt->materialIdx);
pt->accumulatedLight = Eigen::Vector3f::Zero(); // Initialize clean
pt->lastUpdateFrame = -1;
node->points.push_back(pt);
}
@@ -1334,7 +1383,7 @@ private:
}
}
if (totalWeight > 1e-6) {
if (totalWeight > EPSILON) {
return {density, accumulatedColor / totalWeight};
}
@@ -1401,6 +1450,12 @@ public:
return backgroundColor_;
}
void setSun(const Eigen::Vector3f& direction, const Eigen::Vector3f& color, float intensity) {
sunDirection_ = direction.normalized();
sunColor_ = color;
sunIntensity_ = intensity;
}
void setLODFalloff(float rate) { lodFalloffRate_ = rate; }
void setLODMinDistance(float dist) { lodMinDistance_ = dist; }
void setMaxDistance(float dist) { maxDistance_ = dist; }
@@ -1443,6 +1498,10 @@ public:
writeVec3(out, skylight_);
writeVec3(out, backgroundColor_);
writeVec3(out, sunDirection_);
writeVec3(out, sunColor_);
writeVal(out, sunIntensity_);
{
std::lock_guard<std::mutex> lock(*mapMutex_);
size_t cMapSize = colorMap_.size();
@@ -1490,6 +1549,10 @@ public:
readVec3(in, skylight_);
readVec3(in, backgroundColor_);
readVec3(in, sunDirection_);
readVec3(in, sunColor_);
readVal(in, sunIntensity_);
{
std::lock_guard<std::mutex> lock(*mapMutex_);
colorMap_.clear();
@@ -1749,6 +1812,7 @@ public:
frame renderFrame(const Camera& cam, int height, int width, frame::colormap colorformat = frame::colormap::RGB, int samplesPerPixel = 2,
int maxBounces = 4, bool globalIllumination = false, bool useLod = true) {
frameCount_++; // Advance time
generateLODs();
PointType origin = cam.origin;
PointType dir = cam.direction.normalized();
@@ -1770,7 +1834,7 @@ public:
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int pidx = (y * width + x);
uint32_t seed = pidx * 1973 + 9277;
uint32_t seed = pidx * 1973 + 9277 + frameCount_ * 12345;
int idx = pidx * channels;
float px = (2.0f * (x + 0.5f) / width - 1.0f) * tanfovx;
@@ -1814,6 +1878,7 @@ public:
frame fastRenderFrame(const Camera& cam, int height, int width, frame::colormap colorformat = frame::colormap::RGB) {
//TIME_FUNCTION;
frameCount_++;
generateLODs();
PointType origin = cam.origin;
PointType dir = cam.direction.normalized();
@@ -1830,7 +1895,7 @@ public:
const float tanfovy = tanHalfFov;
const float tanfovx = tanHalfFov * aspect;
const PointType globalLightDir = (-cam.direction * 0.2f).normalized();
const PointType globalLightDir = sunDirection_.normalized();
const float fogStart = 1000.0f;
const float minVisibility = 0.2f;
@@ -1862,9 +1927,12 @@ public:
if (objMat.emittance > 0.0f) {
color = color * objMat.emittance;
} else {
// Use Sun Direction for quick shading
float diffuse = std::max(0.0f, normal.dot(globalLightDir));
float ambient = 0.35f;
float intensity = std::min(1.0f, ambient + diffuse * 0.65f);
if (sunIntensity_ > 0.0f) intensity = std::min(1.0f, ambient + diffuse * sunIntensity_);
color = color * intensity;
}
@@ -1888,6 +1956,7 @@ public:
frame renderFrameTimed(const Camera& cam, int height, int width, frame::colormap colorformat = frame::colormap::RGB,
double maxTimeSeconds = 0.16, int maxBounces = 4, bool globalIllumination = false, bool useLod = true) {
auto startTime = std::chrono::high_resolution_clock::now();
frameCount_++;
generateLODs();
PointType origin = cam.origin;
@@ -1906,7 +1975,7 @@ public:
const float tanfovy = tanHalfFov;
const float tanfovx = tanHalfFov * aspect;
const PointType globalLightDir = (-cam.direction * 0.2f).normalized();
const PointType globalLightDir = sunDirection_.normalized();
const float fogStart = 1000.0f;
const float minVisibility = 0.2f;
@@ -1940,6 +2009,7 @@ public:
float diffuse = std::max(0.0f, normal.dot(globalLightDir));
float ambient = 0.35f;
float intensity = std::min(1.0f, ambient + diffuse * 0.65f);
if (sunIntensity_ > 0.0f) intensity = std::min(1.0f, ambient + diffuse * sunIntensity_);
color = color * intensity;
}
@@ -2000,7 +2070,7 @@ public:
rayDir.normalize();
uint32_t pass = currentOffset / totalPixels;
uint32_t seed = pidx * 1973 + pass * 12345 + localSeed;
uint32_t seed = pidx * 1973 + pass * 12345 + localSeed + frameCount_ * 777;
Eigen::Vector3f pbrColor = traceRay(origin, rayDir, 0, seed, maxBounces, globalIllumination, useLod);
@@ -2369,12 +2439,8 @@ public:
}
size = 0;
}
void collectNodesByObjectId(int id, std::vector<std::shared_ptr<NodeData>>& results) const {
std::unordered_set<std::shared_ptr<NodeData>> seen;
collectNodesByObjectIdRecursive(root_.get(), id, results, seen);
frameCount_ = 0;
}
};
#endif
#endif

View File

@@ -189,7 +189,8 @@ inline void updateNoiseTexture(NoisePreviewState& state) {
glBindTexture(GL_TEXTURE_2D, state.textureId);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, state.width, state.height, 0, GL_RGB, GL_UNSIGNED_BYTE, state.pixelBuffer.data());
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, state.width, state.height,
0, GL_RGB, GL_UNSIGNED_BYTE, state.pixelBuffer.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

View File

@@ -837,57 +837,6 @@ public:
void addMoon() {
///TODO: using planetConfig, add moon(s).
TIME_FUNCTION;
const float realEarthRadiusKm = 6371.0f;
const float realMoonRadiusKm = 1737.4f;
const float realOrbitKm = 384400.0f;
float simScale = config.radius / realEarthRadiusKm;
float moonRadius = realMoonRadiusKm * simScale;
float orbitDistance = realOrbitKm * simScale;
std::cout << "--- MOON GENERATION ---" << std::endl;
std::cout << "Sim Scale: " << simScale << " units/km" << std::endl;
std::cout << "Moon Radius: " << moonRadius << " units" << std::endl;
std::cout << "Orbit Distance: " << orbitDistance << " units" << std::endl;
// Place the moon on the Z-axis to keep it distinct from the star (which is on the X-axis)
v3 moonCenter = config.center + v3(0.0f, 0.0f, orbitDistance);
v3 moonColor = v3(0.7f, 0.7f, 0.72f); // Pale gray color
// Surface area ratio Moon/Earth is ~0.074. We scale the points to maintain node density.
int moonPoints = std::max(1000, static_cast<int>(config.surfacePoints * 0.075f));
for (int i = 0; i < moonPoints; i++) {
float y = 1.0f - (i * 2.0f) / (moonPoints - 1);
float radiusY = std::sqrt(1.0f - y * y);
float Θ = Φ * i;
float x = std::cos(Θ) * radiusY;
float z = std::sin(Θ) * radiusY;
v3 dir(x, y, z);
v3 pos = moonCenter + dir * moonRadius;
Particle pt;
pt.altPos = std::make_unique<AltPositions>();
pt.altPos->originalPos = pos.cast<Eigen::half>();
pt.altPos->noisePos = pos.cast<Eigen::half>();
pt.altPos->tectonicPos = pos.cast<Eigen::half>();
pt.currentPos = pos;
pt.originColor = moonColor.cast<Eigen::half>();
pt.noiseDisplacement = 0.0f;
pt.surface = true;
config.surfaceNodes.emplace_back(pt);
grid.set(pt, pt.currentPos, true, pt.originColor.cast<float>(), config.voxelSize, true, 3, 0, 1.0, 0.0f, 0.0f, 1.0f);
}
grid.optimize();
std::cout << "Moon generation complete. Placed " << moonPoints << " nodes." << std::endl;
starAdded = true;
}
void stretchPlanet() {

View File

@@ -1,421 +0,0 @@
#ifndef WORLDBOX_HPP
#define WORLDBOX_HPP
#include <iostream>
#include <vector>
#include <chrono>
#include <thread>
#include <mutex>
#include <cmath>
#include <random>
#include <algorithm>
#include "../grid/grid3eigen.hpp"
#include "../timing_decorator.cpp"
using v3 = Eigen::Vector3f;
struct WorldVoxel {
float nutrients = 1.0f;
float moisture = 0.5f;
int type = 0; // 0=Dirt, 1=Rock, 2=Grass, 3=Star, 4=Cloud, 5=Rain
WorldVoxel() = default;
WorldVoxel(float nut, float mois, int t) : nutrients(nut), moisture(mois), type(t) {}
};
struct WorldBoxConfig {
v3 center = v3(0, 0, 0);
float worldSizeX = 1000.0f;
float worldSizeZ = 1000.0f;
float worldDepth = 20.0f;
float voxelSize = 2.0f;
v3 baseDirtColor = v3(0.36f, 0.25f, 0.14f);
v3 baseRockColor = v3(0.45f, 0.45f, 0.45f);
float gridSizeCubeMin = 1024.0f;
// Grass Config
float grassDensity = 0.05f;
v3 grassColorBase = v3(0.2f, 0.6f, 0.15f);
// Star Config
bool enableStarRotation = false; // Off by default
float starOrbitRadius = 800.0f;
float starPanelSize = 100.0f;
float starVoxelSize = 10.0f;
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 {
public:
WorldBoxConfig config;
Octree<WorldVoxel> grid;
std::mt19937 rng;
std::vector<v3> starVoxelPositions;
std::vector<CloudVoxel> clouds;
std::vector<RainDrop> rainDrops;
float physicsTimer = 0.0f;
worldboxsim() : rng(42) {
config = WorldBoxConfig();
grid = Octree<WorldVoxel>(v3(-config.gridSizeCubeMin, -config.gridSizeCubeMin, -config.gridSizeCubeMin),v3(config.gridSizeCubeMin, config.gridSizeCubeMin, config.gridSizeCubeMin), 16, 32);
grid.setBackgroundColor(v3(0.53f, 0.81f, 0.92f));
}
void updateStar(float dt) {
if (!config.enableStarRotation) {
if (!starVoxelPositions.empty()) {
for(const auto& pos : starVoxelPositions) {
grid.remove(pos);
}
starVoxelPositions.clear();
}
return;
}
// Calculate rotation
config.starAngle += dt * config.starSpeed;
if (config.starAngle > 2 * M_PI) config.starAngle -= 2 * M_PI;
// 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);
// 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);
v3 right = worldUp.cross(n).normalized();
v3 up = n.cross(right).normalized();
int halfGrid = std::max(1, static_cast<int>((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<v3> newPositions;
newPositions.reserve((2 * halfGrid + 1) * (2 * halfGrid + 1));
for (int i = -halfGrid; i <= halfGrid; ++i) {
for (int j = -halfGrid; j <= halfGrid; ++j) {
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<float> randX(-config.worldSizeX/2, config.worldSizeX/2);
std::uniform_real_distribution<float> randZ(-config.worldSizeZ/2, config.worldSizeZ/2);
std::uniform_real_distribution<float> randY(config.cloudHeight - 10.0f, config.cloudHeight + 10.0f);
for (int i=0; i<config.cloudCount; ++i) {
v3 center(randX(rng), randY(rng), randZ(rng));
int numVoxels = 10 + (rng() % 20);
for (int j=0; j<numVoxels; ++j) {
v3 offset(
(rng() % 40) - 20,
(rng() % 10) - 5,
(rng() % 40) - 20
);
v3 pos = center + offset;
float size = config.cloudBaseSize + (rng() % 6);
WorldVoxel vox(0.0f, 1.0f, 4); // Type 4 = Cloud
// Adding to grid with transmission=0.4f (makes it partially transparent for RTX)
grid.set(vox, pos, true, config.cloudColor, size, true, 4, 0, 0.0f, 1.0f, 0.0f, 0.4f);
clouds.push_back({pos, size});
}
}
}
void updateWeatherAndPhysics(float dt) {
float halfX = config.worldSizeX / 2.0f;
float halfZ = config.worldSizeZ / 2.0f;
// 1. Clouds Update
std::vector<CloudVoxel> 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<float> 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<RainDrop> 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<std::shared_ptr<Octree<WorldVoxel>::NodeData>> nodes;
grid.collectNodesByObjectId( -1, nodes);
std::vector<std::shared_ptr<Octree<WorldVoxel>::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);
}
}
}
void generateGrass() {
TIME_FUNCTION;
float halfX = config.worldSizeX / 2.0f;
float halfZ = config.worldSizeZ / 2.0f;
float surfaceY = 0.0f;
int stepsX = static_cast<int>(std::round(config.worldSizeX / config.voxelSize)) + 1;
int stepsZ = static_cast<int>(std::round(config.worldSizeZ / config.voxelSize)) + 1;
int grassCount = 0;
#pragma omp parallel
{
std::random_device rd;
std::mt19937 local_rng(rd() ^ std::hash<std::thread::id>()(std::this_thread::get_id()));
std::uniform_real_distribution<float> probDist(0.0f, 1.0f);
std::uniform_int_distribution<int> grassHeightDist(1, 8);
#pragma omp for schedule(static) collapse(2)
for (int i = 0; i < stepsX; ++i) {
for (int j = 0; j < stepsZ; ++j) {
float x = -halfX + i * config.voxelSize;
float z = -halfZ + j * config.voxelSize;
if (x > halfX || z > halfZ) continue;
if (probDist(local_rng) < config.grassDensity) {
int gHeight = grassHeightDist(local_rng);
float gSize = config.voxelSize / 25.0f;
std::uniform_real_distribution<float> offDist(-config.voxelSize/2.0f + gSize/2.0f, config.voxelSize/2.0f - gSize/2.0f);
float offsetX = offDist(local_rng);
float offsetZ = offDist(local_rng);
WorldVoxel gVox(1.0f, 0.8f, 2); // Type 2 = Grass
float baseY = surfaceY + (config.voxelSize / 2.0f) + (gSize / 2.0f);
#pragma omp critical
{
for (int g = 0; g < gHeight; ++g) {
v3 gPos(x + offsetX, baseY + g * gSize, z + offsetZ);
grid.set(gVox, gPos, true, config.grassColorBase, gSize, true, 1, 0);
grassCount++;
}
}
}
}
}
}
std::cout << "Grass generation complete. Placed " << grassCount << " grass voxels." << std::endl;
}
void generateFlatWorld() {
TIME_FUNCTION;
grid.clear();
float halfX = config.worldSizeX / 2.0f;
float halfZ = config.worldSizeZ / 2.0f;
float surfaceY = 0.0f;
// 1. Calculate integer bounds to satisfy OpenMP
int stepsX = static_cast<int>(std::round(config.worldSizeX / config.voxelSize)) + 1;
int stepsZ = static_cast<int>(std::round(config.worldSizeZ / config.voxelSize)) + 1;
int stepsY = static_cast<int>(std::round(config.worldDepth / config.voxelSize)) + 1;
size_t maxSteps = stepsX * stepsZ * stepsY;
int nodeCount = 0;
#pragma omp parallel for schedule(static) collapse(3)
for (int i = 0; i < stepsX; ++i) {
for (int j = 0; j < stepsZ; ++j) {
for (int k = 0; k < stepsY; ++k) {
float x = -halfX + i * config.voxelSize;
float z = -halfZ + j * config.voxelSize;
float y = surfaceY - k * config.voxelSize;
if (x > halfX || z > halfZ || y < surfaceY - config.worldDepth) {
continue;
}
WorldVoxel voxel;
v3 color;
float depthRatio = std::abs(y - surfaceY) / config.worldDepth;
if (depthRatio > 0.8f) {
voxel.type = 1;
voxel.nutrients = 0.1f;
color = config.baseRockColor;
} else {
voxel.type = 0;
voxel.nutrients = 1.0f - depthRatio;
color = config.baseDirtColor;
}
v3 pos(x, y, z);
#pragma omp critical
grid.set(voxel, pos, true, color, config.voxelSize, true, 1, 0);
// nodeCount++;
}
}
}
std::cout << "World generation complete. Placed " << nodeCount << " voxels." << std::endl;
}
void clearWorld() {
grid.clear();
clouds.clear();
rainDrops.clear();
starVoxelPositions.clear();
}
};
#endif