Files
Modularity/src/EditorWindows/ViewportWindows.cpp
2025-12-19 07:03:03 -05:00

1761 lines
82 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Engine.h"
#include "ModelLoader.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdlib>
#include <cfloat>
#include <cmath>
#include <functional>
#include <sstream>
#include <unordered_set>
#include <optional>
#include <future>
#include <chrono>
#include <future>
#ifdef _WIN32
#include <shlobj.h>
#endif
namespace GizmoToolbar {
enum class Icon {
Translate,
Rotate,
Scale,
Bounds,
Universal
};
static ImVec4 ScaleColor(const ImVec4& c, float s) {
return ImVec4(
std::clamp(c.x * s, 0.0f, 1.0f),
std::clamp(c.y * s, 0.0f, 1.0f),
std::clamp(c.z * s, 0.0f, 1.0f),
c.w
);
}
static bool TextButton(const char* label, bool active, const ImVec2& size, ImU32 base, ImU32 hover, ImU32 activeCol, ImU32 accent, ImU32 textColor) {
ImGui::PushStyleColor(ImGuiCol_Button, active ? accent : base);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, active ? accent : hover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, active ? accent : activeCol);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(textColor));
bool pressed = ImGui::Button(label, size);
ImGui::PopStyleColor(4);
return pressed;
}
static void DrawTranslateIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
float len = (max.x - min.x) * 0.3f;
float head = len * 0.5f;
drawList->AddLine(ImVec2(center.x - len, center.y), ImVec2(center.x + len, center.y), lineColor, 2.4f);
drawList->AddLine(ImVec2(center.x, center.y - len), ImVec2(center.x, center.y + len), lineColor, 2.4f);
drawList->AddTriangleFilled(ImVec2(center.x + len, center.y),
ImVec2(center.x + len - head, center.y - head * 0.6f),
ImVec2(center.x + len - head, center.y + head * 0.6f),
accentColor);
drawList->AddTriangleFilled(ImVec2(center.x - len, center.y),
ImVec2(center.x - len + head, center.y - head * 0.6f),
ImVec2(center.x - len + head, center.y + head * 0.6f),
accentColor);
drawList->AddTriangleFilled(ImVec2(center.x, center.y - len),
ImVec2(center.x - head * 0.6f, center.y - len + head),
ImVec2(center.x + head * 0.6f, center.y - len + head),
accentColor);
drawList->AddTriangleFilled(ImVec2(center.x, center.y + len),
ImVec2(center.x - head * 0.6f, center.y + len - head),
ImVec2(center.x + head * 0.6f, center.y + len - head),
accentColor);
drawList->AddCircleFilled(center, head * 0.35f, lineColor, 16);
}
static void DrawRotateIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
float radius = (max.x - min.x) * 0.28f;
float start = -IM_PI * 0.25f;
float end = IM_PI * 1.1f;
drawList->PathArcTo(center, radius, start, end, 32);
drawList->PathStroke(lineColor, false, 2.4f);
ImVec2 arrow = ImVec2(center.x + cosf(end) * radius, center.y + sinf(end) * radius);
ImVec2 dir = ImVec2(cosf(end), sinf(end));
ImVec2 ortho = ImVec2(-dir.y, dir.x);
float head = radius * 0.5f;
ImVec2 a = ImVec2(arrow.x - dir.x * head + ortho.x * head * 0.55f, arrow.y - dir.y * head + ortho.y * head * 0.55f);
ImVec2 b = ImVec2(arrow.x - dir.x * head - ortho.x * head * 0.55f, arrow.y - dir.y * head - ortho.y * head * 0.55f);
drawList->AddTriangleFilled(arrow, a, b, accentColor);
}
static void DrawScaleIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
ImVec2 pad = ImVec2((max.x - min.x) * 0.2f, (max.y - min.y) * 0.2f);
ImVec2 rMin = ImVec2(min.x + pad.x, min.y + pad.y);
ImVec2 rMax = ImVec2(max.x - pad.x, max.y - pad.y);
drawList->AddRect(rMin, rMax, lineColor, 3.0f, 0, 2.1f);
ImVec2 center = ImVec2((rMin.x + rMax.x) * 0.5f, (rMin.y + rMax.y) * 0.5f);
ImVec2 offsets[] = {
ImVec2(-1, -1),
ImVec2(1, -1),
ImVec2(1, 1),
ImVec2(-1, 1)
};
float arrowLen = pad.x * 0.65f;
float head = arrowLen * 0.5f;
for (const ImVec2& off : offsets) {
ImVec2 dir = ImVec2(off.x * 0.7f, off.y * 0.7f);
ImVec2 tip = ImVec2(center.x + dir.x * arrowLen, center.y + dir.y * arrowLen);
ImVec2 base = ImVec2(center.x + dir.x * (arrowLen * 0.45f), center.y + dir.y * (arrowLen * 0.45f));
ImVec2 ortho = ImVec2(-dir.y, dir.x);
ImVec2 a = ImVec2(base.x + ortho.x * head * 0.35f, base.y + ortho.y * head * 0.35f);
ImVec2 b = ImVec2(base.x - ortho.x * head * 0.35f, base.y - ortho.y * head * 0.35f);
drawList->AddTriangleFilled(tip, a, b, accentColor);
drawList->AddLine(center, tip, lineColor, 2.0f);
}
}
static void DrawBoundsIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
ImVec2 pad = ImVec2((max.x - min.x) * 0.2f, (max.y - min.y) * 0.22f);
ImVec2 rMin = ImVec2(min.x + pad.x, min.y + pad.y);
ImVec2 rMax = ImVec2(max.x - pad.x, max.y - pad.y);
drawList->AddRect(rMin, rMax, lineColor, 4.0f, 0, 2.0f);
float handle = pad.x * 0.6f;
ImVec2 handles[] = {
rMin,
ImVec2((rMin.x + rMax.x) * 0.5f, rMin.y),
ImVec2(rMax.x, rMin.y),
ImVec2(rMax.x, (rMin.y + rMax.y) * 0.5f),
rMax,
ImVec2((rMin.x + rMax.x) * 0.5f, rMax.y),
ImVec2(rMin.x, rMax.y),
ImVec2(rMin.x, (rMin.y + rMax.y) * 0.5f)
};
for (const ImVec2& h : handles) {
drawList->AddRectFilled(
ImVec2(h.x - handle * 0.32f, h.y - handle * 0.32f),
ImVec2(h.x + handle * 0.32f, h.y + handle * 0.32f),
accentColor,
4.0f
);
}
}
static void DrawUniversalIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
float radius = (max.x - min.x) * 0.28f;
drawList->AddCircle(center, radius, lineColor, 20, 2.0f);
float len = radius * 0.95f;
ImVec2 axes[] = {
ImVec2(1, 0), ImVec2(-1, 0), ImVec2(0, 1), ImVec2(0, -1)
};
float head = radius * 0.45f;
for (const ImVec2& dir : axes) {
ImVec2 tip = ImVec2(center.x + dir.x * len, center.y + dir.y * len);
drawList->AddLine(center, tip, accentColor, 2.0f);
ImVec2 ortho = ImVec2(-dir.y, dir.x);
ImVec2 a = ImVec2(tip.x - dir.x * head + ortho.x * head * 0.35f, tip.y - dir.y * head + ortho.y * head * 0.35f);
ImVec2 b = ImVec2(tip.x - dir.x * head - ortho.x * head * 0.35f, tip.y - dir.y * head - ortho.y * head * 0.35f);
drawList->AddTriangleFilled(tip, a, b, accentColor);
}
drawList->AddCircleFilled(center, radius * 0.24f, lineColor, 16);
}
static void DrawIcon(Icon icon, ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 lineColor, ImU32 accentColor) {
switch (icon) {
case Icon::Translate: DrawTranslateIcon(drawList, min, max, lineColor, accentColor); break;
case Icon::Rotate: DrawRotateIcon(drawList, min, max, lineColor, accentColor); break;
case Icon::Scale: DrawScaleIcon(drawList, min, max, lineColor, accentColor); break;
case Icon::Bounds: DrawBoundsIcon(drawList, min, max, lineColor, accentColor); break;
case Icon::Universal: DrawUniversalIcon(drawList, min, max, lineColor, accentColor); break;
}
}
static bool IconButton(const char* id, Icon icon, bool active, const ImVec2& size,
ImU32 baseColor, ImU32 hoverColor, ImU32 activeColor,
ImU32 accentColor, ImU32 iconColor) {
ImGui::PushID(id);
ImGui::InvisibleButton("##btn", size);
bool hovered = ImGui::IsItemHovered();
bool pressed = ImGui::IsItemClicked();
ImVec2 min = ImGui::GetItemRectMin();
ImVec2 max = ImGui::GetItemRectMax();
float rounding = 9.0f;
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImU32 bg = active ? activeColor : (hovered ? hoverColor : baseColor);
ImVec4 bgCol = ImGui::ColorConvertU32ToFloat4(bg);
ImU32 top = ImGui::GetColorU32(ScaleColor(bgCol, 1.07f));
ImU32 bottom = ImGui::GetColorU32(ScaleColor(bgCol, 0.93f));
drawList->AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
drawList->AddRect(min, max, ImGui::GetColorU32(ImVec4(1, 1, 1, active ? 0.35f : 0.18f)), rounding);
DrawIcon(icon, drawList, min, max, iconColor, accentColor);
ImGui::PopID();
return pressed;
}
static bool TextButton(const char* id, const char* label, bool active, const ImVec2& size,
ImU32 baseColor, ImU32 hoverColor, ImU32 activeColor, ImU32 borderColor, ImVec4 textColor) {
ImGui::PushID(id);
ImGui::InvisibleButton("##btn", size);
bool hovered = ImGui::IsItemHovered();
bool pressed = ImGui::IsItemClicked();
ImVec2 min = ImGui::GetItemRectMin();
ImVec2 max = ImGui::GetItemRectMax();
float rounding = 8.0f;
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImU32 bg = active ? activeColor : (hovered ? hoverColor : baseColor);
ImVec4 bgCol = ImGui::ColorConvertU32ToFloat4(bg);
ImU32 top = ImGui::GetColorU32(ScaleColor(bgCol, 1.06f));
ImU32 bottom = ImGui::GetColorU32(ScaleColor(bgCol, 0.94f));
drawList->AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
drawList->AddRect(min, max, borderColor, rounding);
ImVec2 textSize = ImGui::CalcTextSize(label);
ImVec2 textPos = ImVec2(
min.x + (size.x - textSize.x) * 0.5f,
min.y + (size.y - textSize.y) * 0.5f - 1.0f
);
drawList->AddText(textPos, ImGui::GetColorU32(textColor), label);
ImGui::PopID();
return pressed;
}
static bool ModeButton(const char* label, bool active, const ImVec2& size, ImVec4 baseColor, ImVec4 activeColor, ImVec4 textColor) {
ImGui::PushStyleColor(ImGuiCol_Button, active ? activeColor : baseColor);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, active ? activeColor : baseColor);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, active ? activeColor : baseColor);
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
bool pressed = ImGui::Button(label, size);
ImGui::PopStyleColor(4);
return pressed;
}
}
void Engine::renderGameViewportWindow() {
gameViewportFocused = false;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f));
ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar);
bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
ImVec2 avail = ImGui::GetContentRegionAvail();
int width = std::max(160, (int)avail.x);
int height = std::max(120, (int)avail.y);
SceneObject* playerCam = nullptr;
for (auto& obj : sceneObjects) {
if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) {
playerCam = &obj;
break;
}
}
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.09f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.12f, 0.14f, 0.16f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.14f, 0.18f, 0.20f, 1.0f));
ImGui::BeginDisabled(playerCam == nullptr);
bool dummyToggle = false;
bool postFxChanged = false;
if (playerCam) {
bool before = playerCam->camera.applyPostFX;
if (ImGui::Checkbox("Post FX", &playerCam->camera.applyPostFX)) {
postFxChanged = (before != playerCam->camera.applyPostFX);
}
} else {
ImGui::Checkbox("Post FX", &dummyToggle);
}
ImGui::SameLine();
ImGui::Checkbox("Text", &showUITextOverlay);
ImGui::SameLine();
ImGui::Checkbox("Canvas Guides", &showCanvasOverlay);
ImGui::EndDisabled();
ImGui::PopStyleColor(3);
if (playerCam && postFxChanged) {
projectManager.currentProject.hasUnsavedChanges = true;
}
if (!isPlaying) {
gameViewCursorLocked = false;
}
if (playerCam && rendererInitialized) {
unsigned int tex = renderer.renderScenePreview(
makeCameraFromObject(*playerCam),
sceneObjects,
width,
height,
playerCam->camera.fov,
playerCam->camera.nearClip,
playerCam->camera.farClip,
playerCam->camera.applyPostFX
);
ImGui::Image((void*)(intptr_t)tex, ImVec2((float)width, (float)height), ImVec2(0, 1), ImVec2(1, 0));
ImVec2 imageMin = ImGui::GetItemRectMin();
ImVec2 imageMax = ImGui::GetItemRectMax();
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (showCanvasOverlay) {
ImVec2 pad(8.0f, 8.0f);
ImVec2 tl(imageMin.x + pad.x, imageMin.y + pad.y);
ImVec2 br(imageMax.x - pad.x, imageMax.y - pad.y);
drawList->AddRect(tl, br, IM_COL32(110, 170, 255, 180), 8.0f, 0, 2.0f);
}
if (showUITextOverlay) {
const char* textLabel = "Text Overlay";
ImVec2 textPos(imageMin.x + 16.0f, imageMin.y + 16.0f);
ImVec2 size = ImGui::CalcTextSize(textLabel);
ImVec2 bgPad(6.0f, 4.0f);
ImVec2 bgMin(textPos.x - bgPad.x, textPos.y - bgPad.y);
ImVec2 bgMax(textPos.x + size.x + bgPad.x, textPos.y + size.y + bgPad.y);
drawList->AddRectFilled(bgMin, bgMax, IM_COL32(20, 20, 24, 200), 4.0f);
drawList->AddText(textPos, IM_COL32(235, 235, 245, 255), textLabel);
}
bool hovered = ImGui::IsItemHovered();
bool clicked = hovered && isPlaying && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
if (clicked && !gameViewCursorLocked) {
gameViewCursorLocked = true;
}
if (gameViewCursorLocked && (!isPlaying || !windowFocused || ImGui::IsKeyPressed(ImGuiKey_Escape))) {
gameViewCursorLocked = false;
}
gameViewportFocused = windowFocused && gameViewCursorLocked;
ImGui::TextDisabled(gameViewCursorLocked ? "Camera captured (ESC to release)" : "Click to capture");
} else {
ImGui::TextDisabled("No player camera found (Camera Type: Player).");
gameViewportFocused = ImGui::IsWindowFocused();
}
ImGui::End();
ImGui::PopStyleVar();
}
void Engine::renderPlayControlsBar() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 buttonPadding(10.0f, 4.0f);
const char* playLabel = isPlaying ? "Stop" : "Play";
const char* pauseLabel = isPaused ? "Resume" : "Pause";
const char* specLabel = specMode ? "Spec On" : "Spec Mode";
auto buttonWidth = [&](const char* label) {
ImVec2 textSize = ImGui::CalcTextSize(label);
return textSize.x + buttonPadding.x * 2.0f + style.FrameBorderSize * 2.0f;
};
float playWidth = buttonWidth(playLabel);
float pauseWidth = buttonWidth(pauseLabel);
float specWidth = buttonWidth(specLabel);
float spacing = style.ItemSpacing.x;
float totalWidth = playWidth + pauseWidth + specWidth + spacing * 2.0f;
// Center the controls inside the dockspace menu bar.
float regionMinX = ImGui::GetWindowContentRegionMin().x;
float regionMaxX = ImGui::GetWindowContentRegionMax().x;
float regionWidth = regionMaxX - regionMinX;
float startX = (regionWidth - totalWidth) * 0.5f + regionMinX;
if (startX < regionMinX) startX = regionMinX;
ImVec2 cursor = ImGui::GetCursorPos();
ImGui::SetCursorPos(ImVec2(startX, cursor.y));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, buttonPadding);
bool playPressed = ImGui::Button(playLabel);
ImGui::SameLine(0.0f, spacing);
bool pausePressed = ImGui::Button(pauseLabel);
ImGui::SameLine(0.0f, spacing);
bool specPressed = ImGui::Button(specLabel);
ImGui::PopStyleVar();
if (playPressed) {
bool newState = !isPlaying;
if (newState) {
if (physics.isReady() || physics.init()) {
physics.onPlayStart(sceneObjects);
} else {
addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning);
}
audio.onPlayStart(sceneObjects);
} else {
physics.onPlayStop();
audio.onPlayStop();
isPaused = false;
if (specMode && (physics.isReady() || physics.init())) {
physics.onPlayStart(sceneObjects);
}
}
isPlaying = newState;
}
if (pausePressed) {
isPaused = !isPaused;
if (isPaused) isPlaying = true; // placeholder: pausing implies were in play mode
}
if (specPressed) {
bool enable = !specMode;
if (enable && !physics.isReady() && !physics.init()) {
addConsoleMessage("PhysX failed to initialize; spec mode disabled", ConsoleMessageType::Warning);
enable = false;
}
specMode = enable;
if (!isPlaying) {
if (specMode) {
physics.onPlayStart(sceneObjects);
audio.onPlayStart(sceneObjects);
} else {
physics.onPlayStop();
audio.onPlayStop();
}
}
}
}
void Engine::renderMainMenuBar() {
refreshScriptEditorWindows();
if (ImGui::BeginMainMenuBar()) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(14.0f, 8.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 6.0f));
ImVec4 accent = ImGui::GetStyleColorVec4(ImGuiCol_CheckMark);
ImVec4 subtle = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
showNewSceneDialog = true;
memset(newSceneName, 0, sizeof(newSceneName));
}
if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {
saveCurrentScene();
}
if (ImGui::MenuItem("Save Scene As...")) {
showSaveSceneAsDialog = true;
strncpy(saveSceneAsName, projectManager.currentProject.currentSceneName.c_str(),
sizeof(saveSceneAsName) - 1);
}
ImGui::Separator();
if (ImGui::MenuItem("Close Project")) {
if (projectManager.currentProject.hasUnsavedChanges) {
saveCurrentScene();
}
projectManager.currentProject = Project();
sceneObjects.clear();
clearSelection();
scriptEditorWindows.clear();
scriptEditorWindowsDirty = true;
showLauncher = true;
}
ImGui::Separator();
if (ImGui::MenuItem("Exit")) {
glfwSetWindowShouldClose(editorWindow, true);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, false)) {}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, false)) {}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Hierarchy", nullptr, &showHierarchy);
ImGui::MenuItem("Inspector", nullptr, &showInspector);
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);
if (!scriptEditorWindows.empty()) {
ImGui::Separator();
ImGui::TextDisabled("Scripted Windows");
for (auto& window : scriptEditorWindows) {
ImGui::MenuItem(window.label.c_str(), nullptr, &window.open);
}
}
ImGui::Separator();
if (ImGui::MenuItem("Fullscreen Viewport", "F11", viewportFullscreen)) {
viewportFullscreen = !viewportFullscreen;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Scripts")) {
auto toggleSpec = [&](bool enabled) {
if (specMode == enabled) return;
if (enabled && !physics.isReady() && !physics.init()) {
addConsoleMessage("PhysX failed to initialize; spec mode disabled", ConsoleMessageType::Warning);
specMode = false;
return;
}
specMode = enabled;
if (!isPlaying) {
if (specMode) physics.onPlayStart(sceneObjects);
else physics.onPlayStop();
}
};
bool specValue = specMode;
if (ImGui::MenuItem("Spec Mode (run Script_Spec)", nullptr, &specValue)) {
toggleSpec(specValue);
}
ImGui::MenuItem("Test Mode (run Script_TestEditor)", nullptr, &testMode);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Create")) {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera");
if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
if (ImGui::MenuItem("Point Light")) addObject(ObjectType::PointLight, "Point Light");
if (ImGui::MenuItem("Spot Light")) addObject(ObjectType::SpotLight, "Spot Light");
if (ImGui::MenuItem("Area Light")) addObject(ObjectType::AreaLight, "Area Light");
if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
logToConsole("Modularity Engine v0.6.8");
}
ImGui::EndMenu();
}
ImGui::Separator();
ImGui::TextColored(subtle, "Project");
ImGui::SameLine();
std::string projectLabel = projectManager.currentProject.name.empty() ?
"New Project" : projectManager.currentProject.name;
ImGui::TextColored(accent, "%s", projectLabel.c_str());
ImGui::SameLine();
ImGui::TextColored(subtle, "|");
ImGui::SameLine();
std::string sceneLabel = projectManager.currentProject.currentSceneName.empty() ?
"No Scene Loaded" : projectManager.currentProject.currentSceneName;
ImGui::TextUnformatted(sceneLabel.c_str());
float rightX = ImGui::GetWindowWidth() - 220.0f;
if (rightX > ImGui::GetCursorPosX()) {
ImGui::SameLine(rightX);
} else {
ImGui::SameLine();
}
ImGui::TextColored(subtle, "Viewport");
ImGui::SameLine();
ImGui::TextColored(accent, viewportFullscreen ? "Fullscreen" : "Docked");
ImGui::PopStyleVar(2);
ImGui::EndMainMenuBar();
}
}
void Engine::renderViewport() {
ImGuiWindowFlags viewportFlags = ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar;
if (viewportFullscreen) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
viewportFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("Viewport", nullptr, viewportFlags);
ImGui::PopStyleVar();
ImVec2 fullAvail = ImGui::GetContentRegionAvail();
const float toolbarHeight = 0.0f;
ImVec2 imageSize = fullAvail;
imageSize.y = ImMax(1.0f, imageSize.y - toolbarHeight);
if (imageSize.x > 0 && imageSize.y > 0) {
viewportWidth = static_cast<int>(imageSize.x);
viewportHeight = static_cast<int>(imageSize.y);
if (rendererInitialized) {
renderer.resize(viewportWidth, viewportHeight);
}
}
bool mouseOverViewportImage = false;
bool blockSelection = false;
if (rendererInitialized) {
glm::mat4 proj = glm::perspective(
glm::radians(FOV),
(float)viewportWidth / (float)viewportHeight,
NEAR_PLANE, FAR_PLANE
);
glm::mat4 view = camera.getViewMatrix();
renderer.beginRender(view, proj, camera.position);
renderer.renderScene(camera, sceneObjects, selectedObjectId);
unsigned int tex = renderer.getViewportTexture();
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
ImVec2 imageMin = ImGui::GetItemRectMin();
ImVec2 imageMax = ImGui::GetItemRectMax();
mouseOverViewportImage = ImGui::IsItemHovered();
ImDrawList* viewportDrawList = ImGui::GetWindowDrawList();
auto setCameraFacing = [&](const glm::vec3& dir) {
glm::vec3 worldUp = glm::vec3(0, 1, 0);
glm::vec3 n = glm::normalize(dir);
glm::vec3 up = worldUp;
if (std::abs(glm::dot(n, worldUp)) > 0.98f) {
up = glm::vec3(0, 0, 1);
}
glm::vec3 right = glm::normalize(glm::cross(up, n));
if (glm::length(right) < 1e-4f) {
right = glm::vec3(1, 0, 0);
}
up = glm::normalize(glm::cross(n, right));
camera.front = n;
camera.up = up;
camera.pitch = glm::degrees(std::asin(glm::clamp(n.y, -1.0f, 1.0f)));
camera.pitch = glm::clamp(camera.pitch, -89.0f, 89.0f);
camera.yaw = glm::degrees(std::atan2(n.z, n.x));
camera.firstMouse = true;
};
// Draw small axis widget in top-right of viewport
{
const float widgetSize = 94.0f;
const float padding = 12.0f;
ImVec2 center = ImVec2(
imageMax.x - padding - widgetSize * 0.5f,
imageMin.y + padding + widgetSize * 0.5f
);
float radius = widgetSize * 0.46f;
ImU32 ringCol = ImGui::GetColorU32(ImVec4(0.07f, 0.07f, 0.1f, 0.9f));
ImU32 ringBorder = ImGui::GetColorU32(ImVec4(1, 1, 1, 0.18f));
viewportDrawList->AddCircleFilled(center, radius + 10.0f, ringCol, 48);
viewportDrawList->AddCircle(center, radius + 10.0f, ringBorder, 48);
viewportDrawList->AddCircle(center, radius + 3.0f, ImGui::GetColorU32(ImVec4(1,1,1,0.08f)), 32);
viewportDrawList->AddCircleFilled(center, 5.5f, ImGui::GetColorU32(ImVec4(1,1,1,0.6f)), 24);
glm::mat3 viewRot = glm::mat3(view);
ImVec2 widgetMin = ImVec2(center.x - widgetSize * 0.5f, center.y - widgetSize * 0.5f);
ImVec2 widgetMax = ImVec2(center.x + widgetSize * 0.5f, center.y + widgetSize * 0.5f);
bool widgetHover = ImGui::IsMouseHoveringRect(widgetMin, widgetMax);
struct AxisArrow {
glm::vec3 dir;
ImU32 color;
const char* label;
};
AxisArrow arrows[] = {
{ glm::vec3(1, 0, 0), ImGui::GetColorU32(ImVec4(0.9f, 0.2f, 0.2f, 1.0f)), "X" },
{ glm::vec3(-1, 0, 0), ImGui::GetColorU32(ImVec4(0.6f, 0.2f, 0.2f, 1.0f)), "-X" },
{ glm::vec3(0, 1, 0), ImGui::GetColorU32(ImVec4(0.2f, 0.9f, 0.2f, 1.0f)), "Y" },
{ glm::vec3(0,-1, 0), ImGui::GetColorU32(ImVec4(0.2f, 0.6f, 0.2f, 1.0f)), "-Y" },
{ glm::vec3(0, 0, 1), ImGui::GetColorU32(ImVec4(0.2f, 0.4f, 0.9f, 1.0f)), "Z" },
{ glm::vec3(0, 0,-1), ImGui::GetColorU32(ImVec4(0.2f, 0.3f, 0.6f, 1.0f)), "-Z" },
};
ImVec2 mouse = ImGui::GetIO().MousePos;
int clickedIdx = -1;
float clickRadius = 12.0f;
for (int i = 0; i < 6; ++i) {
glm::vec3 camSpace = viewRot * arrows[i].dir;
glm::vec2 dir2 = glm::normalize(glm::vec2(camSpace.x, -camSpace.y));
float depthScale = glm::clamp(0.35f + 0.65f * ((camSpace.z + 1.0f) * 0.5f), 0.25f, 1.0f);
float len = radius * depthScale;
ImVec2 tip = ImVec2(center.x + dir2.x * len, center.y + dir2.y * len);
ImVec2 base1 = ImVec2(center.x + dir2.x * (len * 0.55f) + dir2.y * (len * 0.12f),
center.y + dir2.y * (len * 0.55f) - dir2.x * (len * 0.12f));
ImVec2 base2 = ImVec2(center.x + dir2.x * (len * 0.55f) - dir2.y * (len * 0.12f),
center.y + dir2.y * (len * 0.55f) + dir2.x * (len * 0.12f));
viewportDrawList->AddTriangleFilled(base1, tip, base2, arrows[i].color);
viewportDrawList->AddTriangle(base1, tip, base2, ImGui::GetColorU32(ImVec4(0,0,0,0.35f)));
ImVec2 labelPos = ImVec2(center.x + dir2.x * (len * 0.78f), center.y + dir2.y * (len * 0.78f));
viewportDrawList->AddCircleFilled(labelPos, 6.0f, ImGui::GetColorU32(ImVec4(0,0,0,0.5f)), 12);
viewportDrawList->AddText(ImVec2(labelPos.x - 4.0f, labelPos.y - 7.0f), ImGui::GetColorU32(ImVec4(1,1,1,0.95f)), arrows[i].label);
if (widgetHover) {
float dx = mouse.x - tip.x;
float dy = mouse.y - tip.y;
if (std::sqrt(dx*dx + dy*dy) <= clickRadius && ImGui::IsMouseReleased(0)) {
clickedIdx = i;
}
}
}
if (clickedIdx >= 0) {
setCameraFacing(arrows[clickedIdx].dir);
}
// Prevent viewport picking when interacting with the axis widget.
if (widgetHover) {
blockSelection = true;
}
}
auto projectToScreen = [&](const glm::vec3& p) -> std::optional<ImVec2> {
glm::vec4 clip = proj * view * glm::vec4(p, 1.0f);
if (clip.w <= 0.0f) return std::nullopt;
glm::vec3 ndc = glm::vec3(clip) / clip.w;
ImVec2 screen;
screen.x = imageMin.x + (ndc.x * 0.5f + 0.5f) * (imageMax.x - imageMin.x);
screen.y = imageMin.y + (1.0f - (ndc.y * 0.5f + 0.5f)) * (imageMax.y - imageMin.y);
return screen;
};
SceneObject* selectedObj = getSelectedObject();
if (selectedObj && selectedObj->type != ObjectType::PostFXNode) {
ImGuizmo::BeginFrame();
ImGuizmo::Enable(true);
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
ImGuizmo::SetRect(
imageMin.x,
imageMin.y,
imageMax.x - imageMin.x,
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, 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;
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 (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)));
}
}
}
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);
}
}
// 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);
}
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 {
// 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 = NormalizeEulerDegrees(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;
}
}
}
auto drawCameraDirection = [&](const SceneObject& camObj) {
glm::quat q = glm::quat(glm::radians(camObj.rotation));
glm::mat3 rot = glm::mat3_cast(q);
glm::vec3 forward = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 upDir = glm::normalize(rot * glm::vec3(0.0f, 1.0f, 0.0f));
if (!std::isfinite(forward.x) || glm::length(forward) < 1e-3f) return;
auto start = projectToScreen(camObj.position);
auto end = projectToScreen(camObj.position + forward * 1.4f);
auto upTip = projectToScreen(camObj.position + upDir * 0.6f);
if (start && end) {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 lineCol = ImGui::GetColorU32(ImVec4(0.3f, 0.8f, 1.0f, 0.9f));
ImU32 headCol = ImGui::GetColorU32(ImVec4(0.9f, 1.0f, 1.0f, 0.95f));
dl->AddLine(*start, *end, lineCol, 2.5f);
ImVec2 dir = ImVec2(end->x - start->x, end->y - start->y);
float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
if (len > 1.0f) {
ImVec2 normDir = ImVec2(dir.x / len, dir.y / len);
ImVec2 left = ImVec2(-normDir.y, normDir.x);
float head = 10.0f;
ImVec2 tip = *end;
ImVec2 p1 = ImVec2(tip.x - normDir.x * head + left.x * head * 0.6f, tip.y - normDir.y * head + left.y * head * 0.6f);
ImVec2 p2 = ImVec2(tip.x - normDir.x * head - left.x * head * 0.6f, tip.y - normDir.y * head - left.y * head * 0.6f);
dl->AddTriangleFilled(tip, p1, p2, headCol);
}
if (upTip) {
dl->AddCircleFilled(*upTip, 3.0f, ImGui::GetColorU32(ImVec4(0.8f, 1.0f, 0.6f, 0.8f)));
}
}
};
if (showSceneGizmos) {
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::Camera) {
drawCameraDirection(obj);
}
}
}
// Light visualization overlays
auto drawLightOverlays = [&](const SceneObject& lightObj) {
if (!lightObj.light.enabled) return;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 col = ImGui::GetColorU32(ImVec4(1.0f, 0.9f, 0.4f, 0.7f));
ImU32 faint = ImGui::GetColorU32(ImVec4(1.0f, 0.9f, 0.4f, 0.25f));
auto forwardFromRotation = [](const SceneObject& obj) {
glm::vec3 f = glm::normalize(glm::vec3(
glm::sin(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x)),
glm::sin(glm::radians(obj.rotation.x)),
glm::cos(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x))
));
if (glm::length(f) < 1e-3f || !std::isfinite(f.x)) f = glm::vec3(0.0f, -1.0f, 0.0f);
return f;
};
if (lightObj.type == ObjectType::PointLight) {
auto center = projectToScreen(lightObj.position);
glm::vec3 offset = lightObj.position + glm::vec3(lightObj.light.range, 0.0f, 0.0f);
auto edge = projectToScreen(offset);
if (center && edge) {
float r = std::sqrt((center->x - edge->x)*(center->x - edge->x) + (center->y - edge->y)*(center->y - edge->y));
dl->AddCircle(*center, r, faint, 48, 2.0f);
}
} else if (lightObj.type == ObjectType::SpotLight) {
glm::vec3 dir = forwardFromRotation(lightObj);
glm::vec3 tip = lightObj.position;
glm::vec3 end = tip + dir * lightObj.light.range;
float innerRad = glm::tan(glm::radians(lightObj.light.innerAngle)) * lightObj.light.range;
float outerRad = glm::tan(glm::radians(lightObj.light.outerAngle)) * lightObj.light.range;
// Build basis
glm::vec3 up = glm::abs(dir.y) > 0.9f ? glm::vec3(1,0,0) : glm::vec3(0,1,0);
glm::vec3 right = glm::normalize(glm::cross(dir, up));
up = glm::normalize(glm::cross(right, dir));
auto drawConeRing = [&](float radius, ImU32 color) {
const int segments = 24;
ImVec2 prev;
bool first = true;
for (int i = 0; i <= segments; ++i) {
float a = (float)i / segments * 2.0f * PI;
glm::vec3 p = end + right * std::cos(a) * radius + up * std::sin(a) * radius;
auto sp = projectToScreen(p);
if (!sp) continue;
if (first) { prev = *sp; first = false; continue; }
dl->AddLine(prev, *sp, color, 1.5f);
prev = *sp;
}
};
auto sTip = projectToScreen(tip);
auto sEnd = projectToScreen(end);
if (sTip && sEnd) {
dl->AddLine(*sTip, *sEnd, col, 2.0f);
drawConeRing(innerRad, col);
drawConeRing(outerRad, faint);
}
} else if (lightObj.type == ObjectType::AreaLight) {
glm::vec3 n = forwardFromRotation(lightObj);
glm::vec3 up = glm::abs(n.y) > 0.9f ? glm::vec3(1,0,0) : glm::vec3(0,1,0);
glm::vec3 tangent = glm::normalize(glm::cross(up, n));
glm::vec3 bitangent = glm::cross(n, tangent);
glm::vec2 half = lightObj.light.size * 0.5f;
glm::vec3 c = lightObj.position;
glm::vec3 corners[4] = {
c + tangent * half.x + bitangent * half.y,
c - tangent * half.x + bitangent * half.y,
c - tangent * half.x - bitangent * half.y,
c + tangent * half.x - bitangent * half.y
};
ImVec2 projected[4];
bool ok = true;
for (int i = 0; i < 4; ++i) {
auto p = projectToScreen(corners[i]);
if (!p) { ok = false; break; }
projected[i] = *p;
}
if (ok) {
for (int i = 0; i < 4; ++i) {
dl->AddLine(projected[i], projected[(i+1)%4], col, 2.0f);
}
// normal indicator
auto cproj = projectToScreen(c);
auto nproj = projectToScreen(c + n * glm::max(lightObj.light.range, 0.5f));
if (cproj && nproj) {
dl->AddLine(*cproj, *nproj, col, 2.0f);
dl->AddCircleFilled(*nproj, 4.0f, col);
}
}
}
};
if (showSceneGizmos) {
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight) {
drawLightOverlays(obj);
}
}
}
// Toolbar
const float toolbarPadding = 6.0f;
const float toolbarSpacing = 5.0f;
const ImVec2 gizmoButtonSize(60.0f, 24.0f);
const float toolbarWidthEstimate = 520.0f;
const float toolbarHeightEstimate = 42.0f; // rough height to keep toolbar on-screen when anchoring bottom
ImVec2 desiredBottomLeft = ImVec2(imageMin.x + 12.0f, imageMax.y - 12.0f);
float minX = imageMin.x + 12.0f;
float maxX = imageMax.x - 12.0f;
float toolbarLeft = desiredBottomLeft.x;
if (toolbarLeft + toolbarWidthEstimate > maxX) toolbarLeft = maxX - toolbarWidthEstimate;
if (toolbarLeft < minX) toolbarLeft = minX;
float minY = imageMin.y + 12.0f;
float toolbarTop = desiredBottomLeft.y - toolbarHeightEstimate;
if (toolbarTop < minY) toolbarTop = minY;
ImVec2 toolbarPos = ImVec2(toolbarLeft, toolbarTop);
const ImGuiStyle& style = ImGui::GetStyle();
ImVec4 bgCol = style.Colors[ImGuiCol_PopupBg];
bgCol.w = 0.78f;
ImVec4 baseCol = style.Colors[ImGuiCol_FrameBg];
baseCol.w = 0.85f;
ImVec4 hoverCol = style.Colors[ImGuiCol_ButtonHovered];
hoverCol.w = 0.95f;
ImVec4 activeCol = style.Colors[ImGuiCol_ButtonActive];
activeCol.w = 1.0f;
ImVec4 accentCol = style.Colors[ImGuiCol_HeaderActive];
accentCol.w = 1.0f;
ImVec4 textCol = style.Colors[ImGuiCol_Text];
ImU32 baseBtn = ImGui::GetColorU32(baseCol);
ImU32 hoverBtn = ImGui::GetColorU32(GizmoToolbar::ScaleColor(hoverCol, 1.05f));
ImU32 activeBtn = ImGui::GetColorU32(GizmoToolbar::ScaleColor(activeCol, 1.08f));
ImU32 accent = ImGui::GetColorU32(accentCol);
ImU32 iconColor = ImGui::GetColorU32(ImVec4(0.95f, 0.98f, 1.0f, 0.95f));
ImU32 toolbarBg = ImGui::GetColorU32(bgCol);
ImU32 toolbarOutline = ImGui::GetColorU32(ImVec4(1, 1, 1, 0.0f));
ImDrawList* toolbarDrawList = ImGui::GetWindowDrawList();
ImDrawListSplitter splitter;
splitter.Split(toolbarDrawList, 2);
splitter.SetCurrentChannel(toolbarDrawList, 1);
ImVec2 contentStart = ImVec2(toolbarPos.x + toolbarPadding, toolbarPos.y + toolbarPadding);
ImVec2 windowPos = ImGui::GetWindowPos();
ImVec2 contentStartLocal = ImVec2(contentStart.x - windowPos.x, contentStart.y - windowPos.y);
ImGui::SetCursorPos(contentStartLocal);
ImVec2 contentStartScreen = ImGui::GetCursorScreenPos();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(toolbarSpacing, toolbarSpacing));
ImGui::BeginGroup();
auto gizmoButton = [&](const char* label, ImGuizmo::OPERATION op, const char* tooltip) {
if (GizmoToolbar::TextButton(label, mCurrentGizmoOperation == op, gizmoButtonSize, baseBtn, hoverBtn, activeBtn, accent, iconColor)) {
mCurrentGizmoOperation = op;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", tooltip);
}
};
gizmoButton("Move", ImGuizmo::TRANSLATE, "Translate");
ImGui::SameLine(0.0f, toolbarSpacing);
gizmoButton("Rotate", ImGuizmo::ROTATE, "Rotate");
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");
ImGui::SameLine(0.0f, toolbarSpacing * 1.25f);
ImVec2 modeSize(56.0f, 24.0f);
if (GizmoToolbar::ModeButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL, modeSize, baseCol, accentCol, textCol)) {
mCurrentGizmoMode = ImGuizmo::LOCAL;
}
ImGui::SameLine(0.0f, toolbarSpacing * 0.8f);
if (GizmoToolbar::ModeButton("World", mCurrentGizmoMode == ImGuizmo::WORLD, modeSize, baseCol, accentCol, textCol)) {
mCurrentGizmoMode = ImGuizmo::WORLD;
}
ImGui::SameLine(0.0f, toolbarSpacing);
ImGui::Checkbox("Snap", &useSnap);
if (useSnap) {
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
if (mCurrentGizmoOperation == ImGuizmo::ROTATE) {
ImGui::DragFloat("##snapAngle", &rotationSnapValue, 1.0f, 1.0f, 90.0f, "%.0f deg");
} else {
ImGui::DragFloat("##snapVal", &snapValue[0], 0.1f, 0.1f, 10.0f, "%.1f");
snapValue[1] = snapValue[2] = snapValue[0];
}
}
ImGui::SameLine(0.0f, toolbarSpacing * 1.25f);
if (GizmoToolbar::ModeButton("Gizmos", showSceneGizmos, ImVec2(70, 24), baseCol, accentCol, textCol)) {
showSceneGizmos = !showSceneGizmos;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Toggle light/camera scene symbols");
}
ImGui::EndGroup();
ImGui::PopStyleVar();
ImVec2 groupMax = ImGui::GetItemRectMax();
splitter.SetCurrentChannel(toolbarDrawList, 0);
float rounding = 10.0f;
ImVec2 bgMin = ImVec2(contentStartScreen.x - toolbarPadding, contentStartScreen.y - toolbarPadding);
ImVec2 bgMax = ImVec2(groupMax.x + toolbarPadding, groupMax.y + toolbarPadding);
toolbarDrawList->AddRectFilled(bgMin, bgMax, toolbarBg, rounding, ImDrawFlags_RoundCornersAll);
toolbarDrawList->AddRect(bgMin, bgMax, toolbarOutline, rounding, ImDrawFlags_RoundCornersAll, 1.5f);
splitter.Merge(toolbarDrawList);
// Prevent viewport picking when clicking on the toolbar overlay.
if (ImGui::IsMouseHoveringRect(bgMin, bgMax)) {
blockSelection = true;
}
// Left-click picking inside viewport
if (mouseOverViewportImage &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGuizmo::IsUsing() && !ImGuizmo::IsOver() &&
!blockSelection)
{
glm::mat4 invViewProj = glm::inverse(proj * view);
ImVec2 mousePos = ImGui::GetMousePos();
auto makeRay = [&](const ImVec2& pos) {
float x = (pos.x - imageMin.x) / (imageMax.x - imageMin.x);
float y = (pos.y - imageMin.y) / (imageMax.y - imageMin.y);
x = x * 2.0f - 1.0f;
y = 1.0f - y * 2.0f;
glm::vec4 nearPt = invViewProj * glm::vec4(x, y, -1.0f, 1.0f);
glm::vec4 farPt = invViewProj * glm::vec4(x, y, 1.0f, 1.0f);
nearPt /= nearPt.w;
farPt /= farPt.w;
glm::vec3 origin = glm::vec3(nearPt);
glm::vec3 dir = glm::normalize(glm::vec3(farPt - nearPt));
return std::make_pair(origin, dir);
};
auto rayAabb = [](const glm::vec3& orig, const glm::vec3& dir, const glm::vec3& bmin, const glm::vec3& bmax, float& tHit) {
float tmin = -FLT_MAX;
float tmax = FLT_MAX;
for (int i = 0; i < 3; ++i) {
if (std::abs(dir[i]) < 1e-6f) {
if (orig[i] < bmin[i] || orig[i] > bmax[i]) return false;
continue;
}
float invD = 1.0f / dir[i];
float t1 = (bmin[i] - orig[i]) * invD;
float t2 = (bmax[i] - orig[i]) * invD;
if (t1 > t2) std::swap(t1, t2);
tmin = std::max(tmin, t1);
tmax = std::min(tmax, t2);
if (tmin > tmax) return false;
}
tHit = (tmin >= 0.0f) ? tmin : tmax;
return tmax >= 0.0f;
};
auto raySphere = [](const glm::vec3& orig, const glm::vec3& dir, float radius, float& tHit) {
float b = glm::dot(dir, orig);
float c = glm::dot(orig, orig) - radius * radius;
float disc = b * b - c;
if (disc < 0.0f) return false;
float sqrtDisc = sqrtf(disc);
float t0 = -b - sqrtDisc;
float t1 = -b + sqrtDisc;
float t = (t0 >= 0.0f) ? t0 : t1;
if (t < 0.0f) return false;
tHit = t;
return true;
};
auto rayTriangle = [](const glm::vec3& orig, const glm::vec3& dir, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& tHit) {
const float EPSILON = 1e-6f;
glm::vec3 e1 = v1 - v0;
glm::vec3 e2 = v2 - v0;
glm::vec3 pvec = glm::cross(dir, e2);
float det = glm::dot(e1, pvec);
if (fabs(det) < EPSILON) return false;
float invDet = 1.0f / det;
glm::vec3 tvec = orig - v0;
float u = glm::dot(tvec, pvec) * invDet;
if (u < 0.0f || u > 1.0f) return false;
glm::vec3 qvec = glm::cross(tvec, e1);
float v = glm::dot(dir, qvec) * invDet;
if (v < 0.0f || u + v > 1.0f) return false;
float t = glm::dot(e2, qvec) * invDet;
if (t < 0.0f) return false;
tHit = t;
return true;
};
auto ray = makeRay(mousePos);
float closest = FLT_MAX;
int hitId = -1;
for (const auto& obj : sceneObjects) {
glm::vec3 aabbMin(-0.5f);
glm::vec3 aabbMax(0.5f);
glm::mat4 model(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
glm::mat4 invModel = glm::inverse(model);
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(ray.first, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(ray.second, 0.0f)));
float hitT = 0.0f;
bool hit = false;
switch (obj.type) {
case ObjectType::Cube:
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f), glm::vec3(0.5f), hitT);
break;
case ObjectType::Sphere:
hit = raySphere(localOrigin, localDir, 0.5f, hitT);
break;
case ObjectType::Capsule:
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.35f, -0.9f, -0.35f), glm::vec3(0.35f, 0.9f, 0.35f), hitT);
break;
case ObjectType::Mirror:
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT);
break;
case ObjectType::OBJMesh: {
const auto* info = g_objLoader.getMeshInfo(obj.meshId);
if (info && info->boundsMin.x < info->boundsMax.x) {
aabbMin = info->boundsMin;
aabbMax = info->boundsMax;
}
bool aabbHit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
if (aabbHit && info && !info->triangleVertices.empty()) {
float triBest = FLT_MAX;
for (size_t i = 0; i + 2 < info->triangleVertices.size(); i += 3) {
float triT = 0.0f;
if (rayTriangle(localOrigin, localDir, info->triangleVertices[i], info->triangleVertices[i + 1], info->triangleVertices[i + 2], triT)) {
if (triT < triBest && triT >= 0.0f) triBest = triT;
}
}
if (triBest < FLT_MAX) {
hit = true;
hitT = triBest;
} else {
hit = false;
}
} else {
hit = aabbHit;
}
break;
}
case ObjectType::Model: {
const auto* info = getModelLoader().getMeshInfo(obj.meshId);
if (info && info->boundsMin.x < info->boundsMax.x) {
aabbMin = info->boundsMin;
aabbMax = info->boundsMax;
}
bool aabbHit = rayAabb(localOrigin, localDir, aabbMin, aabbMax, hitT);
if (aabbHit && info && !info->triangleVertices.empty()) {
float triBest = FLT_MAX;
for (size_t i = 0; i + 2 < info->triangleVertices.size(); i += 3) {
float triT = 0.0f;
if (rayTriangle(localOrigin, localDir, info->triangleVertices[i], info->triangleVertices[i + 1], info->triangleVertices[i + 2], triT)) {
if (triT < triBest && triT >= 0.0f) triBest = triT;
}
}
if (triBest < FLT_MAX) {
hit = true;
hitT = triBest;
} else {
hit = false;
}
} else {
hit = aabbHit;
}
break;
}
case ObjectType::Camera:
hit = raySphere(localOrigin, localDir, 0.3f, hitT);
break;
case ObjectType::DirectionalLight:
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight:
hit = raySphere(localOrigin, localDir, 0.3f, hitT);
break;
case ObjectType::PostFXNode:
hit = false;
break;
}
if (hit && hitT < closest && hitT >= 0.0f) {
closest = hitT;
hitId = obj.id;
}
}
viewportController.setFocused(true);
if (hitId != -1) {
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
setPrimarySelection(hitId, additive);
} else {
clearSelection();
}
}
if (mouseOverViewportImage && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
viewportController.setFocused(true);
cursorLocked = true;
camera.firstMouse = true;
}
if (cursorLocked && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
cursorLocked = false;
camera.firstMouse = true;
}
if (cursorLocked) {
viewportController.setFocused(true);
}
if (isPlaying && showViewOutput) {
std::vector<const SceneObject*> playerCams;
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) {
playerCams.push_back(&obj);
}
}
if (playerCams.empty()) {
previewCameraId = -1;
} else {
auto findCamById = [&](int id) -> const SceneObject* {
auto it = std::find_if(playerCams.begin(), playerCams.end(), [id](const SceneObject* o) { return o->id == id; });
return (it != playerCams.end()) ? *it : nullptr;
};
const SceneObject* previewCam = findCamById(previewCameraId);
if (!previewCam) {
previewCam = playerCams.front();
previewCameraId = previewCam->id;
}
int previewWidth = static_cast<int>(imageSize.x * 0.28f);
previewWidth = std::clamp(previewWidth, 180, 420);
int previewHeight = static_cast<int>(previewWidth / 16.0f * 9.0f);
unsigned int previewTex = renderer.renderScenePreview(
makeCameraFromObject(*previewCam),
sceneObjects,
previewWidth,
previewHeight,
previewCam->camera.fov,
previewCam->camera.nearClip,
previewCam->camera.farClip,
previewCam->camera.applyPostFX
);
if (previewTex != 0) {
ImVec2 overlaySize(previewWidth + 20.0f, previewHeight + 64.0f);
ImVec2 overlayPos = ImVec2(imageMax.x - overlaySize.x - 12.0f, imageMax.y - overlaySize.y - 12.0f);
ImVec2 winPos = ImGui::GetWindowPos();
ImVec2 localPos = ImVec2(overlayPos.x - winPos.x, overlayPos.y - winPos.y);
ImGui::SetCursorPos(localPos);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f));
ImGui::BeginChild("ViewOutputOverlay", overlaySize, true, ImGuiWindowFlags_NoScrollbar);
ImGui::TextDisabled("View Output");
if (ImGui::BeginCombo("##ViewOutputCamera", previewCam->name.c_str())) {
for (const auto* cam : playerCams) {
bool selected = cam->id == previewCameraId;
if (ImGui::Selectable(cam->name.c_str(), selected)) {
previewCameraId = cam->id;
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::Image((void*)(intptr_t)previewTex, ImVec2((float)previewWidth, (float)previewHeight), ImVec2(0, 1), ImVec2(1, 0));
ImGui::EndChild();
ImGui::PopStyleVar();
}
}
} else {
previewCameraId = -1;
}
}
// Overlay hint
ImGui::SetCursorPos(ImVec2(10, 30));
ImGui::TextColored(
ImVec4(1, 1, 1, 0.3f),
"Hold RMB: Look & Move | LMB: Select | WASD+QE: Move | ESC: Release | F11: Fullscreen"
);
if (cursorLocked) {
ImGui::SetCursorPos(ImVec2(10, 50));
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Freelook Active");
} else if (viewportController.isViewportFocused()) {
ImGui::SetCursorPos(ImVec2(10, 50));
ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Viewport Focused");
}
bool windowFocused = ImGui::IsWindowFocused();
viewportController.updateFocusFromImGui(windowFocused, cursorLocked);
ImGui::End();
}