From 57fb740b042dd7257b15b23f71f1d36345b79124 Mon Sep 17 00:00:00 2001 From: Anemunt <69436164+darkresident55@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:50:13 -0500 Subject: [PATCH] changed lock to viewport when clicking to Hold right click to move around, added being able to select objects through viewport and improved Material support --- src/Common.h | 1 + src/Engine.cpp | 151 ++++++++++---- src/Engine.h | 18 +- src/EnginePanels.cpp | 473 ++++++++++++++++++++++++++++++++++++------- src/ModelLoader.cpp | 23 ++- src/ModelLoader.h | 4 +- src/Rendering.cpp | 12 ++ src/Rendering.h | 2 + 8 files changed, 558 insertions(+), 126 deletions(-) diff --git a/src/Common.h b/src/Common.h index 90f7cc6..9f62b66 100644 --- a/src/Common.h +++ b/src/Common.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Engine.cpp b/src/Engine.cpp index 5800997..63ba2d3 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -3,6 +3,71 @@ #include #include +namespace { +struct MaterialFileData { + MaterialProperties props; + std::string albedo; + std::string overlay; + std::string normal; + bool useOverlay = false; +}; + +bool readMaterialFile(const std::string& path, MaterialFileData& outData) { + std::ifstream f(path); + if (!f.is_open()) { + return false; + } + + std::string line; + while (std::getline(f, line)) { + line.erase(0, line.find_first_not_of(" \t\r\n")); + if (line.empty() || line[0] == '#') continue; + auto pos = line.find('='); + if (pos == std::string::npos) continue; + std::string key = line.substr(0, pos); + std::string val = line.substr(pos + 1); + if (key == "color") { + sscanf(val.c_str(), "%f,%f,%f", &outData.props.color.r, &outData.props.color.g, &outData.props.color.b); + } else if (key == "ambient") { + outData.props.ambientStrength = std::stof(val); + } else if (key == "specular") { + outData.props.specularStrength = std::stof(val); + } else if (key == "shininess") { + outData.props.shininess = std::stof(val); + } else if (key == "textureMix") { + outData.props.textureMix = std::stof(val); + } else if (key == "albedo") { + outData.albedo = val; + } else if (key == "overlay") { + outData.overlay = val; + } else if (key == "normal") { + outData.normal = val; + } else if (key == "useOverlay") { + outData.useOverlay = std::stoi(val) != 0; + } + } + return true; +} + +bool writeMaterialFile(const MaterialFileData& data, const std::string& path) { + std::ofstream f(path); + if (!f.is_open()) { + return false; + } + f << "# Material\n"; + f << "color=" << data.props.color.r << "," << data.props.color.g << "," << data.props.color.b << "\n"; + f << "ambient=" << data.props.ambientStrength << "\n"; + f << "specular=" << data.props.specularStrength << "\n"; + f << "shininess=" << data.props.shininess << "\n"; + f << "textureMix=" << data.props.textureMix << "\n"; + f << "useOverlay=" << (data.useOverlay ? 1 : 0) << "\n"; + f << "albedo=" << data.albedo << "\n"; + f << "overlay=" << data.overlay << "\n"; + f << "normal=" << data.normal << "\n"; + return true; +} +} // namespace + void window_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } @@ -261,39 +326,16 @@ void Engine::importModelToScene(const std::string& filepath, const std::string& void Engine::loadMaterialFromFile(SceneObject& obj) { if (obj.materialPath.empty()) return; try { - std::ifstream f(obj.materialPath); - if (!f.is_open()) { + MaterialFileData data; + if (!readMaterialFile(obj.materialPath, data)) { addConsoleMessage("Failed to open material: " + obj.materialPath, ConsoleMessageType::Error); return; } - std::string line; - while (std::getline(f, line)) { - line.erase(0, line.find_first_not_of(" \t\r\n")); - if (line.empty() || line[0] == '#') continue; - auto pos = line.find('='); - if (pos == std::string::npos) continue; - std::string key = line.substr(0, pos); - std::string val = line.substr(pos + 1); - if (key == "color") { - sscanf(val.c_str(), "%f,%f,%f", &obj.material.color.r, &obj.material.color.g, &obj.material.color.b); - } else if (key == "ambient") { - obj.material.ambientStrength = std::stof(val); - } else if (key == "specular") { - obj.material.specularStrength = std::stof(val); - } else if (key == "shininess") { - obj.material.shininess = std::stof(val); - } else if (key == "textureMix") { - obj.material.textureMix = std::stof(val); - } else if (key == "albedo") { - obj.albedoTexturePath = val; - } else if (key == "overlay") { - obj.overlayTexturePath = val; - } else if (key == "normal") { - obj.normalMapPath = val; - } else if (key == "useOverlay") { - obj.useOverlay = std::stoi(val) != 0; - } - } + obj.material = data.props; + obj.albedoTexturePath = data.albedo; + obj.overlayTexturePath = data.overlay; + obj.normalMapPath = data.normal; + obj.useOverlay = data.useOverlay; addConsoleMessage("Applied material: " + obj.materialPath, ConsoleMessageType::Success); projectManager.currentProject.hasUnsavedChanges = true; } catch (...) { @@ -301,27 +343,52 @@ void Engine::loadMaterialFromFile(SceneObject& obj) { } } +bool Engine::loadMaterialData(const std::string& path, MaterialProperties& props, + std::string& albedo, std::string& overlay, + std::string& normal, bool& useOverlay) +{ + MaterialFileData data; + if (!readMaterialFile(path, data)) { + return false; + } + props = data.props; + albedo = data.albedo; + overlay = data.overlay; + normal = data.normal; + useOverlay = data.useOverlay; + return true; +} + +bool Engine::saveMaterialData(const std::string& path, const MaterialProperties& props, + const std::string& albedo, const std::string& overlay, + const std::string& normal, bool useOverlay) +{ + MaterialFileData data; + data.props = props; + data.albedo = albedo; + data.overlay = overlay; + data.normal = normal; + data.useOverlay = useOverlay; + return writeMaterialFile(data, path); +} + void Engine::saveMaterialToFile(const SceneObject& obj) { if (obj.materialPath.empty()) { addConsoleMessage("Material path is empty", ConsoleMessageType::Warning); return; } try { - std::ofstream f(obj.materialPath); - if (!f.is_open()) { + MaterialFileData data; + data.props = obj.material; + data.albedo = obj.albedoTexturePath; + data.overlay = obj.overlayTexturePath; + data.normal = obj.normalMapPath; + data.useOverlay = obj.useOverlay; + + if (!writeMaterialFile(data, obj.materialPath)) { addConsoleMessage("Failed to open material for writing: " + obj.materialPath, ConsoleMessageType::Error); return; } - f << "# Material\n"; - f << "color=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n"; - f << "ambient=" << obj.material.ambientStrength << "\n"; - f << "specular=" << obj.material.specularStrength << "\n"; - f << "shininess=" << obj.material.shininess << "\n"; - f << "textureMix=" << obj.material.textureMix << "\n"; - f << "useOverlay=" << (obj.useOverlay ? 1 : 0) << "\n"; - f << "albedo=" << obj.albedoTexturePath << "\n"; - f << "overlay=" << obj.overlayTexturePath << "\n"; - f << "normal=" << obj.normalMapPath << "\n"; addConsoleMessage("Saved material: " + obj.materialPath, ConsoleMessageType::Success); } catch (...) { addConsoleMessage("Failed to save material: " + obj.materialPath, ConsoleMessageType::Error); diff --git a/src/Engine.h b/src/Engine.h index 15a453b..3b9070a 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -19,9 +19,17 @@ private: ViewportController viewportController; float deltaTime = 0.0f; float lastFrame = 0.0f; - bool cursorLocked = false; + bool cursorLocked = false; // true only while holding right mouse for freelook int viewportWidth = 800; int viewportHeight = 600; + // Standalone material inspection cache + std::string inspectedMaterialPath; + MaterialProperties inspectedMaterial; + std::string inspectedAlbedo; + std::string inspectedOverlay; + std::string inspectedNormal; + bool inspectedUseOverlay = false; + bool inspectedMaterialValid = false; std::vector sceneObjects; int selectedObjectId = -1; @@ -113,6 +121,14 @@ private: // Console/logging void addConsoleMessage(const std::string& message, ConsoleMessageType type); void logToConsole(const std::string& message); + + // Material helpers + bool loadMaterialData(const std::string& path, MaterialProperties& props, + std::string& albedo, std::string& overlay, + std::string& normal, bool& useOverlay); + bool saveMaterialData(const std::string& path, const MaterialProperties& props, + const std::string& albedo, const std::string& overlay, + const std::string& normal, bool useOverlay); // ImGui setup void setupImGui(); diff --git a/src/EnginePanels.cpp b/src/EnginePanels.cpp index 6b41b5e..3231720 100644 --- a/src/EnginePanels.cpp +++ b/src/EnginePanels.cpp @@ -1,6 +1,7 @@ #include "Engine.h" #include "ModelLoader.h" #include +#include #ifdef _WIN32 #include @@ -376,9 +377,11 @@ void Engine::renderFileBrowserPanel() { for (size_t i = 0; i < pathParts.size(); i++) { std::string name = (i == 0) ? "Project" : pathParts[i].filename().string(); + ImGui::PushID(static_cast(i)); if (ImGui::SmallButton(name.c_str())) { fileBrowser.navigateTo(pathParts[i]); } + ImGui::PopID(); if (i < pathParts.size() - 1) { ImGui::SameLine(0, 2); ImGui::TextDisabled("/"); @@ -1284,8 +1287,168 @@ void Engine::renderInspectorPanel() { ImGui::Spacing(); } + fs::path selectedMaterialPath; + bool browserHasMaterial = false; + if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) { + fs::directory_entry entry(fileBrowser.selectedFile); + if (fileBrowser.getFileCategory(entry) == FileCategory::Material) { + selectedMaterialPath = entry.path(); + browserHasMaterial = true; + if (inspectedMaterialPath != selectedMaterialPath.string()) { + inspectedMaterialValid = loadMaterialData( + selectedMaterialPath.string(), + inspectedMaterial, + inspectedAlbedo, + inspectedOverlay, + inspectedNormal, + inspectedUseOverlay + ); + inspectedMaterialPath = selectedMaterialPath.string(); + } + } else { + inspectedMaterialPath.clear(); + inspectedMaterialValid = false; + } + } else { + inspectedMaterialPath.clear(); + inspectedMaterialValid = false; + } + + auto renderMaterialAssetPanel = [&](const char* headerTitle, bool allowApply) { + if (!browserHasMaterial) return; + + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f)); + if (ImGui::CollapsingHeader(headerTitle, ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(8.0f); + if (!inspectedMaterialValid) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Failed to read material file."); + } else { + auto textureField = [&](const char* label, const char* idSuffix, std::string& path) { + bool changed = false; + ImGui::PushID(idSuffix); + ImGui::TextUnformatted(label); + ImGui::SetNextItemWidth(-140); + char buf[512] = {}; + std::snprintf(buf, sizeof(buf), "%s", path.c_str()); + if (ImGui::InputText("##Path", buf, sizeof(buf))) { + path = buf; + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { + path.clear(); + changed = true; + } + ImGui::SameLine(); + bool canUseTex = !fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && + fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile)); + ImGui::BeginDisabled(!canUseTex); + std::string btnLabel = std::string("Use Selection##") + idSuffix; + if (ImGui::SmallButton(btnLabel.c_str())) { + path = fileBrowser.selectedFile.string(); + changed = true; + } + ImGui::EndDisabled(); + ImGui::PopID(); + return changed; + }; + + ImGui::TextDisabled("%s", selectedMaterialPath.filename().string().c_str()); + ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 1.0f), "%s", selectedMaterialPath.string().c_str()); + ImGui::Spacing(); + + bool matChanged = false; + if (ImGui::ColorEdit3("Base Color", &inspectedMaterial.color.x)) { + matChanged = true; + } + float metallic = inspectedMaterial.specularStrength; + if (ImGui::SliderFloat("Metallic", &metallic, 0.0f, 1.0f)) { + inspectedMaterial.specularStrength = metallic; + matChanged = true; + } + float smoothness = inspectedMaterial.shininess / 256.0f; + if (ImGui::SliderFloat("Smoothness", &smoothness, 0.0f, 1.0f)) { + smoothness = std::clamp(smoothness, 0.0f, 1.0f); + inspectedMaterial.shininess = smoothness * 256.0f; + matChanged = true; + } + if (ImGui::SliderFloat("Ambient Light", &inspectedMaterial.ambientStrength, 0.0f, 1.0f)) { + matChanged = true; + } + if (ImGui::SliderFloat("Detail Mix", &inspectedMaterial.textureMix, 0.0f, 1.0f)) { + matChanged = true; + } + + ImGui::Spacing(); + matChanged |= textureField("Base Map", "PreviewAlbedo", inspectedAlbedo); + if (ImGui::Checkbox("Use Detail Map", &inspectedUseOverlay)) { + matChanged = true; + } + matChanged |= textureField("Detail Map", "PreviewOverlay", inspectedOverlay); + matChanged |= textureField("Normal Map", "PreviewNormal", inspectedNormal); + + ImGui::Spacing(); + if (ImGui::Button("Reload")) { + inspectedMaterialValid = loadMaterialData( + selectedMaterialPath.string(), + inspectedMaterial, + inspectedAlbedo, + inspectedOverlay, + inspectedNormal, + inspectedUseOverlay + ); + } + ImGui::SameLine(); + if (ImGui::Button("Save")) { + if (saveMaterialData( + selectedMaterialPath.string(), + inspectedMaterial, + inspectedAlbedo, + inspectedOverlay, + inspectedNormal, + inspectedUseOverlay)) + { + addConsoleMessage("Saved material: " + selectedMaterialPath.string(), ConsoleMessageType::Success); + } else { + addConsoleMessage("Failed to save material: " + selectedMaterialPath.string(), ConsoleMessageType::Error); + } + } + + if (allowApply) { + ImGui::SameLine(); + SceneObject* target = getSelectedObject(); + bool canApply = target != nullptr; + ImGui::BeginDisabled(!canApply); + if (ImGui::Button("Apply to Selection")) { + if (target) { + target->material = inspectedMaterial; + target->albedoTexturePath = inspectedAlbedo; + target->overlayTexturePath = inspectedOverlay; + target->normalMapPath = inspectedNormal; + target->useOverlay = inspectedUseOverlay; + target->materialPath = selectedMaterialPath.string(); + projectManager.currentProject.hasUnsavedChanges = true; + addConsoleMessage("Applied material to " + target->name, ConsoleMessageType::Success); + } + } + ImGui::EndDisabled(); + } + + if (matChanged) { + inspectedMaterialValid = true; + } + } + ImGui::Unindent(8.0f); + } + ImGui::PopStyleColor(); + }; + if (selectedObjectId == -1) { - ImGui::TextDisabled("No object selected"); + if (browserHasMaterial) { + renderMaterialAssetPanel("Material Asset", true); + } else { + ImGui::TextDisabled("No object selected"); + } ImGui::End(); return; } @@ -1393,88 +1556,106 @@ void Engine::renderInspectorPanel() { if (ImGui::CollapsingHeader("Material", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(10.0f); - if (ImGui::ColorEdit3("Color", &obj.material.color.x)) { - projectManager.currentProject.hasUnsavedChanges = true; - } - - if (ImGui::SliderFloat("Ambient", &obj.material.ambientStrength, 0.0f, 1.0f)) { - projectManager.currentProject.hasUnsavedChanges = true; - } - if (ImGui::SliderFloat("Specular", &obj.material.specularStrength, 0.0f, 2.0f)) { - projectManager.currentProject.hasUnsavedChanges = true; - } - if (ImGui::SliderFloat("Shininess", &obj.material.shininess, 1.0f, 256.0f)) { - projectManager.currentProject.hasUnsavedChanges = true; - } - if (ImGui::SliderFloat("Texture Mix", &obj.material.textureMix, 0.0f, 1.0f)) { - projectManager.currentProject.hasUnsavedChanges = true; - } - - ImGui::Separator(); - ImGui::Text("Textures"); - char albedoBuf[512] = {}; - std::snprintf(albedoBuf, sizeof(albedoBuf), "%s", obj.albedoTexturePath.c_str()); - if (ImGui::InputText("Albedo##Tex", albedoBuf, sizeof(albedoBuf))) { - obj.albedoTexturePath = albedoBuf; - projectManager.currentProject.hasUnsavedChanges = true; - } - if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile))) { - if (ImGui::Button("Use Selected##Albedo")) { - obj.albedoTexturePath = fileBrowser.selectedFile.string(); - projectManager.currentProject.hasUnsavedChanges = true; + auto textureField = [&](const char* label, const char* idSuffix, std::string& path) { + bool changed = false; + ImGui::PushID(idSuffix); + ImGui::TextUnformatted(label); + ImGui::SetNextItemWidth(-160); + char buf[512] = {}; + std::snprintf(buf, sizeof(buf), "%s", path.c_str()); + if (ImGui::InputText("##Path", buf, sizeof(buf))) { + path = buf; + changed = true; } - } else { - ImGui::Button("Use Selected##Albedo"); - } - - ImGui::Checkbox("Use Overlay", &obj.useOverlay); - char overlayBuf[512] = {}; - std::snprintf(overlayBuf, sizeof(overlayBuf), "%s", obj.overlayTexturePath.c_str()); - if (ImGui::InputText("Overlay##Tex", overlayBuf, sizeof(overlayBuf))) { - obj.overlayTexturePath = overlayBuf; - projectManager.currentProject.hasUnsavedChanges = true; - } - if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile))) { - if (ImGui::Button("Use Selected##Overlay")) { - obj.overlayTexturePath = fileBrowser.selectedFile.string(); - projectManager.currentProject.hasUnsavedChanges = true; + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { + path.clear(); + changed = true; } - } else { - ImGui::Button("Use Selected##Overlay"); - } - - char normalBuf[512] = {}; - std::snprintf(normalBuf, sizeof(normalBuf), "%s", obj.normalMapPath.c_str()); - if (ImGui::InputText("Normal Map##Tex", normalBuf, sizeof(normalBuf))) { - obj.normalMapPath = normalBuf; - projectManager.currentProject.hasUnsavedChanges = true; - } - if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile))) { - if (ImGui::Button("Use Selected##Normal")) { - obj.normalMapPath = fileBrowser.selectedFile.string(); - projectManager.currentProject.hasUnsavedChanges = true; + ImGui::SameLine(); + bool canUseTex = !fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && + fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile)); + ImGui::BeginDisabled(!canUseTex); + std::string btnLabel = std::string("Use Selection##") + idSuffix; + if (ImGui::SmallButton(btnLabel.c_str())) { + path = fileBrowser.selectedFile.string(); + changed = true; } - } else { - ImGui::Button("Use Selected##Normal"); + ImGui::EndDisabled(); + ImGui::PopID(); + return changed; + }; + + bool materialChanged = false; + + ImGui::TextColored(ImVec4(0.8f, 0.7f, 1.0f, 1.0f), "Surface Inputs"); + if (ImGui::ColorEdit3("Base Color", &obj.material.color.x)) { + materialChanged = true; } + float metallic = obj.material.specularStrength; + if (ImGui::SliderFloat("Metallic", &metallic, 0.0f, 1.0f)) { + obj.material.specularStrength = metallic; + materialChanged = true; + } + + float smoothness = obj.material.shininess / 256.0f; + if (ImGui::SliderFloat("Smoothness", &smoothness, 0.0f, 1.0f)) { + smoothness = std::clamp(smoothness, 0.0f, 1.0f); + obj.material.shininess = smoothness * 256.0f; + materialChanged = true; + } + + if (ImGui::SliderFloat("Ambient Light", &obj.material.ambientStrength, 0.0f, 1.0f)) { + materialChanged = true; + } + if (ImGui::SliderFloat("Detail Mix", &obj.material.textureMix, 0.0f, 1.0f)) { + materialChanged = true; + } + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Maps"); + materialChanged |= textureField("Base Map", "ObjAlbedo", obj.albedoTexturePath); + if (ImGui::Checkbox("Use Detail Map", &obj.useOverlay)) { + materialChanged = true; + } + materialChanged |= textureField("Detail Map", "ObjOverlay", obj.overlayTexturePath); + materialChanged |= textureField("Normal Map", "ObjNormal", obj.normalMapPath); + + ImGui::Spacing(); ImGui::Separator(); ImGui::Text("Material Asset"); - static char matPathBuf[512] = {}; - if (!obj.materialPath.empty()) { - std::snprintf(matPathBuf, sizeof(matPathBuf), "%s", obj.materialPath.c_str()); - } - if (ImGui::InputText("Path##Mat", matPathBuf, sizeof(matPathBuf))) { - // Defer applying until user hits button - } - if (ImGui::Button("Apply Material")) { + + char matPathBuf[512] = {}; + std::snprintf(matPathBuf, sizeof(matPathBuf), "%s", obj.materialPath.c_str()); + ImGui::SetNextItemWidth(-1); + if (ImGui::InputText("##MaterialPath", matPathBuf, sizeof(matPathBuf))) { obj.materialPath = matPathBuf; - loadMaterialFromFile(obj); + materialChanged = true; + } + + bool hasMatPath = obj.materialPath.size() > 0; + ImGui::BeginDisabled(!hasMatPath); + if (ImGui::Button("Save Material")) { + saveMaterialToFile(obj); } ImGui::SameLine(); - if (ImGui::Button("Save Material")) { - obj.materialPath = matPathBuf; - saveMaterialToFile(obj); + if (ImGui::Button("Reload Material")) { + loadMaterialFromFile(obj); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::BeginDisabled(!browserHasMaterial); + if (ImGui::Button("Load Selected")) { + obj.materialPath = selectedMaterialPath.string(); + loadMaterialFromFile(obj); + materialChanged = true; + } + ImGui::EndDisabled(); + + if (materialChanged) { + projectManager.currentProject.hasUnsavedChanges = true; } ImGui::Unindent(10.0f); @@ -1659,6 +1840,11 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } + if (browserHasMaterial) { + ImGui::Spacing(); + renderMaterialAssetPanel("Material Asset (File Browser)", true); + } + ImGui::End(); } @@ -1835,27 +2021,162 @@ void Engine::renderViewport() { } } + // Left-click picking inside viewport if (mouseOverViewportImage && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGuizmo::IsUsing() && !ImGuizmo::IsOver()) { + glm::mat4 invViewProj = glm::inverse(proj * view); + ImVec2 mousePos = ImGui::GetMousePos(); + + auto makeRay = [&](const ImVec2& pos) { + float x = (pos.x - imageMin.x) / (imageMax.x - imageMin.x); + float y = (pos.y - imageMin.y) / (imageMax.y - imageMin.y); + x = x * 2.0f - 1.0f; + y = 1.0f - y * 2.0f; + + glm::vec4 nearPt = invViewProj * glm::vec4(x, y, -1.0f, 1.0f); + glm::vec4 farPt = invViewProj * glm::vec4(x, y, 1.0f, 1.0f); + nearPt /= nearPt.w; + farPt /= farPt.w; + + glm::vec3 origin = glm::vec3(nearPt); + glm::vec3 dir = glm::normalize(glm::vec3(farPt - nearPt)); + return std::make_pair(origin, dir); + }; + + auto rayAabb = [](const glm::vec3& orig, const glm::vec3& dir, const glm::vec3& bmin, const glm::vec3& bmax, float& tHit) { + float tmin = -FLT_MAX; + float tmax = FLT_MAX; + for (int i = 0; i < 3; ++i) { + if (std::abs(dir[i]) < 1e-6f) { + if (orig[i] < bmin[i] || orig[i] > bmax[i]) return false; + continue; + } + float invD = 1.0f / dir[i]; + float t1 = (bmin[i] - orig[i]) * invD; + float t2 = (bmax[i] - orig[i]) * invD; + if (t1 > t2) std::swap(t1, t2); + tmin = std::max(tmin, t1); + tmax = std::min(tmax, t2); + if (tmin > tmax) return false; + } + tHit = (tmin >= 0.0f) ? tmin : tmax; + return tmax >= 0.0f; + }; + + auto raySphere = [](const glm::vec3& orig, const glm::vec3& dir, float radius, float& tHit) { + float b = glm::dot(dir, orig); + float c = glm::dot(orig, orig) - radius * radius; + float disc = b * b - c; + if (disc < 0.0f) return false; + float sqrtDisc = sqrtf(disc); + float t0 = -b - sqrtDisc; + float t1 = -b + sqrtDisc; + float t = (t0 >= 0.0f) ? t0 : t1; + if (t < 0.0f) return false; + tHit = t; + return true; + }; + + auto ray = makeRay(mousePos); + float closest = FLT_MAX; + int hitId = -1; + + for (const auto& obj : sceneObjects) { + glm::vec3 aabbMin(-0.5f); + glm::vec3 aabbMax(0.5f); + + glm::mat4 model(1.0f); + model = glm::translate(model, obj.position); + model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0)); + model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0)); + model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1)); + model = glm::scale(model, obj.scale); + + glm::mat4 invModel = glm::inverse(model); + glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(ray.first, 1.0f)); + glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(ray.second, 0.0f))); + + float hitT = 0.0f; + bool hit = false; + switch (obj.type) { + case ObjectType::Cube: + hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f), glm::vec3(0.5f), hitT); + break; + case ObjectType::Sphere: + hit = raySphere(localOrigin, localDir, 0.5f, hitT); + break; + case ObjectType::Capsule: + hit = rayAabb(localOrigin, localDir, glm::vec3(-0.35f, -0.9f, -0.35f), glm::vec3(0.35f, 0.9f, 0.35f), hitT); + break; + case ObjectType::OBJMesh: { + const auto* info = g_objLoader.getMeshInfo(obj.meshId); + if (info && info->boundsMin.x < info->boundsMax.x) { + aabbMin = info->boundsMin; + aabbMax = info->boundsMax; + } + hit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT); + break; + } + case ObjectType::Model: { + const auto* info = getModelLoader().getMeshInfo(obj.meshId); + if (info && info->boundsMin.x < info->boundsMax.x) { + aabbMin = info->boundsMin; + aabbMax = info->boundsMax; + } + hit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT); + break; + } + case ObjectType::DirectionalLight: + case ObjectType::PointLight: + case ObjectType::SpotLight: + case ObjectType::AreaLight: + hit = raySphere(localOrigin, localDir, 0.3f, hitT); + break; + } + + if (hit && hitT < closest && hitT >= 0.0f) { + closest = hitT; + hitId = obj.id; + } + } + + viewportController.setFocused(true); + if (hitId != -1) { + selectedObjectId = hitId; + } else { + selectedObjectId = -1; + } + } + + if (mouseOverViewportImage && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { viewportController.setFocused(true); cursorLocked = true; glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED); camera.firstMouse = true; } + + if (cursorLocked && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + cursorLocked = false; + glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + camera.firstMouse = true; + } } // Overlay hint ImGui::SetCursorPos(ImVec2(10, 30)); ImGui::TextColored( ImVec4(1, 1, 1, 0.3f), - "WASD: Move | QE: Up/Down | Shift: Sprint | ESC: Release | F11: Fullscreen" + "Hold RMB: Look & Move | LMB: Select | WASD+QE: Move | ESC: Release | F11: Fullscreen" ); - if (viewportController.isViewportFocused()) { + if (cursorLocked) { ImGui::SetCursorPos(ImVec2(10, 50)); - ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Camera Active"); + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Freelook Active"); + } else if (viewportController.isViewportFocused()) { + ImGui::SetCursorPos(ImVec2(10, 50)); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Viewport Focused"); } bool windowFocused = ImGui::IsWindowFocused(); diff --git a/src/ModelLoader.cpp b/src/ModelLoader.cpp index 21705e6..a426ac0 100644 --- a/src/ModelLoader.cpp +++ b/src/ModelLoader.cpp @@ -117,6 +117,9 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { return result; } + glm::vec3 boundsMin(FLT_MAX); + glm::vec3 boundsMax(-FLT_MAX); + // Process all meshes in the scene std::vector vertices; result.meshCount = scene->mNumMeshes; @@ -125,7 +128,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { result.hasTangents = false; // Process the root node recursively - processNode(scene->mRootNode, scene, vertices); + processNode(scene->mRootNode, scene, vertices, boundsMin, boundsMax); // Check mesh properties for (unsigned int i = 0; i < scene->mNumMeshes; i++) { @@ -152,6 +155,9 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { loaded.hasNormals = result.hasNormals; loaded.hasTexCoords = result.hasTexCoords; + loaded.boundsMin = boundsMin; + loaded.boundsMax = boundsMax; + loadedMeshes.push_back(std::move(loaded)); result.success = true; @@ -165,20 +171,20 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { return result; } -void ModelLoader::processNode(aiNode* node, const aiScene* scene, std::vector& vertices) { +void ModelLoader::processNode(aiNode* node, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax) { // Process all meshes in this node for (unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; - processMesh(mesh, scene, vertices); + processMesh(mesh, scene, vertices, boundsMin, boundsMax); } // Process children nodes for (unsigned int i = 0; i < node->mNumChildren; i++) { - processNode(node->mChildren[i], scene, vertices); + processNode(node->mChildren[i], scene, vertices, boundsMin, boundsMax); } } -void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices) { +void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax) { // Process each face for (unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; @@ -191,6 +197,13 @@ void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vectormVertices[index].x); vertices.push_back(mesh->mVertices[index].y); vertices.push_back(mesh->mVertices[index].z); + + boundsMin.x = std::min(boundsMin.x, mesh->mVertices[index].x); + boundsMin.y = std::min(boundsMin.y, mesh->mVertices[index].y); + boundsMin.z = std::min(boundsMin.z, mesh->mVertices[index].z); + boundsMax.x = std::max(boundsMax.x, mesh->mVertices[index].x); + boundsMax.y = std::max(boundsMax.y, mesh->mVertices[index].y); + boundsMax.z = std::max(boundsMax.z, mesh->mVertices[index].z); // Normal if (mesh->mNormals) { diff --git a/src/ModelLoader.h b/src/ModelLoader.h index b04baaa..a11255f 100644 --- a/src/ModelLoader.h +++ b/src/ModelLoader.h @@ -64,8 +64,8 @@ private: ModelLoader& operator=(const ModelLoader&) = delete; // Process Assimp scene - void processNode(aiNode* node, const aiScene* scene, std::vector& vertices); - void processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices); + void processNode(aiNode* node, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax); + void processMesh(aiMesh* mesh, const aiScene* scene, std::vector& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax); // Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure) std::vector loadedMeshes; diff --git a/src/Rendering.cpp b/src/Rendering.cpp index b11a572..ae34756 100644 --- a/src/Rendering.cpp +++ b/src/Rendering.cpp @@ -296,6 +296,9 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) { faceCount += static_cast(shape.mesh.num_face_vertices.size()); } + glm::vec3 boundsMin(FLT_MAX); + glm::vec3 boundsMax(-FLT_MAX); + for (const auto& shape : shapes) { size_t indexOffset = 0; for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); f++) { @@ -317,6 +320,13 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) { tv.pos.y = attrib.vertices[3 * size_t(idx.vertex_index) + 1]; tv.pos.z = attrib.vertices[3 * size_t(idx.vertex_index) + 2]; + boundsMin.x = std::min(boundsMin.x, tv.pos.x); + boundsMin.y = std::min(boundsMin.y, tv.pos.y); + boundsMin.z = std::min(boundsMin.z, tv.pos.z); + boundsMax.x = std::max(boundsMax.x, tv.pos.x); + boundsMax.y = std::max(boundsMax.y, tv.pos.y); + boundsMax.z = std::max(boundsMax.z, tv.pos.z); + if (idx.texcoord_index >= 0 && !attrib.texcoords.empty()) { tv.uv.x = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0]; tv.uv.y = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1]; @@ -378,6 +388,8 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) { loaded.faceCount = faceCount; loaded.hasNormals = hasNormalsInFile; loaded.hasTexCoords = !attrib.texcoords.empty(); + loaded.boundsMin = boundsMin; + loaded.boundsMax = boundsMax; loadedMeshes.push_back(std::move(loaded)); return static_cast(loadedMeshes.size() - 1); diff --git a/src/Rendering.h b/src/Rendering.h index 09eb2ce..5a7b0ac 100644 --- a/src/Rendering.h +++ b/src/Rendering.h @@ -37,6 +37,8 @@ public: int faceCount = 0; bool hasNormals = false; bool hasTexCoords = false; + glm::vec3 boundsMin = glm::vec3(FLT_MAX); + glm::vec3 boundsMax = glm::vec3(-FLT_MAX); }; private: