#if defined( __APPLE__ ) #define GL_SILENCE_DEPRECATION #endif #if defined( __APPLE__ ) #define GL_SILENCE_DEPRECATION #endif #define GLFW_INCLUDE_NONE #include #ifdef __APPLE__ #include #endif #include #include #include #include #include #include #include "ImguiStyle.hpp" #include #include #include #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 #include #include #include #include #include #include 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 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> models; std::vector 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(); 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( header ), 18 ); unsigned char pixel[4] = { 255, 0, 0, 200 }; out.write( reinterpret_cast( 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 paintedCircles; static double lastPaintTime = 0.0; const double paintInterval = 0.08; std::vector> worldBoxes; std::vector> 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(); loadedSunDir.y = sj["direction"][1].get(); loadedSunDir.z = sj["direction"][2].get(); } if( sj.contains( "color" ) && sj["color"].is_array() && sj["color"].size() >= 3 ) { loadedSunColor.r = sj["color"][0].get(); loadedSunColor.g = sj["color"][1].get(); loadedSunColor.b = sj["color"][2].get(); } if( sj.contains( "intensity" ) ) loadedSunIntensity = sj["intensity"].get(); 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( 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 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(); sd.y = sj["direction"][1].get(); sd.z = sj["direction"][2].get(); 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(); sc.g = sj["color"][1].get(); sc.b = sj["color"][2].get(); sun.setColor( sc ); } if( sj.contains( "intensity" ) ) sun.setIntensity( sj["intensity"].get() ); } 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( placementBrush ); if( ImGui::Combo( "Mode", &brushIdx, brushLabels, IM_ARRAYSIZE( brushLabels ) ) ) { placementBrush = static_cast( 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( 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( xpos - lastX ); float yoffset = static_cast( 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( 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( 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( 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( 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( 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( fbw ) ); texShader.setFloat( "screenHeight", static_cast( 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( 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( texList.size() ) ) { texList[tidx].bind( 0 ); } int nidx = -1; if( miIdx < meshToNormal.size() ) nidx = meshToNormal[miIdx]; if( nidx >= 0 && nidx < static_cast( 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> 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( 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( 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( instAfterUI - instSegStart ).count(); instAccumUI += uiMs; auto instFrameEnd = Clock::now(); double frameMs = std::chrono::duration( instFrameEnd - instFrameStart ).count(); instAccumFrame += frameMs; ++instFrames; if( std::chrono::duration( 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; }