aboutsummaryrefslogtreecommitdiff
path: root/apps/openmb/main.cpp
diff options
context:
space:
mode:
authorEthan Morgan <ethan@gweithio.com>2026-02-14 16:44:06 +0000
committerEthan Morgan <ethan@gweithio.com>2026-02-14 16:44:06 +0000
commit54409423f767d8b1cf30cb7d0efca6b4ca138823 (patch)
treed915ac7828703ce4b963efdd9728a1777ba18c1e /apps/openmb/main.cpp
move to own git serverHEADmaster
Diffstat (limited to 'apps/openmb/main.cpp')
-rw-r--r--apps/openmb/main.cpp1753
1 files changed, 1753 insertions, 0 deletions
diff --git a/apps/openmb/main.cpp b/apps/openmb/main.cpp
new file mode 100644
index 0000000..54027b9
--- /dev/null
+++ b/apps/openmb/main.cpp
@@ -0,0 +1,1753 @@
+#if defined( __APPLE__ )
+#define GL_SILENCE_DEPRECATION
+#endif
+#if defined( __APPLE__ )
+#define GL_SILENCE_DEPRECATION
+#endif
+
+#define GLFW_INCLUDE_NONE
+#include <GLFW/glfw3.h>
+#ifdef __APPLE__
+#include <OpenGL/gl3.h>
+#endif
+
+#include <chrono>
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/type_ptr.hpp>
+#include <iostream>
+#include <vector>
+
+#include "ImguiStyle.hpp"
+#include <imgui.h>
+#include <imgui_impl_glfw.h>
+#include <imgui_impl_opengl3.h>
+
+#include "renderer/DirectionalLight.hpp"
+#include "renderer/EditorHelpers.hpp"
+#include "renderer/GLHelpers.hpp"
+#include "renderer/Model.hpp"
+#include "renderer/SSAORenderer.hpp"
+#include "renderer/Shader.hpp"
+#include "renderer/Skybox.hpp"
+#include "renderer/Texture.hpp"
+#include "renderer/TextureManager.hpp"
+#include "renderer/primitives.hpp"
+#include "scene/Camera.hpp"
+
+#include "scene/GridSystem.hpp"
+#include "scene/VoxelEditor.hpp"
+#include <algorithm>
+#include <assimp/Importer.hpp>
+#include <assimp/postprocess.h>
+#include <assimp/scene.h>
+#include <filesystem>
+#include <fstream>
+#include <nlohmann/json.hpp>
+
+void keyCallback ( GLFWwindow* window, int key, int scancode, int action, int mods ) {}
+
+int main () {
+ if( !glfwInit() ) {
+ std::cerr << "Failed to initialize GLFW!" << std::endl;
+ return -1;
+ }
+
+ glfwWindowHint( GLFW_DEPTH_BITS, 24 );
+
+ glfwWindowHint( GLFW_CONTEXT_VERSION_MAJOR, 3 );
+ glfwWindowHint( GLFW_CONTEXT_VERSION_MINOR, 3 );
+ glfwWindowHint( GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE );
+
+#ifdef __APPLE__
+ glfwWindowHint( GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE );
+#endif
+
+ int width = 1280;
+ int height = 720;
+ GLFWwindow* window = glfwCreateWindow( width, height, "oh yeah, thats hotsauce alright", nullptr, nullptr );
+ if( !window ) {
+ std::cerr << "Failed to create GLFW window!" << std::endl;
+ glfwTerminate();
+ return -1;
+ }
+ glfwMakeContextCurrent( window );
+ glfwSwapInterval( 1 );
+
+ glfwMaximizeWindow( window );
+ int fbWidth = 0, fbHeight = 0;
+ glfwGetFramebufferSize( window, &fbWidth, &fbHeight );
+ if( fbWidth == 0 || fbHeight == 0 ) {
+ fbWidth = width;
+ fbHeight = height;
+ }
+ bool ePrev = false;
+ bool mouseCaptured = true;
+
+ glViewport( 0, 0, fbWidth, fbHeight );
+ glfwSetFramebufferSizeCallback( window, [] ( GLFWwindow* /*w*/, int w, int h ) { glViewport( 0, 0, w, h ); } );
+ glfwSetKeyCallback( window, keyCallback );
+
+ if( !renderer::initGL() ) {
+ std::cerr << "Failed to initialize OpenGL" << std::endl;
+ glfwDestroyWindow( window );
+ glfwTerminate();
+ return -1;
+ }
+
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGuiIO& io = ImGui::GetIO();
+ (void)io;
+ ImGui::StyleColorsDark();
+ ApplyImguiTheme();
+ ImGui_ImplGlfw_InitForOpenGL( window, true );
+ ImGui_ImplOpenGL3_Init( "#version 330 core" );
+
+ renderer::Shader shader;
+ const std::string vertPath = "apps/openmb/resources/shaders/cube.vert";
+ const std::string fragPath = "apps/openmb/resources/shaders/cube.frag";
+ if( !shader.fromFiles( vertPath, fragPath ) ) {
+ std::cerr << "Failed to load/compile shader files: " << vertPath << " and " << fragPath << std::endl;
+ glfwDestroyWindow( window );
+ glfwTerminate();
+ return -1;
+ }
+
+ scene::GridSystem gridSystem;
+
+ renderer::Mesh cube = renderer::primitives::makeCube();
+ renderer::Mesh texturedCube = renderer::primitives::makeTexturedCubeWithNormals();
+ renderer::Mesh grid = renderer::primitives::makeGrid( 100, gridSystem.getCellSize() );
+
+ renderer::Skybox skybox;
+ if( !skybox.loadFromDirectory( "apps/openmb/resources/skybox" ) ) {
+ std::cerr << "Warning: failed to load skybox from resources/skybox" << std::endl;
+ }
+
+ renderer::Shader texShader;
+ const std::string tVert = "apps/openmb/resources/shaders/textured.vert";
+ const std::string tFrag = "apps/openmb/resources/shaders/textured_lit.frag";
+ if( !texShader.fromFiles( tVert, tFrag ) ) {
+ std::cerr << "Failed to load textured shader files: " << tVert << " and " << tFrag << std::endl;
+ } else {
+ texShader.use();
+ texShader.setInt( "radialEnabled", 0 );
+ texShader.setFloat( "radialInner", 0.38f );
+ texShader.setFloat( "radialOuter", 0.5f );
+ texShader.setInt( "albedo", 0 );
+ texShader.setInt( "normalMap", 1 );
+ texShader.setInt( "normalEnabled", 0 );
+ texShader.setFloat( "normalStrength", 1.0f );
+ texShader.setInt( "shadowMap", 2 );
+ texShader.setInt( "ssao", 3 );
+ texShader.setFloat( "aoStrength", 1.0f );
+ }
+
+ glm::vec3 loadedSunDir( 0.3f, 1.0f, 0.5f );
+ glm::vec3 loadedSunColor( 1.0f, 0.98f, 0.9f );
+ float loadedSunIntensity = 1.0f;
+ bool loadedSunAvailable = false;
+
+ renderer::DirectionalLight sun;
+ sun.setDirection( glm::normalize( glm::vec3( 0.3f, 1.0f, 0.5f ) ) );
+ sun.setColor( glm::vec3( 1.0f, 0.98f, 0.9f ) );
+ sun.setIntensity( 1.0f );
+ static bool lightingEnabled = true;
+
+ static float shadowBiasMinGui = 0.00200f;
+ static float shadowBiasScaleGui = 0.005f;
+ static int pcfRadiusGui = 0;
+ static bool snapToTexels = true;
+
+ // Fog parameters (world-space exponential fog)
+ static glm::vec3 fogColor = glm::vec3( 0.6f, 0.65f, 0.7f );
+ static float fogDensity = 0.0741f;
+ static float fogAmount = 1.f;
+
+ // God-rays parameters
+ static bool godraysEnabled = false;
+ // overall multiplier applied to the god-rays composite (keeps defaults conservative)
+ static float godraysIntensity = 0.08f;
+ static int godraysSamples = 30;
+ static float godraysDensity = 0.8f;
+ static float godraysWeight = 0.4f;
+ static float godraysDecay = 0.95f;
+ static int godraysDownscale = 4; // occlusion texture downscale factor
+
+ texShader.use();
+ texShader.setFloat( "shadowBiasMin", shadowBiasMinGui );
+ texShader.setFloat( "shadowBiasScale", shadowBiasScaleGui );
+ texShader.setInt( "pcfRadius", pcfRadiusGui );
+
+ const unsigned int shadowWidth = 4096, shadowHeight = 4096;
+ renderer::Shader depthShader;
+ const std::string dVert = "apps/openmb/resources/shaders/depth.vert";
+ const std::string dFrag = "apps/openmb/resources/shaders/depth.frag";
+ if( !depthShader.fromFiles( dVert, dFrag ) ) {
+ std::cerr << "Warning: failed to load depth shader for shadow mapping" << std::endl;
+ }
+
+ unsigned int depthMapFBO = 0;
+ unsigned int depthMap = 0;
+ glGenFramebuffers( 1, &depthMapFBO );
+ glGenTextures( 1, &depthMap );
+ glBindTexture( GL_TEXTURE_2D, depthMap );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowWidth, shadowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
+ nullptr );
+
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER );
+ float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+ glTexParameterfv( GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor );
+
+ // We'll perform manual depth comparisons in the shader, so disable sampler compare mode.
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL ); // ignored when compare mode is NONE
+
+ glBindFramebuffer( GL_FRAMEBUFFER, depthMapFBO );
+ glFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0 );
+ glDrawBuffer( GL_NONE );
+ glReadBuffer( GL_NONE );
+ if( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE ) {
+ std::cerr << "Warning: Shadow framebuffer not complete" << std::endl;
+ }
+ glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+
+ // --- God rays / occlusion resources ---
+ renderer::Shader occlusionShader;
+ const std::string occVert = "apps/openmb/resources/shaders/occlusion.vert";
+ const std::string occFrag = "apps/openmb/resources/shaders/occlusion.frag";
+ if( !occlusionShader.fromFiles( occVert, occFrag ) ) {
+ std::cerr << "Warning: failed to load occlusion shader" << std::endl;
+ }
+
+ renderer::Shader godraysShader;
+ const std::string grVert = "apps/openmb/resources/shaders/godrays_quad.vert";
+ const std::string grFrag = "apps/openmb/resources/shaders/godrays_radial.frag";
+ if( !godraysShader.fromFiles( grVert, grFrag ) ) {
+ std::cerr << "Warning: failed to load godrays shader" << std::endl;
+ }
+
+ // fullscreen quad (NDC coords)
+ renderer::Mesh quadMesh;
+ {
+ std::vector<float> q = {
+ // pos.x, pos.y, pos.z, u, v
+ -1.0f,
+ -1.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ -1.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ -1.0f,
+ -1.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ 0.0f,
+ 1.0f,
+ 1.0f,
+ -1.0f,
+ 1.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ };
+ quadMesh.createFromPosTex( q );
+ }
+
+ // occlusion FBO (low-res)
+ unsigned int occlusionFBO = 0;
+ unsigned int occlusionTex = 0;
+ unsigned int occlusionDepthRBO = 0;
+ glGenFramebuffers( 1, &occlusionFBO );
+ glGenTextures( 1, &occlusionTex );
+ int occlusionW = 0;
+ int occlusionH = 0;
+ // we'll allocate size at first frame (when framebuffer size is known)
+
+ glm::mat4 dirLightSpace( 1.0f );
+
+ renderer::TextureManager textureManager;
+ textureManager.scanDirectory( "apps/openmb/resources/textures" );
+
+ std::vector<std::unique_ptr<renderer::Model>> models;
+ std::vector<std::string> modelNames;
+ {
+ std::string modelsDir = "apps/openmb/resources/models/rocks";
+ if( std::filesystem::exists( modelsDir ) ) {
+ for( const auto& entry : std::filesystem::directory_iterator( modelsDir ) ) {
+ if( entry.path().extension() == ".obj" ) {
+ auto mptr = std::make_unique<renderer::Model>();
+ if( mptr->loadFromFile( entry.path().string() ) ) {
+ modelNames.push_back( entry.path().stem().string() );
+ models.push_back( std::move( mptr ) );
+ } else {
+ std::cerr << "Warning: failed to load model: " << entry.path().string() << std::endl;
+ }
+ }
+ }
+ }
+ }
+
+ renderer::Texture rocksAlbedo;
+ renderer::Texture rocksNormal;
+ bool rocksAlbedoLoaded =
+ rocksAlbedo.loadFromFile( "apps/openmb/resources/models/rocks/Rocks_lp_SM_Rock_01_BaseMap.png" );
+ bool rocksNormalLoaded =
+ rocksNormal.loadFromFile( "apps/openmb/resources/models/rocks/Rocks_lp_SM_Rock_01_Normal.png" );
+ if( !rocksAlbedoLoaded ) {
+ std::cerr << "Warning: failed to load rocks albedo at resources/models/rocks/Rocks_lp_SM_Rock_01_BaseMap.png"
+ << std::endl;
+ }
+ if( !rocksNormalLoaded ) {
+ std::cerr << "Warning: failed to load rocks normal at resources/models/rocks/Rocks_lp_SM_Rock_01_Normal.png"
+ << std::endl;
+ }
+
+ std::filesystem::path bluePath = std::filesystem::path( "apps/openmb/resources/textures/brush/basic/blue.tga" );
+ if( !std::filesystem::exists( bluePath ) ) {
+ std::filesystem::create_directories( bluePath.parent_path() );
+ std::ofstream out( bluePath, std::ios::binary );
+ if( out ) {
+ unsigned char header[18] = { 0 };
+ header[2] = 2;
+ header[12] = 1;
+ header[13] = 0;
+ header[14] = 1;
+ header[15] = 0;
+ header[16] = 32;
+ out.write( reinterpret_cast<char*>( header ), 18 );
+ unsigned char pixel[4] = { 255, 0, 0, 200 };
+ out.write( reinterpret_cast<char*>( pixel ), 4 );
+ out.close();
+ std::cout << "Wrote blue texture to " << bluePath.string() << std::endl;
+ }
+ textureManager.scanDirectory( "apps/openmb/resources/textures" );
+ }
+ textureManager.setCurrentTexture( "brush", "basic", "blue.tga" );
+
+ auto categories = textureManager.getCategories();
+ if( !categories.empty() ) {
+ auto subcategories = textureManager.getSubcategories( categories[0] );
+ if( !subcategories.empty() ) {
+ auto textures = textureManager.getTextureNames( categories[0], subcategories[0] );
+ if( !textures.empty() ) {
+ textureManager.setCurrentTexture( categories[0], subcategories[0], textures[0] );
+ }
+ }
+ }
+
+ const float tileSize = gridSystem.getCellSize();
+
+ static float brushRadius = 1.5f;
+ static float prevBrushRadius = -1.0f;
+
+ static float brushHeight = gridSystem.getCellSize();
+ enum class PlacementBrush {
+ Brush = 0,
+ Cube = 1,
+ Model = 2,
+ };
+
+ static PlacementBrush placementBrush = PlacementBrush::Model;
+ static bool useCircleBrush = ( placementBrush == PlacementBrush::Brush );
+ static int selectedModelIndex = 0;
+ static float selectedModelScale = 0.017f;
+ static float selectedModelYaw = 0.0f;
+ static bool placeCollidable = true;
+
+ renderer::Mesh circleWire = renderer::editor::makeCircleWire( brushRadius, 64 );
+
+ renderer::Mesh filledCircle = renderer::editor::makeCircleFilled( 1.0f, 64 );
+
+ struct PaintedCircle {
+ glm::vec3 mCenter;
+ float mRadius;
+ int mTextureId;
+ };
+
+ std::vector<PaintedCircle> paintedCircles;
+
+ static double lastPaintTime = 0.0;
+ const double paintInterval = 0.08;
+
+ std::vector<std::pair<glm::vec3, glm::vec3>> worldBoxes;
+ std::vector<std::pair<glm::vec3, glm::vec3>> baseWorldBoxes;
+
+ scene::VoxelEditor voxelEditor( gridSystem );
+
+ const std::string defaultLevelPath = "apps/openmb/resources/levels/default.json";
+ if( std::filesystem::exists( defaultLevelPath ) ) {
+ if( voxelEditor.loadFromFile( defaultLevelPath ) ) {
+ std::cout << "Loaded default level (voxels) from " << defaultLevelPath << std::endl;
+ } else {
+ std::cerr << "Failed to load default level voxels" << std::endl;
+ }
+
+ try {
+ std::ifstream in( defaultLevelPath );
+ if( in ) {
+ nlohmann::json j;
+ in >> j;
+ if( j.contains( "paintedCircles" ) && j["paintedCircles"].is_array() ) {
+ for( const auto& pcj : j["paintedCircles"] ) {
+ PaintedCircle pc;
+ pc.mCenter.x = pcj.value( "x", 0.0f );
+ pc.mCenter.y = pcj.value( "y", 0.0f );
+ pc.mCenter.z = pcj.value( "z", 0.0f );
+ pc.mRadius = pcj.value( "radius", 1.0f );
+ pc.mTextureId = pcj.value( "textureId", 0 );
+ paintedCircles.push_back( pc );
+ }
+ std::cout << "Loaded " << paintedCircles.size() << " painted circles from " << defaultLevelPath
+ << std::endl;
+ }
+ if( j.contains( "sun" ) && j["sun"].is_object() ) {
+ try {
+ const auto& sj = j["sun"];
+ if( sj.contains( "direction" ) && sj["direction"].is_array() && sj["direction"].size() >= 3 ) {
+ loadedSunDir.x = sj["direction"][0].get<float>();
+ loadedSunDir.y = sj["direction"][1].get<float>();
+ loadedSunDir.z = sj["direction"][2].get<float>();
+ }
+ if( sj.contains( "color" ) && sj["color"].is_array() && sj["color"].size() >= 3 ) {
+ loadedSunColor.r = sj["color"][0].get<float>();
+ loadedSunColor.g = sj["color"][1].get<float>();
+ loadedSunColor.b = sj["color"][2].get<float>();
+ }
+ if( sj.contains( "intensity" ) )
+ loadedSunIntensity = sj["intensity"].get<float>();
+ loadedSunAvailable = true;
+ } catch( ... ) {
+ }
+ }
+ }
+ } catch( ... ) {
+ }
+ }
+
+ if( loadedSunAvailable ) {
+ sun.setDirection( glm::normalize( loadedSunDir ) );
+ sun.setColor( loadedSunColor );
+ sun.setIntensity( loadedSunIntensity );
+ }
+
+ renderer::Mesh wireCube = renderer::editor::makeWireCube( tileSize );
+
+ auto rayAABBIntersect = [&] ( const glm::vec3& ro, const glm::vec3& rd, const glm::vec3& bmin, const glm::vec3& bmax,
+ float& outT ) -> bool {
+ float tmin = -FLT_MAX;
+ float tmax = FLT_MAX;
+ for( int i = 0; i < 3; ++i ) {
+ float origin = ro[i];
+ float dir = rd[i];
+ float minB = bmin[i];
+ float maxB = bmax[i];
+ if( std::abs( dir ) < 1e-6f ) {
+ if( origin < minB || origin > maxB )
+ return false;
+ } else {
+ float t1 = ( minB - origin ) / dir;
+ float t2 = ( maxB - origin ) / dir;
+ if( t1 > t2 )
+ std::swap( t1, t2 );
+ tmin = std::max( tmin, t1 );
+ tmax = std::min( tmax, t2 );
+ if( tmin > tmax )
+ return false;
+ }
+ }
+ if( tmax < 0.0f )
+ return false;
+ outT = tmin >= 0.0f ? tmin : tmax;
+ return true;
+ };
+
+ bool editorActive = false;
+ glm::vec3 lastRayOrigin( 0.0f );
+ glm::vec3 lastRayDir( 0.0f );
+
+ int initialFBW = 0, initialFBH = 0;
+ glfwGetFramebufferSize( window, &initialFBW, &initialFBH );
+ if( initialFBW == 0 || initialFBH == 0 ) {
+ initialFBW = width;
+ initialFBH = height;
+ }
+ glViewport( 0, 0, initialFBW, initialFBH );
+
+ scene::Camera camera( glm::vec3( 0.0f, 4.0f, 6.0f ), glm::vec3( 0.0f, 4.0f, 0.0f ), -90.0f, 0.0f );
+ glfwSetInputMode( window, GLFW_CURSOR, GLFW_CURSOR_DISABLED );
+ double lastX = width / 2.0, lastY = height / 2.0;
+ bool firstMouse = true;
+ float lastFrame = 0.0f;
+ bool fPrev = false;
+ bool spacePrev = false;
+ bool zPrev = false;
+ bool yPrev = false;
+ bool lPrev = false;
+ bool placementEditorEnabled = true;
+ double lastTime = 0.0;
+ double deltaTime = 0.0;
+ double fps = 0.0;
+
+ using Clock = std::chrono::high_resolution_clock;
+ auto instLastReport = Clock::now();
+ double instAccumShadow = 0.0;
+ double instAccumSSAO = 0.0;
+ double instAccumDraw = 0.0;
+ double instAccumUI = 0.0;
+ double instAccumFrame = 0.0;
+ int instFrames = 0;
+
+ while( !glfwWindowShouldClose( window ) ) {
+ auto instFrameStart = Clock::now();
+ auto instSegStart = instFrameStart;
+ float currentFrame = static_cast<float>( glfwGetTime() );
+ float deltaTime = currentFrame - lastFrame;
+ lastFrame = currentFrame;
+
+ bool fCur = glfwGetKey( window, GLFW_KEY_F ) == GLFW_PRESS;
+ if( fCur && !fPrev )
+ camera.toggleFly();
+ fPrev = fCur;
+
+ bool spaceCur = glfwGetKey( window, GLFW_KEY_SPACE ) == GLFW_PRESS;
+ if( spaceCur && !spacePrev )
+ camera.jump();
+ spacePrev = spaceCur;
+
+ bool eCur = glfwGetKey( window, GLFW_KEY_E ) == GLFW_PRESS;
+ if( eCur && !ePrev ) {
+ mouseCaptured = !mouseCaptured;
+ if( mouseCaptured ) {
+ glfwSetInputMode( window, GLFW_CURSOR, GLFW_CURSOR_DISABLED );
+ } else {
+ glfwSetInputMode( window, GLFW_CURSOR, GLFW_CURSOR_NORMAL );
+ }
+ }
+ ePrev = eCur;
+
+ bool ctrl = ( glfwGetKey( window, GLFW_KEY_LEFT_CONTROL ) == GLFW_PRESS ) ||
+ ( glfwGetKey( window, GLFW_KEY_RIGHT_CONTROL ) == GLFW_PRESS );
+ bool zCur = glfwGetKey( window, GLFW_KEY_Z ) == GLFW_PRESS;
+ bool yCur = glfwGetKey( window, GLFW_KEY_Y ) == GLFW_PRESS;
+ bool lCur = glfwGetKey( window, GLFW_KEY_L ) == GLFW_PRESS;
+
+ if( ctrl && zCur && !zPrev ) {
+ voxelEditor.undo();
+ } else if( ctrl && yCur && !yPrev ) {
+ voxelEditor.redo();
+ }
+
+ zPrev = zCur;
+ yPrev = yCur;
+ if( lCur && !lPrev ) {
+ placementEditorEnabled = !placementEditorEnabled;
+ }
+ lPrev = lCur;
+
+ double now = glfwGetTime();
+ deltaTime = now - lastTime;
+ lastTime = now;
+ fps = 1.0 / deltaTime;
+
+ ImGui_ImplOpenGL3_NewFrame();
+ ImGui_ImplGlfw_NewFrame();
+ ImGui::NewFrame();
+
+ if( mouseCaptured ) {
+ ImGuiIO& io = ImGui::GetIO();
+
+ for( int i = 0; i < IM_ARRAYSIZE( io.MouseDown ); ++i )
+ io.MouseDown[i] = false;
+ io.WantCaptureMouse = false;
+ io.WantCaptureKeyboard = false;
+ }
+
+ {
+ static bool requireFlyForEditing = true;
+
+ editorActive = ( !requireFlyForEditing || camera.isFlying() ) && placementEditorEnabled;
+ ImGui::Begin( "Player" );
+ ImGui::Text( "Flying: %s", camera.isFlying() ? "Yes" : "No" );
+ ImGui::Text( "Grounded: %s", camera.isGrounded() ? "Yes" : "No" );
+ float sm = camera.getSpeedMultiplier();
+ ImGui::Text( "Speed x%.2f", sm );
+ if( ImGui::Button( "Toggle Fly" ) )
+ camera.toggleFly();
+ if( ImGui::Button( "Jump" ) )
+ camera.jump();
+ ImGui::Separator();
+ ImGui::Separator();
+ ImGui::Text( "Edit: Ctrl+Z undo, Ctrl+Y redo" );
+ ImGui::Text( "Undo stack: %zu", voxelEditor.getUndoStackSize() );
+ ImGui::Text( "Redo stack: %zu", voxelEditor.getRedoStackSize() );
+
+ ImGui::Separator();
+ ImGui::Text( "Level Management:" );
+
+ static char levelName[128] = "my_level";
+ ImGui::InputText( "Level Name", levelName, IM_ARRAYSIZE( levelName ) );
+
+ if( ImGui::Button( "Save Level" ) ) {
+ std::filesystem::create_directories( "apps/openmb/resources/levels" );
+ std::string filepath = std::string( "apps/openmb/resources/levels/" ) + levelName + ".json";
+ try {
+ nlohmann::json j;
+ j["version"] = 1;
+
+ nlohmann::json voxels = nlohmann::json::array();
+ for( const auto& cell : voxelEditor.getPlacedCells() ) {
+ nlohmann::json v;
+ v["x"] = cell.x;
+ v["y"] = cell.y;
+ v["z"] = cell.z;
+ v["textureId"] = voxelEditor.getTextureIdForCell( cell );
+ voxels.push_back( v );
+ }
+ j["voxels"] = voxels;
+
+ nlohmann::json pcs = nlohmann::json::array();
+ for( const auto& pc : paintedCircles ) {
+ nlohmann::json p;
+ p["x"] = pc.mCenter.x;
+ p["y"] = pc.mCenter.y;
+ p["z"] = pc.mCenter.z;
+ p["radius"] = pc.mRadius;
+ p["textureId"] = pc.mTextureId;
+ pcs.push_back( p );
+ }
+ j["paintedCircles"] = pcs;
+
+ nlohmann::json modelsJson = nlohmann::json::array();
+ for( const auto& mi : voxelEditor.getPlacedModels() ) {
+ nlohmann::json mj;
+ mj["modelIndex"] = mi.modelIndex;
+ mj["x"] = mi.pos.x;
+ mj["y"] = mi.pos.y;
+ mj["z"] = mi.pos.z;
+ mj["yaw"] = mi.yaw;
+ mj["scale"] = mi.scale;
+ modelsJson.push_back( mj );
+ }
+ j["models"] = modelsJson;
+
+ try {
+ nlohmann::json sj;
+ auto sd = sun.getDirection();
+ sj["direction"] = { sd.x, sd.y, sd.z };
+ auto sc = sun.getColor();
+ sj["color"] = { sc.r, sc.g, sc.b };
+ sj["intensity"] = sun.getIntensity();
+ j["sun"] = sj;
+ } catch( ... ) {
+ }
+
+ std::ofstream out( filepath );
+ if( !out ) {
+ throw std::runtime_error( "failed to open file for writing" );
+ }
+ out << j.dump( 2 );
+ out.close();
+ std::cout << "Level saved to " << filepath << std::endl;
+ } catch( const std::exception& e ) {
+ std::cerr << "Failed to save level: " << e.what() << std::endl;
+ }
+ }
+
+ static int selectedLevel = 0;
+ static std::vector<std::string> levelFiles;
+ static bool levelsScanned = false;
+
+ if( !levelsScanned || ImGui::Button( "Refresh Levels" ) ) {
+ levelFiles.clear();
+ std::string levelsDir = "apps/openmb/resources/levels";
+ if( std::filesystem::exists( levelsDir ) ) {
+ for( const auto& entry : std::filesystem::directory_iterator( levelsDir ) ) {
+ if( entry.path().extension() == ".json" ) {
+ levelFiles.push_back( entry.path().stem().string() );
+ }
+ }
+ }
+ std::sort( levelFiles.begin(), levelFiles.end() );
+ levelsScanned = true;
+ selectedLevel = 0;
+ }
+
+ if( !levelFiles.empty() ) {
+ if( ImGui::BeginCombo( "Available Levels", levelFiles[selectedLevel].c_str() ) ) {
+ for( int i = 0; i < levelFiles.size(); ++i ) {
+ bool isSelected = ( selectedLevel == i );
+ if( ImGui::Selectable( levelFiles[i].c_str(), isSelected ) ) {
+ selectedLevel = i;
+ }
+ if( isSelected )
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+
+ if( ImGui::Button( "Load Selected" ) ) {
+ std::string filepath =
+ std::string( "apps/openmb/resources/levels/" ) + levelFiles[selectedLevel] + ".json";
+
+ if( voxelEditor.loadFromFile( filepath ) ) {
+ std::cout << "Level loaded (voxels) from " << filepath << std::endl;
+ } else {
+ std::cerr << "Failed to load level voxels" << std::endl;
+ }
+
+ paintedCircles.clear();
+ try {
+ std::ifstream in( filepath );
+ if( in ) {
+ nlohmann::json j;
+ in >> j;
+ if( j.contains( "paintedCircles" ) && j["paintedCircles"].is_array() ) {
+ for( const auto& pcj : j["paintedCircles"] ) {
+ PaintedCircle pc;
+ pc.mCenter.x = pcj.value( "x", 0.0f );
+ pc.mCenter.y = pcj.value( "y", 0.0f );
+ pc.mCenter.z = pcj.value( "z", 0.0f );
+ pc.mRadius = pcj.value( "radius", 1.0f );
+ pc.mTextureId = pcj.value( "textureId", 0 );
+ paintedCircles.push_back( pc );
+ }
+ std::cout << "Loaded " << paintedCircles.size() << " painted circles from " << filepath
+ << std::endl;
+ }
+ if( j.contains( "sun" ) && j["sun"].is_object() ) {
+ try {
+ const auto& sj = j["sun"];
+ if( sj.contains( "direction" ) && sj["direction"].is_array() &&
+ sj["direction"].size() >= 3 ) {
+ glm::vec3 sd;
+ sd.x = sj["direction"][0].get<float>();
+ sd.y = sj["direction"][1].get<float>();
+ sd.z = sj["direction"][2].get<float>();
+ sun.setDirection( glm::normalize( sd ) );
+ }
+ if( sj.contains( "color" ) && sj["color"].is_array() && sj["color"].size() >= 3 ) {
+ glm::vec3 sc;
+ sc.r = sj["color"][0].get<float>();
+ sc.g = sj["color"][1].get<float>();
+ sc.b = sj["color"][2].get<float>();
+ sun.setColor( sc );
+ }
+ if( sj.contains( "intensity" ) )
+ sun.setIntensity( sj["intensity"].get<float>() );
+ } catch( ... ) {
+ }
+ }
+ }
+ } catch( ... ) {
+ }
+ }
+ } else {
+ ImGui::Text( "No levels found" );
+ }
+
+ if( ImGui::Button( "Clear All" ) ) {
+ voxelEditor.clear();
+ }
+
+ ImGui::Separator();
+ ImGui::Text( "Cube Texture:" );
+
+ auto categories = textureManager.getCategories();
+ if( !categories.empty() ) {
+ static int selectedCategory = 0;
+ static int selectedSubcategory = 0;
+ static int selectedTexture = 0;
+ static std::string lastCategory;
+ static std::string lastSubcategory;
+
+ std::string currentCat = textureManager.getCurrentCategory();
+ if( !currentCat.empty() ) {
+ auto it = std::find( categories.begin(), categories.end(), currentCat );
+ if( it != categories.end() )
+ selectedCategory = std::distance( categories.begin(), it );
+ }
+
+ if( ImGui::BeginCombo( "Category", categories[selectedCategory].c_str() ) ) {
+ for( int i = 0; i < categories.size(); ++i ) {
+ bool isSelected = ( selectedCategory == i );
+ if( ImGui::Selectable( categories[i].c_str(), isSelected ) ) {
+ selectedCategory = i;
+ selectedSubcategory = 0;
+ selectedTexture = 0;
+ lastCategory = categories[selectedCategory];
+
+ auto newSubcats = textureManager.getSubcategories( categories[selectedCategory] );
+ if( !newSubcats.empty() ) {
+ auto newTextures =
+ textureManager.getTextureNames( categories[selectedCategory], newSubcats[0] );
+ if( !newTextures.empty() ) {
+ textureManager.setCurrentTexture( categories[selectedCategory], newSubcats[0],
+ newTextures[0] );
+ }
+ }
+ }
+ if( isSelected )
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+
+ auto subcategories = textureManager.getSubcategories( categories[selectedCategory] );
+ if( !subcategories.empty() ) {
+
+ if( selectedSubcategory >= subcategories.size() )
+ selectedSubcategory = 0;
+
+ std::string currentSub = textureManager.getCurrentSubcategory();
+ if( !currentSub.empty() && textureManager.getCurrentCategory() == categories[selectedCategory] ) {
+ auto it = std::find( subcategories.begin(), subcategories.end(), currentSub );
+ if( it != subcategories.end() )
+ selectedSubcategory = std::distance( subcategories.begin(), it );
+ }
+
+ if( ImGui::BeginCombo( "Style", subcategories[selectedSubcategory].c_str() ) ) {
+ for( int i = 0; i < subcategories.size(); ++i ) {
+ bool isSelected = ( selectedSubcategory == i );
+ if( ImGui::Selectable( subcategories[i].c_str(), isSelected ) ) {
+ selectedSubcategory = i;
+ selectedTexture = 0;
+ lastSubcategory = subcategories[selectedSubcategory];
+
+ auto newTextures = textureManager.getTextureNames( categories[selectedCategory],
+ subcategories[selectedSubcategory] );
+ if( !newTextures.empty() ) {
+ textureManager.setCurrentTexture( categories[selectedCategory],
+ subcategories[selectedSubcategory],
+ newTextures[0] );
+ }
+ }
+ if( isSelected )
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+
+ auto textures = textureManager.getTextureNames( categories[selectedCategory],
+ subcategories[selectedSubcategory] );
+ if( !textures.empty() ) {
+
+ if( selectedTexture >= textures.size() )
+ selectedTexture = 0;
+
+ std::string currentTex = textureManager.getCurrentTextureName();
+ if( !currentTex.empty() &&
+ textureManager.getCurrentCategory() == categories[selectedCategory] &&
+ textureManager.getCurrentSubcategory() == subcategories[selectedSubcategory] ) {
+ auto it = std::find( textures.begin(), textures.end(), currentTex );
+ if( it != textures.end() )
+ selectedTexture = std::distance( textures.begin(), it );
+ }
+
+ if( ImGui::BeginCombo( "Texture", textures[selectedTexture].c_str() ) ) {
+ for( int i = 0; i < textures.size(); ++i ) {
+ bool isSelected = ( selectedTexture == i );
+ if( ImGui::Selectable( textures[i].c_str(), isSelected ) ) {
+ selectedTexture = i;
+ textureManager.setCurrentTexture( categories[selectedCategory],
+ subcategories[selectedSubcategory],
+ textures[selectedTexture] );
+ }
+ if( isSelected )
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+ }
+ }
+ }
+
+ ImGui::Separator();
+ ImGui::Text( "Placement Mode:" );
+
+ const char* brushLabels[] = { "Brush", "Cube", "Model" };
+ int brushIdx = static_cast<int>( placementBrush );
+ if( ImGui::Combo( "Mode", &brushIdx, brushLabels, IM_ARRAYSIZE( brushLabels ) ) ) {
+ placementBrush = static_cast<PlacementBrush>( brushIdx );
+ useCircleBrush = ( placementBrush == PlacementBrush::Brush );
+ }
+
+ if( placementBrush == PlacementBrush::Brush ) {
+ ImGui::Text( "Circle Brush:" );
+ ImGui::SliderFloat( "Brush Radius (m)", &brushRadius, 0.1f, 20.0f );
+ {
+ float minH = gridSystem.getFloorY();
+ float maxH = gridSystem.getFloorY() + gridSystem.getWallHeight() * gridSystem.getCellSize();
+ ImGui::SliderFloat( "Brush Height", &brushHeight, minH, maxH );
+ }
+ } else if( placementBrush == PlacementBrush::Model ) {
+ ImGui::Text( "Model Placement:" );
+ if( modelNames.empty() ) {
+ ImGui::Text( "No models found in resources/models/rocks" );
+ } else {
+ if( selectedModelIndex >= modelNames.size() )
+ selectedModelIndex = 0;
+ if( ImGui::BeginCombo( "Model", modelNames[selectedModelIndex].c_str() ) ) {
+ for( int i = 0; i < modelNames.size(); ++i ) {
+ bool sel = ( i == selectedModelIndex );
+ if( ImGui::Selectable( modelNames[i].c_str(), sel ) )
+ selectedModelIndex = i;
+ if( sel )
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::SliderFloat( "Scale", &selectedModelScale, 0.01f, 2.0f );
+ ImGui::SliderFloat( "Yaw (deg)", &selectedModelYaw, -180.0f, 180.0f );
+ ImGui::Checkbox( "Place Collidable", &placeCollidable );
+ ImGui::Text( "Left-click to place selected model at cursor height" );
+ }
+ } else {
+ }
+ ImGui::Separator();
+ ImGui::Checkbox( "Enable Lighting", &lightingEnabled );
+
+ ImGui::Text( "Sun / Directional Light" );
+ glm::vec3 sunDir = sun.getDirection();
+ float dirVals[3] = { sunDir.x, sunDir.y, sunDir.z };
+ if( ImGui::SliderFloat3( "Direction", dirVals, -1.0f, 1.0f ) ) {
+ glm::vec3 nd = glm::normalize( glm::vec3( dirVals[0], dirVals[1], dirVals[2] ) );
+ if( glm::length( nd ) > 0.0f )
+ sun.setDirection( nd );
+ }
+
+ glm::vec3 sunCol = sun.getColor();
+ float colVals[3] = { sunCol.r, sunCol.g, sunCol.b };
+ if( ImGui::ColorEdit3( "Color", colVals ) ) {
+ sun.setColor( glm::vec3( colVals[0], colVals[1], colVals[2] ) );
+ }
+
+ float intensity = sun.getIntensity();
+ if( ImGui::SliderFloat( "Intensity", &intensity, 0.0f, 5.0f ) ) {
+ sun.setIntensity( intensity );
+ }
+
+ ImGui::Separator();
+ ImGui::Text( "Shadows" );
+ ImGui::Checkbox( "Snap Shadow Texels", &snapToTexels );
+ ImGui::SliderFloat( "Shadow Bias Min", &shadowBiasMinGui, 0.0f, 0.01f, "%.6f" );
+ ImGui::SliderFloat( "Shadow Bias Scale", &shadowBiasScaleGui, 0.0f, 0.05f, "%.5f" );
+ ImGui::SliderInt( "PCF Radius", &pcfRadiusGui, 0, 4 );
+
+ ImGui::Separator();
+ ImGui::Text( "Fog" );
+ float fogColVals[3] = { fogColor.r, fogColor.g, fogColor.b };
+ if( ImGui::ColorEdit3( "Fog Color", fogColVals ) ) {
+ fogColor = glm::vec3( fogColVals[0], fogColVals[1], fogColVals[2] );
+ }
+ ImGui::SliderFloat( "Fog Density", &fogDensity, 0.0f, 0.1f, "%.4f" );
+ ImGui::SliderFloat( "Fog Amount", &fogAmount, 0.0f, 1.0f );
+
+ ImGui::Separator();
+ ImGui::Text( "God Rays" );
+ ImGui::Checkbox( "Enable God Rays", &godraysEnabled );
+ ImGui::SliderFloat( "God Rays Intensity", &godraysIntensity, 0.0f, 2.0f );
+ ImGui::SliderInt( "Samples", &godraysSamples, 4, 256 );
+ ImGui::SliderFloat( "Density", &godraysDensity, 0.0f, 2.0f );
+ ImGui::SliderFloat( "Weight", &godraysWeight, 0.0f, 2.0f );
+ ImGui::SliderFloat( "Decay", &godraysDecay, 0.0f, 1.0f );
+ ImGui::SliderInt( "Downscale", &godraysDownscale, 1, 8 );
+
+ if( !lightingEnabled ) {
+
+ texShader.use();
+ texShader.setFloat( "dirLight.intensity", 0.0f );
+ }
+
+ ImGui::End();
+ }
+
+ ImGui::SetNextWindowPos( ImVec2( io.DisplaySize.x - 10.0f, 10.0f ), ImGuiCond_Always, ImVec2( 1.0f, 0.0f ) );
+
+ ImGui::SetNextWindowSize( ImVec2( 180.0f, 0.0f ), ImGuiCond_Once );
+
+ ImGuiWindowFlags perfFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
+ ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoFocusOnAppearing |
+ ImGuiWindowFlags_NoBringToFrontOnFocus;
+
+ ImGui::PushStyleVar( ImGuiStyleVar_WindowRounding, 6.0f );
+ ImGui::PushStyleVar( ImGuiStyleVar_WindowPadding, ImVec2( 10, 6 ) );
+ if( ImGui::Begin( "Performance", nullptr, perfFlags ) ) {
+ ImGui::Text( "FPS: %1.0f", fps );
+ ImGui::Separator();
+ ImGui::Text( "Resolution: %dx%d", fbWidth, fbHeight );
+ }
+ ImGui::End();
+ ImGui::PopStyleVar( 2 );
+
+ bool shift = ( glfwGetKey( window, GLFW_KEY_LEFT_SHIFT ) == GLFW_PRESS ) ||
+ ( glfwGetKey( window, GLFW_KEY_RIGHT_SHIFT ) == GLFW_PRESS );
+ camera.setSpeedMultiplier( shift ? 3.0f : 1.0f );
+
+ if( mouseCaptured ) {
+ if( glfwGetKey( window, GLFW_KEY_W ) == GLFW_PRESS )
+ camera.processKeyboard( scene::Movement::Forward, deltaTime );
+ if( glfwGetKey( window, GLFW_KEY_S ) == GLFW_PRESS )
+ camera.processKeyboard( scene::Movement::Backward, deltaTime );
+ if( glfwGetKey( window, GLFW_KEY_A ) == GLFW_PRESS )
+ camera.processKeyboard( scene::Movement::Left, deltaTime );
+ if( glfwGetKey( window, GLFW_KEY_D ) == GLFW_PRESS )
+ camera.processKeyboard( scene::Movement::Right, deltaTime );
+ }
+
+ worldBoxes = voxelEditor.getAllCollisionBoxes( baseWorldBoxes );
+
+ if( !models.empty() ) {
+ const auto& placed = voxelEditor.getPlacedModels();
+ for( const auto& mi : placed ) {
+ if( !mi.mCollidable )
+ continue;
+ if( mi.modelIndex < 0 || mi.modelIndex >= static_cast<int>( models.size() ) )
+ continue;
+ renderer::Model* mdl = models[mi.modelIndex].get();
+ glm::vec3 bmin = mdl->getBoundsMin();
+ glm::vec3 bmax = mdl->getBoundsMax();
+
+ glm::mat4 modelMat = glm::translate( glm::mat4( 1.0f ), mi.pos );
+ modelMat = glm::rotate( modelMat, glm::radians( mi.yaw ), glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ modelMat = glm::scale( modelMat, glm::vec3( mi.scale ) );
+
+ glm::vec3 corners[8];
+ corners[0] = glm::vec3( bmin.x, bmin.y, bmin.z );
+ corners[1] = glm::vec3( bmax.x, bmin.y, bmin.z );
+ corners[2] = glm::vec3( bmin.x, bmax.y, bmin.z );
+ corners[3] = glm::vec3( bmin.x, bmin.y, bmax.z );
+ corners[4] = glm::vec3( bmax.x, bmax.y, bmin.z );
+ corners[5] = glm::vec3( bmax.x, bmin.y, bmax.z );
+ corners[6] = glm::vec3( bmin.x, bmax.y, bmax.z );
+ corners[7] = glm::vec3( bmax.x, bmax.y, bmax.z );
+
+ glm::vec3 wmin( FLT_MAX );
+ glm::vec3 wmax( -FLT_MAX );
+ for( int i = 0; i < 8; ++i ) {
+ glm::vec4 wc = modelMat * glm::vec4( corners[i], 1.0f );
+ wmin = glm::min( wmin, glm::vec3( wc ) );
+ wmax = glm::max( wmax, glm::vec3( wc ) );
+ }
+ worldBoxes.emplace_back( wmin, wmax );
+ }
+ }
+
+ camera.updatePhysics( deltaTime, worldBoxes, 0.0f );
+
+ double xpos, ypos;
+ glfwGetCursorPos( window, &xpos, &ypos );
+ if( firstMouse ) {
+ lastX = xpos;
+ lastY = ypos;
+ firstMouse = false;
+ }
+ float xoffset = static_cast<float>( xpos - lastX );
+ float yoffset = static_cast<float>( lastY - ypos );
+ lastX = xpos;
+ lastY = ypos;
+ if( mouseCaptured )
+ camera.processMouseMovement( xoffset, yoffset );
+
+ glm::vec3 sceneCenter( 0.0f, 5.0f, 0.0f );
+ float sceneRadius = 50.0f;
+ for( const auto& c : voxelEditor.getPlacedCells() ) {
+ glm::mat4 model = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ texturedCube.draw();
+ }
+ int fbw = 0, fbh = 0;
+ glfwGetFramebufferSize( window, &fbw, &fbh );
+ glViewport( 0, 0, fbw, fbh );
+ glEnable( GL_DEPTH_TEST );
+ glClearColor( 0.1f, 0.12f, 0.15f, 1.0f );
+ glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
+
+ float aspect = ( fbw > 0 && fbh > 0 ) ? (float)fbw / (float)fbh : (float)width / (float)height;
+ glm::mat4 proj = camera.getProjectionMatrix( aspect );
+ glm::mat4 view = camera.getViewMatrix();
+
+ {
+ float nearPlane = 1.0f;
+ float farPlane = 200.0f;
+ glm::mat4 lightProj =
+ glm::ortho( -sceneRadius, sceneRadius, -sceneRadius, sceneRadius, nearPlane, farPlane );
+ glm::vec3 lightDir = sun.getDirection();
+
+ glm::vec3 initialLightPos = sceneCenter - lightDir * 50.0f;
+ glm::mat4 initialLightView = glm::lookAt( initialLightPos, sceneCenter, glm::vec3( 0.0f, 1.0f, 0.0f ) );
+
+ glm::vec4 centerLS = initialLightView * glm::vec4( sceneCenter, 1.0f );
+
+ float worldTexelSize = ( 2.0f * sceneRadius ) / static_cast<float>( shadowWidth );
+
+ if( snapToTexels ) {
+ centerLS.x = std::floor( centerLS.x / worldTexelSize + 0.5f ) * worldTexelSize;
+ centerLS.y = std::floor( centerLS.y / worldTexelSize + 0.5f ) * worldTexelSize;
+ }
+
+ glm::mat4 invInitialLightView = glm::inverse( initialLightView );
+ glm::vec4 snappedCenterWorld4 = invInitialLightView * centerLS;
+ glm::vec3 snappedCenterWorld = glm::vec3( snappedCenterWorld4 );
+
+ static bool shadeSmoothInit = false;
+ static glm::vec3 smoothedCenter( 0.0f );
+ if( !shadeSmoothInit ) {
+ smoothedCenter = snappedCenterWorld;
+ shadeSmoothInit = true;
+ }
+
+ float smoothAlpha = ( shadowWidth > 4096 ) ? 0.20f : 0.08f;
+ smoothedCenter = glm::mix( smoothedCenter, snappedCenterWorld, smoothAlpha );
+
+ glm::vec3 lightPos = smoothedCenter - lightDir * 50.0f;
+ glm::mat4 lightView = glm::lookAt( lightPos, smoothedCenter, glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ glm::mat4 lightSpace = lightProj * lightView;
+
+ dirLightSpace = lightSpace;
+
+ glViewport( 0, 0, shadowWidth, shadowHeight );
+ glBindFramebuffer( GL_FRAMEBUFFER, depthMapFBO );
+ glClear( GL_DEPTH_BUFFER_BIT );
+ if( depthShader.id() != 0 ) {
+ depthShader.use();
+ depthShader.setMat4( "lightSpace", lightSpace );
+
+ glm::mat4 model = glm::mat4( 1.0f );
+ depthShader.setMat4( "model", model );
+ grid.draw();
+
+ for( const auto& c : voxelEditor.getPlacedCells() ) {
+ glm::mat4 m = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ depthShader.setMat4( "model", m );
+ texturedCube.draw();
+ }
+
+ if( !models.empty() ) {
+ const auto& placed = voxelEditor.getPlacedModels();
+ for( const auto& mi : placed ) {
+ if( mi.modelIndex < 0 || mi.modelIndex >= static_cast<int>( models.size() ) )
+ continue;
+ renderer::Model* mdl = models[mi.modelIndex].get();
+ glm::mat4 modelMat = glm::translate( glm::mat4( 1.0f ), mi.pos );
+ modelMat = glm::rotate( modelMat, glm::radians( mi.yaw ), glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ modelMat = glm::scale( modelMat, glm::vec3( mi.scale ) );
+
+ const auto& meshList = mdl->getMeshes();
+ for( size_t miIdx = 0; miIdx < meshList.size(); ++miIdx ) {
+ depthShader.setMat4( "model", modelMat );
+ meshList[miIdx]->draw();
+ }
+ }
+ }
+ }
+ glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+
+ {
+ auto instAfterShadow = Clock::now();
+ double shadowMs = std::chrono::duration<double, std::milli>( instAfterShadow - instSegStart ).count();
+ instAccumShadow += shadowMs;
+
+ instSegStart = Clock::now();
+ }
+
+ glViewport( 0, 0, fbw, fbh );
+
+ glActiveTexture( GL_TEXTURE2 );
+ glBindTexture( GL_TEXTURE_2D, depthMap );
+ glActiveTexture( GL_TEXTURE0 );
+
+ texShader.use();
+ texShader.setMat4( "lightSpace", lightSpace );
+
+ static renderer::SSAORenderer ssaoRenderer;
+ static bool ssaoInit = false;
+ if( !ssaoInit ) {
+ int initW = fbw > 0 ? fbw : width;
+ int initH = fbh > 0 ? fbh : height;
+ if( !ssaoRenderer.init( initW, initH, true ) )
+ std::cerr << "Warning: failed to init SSAORenderer" << std::endl;
+ ssaoInit = true;
+ }
+
+ ssaoRenderer.resize( fbw, fbh );
+
+ ssaoRenderer.bindGBuffer();
+ {
+ renderer::Shader& gs = ssaoRenderer.getGBufferShader();
+ gs.use();
+ gs.setMat4( "view", view );
+ gs.setMat4( "proj", proj );
+
+ glm::mat4 gridModel = glm::translate( glm::mat4( 1.0f ), glm::vec3( 0.0f, 0.0f, 0.0f ) );
+ gs.setMat4( "model", gridModel );
+ grid.draw();
+
+ for( const auto& c : voxelEditor.getPlacedCells() ) {
+ glm::mat4 model = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ gs.setMat4( "model", model );
+ texturedCube.draw();
+ }
+
+ if( !models.empty() ) {
+ const auto& placed = voxelEditor.getPlacedModels();
+ for( const auto& mi : placed ) {
+ if( mi.modelIndex < 0 || mi.modelIndex >= static_cast<int>( models.size() ) )
+ continue;
+
+ renderer::Model* mdl = models[mi.modelIndex].get();
+ glm::mat4 modelMat = glm::translate( glm::mat4( 1.0f ), mi.pos );
+ modelMat = glm::rotate( modelMat, glm::radians( mi.yaw ), glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ modelMat = glm::scale( modelMat, glm::vec3( mi.scale ) );
+
+ const auto& meshList = mdl->getMeshes();
+ for( size_t miIdx = 0; miIdx < meshList.size(); ++miIdx ) {
+ gs.setMat4( "model", modelMat );
+ meshList[miIdx]->draw();
+ }
+ }
+ }
+ }
+ ssaoRenderer.unbind();
+
+ glm::mat4 invProj = glm::inverse( proj );
+ float aoRadius = 1.0f;
+ float aoBias = 0.025f;
+ float aoPower = 1.0f;
+ ssaoRenderer.computeSSAO( proj, invProj, aoRadius, aoBias, aoPower );
+ ssaoRenderer.blurSSAO();
+
+ ssaoRenderer.bindSSAOTextureToUnit( 3, texShader, "ssao" );
+
+ glActiveTexture( GL_TEXTURE2 );
+ glBindTexture( GL_TEXTURE_2D, depthMap );
+ glActiveTexture( GL_TEXTURE0 );
+
+ {
+ auto instAfterSSAO = Clock::now();
+ double ssaoMs = std::chrono::duration<double, std::milli>( instAfterSSAO - instSegStart ).count();
+ instAccumSSAO += ssaoMs;
+
+ instSegStart = Clock::now();
+ }
+ }
+
+ if( editorActive && !io.WantCaptureMouse ) {
+ glm::vec2 mouseFb( io.MousePos.x * io.DisplayFramebufferScale.x,
+ io.MousePos.y * io.DisplayFramebufferScale.y );
+ glm::vec3 rayOrigin, rayDir;
+ renderer::editor::makeRayFromMouse( mouseFb, fbw, fbh, view, proj, camera.position(), rayOrigin, rayDir );
+
+ lastRayOrigin = rayOrigin;
+ lastRayDir = rayDir;
+
+ bool leftDown = ( glfwGetMouseButton( window, GLFW_MOUSE_BUTTON_LEFT ) == GLFW_PRESS ) || io.MouseDown[0];
+ bool rightDown = ( glfwGetMouseButton( window, GLFW_MOUSE_BUTTON_RIGHT ) == GLFW_PRESS ) || io.MouseDown[1];
+
+ if( placementBrush == PlacementBrush::Brush ) {
+ static bool prevLeftDownLocal = false;
+
+ if( std::abs( rayDir.y ) > 1e-6f ) {
+ float t = ( brushHeight - rayOrigin.y ) / rayDir.y;
+ if( t > 0.0f ) {
+ glm::vec3 centerWorld = rayOrigin + rayDir * t;
+
+ double now = glfwGetTime();
+ if( leftDown ) {
+ if( !prevLeftDownLocal || ( now - lastPaintTime ) >= paintInterval ) {
+ PaintedCircle pc;
+ pc.mCenter = centerWorld;
+ pc.mRadius = brushRadius;
+ pc.mTextureId = textureManager.getCurrentTextureId();
+ paintedCircles.push_back( pc );
+ lastPaintTime = now;
+ }
+ } else {
+ }
+ }
+ }
+
+ prevLeftDownLocal = leftDown;
+ } else if( placementBrush == PlacementBrush::Cube ) {
+ voxelEditor.processInput( rayOrigin, rayDir, leftDown, rightDown, shift, baseWorldBoxes,
+ textureManager.getCurrentTextureId(), placeCollidable );
+ } else if( placementBrush == PlacementBrush::Model ) {
+ static bool prevLeftDownModel = false;
+
+ if( std::abs( rayDir.y ) > 1e-6f ) {
+ float t = ( brushHeight - rayOrigin.y ) / rayDir.y;
+ if( t > 0.0f ) {
+ glm::vec3 centerWorld = rayOrigin + rayDir * t;
+
+ double now = glfwGetTime();
+ if( leftDown ) {
+ if( !prevLeftDownModel ) {
+ if( !models.empty() ) {
+ scene::VoxelEditor::ModelInstance mi;
+ mi.modelIndex = selectedModelIndex;
+ mi.pos = centerWorld;
+ mi.yaw = selectedModelYaw;
+ mi.scale = selectedModelScale;
+ mi.mCollidable = placeCollidable;
+ voxelEditor.addModelInstance( mi );
+ }
+ }
+ } else {
+ }
+ }
+ }
+
+ prevLeftDownModel = leftDown;
+ }
+ }
+
+ glViewport( 0, 0, fbw, fbh );
+
+ skybox.draw( view, proj );
+
+ shader.use();
+ glm::mat4 lineModel = glm::mat4( 1.0f );
+ shader.setMat4( "model", lineModel );
+ shader.setMat4( "view", view );
+ shader.setMat4( "proj", proj );
+ // Provide camera position and fog parameters to simple shader
+ shader.setVec3( "cameraPos", camera.position() );
+ shader.setVec3( "fogColor", fogColor );
+ shader.setFloat( "fogDensity", fogDensity );
+ shader.setFloat( "fogAmount", fogAmount );
+
+ glm::mat4 gridModel = glm::translate( glm::mat4( 1.0f ), glm::vec3( 0.0f, 0.0f, 0.0f ) );
+ shader.setMat4( "model", gridModel );
+ shader.setVec3( "color", glm::vec3( 0.25f, 0.5f, 0.25f ) );
+ grid.draw();
+
+ texShader.use();
+ shader.use();
+ shader.setMat4( "view", view );
+ shader.setMat4( "proj", proj );
+
+ texShader.use();
+ texShader.setMat4( "view", view );
+ texShader.setMat4( "proj", proj );
+
+ // Provide camera position and fog parameters to textured shader
+ texShader.setVec3( "cameraPos", camera.position() );
+ texShader.setVec3( "fogColor", fogColor );
+ texShader.setFloat( "fogDensity", fogDensity );
+ texShader.setFloat( "fogAmount", fogAmount );
+
+ texShader.setFloat( "shadowBiasMin", shadowBiasMinGui );
+ texShader.setFloat( "shadowBiasScale", shadowBiasScaleGui );
+ texShader.setInt( "pcfRadius", pcfRadiusGui );
+
+ if( lightingEnabled )
+ sun.applyToShader( texShader );
+ else {
+ texShader.setVec3( "dirLight.direction", glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ texShader.setVec3( "dirLight.color", glm::vec3( 0.0f ) );
+ texShader.setFloat( "dirLight.intensity", 0.0f );
+ }
+
+ texShader.setMat4( "lightSpace", dirLightSpace );
+ texShader.setFloat( "aoStrength", 1.0f );
+ texShader.setFloat( "screenWidth", static_cast<float>( fbw ) );
+ texShader.setFloat( "screenHeight", static_cast<float>( fbh ) );
+ texShader.setVec3( "tint", glm::vec3( 1.0f, 1.0f, 1.0f ) );
+ texShader.setInt( "albedo", 0 );
+
+ for( const auto& c : voxelEditor.getPlacedCells() ) {
+ int textureId = voxelEditor.getTextureIdForCell( c );
+ auto* cellTexture = textureManager.getTextureById( textureId );
+
+ if( cellTexture ) {
+ cellTexture->bind( 0 );
+ glm::mat4 model = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ texShader.setMat4( "model", model );
+ texShader.setVec3( "tint", glm::vec3( 1.0f, 1.0f, 1.0f ) );
+
+ texShader.setInt( "normalEnabled", 0 );
+ texturedCube.draw();
+ }
+ }
+
+ if( !models.empty() ) {
+ texShader.use();
+ texShader.setMat4( "view", view );
+ texShader.setMat4( "proj", proj );
+ texShader.setMat4( "lightSpace", dirLightSpace );
+ const auto& placed = voxelEditor.getPlacedModels();
+ for( const auto& mi : placed ) {
+ if( mi.modelIndex < 0 || mi.modelIndex >= static_cast<int>( models.size() ) )
+ continue;
+
+ renderer::Model* mdl = models[mi.modelIndex].get();
+ glm::mat4 modelMat = glm::translate( glm::mat4( 1.0f ), mi.pos );
+ modelMat = glm::rotate( modelMat, glm::radians( mi.yaw ), glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ modelMat = glm::scale( modelMat, glm::vec3( mi.scale ) );
+
+ const auto& meshList = mdl->getMeshes();
+ const auto& texList = mdl->getTextures();
+ const auto& normalList = mdl->getNormalTextures();
+ const auto& meshToTex = mdl->getMeshTextureIndex();
+ const auto& meshToNormal = mdl->getMeshNormalIndex();
+
+ for( size_t miIdx = 0; miIdx < meshList.size(); ++miIdx ) {
+ texShader.setMat4( "model", modelMat );
+ int tidx = -1;
+ if( miIdx < meshToTex.size() )
+ tidx = meshToTex[miIdx];
+ bool hasNormal = false;
+ if( tidx >= 0 && tidx < static_cast<int>( texList.size() ) ) {
+ texList[tidx].bind( 0 );
+ }
+ int nidx = -1;
+ if( miIdx < meshToNormal.size() )
+ nidx = meshToNormal[miIdx];
+ if( nidx >= 0 && nidx < static_cast<int>( normalList.size() ) ) {
+ normalList[nidx].bind( 1 );
+ texShader.setInt( "normalEnabled", 1 );
+ hasNormal = true;
+ } else {
+ texShader.setInt( "normalEnabled", 0 );
+ }
+
+ texShader.setVec3( "tint", glm::vec3( 1.0f, 1.0f, 1.0f ) );
+ meshList[miIdx]->draw();
+ if( hasNormal ) {
+
+ glActiveTexture( GL_TEXTURE1 );
+ glBindTexture( GL_TEXTURE_2D, 0 );
+ glActiveTexture( GL_TEXTURE0 );
+ }
+ }
+ }
+ }
+
+ shader.use();
+
+ const auto& previewCells = voxelEditor.getPreviewCells();
+ if( !previewCells.empty() && placementEditorEnabled ) {
+ if( previewCells.size() > 1 ) {
+ glm::ivec3 minC = previewCells.front();
+ glm::ivec3 maxC = previewCells.front();
+ for( const auto& c : previewCells ) {
+ minC.x = std::min( minC.x, c.x );
+ minC.y = std::min( minC.y, c.y );
+ minC.z = std::min( minC.z, c.z );
+ maxC.x = std::max( maxC.x, c.x );
+ maxC.y = std::max( maxC.y, c.y );
+ maxC.z = std::max( maxC.z, c.z );
+ }
+
+ glm::vec3 aabbMin = voxelEditor.cellToAABB( minC ).first;
+ glm::vec3 aabbMax = voxelEditor.cellToAABB( maxC ).second;
+
+ glm::vec3 center = ( aabbMin + aabbMax ) * 0.5f;
+ glm::vec3 size = aabbMax - aabbMin;
+
+ glm::vec3 scaleVec = size / tileSize;
+
+ glm::mat4 model =
+ glm::translate( glm::mat4( 1.0f ), center ) * glm::scale( glm::mat4( 1.0f ), scaleVec );
+
+ shader.setVec3( "color", glm::vec3( 0.2f, 0.9f, 0.9f ) );
+ shader.setMat4( "model", model );
+ wireCube.draw();
+ } else {
+ const float previewScale = 0.995f;
+ glm::mat4 scaleMat = glm::scale( glm::mat4( 1.0f ), glm::vec3( previewScale ) );
+
+ shader.setVec3( "color", glm::vec3( 0.2f, 0.9f, 0.9f ) );
+
+ for( const auto& c : previewCells ) {
+ glm::mat4 model = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ model = model * scaleMat;
+ shader.setMat4( "model", model );
+ wireCube.draw();
+ }
+ }
+ }
+
+ if( useCircleBrush && editorActive ) {
+ if( std::abs( brushRadius - prevBrushRadius ) > 1e-6f ) {
+ circleWire = renderer::editor::makeCircleWire( brushRadius, 64 );
+ prevBrushRadius = brushRadius;
+ }
+
+ if( std::abs( lastRayDir.y ) > 1e-6f ) {
+ float t = ( brushHeight - lastRayOrigin.y ) / lastRayDir.y;
+ if( t > 0.0f ) {
+ glm::vec3 centerWorld = lastRayOrigin + lastRayDir * t;
+
+ const float previewOffset = 0.01f;
+ glm::mat4 model = glm::translate(
+ glm::mat4( 1.0f ), glm::vec3( centerWorld.x, brushHeight + previewOffset, centerWorld.z ) );
+
+ glEnable( GL_BLEND );
+ glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
+ glDepthMask( GL_FALSE );
+ texShader.use();
+ texShader.setMat4( "view", view );
+ texShader.setMat4( "proj", proj );
+ if( lightingEnabled )
+ sun.applyToShader( texShader );
+ else
+ texShader.setFloat( "dirLight.intensity", 0.0f );
+ texShader.setMat4( "lightSpace", dirLightSpace );
+ texShader.setMat4( "model", glm::scale( model, glm::vec3( brushRadius, 1.0f, brushRadius ) ) );
+ texShader.setVec3( "tint", glm::vec3( 1.0f, 1.0f, 1.0f ) );
+
+ auto* previewTex = textureManager.getTextureById( textureManager.getCurrentTextureId() );
+ if( previewTex )
+ previewTex->bind( 0 );
+
+ texShader.setInt( "radialEnabled", 1 );
+ texShader.setFloat( "radialInner", 0.38f );
+ texShader.setFloat( "radialOuter", 0.5f );
+ filledCircle.draw();
+
+ texShader.setInt( "radialEnabled", 0 );
+ glDepthMask( GL_TRUE );
+ glDisable( GL_BLEND );
+
+ shader.setVec3( "color", glm::vec3( 0.0f, 0.5f, 1.0f ) );
+ shader.setMat4( "model", model );
+ glLineWidth( 2.5f );
+ circleWire.draw();
+ glLineWidth( 1.0f );
+ }
+ }
+ }
+
+ if( !paintedCircles.empty() ) {
+ glEnable( GL_BLEND );
+ glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
+ texShader.use();
+ texShader.setMat4( "view", view );
+ texShader.setMat4( "proj", proj );
+ if( lightingEnabled )
+ sun.applyToShader( texShader );
+ else
+ texShader.setFloat( "dirLight.intensity", 0.0f );
+
+ texShader.setInt( "radialEnabled", 1 );
+ texShader.setFloat( "radialInner", 0.38f );
+ texShader.setFloat( "radialOuter", 0.5f );
+
+ std::vector<std::pair<float, size_t>> distIndex;
+ distIndex.reserve( paintedCircles.size() );
+ for( size_t i = 0; i < paintedCircles.size(); ++i ) {
+ const auto& pc = paintedCircles[i];
+ float d2 = glm::length( camera.position() - pc.mCenter );
+ distIndex.emplace_back( d2, i );
+ }
+ std::sort( distIndex.begin(), distIndex.end(),
+ [] ( const auto& a, const auto& b ) { return a.first > b.first; } );
+
+ glDepthMask( GL_FALSE );
+ for( const auto& di : distIndex ) {
+ const auto& pc = paintedCircles[di.second];
+ auto* tex = textureManager.getTextureById( pc.mTextureId );
+ if( !tex )
+ continue;
+ tex->bind( 0 );
+ glm::mat4 model =
+ glm::translate( glm::mat4( 1.0f ), glm::vec3( pc.mCenter.x, pc.mCenter.y + 0.02f, pc.mCenter.z ) );
+ model = glm::scale( model, glm::vec3( pc.mRadius, 1.0f, pc.mRadius ) );
+ texShader.setMat4( "model", model );
+ texShader.setVec3( "tint", glm::vec3( 1.0f, 1.0f, 1.0f ) );
+ filledCircle.draw();
+ }
+
+ glDepthMask( GL_TRUE );
+
+ texShader.setInt( "radialEnabled", 0 );
+
+ glDisable( GL_BLEND );
+ }
+
+ {
+ auto instAfterDraw = Clock::now();
+ double drawMs = std::chrono::duration<double, std::milli>( instAfterDraw - instSegStart ).count();
+ instAccumDraw += drawMs;
+
+ instSegStart = Clock::now();
+ }
+
+ // --- Occlusion pass for god rays ---
+ {
+ int occW = std::max( 1, fbw / godraysDownscale );
+ int occH = std::max( 1, fbh / godraysDownscale );
+ if( occlusionW != occW || occlusionH != occH ) {
+ occlusionW = occW;
+ occlusionH = occH;
+
+ glBindTexture( GL_TEXTURE_2D, occlusionTex );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, occlusionW, occlusionH, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+
+ if( occlusionDepthRBO == 0 )
+ glGenRenderbuffers( 1, &occlusionDepthRBO );
+ glBindRenderbuffer( GL_RENDERBUFFER, occlusionDepthRBO );
+ glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, occlusionW, occlusionH );
+
+ glBindFramebuffer( GL_FRAMEBUFFER, occlusionFBO );
+ glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, occlusionTex, 0 );
+ glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, occlusionDepthRBO );
+ GLenum drawbuf = GL_COLOR_ATTACHMENT0;
+ glDrawBuffers( 1, &drawbuf );
+ if( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE ) {
+ std::cerr << "Warning: occlusion framebuffer not complete" << std::endl;
+ }
+ glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+ }
+
+ // Render occluders to low-res occlusion texture
+ glViewport( 0, 0, occlusionW, occlusionH );
+ glBindFramebuffer( GL_FRAMEBUFFER, occlusionFBO );
+ glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
+ glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
+ glEnable( GL_DEPTH_TEST );
+
+ occlusionShader.use();
+ occlusionShader.setMat4( "view", view );
+ occlusionShader.setMat4( "proj", proj );
+
+ // draw grid
+ {
+ glm::mat4 model = glm::mat4( 1.0f );
+ occlusionShader.setMat4( "model", model );
+ grid.draw();
+ }
+
+ // draw voxel cells
+ for( const auto& c : voxelEditor.getPlacedCells() ) {
+ glm::mat4 model = glm::translate( glm::mat4( 1.0f ), voxelEditor.cellToWorldCenter( c ) );
+ occlusionShader.setMat4( "model", model );
+ texturedCube.draw();
+ }
+
+ // draw placed models
+ if( !models.empty() ) {
+ const auto& placed = voxelEditor.getPlacedModels();
+ for( const auto& mi : placed ) {
+ if( mi.modelIndex < 0 || mi.modelIndex >= static_cast<int>( models.size() ) )
+ continue;
+ renderer::Model* mdl = models[mi.modelIndex].get();
+ glm::mat4 modelMat = glm::translate( glm::mat4( 1.0f ), mi.pos );
+ modelMat = glm::rotate( modelMat, glm::radians( mi.yaw ), glm::vec3( 0.0f, 1.0f, 0.0f ) );
+ modelMat = glm::scale( modelMat, glm::vec3( mi.scale ) );
+
+ const auto& meshList = mdl->getMeshes();
+ for( size_t miIdx = 0; miIdx < meshList.size(); ++miIdx ) {
+ occlusionShader.setMat4( "model", modelMat );
+ meshList[miIdx]->draw();
+ }
+ }
+ }
+
+ glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+ glViewport( 0, 0, fbw, fbh );
+
+ // Compute light screen position
+ glm::vec3 lightWorldPos = sceneCenter - sun.getDirection() * 50.0f;
+ glm::vec4 clip = proj * view * glm::vec4( lightWorldPos, 1.0f );
+ glm::vec3 ndc = glm::vec3( clip ) / clip.w;
+ glm::vec2 lightScreen = glm::vec2( ndc.x, ndc.y ) * 0.5f + glm::vec2( 0.5f );
+
+ // Composite god rays using alpha so it's less likely to blow out the scene
+ glEnable( GL_BLEND );
+ glBlendFuncSeparate( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA );
+ glDisable( GL_DEPTH_TEST );
+
+ if( godraysEnabled ) {
+ godraysShader.use();
+ godraysShader.setInt( "occlusionTex", 4 );
+ godraysShader.setVec2( "lightScreenPos", lightScreen );
+ godraysShader.setVec3( "sunColor", sun.getColor() );
+ godraysShader.setFloat( "sunIntensity", sun.getIntensity() );
+ godraysShader.setFloat( "globalIntensity", godraysIntensity );
+ godraysShader.setInt( "samples", godraysSamples );
+ godraysShader.setFloat( "density", godraysDensity );
+ godraysShader.setFloat( "weight", godraysWeight );
+ godraysShader.setFloat( "decay", godraysDecay );
+
+ glActiveTexture( GL_TEXTURE4 );
+ glBindTexture( GL_TEXTURE_2D, occlusionTex );
+ glActiveTexture( GL_TEXTURE0 );
+
+ quadMesh.draw();
+ }
+
+ glActiveTexture( GL_TEXTURE4 );
+ glBindTexture( GL_TEXTURE_2D, occlusionTex );
+ glActiveTexture( GL_TEXTURE0 );
+
+ quadMesh.draw();
+
+ glDisable( GL_BLEND );
+ glEnable( GL_DEPTH_TEST );
+ }
+
+ ImGui::Render();
+ ImGui_ImplOpenGL3_RenderDrawData( ImGui::GetDrawData() );
+
+ {
+ auto instAfterUI = Clock::now();
+ double uiMs = std::chrono::duration<double, std::milli>( instAfterUI - instSegStart ).count();
+ instAccumUI += uiMs;
+
+ auto instFrameEnd = Clock::now();
+ double frameMs = std::chrono::duration<double, std::milli>( instFrameEnd - instFrameStart ).count();
+ instAccumFrame += frameMs;
+ ++instFrames;
+
+ if( std::chrono::duration<double>( instFrameEnd - instLastReport ).count() >= 1.0 ) {
+ double avgShadow = instAccumShadow / double( instFrames );
+ double avgSSAO = instAccumSSAO / double( instFrames );
+ double avgDraw = instAccumDraw / double( instFrames );
+ double avgUI = instAccumUI / double( instFrames );
+ double avgFrame = instAccumFrame / double( instFrames );
+ std::cout << "[TIMING] frames=" << instFrames << " frame(ms)=" << avgFrame << " shadow=" << avgShadow
+ << " ssao=" << avgSSAO << " draw=" << avgDraw << " ui=" << avgUI << std::endl;
+
+ instLastReport = instFrameEnd;
+ instAccumShadow = 0.0;
+ instAccumSSAO = 0.0;
+ instAccumDraw = 0.0;
+ instAccumUI = 0.0;
+ instAccumFrame = 0.0;
+ instFrames = 0;
+ }
+ }
+
+ glfwSwapBuffers( window );
+ glfwPollEvents();
+ }
+
+ ImGui_ImplOpenGL3_Shutdown();
+ ImGui_ImplGlfw_Shutdown();
+ ImGui::DestroyContext();
+
+ glfwDestroyWindow( window );
+ glfwTerminate();
+
+ return 0;
+}