oh my fucking god that parent with positions following thing took far too long 😭 anyway, yey! better icons and hierarchy changes!

This commit is contained in:
Anemunt
2025-12-27 17:56:13 -05:00
parent cf8cd3a42a
commit 0035138a6c
19 changed files with 1353 additions and 412 deletions

View File

@@ -159,7 +159,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
}
}
bool AudioSystem::playPreview(const std::string& path, float volume) {
bool AudioSystem::playPreview(const std::string& path, float volume, bool loop) {
if (path.empty()) return false;
if (!initialized && !init()) return false;
@@ -169,6 +169,7 @@ bool AudioSystem::playPreview(const std::string& path, float volume) {
std::cerr << "AudioSystem: preview load failed for " << path << " (" << res << ")\n";
return false;
}
ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE);
ma_sound_set_volume(&previewSound, volume);
ma_sound_set_spatialization_enabled(&previewSound, MA_FALSE);
previewPath = path;
@@ -217,6 +218,12 @@ bool AudioSystem::seekPreview(const std::string& path, double seconds) {
return res == MA_SUCCESS;
}
bool AudioSystem::setPreviewLoop(bool loop) {
if (!previewActive) return false;
ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE);
return true;
}
bool AudioSystem::playObjectSound(const SceneObject& obj) {
if (!obj.hasAudioSource || obj.audioSource.clipPath.empty() || !obj.audioSource.enabled) return false;
if (!ensureSoundFor(obj)) return false;
@@ -268,12 +275,18 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
if (preview.channels >= 2) {
preview.waveformLeft.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
preview.waveformRight.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
}
std::vector<float> temp(kPreviewChunkFrames * preview.channels);
ma_uint64 frameCursor = 0;
size_t bucketIndex = 0;
ma_uint64 bucketCursor = 0;
float bucketMax = 0.0f;
float bucketMaxLeft = 0.0f;
float bucketMaxRight = 0.0f;
while (frameCursor < totalFrames && bucketIndex < preview.waveform.size()) {
ma_uint64 framesToRead = std::min<ma_uint64>(kPreviewChunkFrames, totalFrames - frameCursor);
@@ -285,26 +298,43 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
if (framesRead == 0) break;
for (ma_uint64 f = 0; f < framesRead; ++f) {
size_t frameOffset = static_cast<size_t>(f * preview.channels);
for (ma_uint32 c = 0; c < preview.channels; ++c) {
float sample = temp[static_cast<size_t>(f * preview.channels + c)];
float sample = temp[frameOffset + c];
bucketMax = std::max(bucketMax, std::fabs(sample));
}
if (preview.channels >= 2) {
float leftSample = temp[frameOffset];
float rightSample = temp[frameOffset + 1];
bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(leftSample));
bucketMaxRight = std::max(bucketMaxRight, std::fabs(rightSample));
}
bucketCursor++;
frameCursor++;
if (bucketCursor >= framesPerBucket) {
if (bucketIndex < preview.waveform.size()) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
bucketIndex++;
}
bucketCursor = 0;
bucketMax = 0.0f;
bucketMaxLeft = 0.0f;
bucketMaxRight = 0.0f;
}
}
}
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
}
ma_decoder_uninit(&decoder);

View File

@@ -14,6 +14,8 @@ struct AudioClipPreview {
uint32_t sampleRate = 0;
double durationSeconds = 0.0;
std::vector<float> waveform; // Normalized 0..1 amplitude envelope for drawing
std::vector<float> waveformLeft; // Left channel envelope (if available)
std::vector<float> waveformRight; // Right channel envelope (if available)
};
class AudioSystem {
@@ -26,12 +28,13 @@ public:
void onPlayStop();
void update(const std::vector<SceneObject>& objects, const Camera& listenerCamera, bool playing);
bool playPreview(const std::string& path, float volume = 1.0f);
bool playPreview(const std::string& path, float volume = 1.0f, bool loop = false);
void stopPreview();
bool isPreviewing(const std::string& path) const;
const AudioClipPreview* getPreview(const std::string& path);
bool getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const;
bool seekPreview(const std::string& path, double seconds);
bool setPreviewLoop(bool loop);
// Scene audio control (runtime)
bool playObjectSound(const SceneObject& obj);

View File

@@ -188,81 +188,82 @@ void applyModernTheme() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
ImVec4 slate = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
ImVec4 panel = ImVec4(0.11f, 0.12f, 0.14f, 1.00f);
ImVec4 overlay = ImVec4(0.07f, 0.08f, 0.10f, 0.98f);
ImVec4 accent = ImVec4(0.33f, 0.63f, 0.98f, 1.00f);
ImVec4 accentMuted = ImVec4(0.25f, 0.46f, 0.78f, 1.00f);
ImVec4 highlight = ImVec4(0.18f, 0.23f, 0.30f, 1.00f);
ImVec4 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f);
ImVec4 overlay = ImVec4(0.09f, 0.10f, 0.14f, 0.98f);
ImVec4 accent = ImVec4(0.58f, 0.34f, 0.78f, 1.00f);
ImVec4 accentMuted = ImVec4(0.46f, 0.30f, 0.68f, 1.00f);
ImVec4 highlight = ImVec4(0.20f, 0.22f, 0.30f, 1.00f);
colors[ImGuiCol_Text] = ImVec4(0.87f, 0.89f, 0.92f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.52f, 0.56f, 0.62f, 1.00f);
colors[ImGuiCol_Text] = ImVec4(0.90f, 0.91f, 0.94f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.56f, 0.60f, 0.66f, 1.00f);
colors[ImGuiCol_WindowBg] = panel;
colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f);
colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.13f, 0.18f, 1.00f);
colors[ImGuiCol_ChildBg] = panel;
colors[ImGuiCol_PopupBg] = overlay;
colors[ImGuiCol_Border] = ImVec4(0.21f, 0.24f, 0.28f, 0.60f);
colors[ImGuiCol_Border] = ImVec4(0.20f, 0.22f, 0.30f, 0.70f);
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.09f, 0.10f, 0.14f, 1.00f);
colors[ImGuiCol_Header] = highlight;
colors[ImGuiCol_HeaderHovered] = ImVec4(0.22f, 0.28f, 0.36f, 1.00f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.44f, 0.72f, 1.00f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.28f, 0.38f, 1.00f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.44f, 0.30f, 0.68f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.17f, 0.19f, 0.22f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.23f, 0.27f, 0.33f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.36f, 0.46f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.24f, 0.26f, 0.34f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.32f, 0.36f, 0.48f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.40f, 0.28f, 0.60f, 1.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.32f, 0.41f, 1.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.19f, 0.26f, 1.00f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.26f, 0.36f, 1.00f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.26f, 0.44f, 1.00f);
colors[ImGuiCol_TitleBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.12f, 0.14f, 1.00f);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.04f, 0.05f, 0.06f, 1.00f);
colors[ImGuiCol_TitleBg] = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.14f, 0.15f, 0.21f, 1.00f);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.08f, 0.09f, 0.13f, 1.00f);
colors[ImGuiCol_Tab] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f);
colors[ImGuiCol_TabHovered] = ImVec4(0.20f, 0.28f, 0.38f, 1.00f);
colors[ImGuiCol_TabActive] = ImVec4(0.16f, 0.19f, 0.23f, 1.00f);
colors[ImGuiCol_TabUnfocused] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.13f, 0.15f, 0.18f, 1.00f);
colors[ImGuiCol_Tab] = ImVec4(0.13f, 0.14f, 0.20f, 1.00f);
colors[ImGuiCol_TabHovered] = ImVec4(0.30f, 0.26f, 0.42f, 1.00f);
colors[ImGuiCol_TabActive] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f);
colors[ImGuiCol_TabUnfocused] = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.15f, 0.16f, 0.22f, 1.00f);
colors[ImGuiCol_Separator] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.24f, 0.32f, 0.42f, 1.00f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.44f, 0.60f, 1.00f);
colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.30f, 1.00f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.28f, 0.40f, 1.00f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.50f, 0.32f, 0.70f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.07f, 0.08f, 0.10f, 1.00f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.21f, 0.23f, 0.27f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.26f, 0.30f, 0.36f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.30f, 0.36f, 0.44f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.15f, 1.00f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.22f, 0.24f, 0.32f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.32f, 0.44f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.40f, 0.32f, 0.54f, 1.00f);
colors[ImGuiCol_CheckMark] = accent;
colors[ImGuiCol_SliderGrab] = accent;
colors[ImGuiCol_SliderGrabActive] = accentMuted;
colors[ImGuiCol_ResizeGrip] = ImVec4(0.22f, 0.26f, 0.33f, 1.00f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.32f, 0.46f, 0.62f, 0.80f);
colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.28f, 0.38f, 1.00f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.42f, 0.32f, 0.62f, 0.80f);
colors[ImGuiCol_ResizeGripActive] = accent;
colors[ImGuiCol_DockingPreview] = ImVec4(0.33f, 0.63f, 0.98f, 0.60f);
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.05f, 0.06f, 0.07f, 1.00f);
colors[ImGuiCol_DockingPreview] = ImVec4(0.58f, 0.34f, 0.78f, 0.55f);
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.08f, 0.09f, 0.13f, 1.00f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.33f, 0.63f, 0.98f, 0.25f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.58f, 0.34f, 0.78f, 0.25f);
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_TableHeaderBg] = ImVec4(0.15f, 0.17f, 0.20f, 1.00f);
colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f);
style.WindowRounding = 5.0f;
style.ChildRounding = 5.0f;
style.FrameRounding = 6.0f;
style.PopupRounding = 6.0f;
style.WindowRounding = 10.0f;
style.ChildRounding = 12.0f;
style.FrameRounding = 10.0f;
style.PopupRounding = 12.0f;
style.ScrollbarRounding = 10.0f;
style.GrabRounding = 6.0f;
style.TabRounding = 6.0f;
style.GrabRounding = 8.0f;
style.TabRounding = 10.0f;
style.WindowPadding = ImVec2(10.0f, 10.0f);
style.FramePadding = ImVec2(9.0f, 5.0f);
style.ItemSpacing = ImVec2(10.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.WindowPadding = ImVec2(12.0f, 12.0f);
style.FramePadding = ImVec2(10.0f, 6.0f);
style.ItemSpacing = ImVec2(10.0f, 8.0f);
style.ItemInnerSpacing = ImVec2(8.0f, 6.0f);
style.IndentSpacing = 18.0f;
style.WindowBorderSize = 1.0f;

View File

@@ -21,268 +21,367 @@
#endif
namespace FileIcons {
namespace {
ImU32 BlendColor(ImU32 a, ImU32 b, float t) {
int ar = a & 0xFF;
int ag = (a >> 8) & 0xFF;
int ab = (a >> 16) & 0xFF;
int aa = (a >> 24) & 0xFF;
int br = b & 0xFF;
int bg = (b >> 8) & 0xFF;
int bb = (b >> 16) & 0xFF;
int ba = (b >> 24) & 0xFF;
int r = ar + static_cast<int>((br - ar) * t);
int g = ag + static_cast<int>((bg - ag) * t);
int bch = ab + static_cast<int>((bb - ab) * t);
int aout = aa + static_cast<int>((ba - aa) * t);
return IM_COL32(r, g, bch, aout);
}
void DrawPaperSpeckles(ImDrawList* drawList, ImVec2 min, ImVec2 max, ImU32 color) {
const ImVec2 points[] = {
ImVec2(0.18f, 0.22f), ImVec2(0.42f, 0.36f), ImVec2(0.66f, 0.28f),
ImVec2(0.28f, 0.62f), ImVec2(0.52f, 0.68f), ImVec2(0.74f, 0.58f),
ImVec2(0.36f, 0.82f), ImVec2(0.82f, 0.78f)
};
float w = max.x - min.x;
float h = max.y - min.y;
float r = std::min(w, h) * 0.015f;
for (const auto& p : points) {
ImVec2 dot(min.x + w * p.x, min.y + h * p.y);
drawList->AddCircleFilled(dot, r, color);
}
}
struct PaperFrame {
ImVec2 min;
ImVec2 max;
};
PaperFrame DrawPaperFileBase(ImDrawList* drawList, ImVec2 pos, float size, ImU32 accentColor) {
const ImU32 kPaperBase = IM_COL32(205, 185, 128, 255);
const ImU32 kPaperEdge = IM_COL32(122, 106, 72, 200);
const ImU32 kPaperFold = IM_COL32(223, 206, 150, 230);
const ImU32 kPaperShadow = IM_COL32(110, 95, 60, 80);
const ImU32 kPaperSpeck = IM_COL32(120, 110, 80, 55);
float w = size * 0.78f;
float h = size * 0.95f;
float offsetX = (size - w) * 0.5f;
float offsetY = (size - h) * 0.5f;
float cornerSize = w * 0.22f;
float rounding = size * 0.08f;
float shadowOffset = size * 0.04f;
ImVec2 min = ImVec2(pos.x + offsetX, pos.y + offsetY);
ImVec2 max = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h);
drawList->AddRectFilled(ImVec2(min.x + shadowOffset, min.y + shadowOffset),
ImVec2(max.x + shadowOffset, max.y + shadowOffset),
kPaperShadow, rounding);
drawList->AddRectFilled(min, max, kPaperBase, rounding);
drawList->AddRect(min, max, kPaperEdge, rounding, 0, 1.2f);
ImVec2 foldA(max.x - cornerSize, min.y);
ImVec2 foldB(max.x, min.y + cornerSize);
drawList->AddTriangleFilled(foldA, foldB, ImVec2(max.x - cornerSize, min.y + cornerSize), kPaperFold);
ImU32 band = BlendColor(accentColor, kPaperBase, 0.65f);
float bandH = h * 0.14f;
drawList->AddRectFilled(ImVec2(min.x, max.y - bandH), max, band, rounding);
DrawPaperSpeckles(drawList, min, ImVec2(max.x, max.y - bandH), kPaperSpeck);
ImVec2 contentMin(min.x + w * 0.12f, min.y + h * 0.16f);
ImVec2 contentMax(max.x - w * 0.12f, max.y - h * 0.22f);
return {contentMin, contentMax};
}
PaperFrame DrawSheetFileBase(ImDrawList* drawList, ImVec2 pos, float size, ImU32 accentColor) {
const ImU32 kSheetBase = IM_COL32(248, 248, 248, 255);
const ImU32 kSheetBack = IM_COL32(238, 238, 238, 235);
const ImU32 kSheetEdge = IM_COL32(185, 185, 185, 200);
const ImU32 kSheetFold = IM_COL32(232, 232, 232, 255);
const ImU32 kSheetShadow = IM_COL32(0, 0, 0, 55);
const ImU32 kSheetInner = IM_COL32(255, 255, 255, 80);
const ImU32 kFoldShadow = IM_COL32(0, 0, 0, 35);
float w = size * 0.78f;
float h = size * 0.95f;
float offsetX = (size - w) * 0.5f;
float offsetY = (size - h) * 0.5f;
float cornerSize = w * 0.24f;
float rounding = size * 0.075f;
float shadowOffset = size * 0.04f;
float backOffset = size * 0.02f;
ImVec2 min = ImVec2(pos.x + offsetX, pos.y + offsetY);
ImVec2 max = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h);
ImVec2 backMin = ImVec2(min.x + backOffset, min.y + backOffset * 0.6f);
ImVec2 backMax = ImVec2(max.x + backOffset, max.y + backOffset * 0.6f);
drawList->AddRectFilled(backMin, backMax, kSheetBack, rounding);
drawList->AddRectFilled(ImVec2(min.x + shadowOffset, min.y + shadowOffset),
ImVec2(max.x + shadowOffset, max.y + shadowOffset),
kSheetShadow, rounding);
drawList->AddRectFilled(min, max, kSheetBase, rounding);
drawList->AddRect(min, max, kSheetEdge, rounding, 0, 1.2f);
drawList->AddRect(ImVec2(min.x + 1.0f, min.y + 1.0f),
ImVec2(max.x - 1.0f, max.y - 1.0f),
kSheetInner, rounding, 0, 1.0f);
ImVec2 foldA(max.x - cornerSize, min.y);
ImVec2 foldB(max.x, min.y + cornerSize);
ImVec2 foldC(max.x - cornerSize, min.y + cornerSize);
drawList->AddTriangleFilled(ImVec2(foldA.x + 1.0f, foldA.y + 1.0f),
ImVec2(foldB.x + 1.0f, foldB.y + 1.0f),
ImVec2(foldC.x + 1.0f, foldC.y + 1.0f),
kFoldShadow);
drawList->AddTriangleFilled(foldA, foldB, ImVec2(max.x - cornerSize, min.y + cornerSize), kSheetFold);
drawList->AddTriangle(foldA, foldB, foldC, kSheetEdge, 1.0f);
ImU32 band = BlendColor(accentColor, kSheetBase, 0.55f);
float bandH = h * 0.16f;
drawList->AddRectFilled(ImVec2(min.x, max.y - bandH), max, band, rounding);
ImVec2 contentMin(min.x + w * 0.12f, min.y + h * 0.16f);
ImVec2 contentMax(max.x - w * 0.12f, max.y - h * 0.24f);
return {contentMin, contentMax};
}
ImU32 InkColor() {
return IM_COL32(92, 78, 52, 220);
}
}
// Draw a folder icon
void DrawFolderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
const ImU32 kFolderBase = IM_COL32(198, 178, 120, 255);
const ImU32 kFolderTab = IM_COL32(215, 197, 140, 255);
const ImU32 kFolderEdge = IM_COL32(120, 104, 70, 200);
const ImU32 kFolderShadow = IM_COL32(110, 95, 60, 70);
const ImU32 kFolderSpeck = IM_COL32(120, 110, 80, 55);
float w = size;
float h = size * 0.75f;
float tabW = w * 0.4f;
float tabH = h * 0.15f;
// Folder body
drawList->AddRectFilled(
ImVec2(pos.x, pos.y + tabH),
ImVec2(pos.x + w, pos.y + h),
color, 3.0f
);
// Folder tab
drawList->AddRectFilled(
ImVec2(pos.x, pos.y),
ImVec2(pos.x + tabW, pos.y + tabH + 2),
color, 2.0f
);
float h = size * 0.72f;
float tabW = w * 0.46f;
float tabH = h * 0.28f;
float rounding = size * 0.08f;
float shadowOffset = size * 0.04f;
ImVec2 bodyMin(pos.x, pos.y + tabH * 0.55f);
ImVec2 bodyMax(pos.x + w, pos.y + h);
ImVec2 tabMin(pos.x + w * 0.06f, pos.y);
ImVec2 tabMax(pos.x + tabW, pos.y + tabH);
drawList->AddRectFilled(ImVec2(bodyMin.x + shadowOffset, bodyMin.y + shadowOffset),
ImVec2(bodyMax.x + shadowOffset, bodyMax.y + shadowOffset),
kFolderShadow, rounding);
drawList->AddRectFilled(bodyMin, bodyMax, kFolderBase, rounding);
drawList->AddRect(bodyMin, bodyMax, kFolderEdge, rounding, 0, 1.2f);
drawList->AddRectFilled(tabMin, tabMax, kFolderTab, rounding);
drawList->AddRect(tabMin, tabMax, kFolderEdge, rounding, 0, 1.0f);
ImU32 band = BlendColor(color, kFolderBase, 0.7f);
float bandH = h * 0.14f;
drawList->AddRectFilled(ImVec2(bodyMin.x, bodyMax.y - bandH), bodyMax, band, rounding);
DrawPaperSpeckles(drawList, bodyMin, ImVec2(bodyMax.x, bodyMax.y - bandH), kFolderSpeck);
}
// Draw a scene/document icon
void DrawSceneIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size * 0.8f;
float h = size;
float cornerSize = w * 0.25f;
// Main document body
ImVec2 p1 = ImVec2(pos.x, pos.y);
ImVec2 p2 = ImVec2(pos.x + w - cornerSize, pos.y);
ImVec2 p3 = ImVec2(pos.x + w, pos.y + cornerSize);
ImVec2 p4 = ImVec2(pos.x + w, pos.y + h);
ImVec2 p5 = ImVec2(pos.x, pos.y + h);
drawList->AddQuadFilled(p1, ImVec2(pos.x + w - cornerSize, pos.y), ImVec2(pos.x + w - cornerSize, pos.y + h), p5, color);
drawList->AddTriangleFilled(p2, p3, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), color);
drawList->AddRectFilled(ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p4, color);
// Corner fold
drawList->AddTriangleFilled(p2, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p3,
IM_COL32(255, 255, 255, 60));
// Scene icon indicator (play triangle)
float cx = pos.x + w * 0.5f;
float cy = pos.y + h * 0.55f;
float triSize = size * 0.25f;
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
float w = frame.max.x - frame.min.x;
float h = frame.max.y - frame.min.y;
ImVec2 center(frame.min.x + w * 0.55f, frame.min.y + h * 0.55f);
float tri = std::min(w, h) * 0.35f;
drawList->AddTriangleFilled(
ImVec2(cx - triSize * 0.4f, cy - triSize * 0.5f),
ImVec2(cx - triSize * 0.4f, cy + triSize * 0.5f),
ImVec2(cx + triSize * 0.5f, cy),
IM_COL32(255, 255, 255, 180)
ImVec2(center.x - tri * 0.4f, center.y - tri * 0.55f),
ImVec2(center.x - tri * 0.4f, center.y + tri * 0.55f),
ImVec2(center.x + tri * 0.6f, center.y),
ink
);
}
// Draw a 3D model icon (cube wireframe)
void DrawModelIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float s = size * 0.8f;
float offset = size * 0.1f;
float depth = s * 0.3f;
// Front face
ImVec2 f1 = ImVec2(pos.x + offset, pos.y + offset + depth);
ImVec2 f2 = ImVec2(pos.x + offset + s, pos.y + offset + depth);
ImVec2 f3 = ImVec2(pos.x + offset + s, pos.y + offset + s);
ImVec2 f4 = ImVec2(pos.x + offset, pos.y + offset + s);
// Back face
ImVec2 b1 = ImVec2(f1.x + depth, f1.y - depth);
ImVec2 b2 = ImVec2(f2.x + depth, f2.y - depth);
ImVec2 b3 = ImVec2(f3.x + depth, f3.y - depth);
// Fill front face
drawList->AddQuadFilled(f1, f2, f3, f4, color);
// Fill top face
drawList->AddQuadFilled(f1, f2, b2, b1, IM_COL32(
(color & 0xFF) * 0.7f,
((color >> 8) & 0xFF) * 0.7f,
((color >> 16) & 0xFF) * 0.7f,
(color >> 24) & 0xFF
));
// Fill right face
drawList->AddQuadFilled(f2, b2, b3, f3, IM_COL32(
(color & 0xFF) * 0.5f,
((color >> 8) & 0xFF) * 0.5f,
((color >> 16) & 0xFF) * 0.5f,
(color >> 24) & 0xFF
));
// Edges
ImU32 edgeColor = IM_COL32(255, 255, 255, 100);
drawList->AddLine(f1, f2, edgeColor, 1.0f);
drawList->AddLine(f2, f3, edgeColor, 1.0f);
drawList->AddLine(f3, f4, edgeColor, 1.0f);
drawList->AddLine(f4, f1, edgeColor, 1.0f);
drawList->AddLine(f1, b1, edgeColor, 1.0f);
drawList->AddLine(f2, b2, edgeColor, 1.0f);
drawList->AddLine(b1, b2, edgeColor, 1.0f);
drawList->AddLine(f3, b3, edgeColor, 1.0f);
drawList->AddLine(b2, b3, edgeColor, 1.0f);
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
float s = std::min(w, h) * 0.6f;
ImVec2 origin(min.x + w * 0.25f, min.y + h * 0.25f);
float depth = s * 0.35f;
ImVec2 f1(origin.x, origin.y + depth);
ImVec2 f2(origin.x + s, origin.y + depth);
ImVec2 f3(origin.x + s, origin.y + s + depth);
ImVec2 f4(origin.x, origin.y + s + depth);
ImVec2 b1(f1.x + depth, f1.y - depth);
ImVec2 b2(f2.x + depth, f2.y - depth);
ImVec2 b3(f3.x + depth, f3.y - depth);
ImVec2 b4(f4.x + depth, f4.y - depth);
drawList->AddLine(f1, f2, ink, 1.4f);
drawList->AddLine(f2, f3, ink, 1.4f);
drawList->AddLine(f3, f4, ink, 1.4f);
drawList->AddLine(f4, f1, ink, 1.4f);
drawList->AddLine(b1, b2, ink, 1.2f);
drawList->AddLine(b2, b3, ink, 1.2f);
drawList->AddLine(b3, b4, ink, 1.2f);
drawList->AddLine(b4, b1, ink, 1.2f);
drawList->AddLine(f1, b1, ink, 1.2f);
drawList->AddLine(f2, b2, ink, 1.2f);
drawList->AddLine(f3, b3, ink, 1.2f);
drawList->AddLine(f4, b4, ink, 1.2f);
}
// Draw a texture/image icon
void DrawTextureIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.1f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Frame
drawList->AddRectFilled(tl, br, color, 2.0f);
// Mountain landscape
float midY = pos.y + size * 0.6f;
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
ImVec2 frameMin(min.x + w * 0.08f, min.y + h * 0.12f);
ImVec2 frameMax(max.x - w * 0.08f, max.y - h * 0.1f);
drawList->AddRect(frameMin, frameMax, ink, 3.0f, 0, 1.2f);
float midY = frameMax.y - h * 0.28f;
drawList->AddTriangleFilled(
ImVec2(pos.x + size * 0.2f, br.y - padding),
ImVec2(pos.x + size * 0.45f, midY),
ImVec2(pos.x + size * 0.7f, br.y - padding),
IM_COL32(60, 60, 60, 255)
ImVec2(frameMin.x + w * 0.1f, frameMax.y),
ImVec2(frameMin.x + w * 0.35f, midY),
ImVec2(frameMin.x + w * 0.6f, frameMax.y),
ink
);
drawList->AddTriangleFilled(
ImVec2(pos.x + size * 0.5f, br.y - padding),
ImVec2(pos.x + size * 0.7f, midY + size * 0.1f),
ImVec2(pos.x + size * 0.9f, br.y - padding),
IM_COL32(80, 80, 80, 255)
ImVec2(frameMin.x + w * 0.45f, frameMax.y),
ImVec2(frameMin.x + w * 0.7f, midY + h * 0.1f),
ImVec2(frameMin.x + w * 0.9f, frameMax.y),
ink
);
// Sun
float sunR = size * 0.1f;
drawList->AddCircleFilled(ImVec2(pos.x + size * 0.75f, pos.y + size * 0.35f), sunR, IM_COL32(255, 220, 100, 255));
float sunR = std::min(w, h) * 0.12f;
drawList->AddCircleFilled(ImVec2(frameMax.x - w * 0.18f, frameMin.y + h * 0.2f), sunR, ink);
}
// Draw a shader icon (code brackets)
void DrawShaderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.15f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Background
drawList->AddRectFilled(tl, br, color, 3.0f);
// Code lines
ImU32 lineColor = IM_COL32(255, 255, 255, 180);
float lineY = pos.y + size * 0.35f;
float lineH = size * 0.08f;
float lineSpacing = size * 0.15f;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.7f, lineY + lineH), lineColor);
lineY += lineSpacing;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.3f, lineY), ImVec2(pos.x + size * 0.8f, lineY + lineH), lineColor);
lineY += lineSpacing;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.55f, lineY + lineH), lineColor);
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
float lineH = h * 0.12f;
float lineY = min.y + h * 0.2f;
drawList->AddRectFilled(ImVec2(min.x + w * 0.15f, lineY), ImVec2(min.x + w * 0.75f, lineY + lineH), ink);
lineY += h * 0.22f;
drawList->AddRectFilled(ImVec2(min.x + w * 0.2f, lineY), ImVec2(min.x + w * 0.85f, lineY + lineH), ink);
lineY += h * 0.22f;
drawList->AddRectFilled(ImVec2(min.x + w * 0.15f, lineY), ImVec2(min.x + w * 0.55f, lineY + lineH), ink);
}
// Draw an audio icon (speaker/waveform)
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
// Speaker body
float spkW = size * 0.25f;
float spkH = size * 0.3f;
float cx = pos.x + size * 0.35f;
float cy = pos.y + size * 0.5f;
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
float spkW = w * 0.25f;
float spkH = h * 0.28f;
float cx = min.x + w * 0.35f;
float cy = min.y + h * 0.55f;
drawList->AddRectFilled(
ImVec2(cx - spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
color
ink
);
// Speaker cone
drawList->AddTriangleFilled(
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
ImVec2(cx + spkW * 1.2f, cy + spkH),
color
ImVec2(cx + spkW * 1.1f, cy + spkH * 0.9f),
ink
);
drawList->AddTriangleFilled(
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 1.2f, cy - spkH),
ImVec2(cx + spkW * 1.2f, cy + spkH),
color
ImVec2(cx + spkW * 1.1f, cy - spkH * 0.9f),
ImVec2(cx + spkW * 1.1f, cy + spkH * 0.9f),
ink
);
// Sound waves
ImU32 waveColor = IM_COL32(255, 255, 255, 150);
float waveX = cx + spkW * 1.5f;
float waveX = cx + spkW * 1.4f;
drawList->AddBezierQuadratic(
ImVec2(waveX, cy - size * 0.15f),
ImVec2(waveX + size * 0.1f, cy),
ImVec2(waveX, cy + size * 0.15f),
waveColor, 2.0f
ImVec2(waveX, cy - h * 0.12f),
ImVec2(waveX + w * 0.12f, cy),
ImVec2(waveX, cy + h * 0.12f),
ink, 1.6f
);
waveX += size * 0.12f;
waveX += w * 0.12f;
drawList->AddBezierQuadratic(
ImVec2(waveX, cy - size * 0.22f),
ImVec2(waveX + size * 0.12f, cy),
ImVec2(waveX, cy + size * 0.22f),
waveColor, 2.0f
ImVec2(waveX, cy - h * 0.2f),
ImVec2(waveX + w * 0.14f, cy),
ImVec2(waveX, cy + h * 0.2f),
ink, 1.6f
);
}
// Draw a generic file icon
void DrawFileIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size * 0.7f;
float h = size * 0.9f;
float offsetX = (size - w) * 0.5f;
float offsetY = (size - h) * 0.5f;
float cornerSize = w * 0.25f;
ImVec2 p1 = ImVec2(pos.x + offsetX, pos.y + offsetY);
ImVec2 p2 = ImVec2(pos.x + offsetX + w - cornerSize, pos.y + offsetY);
ImVec2 p3 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + cornerSize);
ImVec2 p4 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h);
ImVec2 p5 = ImVec2(pos.x + offsetX, pos.y + offsetY + h);
// Main body
drawList->AddQuadFilled(p1, p2, ImVec2(p2.x, p4.y), p5, color);
drawList->AddTriangleFilled(p2, p3, ImVec2(p2.x, p3.y), color);
drawList->AddRectFilled(ImVec2(p2.x, p3.y), p4, color);
// Corner fold
drawList->AddTriangleFilled(p2, ImVec2(p2.x, p3.y), p3, IM_COL32(255, 255, 255, 50));
DrawSheetFileBase(drawList, pos, size, color);
}
// Draw a script/code icon
void DrawScriptIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.12f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Background
drawList->AddRectFilled(tl, br, color, 3.0f);
// Brackets < >
ImU32 bracketColor = IM_COL32(255, 255, 255, 200);
float cx = pos.x + size * 0.5f;
float cy = pos.y + size * 0.5f;
float bSize = size * 0.2f;
// Left bracket <
drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.5f, cy), bracketColor, 2.5f);
drawList->AddLine(ImVec2(cx - bSize * 1.5f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
// Right bracket >
drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.5f, cy), bracketColor, 2.5f);
drawList->AddLine(ImVec2(cx + bSize * 1.5f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
float cx = min.x + w * 0.5f;
float cy = min.y + h * 0.5f;
float bSize = std::min(w, h) * 0.28f;
drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.3f, cy), ink, 1.8f);
drawList->AddLine(ImVec2(cx - bSize * 1.3f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), ink, 1.8f);
drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.3f, cy), ink, 1.8f);
drawList->AddLine(ImVec2(cx + bSize * 1.3f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), ink, 1.8f);
}
// Draw a text icon
void DrawTextIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
DrawFileIcon(drawList, pos, size, color);
// Text lines
ImU32 lineColor = IM_COL32(255, 255, 255, 150);
float startX = pos.x + size * 0.25f;
float endX = pos.x + size * 0.65f;
float lineY = pos.y + size * 0.4f;
float lineH = size * 0.06f;
float spacing = size * 0.12f;
for (int i = 0; i < 3; i++) {
float w = (i == 1) ? (endX - startX) * 0.7f : (endX - startX);
drawList->AddRectFilled(ImVec2(startX, lineY), ImVec2(startX + w, lineY + lineH), lineColor);
PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color);
ImU32 ink = IM_COL32(90, 90, 90, 230);
ImVec2 min = frame.min;
ImVec2 max = frame.max;
float w = max.x - min.x;
float h = max.y - min.y;
float lineY = min.y + h * 0.25f;
float lineH = h * 0.12f;
float spacing = h * 0.2f;
for (int i = 0; i < 3; ++i) {
float lineW = (i == 1) ? w * 0.55f : w * 0.75f;
drawList->AddRectFilled(ImVec2(min.x + w * 0.12f, lineY),
ImVec2(min.x + w * 0.12f + lineW, lineY + lineH),
ink);
lineY += spacing;
}
}
@@ -647,23 +746,7 @@ void Engine::renderFileBrowserPanel() {
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
ImGui::SameLine();
if (ImGui::Button("Refresh", ImVec2(68, 0))) {
fileBrowser.needsRefresh = true;
}
ImGui::SameLine();
if (ImGui::Button("New Mat", ImVec2(78, 0))) {
fs::path target = fileBrowser.currentPath / "NewMaterial.mat";
int counter = 1;
while (fs::exists(target)) {
target = fileBrowser.currentPath / ("NewMaterial" + std::to_string(counter++) + ".mat");
}
SceneObject temp("Material", ObjectType::Cube, -1);
temp.materialPath = target.string();
saveMaterialToFile(temp);
fileBrowser.needsRefresh = true;
}
ImGui::EndChild();
ImGui::PopStyleVar(2);

View File

@@ -125,9 +125,11 @@ void Engine::renderLauncher() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 24.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 18.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.09f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::SetNextWindowPos(ImVec2(0, 0));
@@ -143,27 +145,65 @@ void Engine::renderLauncher() {
if (ImGui::Begin("Launcher", nullptr, flags))
{
float leftPanelWidth = 280.0f;
const float leftPanelWidth = 300.0f;
const float heroHeight = 120.0f;
const ImVec4 bgTopLeft = ImVec4(0.10f, 0.11f, 0.16f, 1.0f);
const ImVec4 bgTopRight = ImVec4(0.15f, 0.16f, 0.22f, 1.0f);
const ImVec4 bgBottomRight = ImVec4(0.07f, 0.08f, 0.12f, 1.0f);
const ImVec4 bgBottomLeft = ImVec4(0.08f, 0.09f, 0.13f, 1.0f);
const ImVec4 cardBg = ImVec4(0.14f, 0.15f, 0.21f, 0.98f);
const ImVec4 cardOutline = ImVec4(0.20f, 0.22f, 0.30f, 0.70f);
const ImVec4 accent = ImVec4(0.91f, 0.42f, 0.78f, 1.0f);
const ImVec4 accentCool = ImVec4(0.42f, 0.72f, 0.96f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.06f, 0.06f, 0.07f, 1.0f));
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 windowPos = ImGui::GetWindowPos();
ImVec2 windowSize = ImGui::GetWindowSize();
drawList->AddRectFilledMultiColor(
windowPos,
ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y),
ImGui::GetColorU32(bgTopLeft),
ImGui::GetColorU32(bgTopRight),
ImGui::GetColorU32(bgBottomRight),
ImGui::GetColorU32(bgBottomLeft)
);
ImGui::BeginChild("LauncherHero", ImVec2(0, heroHeight), true,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground);
ImDrawList* heroDraw = ImGui::GetWindowDrawList();
ImVec2 heroPos = ImGui::GetWindowPos();
ImVec2 heroSize = ImGui::GetWindowSize();
heroDraw->AddRectFilled(heroPos, ImVec2(heroPos.x + heroSize.x, heroPos.y + heroSize.y),
ImGui::GetColorU32(cardBg), 18.0f);
heroDraw->AddRect(heroPos, ImVec2(heroPos.x + heroSize.x, heroPos.y + heroSize.y),
ImGui::GetColorU32(cardBg), 18.0f);
ImGui::SetCursorPos(ImVec2(28.0f, 24.0f));
ImGui::TextDisabled("Project Manager");
ImGui::SetWindowFontScale(1.4f);
ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity");
ImGui::SetWindowFontScale(1.0f);
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Debug Build V0.7.0");
ImGui::EndChild();
ImGui::Spacing();
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_ChildBg, cardBg);
ImGui::BeginChild("LauncherLeft", ImVec2(leftPanelWidth, 0), true);
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.45f, 0.72f, 0.95f, 1.0f), "MODULARITY");
ImGui::TextDisabled("Game Engine");
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "GET STARTED");
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "GET STARTED");
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.32f, 0.22f, 0.54f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.43f, 0.30f, 0.70f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.26f, 0.60f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.38f, 0.55f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.48f, 0.68f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.42f, 0.60f, 1.0f));
if (ImGui::Button("New Project", ImVec2(-1, 36.0f)))
if (ImGui::Button("New Project", ImVec2(-1, 40.0f)))
{
projectManager.showNewProjectDialog = true;
projectManager.errorMessage.clear();
@@ -186,7 +226,7 @@ void Engine::renderLauncher() {
ImGui::Spacing();
if (ImGui::Button("Open Project", ImVec2(-1, 36.0f)))
if (ImGui::Button("Open Project", ImVec2(-1, 40.0f)))
{
projectManager.showOpenProjectDialog = true;
projectManager.errorMessage.clear();
@@ -194,44 +234,48 @@ void Engine::renderLauncher() {
ImGui::PopStyleColor(3);
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "QUICK ACTIONS");
ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "QUICK ACTIONS");
ImGui::Spacing();
if (ImGui::Button("Documentation", ImVec2(-1, 30.0f)))
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.22f, 0.32f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.30f, 0.42f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.24f, 0.28f, 0.40f, 1.0f));
if (ImGui::Button("Documentation", ImVec2(-1, 34.0f)))
{
#ifdef _WIN32
system("start https://github.com");
system("start https://docs.shockinteractive.xyz");
#else
system("xdg-open https://github.com &");
system("xdg-open https://docs.shockinteractive.xyz &");
#endif
}
if (ImGui::Button("Exit", ImVec2(-1, 30.0f)))
if (ImGui::Button("Exit", ImVec2(-1, 34.0f)))
{
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
}
ImGui::PopStyleColor(3);
ImGui::EndChild();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.11f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, cardBg);
ImGui::BeginChild("LauncherRight", ImVec2(0, 0), true);
ImGui::PopStyleColor();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "RECENT PROJECTS");
ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "RECENT PROJECTS");
ImGui::Spacing();
if (projectManager.recentProjects.empty())
{
ImGui::Spacing();
ImGui::TextDisabled("No recent projects");
ImGui::TextDisabled("Create a new project to get started!");
ImGui::TextDisabled("There are no recent projects yet.\nCreate or open a project to get started.");
}
else
{
@@ -241,30 +285,12 @@ void Engine::renderLauncher() {
const auto& rp = projectManager.recentProjects[i];
ImGui::PushID(static_cast<int>(i));
char label[512];
std::snprintf(label, sizeof(label), "%s\n%s",
rp.name.c_str(), rp.path.c_str());
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f));
bool selected = ImGui::Selectable(
label,
false,
ImGuiSelectableFlags_AllowDoubleClick,
ImVec2(availWidth, 48.0f)
);
ImGui::PopStyleColor(3);
// Dummy to extend window bounds properly
ImGui::Dummy(ImVec2(0, 0));
if (selected || ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
OpenProjectPath(rp.path);
}
const ImVec2 cardSize(availWidth, 72.0f);
const ImVec2 cardPos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("RecentCard", cardSize);
bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked();
ImVec2 afterPos = ImGui::GetCursorScreenPos();
if (ImGui::BeginPopupContextItem("RecentProjectContext"))
{
@@ -287,6 +313,34 @@ void Engine::renderLauncher() {
ImGui::EndPopup();
}
ImU32 cardCol = ImGui::GetColorU32(hovered ? ImVec4(0.18f, 0.19f, 0.27f, 1.0f)
: ImVec4(0.16f, 0.17f, 0.24f, 1.0f));
ImDrawList* list = ImGui::GetWindowDrawList();
list->AddRectFilled(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y), cardCol, 14.0f);
list->AddRect(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y),
ImGui::GetColorU32(cardOutline), 14.0f);
ImVec2 textPos = ImVec2(cardPos.x + 16.0f, cardPos.y + 14.0f);
ImGui::SetCursorScreenPos(textPos);
ImGui::TextColored(ImVec4(0.92f, 0.93f, 0.96f, 1.0f), "%s", rp.name.c_str());
ImGui::SetCursorScreenPos(ImVec2(textPos.x, textPos.y + 22.0f));
ImGui::TextDisabled("%s", rp.path.c_str());
const float buttonWidth = 88.0f;
ImGui::SetCursorScreenPos(ImVec2(cardPos.x + cardSize.x - buttonWidth - 16.0f, cardPos.y + 20.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.24f, 0.28f, 0.40f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.32f, 0.38f, 0.55f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.30f, 0.35f, 0.50f, 1.0f));
bool openClicked = ImGui::Button("Open", ImVec2(buttonWidth, 30.0f));
ImGui::PopStyleColor(3);
if ((clicked && !openClicked) || openClicked)
{
OpenProjectPath(rp.path);
}
ImGui::SetCursorScreenPos(afterPos);
ImGui::PopID();
ImGui::Spacing();
}
@@ -303,7 +357,7 @@ void Engine::renderLauncher() {
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(3);
ImGui::PopStyleVar(5);
if (projectManager.showNewProjectDialog)
renderNewProjectDialog();

View File

@@ -18,6 +18,83 @@
#include <shlobj.h>
#endif
namespace {
ImU32 GetHierarchyTypeColor(ObjectType type) {
switch (type) {
case ObjectType::Camera: return IM_COL32(110, 175, 235, 220);
case ObjectType::DirectionalLight:
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220);
case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220);
case ObjectType::OBJMesh:
case ObjectType::Model: return IM_COL32(120, 200, 150, 220);
case ObjectType::Mirror: return IM_COL32(180, 200, 210, 220);
case ObjectType::Plane: return IM_COL32(170, 180, 190, 220);
case ObjectType::Torus: return IM_COL32(155, 215, 180, 220);
default: return IM_COL32(140, 190, 235, 220);
}
}
void DrawFileOutlineIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size * 0.78f;
float h = size * 0.95f;
float offsetX = (size - w) * 0.5f;
float offsetY = (size - h) * 0.5f;
float corner = w * 0.28f;
ImVec2 min(pos.x + offsetX, pos.y + offsetY);
ImVec2 max(pos.x + offsetX + w, pos.y + offsetY + h);
ImVec2 foldA(max.x - corner, min.y);
ImVec2 foldB(max.x, min.y + corner);
ImVec2 foldC(max.x - corner, min.y + corner);
drawList->AddRect(min, max, color, size * 0.12f, 0, 1.2f);
drawList->AddTriangle(foldA, foldB, foldC, color, 1.2f);
drawList->AddLine(ImVec2(foldA.x, foldA.y), ImVec2(foldC.x, foldC.y), color, 1.2f);
}
void DrawCubeOutlineIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float inset = size * 0.18f;
float backOffset = size * 0.16f;
ImVec2 frontMin(pos.x + inset, pos.y + inset + backOffset);
ImVec2 frontMax(pos.x + size - inset, pos.y + size - inset + backOffset);
ImVec2 backMin(pos.x + inset + backOffset, pos.y + inset);
ImVec2 backMax(pos.x + size - inset + backOffset, pos.y + size - inset);
drawList->AddRect(frontMin, frontMax, color, 0.0f, 0, 1.2f);
drawList->AddRect(backMin, backMax, color, 0.0f, 0, 1.2f);
drawList->AddLine(frontMin, backMin, color, 1.2f);
drawList->AddLine(ImVec2(frontMax.x, frontMin.y), ImVec2(backMax.x, backMin.y), color, 1.2f);
drawList->AddLine(ImVec2(frontMin.x, frontMax.y), ImVec2(backMin.x, backMax.y), color, 1.2f);
drawList->AddLine(frontMax, backMax, color, 1.2f);
}
void DrawHierarchyLines(ImDrawList* drawList, const ImVec2& itemMin, const ImVec2& itemMax,
const std::vector<bool>& ancestorHasNext, int depth, bool isLast) {
if (depth <= 0) {
return;
}
ImGuiStyle& style = ImGui::GetStyle();
ImVec4 base = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
ImU32 lineColor = ImGui::ColorConvertFloat4ToU32(ImVec4(base.x, base.y, base.z, 0.6f));
float indent = style.IndentSpacing;
float rowTop = itemMin.y;
float rowBottom = itemMax.y;
float rowMid = (rowTop + rowBottom) * 0.5f;
float baseX = itemMin.x - indent * depth;
for (int i = 0; i < depth && i < static_cast<int>(ancestorHasNext.size()); ++i) {
if (ancestorHasNext[i]) {
float x = baseX + indent * (i + 0.5f);
drawList->AddLine(ImVec2(x, rowTop), ImVec2(x, rowBottom), lineColor, 1.0f);
}
}
float connectorX = baseX + indent * (depth - 0.5f);
float vertEnd = isLast ? rowMid : rowBottom;
drawList->AddLine(ImVec2(connectorX, rowTop), ImVec2(connectorX, vertEnd), lineColor, 1.0f);
drawList->AddLine(ImVec2(connectorX, rowMid), ImVec2(itemMin.x + 6.0f, rowMid), lineColor, 1.0f);
}
}
void Engine::renderHierarchyPanel() {
ImGui::Begin("Hierarchy", &showHierarchy);
@@ -96,11 +173,18 @@ void Engine::renderHierarchyPanel() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 2.0f));
ImGui::BeginChild("HierarchyList", ImVec2(0, 0), true);
std::vector<size_t> rootIndices;
rootIndices.reserve(sceneObjects.size());
for (size_t i = 0; i < sceneObjects.size(); i++) {
if (sceneObjects[i].parentId != -1)
continue;
if (sceneObjects[i].parentId == -1) {
rootIndices.push_back(i);
}
}
renderObjectNode(sceneObjects[i], filter);
std::vector<bool> ancestorHasNext;
for (size_t i = 0; i < rootIndices.size(); ++i) {
bool isLastRoot = (i + 1 == rootIndices.size());
renderObjectNode(sceneObjects[rootIndices[i]], filter, ancestorHasNext, isLastRoot, 0);
}
if (ImGui::BeginPopupContextWindow("HierarchyBackground",
@@ -115,6 +199,8 @@ void Engine::renderHierarchyPanel() {
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("Plane")) addObject(ObjectType::Plane, "Plane");
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
ImGui::EndMenu();
}
@@ -149,7 +235,8 @@ void Engine::renderHierarchyPanel() {
ImGui::End();
}
void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
std::vector<bool>& ancestorHasNext, bool isLast, int depth) {
std::string nameLower = obj.name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
@@ -164,23 +251,23 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf;
const char* icon = "";
switch (obj.type) {
case ObjectType::Cube: icon = "[#]"; break;
case ObjectType::Sphere: icon = "(O)"; break;
case ObjectType::Capsule: icon = "[|]"; break;
case ObjectType::OBJMesh: icon = "[M]"; break;
case ObjectType::Model: icon = "[A]"; break;
case ObjectType::Camera: icon = "(C)"; break;
case ObjectType::DirectionalLight: icon = "(D)"; break;
case ObjectType::PointLight: icon = "(P)"; break;
case ObjectType::SpotLight: icon = "(S)"; break;
case ObjectType::AreaLight: icon = "(L)"; break;
case ObjectType::PostFXNode: icon = "(FX)"; break;
case ObjectType::Mirror: icon = "[R]"; break;
}
std::string label = " " + obj.name;
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s", label.c_str());
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
ImVec2 itemMin = ImGui::GetItemRectMin();
ImVec2 itemMax = ImGui::GetItemRectMax();
DrawHierarchyLines(ImGui::GetWindowDrawList(), itemMin, itemMax, ancestorHasNext, depth, isLast);
float lineHeight = itemMax.y - itemMin.y;
float iconSize = std::max(8.0f, lineHeight - 6.0f);
float labelStart = itemMin.x + ImGui::GetTreeNodeToLabelSpacing();
ImVec2 iconPos(labelStart, itemMin.y + (lineHeight - iconSize) * 0.5f);
ImU32 iconColor = GetHierarchyTypeColor(obj.type);
if (obj.parentId == -1) {
DrawCubeOutlineIcon(ImGui::GetWindowDrawList(), iconPos, iconSize, iconColor);
} else {
DrawFileOutlineIcon(ImGui::GetWindowDrawList(), iconPos, iconSize, iconColor);
}
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
@@ -290,13 +377,26 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) {
}
if (nodeOpen) {
std::vector<SceneObject*> visibleChildren;
visibleChildren.reserve(obj.childIds.size());
for (int childId : obj.childIds) {
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[childId](const SceneObject& o) { return o.id == childId; });
if (it != sceneObjects.end()) {
renderObjectNode(*it, filter);
std::string childLower = it->name;
std::transform(childLower.begin(), childLower.end(), childLower.begin(), ::tolower);
if (filter.empty() || childLower.find(filter) != std::string::npos) {
visibleChildren.push_back(&(*it));
}
}
}
ancestorHasNext.push_back(!isLast);
for (size_t i = 0; i < visibleChildren.size(); ++i) {
bool childLast = (i + 1 == visibleChildren.size());
renderObjectNode(*visibleChildren[i], filter, ancestorHasNext, childLast, depth + 1);
}
ancestorHasNext.pop_back();
ImGui::TreePop();
}
}
@@ -348,8 +448,23 @@ void Engine::renderInspectorPanel() {
inspectedMaterialValid = false;
}
if (browserHasAudio) {
std::string selectedAudio = selectedAudioPath.string();
if (selectedAudio != audioPreviewSelectedPath) {
audioPreviewSelectedPath = selectedAudio;
if (audioPreviewAutoPlay) {
audio.playPreview(selectedAudio, 1.0f, audioPreviewLoop);
}
}
} else {
audioPreviewSelectedPath.clear();
}
auto drawWaveform = [&](const char* id, const AudioClipPreview* preview, const ImVec2& size, float progressRatio, float* seekRatioOut) {
if (!preview || preview->waveform.empty()) {
bool hasStereo = preview && preview->channels >= 2
&& !preview->waveformLeft.empty()
&& !preview->waveformRight.empty();
if (!preview || (!hasStereo && preview->waveform.empty())) {
ImGui::Dummy(size);
return;
}
@@ -360,14 +475,33 @@ void Engine::renderInspectorPanel() {
dl->AddRectFilled(start, end, IM_COL32(30, 35, 45, 180), 4.0f);
float midY = (start.y + end.y) * 0.5f;
float usableHeight = size.y * 0.45f;
size_t count = preview->waveform.size();
size_t count = hasStereo
? std::min(preview->waveformLeft.size(), preview->waveformRight.size())
: preview->waveform.size();
float step = count > 1 ? size.x / static_cast<float>(count - 1) : size.x;
ImU32 color = IM_COL32(255, 180, 100, 200);
for (size_t i = 0; i < count; ++i) {
float amp = std::clamp(preview->waveform[i], 0.0f, 1.0f);
float x = start.x + step * static_cast<float>(i);
float yOff = amp * usableHeight;
dl->AddLine(ImVec2(x, midY - yOff), ImVec2(x, midY + yOff), color, 1.2f);
if (hasStereo) {
ImU32 leftColor = IM_COL32(255, 190, 90, 200);
ImU32 rightColor = IM_COL32(100, 200, 255, 200);
float topMidY = start.y + size.y * 0.25f;
float bottomMidY = start.y + size.y * 0.75f;
float stereoHeight = size.y * 0.22f;
for (size_t i = 0; i < count; ++i) {
float leftAmp = std::clamp(preview->waveformLeft[i], 0.0f, 1.0f);
float rightAmp = std::clamp(preview->waveformRight[i], 0.0f, 1.0f);
float x = start.x + step * static_cast<float>(i);
float leftOff = leftAmp * stereoHeight;
float rightOff = rightAmp * stereoHeight;
dl->AddLine(ImVec2(x, topMidY - leftOff), ImVec2(x, topMidY + leftOff), leftColor, 1.2f);
dl->AddLine(ImVec2(x, bottomMidY - rightOff), ImVec2(x, bottomMidY + rightOff), rightColor, 1.2f);
}
} else {
ImU32 color = IM_COL32(255, 180, 100, 200);
for (size_t i = 0; i < count; ++i) {
float amp = std::clamp(preview->waveform[i], 0.0f, 1.0f);
float x = start.x + step * static_cast<float>(i);
float yOff = amp * usableHeight;
dl->AddLine(ImVec2(x, midY - yOff), ImVec2(x, midY + yOff), color, 1.2f);
}
}
if (progressRatio >= 0.0f && progressRatio <= 1.0f) {
@@ -648,7 +782,19 @@ void Engine::renderInspectorPanel() {
if (isPlayingPreview) {
audio.stopPreview();
} else {
audio.playPreview(selectedAudioPath.string());
audio.playPreview(selectedAudioPath.string(), 1.0f, audioPreviewLoop);
}
}
ImGui::SameLine();
if (ImGui::Checkbox("Loop##AudioPreview", &audioPreviewLoop)) {
if (isPlayingPreview) {
audio.setPreviewLoop(audioPreviewLoop);
}
}
ImGui::SameLine();
if (ImGui::Checkbox("Auto Play##AudioPreview", &audioPreviewAutoPlay)) {
if (audioPreviewAutoPlay && !selectedAudioPath.empty() && !isPlayingPreview) {
audio.playPreview(selectedAudioPath.string(), 1.0f, audioPreviewLoop);
}
}
@@ -787,6 +933,8 @@ void Engine::renderInspectorPanel() {
case ObjectType::AreaLight: typeLabel = "Area Light"; break;
case ObjectType::PostFXNode: typeLabel = "Post FX Node"; break;
case ObjectType::Mirror: typeLabel = "Mirror"; break;
case ObjectType::Plane: typeLabel = "Plane"; break;
case ObjectType::Torus: typeLabel = "Torus"; break;
}
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeLabel);
@@ -837,6 +985,7 @@ void Engine::renderInspectorPanel() {
ImGui::Text("Position");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Position", &obj.position.x, 0.1f)) {
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
@@ -847,6 +996,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Rotation", &obj.rotation.x, 1.0f, -360.0f, 360.0f)) {
obj.rotation = NormalizeEulerDegrees(obj.rotation);
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
@@ -856,6 +1006,7 @@ void Engine::renderInspectorPanel() {
ImGui::Text("Scale");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Scale", &obj.scale.x, 0.05f, 0.01f, 100.0f)) {
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
@@ -866,6 +1017,7 @@ void Engine::renderInspectorPanel() {
obj.position = glm::vec3(0.0f);
obj.rotation = glm::vec3(0.0f);
obj.scale = glm::vec3(1.0f);
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
}
@@ -1110,7 +1262,7 @@ void Engine::renderInspectorPanel() {
if (previewPlaying) {
audio.stopPreview();
} else if (!src.clipPath.empty()) {
audio.playPreview(src.clipPath, src.volume);
audio.playPreview(src.clipPath, src.volume, src.loop);
}
}
ImGui::SameLine();
@@ -1234,7 +1386,7 @@ void Engine::renderInspectorPanel() {
if (ImGui::SliderFloat("Threshold", &obj.postFx.bloomThreshold, 0.0f, 3.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Intensity", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) {
if (ImGui::SliderFloat("Intensity##Bloom", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Spread", &obj.postFx.bloomRadius, 0.5f, 3.5f, "%.2f")) {
@@ -1279,7 +1431,7 @@ void Engine::renderInspectorPanel() {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.vignetteEnabled);
if (ImGui::SliderFloat("Intensity", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) {
if (ImGui::SliderFloat("Intensity##Vignette", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Smoothness", &obj.postFx.vignetteSmoothness, 0.05f, 1.0f, "%.2f")) {
@@ -1912,6 +2064,7 @@ void Engine::renderInspectorPanel() {
obj.rigidbody.useGravity = true;
obj.rigidbody.isKinematic = false;
obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
syncLocalTransform(obj);
componentChanged = true;
}
if (!obj.hasAudioSource && ImGui::MenuItem("Audio Source")) {

View File

@@ -525,6 +525,9 @@ void Engine::renderMainMenuBar() {
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("Plane")) addObject(ObjectType::Plane, "Plane");
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
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");
@@ -1066,6 +1069,14 @@ void Engine::renderViewport() {
gizmoBoundsMin = glm::vec3(-0.35f, -0.9f, -0.35f);
gizmoBoundsMax = glm::vec3(0.35f, 0.9f, 0.35f);
break;
case ObjectType::Plane:
gizmoBoundsMin = glm::vec3(-0.5f, -0.5f, -0.02f);
gizmoBoundsMax = glm::vec3(0.5f, 0.5f, 0.02f);
break;
case ObjectType::Torus:
gizmoBoundsMin = glm::vec3(-0.5f);
gizmoBoundsMax = glm::vec3(0.5f);
break;
case ObjectType::OBJMesh: {
const auto* info = g_objLoader.getMeshInfo(selectedObj->meshId);
if (info && info->boundsMin.x < info->boundsMax.x) {
@@ -1167,12 +1178,33 @@ void Engine::renderViewport() {
o.position = t;
o.rotation = NormalizeEulerDegrees(glm::degrees(r));
o.scale = s;
syncLocalTransform(o);
};
if (selectedObjectIds.size() <= 1) {
applyDelta(*selectedObj);
} else {
std::unordered_set<int> selectedSet(selectedObjectIds.begin(), selectedObjectIds.end());
auto getParentId = [&](int id) -> int {
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[id](const SceneObject& o){ return o.id == id; });
if (it == sceneObjects.end()) return -1;
return it->parentId;
};
auto hasSelectedAncestor = [&](int id) -> bool {
int parentId = getParentId(id);
while (parentId != -1) {
if (selectedSet.count(parentId)) {
return true;
}
parentId = getParentId(parentId);
}
return false;
};
for (int id : selectedObjectIds) {
if (hasSelectedAncestor(id)) {
continue;
}
auto itObj = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[id](const SceneObject& o){ return o.id == id; });
if (itObj != sceneObjects.end()) {
@@ -1597,9 +1629,15 @@ void Engine::renderViewport() {
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::Plane:
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), 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::Torus:
hit = raySphere(localOrigin, localDir, 0.5f, hitT);
break;
case ObjectType::OBJMesh: {
const auto* info = g_objLoader.getMeshInfo(obj.meshId);
if (info && info->boundsMin.x < info->boundsMax.x) {

View File

@@ -2,6 +2,7 @@
#include "ModelLoader.h"
#include <iostream>
#include <fstream>
#include <functional>
#include <unordered_set>
#include <unordered_map>
@@ -153,6 +154,15 @@ glm::vec3 ExtractEulerXYZ(const glm::mat3& m) {
// GLM's extractEulerAngleXYZ returns (-T1, -T2, -T3)
return glm::vec3(-T1, -T2, -T3);
}
glm::quat QuatFromEulerXYZ(const glm::vec3& deg) {
glm::vec3 r = glm::radians(deg);
glm::mat4 m(1.0f);
m = glm::rotate(m, r.x, glm::vec3(1.0f, 0.0f, 0.0f));
m = glm::rotate(m, r.y, glm::vec3(0.0f, 1.0f, 0.0f));
m = glm::rotate(m, r.z, glm::vec3(0.0f, 0.0f, 1.0f));
return glm::quat_cast(glm::mat3(m));
}
}
void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) {
@@ -165,11 +175,27 @@ void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3&
if (scale.x != 0.0f) rotMat[0] /= scale.x;
if (scale.y != 0.0f) rotMat[1] /= scale.y;
if (scale.z != 0.0f) rotMat[2] /= scale.z;
// Orthonormalize to reduce shear-induced rotation jitter.
rotMat[0] = glm::normalize(rotMat[0]);
rotMat[1] = glm::normalize(rotMat[1] - rotMat[0] * glm::dot(rotMat[0], rotMat[1]));
rotMat[2] = glm::normalize(glm::cross(rotMat[0], rotMat[1]));
// Use explicit XYZ extraction so yaw isn't clamped to [-90, 90] like glm::yaw/pitch/roll.
rot = ExtractEulerXYZ(rotMat);
}
glm::mat4 Engine::ComposeTransform(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& scale) {
glm::mat4 m(1.0f);
m = glm::translate(m, position);
m *= glm::mat4_cast(rotation);
m = glm::scale(m, scale);
return m;
}
glm::mat4 Engine::ComposeTransform(const glm::vec3& position, const glm::vec3& rotationDeg, const glm::vec3& scale) {
return ComposeTransform(position, QuatFromEulerXYZ(rotationDeg), scale);
}
void Engine::recordState(const char* /*reason*/) {
SceneSnapshot snap;
snap.objects = sceneObjects;
@@ -327,11 +353,15 @@ void Engine::run() {
updatePlayerController(deltaTime);
}
updateHierarchyWorldTransforms();
bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode));
if (simulatePhysics) {
physics.simulate(deltaTime, sceneObjects);
}
updateHierarchyWorldTransforms();
bool audioShouldPlay = isPlaying || specMode || testMode;
Camera listenerCamera = camera;
for (const auto& obj : sceneObjects) {
@@ -901,6 +931,162 @@ void Engine::updatePlayerController(float delta) {
if (!physics.setLinearVelocity(player->id, velocity)) {
player->position += velocity * delta;
}
syncLocalTransform(*player);
}
void Engine::updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) {
auto safeDiv = [](float v, float d) { return (std::abs(d) > 1e-6f) ? (v / d) : 0.0f; };
auto unwrapNear = [](float angle, float reference) {
float result = angle;
while (result - reference > 180.0f) result -= 360.0f;
while (reference - result > 180.0f) result += 360.0f;
return result;
};
glm::quat invParent = glm::inverse(parentRot);
glm::vec3 localPos = invParent * (obj.position - parentPos);
localPos.x = safeDiv(localPos.x, parentScale.x);
localPos.y = safeDiv(localPos.y, parentScale.y);
localPos.z = safeDiv(localPos.z, parentScale.z);
glm::quat worldRot = QuatFromEulerXYZ(obj.rotation);
glm::quat localRot = invParent * worldRot;
glm::vec3 localRotDeg = glm::degrees(ExtractEulerXYZ(glm::mat3_cast(localRot)));
glm::vec3 refRot = obj.localInitialized ? obj.localRotation : obj.rotation;
localRotDeg.x = unwrapNear(localRotDeg.x, refRot.x);
localRotDeg.y = unwrapNear(localRotDeg.y, refRot.y);
localRotDeg.z = unwrapNear(localRotDeg.z, refRot.z);
glm::vec3 localScale(1.0f);
localScale.x = safeDiv(obj.scale.x, parentScale.x);
localScale.y = safeDiv(obj.scale.y, parentScale.y);
localScale.z = safeDiv(obj.scale.z, parentScale.z);
obj.localPosition = localPos;
obj.localRotation = localRotDeg;
obj.localScale = localScale;
obj.localInitialized = true;
}
void Engine::initializeLocalTransformsFromWorld(int sceneVersion) {
if (sceneObjects.empty()) return;
if (sceneVersion >= 10) {
for (auto& obj : sceneObjects) {
if (!obj.localInitialized) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
}
}
updateHierarchyWorldTransforms();
return;
}
std::unordered_map<int, glm::mat4> worldById;
worldById.reserve(sceneObjects.size());
for (const auto& obj : sceneObjects) {
worldById[obj.id] = ComposeTransform(obj.position, obj.rotation, obj.scale);
}
for (auto& obj : sceneObjects) {
if (obj.parentId == -1) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
continue;
}
auto itParent = worldById.find(obj.parentId);
if (itParent == worldById.end()) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
continue;
}
glm::vec3 pPos, pRotDeg, pScale;
DecomposeMatrix(itParent->second, pPos, pRotDeg, pScale);
updateLocalFromWorld(obj, pPos, QuatFromEulerXYZ(pRotDeg), pScale);
}
updateHierarchyWorldTransforms();
}
void Engine::updateHierarchyWorldTransforms() {
if (sceneObjects.empty()) return;
std::unordered_map<int, size_t> indexById;
indexById.reserve(sceneObjects.size());
for (size_t i = 0; i < sceneObjects.size(); ++i) {
indexById[sceneObjects[i].id] = i;
}
auto unwrapNear = [](float angle, float reference) {
float result = angle;
while (result - reference > 180.0f) result -= 360.0f;
while (reference - result > 180.0f) result += 360.0f;
return result;
};
std::unordered_set<int> visiting;
std::unordered_set<int> visited;
visiting.reserve(sceneObjects.size());
visited.reserve(sceneObjects.size());
std::function<void(int, const glm::vec3&, const glm::quat&, const glm::vec3&)> processNode =
[&](int id, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) {
if (visited.count(id)) return;
if (visiting.count(id)) return;
auto itIndex = indexById.find(id);
if (itIndex == indexById.end()) return;
visiting.insert(id);
SceneObject& obj = sceneObjects[itIndex->second];
if (!obj.localInitialized) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
}
bool useWorldAuthoritative = obj.hasRigidbody && obj.rigidbody.enabled && !obj.rigidbody.isKinematic;
glm::vec3 worldPos = obj.position;
glm::quat worldRot = QuatFromEulerXYZ(obj.rotation);
glm::vec3 worldScale = obj.scale;
if (useWorldAuthoritative) {
updateLocalFromWorld(obj, parentPos, parentRot, parentScale);
worldPos = obj.position;
worldRot = QuatFromEulerXYZ(obj.rotation);
worldScale = obj.scale;
} else {
glm::quat localRot = QuatFromEulerXYZ(obj.localRotation);
worldRot = parentRot * localRot;
worldScale = parentScale * obj.localScale;
worldPos = parentPos + parentRot * (parentScale * obj.localPosition);
glm::vec3 worldRotDeg = glm::degrees(ExtractEulerXYZ(glm::mat3_cast(worldRot)));
worldRotDeg.x = unwrapNear(worldRotDeg.x, obj.rotation.x);
worldRotDeg.y = unwrapNear(worldRotDeg.y, obj.rotation.y);
worldRotDeg.z = unwrapNear(worldRotDeg.z, obj.rotation.z);
obj.position = worldPos;
obj.rotation = worldRotDeg;
obj.scale = worldScale;
}
for (int childId : obj.childIds) {
processNode(childId, worldPos, worldRot, worldScale);
}
visiting.erase(id);
visited.insert(id);
};
for (const auto& obj : sceneObjects) {
if (obj.parentId == -1 || indexById.find(obj.parentId) == indexById.end()) {
processNode(obj.id, glm::vec3(0.0f), glm::quat(1.0f, 0.0f, 0.0f, 0.0f), glm::vec3(1.0f));
}
}
}
void Engine::OpenProjectPath(const std::string& path) {
try {
@@ -999,7 +1185,9 @@ void Engine::loadRecentScenes() {
fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName);
if (fs::exists(scenePath)) {
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
int sceneVersion = 9;
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) {
initializeLocalTransformsFromWorld(sceneVersion);
addConsoleMessage("Loaded scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success);
} else {
addConsoleMessage("Warning: Failed to load scene, starting fresh", ConsoleMessageType::Warning);
@@ -1037,7 +1225,9 @@ void Engine::loadScene(const std::string& sceneName) {
}
fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName);
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
int sceneVersion = 9;
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) {
initializeLocalTransformsFromWorld(sceneVersion);
undoStack.clear();
redoStack.clear();
projectManager.currentProject.currentSceneName = sceneName;
@@ -1112,7 +1302,13 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
sceneObjects.back().material.textureMix = 1.0f;
sceneObjects.back().material.color = glm::vec3(1.0f);
sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f);
} else if (type == ObjectType::Plane) {
sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f);
}
sceneObjects.back().localPosition = sceneObjects.back().position;
sceneObjects.back().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation);
sceneObjects.back().localScale = sceneObjects.back().scale;
sceneObjects.back().localInitialized = true;
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
@@ -1150,6 +1346,10 @@ void Engine::duplicateSelected() {
newObj.collider = it->collider;
newObj.hasPlayerController = it->hasPlayerController;
newObj.playerController = it->playerController;
newObj.localPosition = newObj.position;
newObj.localRotation = NormalizeEulerDegrees(newObj.rotation);
newObj.localScale = newObj.scale;
newObj.localInitialized = true;
newObj.hasAudioSource = it->hasAudioSource;
newObj.audioSource = it->audioSource;
@@ -1202,6 +1402,19 @@ void Engine::setParent(int childId, int parentId) {
newParentIt->childIds.push_back(childId);
}
}
{
glm::vec3 parentPos(0.0f);
glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f);
glm::vec3 parentScale(1.0f);
if (parentId != -1) {
if (SceneObject* parent = findObjectById(parentId)) {
parentPos = parent->position;
parentRot = QuatFromEulerXYZ(parent->rotation);
parentScale = parent->scale;
}
}
updateLocalFromWorld(*childIt, parentPos, parentRot, parentScale);
}
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
@@ -1309,6 +1522,30 @@ bool Engine::addRigidbodyAngularImpulseFromScript(int id, const glm::vec3& impul
return physics.addAngularImpulse(id, impulse);
}
bool Engine::setRigidbodyYawFromScript(int id, float yawDegrees) {
return physics.setActorYaw(id, yawDegrees);
}
bool Engine::raycastClosestFromScript(const glm::vec3& origin, const glm::vec3& dir, float distance,
int ignoreId, glm::vec3* hitPos, glm::vec3* hitNormal,
float* hitDistance) const {
return physics.raycastClosest(origin, dir, distance, ignoreId, hitPos, hitNormal, hitDistance);
}
void Engine::syncLocalTransform(SceneObject& obj) {
glm::vec3 parentPos(0.0f);
glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f);
glm::vec3 parentScale(1.0f);
if (obj.parentId != -1) {
if (SceneObject* parent = findObjectById(obj.parentId)) {
parentPos = parent->position;
parentRot = QuatFromEulerXYZ(parent->rotation);
parentScale = parent->scale;
}
}
updateLocalFromWorld(obj, parentPos, parentRot, parentScale);
}
bool Engine::playAudioFromScript(int id) {
SceneObject* obj = findObjectById(id);
if (!obj || !obj->hasAudioSource) return false;

View File

@@ -102,6 +102,9 @@ private:
bool hierarchyShowTexturePreview = false;
bool hierarchyPreviewNearest = false;
std::unordered_map<std::string, bool> texturePreviewFilterOverrides;
bool audioPreviewLoop = false;
bool audioPreviewAutoPlay = false;
std::string audioPreviewSelectedPath;
bool isPlaying = false;
bool isPaused = false;
bool showViewOutput = true;
@@ -136,13 +139,17 @@ private:
bool specMode = false;
bool testMode = false;
bool collisionWireframe = false;
// 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);
static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& scale);
static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::vec3& rotationDeg, const glm::vec3& scale);
void updateHierarchyWorldTransforms();
void updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale);
void initializeLocalTransformsFromWorld(int sceneVersion);
void importOBJToScene(const std::string& filepath, const std::string& objectName);
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
@@ -161,7 +168,8 @@ private:
void renderEnvironmentWindow();
void renderCameraWindow();
void renderHierarchyPanel();
void renderObjectNode(SceneObject& obj, const std::string& filter);
void renderObjectNode(SceneObject& obj, const std::string& filter,
std::vector<bool>& ancestorHasNext, bool isLast, int depth);
void renderFileBrowserPanel();
void renderMeshBuilderPanel();
void renderInspectorPanel();
@@ -246,10 +254,15 @@ public:
bool addRigidbodyImpulseFromScript(int id, const glm::vec3& impulse);
bool addRigidbodyTorqueFromScript(int id, const glm::vec3& torque);
bool addRigidbodyAngularImpulseFromScript(int id, const glm::vec3& impulse);
bool setRigidbodyYawFromScript(int id, float yawDegrees);
bool raycastClosestFromScript(const glm::vec3& origin, const glm::vec3& dir, float distance,
int ignoreId, glm::vec3* hitPos, glm::vec3* hitNormal,
float* hitDistance) const;
// Audio control exposed to scripts
bool playAudioFromScript(int id);
bool stopAudioFromScript(int id);
bool setAudioLoopFromScript(int id, bool loop);
bool setAudioVolumeFromScript(int id, float volume);
bool setAudioClipFromScript(int id, const std::string& path);
void syncLocalTransform(SceneObject& obj);
};

View File

@@ -203,6 +203,20 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic);
break;
}
case ObjectType::Plane: {
glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f));
halfExtents.z = std::max(halfExtents.z, 0.01f);
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break;
}
case ObjectType::Torus: {
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
radius = std::max(radius, 0.01f);
shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true);
tuneShape(shape, radius * 2.0f, isDynamic);
break;
}
default:
break;
}

View File

@@ -258,7 +258,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false;
file << "# Scene File\n";
file << "version=9\n";
file << "version=10\n";
file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n";
file << "\n";
@@ -272,9 +272,9 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "layer=" << obj.layer << "\n";
file << "tag=" << obj.tag << "\n";
file << "parentId=" << obj.parentId << "\n";
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n";
file << "position=" << obj.localPosition.x << "," << obj.localPosition.y << "," << obj.localPosition.z << "\n";
file << "rotation=" << obj.localRotation.x << "," << obj.localRotation.y << "," << obj.localRotation.z << "\n";
file << "scale=" << obj.localScale.x << "," << obj.localScale.y << "," << obj.localScale.z << "\n";
file << "hasRigidbody=" << (obj.hasRigidbody ? 1 : 0) << "\n";
if (obj.hasRigidbody) {
file << "rbEnabled=" << (obj.rigidbody.enabled ? 1 : 0) << "\n";
@@ -409,7 +409,8 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
bool SceneSerializer::loadScene(const fs::path& filePath,
std::vector<SceneObject>& objects,
int& nextId) {
int& nextId,
int& outVersion) {
try {
std::ifstream file(filePath);
if (!file.is_open()) return false;
@@ -417,6 +418,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
objects.clear();
std::string line;
SceneObject* currentObj = nullptr;
int sceneVersion = 9;
while (std::getline(file, line)) {
size_t first = line.find_first_not_of(" \t\r\n");
@@ -445,7 +447,9 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
std::string key = line.substr(0, eqPos);
std::string value = line.substr(eqPos + 1);
if (key == "nextId") {
if (key == "version") {
sceneVersion = std::stoi(value);
} else if (key == "nextId") {
nextId = std::stoi(value);
} else if (currentObj) {
if (key == "id") {
@@ -474,17 +478,23 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
&currentObj->position.x,
&currentObj->position.y,
&currentObj->position.z);
currentObj->localPosition = currentObj->position;
currentObj->localInitialized = true;
} else if (key == "rotation") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->rotation.x,
&currentObj->rotation.y,
&currentObj->rotation.z);
currentObj->rotation = NormalizeEulerDegrees(currentObj->rotation);
currentObj->localRotation = currentObj->rotation;
currentObj->localInitialized = true;
} else if (key == "scale") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->scale.x,
&currentObj->scale.y,
&currentObj->scale.z);
currentObj->localScale = currentObj->scale;
currentObj->localInitialized = true;
} else if (key == "hasRigidbody") {
currentObj->hasRigidbody = std::stoi(value) != 0;
} else if (key == "rbEnabled") {
@@ -752,6 +762,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
}
file.close();
outVersion = sceneVersion;
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to load scene: " << e.what() << std::endl;

View File

@@ -59,5 +59,6 @@ public:
static bool loadScene(const fs::path& filePath,
std::vector<SceneObject>& objects,
int& nextId);
int& nextId,
int& outVersion);
};

View File

@@ -232,6 +232,58 @@ std::vector<float> generateCapsule(int segments, int rings) {
return triangulated;
}
std::vector<float> generateTorus(int segments, int sides) {
std::vector<float> vertices;
float majorRadius = 0.35f;
float minorRadius = 0.15f;
for (int seg = 0; seg <= segments; ++seg) {
float u = seg * 2.0f * PI / segments;
float cosU = cos(u);
float sinU = sin(u);
for (int side = 0; side <= sides; ++side) {
float v = side * 2.0f * PI / sides;
float cosV = cos(v);
float sinV = sin(v);
float x = (majorRadius + minorRadius * cosV) * cosU;
float y = minorRadius * sinV;
float z = (majorRadius + minorRadius * cosV) * sinU;
glm::vec3 normal = glm::normalize(glm::vec3(cosU * cosV, sinV, sinU * cosV));
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
vertices.push_back((float)seg / segments);
vertices.push_back((float)side / sides);
}
}
std::vector<float> triangulated;
int stride = sides + 1;
for (int seg = 0; seg < segments; ++seg) {
for (int side = 0; side < sides; ++side) {
int current = seg * stride + side;
int next = current + stride;
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[current * 8 + i]);
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(next + 1) * 8 + i]);
}
}
return triangulated;
}
// Mesh implementation
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float));
@@ -435,6 +487,7 @@ Renderer::~Renderer() {
delete sphereMesh;
delete capsuleMesh;
delete planeMesh;
delete torusMesh;
delete skybox;
delete postShader;
delete brightShader;
@@ -458,7 +511,6 @@ Renderer::~Renderer() {
releaseRenderTarget(entry.second);
}
mirrorTargets.clear();
mirrorSmooth.clear();
if (framebuffer) glDeleteFramebuffers(1, &framebuffer);
if (viewportTexture) glDeleteTextures(1, &viewportTexture);
if (rbo) glDeleteRenderbuffers(1, &rbo);
@@ -555,6 +607,8 @@ void Renderer::initialize() {
auto capsuleVerts = generateCapsule();
capsuleMesh = new Mesh(capsuleVerts.data(), capsuleVerts.size() * sizeof(float));
planeMesh = new Mesh(mirrorPlaneVertices, sizeof(mirrorPlaneVertices));
auto torusVerts = generateTorus();
torusMesh = new Mesh(torusVerts.data(), torusVerts.size() * sizeof(float));
skybox = new Skybox();
@@ -732,14 +786,6 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
}
return glm::normalize(n);
};
auto planeUp = [](const SceneObject& obj) {
glm::quat q = glm::quat(glm::radians(obj.rotation));
glm::vec3 u = q * glm::vec3(0.0f, 1.0f, 0.0f);
if (!std::isfinite(u.x) || glm::length(u) < 1e-3f) {
u = glm::vec3(0.0f, 1.0f, 0.0f);
}
return glm::normalize(u);
};
for (const auto& obj : sceneObjects) {
if (!obj.enabled || obj.type != ObjectType::Mirror) continue;
@@ -756,30 +802,20 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
glm::vec3 n = planeNormal(obj);
glm::vec3 planePoint = obj.position;
glm::vec3 upVec = planeUp(obj);
glm::vec3 tangent = glm::normalize(glm::cross(upVec, n));
if (!std::isfinite(tangent.x) || glm::length(tangent) < 1e-3f) {
tangent = glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 bitangent = glm::cross(n, tangent);
auto reflectPoint = [&](const glm::vec3& p) {
float dist = glm::dot(p - planePoint, n);
return p - 2.0f * dist * n;
};
auto reflectDir = [&](const glm::vec3& v) {
float dist = glm::dot(v, n);
return v - 2.0f * dist * n;
};
Camera mirrorCam = camera;
glm::vec3 relToPlane = camera.position - planePoint;
float alongT = glm::dot(relToPlane, tangent);
float alongB = glm::dot(relToPlane, bitangent);
MirrorSmoothing& sm = mirrorSmooth[obj.id];
if (!sm.initialized) {
sm.planar = glm::vec2(alongT, alongB);
sm.initialized = true;
} else {
float lerp = 0.2f; // slow the planar tracking slightly
sm.planar = glm::mix(sm.planar, glm::vec2(alongT, alongB), lerp);
}
float fixedDepth = 0.05f; // keep a small offset off the plane; ignore viewer local Z movement
mirrorCam.position = planePoint + tangent * sm.planar.x + bitangent * sm.planar.y + n * fixedDepth;
mirrorCam.front = n; // Look straight out from the mirror face
mirrorCam.up = upVec;
mirrorCam.position = reflectPoint(camera.position);
mirrorCam.front = glm::normalize(reflectDir(camera.front));
mirrorCam.up = glm::normalize(reflectDir(camera.up));
if (!std::isfinite(mirrorCam.front.x) || glm::length(mirrorCam.front) < 1e-3f) {
mirrorCam.front = glm::vec3(0.0f, 0.0f, -1.0f);
}
@@ -795,7 +831,6 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
for (auto it = mirrorTargets.begin(); it != mirrorTargets.end(); ) {
if (active.find(it->first) == active.end()) {
releaseRenderTarget(it->second);
mirrorSmooth.erase(it->first);
it = mirrorTargets.erase(it);
} else {
++it;
@@ -972,9 +1007,15 @@ void Renderer::renderObject(const SceneObject& obj) {
case ObjectType::Capsule:
capsuleMesh->draw();
break;
case ObjectType::Plane:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Mirror:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Torus:
if (torusMesh) torusMesh->draw();
break;
case ObjectType::OBJMesh:
if (obj.meshId >= 0) {
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
@@ -1192,7 +1233,9 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh;
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId != -1) {
@@ -1259,6 +1302,8 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
if (allowHistory) {
ensureRenderTarget(historyTarget, width, height);
}
ensureRenderTarget(bloomTargetA, width, height);
ensureRenderTarget(bloomTargetB, width, height);
if (target.fbo == 0 || target.texture == 0) {
if (allowHistory) {
displayTexture = sourceTexture;

View File

@@ -13,6 +13,7 @@ extern float vertices[];
// Primitive generation functions
std::vector<float> generateSphere(int segments = 32, int rings = 16);
std::vector<float> generateCapsule(int segments = 16, int rings = 8);
std::vector<float> generateTorus(int segments = 32, int sides = 16);
class Mesh {
private:
@@ -73,11 +74,6 @@ private:
RenderTarget historyTarget;
RenderTarget bloomTargetA;
RenderTarget bloomTargetB;
struct MirrorSmoothing {
glm::vec2 planar = glm::vec2(0.0f);
bool initialized = false;
};
std::unordered_map<int, MirrorSmoothing> mirrorSmooth;
Shader* shader = nullptr;
Shader* defaultShader = nullptr;
Shader* postShader = nullptr;
@@ -108,6 +104,7 @@ private:
Mesh* sphereMesh = nullptr;
Mesh* capsuleMesh = nullptr;
Mesh* planeMesh = nullptr;
Mesh* torusMesh = nullptr;
Skybox* skybox = nullptr;
unsigned int quadVAO = 0;
unsigned int quadVBO = 0;

View File

@@ -14,7 +14,9 @@ enum class ObjectType {
AreaLight,
Camera,
PostFXNode,
Mirror
Mirror,
Plane,
Torus
};
struct MaterialProperties {
@@ -161,6 +163,10 @@ public:
glm::vec3 position;
glm::vec3 rotation;
glm::vec3 scale;
glm::vec3 localPosition;
glm::vec3 localRotation;
glm::vec3 localScale;
bool localInitialized = false;
int id;
int parentId = -1;
std::vector<int> childIds;
@@ -190,5 +196,14 @@ public:
AudioSourceComponent audioSource;
SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
: name(name),
type(type),
position(0.0f),
rotation(0.0f),
scale(1.0f),
localPosition(0.0f),
localRotation(0.0f),
localScale(1.0f),
localInitialized(true),
id(id) {}
};

View File

@@ -87,6 +87,12 @@ bool ScriptContext::IsInLayer(int layer) const {
void ScriptContext::SetPosition(const glm::vec3& pos) {
if (object) {
object->position = pos;
if (engine) {
engine->syncLocalTransform(*object);
} else {
object->localPosition = object->position;
object->localInitialized = true;
}
MarkDirty();
}
}
@@ -94,6 +100,12 @@ void ScriptContext::SetPosition(const glm::vec3& pos) {
void ScriptContext::SetRotation(const glm::vec3& rot) {
if (object) {
object->rotation = NormalizeEulerDegrees(rot);
if (engine) {
engine->syncLocalTransform(*object);
} else {
object->localRotation = object->rotation;
object->localInitialized = true;
}
MarkDirty();
if (engine && HasRigidbody()) {
engine->teleportPhysicsActorFromScript(object->id, object->position, object->rotation);
@@ -104,6 +116,12 @@ void ScriptContext::SetRotation(const glm::vec3& rot) {
void ScriptContext::SetScale(const glm::vec3& scl) {
if (object) {
object->scale = scl;
if (engine) {
engine->syncLocalTransform(*object);
} else {
object->localScale = object->scale;
object->localInitialized = true;
}
MarkDirty();
}
}
@@ -152,9 +170,22 @@ bool ScriptContext::AddRigidbodyAngularImpulse(const glm::vec3& impulse) {
return engine->addRigidbodyAngularImpulseFromScript(object->id, impulse);
}
bool ScriptContext::SetRigidbodyYaw(float yawDegrees) {
if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyYawFromScript(object->id, yawDegrees);
}
bool ScriptContext::RaycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance,
glm::vec3* hitPos, glm::vec3* hitNormal, float* hitDistance) const {
if (!engine) return false;
int ignoreId = object ? object->id : -1;
return engine->raycastClosestFromScript(origin, dir, distance, ignoreId, hitPos, hitNormal, hitDistance);
}
bool ScriptContext::SetRigidbodyRotation(const glm::vec3& rotDeg) {
if (!engine || !object || !HasRigidbody()) return false;
object->rotation = NormalizeEulerDegrees(rotDeg);
engine->syncLocalTransform(*object);
MarkDirty();
return engine->teleportPhysicsActorFromScript(object->id, object->position, object->rotation);
}
@@ -163,6 +194,7 @@ bool ScriptContext::TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rot
if (!engine || !object) return false;
object->position = pos;
object->rotation = NormalizeEulerDegrees(rotDeg);
engine->syncLocalTransform(*object);
MarkDirty();
return engine->teleportPhysicsActorFromScript(object->id, pos, object->rotation);
}

View File

@@ -45,6 +45,10 @@ struct ScriptContext {
bool AddRigidbodyImpulse(const glm::vec3& impulse);
bool AddRigidbodyTorque(const glm::vec3& torque);
bool AddRigidbodyAngularImpulse(const glm::vec3& impulse);
bool SetRigidbodyYaw(float yawDegrees);
bool RaycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance,
glm::vec3* hitPos = nullptr, glm::vec3* hitNormal = nullptr,
float* hitDistance = nullptr) const;
bool SetRigidbodyRotation(const glm::vec3& rotDeg);
bool TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg);
// Audio helpers

View File

@@ -1,5 +1,6 @@
#include "../../include/Window/Window.h"
#include "../../include/ThirdParty/stb_image.h"
#include <cstdlib>
int width, height, channels;
@@ -7,7 +8,17 @@ GLFWwindow *Window::makeWindow() {
unsigned char *pixels = stbi_load("Resources/Engine-Root/Modu-Logo.png",
&width, &height, &channels, 4);
#if defined(__linux__)
setenv("XDG_SESSION_TYPE", "x11", 1);
const char *wayland_display = std::getenv("WAYLAND_DISPLAY");
const char *x11_display = std::getenv("DISPLAY");
if (wayland_display && *wayland_display &&
glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_WAYLAND);
} else if (x11_display && *x11_display &&
glfwPlatformSupported(GLFW_PLATFORM_X11)) {
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
} else {
glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM);
}
#endif
if (!glfwInit()) {