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

This commit is contained in:
Anemunt
2025-12-09 16:50:13 -05:00
parent 9adb1ff2f5
commit 57fb740b04
8 changed files with 558 additions and 126 deletions

View File

@@ -10,6 +10,7 @@
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cfloat>
#include <string>
#include <vector>
#include <memory>

View File

@@ -3,6 +3,71 @@
#include <iostream>
#include <fstream>
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);

View File

@@ -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<SceneObject> sceneObjects;
int selectedObjectId = -1;
@@ -114,6 +122,14 @@ private:
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();
bool initRenderer();

View File

@@ -1,6 +1,7 @@
#include "Engine.h"
#include "ModelLoader.h"
#include <algorithm>
#include <cfloat>
#ifdef _WIN32
#include <shlobj.h>
@@ -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<int>(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) {
if (browserHasMaterial) {
renderMaterialAssetPanel("Material Asset", true);
} else {
ImGui::TextDisabled("No object selected");
}
ImGui::End();
return;
}
@@ -1393,89 +1556,107 @@ 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;
}
} 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;
}
} 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;
}
} else {
ImGui::Button("Use Selected##Normal");
}
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")) {
obj.materialPath = matPathBuf;
loadMaterialFromFile(obj);
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;
}
ImGui::SameLine();
if (ImGui::Button("Save Material")) {
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;
};
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");
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;
materialChanged = true;
}
bool hasMatPath = obj.materialPath.size() > 0;
ImGui::BeginDisabled(!hasMatPath);
if (ImGui::Button("Save Material")) {
saveMaterialToFile(obj);
}
ImGui::SameLine();
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();

View File

@@ -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<float> 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<float>& vertices) {
void ModelLoader::processNode(aiNode* node, const aiScene* scene, std::vector<float>& 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<float>& vertices) {
void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector<float>& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax) {
// Process each face
for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
@@ -192,6 +198,13 @@ void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector<fl
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) {
vertices.push_back(mesh->mNormals[index].x);

View File

@@ -64,8 +64,8 @@ private:
ModelLoader& operator=(const ModelLoader&) = delete;
// Process Assimp scene
void processNode(aiNode* node, const aiScene* scene, std::vector<float>& vertices);
void processMesh(aiMesh* mesh, const aiScene* scene, std::vector<float>& vertices);
void processNode(aiNode* node, const aiScene* scene, std::vector<float>& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax);
void processMesh(aiMesh* mesh, const aiScene* scene, std::vector<float>& vertices, glm::vec3& boundsMin, glm::vec3& boundsMax);
// Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure)
std::vector<OBJLoader::LoadedMesh> loadedMeshes;

View File

@@ -296,6 +296,9 @@ int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
faceCount += static_cast<int>(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<int>(loadedMeshes.size() - 1);

View File

@@ -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: