From 54409423f767d8b1cf30cb7d0efca6b4ca138823 Mon Sep 17 00:00:00 2001 From: Ethan Morgan Date: Sat, 14 Feb 2026 16:44:06 +0000 Subject: move to own git server --- apps/openmb/main.cpp | 1753 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1753 insertions(+) create mode 100644 apps/openmb/main.cpp (limited to 'apps/openmb/main.cpp') 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 +#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; +} -- cgit v1.2.3