Added Meshbuilder + new RMesh type
This commit is contained in:
@@ -97,7 +97,7 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
|
|||||||
// Model files
|
// Model files
|
||||||
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
|
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
|
||||||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" ||
|
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" ||
|
||||||
ext == ".stl" || ext == ".x" || ext == ".md5mesh") {
|
ext == ".stl" || ext == ".x" || ext == ".md5mesh" || ext == ".rmesh") {
|
||||||
return FileCategory::Model;
|
return FileCategory::Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
src/Engine.cpp
122
src/Engine.cpp
@@ -87,6 +87,42 @@ SceneObject* Engine::getSelectedObject() {
|
|||||||
return (it != sceneObjects.end()) ? &(*it) : nullptr;
|
return (it != sceneObjects.end()) ? &(*it) : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glm::vec3 Engine::getSelectionCenterWorld(bool worldSpace) const {
|
||||||
|
if (selectedObjectIds.empty()) return glm::vec3(0.0f);
|
||||||
|
glm::vec3 acc(0.0f);
|
||||||
|
int count = 0;
|
||||||
|
auto findObj = [&](int id) -> const SceneObject* {
|
||||||
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), [id](const SceneObject& o){ return o.id == id; });
|
||||||
|
return it == sceneObjects.end() ? nullptr : &(*it);
|
||||||
|
};
|
||||||
|
for (int id : selectedObjectIds) {
|
||||||
|
const SceneObject* o = findObj(id);
|
||||||
|
if (!o) continue;
|
||||||
|
acc += worldSpace ? o->position : glm::vec3(0.0f);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count == 0) return glm::vec3(0.0f);
|
||||||
|
return acc / (float)count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::setPrimarySelection(int id, bool additive) {
|
||||||
|
if (!additive) {
|
||||||
|
selectedObjectIds.clear();
|
||||||
|
}
|
||||||
|
if (id >= 0) {
|
||||||
|
selectedObjectIds.push_back(id);
|
||||||
|
selectedObjectId = id;
|
||||||
|
} else {
|
||||||
|
selectedObjectIds.clear();
|
||||||
|
selectedObjectId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::clearSelection() {
|
||||||
|
selectedObjectIds.clear();
|
||||||
|
selectedObjectId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
Camera Engine::makeCameraFromObject(const SceneObject& obj) const {
|
Camera Engine::makeCameraFromObject(const SceneObject& obj) const {
|
||||||
Camera cam;
|
Camera cam;
|
||||||
cam.position = obj.position;
|
cam.position = obj.position;
|
||||||
@@ -120,7 +156,7 @@ void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3&
|
|||||||
void Engine::recordState(const char* /*reason*/) {
|
void Engine::recordState(const char* /*reason*/) {
|
||||||
SceneSnapshot snap;
|
SceneSnapshot snap;
|
||||||
snap.objects = sceneObjects;
|
snap.objects = sceneObjects;
|
||||||
snap.selectedId = selectedObjectId;
|
snap.selectedIds = selectedObjectIds;
|
||||||
snap.nextId = nextObjectId;
|
snap.nextId = nextObjectId;
|
||||||
|
|
||||||
undoStack.push_back(std::move(snap));
|
undoStack.push_back(std::move(snap));
|
||||||
@@ -135,7 +171,7 @@ void Engine::undo() {
|
|||||||
|
|
||||||
SceneSnapshot current;
|
SceneSnapshot current;
|
||||||
current.objects = sceneObjects;
|
current.objects = sceneObjects;
|
||||||
current.selectedId = selectedObjectId;
|
current.selectedIds = selectedObjectIds;
|
||||||
current.nextId = nextObjectId;
|
current.nextId = nextObjectId;
|
||||||
|
|
||||||
SceneSnapshot snap = undoStack.back();
|
SceneSnapshot snap = undoStack.back();
|
||||||
@@ -143,7 +179,8 @@ void Engine::undo() {
|
|||||||
|
|
||||||
redoStack.push_back(std::move(current));
|
redoStack.push_back(std::move(current));
|
||||||
sceneObjects = std::move(snap.objects);
|
sceneObjects = std::move(snap.objects);
|
||||||
selectedObjectId = snap.selectedId;
|
selectedObjectIds = snap.selectedIds;
|
||||||
|
selectedObjectId = selectedObjectIds.empty() ? -1 : selectedObjectIds.back();
|
||||||
nextObjectId = snap.nextId;
|
nextObjectId = snap.nextId;
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
}
|
}
|
||||||
@@ -153,7 +190,7 @@ void Engine::redo() {
|
|||||||
|
|
||||||
SceneSnapshot current;
|
SceneSnapshot current;
|
||||||
current.objects = sceneObjects;
|
current.objects = sceneObjects;
|
||||||
current.selectedId = selectedObjectId;
|
current.selectedIds = selectedObjectIds;
|
||||||
current.nextId = nextObjectId;
|
current.nextId = nextObjectId;
|
||||||
|
|
||||||
SceneSnapshot snap = redoStack.back();
|
SceneSnapshot snap = redoStack.back();
|
||||||
@@ -161,7 +198,8 @@ void Engine::redo() {
|
|||||||
|
|
||||||
undoStack.push_back(std::move(current));
|
undoStack.push_back(std::move(current));
|
||||||
sceneObjects = std::move(snap.objects);
|
sceneObjects = std::move(snap.objects);
|
||||||
selectedObjectId = snap.selectedId;
|
selectedObjectIds = snap.selectedIds;
|
||||||
|
selectedObjectId = selectedObjectIds.empty() ? -1 : selectedObjectIds.back();
|
||||||
nextObjectId = snap.nextId;
|
nextObjectId = snap.nextId;
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
}
|
}
|
||||||
@@ -301,6 +339,7 @@ void Engine::run() {
|
|||||||
if (showHierarchy) renderHierarchyPanel();
|
if (showHierarchy) renderHierarchyPanel();
|
||||||
if (showInspector) renderInspectorPanel();
|
if (showInspector) renderInspectorPanel();
|
||||||
if (showFileBrowser) renderFileBrowserPanel();
|
if (showFileBrowser) renderFileBrowserPanel();
|
||||||
|
if (showMeshBuilder) renderMeshBuilderPanel();
|
||||||
if (showConsole) renderConsolePanel();
|
if (showConsole) renderConsolePanel();
|
||||||
if (showEnvironmentWindow) renderEnvironmentWindow();
|
if (showEnvironmentWindow) renderEnvironmentWindow();
|
||||||
if (showCameraWindow) renderCameraWindow();
|
if (showCameraWindow) renderCameraWindow();
|
||||||
@@ -372,7 +411,7 @@ void Engine::importOBJToScene(const std::string& filepath, const std::string& ob
|
|||||||
obj.meshId = meshId;
|
obj.meshId = meshId;
|
||||||
|
|
||||||
sceneObjects.push_back(obj);
|
sceneObjects.push_back(obj);
|
||||||
selectedObjectId = id;
|
setPrimarySelection(id);
|
||||||
|
|
||||||
if (projectManager.currentProject.isLoaded) {
|
if (projectManager.currentProject.isLoaded) {
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
@@ -407,7 +446,7 @@ void Engine::importModelToScene(const std::string& filepath, const std::string&
|
|||||||
obj.meshId = result.meshIndex;
|
obj.meshId = result.meshIndex;
|
||||||
|
|
||||||
sceneObjects.push_back(obj);
|
sceneObjects.push_back(obj);
|
||||||
selectedObjectId = id;
|
setPrimarySelection(id);
|
||||||
|
|
||||||
if (projectManager.currentProject.isLoaded) {
|
if (projectManager.currentProject.isLoaded) {
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
@@ -422,6 +461,61 @@ void Engine::importModelToScene(const std::string& filepath, const std::string&
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Engine::convertModelToRawMesh(const std::string& filepath) {
|
||||||
|
auto& modelLoader = getModelLoader();
|
||||||
|
fs::path inPath(filepath);
|
||||||
|
fs::path outPath = inPath;
|
||||||
|
outPath.replace_extension(".rmesh");
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
if (modelLoader.exportRawMesh(filepath, outPath.string(), error)) {
|
||||||
|
addConsoleMessage("Converted to raw mesh: " + outPath.string(), ConsoleMessageType::Success);
|
||||||
|
fileBrowser.needsRefresh = true;
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Raw mesh export failed: " + error, ConsoleMessageType::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Engine::ensureMeshEditTarget(SceneObject* obj) {
|
||||||
|
if (!obj) return false;
|
||||||
|
fs::path ext = fs::path(obj->meshPath).extension();
|
||||||
|
std::string extLower = ext.string();
|
||||||
|
std::transform(extLower.begin(), extLower.end(), extLower.begin(), ::tolower);
|
||||||
|
if (extLower != ".rmesh") return false;
|
||||||
|
|
||||||
|
if (meshEditLoaded && meshEditPath == obj->meshPath) {
|
||||||
|
if (meshEditSelectedVertices.empty() && !meshEditAsset.positions.empty()) {
|
||||||
|
meshEditSelectedVertices.push_back(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
if (!getModelLoader().loadRawMesh(obj->meshPath, meshEditAsset, err)) {
|
||||||
|
addConsoleMessage("Mesh edit load failed: " + err, ConsoleMessageType::Error);
|
||||||
|
meshEditLoaded = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
meshEditLoaded = true;
|
||||||
|
meshEditPath = obj->meshPath;
|
||||||
|
meshEditSelectedVertices.clear();
|
||||||
|
meshEditSelectedEdges.clear();
|
||||||
|
meshEditSelectedFaces.clear();
|
||||||
|
if (!meshEditAsset.positions.empty()) meshEditSelectedVertices.push_back(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Engine::syncMeshEditToGPU(SceneObject* obj) {
|
||||||
|
if (!obj || !meshEditLoaded) return false;
|
||||||
|
std::string err;
|
||||||
|
if (!getModelLoader().updateRawMesh(obj->meshId, meshEditAsset, err)) {
|
||||||
|
addConsoleMessage("Mesh GPU sync failed: " + err, ConsoleMessageType::Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void Engine::loadMaterialFromFile(SceneObject& obj) {
|
void Engine::loadMaterialFromFile(SceneObject& obj) {
|
||||||
if (obj.materialPath.empty()) return;
|
if (obj.materialPath.empty()) return;
|
||||||
try {
|
try {
|
||||||
@@ -648,7 +742,7 @@ void Engine::createNewProject(const char* name, const char* location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sceneObjects.clear();
|
sceneObjects.clear();
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
nextObjectId = 0;
|
nextObjectId = 0;
|
||||||
|
|
||||||
addObject(ObjectType::Cube, "Cube");
|
addObject(ObjectType::Cube, "Cube");
|
||||||
@@ -671,7 +765,7 @@ void Engine::createNewProject(const char* name, const char* location) {
|
|||||||
|
|
||||||
void Engine::loadRecentScenes() {
|
void Engine::loadRecentScenes() {
|
||||||
sceneObjects.clear();
|
sceneObjects.clear();
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
nextObjectId = 0;
|
nextObjectId = 0;
|
||||||
undoStack.clear();
|
undoStack.clear();
|
||||||
redoStack.clear();
|
redoStack.clear();
|
||||||
@@ -722,7 +816,7 @@ void Engine::loadScene(const std::string& sceneName) {
|
|||||||
projectManager.currentProject.currentSceneName = sceneName;
|
projectManager.currentProject.currentSceneName = sceneName;
|
||||||
projectManager.currentProject.hasUnsavedChanges = false;
|
projectManager.currentProject.hasUnsavedChanges = false;
|
||||||
projectManager.currentProject.saveProjectFile();
|
projectManager.currentProject.saveProjectFile();
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
bool hasDirLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) {
|
bool hasDirLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) {
|
||||||
return o.type == ObjectType::DirectionalLight;
|
return o.type == ObjectType::DirectionalLight;
|
||||||
});
|
});
|
||||||
@@ -744,7 +838,7 @@ void Engine::createNewScene(const std::string& sceneName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sceneObjects.clear();
|
sceneObjects.clear();
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
nextObjectId = 0;
|
nextObjectId = 0;
|
||||||
undoStack.clear();
|
undoStack.clear();
|
||||||
redoStack.clear();
|
redoStack.clear();
|
||||||
@@ -787,7 +881,7 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
|
|||||||
sceneObjects.back().camera.type = SceneCameraType::Player;
|
sceneObjects.back().camera.type = SceneCameraType::Player;
|
||||||
sceneObjects.back().camera.fov = 60.0f;
|
sceneObjects.back().camera.fov = 60.0f;
|
||||||
}
|
}
|
||||||
selectedObjectId = id;
|
setPrimarySelection(id);
|
||||||
if (projectManager.currentProject.isLoaded) {
|
if (projectManager.currentProject.isLoaded) {
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
}
|
}
|
||||||
@@ -820,7 +914,7 @@ void Engine::duplicateSelected() {
|
|||||||
newObj.postFx = it->postFx;
|
newObj.postFx = it->postFx;
|
||||||
|
|
||||||
sceneObjects.push_back(newObj);
|
sceneObjects.push_back(newObj);
|
||||||
selectedObjectId = id;
|
setPrimarySelection(id);
|
||||||
if (projectManager.currentProject.isLoaded) {
|
if (projectManager.currentProject.isLoaded) {
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
}
|
}
|
||||||
@@ -836,7 +930,7 @@ void Engine::deleteSelected() {
|
|||||||
if (it != sceneObjects.end()) {
|
if (it != sceneObjects.end()) {
|
||||||
logToConsole("Deleted object");
|
logToConsole("Deleted object");
|
||||||
sceneObjects.erase(it, sceneObjects.end());
|
sceneObjects.erase(it, sceneObjects.end());
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
if (projectManager.currentProject.isLoaded) {
|
if (projectManager.currentProject.isLoaded) {
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/Engine.h
26
src/Engine.h
@@ -6,6 +6,7 @@
|
|||||||
#include "Rendering.h"
|
#include "Rendering.h"
|
||||||
#include "ProjectManager.h"
|
#include "ProjectManager.h"
|
||||||
#include "EditorUI.h"
|
#include "EditorUI.h"
|
||||||
|
#include "MeshBuilder.h"
|
||||||
#include "../include/Window/Window.h"
|
#include "../include/Window/Window.h"
|
||||||
|
|
||||||
void window_size_callback(GLFWwindow* window, int width, int height);
|
void window_size_callback(GLFWwindow* window, int width, int height);
|
||||||
@@ -35,14 +36,15 @@ private:
|
|||||||
bool inspectedMaterialValid = false;
|
bool inspectedMaterialValid = false;
|
||||||
struct SceneSnapshot {
|
struct SceneSnapshot {
|
||||||
std::vector<SceneObject> objects;
|
std::vector<SceneObject> objects;
|
||||||
int selectedId = -1;
|
std::vector<int> selectedIds;
|
||||||
int nextId = 0;
|
int nextId = 0;
|
||||||
};
|
};
|
||||||
std::vector<SceneSnapshot> undoStack;
|
std::vector<SceneSnapshot> undoStack;
|
||||||
std::vector<SceneSnapshot> redoStack;
|
std::vector<SceneSnapshot> redoStack;
|
||||||
|
|
||||||
std::vector<SceneObject> sceneObjects;
|
std::vector<SceneObject> sceneObjects;
|
||||||
int selectedObjectId = -1;
|
int selectedObjectId = -1; // primary selection (last)
|
||||||
|
std::vector<int> selectedObjectIds; // multi-select
|
||||||
int nextObjectId = 0;
|
int nextObjectId = 0;
|
||||||
|
|
||||||
// Gizmo state
|
// Gizmo state
|
||||||
@@ -59,6 +61,7 @@ private:
|
|||||||
bool showFileBrowser = true;
|
bool showFileBrowser = true;
|
||||||
bool showConsole = true;
|
bool showConsole = true;
|
||||||
bool showProjectBrowser = true; // Now merged into file browser
|
bool showProjectBrowser = true; // Now merged into file browser
|
||||||
|
bool showMeshBuilder = false;
|
||||||
bool firstFrame = true;
|
bool firstFrame = true;
|
||||||
std::vector<std::string> consoleLog;
|
std::vector<std::string> consoleLog;
|
||||||
int draggedObjectId = -1;
|
int draggedObjectId = -1;
|
||||||
@@ -86,13 +89,31 @@ private:
|
|||||||
bool isPaused = false;
|
bool isPaused = false;
|
||||||
bool showViewOutput = true;
|
bool showViewOutput = true;
|
||||||
int previewCameraId = -1;
|
int previewCameraId = -1;
|
||||||
|
MeshBuilder meshBuilder;
|
||||||
|
char meshBuilderPath[260] = "";
|
||||||
|
char meshBuilderFaceInput[128] = "";
|
||||||
|
bool meshEditMode = false;
|
||||||
|
bool meshEditLoaded = false;
|
||||||
|
std::string meshEditPath;
|
||||||
|
RawMeshAsset meshEditAsset;
|
||||||
|
std::vector<int> meshEditSelectedVertices;
|
||||||
|
std::vector<int> meshEditSelectedEdges; // indices into generated edge list
|
||||||
|
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
|
||||||
|
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
|
||||||
|
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
SceneObject* getSelectedObject();
|
SceneObject* getSelectedObject();
|
||||||
|
glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
|
||||||
|
void setPrimarySelection(int id, bool additive = false);
|
||||||
|
void clearSelection();
|
||||||
static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale);
|
static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale);
|
||||||
|
|
||||||
void importOBJToScene(const std::string& filepath, const std::string& objectName);
|
void importOBJToScene(const std::string& filepath, const std::string& objectName);
|
||||||
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
|
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
|
||||||
|
void convertModelToRawMesh(const std::string& filepath);
|
||||||
|
bool ensureMeshEditTarget(SceneObject* obj);
|
||||||
|
bool syncMeshEditToGPU(SceneObject* obj);
|
||||||
void handleKeyboardShortcuts();
|
void handleKeyboardShortcuts();
|
||||||
void OpenProjectPath(const std::string& path);
|
void OpenProjectPath(const std::string& path);
|
||||||
|
|
||||||
@@ -106,6 +127,7 @@ private:
|
|||||||
void renderHierarchyPanel();
|
void renderHierarchyPanel();
|
||||||
void renderObjectNode(SceneObject& obj, const std::string& filter);
|
void renderObjectNode(SceneObject& obj, const std::string& filter);
|
||||||
void renderFileBrowserPanel();
|
void renderFileBrowserPanel();
|
||||||
|
void renderMeshBuilderPanel();
|
||||||
void renderInspectorPanel();
|
void renderInspectorPanel();
|
||||||
void renderConsolePanel();
|
void renderConsolePanel();
|
||||||
void renderViewport();
|
void renderViewport();
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
#include "ModelLoader.h"
|
#include "ModelLoader.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
#include <cfloat>
|
#include <cfloat>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_set>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -167,8 +170,6 @@ namespace FileIcons {
|
|||||||
|
|
||||||
// Draw an audio icon (speaker/waveform)
|
// Draw an audio icon (speaker/waveform)
|
||||||
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||||
float padding = size * 0.15f;
|
|
||||||
|
|
||||||
// Speaker body
|
// Speaker body
|
||||||
float spkW = size * 0.25f;
|
float spkW = size * 0.25f;
|
||||||
float spkH = size * 0.3f;
|
float spkH = size * 0.3f;
|
||||||
@@ -298,7 +299,6 @@ namespace FileIcons {
|
|||||||
void Engine::renderFileBrowserPanel() {
|
void Engine::renderFileBrowserPanel() {
|
||||||
ImGui::Begin("Project", &showFileBrowser);
|
ImGui::Begin("Project", &showFileBrowser);
|
||||||
ImGuiStyle& style = ImGui::GetStyle();
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
ImVec4 accent = style.Colors[ImGuiCol_CheckMark];
|
|
||||||
ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
|
ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
|
||||||
toolbarBg.x = std::min(toolbarBg.x + 0.02f, 1.0f);
|
toolbarBg.x = std::min(toolbarBg.x + 0.02f, 1.0f);
|
||||||
toolbarBg.y = std::min(toolbarBg.y + 0.02f, 1.0f);
|
toolbarBg.y = std::min(toolbarBg.y + 0.02f, 1.0f);
|
||||||
@@ -606,6 +606,9 @@ void Engine::renderFileBrowserPanel() {
|
|||||||
importModelToScene(entry.path().string(), "");
|
importModelToScene(entry.path().string(), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ImGui::MenuItem("Convert to Raw Mesh")) {
|
||||||
|
convertModelToRawMesh(entry.path().string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||||
if (ImGui::MenuItem("Apply to Selected")) {
|
if (ImGui::MenuItem("Apply to Selected")) {
|
||||||
@@ -689,6 +692,9 @@ void Engine::renderFileBrowserPanel() {
|
|||||||
}
|
}
|
||||||
if (fileBrowser.isModelFile(entry)) {
|
if (fileBrowser.isModelFile(entry)) {
|
||||||
bool isObj = fileBrowser.isOBJFile(entry);
|
bool isObj = fileBrowser.isOBJFile(entry);
|
||||||
|
std::string ext = entry.path().extension().string();
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||||
|
bool isRaw = ext == ".rmesh";
|
||||||
if (ImGui::MenuItem("Import to Scene")) {
|
if (ImGui::MenuItem("Import to Scene")) {
|
||||||
std::string defaultName = entry.path().stem().string();
|
std::string defaultName = entry.path().stem().string();
|
||||||
if (isObj) {
|
if (isObj) {
|
||||||
@@ -708,6 +714,9 @@ void Engine::renderFileBrowserPanel() {
|
|||||||
importModelToScene(entry.path().string(), "");
|
importModelToScene(entry.path().string(), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!isRaw && ImGui::MenuItem("Convert to Raw Mesh")) {
|
||||||
|
convertModelToRawMesh(entry.path().string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||||
if (ImGui::MenuItem("Apply to Selected")) {
|
if (ImGui::MenuItem("Apply to Selected")) {
|
||||||
@@ -761,6 +770,146 @@ void Engine::renderFileBrowserPanel() {
|
|||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Engine::renderMeshBuilderPanel() {
|
||||||
|
ImGui::Begin("Mesh Builder", &showMeshBuilder);
|
||||||
|
|
||||||
|
auto recalcBounds = [this]() {
|
||||||
|
if (!meshBuilder.hasMesh || meshBuilder.mesh.positions.empty()) return;
|
||||||
|
meshBuilder.mesh.boundsMin = glm::vec3(FLT_MAX);
|
||||||
|
meshBuilder.mesh.boundsMax = glm::vec3(-FLT_MAX);
|
||||||
|
for (const auto& p : meshBuilder.mesh.positions) {
|
||||||
|
meshBuilder.mesh.boundsMin.x = std::min(meshBuilder.mesh.boundsMin.x, p.x);
|
||||||
|
meshBuilder.mesh.boundsMin.y = std::min(meshBuilder.mesh.boundsMin.y, p.y);
|
||||||
|
meshBuilder.mesh.boundsMin.z = std::min(meshBuilder.mesh.boundsMin.z, p.z);
|
||||||
|
meshBuilder.mesh.boundsMax.x = std::max(meshBuilder.mesh.boundsMax.x, p.x);
|
||||||
|
meshBuilder.mesh.boundsMax.y = std::max(meshBuilder.mesh.boundsMax.y, p.y);
|
||||||
|
meshBuilder.mesh.boundsMax.z = std::max(meshBuilder.mesh.boundsMax.z, p.z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ImGui::InputText("Mesh Path", meshBuilderPath, sizeof(meshBuilderPath));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Load")) {
|
||||||
|
std::string err;
|
||||||
|
if (!meshBuilder.load(meshBuilderPath, err)) {
|
||||||
|
addConsoleMessage("MeshBuilder load failed: " + err, ConsoleMessageType::Error);
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Loaded raw mesh: " + meshBuilder.loadedPath, ConsoleMessageType::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Save")) {
|
||||||
|
std::string err;
|
||||||
|
std::string path = strlen(meshBuilderPath) ? meshBuilderPath : meshBuilder.loadedPath;
|
||||||
|
if (!meshBuilder.save(path, err)) {
|
||||||
|
addConsoleMessage("MeshBuilder save failed: " + err, ConsoleMessageType::Error);
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Saved raw mesh: " + meshBuilder.loadedPath, ConsoleMessageType::Success);
|
||||||
|
strncpy(meshBuilderPath, meshBuilder.loadedPath.c_str(), sizeof(meshBuilderPath) - 1);
|
||||||
|
meshBuilderPath[sizeof(meshBuilderPath) - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Button("Load Selected File")) {
|
||||||
|
if (!fileBrowser.selectedFile.empty() && fs::path(fileBrowser.selectedFile).extension() == ".rmesh") {
|
||||||
|
strncpy(meshBuilderPath, fileBrowser.selectedFile.string().c_str(), sizeof(meshBuilderPath) - 1);
|
||||||
|
meshBuilderPath[sizeof(meshBuilderPath) - 1] = '\0';
|
||||||
|
std::string err;
|
||||||
|
if (!meshBuilder.load(meshBuilderPath, err)) {
|
||||||
|
addConsoleMessage("MeshBuilder load failed: " + err, ConsoleMessageType::Error);
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Loaded raw mesh: " + meshBuilder.loadedPath, ConsoleMessageType::Success);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Select a .rmesh file in the browser to load", ConsoleMessageType::Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Recompute Normals")) {
|
||||||
|
meshBuilder.recomputeNormals();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (!meshBuilder.hasMesh) {
|
||||||
|
ImGui::TextDisabled("No mesh loaded.");
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Text("Vertices: %zu", meshBuilder.mesh.positions.size());
|
||||||
|
ImGui::Text("Faces: %zu", meshBuilder.mesh.faces.size());
|
||||||
|
ImGui::Text("Bounds Min: (%.3f, %.3f, %.3f)", meshBuilder.mesh.boundsMin.x, meshBuilder.mesh.boundsMin.y, meshBuilder.mesh.boundsMin.z);
|
||||||
|
ImGui::Text("Bounds Max: (%.3f, %.3f, %.3f)", meshBuilder.mesh.boundsMax.x, meshBuilder.mesh.boundsMax.y, meshBuilder.mesh.boundsMax.z);
|
||||||
|
if (meshBuilder.dirty) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(1,0.7f,0.2f,1),"*modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Vertices");
|
||||||
|
ImGui::SetNextItemWidth(100);
|
||||||
|
ImGui::InputInt("Selected", &meshBuilder.selectedVertex);
|
||||||
|
if (meshBuilder.selectedVertex < 0 || meshBuilder.selectedVertex >= (int)meshBuilder.mesh.positions.size()) {
|
||||||
|
meshBuilder.selectedVertex = meshBuilder.mesh.positions.empty() ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meshBuilder.selectedVertex >= 0 && meshBuilder.selectedVertex < (int)meshBuilder.mesh.positions.size()) {
|
||||||
|
glm::vec3& pos = meshBuilder.mesh.positions[meshBuilder.selectedVertex];
|
||||||
|
float edit[3] = { pos.x, pos.y, pos.z };
|
||||||
|
if (ImGui::InputFloat3("Position", edit, "%.4f")) {
|
||||||
|
pos = glm::vec3(edit[0], edit[1], edit[2]);
|
||||||
|
recalcBounds();
|
||||||
|
meshBuilder.recomputeNormals();
|
||||||
|
meshBuilder.dirty = true;
|
||||||
|
}
|
||||||
|
if (meshBuilder.mesh.hasUVs && meshBuilder.selectedVertex < (int)meshBuilder.mesh.uvs.size()) {
|
||||||
|
glm::vec2& uv = meshBuilder.mesh.uvs[meshBuilder.selectedVertex];
|
||||||
|
float uvEdit[2] = { uv.x, uv.y };
|
||||||
|
if (ImGui::InputFloat2("UV", uvEdit, "%.4f")) {
|
||||||
|
uv = glm::vec2(uvEdit[0], uvEdit[1]);
|
||||||
|
meshBuilder.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Add Face / Fill");
|
||||||
|
ImGui::InputTextWithHint("Indices", "e.g. 0,1,2 or 0 1 2 3", meshBuilderFaceInput, sizeof(meshBuilderFaceInput));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Add Face")) {
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
std::string token;
|
||||||
|
std::stringstream ss(meshBuilderFaceInput);
|
||||||
|
while (std::getline(ss, token, ',')) {
|
||||||
|
std::stringstream inner(token);
|
||||||
|
std::string sub;
|
||||||
|
while (inner >> sub) {
|
||||||
|
try {
|
||||||
|
uint32_t idx = static_cast<uint32_t>(std::stoul(sub));
|
||||||
|
indices.push_back(idx);
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indices.empty()) {
|
||||||
|
addConsoleMessage("Enter vertex indices separated by commas or spaces", ConsoleMessageType::Warning);
|
||||||
|
} else {
|
||||||
|
std::string err;
|
||||||
|
if (!meshBuilder.addFace(indices, err)) {
|
||||||
|
addConsoleMessage("Add face failed: " + err, ConsoleMessageType::Error);
|
||||||
|
} else {
|
||||||
|
addConsoleMessage("Added face with " + std::to_string(indices.size()) + " verts", ConsoleMessageType::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Faces (first 16)");
|
||||||
|
int maxFaces = std::min<int>(16, meshBuilder.mesh.faces.size());
|
||||||
|
for (int i = 0; i < maxFaces; i++) {
|
||||||
|
const auto& f = meshBuilder.mesh.faces[i];
|
||||||
|
ImGui::Text("%d: %u, %u, %u", i, f.x, f.y, f.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
void Engine::renderLauncher() {
|
void Engine::renderLauncher() {
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
@@ -1109,7 +1258,7 @@ void Engine::renderMainMenuBar() {
|
|||||||
}
|
}
|
||||||
projectManager.currentProject = Project();
|
projectManager.currentProject = Project();
|
||||||
sceneObjects.clear();
|
sceneObjects.clear();
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
showLauncher = true;
|
showLauncher = true;
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -1131,6 +1280,7 @@ void Engine::renderMainMenuBar() {
|
|||||||
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
|
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
|
||||||
ImGui::MenuItem("Console", nullptr, &showConsole);
|
ImGui::MenuItem("Console", nullptr, &showConsole);
|
||||||
ImGui::MenuItem("Project Manager", nullptr, &showProjectBrowser);
|
ImGui::MenuItem("Project Manager", nullptr, &showProjectBrowser);
|
||||||
|
ImGui::MenuItem("Mesh Builder", nullptr, &showMeshBuilder);
|
||||||
ImGui::MenuItem("Environment", nullptr, &showEnvironmentWindow);
|
ImGui::MenuItem("Environment", nullptr, &showEnvironmentWindow);
|
||||||
ImGui::MenuItem("Camera", nullptr, &showCameraWindow);
|
ImGui::MenuItem("Camera", nullptr, &showCameraWindow);
|
||||||
ImGui::MenuItem("View Output", nullptr, &showViewOutput);
|
ImGui::MenuItem("View Output", nullptr, &showViewOutput);
|
||||||
@@ -1219,7 +1369,6 @@ void Engine::renderHierarchyPanel() {
|
|||||||
headerBg.x = std::min(headerBg.x + 0.02f, 1.0f);
|
headerBg.x = std::min(headerBg.x + 0.02f, 1.0f);
|
||||||
headerBg.y = std::min(headerBg.y + 0.02f, 1.0f);
|
headerBg.y = std::min(headerBg.y + 0.02f, 1.0f);
|
||||||
headerBg.z = std::min(headerBg.z + 0.02f, 1.0f);
|
headerBg.z = std::min(headerBg.z + 0.02f, 1.0f);
|
||||||
ImVec4 headerText = style.Colors[ImGuiCol_CheckMark];
|
|
||||||
ImVec4 listBg = style.Colors[ImGuiCol_WindowBg];
|
ImVec4 listBg = style.Colors[ImGuiCol_WindowBg];
|
||||||
listBg.x = std::min(listBg.x + 0.01f, 1.0f);
|
listBg.x = std::min(listBg.x + 0.01f, 1.0f);
|
||||||
listBg.y = std::min(listBg.y + 0.01f, 1.0f);
|
listBg.y = std::min(listBg.y + 0.01f, 1.0f);
|
||||||
@@ -1311,7 +1460,7 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool hasChildren = !obj.childIds.empty();
|
bool hasChildren = !obj.childIds.empty();
|
||||||
bool isSelected = (selectedObjectId == obj.id);
|
bool isSelected = std::find(selectedObjectIds.begin(), selectedObjectIds.end(), obj.id) != selectedObjectIds.end();
|
||||||
|
|
||||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
|
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
|
||||||
@@ -1335,7 +1484,8 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
|
|||||||
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
|
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
|
||||||
|
|
||||||
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
|
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
|
||||||
selectedObjectId = obj.id;
|
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||||
|
setPrimarySelection(obj.id, additive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
||||||
@@ -1356,11 +1506,11 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
|
|||||||
|
|
||||||
if (ImGui::BeginPopupContextItem()) {
|
if (ImGui::BeginPopupContextItem()) {
|
||||||
if (ImGui::MenuItem("Duplicate")) {
|
if (ImGui::MenuItem("Duplicate")) {
|
||||||
selectedObjectId = obj.id;
|
setPrimarySelection(obj.id);
|
||||||
duplicateSelected();
|
duplicateSelected();
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem("Delete")) {
|
if (ImGui::MenuItem("Delete")) {
|
||||||
selectedObjectId = obj.id;
|
setPrimarySelection(obj.id);
|
||||||
deleteSelected();
|
deleteSelected();
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -1591,7 +1741,7 @@ void Engine::renderInspectorPanel() {
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedObjectId == -1) {
|
if (selectedObjectIds.empty()) {
|
||||||
if (browserHasMaterial) {
|
if (browserHasMaterial) {
|
||||||
renderMaterialAssetPanel("Material Asset", true);
|
renderMaterialAssetPanel("Material Asset", true);
|
||||||
} else {
|
} else {
|
||||||
@@ -1601,8 +1751,9 @@ void Engine::renderInspectorPanel() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int primaryId = selectedObjectId;
|
||||||
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
||||||
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
|
[primaryId](const SceneObject& obj) { return obj.id == primaryId; });
|
||||||
|
|
||||||
if (it == sceneObjects.end()) {
|
if (it == sceneObjects.end()) {
|
||||||
ImGui::TextDisabled("Object not found");
|
ImGui::TextDisabled("Object not found");
|
||||||
@@ -1612,6 +1763,11 @@ void Engine::renderInspectorPanel() {
|
|||||||
|
|
||||||
SceneObject& obj = *it;
|
SceneObject& obj = *it;
|
||||||
|
|
||||||
|
if (selectedObjectIds.size() > 1) {
|
||||||
|
ImGui::Text("Multiple objects selected: %zu", selectedObjectIds.size());
|
||||||
|
ImGui::Separator();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader("Object Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
if (ImGui::CollapsingHeader("Object Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
@@ -2474,7 +2630,6 @@ void Engine::renderViewport() {
|
|||||||
ImGuizmo::Enable(true);
|
ImGuizmo::Enable(true);
|
||||||
ImGuizmo::SetOrthographic(false);
|
ImGuizmo::SetOrthographic(false);
|
||||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||||
|
|
||||||
ImGuizmo::SetRect(
|
ImGuizmo::SetRect(
|
||||||
imageMin.x,
|
imageMin.x,
|
||||||
imageMin.y,
|
imageMin.y,
|
||||||
@@ -2482,141 +2637,429 @@ void Engine::renderViewport() {
|
|||||||
imageMax.y - imageMin.y
|
imageMax.y - imageMin.y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
auto compose = [](const SceneObject& o) {
|
||||||
|
glm::mat4 m(1.0f);
|
||||||
|
m = glm::translate(m, o.position);
|
||||||
|
m = glm::rotate(m, glm::radians(o.rotation.x), glm::vec3(1, 0, 0));
|
||||||
|
m = glm::rotate(m, glm::radians(o.rotation.y), glm::vec3(0, 1, 0));
|
||||||
|
m = glm::rotate(m, glm::radians(o.rotation.z), glm::vec3(0, 0, 1));
|
||||||
|
m = glm::scale(m, o.scale);
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool meshModeActive = meshEditMode && ensureMeshEditTarget(selectedObj);
|
||||||
|
|
||||||
|
glm::vec3 pivotPos = selectedObj->position;
|
||||||
|
if (!meshModeActive && selectedObjectIds.size() > 1 && mCurrentGizmoMode == ImGuizmo::WORLD) {
|
||||||
|
pivotPos = getSelectionCenterWorld(true);
|
||||||
|
}
|
||||||
|
|
||||||
glm::mat4 modelMatrix(1.0f);
|
glm::mat4 modelMatrix(1.0f);
|
||||||
modelMatrix = glm::translate(modelMatrix, selectedObj->position);
|
modelMatrix = glm::translate(modelMatrix, pivotPos);
|
||||||
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.x), glm::vec3(1, 0, 0));
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.x), glm::vec3(1, 0, 0));
|
||||||
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.y), glm::vec3(0, 1, 0));
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.y), glm::vec3(0, 1, 0));
|
||||||
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.z), glm::vec3(0, 0, 1));
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.z), glm::vec3(0, 0, 1));
|
||||||
modelMatrix = glm::scale(modelMatrix, selectedObj->scale);
|
modelMatrix = glm::scale(modelMatrix, selectedObj->scale);
|
||||||
|
glm::mat4 originalModel = modelMatrix;
|
||||||
|
|
||||||
float* snapPtr = nullptr;
|
if (meshModeActive && !meshEditAsset.positions.empty()) {
|
||||||
float snapRot[3] = { rotationSnapValue, rotationSnapValue, rotationSnapValue };
|
// Build helper edge list (dedup) for edge/face modes
|
||||||
|
std::vector<glm::u32vec2> edges;
|
||||||
if (useSnap) {
|
edges.reserve(meshEditAsset.faces.size() * 3);
|
||||||
if (mCurrentGizmoOperation == ImGuizmo::ROTATE) {
|
std::unordered_set<uint64_t> edgeSet;
|
||||||
snapPtr = snapRot;
|
auto edgeKey = [](uint32_t a, uint32_t b) {
|
||||||
} else {
|
return (static_cast<uint64_t>(std::min(a,b)) << 32) | static_cast<uint64_t>(std::max(a,b));
|
||||||
snapPtr = snapValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 gizmoBoundsMin(-0.5f);
|
|
||||||
glm::vec3 gizmoBoundsMax(0.5f);
|
|
||||||
|
|
||||||
switch (selectedObj->type) {
|
|
||||||
case ObjectType::Cube:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.5f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.5f);
|
|
||||||
break;
|
|
||||||
case ObjectType::Sphere:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.5f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.5f);
|
|
||||||
break;
|
|
||||||
case ObjectType::Capsule:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.35f, -0.9f, -0.35f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.35f, 0.9f, 0.35f);
|
|
||||||
break;
|
|
||||||
case ObjectType::OBJMesh: {
|
|
||||||
const auto* info = g_objLoader.getMeshInfo(selectedObj->meshId);
|
|
||||||
if (info && info->boundsMin.x < info->boundsMax.x) {
|
|
||||||
gizmoBoundsMin = info->boundsMin;
|
|
||||||
gizmoBoundsMax = info->boundsMax;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ObjectType::Model: {
|
|
||||||
const auto* info = getModelLoader().getMeshInfo(selectedObj->meshId);
|
|
||||||
if (info && info->boundsMin.x < info->boundsMax.x) {
|
|
||||||
gizmoBoundsMin = info->boundsMin;
|
|
||||||
gizmoBoundsMax = info->boundsMax;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ObjectType::Camera:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.3f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.3f);
|
|
||||||
break;
|
|
||||||
case ObjectType::DirectionalLight:
|
|
||||||
case ObjectType::PointLight:
|
|
||||||
case ObjectType::SpotLight:
|
|
||||||
case ObjectType::AreaLight:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.3f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.3f);
|
|
||||||
break;
|
|
||||||
case ObjectType::PostFXNode:
|
|
||||||
gizmoBoundsMin = glm::vec3(-0.25f);
|
|
||||||
gizmoBoundsMax = glm::vec3(0.25f);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
float bounds[6] = {
|
|
||||||
gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z,
|
|
||||||
gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z
|
|
||||||
};
|
|
||||||
float boundsSnap[3] = { snapValue[0], snapValue[1], snapValue[2] };
|
|
||||||
const float* boundsPtr = (mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? bounds : nullptr;
|
|
||||||
const float* boundsSnapPtr = (useSnap && mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? boundsSnap : nullptr;
|
|
||||||
|
|
||||||
ImGuizmo::Manipulate(
|
|
||||||
glm::value_ptr(view),
|
|
||||||
glm::value_ptr(proj),
|
|
||||||
mCurrentGizmoOperation,
|
|
||||||
mCurrentGizmoMode,
|
|
||||||
glm::value_ptr(modelMatrix),
|
|
||||||
nullptr,
|
|
||||||
snapPtr,
|
|
||||||
boundsPtr,
|
|
||||||
boundsSnapPtr
|
|
||||||
);
|
|
||||||
|
|
||||||
std::array<glm::vec3, 8> corners = {
|
|
||||||
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
|
|
||||||
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
|
|
||||||
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
|
|
||||||
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
|
|
||||||
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
|
|
||||||
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
|
|
||||||
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
|
|
||||||
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
|
|
||||||
};
|
|
||||||
|
|
||||||
std::array<ImVec2, 8> projected{};
|
|
||||||
bool allProjected = true;
|
|
||||||
for (size_t i = 0; i < corners.size(); ++i) {
|
|
||||||
glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(corners[i], 1.0f));
|
|
||||||
auto p = projectToScreen(world);
|
|
||||||
if (!p.has_value()) { allProjected = false; break; }
|
|
||||||
projected[i] = *p;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allProjected) {
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
||||||
ImU32 col = ImGui::GetColorU32(ImVec4(1.0f, 0.93f, 0.35f, 0.45f));
|
|
||||||
const int edges[12][2] = {
|
|
||||||
{0,1},{1,2},{2,3},{3,0},
|
|
||||||
{4,5},{5,6},{6,7},{7,4},
|
|
||||||
{0,4},{1,5},{2,6},{3,7}
|
|
||||||
};
|
};
|
||||||
for (auto& e : edges) {
|
for (size_t fi = 0; fi < meshEditAsset.faces.size(); ++fi) {
|
||||||
dl->AddLine(projected[e[0]], projected[e[1]], col, 2.0f);
|
const auto& f = meshEditAsset.faces[fi];
|
||||||
|
uint32_t tri[3] = { f.x, f.y, f.z };
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
uint32_t a = tri[e];
|
||||||
|
uint32_t b = tri[(e+1)%3];
|
||||||
|
uint64_t key = edgeKey(a,b);
|
||||||
|
if (edgeSet.insert(key).second) {
|
||||||
|
edges.push_back(glm::u32vec2(std::min(a,b), std::max(a,b)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGuizmo::IsUsing()) {
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
if (!gizmoHistoryCaptured) {
|
ImU32 vertCol = ImGui::GetColorU32(ImVec4(0.35f, 0.75f, 1.0f, 0.9f));
|
||||||
recordState("gizmo");
|
ImU32 selCol = ImGui::GetColorU32(ImVec4(1.0f, 0.6f, 0.2f, 1.0f));
|
||||||
gizmoHistoryCaptured = true;
|
ImU32 edgeCol = ImGui::GetColorU32(ImVec4(0.6f, 0.9f, 1.0f, 0.6f));
|
||||||
|
ImU32 faceCol = ImGui::GetColorU32(ImVec4(1.0f, 0.8f, 0.4f, 0.7f));
|
||||||
|
|
||||||
|
float selectRadius = 10.0f;
|
||||||
|
ImVec2 mouse = ImGui::GetIO().MousePos;
|
||||||
|
bool clicked = mouseOverViewportImage && ImGui::IsMouseClicked(0);
|
||||||
|
bool additiveClick = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||||
|
float bestDist = selectRadius;
|
||||||
|
int clickedIndex = -1;
|
||||||
|
|
||||||
|
glm::mat4 invModel = glm::inverse(modelMatrix);
|
||||||
|
|
||||||
|
if (meshEditSelectionMode == MeshEditSelectionMode::Vertex) {
|
||||||
|
const size_t maxDraw = std::min<size_t>(meshEditAsset.positions.size(), 2000);
|
||||||
|
for (size_t i = 0; i < maxDraw; ++i) {
|
||||||
|
glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[i], 1.0f));
|
||||||
|
auto screen = projectToScreen(world);
|
||||||
|
if (!screen) continue;
|
||||||
|
bool sel = std::find(meshEditSelectedVertices.begin(), meshEditSelectedVertices.end(), (int)i) != meshEditSelectedVertices.end();
|
||||||
|
float radius = sel ? 6.5f : 5.0f;
|
||||||
|
dl->AddCircleFilled(*screen, radius, sel ? selCol : vertCol);
|
||||||
|
|
||||||
|
if (clicked) {
|
||||||
|
float dx = screen->x - mouse.x;
|
||||||
|
float dy = screen->y - mouse.y;
|
||||||
|
float dist = std::sqrt(dx*dx + dy*dy);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
clickedIndex = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickedIndex >= 0) {
|
||||||
|
if (additiveClick) {
|
||||||
|
auto itSel = std::find(meshEditSelectedVertices.begin(), meshEditSelectedVertices.end(), clickedIndex);
|
||||||
|
if (itSel == meshEditSelectedVertices.end()) {
|
||||||
|
meshEditSelectedVertices.push_back(clickedIndex);
|
||||||
|
} else {
|
||||||
|
meshEditSelectedVertices.erase(itSel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
meshEditSelectedVertices.clear();
|
||||||
|
meshEditSelectedVertices.push_back(clickedIndex);
|
||||||
|
}
|
||||||
|
meshEditSelectedEdges.clear();
|
||||||
|
meshEditSelectedFaces.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meshEditSelectedVertices.empty()) {
|
||||||
|
meshEditSelectedVertices.push_back(0);
|
||||||
|
}
|
||||||
|
} else if (meshEditSelectionMode == MeshEditSelectionMode::Edge) {
|
||||||
|
for (size_t ei = 0; ei < edges.size(); ++ei) {
|
||||||
|
const auto& e = edges[ei];
|
||||||
|
glm::vec3 a = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[e.x], 1.0f));
|
||||||
|
glm::vec3 b = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[e.y], 1.0f));
|
||||||
|
auto sa = projectToScreen(a);
|
||||||
|
auto sb = projectToScreen(b);
|
||||||
|
if (!sa || !sb) continue;
|
||||||
|
ImVec2 mid = ImVec2((sa->x + sb->x) * 0.5f, (sa->y + sb->y) * 0.5f);
|
||||||
|
bool sel = std::find(meshEditSelectedEdges.begin(), meshEditSelectedEdges.end(), (int)ei) != meshEditSelectedEdges.end();
|
||||||
|
dl->AddLine(*sa, *sb, edgeCol, sel ? 3.0f : 2.0f);
|
||||||
|
dl->AddCircleFilled(mid, sel ? 6.0f : 4.0f, sel ? selCol : edgeCol);
|
||||||
|
|
||||||
|
if (clicked) {
|
||||||
|
float dx = mid.x - mouse.x;
|
||||||
|
float dy = mid.y - mouse.y;
|
||||||
|
float dist = std::sqrt(dx*dx + dy*dy);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
clickedIndex = static_cast<int>(ei);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clickedIndex >= 0) {
|
||||||
|
if (additiveClick) {
|
||||||
|
auto itSel = std::find(meshEditSelectedEdges.begin(), meshEditSelectedEdges.end(), clickedIndex);
|
||||||
|
if (itSel == meshEditSelectedEdges.end()) {
|
||||||
|
meshEditSelectedEdges.push_back(clickedIndex);
|
||||||
|
} else {
|
||||||
|
meshEditSelectedEdges.erase(itSel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
meshEditSelectedEdges.clear();
|
||||||
|
meshEditSelectedEdges.push_back(clickedIndex);
|
||||||
|
}
|
||||||
|
meshEditSelectedVertices.clear();
|
||||||
|
meshEditSelectedFaces.clear();
|
||||||
|
}
|
||||||
|
if (meshEditSelectedEdges.empty() && !edges.empty()) {
|
||||||
|
meshEditSelectedEdges.push_back(0);
|
||||||
|
}
|
||||||
|
} else if (meshEditSelectionMode == MeshEditSelectionMode::Face) {
|
||||||
|
for (size_t fi = 0; fi < meshEditAsset.faces.size(); ++fi) {
|
||||||
|
const auto& f = meshEditAsset.faces[fi];
|
||||||
|
glm::vec3 a = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[f.x], 1.0f));
|
||||||
|
glm::vec3 b = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[f.y], 1.0f));
|
||||||
|
glm::vec3 c = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[f.z], 1.0f));
|
||||||
|
glm::vec3 centroid = (a + b + c) / 3.0f;
|
||||||
|
auto sc = projectToScreen(centroid);
|
||||||
|
if (!sc) continue;
|
||||||
|
bool sel = std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), (int)fi) != meshEditSelectedFaces.end();
|
||||||
|
dl->AddCircleFilled(*sc, sel ? 7.0f : 5.0f, sel ? selCol : faceCol);
|
||||||
|
|
||||||
|
if (clicked) {
|
||||||
|
float dx = sc->x - mouse.x;
|
||||||
|
float dy = sc->y - mouse.y;
|
||||||
|
float dist = std::sqrt(dx*dx + dy*dy);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
clickedIndex = static_cast<int>(fi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clickedIndex >= 0) {
|
||||||
|
if (additiveClick) {
|
||||||
|
auto itSel = std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), clickedIndex);
|
||||||
|
if (itSel == meshEditSelectedFaces.end()) {
|
||||||
|
meshEditSelectedFaces.push_back(clickedIndex);
|
||||||
|
} else {
|
||||||
|
meshEditSelectedFaces.erase(itSel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
meshEditSelectedFaces.clear();
|
||||||
|
meshEditSelectedFaces.push_back(clickedIndex);
|
||||||
|
}
|
||||||
|
meshEditSelectedVertices.clear();
|
||||||
|
meshEditSelectedEdges.clear();
|
||||||
|
}
|
||||||
|
if (meshEditSelectedFaces.empty() && !meshEditAsset.faces.empty()) {
|
||||||
|
meshEditSelectedFaces.push_back(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
float t[3], r[3], s[3];
|
|
||||||
ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(modelMatrix), t, r, s);
|
|
||||||
|
|
||||||
selectedObj->position = glm::vec3(t[0], t[1], t[2]);
|
// Compute affected vertices from selection
|
||||||
selectedObj->rotation = glm::vec3(r[0], r[1], r[2]);
|
std::vector<int> affectedVerts = meshEditSelectedVertices;
|
||||||
selectedObj->scale = glm::vec3(s[0], s[1], s[2]);
|
auto pushUnique = [&](int idx) {
|
||||||
|
if (idx < 0) return;
|
||||||
|
if (std::find(affectedVerts.begin(), affectedVerts.end(), idx) == affectedVerts.end()) {
|
||||||
|
affectedVerts.push_back(idx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (meshEditSelectionMode == MeshEditSelectionMode::Edge) {
|
||||||
|
for (int ei : meshEditSelectedEdges) {
|
||||||
|
if (ei < 0 || ei >= (int)edges.size()) continue;
|
||||||
|
pushUnique(edges[ei].x);
|
||||||
|
pushUnique(edges[ei].y);
|
||||||
|
}
|
||||||
|
} else if (meshEditSelectionMode == MeshEditSelectionMode::Face) {
|
||||||
|
for (int fi : meshEditSelectedFaces) {
|
||||||
|
if (fi < 0 || fi >= (int)meshEditAsset.faces.size()) continue;
|
||||||
|
const auto& f = meshEditAsset.faces[fi];
|
||||||
|
pushUnique(f.x);
|
||||||
|
pushUnique(f.y);
|
||||||
|
pushUnique(f.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (affectedVerts.empty() && !meshEditAsset.positions.empty()) {
|
||||||
|
affectedVerts.push_back(0);
|
||||||
|
}
|
||||||
|
|
||||||
projectManager.currentProject.hasUnsavedChanges = true;
|
glm::vec3 pivotWorld(0.0f);
|
||||||
|
for (int idx : affectedVerts) {
|
||||||
|
glm::vec3 wp = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[idx], 1.0f));
|
||||||
|
pivotWorld += wp;
|
||||||
|
}
|
||||||
|
pivotWorld /= (float)affectedVerts.size();
|
||||||
|
|
||||||
|
glm::mat4 gizmoMat = glm::translate(glm::mat4(1.0f), pivotWorld);
|
||||||
|
|
||||||
|
ImGuizmo::Manipulate(
|
||||||
|
glm::value_ptr(view),
|
||||||
|
glm::value_ptr(proj),
|
||||||
|
ImGuizmo::TRANSLATE,
|
||||||
|
ImGuizmo::WORLD,
|
||||||
|
glm::value_ptr(gizmoMat)
|
||||||
|
);
|
||||||
|
|
||||||
|
static bool meshEditHistoryCaptured = false;
|
||||||
|
if (ImGuizmo::IsUsing()) {
|
||||||
|
if (!meshEditHistoryCaptured) {
|
||||||
|
recordState("meshEdit");
|
||||||
|
meshEditHistoryCaptured = true;
|
||||||
|
}
|
||||||
|
glm::vec3 deltaWorld = glm::vec3(gizmoMat[3]) - pivotWorld;
|
||||||
|
for (int idx : affectedVerts) {
|
||||||
|
glm::vec3 wp = glm::vec3(modelMatrix * glm::vec4(meshEditAsset.positions[idx], 1.0f));
|
||||||
|
wp += deltaWorld;
|
||||||
|
glm::vec3 newLocal = glm::vec3(invModel * glm::vec4(wp, 1.0f));
|
||||||
|
meshEditAsset.positions[idx] = newLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute bounds
|
||||||
|
meshEditAsset.boundsMin = glm::vec3(FLT_MAX);
|
||||||
|
meshEditAsset.boundsMax = glm::vec3(-FLT_MAX);
|
||||||
|
for (const auto& p : meshEditAsset.positions) {
|
||||||
|
meshEditAsset.boundsMin.x = std::min(meshEditAsset.boundsMin.x, p.x);
|
||||||
|
meshEditAsset.boundsMin.y = std::min(meshEditAsset.boundsMin.y, p.y);
|
||||||
|
meshEditAsset.boundsMin.z = std::min(meshEditAsset.boundsMin.z, p.z);
|
||||||
|
meshEditAsset.boundsMax.x = std::max(meshEditAsset.boundsMax.x, p.x);
|
||||||
|
meshEditAsset.boundsMax.y = std::max(meshEditAsset.boundsMax.y, p.y);
|
||||||
|
meshEditAsset.boundsMax.z = std::max(meshEditAsset.boundsMax.z, p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute normals
|
||||||
|
meshEditAsset.normals.assign(meshEditAsset.positions.size(), glm::vec3(0.0f));
|
||||||
|
for (const auto& f : meshEditAsset.faces) {
|
||||||
|
if (f.x >= meshEditAsset.positions.size() || f.y >= meshEditAsset.positions.size() || f.z >= meshEditAsset.positions.size()) continue;
|
||||||
|
const glm::vec3& a = meshEditAsset.positions[f.x];
|
||||||
|
const glm::vec3& b = meshEditAsset.positions[f.y];
|
||||||
|
const glm::vec3& c = meshEditAsset.positions[f.z];
|
||||||
|
glm::vec3 n = glm::normalize(glm::cross(b - a, c - a));
|
||||||
|
meshEditAsset.normals[f.x] += n;
|
||||||
|
meshEditAsset.normals[f.y] += n;
|
||||||
|
meshEditAsset.normals[f.z] += n;
|
||||||
|
}
|
||||||
|
for (auto& n : meshEditAsset.normals) {
|
||||||
|
if (glm::length(n) > 1e-6f) n = glm::normalize(n);
|
||||||
|
}
|
||||||
|
meshEditAsset.hasNormals = true;
|
||||||
|
|
||||||
|
syncMeshEditToGPU(selectedObj);
|
||||||
|
} else {
|
||||||
|
meshEditHistoryCaptured = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
gizmoHistoryCaptured = false;
|
// Object transform mode
|
||||||
|
float* snapPtr = nullptr;
|
||||||
|
float snapRot[3] = { rotationSnapValue, rotationSnapValue, rotationSnapValue };
|
||||||
|
|
||||||
|
if (useSnap) {
|
||||||
|
if (mCurrentGizmoOperation == ImGuizmo::ROTATE) {
|
||||||
|
snapPtr = snapRot;
|
||||||
|
} else {
|
||||||
|
snapPtr = snapValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 gizmoBoundsMin(-0.5f);
|
||||||
|
glm::vec3 gizmoBoundsMax(0.5f);
|
||||||
|
|
||||||
|
switch (selectedObj->type) {
|
||||||
|
case ObjectType::Cube:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.5f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.5f);
|
||||||
|
break;
|
||||||
|
case ObjectType::Sphere:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.5f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.5f);
|
||||||
|
break;
|
||||||
|
case ObjectType::Capsule:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.35f, -0.9f, -0.35f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.35f, 0.9f, 0.35f);
|
||||||
|
break;
|
||||||
|
case ObjectType::OBJMesh: {
|
||||||
|
const auto* info = g_objLoader.getMeshInfo(selectedObj->meshId);
|
||||||
|
if (info && info->boundsMin.x < info->boundsMax.x) {
|
||||||
|
gizmoBoundsMin = info->boundsMin;
|
||||||
|
gizmoBoundsMax = info->boundsMax;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectType::Model: {
|
||||||
|
const auto* info = getModelLoader().getMeshInfo(selectedObj->meshId);
|
||||||
|
if (info && info->boundsMin.x < info->boundsMax.x) {
|
||||||
|
gizmoBoundsMin = info->boundsMin;
|
||||||
|
gizmoBoundsMax = info->boundsMax;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectType::Camera:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.3f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.3f);
|
||||||
|
break;
|
||||||
|
case ObjectType::DirectionalLight:
|
||||||
|
case ObjectType::PointLight:
|
||||||
|
case ObjectType::SpotLight:
|
||||||
|
case ObjectType::AreaLight:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.3f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.3f);
|
||||||
|
break;
|
||||||
|
case ObjectType::PostFXNode:
|
||||||
|
gizmoBoundsMin = glm::vec3(-0.25f);
|
||||||
|
gizmoBoundsMax = glm::vec3(0.25f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
float bounds[6] = {
|
||||||
|
gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z,
|
||||||
|
gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z
|
||||||
|
};
|
||||||
|
float boundsSnap[3] = { snapValue[0], snapValue[1], snapValue[2] };
|
||||||
|
const float* boundsPtr = (mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? bounds : nullptr;
|
||||||
|
const float* boundsSnapPtr = (useSnap && mCurrentGizmoOperation == ImGuizmo::BOUNDS) ? boundsSnap : nullptr;
|
||||||
|
|
||||||
|
ImGuizmo::Manipulate(
|
||||||
|
glm::value_ptr(view),
|
||||||
|
glm::value_ptr(proj),
|
||||||
|
mCurrentGizmoOperation,
|
||||||
|
mCurrentGizmoMode,
|
||||||
|
glm::value_ptr(modelMatrix),
|
||||||
|
nullptr,
|
||||||
|
snapPtr,
|
||||||
|
boundsPtr,
|
||||||
|
boundsSnapPtr
|
||||||
|
);
|
||||||
|
|
||||||
|
std::array<glm::vec3, 8> corners = {
|
||||||
|
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
|
||||||
|
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMin.z),
|
||||||
|
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
|
||||||
|
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMin.z),
|
||||||
|
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
|
||||||
|
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMin.y, gizmoBoundsMax.z),
|
||||||
|
glm::vec3(gizmoBoundsMax.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
|
||||||
|
glm::vec3(gizmoBoundsMin.x, gizmoBoundsMax.y, gizmoBoundsMax.z),
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<ImVec2, 8> projected{};
|
||||||
|
bool allProjected = true;
|
||||||
|
for (size_t i = 0; i < corners.size(); ++i) {
|
||||||
|
glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(corners[i], 1.0f));
|
||||||
|
auto p = projectToScreen(world);
|
||||||
|
if (!p.has_value()) { allProjected = false; break; }
|
||||||
|
projected[i] = *p;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allProjected) {
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImU32 col = ImGui::GetColorU32(ImVec4(1.0f, 0.93f, 0.35f, 0.45f));
|
||||||
|
const int edges[12][2] = {
|
||||||
|
{0,1},{1,2},{2,3},{3,0},
|
||||||
|
{4,5},{5,6},{6,7},{7,4},
|
||||||
|
{0,4},{1,5},{2,6},{3,7}
|
||||||
|
};
|
||||||
|
for (auto& e : edges) {
|
||||||
|
dl->AddLine(projected[e[0]], projected[e[1]], col, 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGuizmo::IsUsing()) {
|
||||||
|
if (!gizmoHistoryCaptured) {
|
||||||
|
recordState("gizmo");
|
||||||
|
gizmoHistoryCaptured = true;
|
||||||
|
}
|
||||||
|
glm::mat4 delta = modelMatrix * glm::inverse(originalModel);
|
||||||
|
|
||||||
|
auto applyDelta = [&](SceneObject& o) {
|
||||||
|
glm::mat4 m = compose(o);
|
||||||
|
glm::mat4 newM = delta * m;
|
||||||
|
glm::vec3 t, r, s;
|
||||||
|
DecomposeMatrix(newM, t, r, s);
|
||||||
|
o.position = t;
|
||||||
|
o.rotation = glm::degrees(r);
|
||||||
|
o.scale = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedObjectIds.size() <= 1) {
|
||||||
|
applyDelta(*selectedObj);
|
||||||
|
} else {
|
||||||
|
for (int id : selectedObjectIds) {
|
||||||
|
auto itObj = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
||||||
|
[id](const SceneObject& o){ return o.id == id; });
|
||||||
|
if (itObj != sceneObjects.end()) {
|
||||||
|
applyDelta(*itObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectManager.currentProject.hasUnsavedChanges = true;
|
||||||
|
} else {
|
||||||
|
gizmoHistoryCaptured = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2727,6 +3170,40 @@ void Engine::renderViewport() {
|
|||||||
ImGui::SameLine(0.0f, toolbarSpacing);
|
ImGui::SameLine(0.0f, toolbarSpacing);
|
||||||
gizmoButton("Scale", ImGuizmo::SCALE, "Scale");
|
gizmoButton("Scale", ImGuizmo::SCALE, "Scale");
|
||||||
ImGui::SameLine(0.0f, toolbarSpacing);
|
ImGui::SameLine(0.0f, toolbarSpacing);
|
||||||
|
bool canMeshEdit = false;
|
||||||
|
if (selectedObj) {
|
||||||
|
std::string ext = fs::path(selectedObj->meshPath).extension().string();
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||||
|
canMeshEdit = ext == ".rmesh";
|
||||||
|
}
|
||||||
|
ImGui::BeginDisabled(!canMeshEdit);
|
||||||
|
if (GizmoToolbar::ModeButton("Mesh Edit", meshEditMode, gizmoButtonSize, baseCol, accentCol, textCol)) {
|
||||||
|
meshEditMode = !meshEditMode;
|
||||||
|
if (!meshEditMode) {
|
||||||
|
meshEditLoaded = false;
|
||||||
|
meshEditPath.clear();
|
||||||
|
meshEditSelectedVertices.clear();
|
||||||
|
meshEditSelectedEdges.clear();
|
||||||
|
meshEditSelectedFaces.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle mesh vertex edit mode");
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (meshEditMode) {
|
||||||
|
ImGui::SameLine(0.0f, toolbarSpacing);
|
||||||
|
if (GizmoToolbar::ModeButton("Verts", meshEditSelectionMode == MeshEditSelectionMode::Vertex, ImVec2(50,24), baseCol, accentCol, textCol)) {
|
||||||
|
meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0.0f, toolbarSpacing * 0.6f);
|
||||||
|
if (GizmoToolbar::ModeButton("Edges", meshEditSelectionMode == MeshEditSelectionMode::Edge, ImVec2(50,24), baseCol, accentCol, textCol)) {
|
||||||
|
meshEditSelectionMode = MeshEditSelectionMode::Edge;
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0.0f, toolbarSpacing * 0.6f);
|
||||||
|
if (GizmoToolbar::ModeButton("Faces", meshEditSelectionMode == MeshEditSelectionMode::Face, ImVec2(50,24), baseCol, accentCol, textCol)) {
|
||||||
|
meshEditSelectionMode = MeshEditSelectionMode::Face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0.0f, toolbarSpacing);
|
||||||
gizmoButton("Rect", ImGuizmo::BOUNDS, "Rect scale");
|
gizmoButton("Rect", ImGuizmo::BOUNDS, "Rect scale");
|
||||||
ImGui::SameLine(0.0f, toolbarSpacing);
|
ImGui::SameLine(0.0f, toolbarSpacing);
|
||||||
gizmoButton("Uni", ImGuizmo::UNIVERSAL, "Universal");
|
gizmoButton("Uni", ImGuizmo::UNIVERSAL, "Universal");
|
||||||
@@ -2952,9 +3429,10 @@ void Engine::renderViewport() {
|
|||||||
|
|
||||||
viewportController.setFocused(true);
|
viewportController.setFocused(true);
|
||||||
if (hitId != -1) {
|
if (hitId != -1) {
|
||||||
selectedObjectId = hitId;
|
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||||
|
setPrimarySelection(hitId, additive);
|
||||||
} else {
|
} else {
|
||||||
selectedObjectId = -1;
|
clearSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
src/MeshBuilder.cpp
Normal file
105
src/MeshBuilder.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#include "MeshBuilder.h"
|
||||||
|
|
||||||
|
void MeshBuilder::clear() {
|
||||||
|
mesh = RawMeshAsset();
|
||||||
|
hasMesh = false;
|
||||||
|
dirty = false;
|
||||||
|
selectedVertex = -1;
|
||||||
|
loadedPath.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MeshBuilder::load(const std::string& path, std::string& error) {
|
||||||
|
auto& loader = getModelLoader();
|
||||||
|
RawMeshAsset loaded;
|
||||||
|
if (!loader.loadRawMesh(path, loaded, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mesh = std::move(loaded);
|
||||||
|
hasMesh = true;
|
||||||
|
dirty = false;
|
||||||
|
loadedPath = path;
|
||||||
|
selectedVertex = mesh.positions.empty() ? -1 : 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MeshBuilder::save(const std::string& path, std::string& error) {
|
||||||
|
if (!hasMesh) {
|
||||||
|
error = "No mesh loaded";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto& loader = getModelLoader();
|
||||||
|
if (!loader.saveRawMesh(mesh, path, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loadedPath = path;
|
||||||
|
dirty = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshBuilder::recomputeNormals() {
|
||||||
|
if (mesh.positions.empty()) return;
|
||||||
|
mesh.normals.assign(mesh.positions.size(), glm::vec3(0.0f));
|
||||||
|
|
||||||
|
for (const auto& face : mesh.faces) {
|
||||||
|
if (face.x >= mesh.positions.size() ||
|
||||||
|
face.y >= mesh.positions.size() ||
|
||||||
|
face.z >= mesh.positions.size()) continue;
|
||||||
|
const glm::vec3& a = mesh.positions[face.x];
|
||||||
|
const glm::vec3& b = mesh.positions[face.y];
|
||||||
|
const glm::vec3& c = mesh.positions[face.z];
|
||||||
|
glm::vec3 n = glm::normalize(glm::cross(b - a, c - a));
|
||||||
|
mesh.normals[face.x] += n;
|
||||||
|
mesh.normals[face.y] += n;
|
||||||
|
mesh.normals[face.z] += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& n : mesh.normals) {
|
||||||
|
if (glm::length(n) > 1e-6f) {
|
||||||
|
n = glm::normalize(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mesh.hasNormals = true;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MeshBuilder::addFace(const std::vector<uint32_t>& indices, std::string& error) {
|
||||||
|
if (indices.size() < 3) {
|
||||||
|
error = "Need at least 3 vertices to form a face";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t idx : indices) {
|
||||||
|
if (idx >= mesh.positions.size()) {
|
||||||
|
error = "Vertex index out of range";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto addTri = [&](uint32_t a, uint32_t b, uint32_t c) {
|
||||||
|
mesh.faces.push_back(glm::u32vec3(a, b, c));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (indices.size() == 3) {
|
||||||
|
addTri(indices[0], indices[1], indices[2]);
|
||||||
|
} else {
|
||||||
|
// Fan triangulation for n-gon
|
||||||
|
for (size_t i = 1; i + 1 < indices.size(); i++) {
|
||||||
|
addTri(indices[0], indices[i], indices[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bounds
|
||||||
|
mesh.boundsMin = glm::vec3(FLT_MAX);
|
||||||
|
mesh.boundsMax = glm::vec3(-FLT_MAX);
|
||||||
|
for (const auto& p : mesh.positions) {
|
||||||
|
mesh.boundsMin.x = std::min(mesh.boundsMin.x, p.x);
|
||||||
|
mesh.boundsMin.y = std::min(mesh.boundsMin.y, p.y);
|
||||||
|
mesh.boundsMin.z = std::min(mesh.boundsMin.z, p.z);
|
||||||
|
mesh.boundsMax.x = std::max(mesh.boundsMax.x, p.x);
|
||||||
|
mesh.boundsMax.y = std::max(mesh.boundsMax.y, p.y);
|
||||||
|
mesh.boundsMax.z = std::max(mesh.boundsMax.z, p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeNormals();
|
||||||
|
dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
22
src/MeshBuilder.h
Normal file
22
src/MeshBuilder.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
#include "ModelLoader.h"
|
||||||
|
|
||||||
|
// Lightweight mesh editing state used by the MeshBuilder panel.
|
||||||
|
class MeshBuilder {
|
||||||
|
public:
|
||||||
|
bool hasMesh = false;
|
||||||
|
RawMeshAsset mesh;
|
||||||
|
std::string loadedPath;
|
||||||
|
bool dirty = false;
|
||||||
|
int selectedVertex = -1;
|
||||||
|
|
||||||
|
bool load(const std::string& path, std::string& error);
|
||||||
|
bool save(const std::string& path, std::string& error);
|
||||||
|
void clear();
|
||||||
|
void recomputeNormals();
|
||||||
|
|
||||||
|
// Add a new face defined by vertex indices (3 = triangle, 4 = quad fan).
|
||||||
|
bool addFace(const std::vector<uint32_t>& indices, std::string& error);
|
||||||
|
};
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
#include "ModelLoader.h"
|
#include "ModelLoader.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
ModelLoader& ModelLoader::getInstance() {
|
ModelLoader& ModelLoader::getInstance() {
|
||||||
static ModelLoader instance;
|
static ModelLoader instance;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out);
|
||||||
|
|
||||||
ModelLoader& getModelLoader() {
|
ModelLoader& getModelLoader() {
|
||||||
return ModelLoader::getInstance();
|
return ModelLoader::getInstance();
|
||||||
}
|
}
|
||||||
@@ -38,6 +43,7 @@ std::vector<ModelFormat> ModelLoader::getSupportedFormats() {
|
|||||||
{".bvh", "Biovision BVH", true},
|
{".bvh", "Biovision BVH", true},
|
||||||
{".csm", "CharacterStudio Motion", true},
|
{".csm", "CharacterStudio Motion", true},
|
||||||
{".irrmesh", "Irrlicht Mesh", false},
|
{".irrmesh", "Irrlicht Mesh", false},
|
||||||
|
{".rmesh", "Modularity Raw Mesh", false},
|
||||||
{".irr", "Irrlicht Scene", false},
|
{".irr", "Irrlicht Scene", false},
|
||||||
{".mdl", "Quake MDL", true},
|
{".mdl", "Quake MDL", true},
|
||||||
{".md2", "Quake II MD2", true},
|
{".md2", "Quake II MD2", true},
|
||||||
@@ -77,6 +83,8 @@ bool ModelLoader::isSupported(const std::string& filepath) const {
|
|||||||
|
|
||||||
ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
||||||
ModelLoadResult result;
|
ModelLoadResult result;
|
||||||
|
std::string extLower = fs::path(filepath).extension().string();
|
||||||
|
std::transform(extLower.begin(), extLower.end(), extLower.begin(), ::tolower);
|
||||||
|
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
for (size_t i = 0; i < loadedMeshes.size(); i++) {
|
for (size_t i = 0; i < loadedMeshes.size(); i++) {
|
||||||
@@ -98,6 +106,87 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle native raw mesh import without Assimp
|
||||||
|
if (extLower == ".rmesh") {
|
||||||
|
RawMeshAsset raw;
|
||||||
|
std::string error;
|
||||||
|
if (!loadRawMesh(filepath, raw, error)) {
|
||||||
|
result.errorMessage = error;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build interleaved triangle list for GPU upload
|
||||||
|
std::vector<float> vertices;
|
||||||
|
vertices.reserve(raw.faces.size() * 3 * 8);
|
||||||
|
std::vector<glm::vec3> triPositions;
|
||||||
|
triPositions.reserve(raw.faces.size() * 3);
|
||||||
|
|
||||||
|
auto getPos = [&](uint32_t idx) -> const glm::vec3& { return raw.positions[idx]; };
|
||||||
|
auto getNorm = [&](uint32_t idx) -> glm::vec3 {
|
||||||
|
if (idx < raw.normals.size()) return raw.normals[idx];
|
||||||
|
return glm::vec3(0.0f);
|
||||||
|
};
|
||||||
|
auto getUV = [&](uint32_t idx) -> glm::vec2 {
|
||||||
|
if (idx < raw.uvs.size()) return raw.uvs[idx];
|
||||||
|
return glm::vec2(0.0f);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& face : raw.faces) {
|
||||||
|
const uint32_t idx[3] = { face.x, face.y, face.z };
|
||||||
|
glm::vec3 faceNormal(0.0f);
|
||||||
|
if (!raw.hasNormals) {
|
||||||
|
const glm::vec3& a = getPos(idx[0]);
|
||||||
|
const glm::vec3& b = getPos(idx[1]);
|
||||||
|
const glm::vec3& c = getPos(idx[2]);
|
||||||
|
faceNormal = glm::normalize(glm::cross(b - a, c - a));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
glm::vec3 pos = getPos(idx[i]);
|
||||||
|
glm::vec3 n = raw.hasNormals ? getNorm(idx[i]) : faceNormal;
|
||||||
|
glm::vec2 uv = raw.hasUVs ? getUV(idx[i]) : glm::vec2(0.0f);
|
||||||
|
|
||||||
|
triPositions.push_back(pos);
|
||||||
|
vertices.push_back(pos.x);
|
||||||
|
vertices.push_back(pos.y);
|
||||||
|
vertices.push_back(pos.z);
|
||||||
|
vertices.push_back(n.x);
|
||||||
|
vertices.push_back(n.y);
|
||||||
|
vertices.push_back(n.z);
|
||||||
|
vertices.push_back(uv.x);
|
||||||
|
vertices.push_back(uv.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vertices.empty()) {
|
||||||
|
result.errorMessage = "No triangles found in raw mesh";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
OBJLoader::LoadedMesh loaded;
|
||||||
|
loaded.path = filepath;
|
||||||
|
loaded.name = fs::path(filepath).stem().string();
|
||||||
|
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
|
||||||
|
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
|
||||||
|
loaded.faceCount = static_cast<int>(raw.faces.size());
|
||||||
|
loaded.hasNormals = raw.hasNormals;
|
||||||
|
loaded.hasTexCoords = raw.hasUVs;
|
||||||
|
loaded.boundsMin = raw.boundsMin;
|
||||||
|
loaded.boundsMax = raw.boundsMax;
|
||||||
|
loaded.triangleVertices = std::move(triPositions);
|
||||||
|
|
||||||
|
loadedMeshes.push_back(std::move(loaded));
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
result.meshIndex = static_cast<int>(loadedMeshes.size() - 1);
|
||||||
|
result.vertexCount = static_cast<int>(raw.positions.size());
|
||||||
|
result.faceCount = static_cast<int>(raw.faces.size());
|
||||||
|
result.meshCount = 1;
|
||||||
|
result.hasNormals = raw.hasNormals;
|
||||||
|
result.hasTexCoords = raw.hasUVs;
|
||||||
|
result.meshNames.push_back(loadedMeshes.back().name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Configure import flags
|
// Configure import flags
|
||||||
unsigned int importFlags =
|
unsigned int importFlags =
|
||||||
aiProcess_Triangulate | // Convert all faces to triangles
|
aiProcess_Triangulate | // Convert all faces to triangles
|
||||||
@@ -173,6 +262,269 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ModelLoader::exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg) {
|
||||||
|
fs::path inPath(inputFile);
|
||||||
|
if (!fs::exists(inPath)) {
|
||||||
|
errorMsg = "File not found: " + inputFile;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isSupported(inputFile)) {
|
||||||
|
errorMsg = "Unsupported file format for raw export";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assimp::Importer localImporter;
|
||||||
|
unsigned int importFlags =
|
||||||
|
aiProcess_Triangulate |
|
||||||
|
aiProcess_JoinIdenticalVertices |
|
||||||
|
aiProcess_GenSmoothNormals |
|
||||||
|
aiProcess_FlipUVs;
|
||||||
|
|
||||||
|
const aiScene* scene = localImporter.ReadFile(inputFile, importFlags);
|
||||||
|
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
|
||||||
|
errorMsg = "Assimp error: " + std::string(localImporter.GetErrorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RawMeshAsset raw;
|
||||||
|
collectRawMeshData(scene->mRootNode, scene, aiMatrix4x4(), raw);
|
||||||
|
|
||||||
|
if (raw.positions.empty() || raw.faces.empty()) {
|
||||||
|
errorMsg = "No geometry found to export";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path outPath(outputFile.empty() ? inPath : fs::path(outputFile));
|
||||||
|
if (outPath.extension().empty()) {
|
||||||
|
outPath.replace_extension(".rmesh");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveRawMesh(raw, outPath.string(), errorMsg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "[ModelLoader] Exported raw mesh to " << outPath << " ("
|
||||||
|
<< raw.positions.size() << " verts, " << raw.faces.size() << " faces)" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModelLoader::loadRawMesh(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg) {
|
||||||
|
out = RawMeshAsset();
|
||||||
|
|
||||||
|
std::ifstream in(filepath, std::ios::binary);
|
||||||
|
if (!in) {
|
||||||
|
errorMsg = "Unable to open raw mesh: " + filepath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
char magic[6];
|
||||||
|
uint32_t version;
|
||||||
|
uint32_t vertexCount;
|
||||||
|
uint32_t faceCount;
|
||||||
|
} header{};
|
||||||
|
|
||||||
|
in.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||||
|
if (std::strncmp(header.magic, "RMESH", 5) != 0) {
|
||||||
|
errorMsg = "Invalid raw mesh header";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (header.version != 1) {
|
||||||
|
errorMsg = "Unsupported raw mesh version";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.vertexCount == 0 || header.faceCount == 0) {
|
||||||
|
errorMsg = "Raw mesh contains no geometry";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
in.read(reinterpret_cast<char*>(&out.boundsMin.x), sizeof(float) * 3);
|
||||||
|
in.read(reinterpret_cast<char*>(&out.boundsMax.x), sizeof(float) * 3);
|
||||||
|
|
||||||
|
out.positions.resize(header.vertexCount);
|
||||||
|
out.normals.resize(header.vertexCount);
|
||||||
|
out.uvs.resize(header.vertexCount);
|
||||||
|
out.faces.resize(header.faceCount);
|
||||||
|
|
||||||
|
in.read(reinterpret_cast<char*>(out.positions.data()), sizeof(glm::vec3) * out.positions.size());
|
||||||
|
in.read(reinterpret_cast<char*>(out.normals.data()), sizeof(glm::vec3) * out.normals.size());
|
||||||
|
in.read(reinterpret_cast<char*>(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size());
|
||||||
|
in.read(reinterpret_cast<char*>(out.faces.data()), sizeof(glm::u32vec3) * out.faces.size());
|
||||||
|
|
||||||
|
if (!in.good()) {
|
||||||
|
errorMsg = "Unexpected EOF while reading raw mesh";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto validIndex = [&](uint32_t idx) { return idx < out.positions.size(); };
|
||||||
|
for (const auto& face : out.faces) {
|
||||||
|
if (!validIndex(face.x) || !validIndex(face.y) || !validIndex(face.z)) {
|
||||||
|
errorMsg = "Raw mesh contains invalid face indices";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.hasNormals = false;
|
||||||
|
for (const auto& n : out.normals) {
|
||||||
|
if (glm::length(n) > 1e-4f) { out.hasNormals = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
out.hasUVs = false;
|
||||||
|
for (const auto& uv : out.uvs) {
|
||||||
|
if (std::abs(uv.x) > 1e-6f || std::abs(uv.y) > 1e-6f) { out.hasUVs = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!out.hasNormals) {
|
||||||
|
out.normals.assign(out.positions.size(), glm::vec3(0.0f));
|
||||||
|
std::vector<glm::vec3> accum(out.positions.size(), glm::vec3(0.0f));
|
||||||
|
|
||||||
|
for (const auto& face : out.faces) {
|
||||||
|
const glm::vec3& a = out.positions[face.x];
|
||||||
|
const glm::vec3& b = out.positions[face.y];
|
||||||
|
const glm::vec3& c = out.positions[face.z];
|
||||||
|
glm::vec3 n = glm::normalize(glm::cross(b - a, c - a));
|
||||||
|
accum[face.x] += n;
|
||||||
|
accum[face.y] += n;
|
||||||
|
accum[face.z] += n;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < accum.size(); i++) {
|
||||||
|
if (glm::length(accum[i]) > 1e-6f) {
|
||||||
|
out.normals[i] = glm::normalize(accum[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.hasNormals = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute bounds if file stored invalid values
|
||||||
|
if (!std::isfinite(out.boundsMin.x) || !std::isfinite(out.boundsMax.x)) {
|
||||||
|
out.boundsMin = glm::vec3(FLT_MAX);
|
||||||
|
out.boundsMax = glm::vec3(-FLT_MAX);
|
||||||
|
for (const auto& p : out.positions) {
|
||||||
|
out.boundsMin.x = std::min(out.boundsMin.x, p.x);
|
||||||
|
out.boundsMin.y = std::min(out.boundsMin.y, p.y);
|
||||||
|
out.boundsMin.z = std::min(out.boundsMin.z, p.z);
|
||||||
|
out.boundsMax.x = std::max(out.boundsMax.x, p.x);
|
||||||
|
out.boundsMax.y = std::max(out.boundsMax.y, p.y);
|
||||||
|
out.boundsMax.z = std::max(out.boundsMax.z, p.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& filepath, std::string& errorMsg) {
|
||||||
|
if (asset.positions.empty() || asset.faces.empty()) {
|
||||||
|
errorMsg = "Raw mesh is empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path outPath(filepath);
|
||||||
|
if (outPath.extension().empty()) {
|
||||||
|
outPath.replace_extension(".rmesh");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
char magic[6] = {'R','M','E','S','H','\0'};
|
||||||
|
uint32_t version = 1;
|
||||||
|
uint32_t vertexCount = 0;
|
||||||
|
uint32_t faceCount = 0;
|
||||||
|
} header;
|
||||||
|
|
||||||
|
header.vertexCount = static_cast<uint32_t>(asset.positions.size());
|
||||||
|
header.faceCount = static_cast<uint32_t>(asset.faces.size());
|
||||||
|
|
||||||
|
std::ofstream out(outPath, std::ios::binary);
|
||||||
|
if (!out) {
|
||||||
|
errorMsg = "Unable to open file for writing: " + outPath.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||||
|
out.write(reinterpret_cast<const char*>(&asset.boundsMin.x), sizeof(float) * 3);
|
||||||
|
out.write(reinterpret_cast<const char*>(&asset.boundsMax.x), sizeof(float) * 3);
|
||||||
|
out.write(reinterpret_cast<const char*>(asset.positions.data()), sizeof(glm::vec3) * asset.positions.size());
|
||||||
|
out.write(reinterpret_cast<const char*>(asset.normals.data()), sizeof(glm::vec3) * asset.normals.size());
|
||||||
|
out.write(reinterpret_cast<const char*>(asset.uvs.data()), sizeof(glm::vec2) * asset.uvs.size());
|
||||||
|
out.write(reinterpret_cast<const char*>(asset.faces.data()), sizeof(glm::u32vec3) * asset.faces.size());
|
||||||
|
|
||||||
|
if (!out.good()) {
|
||||||
|
errorMsg = "Failed while writing raw mesh file";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModelLoader::updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg) {
|
||||||
|
if (meshIndex < 0 || meshIndex >= static_cast<int>(loadedMeshes.size())) {
|
||||||
|
errorMsg = "Invalid mesh index";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (asset.positions.empty() || asset.faces.empty()) {
|
||||||
|
errorMsg = "Raw mesh is empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> vertices;
|
||||||
|
vertices.reserve(asset.faces.size() * 3 * 8);
|
||||||
|
std::vector<glm::vec3> triPositions;
|
||||||
|
triPositions.reserve(asset.faces.size() * 3);
|
||||||
|
|
||||||
|
auto getPos = [&](uint32_t idx) -> const glm::vec3& { return asset.positions[idx]; };
|
||||||
|
auto getNorm = [&](uint32_t idx) -> glm::vec3 {
|
||||||
|
if (idx < asset.normals.size()) return asset.normals[idx];
|
||||||
|
return glm::vec3(0.0f);
|
||||||
|
};
|
||||||
|
auto getUV = [&](uint32_t idx) -> glm::vec2 {
|
||||||
|
if (idx < asset.uvs.size()) return asset.uvs[idx];
|
||||||
|
return glm::vec2(0.0f);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& face : asset.faces) {
|
||||||
|
const uint32_t idx[3] = { face.x, face.y, face.z };
|
||||||
|
glm::vec3 faceNormal(0.0f);
|
||||||
|
if (!asset.hasNormals) {
|
||||||
|
const glm::vec3& a = getPos(idx[0]);
|
||||||
|
const glm::vec3& b = getPos(idx[1]);
|
||||||
|
const glm::vec3& c = getPos(idx[2]);
|
||||||
|
faceNormal = glm::normalize(glm::cross(b - a, c - a));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
glm::vec3 pos = getPos(idx[i]);
|
||||||
|
glm::vec3 n = asset.hasNormals ? getNorm(idx[i]) : faceNormal;
|
||||||
|
glm::vec2 uv = asset.hasUVs ? getUV(idx[i]) : glm::vec2(0.0f);
|
||||||
|
|
||||||
|
triPositions.push_back(pos);
|
||||||
|
vertices.push_back(pos.x);
|
||||||
|
vertices.push_back(pos.y);
|
||||||
|
vertices.push_back(pos.z);
|
||||||
|
vertices.push_back(n.x);
|
||||||
|
vertices.push_back(n.y);
|
||||||
|
vertices.push_back(n.z);
|
||||||
|
vertices.push_back(uv.x);
|
||||||
|
vertices.push_back(uv.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vertices.empty()) {
|
||||||
|
errorMsg = "No vertices generated for GPU upload";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OBJLoader::LoadedMesh& loaded = loadedMeshes[meshIndex];
|
||||||
|
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
|
||||||
|
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
|
||||||
|
loaded.faceCount = static_cast<int>(asset.faces.size());
|
||||||
|
loaded.hasNormals = asset.hasNormals;
|
||||||
|
loaded.hasTexCoords = asset.hasUVs;
|
||||||
|
loaded.boundsMin = asset.boundsMin;
|
||||||
|
loaded.boundsMax = asset.boundsMax;
|
||||||
|
loaded.triangleVertices = std::move(triPositions);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
|
static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
|
||||||
return glm::mat4(
|
return glm::mat4(
|
||||||
m.a1, m.b1, m.c1, m.d1,
|
m.a1, m.b1, m.c1, m.d1,
|
||||||
@@ -182,6 +534,61 @@ static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out) {
|
||||||
|
aiMatrix4x4 current = parentTransform * node->mTransformation;
|
||||||
|
glm::mat4 gTransform = aiToGlm(current);
|
||||||
|
glm::mat3 normalMat = glm::transpose(glm::inverse(glm::mat3(gTransform)));
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
|
||||||
|
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
|
||||||
|
size_t baseIndex = out.positions.size();
|
||||||
|
|
||||||
|
// Vertices
|
||||||
|
for (unsigned int v = 0; v < mesh->mNumVertices; v++) {
|
||||||
|
glm::vec3 pos(mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z);
|
||||||
|
glm::vec4 transformed = gTransform * glm::vec4(pos, 1.0f);
|
||||||
|
glm::vec3 finalPos = glm::vec3(transformed) / (transformed.w == 0.0f ? 1.0f : transformed.w);
|
||||||
|
out.positions.push_back(finalPos);
|
||||||
|
|
||||||
|
out.boundsMin.x = std::min(out.boundsMin.x, finalPos.x);
|
||||||
|
out.boundsMin.y = std::min(out.boundsMin.y, finalPos.y);
|
||||||
|
out.boundsMin.z = std::min(out.boundsMin.z, finalPos.z);
|
||||||
|
out.boundsMax.x = std::max(out.boundsMax.x, finalPos.x);
|
||||||
|
out.boundsMax.y = std::max(out.boundsMax.y, finalPos.y);
|
||||||
|
out.boundsMax.z = std::max(out.boundsMax.z, finalPos.z);
|
||||||
|
|
||||||
|
glm::vec3 normal(0.0f);
|
||||||
|
if (mesh->mNormals) {
|
||||||
|
normal = glm::normalize(normalMat * glm::vec3(mesh->mNormals[v].x, mesh->mNormals[v].y, mesh->mNormals[v].z));
|
||||||
|
}
|
||||||
|
out.normals.push_back(normal);
|
||||||
|
|
||||||
|
glm::vec2 uv(0.0f);
|
||||||
|
if (mesh->mTextureCoords[0]) {
|
||||||
|
uv.x = mesh->mTextureCoords[0][v].x;
|
||||||
|
uv.y = mesh->mTextureCoords[0][v].y;
|
||||||
|
}
|
||||||
|
out.uvs.push_back(uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faces (triangles)
|
||||||
|
for (unsigned int f = 0; f < mesh->mNumFaces; f++) {
|
||||||
|
const aiFace& face = mesh->mFaces[f];
|
||||||
|
if (face.mNumIndices != 3) continue;
|
||||||
|
glm::u32vec3 tri(
|
||||||
|
static_cast<uint32_t>(baseIndex + face.mIndices[0]),
|
||||||
|
static_cast<uint32_t>(baseIndex + face.mIndices[1]),
|
||||||
|
static_cast<uint32_t>(baseIndex + face.mIndices[2])
|
||||||
|
);
|
||||||
|
out.faces.push_back(tri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned int c = 0; c < node->mNumChildren; c++) {
|
||||||
|
collectRawMeshData(node->mChildren[c], scene, current, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, std::vector<float>& vertices, std::vector<glm::vec3>& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax) {
|
void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, std::vector<float>& vertices, std::vector<glm::vec3>& triPositions, glm::vec3& boundsMin, glm::vec3& boundsMax) {
|
||||||
aiMatrix4x4 currentTransform = parentTransform * node->mTransformation;
|
aiMatrix4x4 currentTransform = parentTransform * node->mTransformation;
|
||||||
// Process all meshes in this node
|
// Process all meshes in this node
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "Rendering.h"
|
#include "Rendering.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <assimp/Importer.hpp>
|
#include <assimp/Importer.hpp>
|
||||||
#include <assimp/scene.h>
|
#include <assimp/scene.h>
|
||||||
@@ -28,6 +29,18 @@ struct ModelLoadResult {
|
|||||||
std::vector<std::string> meshNames;
|
std::vector<std::string> meshNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Raw mesh asset for editable geometry (.rmesh)
|
||||||
|
struct RawMeshAsset {
|
||||||
|
std::vector<glm::vec3> positions;
|
||||||
|
std::vector<glm::vec3> normals;
|
||||||
|
std::vector<glm::vec2> uvs;
|
||||||
|
std::vector<glm::u32vec3> faces;
|
||||||
|
glm::vec3 boundsMin = glm::vec3(FLT_MAX);
|
||||||
|
glm::vec3 boundsMax = glm::vec3(-FLT_MAX);
|
||||||
|
bool hasNormals = false;
|
||||||
|
bool hasUVs = false;
|
||||||
|
};
|
||||||
|
|
||||||
class ModelLoader {
|
class ModelLoader {
|
||||||
public:
|
public:
|
||||||
// Singleton access
|
// Singleton access
|
||||||
@@ -48,6 +61,18 @@ public:
|
|||||||
// Check if file extension is supported
|
// Check if file extension is supported
|
||||||
bool isSupported(const std::string& filepath) const;
|
bool isSupported(const std::string& filepath) const;
|
||||||
|
|
||||||
|
// Export a model file into a raw editable mesh asset (.rmesh)
|
||||||
|
bool exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg);
|
||||||
|
|
||||||
|
// Load a raw mesh asset from disk
|
||||||
|
bool loadRawMesh(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg);
|
||||||
|
|
||||||
|
// Save a raw mesh asset to disk
|
||||||
|
bool saveRawMesh(const RawMeshAsset& asset, const std::string& filepath, std::string& errorMsg);
|
||||||
|
|
||||||
|
// Update an already-loaded raw mesh in GPU memory
|
||||||
|
bool updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg);
|
||||||
|
|
||||||
// Get list of supported formats
|
// Get list of supported formats
|
||||||
static std::vector<ModelFormat> getSupportedFormats();
|
static std::vector<ModelFormat> getSupportedFormats();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user