Added Meshbuilder + new RMesh type

This commit is contained in:
Anemunt
2025-12-10 16:40:44 -05:00
parent 7831bea4e2
commit cdb781262f
8 changed files with 1306 additions and 153 deletions

View File

@@ -97,7 +97,7 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
// Model files
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
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;
}

View File

@@ -87,6 +87,42 @@ SceneObject* Engine::getSelectedObject() {
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 cam;
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*/) {
SceneSnapshot snap;
snap.objects = sceneObjects;
snap.selectedId = selectedObjectId;
snap.selectedIds = selectedObjectIds;
snap.nextId = nextObjectId;
undoStack.push_back(std::move(snap));
@@ -135,7 +171,7 @@ void Engine::undo() {
SceneSnapshot current;
current.objects = sceneObjects;
current.selectedId = selectedObjectId;
current.selectedIds = selectedObjectIds;
current.nextId = nextObjectId;
SceneSnapshot snap = undoStack.back();
@@ -143,7 +179,8 @@ void Engine::undo() {
redoStack.push_back(std::move(current));
sceneObjects = std::move(snap.objects);
selectedObjectId = snap.selectedId;
selectedObjectIds = snap.selectedIds;
selectedObjectId = selectedObjectIds.empty() ? -1 : selectedObjectIds.back();
nextObjectId = snap.nextId;
projectManager.currentProject.hasUnsavedChanges = true;
}
@@ -153,7 +190,7 @@ void Engine::redo() {
SceneSnapshot current;
current.objects = sceneObjects;
current.selectedId = selectedObjectId;
current.selectedIds = selectedObjectIds;
current.nextId = nextObjectId;
SceneSnapshot snap = redoStack.back();
@@ -161,7 +198,8 @@ void Engine::redo() {
undoStack.push_back(std::move(current));
sceneObjects = std::move(snap.objects);
selectedObjectId = snap.selectedId;
selectedObjectIds = snap.selectedIds;
selectedObjectId = selectedObjectIds.empty() ? -1 : selectedObjectIds.back();
nextObjectId = snap.nextId;
projectManager.currentProject.hasUnsavedChanges = true;
}
@@ -301,6 +339,7 @@ void Engine::run() {
if (showHierarchy) renderHierarchyPanel();
if (showInspector) renderInspectorPanel();
if (showFileBrowser) renderFileBrowserPanel();
if (showMeshBuilder) renderMeshBuilderPanel();
if (showConsole) renderConsolePanel();
if (showEnvironmentWindow) renderEnvironmentWindow();
if (showCameraWindow) renderCameraWindow();
@@ -372,7 +411,7 @@ void Engine::importOBJToScene(const std::string& filepath, const std::string& ob
obj.meshId = meshId;
sceneObjects.push_back(obj);
selectedObjectId = id;
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
@@ -407,7 +446,7 @@ void Engine::importModelToScene(const std::string& filepath, const std::string&
obj.meshId = result.meshIndex;
sceneObjects.push_back(obj);
selectedObjectId = id;
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
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) {
if (obj.materialPath.empty()) return;
try {
@@ -648,7 +742,7 @@ void Engine::createNewProject(const char* name, const char* location) {
}
sceneObjects.clear();
selectedObjectId = -1;
clearSelection();
nextObjectId = 0;
addObject(ObjectType::Cube, "Cube");
@@ -671,7 +765,7 @@ void Engine::createNewProject(const char* name, const char* location) {
void Engine::loadRecentScenes() {
sceneObjects.clear();
selectedObjectId = -1;
clearSelection();
nextObjectId = 0;
undoStack.clear();
redoStack.clear();
@@ -722,7 +816,7 @@ void Engine::loadScene(const std::string& sceneName) {
projectManager.currentProject.currentSceneName = sceneName;
projectManager.currentProject.hasUnsavedChanges = false;
projectManager.currentProject.saveProjectFile();
selectedObjectId = -1;
clearSelection();
bool hasDirLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) {
return o.type == ObjectType::DirectionalLight;
});
@@ -744,7 +838,7 @@ void Engine::createNewScene(const std::string& sceneName) {
}
sceneObjects.clear();
selectedObjectId = -1;
clearSelection();
nextObjectId = 0;
undoStack.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.fov = 60.0f;
}
selectedObjectId = id;
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
@@ -820,7 +914,7 @@ void Engine::duplicateSelected() {
newObj.postFx = it->postFx;
sceneObjects.push_back(newObj);
selectedObjectId = id;
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
@@ -836,7 +930,7 @@ void Engine::deleteSelected() {
if (it != sceneObjects.end()) {
logToConsole("Deleted object");
sceneObjects.erase(it, sceneObjects.end());
selectedObjectId = -1;
clearSelection();
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}

View File

@@ -6,6 +6,7 @@
#include "Rendering.h"
#include "ProjectManager.h"
#include "EditorUI.h"
#include "MeshBuilder.h"
#include "../include/Window/Window.h"
void window_size_callback(GLFWwindow* window, int width, int height);
@@ -35,14 +36,15 @@ private:
bool inspectedMaterialValid = false;
struct SceneSnapshot {
std::vector<SceneObject> objects;
int selectedId = -1;
std::vector<int> selectedIds;
int nextId = 0;
};
std::vector<SceneSnapshot> undoStack;
std::vector<SceneSnapshot> redoStack;
std::vector<SceneObject> sceneObjects;
int selectedObjectId = -1;
int selectedObjectId = -1; // primary selection (last)
std::vector<int> selectedObjectIds; // multi-select
int nextObjectId = 0;
// Gizmo state
@@ -59,6 +61,7 @@ private:
bool showFileBrowser = true;
bool showConsole = true;
bool showProjectBrowser = true; // Now merged into file browser
bool showMeshBuilder = false;
bool firstFrame = true;
std::vector<std::string> consoleLog;
int draggedObjectId = -1;
@@ -86,13 +89,31 @@ private:
bool isPaused = false;
bool showViewOutput = true;
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
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);
void importOBJToScene(const std::string& filepath, const std::string& objectName);
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 OpenProjectPath(const std::string& path);
@@ -106,6 +127,7 @@ private:
void renderHierarchyPanel();
void renderObjectNode(SceneObject& obj, const std::string& filter);
void renderFileBrowserPanel();
void renderMeshBuilderPanel();
void renderInspectorPanel();
void renderConsolePanel();
void renderViewport();

View File

@@ -2,8 +2,11 @@
#include "ModelLoader.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <cfloat>
#include <cmath>
#include <sstream>
#include <unordered_set>
#include <optional>
#ifdef _WIN32
@@ -167,8 +170,6 @@ namespace FileIcons {
// Draw an audio icon (speaker/waveform)
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.15f;
// Speaker body
float spkW = size * 0.25f;
float spkH = size * 0.3f;
@@ -298,7 +299,6 @@ namespace FileIcons {
void Engine::renderFileBrowserPanel() {
ImGui::Begin("Project", &showFileBrowser);
ImGuiStyle& style = ImGui::GetStyle();
ImVec4 accent = style.Colors[ImGuiCol_CheckMark];
ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
toolbarBg.x = std::min(toolbarBg.x + 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(), "");
}
}
if (ImGui::MenuItem("Convert to Raw Mesh")) {
convertModelToRawMesh(entry.path().string());
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (ImGui::MenuItem("Apply to Selected")) {
@@ -689,6 +692,9 @@ void Engine::renderFileBrowserPanel() {
}
if (fileBrowser.isModelFile(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")) {
std::string defaultName = entry.path().stem().string();
if (isObj) {
@@ -708,6 +714,9 @@ void Engine::renderFileBrowserPanel() {
importModelToScene(entry.path().string(), "");
}
}
if (!isRaw && ImGui::MenuItem("Convert to Raw Mesh")) {
convertModelToRawMesh(entry.path().string());
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (ImGui::MenuItem("Apply to Selected")) {
@@ -761,6 +770,146 @@ void Engine::renderFileBrowserPanel() {
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() {
ImGuiIO& io = ImGui::GetIO();
@@ -1109,7 +1258,7 @@ void Engine::renderMainMenuBar() {
}
projectManager.currentProject = Project();
sceneObjects.clear();
selectedObjectId = -1;
clearSelection();
showLauncher = true;
}
ImGui::Separator();
@@ -1131,6 +1280,7 @@ void Engine::renderMainMenuBar() {
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
ImGui::MenuItem("Console", nullptr, &showConsole);
ImGui::MenuItem("Project Manager", nullptr, &showProjectBrowser);
ImGui::MenuItem("Mesh Builder", nullptr, &showMeshBuilder);
ImGui::MenuItem("Environment", nullptr, &showEnvironmentWindow);
ImGui::MenuItem("Camera", nullptr, &showCameraWindow);
ImGui::MenuItem("View Output", nullptr, &showViewOutput);
@@ -1219,7 +1369,6 @@ void Engine::renderHierarchyPanel() {
headerBg.x = std::min(headerBg.x + 0.02f, 1.0f);
headerBg.y = std::min(headerBg.y + 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];
listBg.x = std::min(listBg.x + 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 isSelected = (selectedObjectId == obj.id);
bool isSelected = std::find(selectedObjectIds.begin(), selectedObjectIds.end(), obj.id) != selectedObjectIds.end();
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
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());
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)) {
@@ -1356,11 +1506,11 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Duplicate")) {
selectedObjectId = obj.id;
setPrimarySelection(obj.id);
duplicateSelected();
}
if (ImGui::MenuItem("Delete")) {
selectedObjectId = obj.id;
setPrimarySelection(obj.id);
deleteSelected();
}
ImGui::Separator();
@@ -1591,7 +1741,7 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor();
};
if (selectedObjectId == -1) {
if (selectedObjectIds.empty()) {
if (browserHasMaterial) {
renderMaterialAssetPanel("Material Asset", true);
} else {
@@ -1601,8 +1751,9 @@ void Engine::renderInspectorPanel() {
return;
}
int primaryId = selectedObjectId;
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()) {
ImGui::TextDisabled("Object not found");
@@ -1612,6 +1763,11 @@ void Engine::renderInspectorPanel() {
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));
if (ImGui::CollapsingHeader("Object Info", ImGuiTreeNodeFlags_DefaultOpen)) {
@@ -2474,7 +2630,6 @@ void Engine::renderViewport() {
ImGuizmo::Enable(true);
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
ImGuizmo::SetRect(
imageMin.x,
imageMin.y,
@@ -2482,141 +2637,429 @@ void Engine::renderViewport() {
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);
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.y), glm::vec3(0, 1, 0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.z), glm::vec3(0, 0, 1));
modelMatrix = glm::scale(modelMatrix, selectedObj->scale);
glm::mat4 originalModel = modelMatrix;
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}
if (meshModeActive && !meshEditAsset.positions.empty()) {
// Build helper edge list (dedup) for edge/face modes
std::vector<glm::u32vec2> edges;
edges.reserve(meshEditAsset.faces.size() * 3);
std::unordered_set<uint64_t> edgeSet;
auto edgeKey = [](uint32_t a, uint32_t b) {
return (static_cast<uint64_t>(std::min(a,b)) << 32) | static_cast<uint64_t>(std::max(a,b));
};
for (auto& e : edges) {
dl->AddLine(projected[e[0]], projected[e[1]], col, 2.0f);
for (size_t fi = 0; fi < meshEditAsset.faces.size(); ++fi) {
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()) {
if (!gizmoHistoryCaptured) {
recordState("gizmo");
gizmoHistoryCaptured = true;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 vertCol = ImGui::GetColorU32(ImVec4(0.35f, 0.75f, 1.0f, 0.9f));
ImU32 selCol = ImGui::GetColorU32(ImVec4(1.0f, 0.6f, 0.2f, 1.0f));
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]);
selectedObj->rotation = glm::vec3(r[0], r[1], r[2]);
selectedObj->scale = glm::vec3(s[0], s[1], s[2]);
// Compute affected vertices from selection
std::vector<int> affectedVerts = meshEditSelectedVertices;
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 {
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);
gizmoButton("Scale", ImGuizmo::SCALE, "Scale");
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");
ImGui::SameLine(0.0f, toolbarSpacing);
gizmoButton("Uni", ImGuizmo::UNIVERSAL, "Universal");
@@ -2952,9 +3429,10 @@ void Engine::renderViewport() {
viewportController.setFocused(true);
if (hitId != -1) {
selectedObjectId = hitId;
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
setPrimarySelection(hitId, additive);
} else {
selectedObjectId = -1;
clearSelection();
}
}

105
src/MeshBuilder.cpp Normal file
View 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
View 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);
};

View File

@@ -1,12 +1,17 @@
#include "ModelLoader.h"
#include <algorithm>
#include <iostream>
#include <fstream>
#include <cstdint>
#include <cstring>
ModelLoader& ModelLoader::getInstance() {
static ModelLoader instance;
return instance;
}
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out);
ModelLoader& getModelLoader() {
return ModelLoader::getInstance();
}
@@ -38,6 +43,7 @@ std::vector<ModelFormat> ModelLoader::getSupportedFormats() {
{".bvh", "Biovision BVH", true},
{".csm", "CharacterStudio Motion", true},
{".irrmesh", "Irrlicht Mesh", false},
{".rmesh", "Modularity Raw Mesh", false},
{".irr", "Irrlicht Scene", false},
{".mdl", "Quake MDL", 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 result;
std::string extLower = fs::path(filepath).extension().string();
std::transform(extLower.begin(), extLower.end(), extLower.begin(), ::tolower);
// Check if already loaded
for (size_t i = 0; i < loadedMeshes.size(); i++) {
@@ -97,6 +105,87 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
result.errorMessage = "Unsupported file format: " + fs::path(filepath).extension().string();
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
unsigned int importFlags =
@@ -173,6 +262,269 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
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) {
return glm::mat4(
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) {
aiMatrix4x4 currentTransform = parentTransform * node->mTransformation;
// Process all meshes in this node

View File

@@ -2,6 +2,7 @@
#include "Common.h"
#include "Rendering.h"
#include <cstdint>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
@@ -28,6 +29,18 @@ struct ModelLoadResult {
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 {
public:
// Singleton access
@@ -48,6 +61,18 @@ public:
// Check if file extension is supported
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
static std::vector<ModelFormat> getSupportedFormats();