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:
@@ -10,6 +10,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
151
src/Engine.cpp
151
src/Engine.cpp
@@ -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);
|
||||
|
||||
18
src/Engine.h
18
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<SceneObject> 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();
|
||||
|
||||
@@ -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) {
|
||||
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();
|
||||
|
||||
@@ -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];
|
||||
@@ -191,6 +197,13 @@ void ModelLoader::processMesh(aiMesh* mesh, const aiScene* scene, std::vector<fl
|
||||
vertices.push_back(mesh->mVertices[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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user