First Commit on new Git-Base, yey!

This commit is contained in:
2026-01-22 12:30:53 -05:00
parent 2061d588e7
commit 303b835ba7
93 changed files with 17252 additions and 1138 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
build/
.cache/
Images-thingy/

View File

@@ -51,12 +51,35 @@ endif()
# ==================== Optional PhysX ====================
option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON)
option(MODULARITY_BUILD_EDITOR "Build the Modularity editor target" ON)
# ==================== Third-party libraries ====================
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
find_package(OpenGL REQUIRED)
# ==================== Mono (managed scripting) ====================
option(MODULARITY_USE_MONO "Enable Mono embedding for managed scripts" ON)
if(MODULARITY_USE_MONO)
set(MONO_ROOT ${PROJECT_SOURCE_DIR}/src/ThirdParty/mono CACHE PATH "Mono root directory")
find_path(MONO_INCLUDE_DIR mono/jit/jit.h
HINTS
${MONO_ROOT}/include/mono-2.0
${MONO_ROOT}/include
)
find_library(MONO_LIBRARY
NAMES mono-2.0-sgen mono-2.0 monosgen-2.0
HINTS
${MONO_ROOT}/lib
${MONO_ROOT}/lib64
)
if(NOT MONO_INCLUDE_DIR OR NOT MONO_LIBRARY)
message(WARNING "Mono not found. Disabling MODULARITY_USE_MONO. Set MONO_ROOT to a Mono runtime with include/mono-2.0 and libmono-2.0-sgen.")
set(MODULARITY_USE_MONO OFF CACHE BOOL "Enable Mono embedding for managed scripts" FORCE)
endif()
endif()
# GLAD
add_library(glad STATIC src/ThirdParty/glad/glad.c)
target_include_directories(glad PUBLIC src/ThirdParty/glad)
@@ -105,6 +128,7 @@ file(GLOB_RECURSE ENGINE_HEADERS CONFIGURE_DEPENDS
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/main_player.cpp")
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
@@ -122,8 +146,29 @@ target_include_directories(core PUBLIC
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
)
target_link_libraries(core PUBLIC glad glm imgui imguizmo)
if(MODULARITY_USE_MONO)
target_include_directories(core PUBLIC ${MONO_INCLUDE_DIR})
target_link_libraries(core PUBLIC ${MONO_LIBRARY})
endif()
target_compile_definitions(core PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS})
add_library(core_player STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
target_compile_definitions(core_player PUBLIC MODULARITY_PLAYER)
target_link_libraries(core_player PUBLIC assimp)
target_include_directories(core_player PUBLIC
${PROJECT_SOURCE_DIR}/src
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
)
target_link_libraries(core_player PUBLIC glad glm imgui imguizmo)
if(MODULARITY_USE_MONO)
target_include_directories(core_player PUBLIC ${MONO_INCLUDE_DIR})
target_link_libraries(core_player PUBLIC ${MONO_LIBRARY})
endif()
target_compile_definitions(core_player PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
target_compile_options(core_player PRIVATE ${MODULARITY_WARNING_FLAGS})
if(MODULARITY_ENABLE_PHYSX)
set(PHYSX_ROOT_DIR ${PROJECT_SOURCE_DIR}/src/ThirdParty/PhysX/physx CACHE PATH "PhysX root directory")
set(TARGET_BUILD_PLATFORM "linux" CACHE STRING "PhysX build platform (linux/windows)")
@@ -134,19 +179,47 @@ if(MODULARITY_ENABLE_PHYSX)
target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include)
target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
target_link_libraries(core PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking)
target_include_directories(core_player PUBLIC ${PHYSX_ROOT_DIR}/include)
target_compile_definitions(core_player PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
target_link_libraries(core_player PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking)
endif()
# ==================== Executable ====================
add_executable(Modularity src/main.cpp)
target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS})
if(MODULARITY_BUILD_EDITOR)
add_executable(Modularity src/main.cpp)
target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS})
endif()
add_executable(ModularityPlayer src/main_player.cpp)
target_compile_options(ModularityPlayer PRIVATE ${MODULARITY_WARNING_FLAGS})
# Link order matters on Linux
if(NOT WIN32)
find_package(X11 REQUIRED)
target_include_directories(Modularity PRIVATE ${X11_INCLUDE_DIR})
if(MODULARITY_BUILD_EDITOR)
target_include_directories(Modularity PRIVATE ${X11_INCLUDE_DIR})
target_link_libraries(Modularity PRIVATE
core
imgui
imguizmo
glad
glm
glfw
OpenGL::GL
pthread
dl
${X11_LIBRARIES}
Xrandr
Xi
Xinerama
Xcursor
)
# Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols.
target_link_options(Modularity PRIVATE "-rdynamic")
endif()
target_link_libraries(Modularity PRIVATE
core
target_include_directories(ModularityPlayer PRIVATE ${X11_INCLUDE_DIR})
target_link_libraries(ModularityPlayer PRIVATE
core_player
imgui
imguizmo
glad
@@ -161,15 +234,25 @@ if(NOT WIN32)
Xinerama
Xcursor
)
# Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols.
target_link_options(Modularity PRIVATE "-rdynamic")
target_link_options(ModularityPlayer PRIVATE "-rdynamic")
else()
target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL)
if(MODULARITY_BUILD_EDITOR)
target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL)
endif()
target_link_libraries(ModularityPlayer PRIVATE core_player glfw OpenGL::GL)
endif()
# ==================== Copy Resources folder after build ====================
add_custom_command(TARGET Modularity POST_BUILD
if(MODULARITY_BUILD_EDITOR)
add_custom_command(TARGET Modularity POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Resources
$<TARGET_FILE_DIR:Modularity>/Resources
)
endif()
add_custom_command(TARGET ModularityPlayer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Resources
$<TARGET_FILE_DIR:Modularity>/Resources
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,44 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in ivec4 aBoneIds;
layout (location = 4) in vec4 aBoneWeights;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 bones[256];
uniform int boneCount;
uniform bool useSkinning;
void main()
{
vec4 localPos = vec4(aPos, 1.0);
vec3 localNormal = aNormal;
if (useSkinning) {
vec4 skinnedPos = vec4(0.0);
vec3 skinnedNormal = vec3(0.0);
for (int i = 0; i < 4; ++i) {
int id = aBoneIds[i];
float w = aBoneWeights[i];
if (w <= 0.0 || id < 0 || id >= boneCount) continue;
mat4 b = bones[id];
skinnedPos += (b * localPos) * w;
skinnedNormal += mat3(b) * localNormal * w;
}
localPos = skinnedPos;
localNormal = skinnedNormal;
}
vec4 worldPos = model * localPos;
FragPos = vec3(worldPos);
Normal = mat3(transpose(inverse(model))) * localNormal;
TexCoord = aTexCoord;
gl_Position = projection * view * worldPos;
}

125
Resources/anim.ini Normal file
View File

@@ -0,0 +1,125 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Modularity - Project Launcher]
Pos=569,288
Size=720,480
Collapsed=0
[Window][New Project]
Pos=679,403
Size=500,250
Collapsed=0
[Window][DockSpace]
Pos=0,24
Size=1000,776
Collapsed=0
[Window][Viewport]
Pos=304,48
Size=329,752
Collapsed=0
DockId=0x0000000B,0
[Window][Hierarchy]
Pos=0,48
Size=304,617
Collapsed=0
DockId=0x0000000D,0
[Window][Inspector]
Pos=633,48
Size=367,557
Collapsed=0
DockId=0x00000001,0
[Window][File Browser]
Pos=756,836
Size=753,221
Collapsed=0
DockId=0xD71539A0,1
[Window][Console]
Pos=0,665
Size=304,135
Collapsed=0
DockId=0x0000000E,0
[Window][Project]
Pos=633,605
Size=367,195
Collapsed=0
DockId=0x00000002,0
[Window][Launcher]
Pos=0,0
Size=1000,800
Collapsed=0
[Window][Camera]
Pos=0,48
Size=304,747
Collapsed=0
DockId=0x00000005,1
[Window][Environment]
Pos=1553,48
Size=347,747
Collapsed=0
DockId=0x00000005,1
[Window][Project Manager]
Pos=787,785
Size=784,221
Collapsed=0
DockId=0xD71539A0,1
[Window][Game Viewport]
Pos=304,48
Size=329,752
Collapsed=0
DockId=0x0000000B,1
[Window][Project Settings]
Pos=633,48
Size=367,557
Collapsed=0
DockId=0x00000001,1
[Window][Animation]
Pos=304,804
Size=1229,218
Collapsed=0
DockId=0x0000000C,0
[Window][Scripting]
Pos=304,48
Size=329,752
Collapsed=0
DockId=0x0000000B,2
[Table][0xFF88847C,2]
RefScale=16
Column 0 Width=220
Column 1 Weight=1.0000
[Docking][Data]
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1000,752 Split=Y
DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=X Selected=0xC450F867
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=304,227 Split=Y Selected=0xBABDAE5E
DockNode ID=0x0000000D Parent=0x00000007 SizeRef=304,799 Selected=0xBABDAE5E
DockNode ID=0x0000000E Parent=0x00000007 SizeRef=304,175 Selected=0xEA83D666
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1596,227 Split=X Selected=0xE9044848
DockNode ID=0x00000003 Parent=0x00000008 SizeRef=1229,227 Split=Y Selected=0xE9044848
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=1213,756 CentralNode=1 Selected=0xE9044848
DockNode ID=0x0000000C Parent=0x00000003 SizeRef=1213,218 Selected=0x5921A509
DockNode ID=0x00000004 Parent=0x00000008 SizeRef=367,227 Split=Y Selected=0x36DC96AB
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=797,722 Selected=0x3F1379AF
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=797,252 Selected=0x9C21DE82
DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82
DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666

View File

@@ -14,45 +14,45 @@ Size=500,250
Collapsed=0
[Window][DockSpace]
Pos=0,23
Size=1920,983
Pos=0,24
Size=1900,998
Collapsed=0
[Window][Viewport]
Pos=306,46
Size=1265,739
Pos=304,48
Size=1249,747
Collapsed=0
DockId=0x00000002,0
DockId=0x0000000F,0
[Window][Hierarchy]
Pos=0,46
Size=304,739
Pos=0,48
Size=304,747
Collapsed=0
DockId=0x00000001,0
DockId=0x00000007,0
[Window][Inspector]
Pos=1573,46
Size=347,960
Pos=1553,48
Size=347,747
Collapsed=0
DockId=0x00000008,0
DockId=0x00000010,0
[Window][File Browser]
Pos=756,836
Size=753,221
Collapsed=0
DockId=0x00000006,1
DockId=0xD71539A0,1
[Window][Console]
Pos=0,787
Size=785,219
Pos=939,795
Size=961,227
Collapsed=0
DockId=0x00000005,0
DockId=0x00000014,0
[Window][Project]
Pos=787,787
Size=784,219
Pos=0,795
Size=939,227
Collapsed=0
DockId=0x00000006,0
DockId=0x00000013,0
[Window][Launcher]
Pos=0,0
@@ -60,43 +60,64 @@ Size=1000,800
Collapsed=0
[Window][Camera]
Pos=0,46
Size=304,739
Pos=0,48
Size=304,747
Collapsed=0
DockId=0x00000001,1
DockId=0x00000007,1
[Window][Environment]
Pos=1573,46
Size=347,960
Pos=1553,48
Size=347,747
Collapsed=0
DockId=0x00000008,1
DockId=0x00000010,1
[Window][Project Manager]
Pos=787,785
Size=784,221
Collapsed=0
DockId=0x00000006,1
DockId=0xD71539A0,1
[Window][Game Viewport]
Pos=306,46
Size=1265,739
Pos=304,48
Size=1249,747
Collapsed=0
DockId=0x00000002,1
DockId=0x0000000F,1
[Window][Project Settings]
Pos=306,46
Size=1265,739
Pos=304,48
Size=1249,747
Collapsed=0
DockId=0x00000002,2
DockId=0x0000000F,2
[Window][Animation]
Pos=583,795
Size=738,227
Collapsed=0
DockId=0x00000013,0
[Window][Scripting]
Pos=304,48
Size=1249,747
Collapsed=0
DockId=0x0000000F,3
[Table][0xFF88847C,2]
RefScale=16
Column 0 Width=220
Column 1 Weight=1.0000
[Docking][Data]
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X
DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y
DockNode ID=0x00000003 Parent=0x00000007 SizeRef=1858,739 Split=X
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=304,758 Selected=0xBABDAE5E
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=694,758 CentralNode=1 Selected=0xC450F867
DockNode ID=0x00000004 Parent=0x00000007 SizeRef=1858,219 Split=X Selected=0xEA83D666
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=929,221 Selected=0xEA83D666
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0x9C21DE82
DockNode ID=0x00000008 Parent=0xD71539A0 SizeRef=347,1015 Selected=0x36DC96AB
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y
DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867
DockNode ID=0x00000001 Parent=0x00000005 SizeRef=1900,747 Split=X Selected=0xE9044848
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=304,486 Selected=0xBABDAE5E
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1596,486 Split=X Selected=0xE9044848
DockNode ID=0x0000000F Parent=0x00000008 SizeRef=1249,486 Selected=0xE9044848
DockNode ID=0x00000010 Parent=0x00000008 SizeRef=347,486 Selected=0x36DC96AB
DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1900,227 Split=X Selected=0x3F1379AF
DockNode ID=0x00000013 Parent=0x00000002 SizeRef=939,488 CentralNode=1 Selected=0x9C21DE82
DockNode ID=0x00000014 Parent=0x00000002 SizeRef=961,488 Selected=0xEA83D666
DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82
DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666

121
Resources/scripter.ini Normal file
View File

@@ -0,0 +1,121 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Modularity - Project Launcher]
Pos=569,288
Size=720,480
Collapsed=0
[Window][New Project]
Pos=679,403
Size=500,250
Collapsed=0
[Window][DockSpace]
Pos=0,24
Size=1900,998
Collapsed=0
[Window][Viewport]
Pos=260,48
Size=741,772
Collapsed=0
DockId=0x00000003,0
[Window][Hierarchy]
Pos=0,48
Size=260,772
Collapsed=0
DockId=0x00000007,1
[Window][Inspector]
Pos=1001,48
Size=899,772
Collapsed=0
DockId=0x00000004,1
[Window][File Browser]
Pos=756,836
Size=753,221
Collapsed=0
DockId=0xD71539A0,1
[Window][Console]
Pos=0,48
Size=260,772
Collapsed=0
DockId=0x00000007,0
[Window][Project]
Pos=0,820
Size=1900,202
Collapsed=0
DockId=0x0000000C,0
[Window][Launcher]
Pos=0,0
Size=1900,1022
Collapsed=0
[Window][Camera]
Pos=0,48
Size=304,747
Collapsed=0
DockId=0x00000005,1
[Window][Environment]
Pos=1553,48
Size=347,747
Collapsed=0
DockId=0x00000005,1
[Window][Project Manager]
Pos=787,785
Size=784,221
Collapsed=0
DockId=0xD71539A0,1
[Window][Game Viewport]
Pos=260,48
Size=741,772
Collapsed=0
DockId=0x00000003,1
[Window][Project Settings]
Pos=1201,48
Size=699,772
Collapsed=0
DockId=0x00000004,2
[Window][Animation]
Pos=583,795
Size=738,227
Collapsed=0
DockId=0x00000003,0
[Window][Scripting]
Pos=1001,48
Size=899,772
Collapsed=0
DockId=0x00000004,0
[Table][0xFF88847C,2]
RefScale=16
Column 0 Width=220
Column 1 Weight=1.0000
[Docking][Data]
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y
DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867
DockNode ID=0x0000000B Parent=0x00000005 SizeRef=1900,772 Split=X Selected=0xE9044848
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=260,114 Selected=0xBABDAE5E
DockNode ID=0x00000008 Parent=0x0000000B SizeRef=1640,114 Split=X Selected=0xE9044848
DockNode ID=0x00000003 Parent=0x00000008 SizeRef=741,114 CentralNode=1 Selected=0xC450F867
DockNode ID=0x00000004 Parent=0x00000008 SizeRef=899,114 Selected=0xBC881222
DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1900,202 Selected=0x9C21DE82
DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82
DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666

282
Scripts/AnimationWindow.cpp Normal file
View File

@@ -0,0 +1,282 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
namespace {
struct Keyframe {
float time = 0.0f;
glm::vec3 position = glm::vec3(0.0f);
glm::vec3 rotation = glm::vec3(0.0f);
glm::vec3 scale = glm::vec3(1.0f);
};
int targetId = -1;
char targetName[128] = "";
std::vector<Keyframe> keyframes;
int selectedKey = -1;
float clipLength = 2.0f;
float currentTime = 0.0f;
float playSpeed = 1.0f;
bool isPlaying = false;
bool loop = true;
bool applyOnScrub = true;
glm::vec3 lerpVec3(const glm::vec3& a, const glm::vec3& b, float t) {
return a + (b - a) * t;
}
float clampFloat(float value, float minValue, float maxValue) {
return std::max(minValue, std::min(value, maxValue));
}
SceneObject* resolveTarget(ScriptContext& ctx) {
if (targetId >= 0) {
if (auto* obj = ctx.FindObjectById(targetId)) {
return obj;
}
}
if (targetName[0] != '\0') {
if (auto* obj = ctx.FindObjectByName(targetName)) {
targetId = obj->id;
return obj;
}
}
return nullptr;
}
void syncTargetLabel(SceneObject* obj) {
if (!obj) return;
strncpy(targetName, obj->name.c_str(), sizeof(targetName) - 1);
targetName[sizeof(targetName) - 1] = '\0';
}
void captureKeyframe(SceneObject& obj, float time) {
float clamped = clampFloat(time, 0.0f, clipLength);
auto it = std::find_if(keyframes.begin(), keyframes.end(),
[&](const Keyframe& k) { return std::abs(k.time - clamped) < 0.0001f; });
if (it == keyframes.end()) {
keyframes.push_back(Keyframe{clamped, obj.position, obj.rotation, obj.scale});
} else {
it->position = obj.position;
it->rotation = obj.rotation;
it->scale = obj.scale;
}
std::sort(keyframes.begin(), keyframes.end(),
[](const Keyframe& a, const Keyframe& b) { return a.time < b.time; });
}
void deleteKeyframe(int index) {
if (index < 0 || index >= static_cast<int>(keyframes.size())) return;
keyframes.erase(keyframes.begin() + index);
if (selectedKey == index) selectedKey = -1;
if (selectedKey > index) selectedKey--;
}
void applyPoseAtTime(ScriptContext& ctx, SceneObject& obj, float time) {
if (keyframes.empty()) return;
if (time <= keyframes.front().time) {
ctx.SetPosition(keyframes.front().position);
ctx.SetRotation(keyframes.front().rotation);
ctx.SetScale(keyframes.front().scale);
ctx.MarkDirty();
return;
}
if (time >= keyframes.back().time) {
ctx.SetPosition(keyframes.back().position);
ctx.SetRotation(keyframes.back().rotation);
ctx.SetScale(keyframes.back().scale);
ctx.MarkDirty();
return;
}
for (size_t i = 0; i + 1 < keyframes.size(); ++i) {
const Keyframe& a = keyframes[i];
const Keyframe& b = keyframes[i + 1];
if (time >= a.time && time <= b.time) {
float span = b.time - a.time;
float t = (span > 0.0f) ? (time - a.time) / span : 0.0f;
ctx.SetPosition(lerpVec3(a.position, b.position, t));
ctx.SetRotation(lerpVec3(a.rotation, b.rotation, t));
ctx.SetScale(lerpVec3(a.scale, b.scale, t));
ctx.MarkDirty();
return;
}
}
}
void drawTimeline(float& time, float length, int& selection) {
ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f);
ImVec2 start = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("Timeline", size);
ImDrawList* draw = ImGui::GetWindowDrawList();
ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg);
ImU32 border = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark);
ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab);
draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f);
draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f);
float clamped = clampFloat(time, 0.0f, length);
float playheadX = start.x + (length > 0.0f ? (clamped / length) * size.x : 0.0f);
draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f);
for (size_t i = 0; i < keyframes.size(); ++i) {
float keyX = start.x + (length > 0.0f ? (keyframes[i].time / length) * size.x : 0.0f);
ImVec2 center(keyX, start.y + size.y * 0.5f);
float radius = (selection == static_cast<int>(i)) ? 6.0f : 4.5f;
draw->AddCircleFilled(center, radius, keyColor);
ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f));
if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
selection = static_cast<int>(i);
time = keyframes[i].time;
}
}
if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
float mouseX = ImGui::GetIO().MousePos.x;
float t = (mouseX - start.x) / size.x;
time = clampFloat(t * length, 0.0f, length);
}
}
void drawKeyframeTable() {
if (keyframes.empty()) {
ImGui::TextDisabled("No keyframes yet.");
return;
}
if (ImGui::BeginTable("KeyframeTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("Time");
ImGui::TableSetupColumn("Position");
ImGui::TableSetupColumn("Rotation");
ImGui::TableSetupColumn("Scale");
ImGui::TableHeadersRow();
for (size_t i = 0; i < keyframes.size(); ++i) {
const auto& key = keyframes[i];
ImGui::TableNextRow();
ImGui::TableNextColumn();
bool selected = selectedKey == static_cast<int>(i);
std::string label = std::to_string(key.time);
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
selectedKey = static_cast<int>(i);
currentTime = key.time;
}
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z);
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z);
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z);
}
ImGui::EndTable();
}
}
} // namespace
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
ImGui::TextUnformatted("Simple Animation");
ImGui::Separator();
SceneObject* selectedObj = ctx.object;
SceneObject* targetObj = resolveTarget(ctx);
ImGui::TextDisabled("Select a GameObject to animate:");
ImGui::BeginDisabled();
ImGui::InputText("##TargetName", targetName, sizeof(targetName));
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Use Selected") && selectedObj) {
targetId = selectedObj->id;
syncTargetLabel(selectedObj);
}
ImGui::SameLine();
if (ImGui::Button("Clear")) {
targetId = -1;
targetName[0] = '\0';
targetObj = nullptr;
}
ImGui::Spacing();
if (ImGui::BeginTabBar("AnimModeTabs")) {
if (ImGui::BeginTabItem("Pose Mode")) {
ImGui::TextDisabled("Pose Editor");
ImGui::Separator();
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
if (ImGui::Button("Key")) {
if (targetObj) captureKeyframe(*targetObj, currentTime);
}
ImGui::SameLine();
if (ImGui::Button("Delete") && selectedKey >= 0) {
deleteKeyframe(selectedKey);
}
ImGui::PopStyleVar();
ImGui::Spacing();
drawTimeline(currentTime, clipLength, selectedKey);
ImGui::SliderFloat("Time", &currentTime, 0.0f, clipLength, "%.2fs");
ImGui::Spacing();
drawKeyframeTable();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Config Mode")) {
ImGui::TextDisabled("Playback");
ImGui::Separator();
ImGui::Checkbox("Loop", &loop);
ImGui::Checkbox("Apply On Scrub", &applyOnScrub);
ImGui::SliderFloat("Length", &clipLength, 0.1f, 20.0f, "%.2fs");
ImGui::SliderFloat("Speed", &playSpeed, 0.1f, 4.0f, "%.2fx");
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Transport");
if (ImGui::Button(isPlaying ? "Pause" : "Play")) {
isPlaying = !isPlaying;
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
isPlaying = false;
currentTime = 0.0f;
}
if (targetObj) {
ImGui::SameLine();
ImGui::TextDisabled("Target: %s", targetObj->name.c_str());
} else {
ImGui::TextDisabled("No target selected.");
}
if (isPlaying && clipLength > 0.0f) {
currentTime += ImGui::GetIO().DeltaTime * playSpeed;
if (currentTime > clipLength) {
if (loop) currentTime = std::fmod(currentTime, clipLength);
else {
currentTime = clipLength;
isPlaying = false;
}
}
}
if (targetObj && (isPlaying || applyOnScrub)) {
applyPoseAtTime(ctx, *targetObj, currentTime);
}
}
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
(void)ctx;
}

297
Scripts/Managed/ModuCPP.cs Normal file
View File

@@ -0,0 +1,297 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace ModuCPP {
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ScriptTickDelegate(IntPtr ctx, float deltaTime);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ScriptInspectorDelegate(IntPtr ctx);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void SetNativeApiDelegate(IntPtr apiPtr);
[StructLayout(LayoutKind.Sequential)]
public struct Vec3 {
public float X;
public float Y;
public float Z;
public Vec3(float x, float y, float z) {
X = x;
Y = y;
Z = z;
}
public static Vec3 operator +(Vec3 a, Vec3 b) => new Vec3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vec3 operator -(Vec3 a, Vec3 b) => new Vec3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vec3 operator *(Vec3 a, float s) => new Vec3(a.X * s, a.Y * s, a.Z * s);
}
public enum ConsoleMessageType {
Info = 0,
Warning = 1,
Error = 2,
Success = 3
}
[StructLayout(LayoutKind.Sequential)]
public struct NativeApi {
public uint Version;
public IntPtr GetObjectId;
public IntPtr GetPosition;
public IntPtr SetPosition;
public IntPtr GetRotation;
public IntPtr SetRotation;
public IntPtr GetScale;
public IntPtr SetScale;
public IntPtr HasRigidbody;
public IntPtr EnsureRigidbody;
public IntPtr SetRigidbodyVelocity;
public IntPtr GetRigidbodyVelocity;
public IntPtr AddRigidbodyForce;
public IntPtr AddRigidbodyImpulse;
public IntPtr GetSettingFloat;
public IntPtr GetSettingBool;
public IntPtr GetSettingString;
public IntPtr SetSettingFloat;
public IntPtr SetSettingBool;
public IntPtr SetSettingString;
public IntPtr AddConsoleMessage;
}
internal unsafe static class Native {
public static NativeApi Api;
public static GetObjectIdFn GetObjectId;
public static GetPositionFn GetPosition;
public static SetPositionFn SetPosition;
public static GetRotationFn GetRotation;
public static SetRotationFn SetRotation;
public static GetScaleFn GetScale;
public static SetScaleFn SetScale;
public static HasRigidbodyFn HasRigidbody;
public static EnsureRigidbodyFn EnsureRigidbody;
public static SetRigidbodyVelocityFn SetRigidbodyVelocity;
public static GetRigidbodyVelocityFn GetRigidbodyVelocity;
public static AddRigidbodyForceFn AddRigidbodyForce;
public static AddRigidbodyImpulseFn AddRigidbodyImpulse;
public static GetSettingFloatFn GetSettingFloat;
public static GetSettingBoolFn GetSettingBool;
public static GetSettingStringFn GetSettingString;
public static SetSettingFloatFn SetSettingFloat;
public static SetSettingBoolFn SetSettingBool;
public static SetSettingStringFn SetSettingString;
public static AddConsoleMessageFn AddConsoleMessage;
public static void BindDelegates() {
GetObjectId = Marshal.GetDelegateForFunctionPointer<GetObjectIdFn>(Api.GetObjectId);
GetPosition = Marshal.GetDelegateForFunctionPointer<GetPositionFn>(Api.GetPosition);
SetPosition = Marshal.GetDelegateForFunctionPointer<SetPositionFn>(Api.SetPosition);
GetRotation = Marshal.GetDelegateForFunctionPointer<GetRotationFn>(Api.GetRotation);
SetRotation = Marshal.GetDelegateForFunctionPointer<SetRotationFn>(Api.SetRotation);
GetScale = Marshal.GetDelegateForFunctionPointer<GetScaleFn>(Api.GetScale);
SetScale = Marshal.GetDelegateForFunctionPointer<SetScaleFn>(Api.SetScale);
HasRigidbody = Marshal.GetDelegateForFunctionPointer<HasRigidbodyFn>(Api.HasRigidbody);
EnsureRigidbody = Marshal.GetDelegateForFunctionPointer<EnsureRigidbodyFn>(Api.EnsureRigidbody);
SetRigidbodyVelocity = Marshal.GetDelegateForFunctionPointer<SetRigidbodyVelocityFn>(Api.SetRigidbodyVelocity);
GetRigidbodyVelocity = Marshal.GetDelegateForFunctionPointer<GetRigidbodyVelocityFn>(Api.GetRigidbodyVelocity);
AddRigidbodyForce = Marshal.GetDelegateForFunctionPointer<AddRigidbodyForceFn>(Api.AddRigidbodyForce);
AddRigidbodyImpulse = Marshal.GetDelegateForFunctionPointer<AddRigidbodyImpulseFn>(Api.AddRigidbodyImpulse);
GetSettingFloat = Marshal.GetDelegateForFunctionPointer<GetSettingFloatFn>(Api.GetSettingFloat);
GetSettingBool = Marshal.GetDelegateForFunctionPointer<GetSettingBoolFn>(Api.GetSettingBool);
GetSettingString = Marshal.GetDelegateForFunctionPointer<GetSettingStringFn>(Api.GetSettingString);
SetSettingFloat = Marshal.GetDelegateForFunctionPointer<SetSettingFloatFn>(Api.SetSettingFloat);
SetSettingBool = Marshal.GetDelegateForFunctionPointer<SetSettingBoolFn>(Api.SetSettingBool);
SetSettingString = Marshal.GetDelegateForFunctionPointer<SetSettingStringFn>(Api.SetSettingString);
AddConsoleMessage = Marshal.GetDelegateForFunctionPointer<AddConsoleMessageFn>(Api.AddConsoleMessage);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int GetObjectIdFn(IntPtr ctx);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void GetPositionFn(IntPtr ctx, float* x, float* y, float* z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetPositionFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void GetRotationFn(IntPtr ctx, float* x, float* y, float* z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetRotationFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void GetScaleFn(IntPtr ctx, float* x, float* y, float* z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetScaleFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int HasRigidbodyFn(IntPtr ctx);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int EnsureRigidbodyFn(IntPtr ctx, int useGravity, int kinematic);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int SetRigidbodyVelocityFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int GetRigidbodyVelocityFn(IntPtr ctx, float* x, float* y, float* z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int AddRigidbodyForceFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int AddRigidbodyImpulseFn(IntPtr ctx, float x, float y, float z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate float GetSettingFloatFn(IntPtr ctx, byte* key, float fallback);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int GetSettingBoolFn(IntPtr ctx, byte* key, int fallback);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void GetSettingStringFn(IntPtr ctx, byte* key, byte* fallback, byte* outBuffer, int outBufferSize);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetSettingFloatFn(IntPtr ctx, byte* key, float value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetSettingBoolFn(IntPtr ctx, byte* key, int value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SetSettingStringFn(IntPtr ctx, byte* key, byte* value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void AddConsoleMessageFn(IntPtr ctx, byte* message, int type);
}
public static unsafe class Host {
public static void SetNativeApi(IntPtr apiPtr) {
Native.Api = Marshal.PtrToStructure<NativeApi>(apiPtr);
Native.BindDelegates();
}
}
public readonly unsafe struct Context {
private readonly IntPtr handle;
public Context(IntPtr ctx) {
handle = ctx;
}
public int ObjectId => Native.GetObjectId(handle);
public Vec3 Position {
get {
float x = 0f, y = 0f, z = 0f;
Native.GetPosition(handle, &x, &y, &z);
return new Vec3(x, y, z);
}
set {
Native.SetPosition(handle, value.X, value.Y, value.Z);
}
}
public Vec3 Rotation {
get {
float x = 0f, y = 0f, z = 0f;
Native.GetRotation(handle, &x, &y, &z);
return new Vec3(x, y, z);
}
set {
Native.SetRotation(handle, value.X, value.Y, value.Z);
}
}
public Vec3 Scale {
get {
float x = 0f, y = 0f, z = 0f;
Native.GetScale(handle, &x, &y, &z);
return new Vec3(x, y, z);
}
set {
Native.SetScale(handle, value.X, value.Y, value.Z);
}
}
public bool HasRigidbody => Native.HasRigidbody(handle) != 0;
public bool EnsureRigidbody(bool useGravity = true, bool kinematic = false) {
return Native.EnsureRigidbody(handle, useGravity ? 1 : 0, kinematic ? 1 : 0) != 0;
}
public Vec3 RigidbodyVelocity {
get {
float x = 0f, y = 0f, z = 0f;
if (Native.GetRigidbodyVelocity(handle, &x, &y, &z) == 0) {
return new Vec3(0f, 0f, 0f);
}
return new Vec3(x, y, z);
}
set {
Native.SetRigidbodyVelocity(handle, value.X, value.Y, value.Z);
}
}
public void AddRigidbodyForce(Vec3 force) {
Native.AddRigidbodyForce(handle, force.X, force.Y, force.Z);
}
public void AddRigidbodyImpulse(Vec3 impulse) {
Native.AddRigidbodyImpulse(handle, impulse.X, impulse.Y, impulse.Z);
}
public float GetSettingFloat(string key, float fallback = 0f) {
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
fixed (byte* keyPtr = keyBytes) {
return Native.GetSettingFloat(handle, keyPtr, fallback);
}
}
public bool GetSettingBool(string key, bool fallback = false) {
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
fixed (byte* keyPtr = keyBytes) {
int value = Native.GetSettingBool(handle, keyPtr, fallback ? 1 : 0);
return value != 0;
}
}
public string GetSettingString(string key, string fallback = "") {
const int bufferSize = 256;
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
byte[] fallbackBytes = Encoding.UTF8.GetBytes((fallback ?? string.Empty) + "\0");
byte* buffer = stackalloc byte[bufferSize];
fixed (byte* keyPtr = keyBytes)
fixed (byte* fallbackPtr = fallbackBytes) {
Native.GetSettingString(handle, keyPtr, fallbackPtr, buffer, bufferSize);
}
return FromUtf8(buffer);
}
public void SetSettingFloat(string key, float value) {
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
fixed (byte* keyPtr = keyBytes) {
Native.SetSettingFloat(handle, keyPtr, value);
}
}
public void SetSettingBool(string key, bool value) {
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
fixed (byte* keyPtr = keyBytes) {
Native.SetSettingBool(handle, keyPtr, value ? 1 : 0);
}
}
public void SetSettingString(string key, string value) {
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
byte[] valueBytes = Encoding.UTF8.GetBytes((value ?? string.Empty) + "\0");
fixed (byte* keyPtr = keyBytes)
fixed (byte* valuePtr = valueBytes) {
Native.SetSettingString(handle, keyPtr, valuePtr);
}
}
public void AddConsoleMessage(string message, ConsoleMessageType type = ConsoleMessageType.Info) {
byte[] msgBytes = Encoding.UTF8.GetBytes((message ?? string.Empty) + "\0");
fixed (byte* msgPtr = msgBytes) {
Native.AddConsoleMessage(handle, msgPtr, (int)type);
}
}
private static string FromUtf8(byte* ptr) {
if (ptr == null) return string.Empty;
int length = 0;
while (ptr[length] != 0) {
length++;
}
if (length == 0) return string.Empty;
byte[] bytes = new byte[length];
Marshal.Copy((IntPtr)ptr, bytes, 0, length);
return Encoding.UTF8.GetString(bytes);
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,68 @@
using System;
namespace ModuCPP {
public static class SampleInspector {
private static bool autoRotate = false;
private static Vec3 spinSpeed = new Vec3(0f, 45f, 0f);
private static Vec3 offset = new Vec3(0f, 1f, 0f);
private static string targetName = "MyTarget"; // Stored for parity; object lookup API not wired yet.
private static void LoadSettings(Context context) {
autoRotate = context.GetSettingBool("autoRotate", autoRotate);
spinSpeed = new Vec3(
context.GetSettingFloat("spinSpeedX", spinSpeed.X),
context.GetSettingFloat("spinSpeedY", spinSpeed.Y),
context.GetSettingFloat("spinSpeedZ", spinSpeed.Z)
);
offset = new Vec3(
context.GetSettingFloat("offsetX", offset.X),
context.GetSettingFloat("offsetY", offset.Y),
context.GetSettingFloat("offsetZ", offset.Z)
);
targetName = context.GetSettingString("targetName", targetName);
}
private static void SaveSettings(Context context) {
context.SetSettingBool("autoRotate", autoRotate);
context.SetSettingFloat("spinSpeedX", spinSpeed.X);
context.SetSettingFloat("spinSpeedY", spinSpeed.Y);
context.SetSettingFloat("spinSpeedZ", spinSpeed.Z);
context.SetSettingFloat("offsetX", offset.X);
context.SetSettingFloat("offsetY", offset.Y);
context.SetSettingFloat("offsetZ", offset.Z);
context.SetSettingString("targetName", targetName);
}
private static void ApplyAutoRotate(Context context, float deltaTime) {
if (!autoRotate) return;
context.Rotation = context.Rotation + (spinSpeed * deltaTime);
}
public static void Script_Begin(IntPtr ctx, float deltaTime) {
var context = new Context(ctx);
LoadSettings(context);
SaveSettings(context);
context.EnsureRigidbody(useGravity: true, kinematic: false);
context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info);
}
public static void Script_OnInspector(IntPtr ctx) {
var context = new Context(ctx);
LoadSettings(context);
SaveSettings(context);
context.AddConsoleMessage("Managed inspector hook (no UI yet)", ConsoleMessageType.Info);
}
public static void Script_Spec(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
public static void Script_TestEditor(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
public static void Script_TickUpdate(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
namespace ModuCPP {
public static class SampleInspectorManaged {
private static bool autoRotate = false;
private static Vec3 spinSpeed = new Vec3(0f, 45f, 0f);
private static Vec3 offset = new Vec3(0f, 1f, 0f);
private static string targetName = "MyTarget"; // Stored for parity; object lookup API not wired yet.
private static void LoadSettings(Context context) {
autoRotate = context.GetSettingBool("autoRotate", autoRotate);
spinSpeed = new Vec3(
context.GetSettingFloat("spinSpeedX", spinSpeed.X),
context.GetSettingFloat("spinSpeedY", spinSpeed.Y),
context.GetSettingFloat("spinSpeedZ", spinSpeed.Z)
);
offset = new Vec3(
context.GetSettingFloat("offsetX", offset.X),
context.GetSettingFloat("offsetY", offset.Y),
context.GetSettingFloat("offsetZ", offset.Z)
);
targetName = context.GetSettingString("targetName", targetName);
}
private static void SaveSettings(Context context) {
context.SetSettingBool("autoRotate", autoRotate);
context.SetSettingFloat("spinSpeedX", spinSpeed.X);
context.SetSettingFloat("spinSpeedY", spinSpeed.Y);
context.SetSettingFloat("spinSpeedZ", spinSpeed.Z);
context.SetSettingFloat("offsetX", offset.X);
context.SetSettingFloat("offsetY", offset.Y);
context.SetSettingFloat("offsetZ", offset.Z);
context.SetSettingString("targetName", targetName);
}
private static void ApplyAutoRotate(Context context, float deltaTime) {
if (!autoRotate) return;
context.Rotation = context.Rotation + (spinSpeed * deltaTime);
}
public static void Script_Begin(IntPtr ctx, float deltaTime) {
var context = new Context(ctx);
LoadSettings(context);
SaveSettings(context);
context.EnsureRigidbody(useGravity: true, kinematic: false);
context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info);
}
public static void Script_OnInspector(IntPtr ctx) {
var context = new Context(ctx);
LoadSettings(context);
SaveSettings(context);
context.AddConsoleMessage("Managed inspector hook (no UI yet)", ConsoleMessageType.Info);
}
public static void Script_Spec(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
public static void Script_TestEditor(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
public static void Script_TickUpdate(IntPtr ctx, float deltaTime) {
ApplyAutoRotate(new Context(ctx), deltaTime);
}
}
}

View File

@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v10.0": {
"ModuCPP/1.0.0": {
"runtime": {
"ModuCPP.dll": {}
}
}
}
},
"libraries": {
"ModuCPP/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
"configProperties": {
"System.Runtime.InteropServices.EnableComHosting": false,
"System.Runtime.InteropServices.BuiltInComInterop": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View File

@@ -0,0 +1,24 @@
{
"runtimeTarget": {
"name": ".NETStandard,Version=v2.0/",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETStandard,Version=v2.0": {},
".NETStandard,Version=v2.0/": {
"ModuCPP/1.0.0": {
"runtime": {
"ModuCPP.dll": {}
}
}
}
},
"libraries": {
"ModuCPP/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]

View File

@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2061d588e7a10416f073bb34ad8bda8e068f291b")]
[assembly: System.Reflection.AssemblyProductAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyTitleAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -0,0 +1 @@
78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375

View File

@@ -0,0 +1,17 @@
is_global = true
build_property.TargetFramework = net10.0
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkVersion = v10.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = ModuCPP
build_property.ProjectDir = /home/anemunt/Git-base/Modularity/Scripts/Managed/
build_property.EnableComHosting = false
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0
build_property.EnableCodeStyleSeverity =

View File

@@ -0,0 +1,8 @@
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;

Binary file not shown.

View File

@@ -0,0 +1 @@
0579f849781bddafc2e55261290008c5a7fb6bddd064d155e3e9c2dd44aec502

View File

@@ -0,0 +1,14 @@
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll

Binary file not shown.

View File

@@ -0,0 +1 @@
b14c7a505f46d8314ef755360e8bbee5cc4a67ee7d033805e0a7f8e8d9b71b40

Binary file not shown.

View File

@@ -0,0 +1 @@
{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]

View File

@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2061d588e7a10416f073bb34ad8bda8e068f291b")]
[assembly: System.Reflection.AssemblyProductAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyTitleAttribute("ModuCPP")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -0,0 +1 @@
78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375

View File

@@ -0,0 +1,8 @@
is_global = true
build_property.RootNamespace = ModuCPP
build_property.ProjectDir = /home/anemunt/Git-base/Modularity/Scripts/Managed/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
build_property.EffectiveAnalysisLevelStyle =
build_property.EnableCodeStyleSeverity =

View File

@@ -0,0 +1,8 @@
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;

View File

@@ -0,0 +1 @@
12a173d9ad34d74a13f6f07a58c9a75f8033484b726d3271d6b9bdffb23c227b

View File

@@ -0,0 +1,11 @@
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll
/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll
/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}}

View File

@@ -0,0 +1,70 @@
{
"format": 1,
"restore": {
"/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj": {}
},
"projects": {
"/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj",
"projectName": "ModuCPP",
"projectPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj",
"packagesPath": "/home/anemunt/.nuget/packages/",
"outputPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/anemunt/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"netstandard2.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"netstandard2.0": {
"targetAlias": "netstandard2.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
},
"frameworks": {
"netstandard2.0": {
"targetAlias": "netstandard2.0",
"dependencies": {
"NETStandard.Library": {
"suppressParent": "All",
"target": "Package",
"version": "[2.0.3, )",
"autoReferenced": true
}
},
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.100/RuntimeIdentifierGraph.json"
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/anemunt/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/anemunt/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/home/anemunt/.nuget/packages/" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)netstandard.library/2.0.3/build/netstandard2.0/NETStandard.Library.targets" Condition="Exists('$(NuGetPackageRoot)netstandard.library/2.0.3/build/netstandard2.0/NETStandard.Library.targets')" />
</ImportGroup>
</Project>

View File

@@ -0,0 +1,247 @@
{
"version": 3,
"targets": {
".NETStandard,Version=v2.0": {
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"compile": {
"lib/netstandard1.0/_._": {}
},
"runtime": {
"lib/netstandard1.0/_._": {}
}
},
"NETStandard.Library/2.0.3": {
"type": "package",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
},
"compile": {
"lib/netstandard1.0/_._": {}
},
"runtime": {
"lib/netstandard1.0/_._": {}
},
"build": {
"build/netstandard2.0/NETStandard.Library.targets": {}
}
}
}
},
"libraries": {
"Microsoft.NETCore.Platforms/1.1.0": {
"sha512": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"type": "package",
"path": "microsoft.netcore.platforms/1.1.0",
"files": [
".nupkg.metadata",
".signature.p7s",
"ThirdPartyNotices.txt",
"dotnet_library_license.txt",
"lib/netstandard1.0/_._",
"microsoft.netcore.platforms.1.1.0.nupkg.sha512",
"microsoft.netcore.platforms.nuspec",
"runtime.json"
]
},
"NETStandard.Library/2.0.3": {
"sha512": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"type": "package",
"path": "netstandard.library/2.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"LICENSE.TXT",
"THIRD-PARTY-NOTICES.TXT",
"build/netstandard2.0/NETStandard.Library.targets",
"build/netstandard2.0/ref/Microsoft.Win32.Primitives.dll",
"build/netstandard2.0/ref/System.AppContext.dll",
"build/netstandard2.0/ref/System.Collections.Concurrent.dll",
"build/netstandard2.0/ref/System.Collections.NonGeneric.dll",
"build/netstandard2.0/ref/System.Collections.Specialized.dll",
"build/netstandard2.0/ref/System.Collections.dll",
"build/netstandard2.0/ref/System.ComponentModel.Composition.dll",
"build/netstandard2.0/ref/System.ComponentModel.EventBasedAsync.dll",
"build/netstandard2.0/ref/System.ComponentModel.Primitives.dll",
"build/netstandard2.0/ref/System.ComponentModel.TypeConverter.dll",
"build/netstandard2.0/ref/System.ComponentModel.dll",
"build/netstandard2.0/ref/System.Console.dll",
"build/netstandard2.0/ref/System.Core.dll",
"build/netstandard2.0/ref/System.Data.Common.dll",
"build/netstandard2.0/ref/System.Data.dll",
"build/netstandard2.0/ref/System.Diagnostics.Contracts.dll",
"build/netstandard2.0/ref/System.Diagnostics.Debug.dll",
"build/netstandard2.0/ref/System.Diagnostics.FileVersionInfo.dll",
"build/netstandard2.0/ref/System.Diagnostics.Process.dll",
"build/netstandard2.0/ref/System.Diagnostics.StackTrace.dll",
"build/netstandard2.0/ref/System.Diagnostics.TextWriterTraceListener.dll",
"build/netstandard2.0/ref/System.Diagnostics.Tools.dll",
"build/netstandard2.0/ref/System.Diagnostics.TraceSource.dll",
"build/netstandard2.0/ref/System.Diagnostics.Tracing.dll",
"build/netstandard2.0/ref/System.Drawing.Primitives.dll",
"build/netstandard2.0/ref/System.Drawing.dll",
"build/netstandard2.0/ref/System.Dynamic.Runtime.dll",
"build/netstandard2.0/ref/System.Globalization.Calendars.dll",
"build/netstandard2.0/ref/System.Globalization.Extensions.dll",
"build/netstandard2.0/ref/System.Globalization.dll",
"build/netstandard2.0/ref/System.IO.Compression.FileSystem.dll",
"build/netstandard2.0/ref/System.IO.Compression.ZipFile.dll",
"build/netstandard2.0/ref/System.IO.Compression.dll",
"build/netstandard2.0/ref/System.IO.FileSystem.DriveInfo.dll",
"build/netstandard2.0/ref/System.IO.FileSystem.Primitives.dll",
"build/netstandard2.0/ref/System.IO.FileSystem.Watcher.dll",
"build/netstandard2.0/ref/System.IO.FileSystem.dll",
"build/netstandard2.0/ref/System.IO.IsolatedStorage.dll",
"build/netstandard2.0/ref/System.IO.MemoryMappedFiles.dll",
"build/netstandard2.0/ref/System.IO.Pipes.dll",
"build/netstandard2.0/ref/System.IO.UnmanagedMemoryStream.dll",
"build/netstandard2.0/ref/System.IO.dll",
"build/netstandard2.0/ref/System.Linq.Expressions.dll",
"build/netstandard2.0/ref/System.Linq.Parallel.dll",
"build/netstandard2.0/ref/System.Linq.Queryable.dll",
"build/netstandard2.0/ref/System.Linq.dll",
"build/netstandard2.0/ref/System.Net.Http.dll",
"build/netstandard2.0/ref/System.Net.NameResolution.dll",
"build/netstandard2.0/ref/System.Net.NetworkInformation.dll",
"build/netstandard2.0/ref/System.Net.Ping.dll",
"build/netstandard2.0/ref/System.Net.Primitives.dll",
"build/netstandard2.0/ref/System.Net.Requests.dll",
"build/netstandard2.0/ref/System.Net.Security.dll",
"build/netstandard2.0/ref/System.Net.Sockets.dll",
"build/netstandard2.0/ref/System.Net.WebHeaderCollection.dll",
"build/netstandard2.0/ref/System.Net.WebSockets.Client.dll",
"build/netstandard2.0/ref/System.Net.WebSockets.dll",
"build/netstandard2.0/ref/System.Net.dll",
"build/netstandard2.0/ref/System.Numerics.dll",
"build/netstandard2.0/ref/System.ObjectModel.dll",
"build/netstandard2.0/ref/System.Reflection.Extensions.dll",
"build/netstandard2.0/ref/System.Reflection.Primitives.dll",
"build/netstandard2.0/ref/System.Reflection.dll",
"build/netstandard2.0/ref/System.Resources.Reader.dll",
"build/netstandard2.0/ref/System.Resources.ResourceManager.dll",
"build/netstandard2.0/ref/System.Resources.Writer.dll",
"build/netstandard2.0/ref/System.Runtime.CompilerServices.VisualC.dll",
"build/netstandard2.0/ref/System.Runtime.Extensions.dll",
"build/netstandard2.0/ref/System.Runtime.Handles.dll",
"build/netstandard2.0/ref/System.Runtime.InteropServices.RuntimeInformation.dll",
"build/netstandard2.0/ref/System.Runtime.InteropServices.dll",
"build/netstandard2.0/ref/System.Runtime.Numerics.dll",
"build/netstandard2.0/ref/System.Runtime.Serialization.Formatters.dll",
"build/netstandard2.0/ref/System.Runtime.Serialization.Json.dll",
"build/netstandard2.0/ref/System.Runtime.Serialization.Primitives.dll",
"build/netstandard2.0/ref/System.Runtime.Serialization.Xml.dll",
"build/netstandard2.0/ref/System.Runtime.Serialization.dll",
"build/netstandard2.0/ref/System.Runtime.dll",
"build/netstandard2.0/ref/System.Security.Claims.dll",
"build/netstandard2.0/ref/System.Security.Cryptography.Algorithms.dll",
"build/netstandard2.0/ref/System.Security.Cryptography.Csp.dll",
"build/netstandard2.0/ref/System.Security.Cryptography.Encoding.dll",
"build/netstandard2.0/ref/System.Security.Cryptography.Primitives.dll",
"build/netstandard2.0/ref/System.Security.Cryptography.X509Certificates.dll",
"build/netstandard2.0/ref/System.Security.Principal.dll",
"build/netstandard2.0/ref/System.Security.SecureString.dll",
"build/netstandard2.0/ref/System.ServiceModel.Web.dll",
"build/netstandard2.0/ref/System.Text.Encoding.Extensions.dll",
"build/netstandard2.0/ref/System.Text.Encoding.dll",
"build/netstandard2.0/ref/System.Text.RegularExpressions.dll",
"build/netstandard2.0/ref/System.Threading.Overlapped.dll",
"build/netstandard2.0/ref/System.Threading.Tasks.Parallel.dll",
"build/netstandard2.0/ref/System.Threading.Tasks.dll",
"build/netstandard2.0/ref/System.Threading.Thread.dll",
"build/netstandard2.0/ref/System.Threading.ThreadPool.dll",
"build/netstandard2.0/ref/System.Threading.Timer.dll",
"build/netstandard2.0/ref/System.Threading.dll",
"build/netstandard2.0/ref/System.Transactions.dll",
"build/netstandard2.0/ref/System.ValueTuple.dll",
"build/netstandard2.0/ref/System.Web.dll",
"build/netstandard2.0/ref/System.Windows.dll",
"build/netstandard2.0/ref/System.Xml.Linq.dll",
"build/netstandard2.0/ref/System.Xml.ReaderWriter.dll",
"build/netstandard2.0/ref/System.Xml.Serialization.dll",
"build/netstandard2.0/ref/System.Xml.XDocument.dll",
"build/netstandard2.0/ref/System.Xml.XPath.XDocument.dll",
"build/netstandard2.0/ref/System.Xml.XPath.dll",
"build/netstandard2.0/ref/System.Xml.XmlDocument.dll",
"build/netstandard2.0/ref/System.Xml.XmlSerializer.dll",
"build/netstandard2.0/ref/System.Xml.dll",
"build/netstandard2.0/ref/System.dll",
"build/netstandard2.0/ref/mscorlib.dll",
"build/netstandard2.0/ref/netstandard.dll",
"build/netstandard2.0/ref/netstandard.xml",
"lib/netstandard1.0/_._",
"netstandard.library.2.0.3.nupkg.sha512",
"netstandard.library.nuspec"
]
}
},
"projectFileDependencyGroups": {
".NETStandard,Version=v2.0": [
"NETStandard.Library >= 2.0.3"
]
},
"packageFolders": {
"/home/anemunt/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj",
"projectName": "ModuCPP",
"projectPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj",
"packagesPath": "/home/anemunt/.nuget/packages/",
"outputPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/anemunt/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"netstandard2.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"netstandard2.0": {
"targetAlias": "netstandard2.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
},
"frameworks": {
"netstandard2.0": {
"targetAlias": "netstandard2.0",
"dependencies": {
"NETStandard.Library": {
"suppressParent": "All",
"target": "Package",
"version": "[2.0.3, )",
"autoReferenced": true
}
},
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.100/RuntimeIdentifierGraph.json"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"dgSpecHash": "iTrhv2TT9CE=",
"success": true,
"projectFilePath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj",
"expectedPackageFiles": [
"/home/anemunt/.nuget/packages/microsoft.netcore.platforms/1.1.0/microsoft.netcore.platforms.1.1.0.nupkg.sha512",
"/home/anemunt/.nuget/packages/netstandard.library/2.0.3/netstandard.library.2.0.3.nupkg.sha512"
],
"logs": []
}

View File

@@ -0,0 +1,74 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
namespace {
float walkSpeed = 4.0f;
float runSpeed = 7.0f;
float acceleration = 18.0f;
float drag = 8.0f;
bool useRigidbody2D = true;
bool warnedMissingRb = false;
} // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ctx.AutoSetting("walkSpeed", walkSpeed);
ctx.AutoSetting("runSpeed", runSpeed);
ctx.AutoSetting("acceleration", acceleration);
ctx.AutoSetting("drag", drag);
ctx.AutoSetting("useRigidbody2D", useRigidbody2D);
ImGui::TextUnformatted("Top Down Movement 2D");
ImGui::Separator();
ImGui::DragFloat("Walk Speed", &walkSpeed, 0.1f, 0.0f, 50.0f, "%.2f");
ImGui::DragFloat("Run Speed", &runSpeed, 0.1f, 0.0f, 80.0f, "%.2f");
ImGui::DragFloat("Acceleration", &acceleration, 0.1f, 0.0f, 200.0f, "%.2f");
ImGui::DragFloat("Drag", &drag, 0.1f, 0.0f, 200.0f, "%.2f");
ImGui::Checkbox("Use Rigidbody2D", &useRigidbody2D);
}
void TickUpdate(ScriptContext& ctx, float dt) {
if (!ctx.object || dt <= 0.0f) return;
glm::vec2 input(0.0f);
if (ImGui::IsKeyDown(ImGuiKey_W)) input.y += 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_S)) input.y -= 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_D)) input.x += 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_A)) input.x -= 1.0f;
if (glm::length(input) > 1e-3f) input = glm::normalize(input);
float speed = ctx.IsSprintDown() ? runSpeed : walkSpeed;
glm::vec2 targetVel = input * speed;
if (useRigidbody2D) {
if (!ctx.HasRigidbody2D()) {
if (!warnedMissingRb) {
ctx.AddConsoleMessage("TopDownMovement2D: add Rigidbody2D to use velocity-based motion.", ConsoleMessageType::Warning);
warnedMissingRb = true;
}
return;
}
glm::vec2 vel(0.0f);
ctx.GetRigidbody2DVelocity(vel);
if (acceleration <= 0.0f) {
vel = targetVel;
} else {
glm::vec2 dv = targetVel - vel;
float maxDelta = acceleration * dt;
float len = glm::length(dv);
if (len > maxDelta && len > 1e-4f) {
dv *= (maxDelta / len);
}
vel += dv;
}
if (glm::length(input) < 1e-3f && drag > 0.0f) {
float damp = std::max(0.0f, 1.0f - drag * dt);
vel *= damp;
}
ctx.SetRigidbody2DVelocity(vel);
} else {
glm::vec2 pos = ctx.object->ui.position;
pos += targetVel * dt;
ctx.SetPosition2D(pos);
}
}

BIN
TheSunset.ttf Normal file

Binary file not shown.

BIN
Thesunsethd-Regular (1).ttf Normal file

Binary file not shown.

View File

@@ -23,18 +23,56 @@ trap finish EXIT
echo -e "================================\n Modularity - Native Linux Builder\n================================"
clean_build=0
for arg in "$@"; do
if [ "$arg" = "--clean" ]; then
clean_build=1
fi
done
git submodule update --init --recursive
if [ -d "build" ]; then
echo -e "[i]: Oh! We found an existing build directory.\nRemoving existing folder..."
if [ -d "build" ] && [ $clean_build -eq 1 ]; then
echo -e "[i]: Cleaning existing build directory..."
rm -rf build/
echo -e "[i]: Build Has been Removed\nContinuing build"
fi
mkdir -p build
cd build
cmake ..
cmake .. -DMONO_ROOT=/usr
cmake --build . -- -j"$(nproc)"
mkdir -p Packages/ThirdParty
find . -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
-not -path "./Packages/*" -exec cp -f {} Packages/ThirdParty/ \;
mkdir -p Packages/Engine
find . -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
-not -path "./Packages/*" -exec cp -f {} Packages/Engine/ \;
cd ..
player_cache_dir="build/player-cache"
if [ $clean_build -eq 1 ] && [ -d "$player_cache_dir" ]; then
echo -e "[i]: Cleaning player cache build directory..."
rm -rf "$player_cache_dir"
fi
mkdir -p "$player_cache_dir"
cmake -S . -B "$player_cache_dir" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE=Release -DMODULARITY_BUILD_EDITOR=OFF
cmake --build "$player_cache_dir" --target ModularityPlayer -- -j"$(nproc)"
mkdir -p "$player_cache_dir/Packages/ThirdParty"
find "$player_cache_dir" -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
-not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/ThirdParty/" \;
mkdir -p "$player_cache_dir/Packages/Engine"
find "$player_cache_dir" -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
-not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/Engine/" \;
cd build
cp -r ../Resources .
cp Resources/imgui.ini .
ln -sf build/compile_commands.json compile_commands.json
ln -sf build/compile_commands.json compile_commands.json

View File

@@ -12,6 +12,7 @@ Scripts in Modularity are native C++ code compiled into shared libraries and loa
## Table of contents
- [Quickstart](#quickstart)
- [C# managed scripting (experimental)](#c-managed-scripting-experimental)
- [Scripts.modu](#scriptsmodu)
- [How compilation works](#how-compilation-works)
- [Lifecycle hooks](#lifecycle-hooks)
@@ -36,6 +37,29 @@ Scripts in Modularity are native C++ code compiled into shared libraries and loa
- In the Inspectors script component menu, choose **Compile**.
5. Implement a tick hook (`TickUpdate`) and observe behavior in play mode.
## C# managed scripting (experimental)
Modularity can host managed C# scripts via the .NET runtime. This is an early, minimal integration
intended for movement/transform tests and simple Rigidbody control.
1. Build the managed project (this now happens automatically when you compile a C# script):
- `dotnet build Scripts/Managed/ModuCPP.csproj`
2. In the Inspector, add a Script component and set:
- `Language` = **C#**
- `Assembly Path` = `Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll` (or point at `Scripts/Managed/SampleInspector.cs`)
- `Type` = `ModuCPP.SampleInspector`
3. Enter play mode. The sample script will auto-rotate the object.
Notes:
- The `ModuCPP.runtimeconfig.json` produced by `dotnet build` must sit next to the DLL.
- The managed host currently expects the script assembly to also contain `ModuCPP.Host`
(use the provided `Scripts/Managed/ModuCPP.csproj` as the entry assembly).
- The managed API surface is tiny for now: position/rotation/scale, basic Rigidbody velocity/forces,
settings, and console logging.
- Requires a local .NET runtime (Windows/Linux). If the runtime is missing, the engine will fail to
initialize managed scripts and report the error in the inspector.
- Managed hooks should be exported as `Script_Begin`, `Script_TickUpdate`, etc. via
`[UnmanagedCallersOnly]` in the C# script class.
## Scripts.modu
Each project has a `Scripts.modu` file (auto-created if missing). It controls compilation.

17
docs/mono-embedding.md Normal file
View File

@@ -0,0 +1,17 @@
# Mono Embedding Setup
This project uses Mono embedding for managed (C#) scripts.
Expected layout (vendored):
`src/ThirdParty/mono/`
- `include/mono-2.0/`
- `lib/` (or `lib64/`) with `mono-2.0-sgen` library
- `etc/mono/` (config files)
- `lib/mono/4.5/` (framework assemblies)
You can override the runtime location at runtime with:
`MODU_MONO_ROOT=/path/to/mono`
Build notes:
- The CMake cache variable `MONO_ROOT` controls where headers/libs are found.
- Managed scripts target `netstandard2.0` and are built with `dotnet build`.

View File

@@ -19,6 +19,7 @@ public:
void setVec2(const std::string &name, const glm::vec2 &value) const;
void setVec3(const std::string &name, const glm::vec3 &value) const;
void setMat4(const std::string &name, const glm::mat4 &mat) const;
void setMat4Array(const std::string &name, const glm::mat4 *data, int count) const;
private:
std::string readShaderFile(const char* filePath);

View File

@@ -2,10 +2,143 @@
#include "../include/ThirdParty/miniaudio.h"
#include "AudioSystem.h"
#include <cmath>
#include <atomic>
#include <array>
namespace {
constexpr size_t kPreviewBuckets = 800;
constexpr ma_uint32 kPreviewChunkFrames = 2048;
constexpr float kReverbSmoothing = 0.12f;
constexpr size_t kReverbCombCount = 4;
constexpr size_t kReverbAllpassCount = 2;
constexpr float kReverbPreDelayMaxSeconds = 0.2f;
constexpr float kReverbReflectionsMaxSeconds = 0.1f;
float DbToLinear(float db) {
return std::pow(10.0f, db / 20.0f);
}
struct ReverbNodeVTable {
ma_node_vtable vtable;
};
static void reverb_node_process(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn,
float** ppFramesOut, ma_uint32* pFrameCountOut) {
if (!pNode || !ppFramesIn || !ppFramesOut) return;
auto* node = reinterpret_cast<AudioSystem::SimpleReverbNode*>(pNode);
(void)pFrameCountIn;
const float* input = ppFramesIn[0];
float* output = ppFramesOut[0];
if (!input || !output) return;
ma_uint32 frameCount = *pFrameCountOut;
int channels = node->channels;
float decayTime = std::max(0.1f, node->decayTime);
float diffusion = std::clamp(node->diffusion, 0.0f, 100.0f);
float density = std::clamp(node->density, 0.0f, 100.0f);
float preDelaySeconds = std::clamp(node->preDelaySeconds, 0.0f, kReverbPreDelayMaxSeconds);
float reflectionsDelaySeconds = std::clamp(node->reflectionsDelaySeconds, 0.0f, kReverbReflectionsMaxSeconds);
size_t preDelayFrames = static_cast<size_t>(preDelaySeconds * static_cast<float>(node->sampleRate));
size_t reflectionsDelayFrames = static_cast<size_t>(reflectionsDelaySeconds * static_cast<float>(node->sampleRate));
float wetGain = std::clamp(node->wetGain, 0.0f, 2.0f);
float reflectionsGain = std::clamp(node->reflectionsGain, 0.0f, 2.0f);
float diffusionNorm = diffusion / 100.0f;
float densityNorm = density / 100.0f;
float allpassGain = 0.2f + 0.55f * diffusionNorm;
float densityScale = 0.6f + 0.4f * densityNorm;
float combGain = 1.0f / static_cast<float>(node->combBuffers.size());
std::array<float, kReverbCombCount> combFeedback{};
for (size_t i = 0; i < node->combBuffers.size(); ++i) {
float delaySec = static_cast<float>(node->combBuffers[i].size() / channels) / static_cast<float>(node->sampleRate);
combFeedback[i] = std::pow(10.0f, (-3.0f * delaySec) / decayTime) * densityScale;
}
float cutoffHz = std::clamp(node->hfReference * node->decayHFRatio, 500.0f, 20000.0f);
float lpAlpha = std::exp(-2.0f * PI * cutoffHz / static_cast<float>(node->sampleRate));
for (ma_uint32 frame = 0; frame < frameCount; ++frame) {
size_t preReadIndex = node->preDelayMaxFrames > 0
? (node->preDelayIndex + node->preDelayMaxFrames - preDelayFrames) % node->preDelayMaxFrames
: 0;
size_t reflectionsReadIndex = node->reflectionsMaxFrames > 0
? (node->reflectionsIndex + node->reflectionsMaxFrames - reflectionsDelayFrames) % node->reflectionsMaxFrames
: 0;
for (int ch = 0; ch < channels; ++ch) {
float inSample = input[frame * channels + ch];
float preSample = inSample;
if (!node->preDelayBuffer.empty()) {
size_t writeBase = node->preDelayIndex * channels;
size_t readBase = preReadIndex * channels;
preSample = node->preDelayBuffer[readBase + ch];
node->preDelayBuffer[writeBase + ch] = inSample;
}
float reflectionsSample = 0.0f;
if (!node->reflectionsBuffer.empty()) {
size_t writeBase = node->reflectionsIndex * channels;
size_t readBase = reflectionsReadIndex * channels;
reflectionsSample = node->reflectionsBuffer[readBase + ch];
node->reflectionsBuffer[writeBase + ch] = preSample;
}
float combSum = 0.0f;
for (size_t i = 0; i < node->combBuffers.size(); ++i) {
auto& buffer = node->combBuffers[i];
size_t idx = node->combIndex[i];
size_t base = idx * channels + ch;
float y = buffer[base];
buffer[base] = preSample + y * combFeedback[i];
combSum += y;
}
combSum *= combGain;
float apOut = combSum;
for (size_t i = 0; i < node->allpassBuffers.size(); ++i) {
auto& buffer = node->allpassBuffers[i];
size_t idx = node->allpassIndex[i];
size_t base = idx * channels + ch;
float buf = buffer[base];
float y = -allpassGain * apOut + buf;
buffer[base] = apOut + buf * allpassGain;
apOut = y;
}
float wetSample = apOut * wetGain + reflectionsSample * reflectionsGain;
float lp = node->lpState.empty() ? wetSample : (lpAlpha * node->lpState[ch] + (1.0f - lpAlpha) * wetSample);
if (!node->lpState.empty()) node->lpState[ch] = lp;
output[frame * channels + ch] = lp;
}
if (!node->preDelayBuffer.empty()) {
node->preDelayIndex = (node->preDelayIndex + 1) % node->preDelayMaxFrames;
}
if (!node->reflectionsBuffer.empty()) {
node->reflectionsIndex = (node->reflectionsIndex + 1) % node->reflectionsMaxFrames;
}
for (size_t i = 0; i < node->combIndex.size(); ++i) {
node->combIndex[i] = (node->combIndex[i] + 1) % (node->combBuffers[i].size() / channels);
}
for (size_t i = 0; i < node->allpassIndex.size(); ++i) {
node->allpassIndex[i] = (node->allpassIndex[i] + 1) % (node->allpassBuffers[i].size() / channels);
}
}
}
static ma_result reverb_node_get_required_input_frames(ma_node* pNode, ma_uint32 outputFrameCount, ma_uint32* pInputFrameCount) {
(void)pNode;
if (pInputFrameCount) *pInputFrameCount = outputFrameCount;
return MA_SUCCESS;
}
static ma_node_vtable g_reverb_node_vtable = {
reverb_node_process,
reverb_node_get_required_input_frames,
1,
1,
0
};
}
bool AudioSystem::init() {
@@ -15,6 +148,65 @@ bool AudioSystem::init() {
std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n";
return false;
}
ma_uint32 channels = ma_engine_get_channels(&engine);
ma_uint32 sampleRate = ma_engine_get_sample_rate(&engine);
ma_splitter_node_config splitterConfig = ma_splitter_node_config_init(channels);
res = ma_splitter_node_init(ma_engine_get_node_graph(&engine), &splitterConfig, nullptr, &reverbSplitter);
if (res == MA_SUCCESS) {
ma_node_config nodeConfig = ma_node_config_init();
nodeConfig.vtable = &g_reverb_node_vtable;
nodeConfig.pInputChannels = reinterpret_cast<const ma_uint32*>(&channels);
nodeConfig.pOutputChannels = reinterpret_cast<const ma_uint32*>(&channels);
res = ma_node_init(ma_engine_get_node_graph(&engine), &nodeConfig, nullptr, reinterpret_cast<ma_node*>(&reverbNode));
if (res == MA_SUCCESS) {
reverbNode.channels = static_cast<int>(channels);
reverbNode.sampleRate = static_cast<int>(sampleRate);
reverbNode.preDelayMaxFrames = static_cast<size_t>(kReverbPreDelayMaxSeconds * sampleRate);
reverbNode.reflectionsMaxFrames = static_cast<size_t>(kReverbReflectionsMaxSeconds * sampleRate);
reverbNode.preDelayBuffer.assign(reverbNode.preDelayMaxFrames * channels, 0.0f);
reverbNode.reflectionsBuffer.assign(reverbNode.reflectionsMaxFrames * channels, 0.0f);
reverbNode.lpState.assign(channels, 0.0f);
const float combDelayMs[kReverbCombCount] = { 29.7f, 37.1f, 41.1f, 43.7f };
reverbNode.combBuffers.resize(kReverbCombCount);
reverbNode.combIndex.assign(kReverbCombCount, 0);
for (size_t i = 0; i < kReverbCombCount; ++i) {
size_t frames = static_cast<size_t>((combDelayMs[i] / 1000.0f) * sampleRate);
frames = std::max<size_t>(1, frames);
reverbNode.combBuffers[i].assign(frames * channels, 0.0f);
}
const float allpassDelayMs[kReverbAllpassCount] = { 5.0f, 1.7f };
reverbNode.allpassBuffers.resize(kReverbAllpassCount);
reverbNode.allpassIndex.assign(kReverbAllpassCount, 0);
for (size_t i = 0; i < kReverbAllpassCount; ++i) {
size_t frames = static_cast<size_t>((allpassDelayMs[i] / 1000.0f) * sampleRate);
frames = std::max<size_t>(1, frames);
reverbNode.allpassBuffers[i].assign(frames * channels, 0.0f);
}
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbSplitter), 0, ma_engine_get_endpoint(&engine), 0);
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbSplitter), 1, reinterpret_cast<ma_node*>(&reverbNode), 0);
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbNode), 0, ma_engine_get_endpoint(&engine), 0);
ma_sound_group_config groupConfig = ma_sound_group_config_init_2(&engine);
groupConfig.pInitialAttachment = reinterpret_cast<ma_node*>(&reverbSplitter);
groupConfig.initialAttachmentInputBusIndex = 0;
res = ma_sound_group_init_ex(&engine, &groupConfig, &reverbGroup);
if (res == MA_SUCCESS) {
reverbReady = true;
ma_sound_group_set_spatialization_enabled(&reverbGroup, MA_FALSE);
ma_sound_group_set_attenuation_model(&reverbGroup, ma_attenuation_model_none);
ma_sound_group_start(&reverbGroup);
ma_node_set_output_bus_volume(reinterpret_cast<ma_node*>(&reverbSplitter), 0, 1.0f);
} else {
ma_node_uninit(reinterpret_cast<ma_node*>(&reverbNode), nullptr);
ma_splitter_node_uninit(&reverbSplitter, nullptr);
}
} else {
ma_splitter_node_uninit(&reverbSplitter, nullptr);
}
}
initialized = true;
return true;
}
@@ -22,6 +214,7 @@ bool AudioSystem::init() {
void AudioSystem::shutdown() {
stopPreview();
destroyActiveSounds();
shutdownReverbGraph();
if (initialized) {
ma_engine_uninit(&engine);
initialized = false;
@@ -81,7 +274,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
&engine,
obj.audioSource.clipPath.c_str(),
MA_SOUND_FLAG_STREAM,
nullptr,
reverbReady ? &reverbGroup : nullptr,
nullptr,
&snd->sound
);
@@ -104,6 +297,26 @@ void AudioSystem::refreshSoundParams(const SceneObject& obj, ActiveSound& snd) {
ma_sound_set_looping(&snd.sound, obj.audioSource.loop ? MA_TRUE : MA_FALSE);
ma_sound_set_volume(&snd.sound, obj.audioSource.volume);
ma_sound_set_spatialization_enabled(&snd.sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE);
if (obj.audioSource.spatial) {
switch (obj.audioSource.rolloffMode) {
case AudioRolloffMode::Linear:
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_linear);
break;
case AudioRolloffMode::Exponential:
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_exponential);
break;
case AudioRolloffMode::Custom:
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none);
break;
case AudioRolloffMode::Logarithmic:
default:
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_inverse);
break;
}
ma_sound_set_rolloff(&snd.sound, std::max(0.01f, obj.audioSource.rolloff));
} else {
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none);
}
ma_sound_set_min_distance(&snd.sound, minDist);
ma_sound_set_max_distance(&snd.sound, maxDist);
ma_sound_set_position(&snd.sound, obj.position.x, obj.position.y, obj.position.z);
@@ -120,6 +333,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
ma_engine_listener_set_position(&engine, 0, listenerCamera.position.x, listenerCamera.position.y, listenerCamera.position.z);
ma_engine_listener_set_direction(&engine, 0, listenerCamera.front.x, listenerCamera.front.y, listenerCamera.front.z);
ma_engine_listener_set_world_up(&engine, 0, listenerCamera.up.x, listenerCamera.up.y, listenerCamera.up.z);
updateReverb(objects, listenerCamera.position);
if (!playing) {
destroyActiveSounds();
@@ -144,6 +358,10 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
if (ensureSoundFor(obj)) {
refreshSoundParams(obj, *activeSounds[obj.id]);
if (obj.audioSource.spatial && obj.audioSource.rolloffMode == AudioRolloffMode::Custom) {
float attenuation = computeCustomAttenuation(obj, listenerCamera.position);
ma_sound_set_volume(&activeSounds[obj.id]->sound, obj.audioSource.volume * attenuation);
}
}
}
@@ -253,6 +471,152 @@ bool AudioSystem::setObjectVolume(const SceneObject& obj, float volume) {
return true;
}
float AudioSystem::computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const {
float minDist = std::max(0.1f, obj.audioSource.minDistance);
float maxDist = std::max(obj.audioSource.maxDistance, minDist + 0.5f);
float dist = glm::length(listenerPos - obj.position);
if (dist <= minDist) return 1.0f;
if (dist >= maxDist) return std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f);
float range = maxDist - minDist;
float midRatio = std::clamp(obj.audioSource.customMidDistance, 0.0f, 1.0f);
float midDist = minDist + range * midRatio;
float midGain = std::clamp(obj.audioSource.customMidGain, 0.0f, 1.0f);
float endGain = std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f);
if (dist <= midDist) {
float t = (dist - minDist) / std::max(0.001f, midDist - minDist);
return std::clamp(1.0f + (midGain - 1.0f) * t, 0.0f, 1.0f);
}
float t = (dist - midDist) / std::max(0.001f, maxDist - midDist);
return std::clamp(midGain + (endGain - midGain) * t, 0.0f, 1.0f);
}
void AudioSystem::updateReverb(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos) {
if (!reverbReady) return;
float blend = 0.0f;
ReverbSettings target = getReverbTarget(objects, listenerPos, blend);
applyReverbSettings(target, blend);
}
AudioSystem::ReverbSettings AudioSystem::getReverbTarget(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos, float& outBlend) const {
ReverbSettings target{};
float bestBlend = 0.0f;
for (const auto& obj : objects) {
if (!obj.enabled || !obj.hasReverbZone || !obj.reverbZone.enabled) continue;
const auto& zone = obj.reverbZone;
float blend = 0.0f;
if (zone.shape == ReverbZoneShape::Sphere) {
float minDist = std::max(0.0f, zone.minDistance);
float maxDist = std::max(zone.maxDistance, minDist + 0.01f);
float radius = std::max(0.01f, zone.radius);
float dist = glm::length(listenerPos - obj.position);
if (dist > radius) continue;
maxDist = std::min(maxDist, radius);
if (dist >= maxDist) continue;
if (dist <= minDist) {
blend = 1.0f;
} else {
blend = std::clamp((maxDist - dist) / (maxDist - minDist), 0.0f, 1.0f);
}
} else {
glm::vec3 halfSize = glm::max(zone.boxSize * 0.5f, glm::vec3(0.01f));
glm::vec3 delta = glm::abs(listenerPos - obj.position);
if (delta.x > halfSize.x || delta.y > halfSize.y || delta.z > halfSize.z) continue;
float edgeDistance = std::min({halfSize.x - delta.x, halfSize.y - delta.y, halfSize.z - delta.z});
if (zone.blendDistance <= 0.001f) {
blend = 1.0f;
} else {
blend = std::clamp(edgeDistance / zone.blendDistance, 0.0f, 1.0f);
}
}
if (blend > bestBlend) {
bestBlend = blend;
target.room = zone.room;
target.roomHF = zone.roomHF;
target.roomLF = zone.roomLF;
target.decayTime = zone.decayTime;
target.decayHFRatio = zone.decayHFRatio;
target.reflections = zone.reflections;
target.reflectionsDelay = zone.reflectionsDelay;
target.reverb = zone.reverb;
target.reverbDelay = zone.reverbDelay;
target.hfReference = zone.hfReference;
target.lfReference = zone.lfReference;
target.roomRolloffFactor = zone.roomRolloffFactor;
target.diffusion = zone.diffusion;
target.density = zone.density;
}
}
outBlend = bestBlend;
return target;
}
void AudioSystem::applyReverbSettings(const ReverbSettings& target, float blend) {
ReverbSettings mixed{};
mixed.room = target.room;
mixed.roomHF = target.roomHF;
mixed.roomLF = target.roomLF;
mixed.decayTime = std::max(0.1f, target.decayTime);
mixed.decayHFRatio = std::clamp(target.decayHFRatio, 0.1f, 2.0f);
mixed.reflections = target.reflections;
mixed.reflectionsDelay = std::clamp(target.reflectionsDelay, 0.0f, kReverbReflectionsMaxSeconds);
mixed.reverb = target.reverb;
mixed.reverbDelay = std::clamp(target.reverbDelay, 0.0f, kReverbPreDelayMaxSeconds);
mixed.hfReference = std::clamp(target.hfReference, 1000.0f, 20000.0f);
mixed.lfReference = std::clamp(target.lfReference, 20.0f, 1000.0f);
mixed.roomRolloffFactor = std::max(0.0f, target.roomRolloffFactor);
mixed.diffusion = std::clamp(target.diffusion, 0.0f, 100.0f);
mixed.density = std::clamp(target.density, 0.0f, 100.0f);
currentReverb.room = currentReverb.room + (mixed.room - currentReverb.room) * kReverbSmoothing;
currentReverb.roomHF = currentReverb.roomHF + (mixed.roomHF - currentReverb.roomHF) * kReverbSmoothing;
currentReverb.roomLF = currentReverb.roomLF + (mixed.roomLF - currentReverb.roomLF) * kReverbSmoothing;
currentReverb.decayTime = currentReverb.decayTime + (mixed.decayTime - currentReverb.decayTime) * kReverbSmoothing;
currentReverb.decayHFRatio = currentReverb.decayHFRatio + (mixed.decayHFRatio - currentReverb.decayHFRatio) * kReverbSmoothing;
currentReverb.reflections = currentReverb.reflections + (mixed.reflections - currentReverb.reflections) * kReverbSmoothing;
currentReverb.reflectionsDelay = currentReverb.reflectionsDelay + (mixed.reflectionsDelay - currentReverb.reflectionsDelay) * kReverbSmoothing;
currentReverb.reverb = currentReverb.reverb + (mixed.reverb - currentReverb.reverb) * kReverbSmoothing;
currentReverb.reverbDelay = currentReverb.reverbDelay + (mixed.reverbDelay - currentReverb.reverbDelay) * kReverbSmoothing;
currentReverb.hfReference = currentReverb.hfReference + (mixed.hfReference - currentReverb.hfReference) * kReverbSmoothing;
currentReverb.lfReference = currentReverb.lfReference + (mixed.lfReference - currentReverb.lfReference) * kReverbSmoothing;
currentReverb.roomRolloffFactor = currentReverb.roomRolloffFactor + (mixed.roomRolloffFactor - currentReverb.roomRolloffFactor) * kReverbSmoothing;
currentReverb.diffusion = currentReverb.diffusion + (mixed.diffusion - currentReverb.diffusion) * kReverbSmoothing;
currentReverb.density = currentReverb.density + (mixed.density - currentReverb.density) * kReverbSmoothing;
constexpr float kDbSoftening = 0.5f;
constexpr float kWetScale = 0.25f;
float reflectionsGain = DbToLinear((currentReverb.reflections + currentReverb.room) * kDbSoftening) * (blend * kWetScale);
float reverbGain = DbToLinear((currentReverb.reverb + currentReverb.room) * kDbSoftening) * (blend * kWetScale);
float dry = std::clamp(1.0f - blend * (currentReverb.roomRolloffFactor * 0.05f), 0.2f, 1.0f);
ma_node_set_output_bus_volume(reinterpret_cast<ma_node*>(&reverbSplitter), 0, dry);
reverbNode.wetGain = std::clamp(reverbGain, 0.0f, 1.0f);
reverbNode.reflectionsGain = std::clamp(reflectionsGain, 0.0f, 1.0f);
reverbNode.decayTime = currentReverb.decayTime;
reverbNode.decayHFRatio = currentReverb.decayHFRatio;
reverbNode.diffusion = currentReverb.diffusion;
reverbNode.density = currentReverb.density;
reverbNode.hfReference = currentReverb.hfReference;
reverbNode.preDelaySeconds = currentReverb.reverbDelay;
reverbNode.reflectionsDelaySeconds = currentReverb.reflectionsDelay;
}
void AudioSystem::shutdownReverbGraph() {
if (reverbReady) {
ma_sound_group_uninit(&reverbGroup);
ma_node_uninit(reinterpret_cast<ma_node*>(&reverbNode), nullptr);
ma_splitter_node_uninit(&reverbSplitter, nullptr);
reverbReady = false;
}
currentReverb = ReverbSettings{};
}
AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
AudioClipPreview preview;
preview.path = path;

View File

@@ -42,7 +42,51 @@ public:
bool setObjectLoop(const SceneObject& obj, bool loop);
bool setObjectVolume(const SceneObject& obj, float volume);
struct SimpleReverbNode {
ma_node_base baseNode;
int channels = 0;
int sampleRate = 0;
std::vector<std::vector<float>> combBuffers;
std::vector<size_t> combIndex;
std::vector<std::vector<float>> allpassBuffers;
std::vector<size_t> allpassIndex;
std::vector<float> preDelayBuffer;
size_t preDelayIndex = 0;
std::vector<float> reflectionsBuffer;
size_t reflectionsIndex = 0;
std::vector<float> lpState;
float wetGain = 0.0f;
float reflectionsGain = 0.0f;
float decayTime = 1.5f;
float decayHFRatio = 0.5f;
float diffusion = 100.0f;
float density = 100.0f;
float hfReference = 5000.0f;
float preDelaySeconds = 0.01f;
float reflectionsDelaySeconds = 0.01f;
size_t preDelayMaxFrames = 0;
size_t reflectionsMaxFrames = 0;
};
private:
struct ReverbSettings {
float room = -10000.0f;
float roomHF = -10000.0f;
float roomLF = -10000.0f;
float decayTime = 1.5f;
float decayHFRatio = 0.5f;
float reflections = -10000.0f;
float reflectionsDelay = 0.01f;
float reverb = -10000.0f;
float reverbDelay = 0.01f;
float hfReference = 5000.0f;
float lfReference = 250.0f;
float roomRolloffFactor = 0.0f;
float diffusion = 100.0f;
float density = 100.0f;
float dry = 1.0f;
};
struct ActiveSound {
ma_sound sound;
std::string clipPath;
@@ -56,6 +100,12 @@ private:
std::unordered_map<std::string, AudioClipPreview> previewCache;
std::unordered_set<std::string> missingClips;
SimpleReverbNode reverbNode{};
ma_splitter_node reverbSplitter{};
ma_sound_group reverbGroup{};
bool reverbReady = false;
ReverbSettings currentReverb{};
ma_sound previewSound{};
bool previewActive = false;
std::string previewPath;
@@ -63,5 +113,10 @@ private:
void destroyActiveSounds();
bool ensureSoundFor(const SceneObject& obj);
void refreshSoundParams(const SceneObject& obj, ActiveSound& snd);
float computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const;
AudioClipPreview loadPreview(const std::string& path);
void updateReverb(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos);
ReverbSettings getReverbTarget(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos, float& outBlend) const;
void applyReverbSettings(const ReverbSettings& target, float blend);
void shutdownReverbGraph();
};

View File

@@ -95,9 +95,10 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
if (ext == ".modu" || ext == ".scene") return FileCategory::Scene;
// Model files
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" ||
ext == ".stl" || ext == ".x" || ext == ".md5mesh" || ext == ".rmesh") {
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".b3d" ||
ext == ".ply" || ext == ".stl" || ext == ".x" || ext == ".md5mesh" ||
ext == ".rmesh") {
return FileCategory::Model;
}
@@ -189,6 +190,60 @@ bool FileBrowser::matchesFilter(const fs::directory_entry& entry) const {
void applyModernTheme() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
ImGuiIO& io = ImGui::GetIO();
const float fontSizeBase = 18.0f;
const float fontSizeOffset = -2.5f;
const float fontSize = std::max(1.0f, fontSizeBase + fontSizeOffset);
ImFont* editorFont = nullptr;
fs::path primaryFontPath;
const fs::path fontCandidates[] = {
fs::path("Resources") / "Fonts" / "TheSunset.ttf",
fs::path("Resources") / "Fonts" / "Thesunsethd-Regular (1).ttf",
fs::path("TheSunset.ttf"),
fs::path("Thesunsethd-Regular (1).ttf")
};
for (const auto& fontPath : fontCandidates) {
if (!fs::exists(fontPath)) {
continue;
}
const std::string fontPathStr = fontPath.string();
editorFont = io.Fonts->AddFontFromFileTTF(fontPathStr.c_str(), fontSize);
if (editorFont) {
primaryFontPath = fontPath;
io.FontDefault = editorFont;
break;
}
}
if (!editorFont) {
std::cerr << "[WARN] Failed to load editor font (TheSunset) from Resources/Fonts."
<< std::endl;
} else {
const fs::path fallbackCandidates[] = {
fs::path("Resources") / "Fonts" / "TheSunset.ttf",
fs::path("TheSunset.ttf")
};
if (primaryFontPath.filename() != "TheSunset.ttf") {
for (const auto& fallbackPath : fallbackCandidates) {
if (!fs::exists(fallbackPath)) {
continue;
}
const std::string fallbackPathStr = fallbackPath.string();
ImFontConfig mergeConfig;
mergeConfig.MergeMode = true;
ImFont* fallbackFont = io.Fonts->AddFontFromFileTTF(
fallbackPathStr.c_str(),
fontSize,
&mergeConfig,
io.Fonts->GetGlyphRangesDefault()
);
if (!fallbackFont) {
std::cerr << "[WARN] Failed to merge fallback font: "
<< fallbackPathStr << std::endl;
}
break;
}
}
}
ImVec4 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f);
@@ -253,26 +308,113 @@ void applyModernTheme() {
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f);
applyEditorLayoutPreset(style);
}
style.WindowRounding = 10.0f;
void applyEditorLayoutPreset(ImGuiStyle& style) {
style.WindowPadding = ImVec2(3.0f, 3.0f);
style.FramePadding = ImVec2(4.0f, 4.0f);
style.ItemSpacing = ImVec2(10.0f, 5.0f);
style.ItemInnerSpacing = ImVec2(2.0f, 2.0f);
style.CellPadding = ImVec2(4.0f, 2.0f);
style.TouchExtraPadding = ImVec2(0.0f, 0.0f);
style.IndentSpacing = 11.0f;
style.GrabMinSize = 8.0f;
style.WindowBorderSize = 0.0f;
style.ChildBorderSize = 1.0f;
style.PopupBorderSize = 1.0f;
style.FrameBorderSize = 0.0f;
style.WindowRounding = 12.0f;
style.ChildRounding = 12.0f;
style.FrameRounding = 10.0f;
style.FrameRounding = 12.0f;
style.PopupRounding = 12.0f;
style.GrabRounding = 12.0f;
style.ScrollbarSize = 11.0f;
style.ScrollbarRounding = 10.0f;
style.GrabRounding = 8.0f;
style.ScrollbarPadding = 1.0f;
style.TabBorderSize = 1.0f;
style.TabBarBorderSize = 1.0f;
style.TabBarOverlineSize = 1.0f;
style.TabMinWidthBase = 1.0f;
style.TabMinWidthShrink = 80.0f;
style.TabCloseButtonMinWidthSelected = -1.0f;
style.TabCloseButtonMinWidthUnselected = 0.0f;
style.TabRounding = 10.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.TableAngledHeadersAngle = 35.0f;
style.TableAngledHeadersTextAlign = ImVec2(0.50f, 0.00f);
style.TreeLinesFlags = ImGuiTreeNodeFlags_DrawLinesNone;
style.TreeLinesSize = 1.0f;
style.TreeLinesRounding = 0.0f;
style.WindowTitleAlign = ImVec2(0.50f, 0.50f);
style.WindowBorderHoverPadding = 6.0f;
style.WindowMenuButtonPosition = ImGuiDir_None;
style.ColorButtonPosition = ImGuiDir_Right;
style.ButtonTextAlign = ImVec2(0.50f, 0.50f);
style.SelectableTextAlign = ImVec2(0.00f, 0.00f);
style.SeparatorTextBorderSize = 2.0f;
style.SeparatorTextAlign = ImVec2(0.50f, 0.50f);
style.SeparatorTextPadding = ImVec2(4.0f, 0.0f);
style.LogSliderDeadzone = 4.0f;
style.ImageBorderSize = 0.0f;
style.DockingNodeHasCloseButton = true;
style.DockingSeparatorSize = 0.0f;
style.DisplayWindowPadding = ImVec2(19.0f, 19.0f);
style.DisplaySafeAreaPadding = ImVec2(0.0f, 0.0f);
}
void applyPixelStyle(ImGuiStyle& style) {
applyEditorLayoutPreset(style);
style.WindowRounding = 0.0f;
style.ChildRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.ScrollbarRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowPadding = ImVec2(8.0f, 6.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 4.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.IndentSpacing = 14.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
style.PopupBorderSize = 1.0f;
style.TabBorderSize = 1.0f;
}
void applySuperRoundStyle(ImGuiStyle& style) {
applyEditorLayoutPreset(style);
style.WindowRounding = 18.0f;
style.ChildRounding = 16.0f;
style.FrameRounding = 16.0f;
style.PopupRounding = 16.0f;
style.ScrollbarRounding = 16.0f;
style.GrabRounding = 14.0f;
style.TabRounding = 16.0f;
style.WindowPadding = ImVec2(14.0f, 10.0f);
style.FramePadding = ImVec2(12.0f, 8.0f);
style.ItemSpacing = ImVec2(10.0f, 8.0f);
style.ItemInnerSpacing = ImVec2(8.0f, 6.0f);
style.IndentSpacing = 18.0f;
style.WindowBorderSize = 0.0f;
style.FrameBorderSize = 0.0f;
style.PopupBorderSize = 0.0f;
style.TabBorderSize = 0.0f;
}
#pragma endregion
#pragma region Dockspace

View File

@@ -69,6 +69,9 @@ public:
// Apply the modern dark theme to ImGui
void applyModernTheme();
void applyEditorLayoutPreset(ImGuiStyle& style);
void applyPixelStyle(ImGuiStyle& style);
void applySuperRoundStyle(ImGuiStyle& style);
// Setup ImGui dockspace for the editor
void setupDockspace(const std::function<void()>& menuBarContent = nullptr);

View File

@@ -0,0 +1,554 @@
#include "Engine.h"
#include "ThirdParty/imgui/imgui.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
void Engine::renderAnimationWindow() {
if (!showAnimationWindow) return;
auto clampFloat = [](float value, float minValue, float maxValue) {
return std::max(minValue, std::min(value, maxValue));
};
auto lerpVec3 = [](const glm::vec3& a, const glm::vec3& b, float t) {
return a + (b - a) * t;
};
auto applyInterpolation = [](float t, AnimationInterpolation interpolation) {
t = std::max(0.0f, std::min(1.0f, t));
switch (interpolation) {
case AnimationInterpolation::SmoothStep:
return t * t * (3.0f - 2.0f * t);
case AnimationInterpolation::EaseIn:
return t * t;
case AnimationInterpolation::EaseOut: {
float inv = 1.0f - t;
return 1.0f - inv * inv;
}
case AnimationInterpolation::EaseInOut:
return (t < 0.5f) ? (2.0f * t * t) : (1.0f - 2.0f * (1.0f - t) * (1.0f - t));
case AnimationInterpolation::Linear:
default:
return t;
}
};
const char* interpLabels[] = { "Linear", "SmoothStep", "Ease In", "Ease Out", "Ease In Out" };
const char* curveModeLabels[] = { "Preset", "Bezier" };
auto getInterpLabel = [&](AnimationInterpolation interpolation) {
int idx = static_cast<int>(interpolation);
if (idx < 0 || idx >= static_cast<int>(IM_ARRAYSIZE(interpLabels))) return "Linear";
return interpLabels[idx];
};
auto cubicBezier = [](float p0, float p1, float p2, float p3, float t) {
float inv = 1.0f - t;
return (inv * inv * inv * p0) +
(3.0f * inv * inv * t * p1) +
(3.0f * inv * t * t * p2) +
(t * t * t * p3);
};
auto cubicBezierDerivative = [](float p0, float p1, float p2, float p3, float t) {
float inv = 1.0f - t;
return (3.0f * inv * inv * (p1 - p0)) +
(6.0f * inv * t * (p2 - p1)) +
(3.0f * t * t * (p3 - p2));
};
auto applyBezier = [&](float t, const glm::vec2& outCtrl, const glm::vec2& inCtrl) {
t = std::max(0.0f, std::min(1.0f, t));
float u = t;
for (int i = 0; i < 6; ++i) {
float x = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, u);
float dx = cubicBezierDerivative(0.0f, outCtrl.x, inCtrl.x, 1.0f, u);
if (std::abs(dx) < 0.0001f) break;
u -= (x - t) / dx;
u = std::max(0.0f, std::min(1.0f, u));
}
float xCheck = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, u);
if (std::abs(xCheck - t) > 0.001f) {
float lo = 0.0f;
float hi = 1.0f;
for (int i = 0; i < 12; ++i) {
float mid = (lo + hi) * 0.5f;
float x = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, mid);
if (x < t) lo = mid;
else hi = mid;
}
u = (lo + hi) * 0.5f;
}
return cubicBezier(0.0f, outCtrl.y, inCtrl.y, 1.0f, u);
};
auto captureKeyframe = [&](SceneObject& obj) {
auto& anim = obj.animation;
float clamped = clampFloat(animationCurrentTime, 0.0f, anim.clipLength);
auto it = std::find_if(anim.keyframes.begin(), anim.keyframes.end(),
[&](const AnimationKeyframe& k) { return std::abs(k.time - clamped) < 0.0001f; });
if (it == anim.keyframes.end()) {
AnimationKeyframe key;
key.time = clamped;
key.position = obj.position;
key.rotation = obj.rotation;
key.scale = obj.scale;
key.interpolation = AnimationInterpolation::SmoothStep;
key.curveMode = AnimationCurveMode::Preset;
anim.keyframes.push_back(key);
} else {
it->position = obj.position;
it->rotation = obj.rotation;
it->scale = obj.scale;
}
std::sort(anim.keyframes.begin(), anim.keyframes.end(),
[](const AnimationKeyframe& a, const AnimationKeyframe& b) { return a.time < b.time; });
projectManager.currentProject.hasUnsavedChanges = true;
};
auto deleteKeyframe = [&](SceneObject& obj) {
auto& anim = obj.animation;
if (animationSelectedKey < 0 || animationSelectedKey >= static_cast<int>(anim.keyframes.size())) return;
anim.keyframes.erase(anim.keyframes.begin() + animationSelectedKey);
if (animationSelectedKey >= static_cast<int>(anim.keyframes.size())) {
animationSelectedKey = static_cast<int>(anim.keyframes.size()) - 1;
}
projectManager.currentProject.hasUnsavedChanges = true;
};
auto applyPoseAtTime = [&](SceneObject& obj, float time) {
auto& anim = obj.animation;
if (anim.keyframes.empty()) return;
if (time <= anim.keyframes.front().time) {
obj.position = anim.keyframes.front().position;
obj.rotation = NormalizeEulerDegrees(anim.keyframes.front().rotation);
obj.scale = anim.keyframes.front().scale;
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
return;
}
if (time >= anim.keyframes.back().time) {
obj.position = anim.keyframes.back().position;
obj.rotation = NormalizeEulerDegrees(anim.keyframes.back().rotation);
obj.scale = anim.keyframes.back().scale;
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
return;
}
for (size_t i = 0; i + 1 < anim.keyframes.size(); ++i) {
const auto& a = anim.keyframes[i];
const auto& b = anim.keyframes[i + 1];
if (time >= a.time && time <= b.time) {
float span = b.time - a.time;
float t = (span > 0.0f) ? (time - a.time) / span : 0.0f;
if (a.curveMode == AnimationCurveMode::Bezier) {
t = applyBezier(t, a.bezierOut, b.bezierIn);
} else {
t = applyInterpolation(t, a.interpolation);
}
obj.position = lerpVec3(a.position, b.position, t);
obj.rotation = NormalizeEulerDegrees(lerpVec3(a.rotation, b.rotation, t));
obj.scale = lerpVec3(a.scale, b.scale, t);
syncLocalTransform(obj);
projectManager.currentProject.hasUnsavedChanges = true;
return;
}
}
};
auto drawTimeline = [&](AnimationComponent& anim) {
ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f);
ImVec2 start = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("AnimationTimeline", size);
ImDrawList* draw = ImGui::GetWindowDrawList();
ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg);
ImU32 border = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark);
ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab);
draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f);
draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f);
float clamped = clampFloat(animationCurrentTime, 0.0f, anim.clipLength);
float playheadX = start.x + (anim.clipLength > 0.0f ? (clamped / anim.clipLength) * size.x : 0.0f);
draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f);
for (size_t i = 0; i < anim.keyframes.size(); ++i) {
float keyX = start.x +
(anim.clipLength > 0.0f ? (anim.keyframes[i].time / anim.clipLength) * size.x : 0.0f);
ImVec2 center(keyX, start.y + size.y * 0.5f);
float radius = (animationSelectedKey == static_cast<int>(i)) ? 6.0f : 4.5f;
draw->AddCircleFilled(center, radius, keyColor);
ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f));
if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
animationSelectedKey = static_cast<int>(i);
animationCurrentTime = anim.keyframes[i].time;
}
}
if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
float mouseX = ImGui::GetIO().MousePos.x;
float t = (mouseX - start.x) / size.x;
animationCurrentTime = clampFloat(t * anim.clipLength, 0.0f, anim.clipLength);
}
};
auto* selectedObj = getSelectedObject();
std::vector<SceneObject*> animTargets;
animTargets.reserve(sceneObjects.size());
for (auto& obj : sceneObjects) {
if (obj.hasAnimation) animTargets.push_back(&obj);
}
auto resolveTarget = [&]() -> SceneObject* {
if (animationTargetId < 0) return nullptr;
SceneObject* obj = findObjectById(animationTargetId);
if (!obj || !obj->hasAnimation) return nullptr;
return obj;
};
SceneObject* targetObj = resolveTarget();
if (!targetObj && !animTargets.empty()) {
animationTargetId = animTargets.front()->id;
animationSelectedKey = -1;
animationLastAppliedTime = -1.0f;
targetObj = resolveTarget();
}
ImGui::Begin("Animation", &showAnimationWindow, ImGuiWindowFlags_NoCollapse);
if (!ImGui::BeginTable("AnimatorLayout", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
ImGui::End();
return;
}
ImGui::TableSetupColumn("Targets", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::BeginChild("AnimatorTargets", ImVec2(0, 0), true);
ImGui::TextDisabled("Targets");
ImGui::Spacing();
ImGui::BeginDisabled(!selectedObj);
if (ImGui::Button("Add Animation to Selected", ImVec2(-1, 0))) {
if (selectedObj && !selectedObj->hasAnimation) {
selectedObj->hasAnimation = true;
selectedObj->animation = AnimationComponent{};
projectManager.currentProject.hasUnsavedChanges = true;
animationTargetId = selectedObj->id;
animationSelectedKey = -1;
animationLastAppliedTime = -1.0f;
animTargets.push_back(selectedObj);
} else if (selectedObj) {
animationTargetId = selectedObj->id;
}
}
ImGui::EndDisabled();
ImGui::Spacing();
if (animTargets.empty()) {
ImGui::TextDisabled("No Animation components yet.");
} else {
for (auto* obj : animTargets) {
bool selected = (targetObj && obj->id == targetObj->id);
if (ImGui::Selectable(obj->name.c_str(), selected)) {
animationTargetId = obj->id;
animationSelectedKey = -1;
animationLastAppliedTime = -1.0f;
animationCurrentTime = 0.0f;
targetObj = obj;
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Keyframes: %zu", obj->animation.keyframes.size());
ImGui::Text("Length: %.2fs", obj->animation.clipLength);
ImGui::EndTooltip();
}
}
}
ImGui::EndChild();
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("AnimatorEditor", ImVec2(0, 0), true);
if (!targetObj) {
ImGui::TextDisabled("Select or add an Animation component to edit.");
ImGui::EndChild();
ImGui::EndTable();
ImGui::End();
return;
}
auto& anim = targetObj->animation;
animationCurrentTime = clampFloat(animationCurrentTime, 0.0f, anim.clipLength);
ImGui::Text("Animator");
ImGui::SameLine();
ImGui::TextDisabled("Target: %s", targetObj->name.c_str());
ImGui::Spacing();
ImGui::Separator();
if (ImGui::BeginTabBar("AnimatorTabs")) {
if (ImGui::BeginTabItem("Pose")) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
ImGui::BeginDisabled(!anim.enabled);
if (ImGui::Button("Key")) {
captureKeyframe(*targetObj);
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(animationSelectedKey < 0);
if (ImGui::Button("Delete")) {
deleteKeyframe(*targetObj);
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(anim.keyframes.empty());
if (ImGui::Button("Sort")) {
std::sort(anim.keyframes.begin(), anim.keyframes.end(),
[](const AnimationKeyframe& a, const AnimationKeyframe& b) { return a.time < b.time; });
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::Spacing();
drawTimeline(anim);
ImGui::SliderFloat("Time", &animationCurrentTime, 0.0f, anim.clipLength, "%.2fs");
if (animationSelectedKey >= 0 && animationSelectedKey < static_cast<int>(anim.keyframes.size())) {
auto& key = anim.keyframes[animationSelectedKey];
ImGui::Separator();
ImGui::TextDisabled("Blend");
int modeIndex = static_cast<int>(key.curveMode);
ImGui::SetNextItemWidth(200.0f);
if (ImGui::Combo("Mode", &modeIndex, curveModeLabels, IM_ARRAYSIZE(curveModeLabels))) {
key.curveMode = static_cast<AnimationCurveMode>(modeIndex);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::SameLine();
if (ImGui::Button("Apply Mode To All")) {
for (auto& k : anim.keyframes) {
k.curveMode = key.curveMode;
}
projectManager.currentProject.hasUnsavedChanges = true;
}
if (key.curveMode == AnimationCurveMode::Preset) {
int interpIndex = static_cast<int>(key.interpolation);
ImGui::SetNextItemWidth(200.0f);
if (ImGui::Combo("Preset", &interpIndex, interpLabels, IM_ARRAYSIZE(interpLabels))) {
key.interpolation = static_cast<AnimationInterpolation>(interpIndex);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::SameLine();
if (ImGui::Button("Apply Preset To All")) {
for (auto& k : anim.keyframes) {
k.interpolation = key.interpolation;
}
projectManager.currentProject.hasUnsavedChanges = true;
}
} else {
ImGui::TextDisabled("Out Handle (to next)");
ImGui::SetNextItemWidth(160.0f);
if (ImGui::SliderFloat2("Out", &key.bezierOut.x, 0.0f, 1.0f, "%.2f")) {
key.bezierOut.x = clampFloat(key.bezierOut.x, 0.0f, 1.0f);
key.bezierOut.y = clampFloat(key.bezierOut.y, 0.0f, 1.0f);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::TextDisabled("In Handle (from prev)");
ImGui::SetNextItemWidth(160.0f);
if (ImGui::SliderFloat2("In", &key.bezierIn.x, 0.0f, 1.0f, "%.2f")) {
key.bezierIn.x = clampFloat(key.bezierIn.x, 0.0f, 1.0f);
key.bezierIn.y = clampFloat(key.bezierIn.y, 0.0f, 1.0f);
projectManager.currentProject.hasUnsavedChanges = true;
}
int nextIndex = animationSelectedKey + 1;
if (nextIndex < static_cast<int>(anim.keyframes.size())) {
static int activeHandle = -1;
auto& nextKey = anim.keyframes[nextIndex];
ImVec2 previewSize(260.0f, 110.0f);
ImGui::TextDisabled("Curve Editor");
ImGui::BeginChild("BezierPreview", previewSize, true, ImGuiWindowFlags_NoScrollbar);
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 p0 = ImGui::GetCursorScreenPos();
ImVec2 p1(p0.x + previewSize.x, p0.y + previewSize.y);
ImU32 grid = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 handleColor = ImGui::GetColorU32(ImGuiCol_SliderGrab);
ImU32 lineColor = ImGui::GetColorU32(ImGuiCol_CheckMark);
draw->AddRect(p0, p1, grid);
auto toScreen = [&](const glm::vec2& v) {
return ImVec2(p0.x + v.x * previewSize.x, p0.y + (1.0f - v.y) * previewSize.y);
};
ImVec2 outHandle = toScreen(key.bezierOut);
ImVec2 inHandle = toScreen(nextKey.bezierIn);
ImVec2 start = toScreen(glm::vec2(0.0f, 0.0f));
ImVec2 end = toScreen(glm::vec2(1.0f, 1.0f));
draw->AddLine(start, outHandle, grid, 1.0f);
draw->AddLine(end, inHandle, grid, 1.0f);
draw->AddCircleFilled(outHandle, 5.0f, handleColor);
draw->AddCircleFilled(inHandle, 5.0f, handleColor);
const int samples = 32;
ImVec2 last = start;
for (int i = 0; i <= samples; ++i) {
float t = static_cast<float>(i) / samples;
float y = applyBezier(t, key.bezierOut, nextKey.bezierIn);
ImVec2 cur(p0.x + t * previewSize.x, p0.y + (1.0f - y) * previewSize.y);
if (i > 0) {
draw->AddLine(last, cur, lineColor, 2.0f);
}
last = cur;
}
ImRect outRect(ImVec2(outHandle.x - 7.0f, outHandle.y - 7.0f),
ImVec2(outHandle.x + 7.0f, outHandle.y + 7.0f));
ImRect inRect(ImVec2(inHandle.x - 7.0f, inHandle.y - 7.0f),
ImVec2(inHandle.x + 7.0f, inHandle.y + 7.0f));
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (ImGui::IsMouseHoveringRect(outRect.Min, outRect.Max)) activeHandle = 0;
else if (ImGui::IsMouseHoveringRect(inRect.Min, inRect.Max)) activeHandle = 1;
else activeHandle = -1;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
activeHandle = -1;
}
if (activeHandle >= 0 && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
ImVec2 mouse = ImGui::GetIO().MousePos;
float x = (mouse.x - p0.x) / previewSize.x;
float y = 1.0f - (mouse.y - p0.y) / previewSize.y;
glm::vec2 clamped(clampFloat(x, 0.0f, 1.0f), clampFloat(y, 0.0f, 1.0f));
if (activeHandle == 0) {
key.bezierOut = clamped;
} else {
nextKey.bezierIn = clamped;
}
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::EndChild();
}
}
}
ImGui::Spacing();
if (anim.keyframes.empty()) {
ImGui::TextDisabled("No keyframes yet.");
} else if (ImGui::BeginTable("AnimationKeyframeTable", 5,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("Time");
ImGui::TableSetupColumn("Blend");
ImGui::TableSetupColumn("Position");
ImGui::TableSetupColumn("Rotation");
ImGui::TableSetupColumn("Scale");
ImGui::TableHeadersRow();
for (size_t i = 0; i < anim.keyframes.size(); ++i) {
const auto& key = anim.keyframes[i];
ImGui::TableNextRow();
ImGui::TableNextColumn();
bool selected = animationSelectedKey == static_cast<int>(i);
char label[32];
std::snprintf(label, sizeof(label), "%.2f", key.time);
if (ImGui::Selectable(label, selected, ImGuiSelectableFlags_SpanAllColumns)) {
animationSelectedKey = static_cast<int>(i);
animationCurrentTime = key.time;
}
ImGui::TableNextColumn();
if (key.curveMode == AnimationCurveMode::Bezier) {
ImGui::TextUnformatted("Bezier");
} else {
ImGui::TextUnformatted(getInterpLabel(key.interpolation));
}
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z);
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z);
ImGui::TableNextColumn();
ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z);
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Config")) {
if (ImGui::DragFloat("Clip Length", &anim.clipLength, 0.05f, 0.1f, 120.0f, "%.2f")) {
anim.clipLength = std::max(0.1f, anim.clipLength);
projectManager.currentProject.hasUnsavedChanges = true;
animationCurrentTime = clampFloat(animationCurrentTime, 0.0f, anim.clipLength);
}
if (ImGui::DragFloat("Play Speed", &anim.playSpeed, 0.05f, 0.05f, 8.0f, "%.2f")) {
anim.playSpeed = std::max(0.05f, anim.playSpeed);
projectManager.currentProject.hasUnsavedChanges = true;
}
if (ImGui::Checkbox("Loop", &anim.loop)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
if (ImGui::Checkbox("Apply On Scrub", &anim.applyOnScrub)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Spacing();
if (ImGui::Button("Clear Keyframes")) {
anim.keyframes.clear();
animationSelectedKey = -1;
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Transport");
ImGui::BeginDisabled(!anim.enabled);
if (ImGui::Button(animationIsPlaying ? "Pause" : "Play")) {
animationIsPlaying = !animationIsPlaying;
animationLastAppliedTime = -1.0f;
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Stop")) {
animationIsPlaying = false;
animationCurrentTime = 0.0f;
animationLastAppliedTime = -1.0f;
}
ImGui::SameLine();
ImGui::TextDisabled("Time: %.2fs / %.2fs", animationCurrentTime, anim.clipLength);
if (animationIsPlaying && anim.clipLength > 0.0f) {
animationCurrentTime += ImGui::GetIO().DeltaTime * anim.playSpeed;
if (animationCurrentTime > anim.clipLength) {
if (anim.loop) {
animationCurrentTime = std::fmod(animationCurrentTime, anim.clipLength);
} else {
animationCurrentTime = anim.clipLength;
animationIsPlaying = false;
}
}
}
if (anim.enabled && (animationIsPlaying || anim.applyOnScrub)) {
if (animationIsPlaying || std::abs(animationCurrentTime - animationLastAppliedTime) > 0.0001f) {
applyPoseAtTime(*targetObj, animationCurrentTime);
animationLastAppliedTime = animationCurrentTime;
}
}
ImGui::EndChild();
ImGui::EndTable();
ImGui::End();
}

View File

@@ -0,0 +1,253 @@
#include "Engine.h"
void Engine::renderBuildSettingsWindow() {
if (!showBuildSettings) return;
ImGui::SetNextWindowSize(ImVec2(760, 520), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Build Settings", &showBuildSettings)) {
ImGui::End();
return;
}
if (!projectManager.currentProject.isLoaded) {
ImGui::TextDisabled("No project loaded.");
ImGui::End();
return;
}
bool changed = false;
ImGui::BeginChild("BuildScenesList", ImVec2(0, 150), true);
ImGui::Text("Scenes In Build");
ImGui::Separator();
for (int i = 0; i < static_cast<int>(buildSettings.scenes.size()); ++i) {
BuildSceneEntry& entry = buildSettings.scenes[i];
ImGui::PushID(i);
bool enabled = entry.enabled;
if (ImGui::Checkbox("##enabled", &enabled)) {
entry.enabled = enabled;
changed = true;
}
ImGui::SameLine();
bool selected = (buildSettingsSelectedIndex == i);
if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
buildSettingsSelectedIndex = i;
}
float rightX = ImGui::GetWindowContentRegionMax().x;
ImGui::SameLine(rightX - 24.0f);
ImGui::TextDisabled("%d", i);
ImGui::PopID();
}
ImGui::EndChild();
float buttonSpacing = ImGui::GetStyle().ItemSpacing.x;
float addWidth = 150.0f;
float removeWidth = 130.0f;
float totalButtons = addWidth + removeWidth + buttonSpacing;
float buttonStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - totalButtons;
if (buttonStart > ImGui::GetCursorPosX()) {
ImGui::SetCursorPosX(buttonStart);
}
if (ImGui::Button("Remove Selected", ImVec2(removeWidth, 0.0f))) {
if (buildSettingsSelectedIndex >= 0 &&
buildSettingsSelectedIndex < static_cast<int>(buildSettings.scenes.size())) {
buildSettings.scenes.erase(buildSettings.scenes.begin() + buildSettingsSelectedIndex);
if (buildSettingsSelectedIndex >= static_cast<int>(buildSettings.scenes.size())) {
buildSettingsSelectedIndex = static_cast<int>(buildSettings.scenes.size()) - 1;
}
changed = true;
}
}
ImGui::SameLine();
if (ImGui::Button("Add Open Scenes", ImVec2(addWidth, 0.0f))) {
if (addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true)) {
changed = true;
}
}
ImGui::Spacing();
ImGui::Text("Platform");
ImGui::Separator();
ImGui::BeginChild("BuildPlatforms", ImVec2(220, 0), true);
ImGui::Selectable("Windows & Linux Standalone", true);
ImGui::BeginDisabled(true);
ImGui::Selectable("Android", false);
ImGui::Selectable("Android | Meta Quest", false);
ImGui::EndDisabled();
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("BuildPlatformSettings", ImVec2(0, 0), true);
ImGui::Text("Target Platform");
const char* targets[] = {"Windows", "Linux"};
int targetIndex = (buildSettings.platform == BuildPlatform::Linux) ? 1 : 0;
if (ImGui::Combo("##target-platform", &targetIndex, targets, 2)) {
buildSettings.platform = (targetIndex == 1) ? BuildPlatform::Linux : BuildPlatform::Windows;
changed = true;
}
ImGui::Text("Architecture");
const char* arches[] = {"x86_64", "x86"};
int archIndex = (buildSettings.architecture == "x86") ? 1 : 0;
if (ImGui::Combo("##architecture", &archIndex, arches, 2)) {
buildSettings.architecture = arches[archIndex];
changed = true;
}
ImGui::Spacing();
if (ImGui::Checkbox("Server Build", &buildSettings.serverBuild)) changed = true;
if (ImGui::Checkbox("Development Build", &buildSettings.developmentBuild)) changed = true;
if (ImGui::Checkbox("Autoconnect Profiler", &buildSettings.autoConnectProfiler)) changed = true;
if (ImGui::Checkbox("Deep Profiling Support", &buildSettings.deepProfiling)) changed = true;
if (ImGui::Checkbox("Script Debugging", &buildSettings.scriptDebugging)) changed = true;
if (ImGui::Checkbox("Scripts Only Build", &buildSettings.scriptsOnlyBuild)) changed = true;
ImGui::Spacing();
ImGui::Text("Compression Method");
const char* compressionOptions[] = {"Default", "None", "LZ4", "LZ4HC"};
int compressionIndex = 0;
for (int i = 0; i < 4; ++i) {
if (buildSettings.compressionMethod == compressionOptions[i]) {
compressionIndex = i;
break;
}
}
if (ImGui::Combo("##compression", &compressionIndex, compressionOptions, 4)) {
buildSettings.compressionMethod = compressionOptions[compressionIndex];
changed = true;
}
ImGui::TextDisabled("Android support will unlock after OpenGLES is available.");
ImGui::EndChild();
ImGui::Separator();
float buildWidth = 90.0f;
float buildRunWidth = 120.0f;
float buildTotal = buildWidth + buildRunWidth + buttonSpacing;
float buildStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - buildTotal;
if (buildStart > ImGui::GetCursorPosX()) {
ImGui::SetCursorPosX(buildStart);
}
if (ImGui::Button("Export Game", ImVec2(buildWidth, 0.0f))) {
exportRunAfter = false;
if (exportOutputPath[0] == '\0') {
fs::path defaultOut = projectManager.currentProject.projectPath / "Builds";
std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", defaultOut.string().c_str());
}
showExportDialog = true;
}
ImGui::SameLine();
if (ImGui::Button("Export & Run", ImVec2(buildRunWidth, 0.0f))) {
exportRunAfter = true;
if (exportOutputPath[0] == '\0') {
fs::path defaultOut = projectManager.currentProject.projectPath / "Builds";
std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", defaultOut.string().c_str());
}
showExportDialog = true;
}
if (changed) {
saveBuildSettings();
}
if (showExportDialog) {
ImGui::SetNextWindowSize(ImVec2(720, 460), ImGuiCond_Appearing);
ImGui::OpenPopup("Export Game");
showExportDialog = false;
}
bool exportPopupOpen = true;
ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoDocking;
bool exportActive = false;
bool exportDone = false;
bool exportSuccess = false;
float exportProgress = 0.0f;
std::string exportStatus;
std::string exportLog;
fs::path exportDir;
{
std::lock_guard<std::mutex> lock(exportMutex);
exportActive = exportJob.active;
exportDone = exportJob.done;
exportSuccess = exportJob.success;
exportProgress = exportJob.progress;
exportStatus = exportJob.status;
exportLog = exportJob.log;
exportDir = exportJob.outputDir;
}
bool allowClose = !exportActive;
if (ImGui::BeginPopupModal("Export Game", allowClose ? &exportPopupOpen : nullptr, popupFlags)) {
ImGui::Text("Output Folder");
ImGui::SetNextItemWidth(-1);
ImGui::BeginDisabled(exportActive);
ImGui::InputText("##ExportOutput", exportOutputPath, sizeof(exportOutputPath));
ImGui::EndDisabled();
if (!exportActive) {
if (ImGui::Button("Use Selected Folder")) {
if (!fileBrowser.selectedFile.empty()) {
fs::path selected = fileBrowser.selectedFile;
fs::path folder = fs::is_directory(selected) ? selected : selected.parent_path();
std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", folder.string().c_str());
}
}
ImGui::SameLine();
if (ImGui::Button("Use Project Folder")) {
fs::path folder = projectManager.currentProject.projectPath / "Builds";
std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", folder.string().c_str());
}
}
ImGui::Spacing();
if (exportActive || exportDone) {
const char* statusLabel = exportStatus.empty() ? "Working..." : exportStatus.c_str();
float barValue = exportActive ? exportProgress : 1.0f;
if (barValue <= 0.0f) barValue = 0.02f;
ImGui::ProgressBar(barValue, ImVec2(-1, 0), statusLabel);
if (exportActive) {
ImGui::TextDisabled("Build can take a while (PhysX/assimp). Output updates after each step finishes.");
}
ImGui::BeginChild("ExportLog", ImVec2(0, 180), true);
if (exportLog.empty()) {
ImGui::TextUnformatted("Waiting for build output...");
} else {
ImGui::TextUnformatted(exportLog.c_str());
}
ImGui::EndChild();
}
ImGui::Separator();
if (!exportActive && !exportDone) {
if (ImGui::Button("Start Export", ImVec2(120, 0))) {
if (!exportOutputPath[0]) {
addConsoleMessage("Please choose an export folder.", ConsoleMessageType::Warning);
} else {
startExportBuild(fs::path(exportOutputPath), exportRunAfter);
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(100, 0))) {
ImGui::CloseCurrentPopup();
}
} else if (!exportActive && exportDone) {
if (exportSuccess && !exportDir.empty()) {
ImGui::TextDisabled("Exported to: %s", exportDir.string().c_str());
}
if (ImGui::Button("Close", ImVec2(100, 0))) {
ImGui::CloseCurrentPopup();
std::lock_guard<std::mutex> lock(exportMutex);
exportJob = ExportJobState{};
}
} else {
if (ImGui::Button("Cancel Export", ImVec2(140, 0))) {
exportCancelRequested = true;
std::lock_guard<std::mutex> lock(exportMutex);
exportJob.status = "Cancelling...";
}
}
ImGui::EndPopup();
}
ImGui::End();
}

View File

@@ -501,6 +501,7 @@ void Engine::renderFileBrowserPanel() {
static fs::path pendingDeletePath;
static fs::path pendingRenamePath;
static char renameName[256] = "";
bool settingsDirty = false;
auto openEntry = [&](const fs::directory_entry& entry) {
if (entry.is_directory()) {
@@ -536,6 +537,10 @@ void Engine::renderFileBrowserPanel() {
logToConsole("Loaded scene: " + sceneName);
return;
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
openScriptInEditor(entry.path());
return;
}
openPathInShell(entry.path());
};
@@ -623,6 +628,15 @@ void Engine::renderFileBrowserPanel() {
}
return false;
};
auto normalizePath = [](const fs::path& path) {
std::error_code ec;
fs::path canonical = fs::weakly_canonical(path, ec);
if (!ec) {
return canonical;
}
return path.lexically_normal();
};
// Get colors for categories
auto getCategoryColor = [](FileCategory cat) -> ImU32 {
@@ -742,16 +756,24 @@ void Engine::renderFileBrowserPanel() {
ImGui::TextDisabled("Size");
ImGui::SameLine();
ImGui::SetNextItemWidth(90);
ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx");
if (ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx")) {
settingsDirty = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
ImGui::SameLine();
}
if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
settingsDirty = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
ImGui::SameLine();
if (ImGui::Button(showFileBrowserSidebar ? "Side" : "Side", ImVec2(52, 0))) {
showFileBrowserSidebar = !showFileBrowserSidebar;
settingsDirty = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle sidebar");
ImGui::EndChild();
ImGui::PopStyleVar(2);
@@ -766,9 +788,162 @@ void Engine::renderFileBrowserPanel() {
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
if (showFileBrowserSidebar) {
float minSidebarWidth = 160.0f;
float maxSidebarWidth = std::max(minSidebarWidth, ImGui::GetContentRegionAvail().x * 0.5f);
fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth);
ImGui::BeginChild("FileSidebar", ImVec2(fileBrowserSidebarWidth, 0), true);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
ImGui::TextDisabled("Favorites");
ImGui::SameLine();
if (ImGui::SmallButton("+")) {
fs::path current = normalizePath(fileBrowser.currentPath);
bool exists = false;
for (const auto& fav : fileBrowserFavorites) {
if (normalizePath(fav) == current) {
exists = true;
break;
}
}
if (!exists) {
fileBrowserFavorites.push_back(current);
settingsDirty = true;
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add current folder");
fs::path baseRoot = fileBrowser.projectRoot.empty()
? projectManager.currentProject.projectPath
: fileBrowser.projectRoot;
fs::path normalizedCurrent = normalizePath(fileBrowser.currentPath);
for (size_t i = 0; i < fileBrowserFavorites.size(); ++i) {
fs::path fav = fileBrowserFavorites[i];
std::string label;
std::error_code ec;
fs::path rel = fs::relative(fav, baseRoot, ec);
std::string relStr = rel.generic_string();
if (!ec && !rel.empty() && relStr.find("..") != 0) {
label = relStr;
if (label.empty() || label == ".") {
label = "Project";
}
} else {
label = fav.filename().string();
if (label.empty()) {
label = fav.string();
}
}
bool exists = fs::exists(fav);
ImGui::PushID(static_cast<int>(i));
if (!exists) {
ImGui::BeginDisabled();
}
if (ImGui::Selectable(label.c_str(), normalizePath(fav) == normalizedCurrent)) {
if (exists) {
fileBrowser.navigateTo(fav);
}
}
if (!exists) {
ImGui::EndDisabled();
}
if (ImGui::BeginPopupContextItem("FavContext")) {
if (ImGui::MenuItem("Remove")) {
fileBrowserFavorites.erase(fileBrowserFavorites.begin() + static_cast<int>(i));
settingsDirty = true;
ImGui::EndPopup();
ImGui::PopID();
break;
}
if (exists && ImGui::MenuItem("Open in File Explorer")) {
openPathInFileManager(fav);
}
ImGui::EndPopup();
}
ImGui::PopID();
}
ImGui::Separator();
ImGui::TextDisabled("Folders");
ImGui::BeginChild("FolderTree", ImVec2(0, 0), false);
auto drawFolderTree = [&](auto&& self, const fs::path& path) -> void {
if (!fs::exists(path)) {
return;
}
std::string name = path.filename().string();
if (name.empty()) {
name = "Project";
}
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_OpenOnDoubleClick |
ImGuiTreeNodeFlags_SpanFullWidth;
if (fileBrowser.currentPath == path) {
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::PushID(path.string().c_str());
bool open = ImGui::TreeNodeEx(name.c_str(), flags);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
fileBrowser.navigateTo(path);
}
if (open) {
std::vector<fs::path> dirs;
std::error_code ec;
for (const auto& entry : fs::directory_iterator(path, ec)) {
if (ec) {
break;
}
if (!entry.is_directory()) {
continue;
}
std::string dirName = entry.path().filename().string();
if (!fileBrowser.showHiddenFiles && !dirName.empty() && dirName[0] == '.') {
continue;
}
dirs.push_back(entry.path());
}
std::sort(dirs.begin(), dirs.end(), [](const fs::path& a, const fs::path& b) {
return a.filename().string() < b.filename().string();
});
for (const auto& dir : dirs) {
self(self, dir);
}
ImGui::TreePop();
}
ImGui::PopID();
};
if (!baseRoot.empty()) {
drawFolderTree(drawFolderTree, baseRoot);
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::SameLine();
float splitterHeight = ImGui::GetContentRegionAvail().y;
if (splitterHeight < 1.0f) {
splitterHeight = 1.0f;
}
ImGui::InvisibleButton("SidebarSplitter", ImVec2(4.0f, splitterHeight));
if (ImGui::IsItemHovered() || ImGui::IsItemActive()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
}
if (ImGui::IsItemActive()) {
fileBrowserSidebarWidth += ImGui::GetIO().MouseDelta.x;
fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth);
settingsDirty = true;
}
ImGui::SameLine();
}
ImGui::BeginChild("FileMain", ImVec2(0, 0), false);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (fileBrowser.viewMode == FileBrowserViewMode::Grid) {
float baseIconSize = 64.0f;
float iconSize = baseIconSize * fileBrowserIconScale;
@@ -776,10 +951,10 @@ void Engine::renderFileBrowserPanel() {
float textHeight = 32.0f; // Space for filename text
float cellWidth = iconSize + padding * 2;
float cellHeight = iconSize + padding * 2 + textHeight;
float windowWidth = ImGui::GetContentRegionAvail().x;
int columns = std::max(1, (int)((windowWidth + padding) / (cellWidth + padding)));
// Use a table for consistent grid layout
if (ImGui::BeginTable("FileGrid", columns, ImGuiTableFlags_NoPadInnerX)) {
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
@@ -787,44 +962,44 @@ void Engine::renderFileBrowserPanel() {
std::string filename = entry.path().filename().string();
FileCategory category = fileBrowser.getFileCategory(entry);
bool isSelected = fileBrowser.selectedFile == entry.path();
ImGui::TableNextColumn();
ImGui::PushID(i);
// Cell content area
ImVec2 cellStart = ImGui::GetCursorScreenPos();
ImVec2 cellEnd(cellStart.x + cellWidth, cellStart.y + cellHeight);
// Invisible button for the entire cell
if (ImGui::InvisibleButton("##cell", ImVec2(cellWidth, cellHeight))) {
fileBrowser.selectedFile = entry.path();
}
bool hovered = ImGui::IsItemHovered();
bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(0);
// Draw background
ImU32 bgColor = isSelected ? IM_COL32(70, 110, 160, 200) :
(hovered ? IM_COL32(60, 65, 75, 180) : IM_COL32(0, 0, 0, 0));
if (bgColor != IM_COL32(0, 0, 0, 0)) {
drawList->AddRectFilled(cellStart, cellEnd, bgColor, 6.0f);
}
// Draw border on selection
if (isSelected) {
drawList->AddRect(cellStart, cellEnd, IM_COL32(100, 150, 220, 255), 6.0f, 0, 2.0f);
}
// Draw icon centered in cell
ImVec2 iconPos(
cellStart.x + (cellWidth - iconSize) * 0.5f,
cellStart.y + padding
);
FileIcons::DrawIcon(drawList, category, iconPos, iconSize, getCategoryColor(category));
// Draw filename below icon (centered, with wrapping)
std::string displayName = filename;
float maxTextWidth = cellWidth - 4;
// Truncate if too long
ImVec2 textSize = ImGui::CalcTextSize(displayName.c_str());
if (textSize.x > maxTextWidth) {
@@ -837,21 +1012,21 @@ void Engine::renderFileBrowserPanel() {
displayName += "...";
textSize = ImGui::CalcTextSize(displayName.c_str());
}
ImVec2 textPos(
cellStart.x + (cellWidth - textSize.x) * 0.5f,
cellStart.y + padding + iconSize + 4
);
// Text with subtle shadow for readability
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 100), displayName.c_str());
drawList->AddText(textPos, IM_COL32(230, 230, 230, 255), displayName.c_str());
// Handle double click
if (doubleClicked) {
openEntry(entry);
}
// Context menu
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
if (ImGui::MenuItem("Open")) {
@@ -960,33 +1135,33 @@ void Engine::renderFileBrowserPanel() {
ImGui::Text("%s", filename.c_str());
ImGui::EndDragDropSource();
}
ImGui::PopID();
}
ImGui::EndTable();
}
} else {
// List View
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2));
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
const auto& entry = fileBrowser.entries[i];
std::string filename = entry.path().filename().string();
FileCategory category = fileBrowser.getFileCategory(entry);
bool isSelected = fileBrowser.selectedFile == entry.path();
ImGui::PushID(i);
// Selectable row
if (ImGui::Selectable("##row", isSelected, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 20))) {
fileBrowser.selectedFile = entry.path();
if (ImGui::IsMouseDoubleClicked(0)) {
openEntry(entry);
}
}
// Context menu
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
if (ImGui::MenuItem("Open")) {
@@ -1098,15 +1273,15 @@ void Engine::renderFileBrowserPanel() {
ImGui::Text("%s", filename.c_str());
ImGui::EndDragDropSource();
}
// Draw icon inline
ImGui::SameLine(4);
ImVec2 iconPos = ImGui::GetCursorScreenPos();
iconPos.y -= 2;
FileIcons::DrawIcon(drawList, category, iconPos, 16, getCategoryColor(category));
ImGui::SameLine(26);
// Color-coded filename
ImVec4 textColor;
switch (category) {
@@ -1118,10 +1293,10 @@ void Engine::renderFileBrowserPanel() {
default: textColor = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); break;
}
ImGui::TextColored(textColor, "%s", filename.c_str());
ImGui::PopID();
}
ImGui::PopStyleVar();
}
@@ -1168,9 +1343,14 @@ void Engine::renderFileBrowserPanel() {
ImGui::EndPopup();
}
ImGui::EndChild();
ImGui::EndChild();
ImGui::PopStyleColor();
if (settingsDirty) {
saveEditorUserSettings();
}
if (triggerDeletePopup) {
ImGui::OpenPopup("Confirm Delete");
triggerDeletePopup = false;

View File

@@ -103,6 +103,34 @@ bool Spinner(const char* label, float radius, int thickness, const ImU32& color)
return true;
}
bool ProgressCircle(const char* label, float radius, float thickness, float value,
const ImU32& color, const ImU32& bgColor) {
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
ImVec2 pos = window->DC.CursorPos;
ImVec2 size((radius) * 2, (radius + style.FramePadding.y) * 2);
const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ItemSize(bb, style.FramePadding.y);
if (!ItemAdd(bb, id))
return false;
ImVec2 centre = ImVec2(pos.x + radius, pos.y + radius + style.FramePadding.y);
float startAngle = -IM_PI * 0.5f;
float endAngle = startAngle + IM_PI * 2.0f * ImClamp(value, 0.0f, 1.0f);
window->DrawList->AddCircle(centre, radius, bgColor, 32, thickness);
window->DrawList->PathClear();
window->DrawList->PathArcTo(centre, radius, startAngle, endAngle, 32);
window->DrawList->PathStroke(color, false, thickness);
return true;
}
} // namespace ImGui
#pragma endregion
@@ -188,7 +216,7 @@ void Engine::renderLauncher() {
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 | Beta V1.0");
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V6.3");
ImGui::EndChild();
@@ -352,7 +380,7 @@ void Engine::renderLauncher() {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Modularity Engine - Beta V1.0");
ImGui::TextDisabled("Modularity Engine - Beta V6.3");
ImGui::EndChild();
}
@@ -365,7 +393,7 @@ void Engine::renderLauncher() {
if (projectManager.showOpenProjectDialog)
renderOpenProjectDialog();
if (projectLoadInProgress) {
if (projectLoadInProgress || sceneLoadInProgress) {
float elapsed = static_cast<float>(glfwGetTime() - projectLoadStartTime);
if (elapsed > 0.15f) {
ImGuiIO& io = ImGui::GetIO();
@@ -396,16 +424,32 @@ void Engine::renderLauncher() {
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "Loading project...");
const char* headline = sceneLoadInProgress ? "Loading scene..." : "Loading project...";
ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "%s", headline);
ImGui::Spacing();
ImGui::TextDisabled("%s", projectLoadPath.c_str());
if (sceneLoadInProgress && !sceneLoadStatus.empty()) {
ImGui::TextDisabled("%s", sceneLoadStatus.c_str());
} else if (!projectLoadPath.empty()) {
ImGui::TextDisabled("%s", projectLoadPath.c_str());
}
ImGui::Spacing();
ImGui::Spinner("##project_load_spinner", 16.0f, 4, ImGui::GetColorU32(ImGuiCol_ButtonHovered));
ImGui::SameLine();
ImGui::BufferingBar("##project_load_bar", std::fmod(elapsed * 0.25f, 1.0f),
ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f),
ImGui::GetColorU32(ImGuiCol_Button),
ImGui::GetColorU32(ImGuiCol_ButtonHovered));
if (sceneLoadInProgress) {
ImGui::ProgressCircle("##project_load_circle", 16.0f, 4.0f, sceneLoadProgress,
ImGui::GetColorU32(ImGuiCol_ButtonHovered),
ImGui::GetColorU32(ImGuiCol_Button));
ImGui::SameLine();
ImGui::BufferingBar("##project_load_bar", sceneLoadProgress,
ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f),
ImGui::GetColorU32(ImGuiCol_Button),
ImGui::GetColorU32(ImGuiCol_ButtonHovered));
} else {
ImGui::Spinner("##project_load_spinner", 16.0f, 4, ImGui::GetColorU32(ImGuiCol_ButtonHovered));
ImGui::SameLine();
ImGui::BufferingBar("##project_load_bar", std::fmod(elapsed * 0.25f, 1.0f),
ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f),
ImGui::GetColorU32(ImGuiCol_Button),
ImGui::GetColorU32(ImGuiCol_ButtonHovered));
}
ImGui::End();
ImGui::PopStyleColor();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,538 @@
#include "Engine.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <unordered_set>
namespace {
static uint64_t hashBuffer(const std::string& text) {
uint64_t hash = 1469598103934665603ull;
for (unsigned char c : text) {
hash ^= static_cast<uint64_t>(c);
hash *= 1099511628211ull;
}
return hash;
}
static std::string trimLeft(const std::string& value) {
size_t start = value.find_first_not_of(" \t");
if (start == std::string::npos) return "";
return value.substr(start);
}
static std::vector<std::string> buildSymbolList(const std::string& text) {
std::vector<std::string> symbols;
std::istringstream input(text);
std::string line;
while (std::getline(input, line)) {
std::string trimmed = trimLeft(line);
if (trimmed.empty()) continue;
if (trimmed.rfind("//", 0) == 0) continue;
auto captureToken = [&](const std::string& prefix) -> bool {
if (trimmed.rfind(prefix, 0) != 0) return false;
size_t start = prefix.size();
while (start < trimmed.size() && std::isspace(static_cast<unsigned char>(trimmed[start]))) {
++start;
}
size_t end = start;
while (end < trimmed.size() &&
(std::isalnum(static_cast<unsigned char>(trimmed[end])) || trimmed[end] == '_' || trimmed[end] == ':')) {
++end;
}
if (end > start) {
symbols.emplace_back(trimmed.substr(0, end));
return true;
}
return false;
};
if (captureToken("class ") || captureToken("struct ") || captureToken("enum ") || captureToken("namespace ")) {
continue;
}
if (trimmed.find('(') != std::string::npos && trimmed.find(')') != std::string::npos &&
(trimmed.find('{') != std::string::npos || trimmed.back() == ';')) {
static const char* kSkip[] = {"if", "for", "while", "switch", "catch"};
bool skip = false;
for (const char* keyword : kSkip) {
if (trimmed.rfind(keyword, 0) == 0) {
skip = true;
break;
}
}
if (skip) continue;
size_t paren = trimmed.find('(');
if (paren != std::string::npos && paren > 0) {
size_t end = paren;
while (end > 0 && std::isspace(static_cast<unsigned char>(trimmed[end - 1]))) {
--end;
}
size_t start = end;
while (start > 0 &&
(std::isalnum(static_cast<unsigned char>(trimmed[start - 1])) || trimmed[start - 1] == '_' ||
trimmed[start - 1] == ':')) {
--start;
}
if (end > start) {
symbols.emplace_back(trimmed.substr(start, paren - start));
}
}
}
}
return symbols;
}
static std::vector<std::string> buildCompletionList(const std::vector<std::string>& pool,
const std::string& prefix,
size_t limit = 16) {
std::vector<std::string> matches;
if (prefix.empty()) return matches;
for (const auto& entry : pool) {
if (entry.rfind(prefix, 0) == 0) {
matches.push_back(entry);
if (matches.size() >= limit) break;
}
}
return matches;
}
static const std::unordered_set<std::string>& cppKeywordSet() {
static const std::unordered_set<std::string> kKeywords = {
"auto", "bool", "break", "case", "catch", "char", "class", "const", "constexpr", "continue",
"default", "delete", "do", "double", "else", "enum", "explicit", "extern", "false", "float",
"for", "friend", "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept",
"operator", "private", "protected", "public", "return", "short", "signed", "sizeof", "static",
"struct", "switch", "template", "this", "throw", "true", "try", "typedef", "typename",
"union", "unsigned", "using", "virtual", "void", "volatile", "while"
};
return kKeywords;
}
static std::vector<std::string> extractIdentifiers(const std::string& text) {
std::unordered_set<std::string> unique;
const auto& keywords = cppKeywordSet();
std::string token;
token.reserve(64);
auto flushToken = [&]() {
if (token.size() >= 2 && keywords.find(token) == keywords.end()) {
unique.insert(token);
}
token.clear();
};
for (char c : text) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
token.push_back(c);
} else if (!token.empty()) {
flushToken();
}
}
if (!token.empty()) {
flushToken();
}
std::vector<std::string> out(unique.begin(), unique.end());
std::sort(out.begin(), out.end());
return out;
}
}
void Engine::refreshScriptingFileList() {
scriptingFileList.clear();
scriptingCompletions.clear();
if (!projectManager.currentProject.isLoaded) {
return;
}
fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject);
ScriptBuildConfig config;
std::string error;
if (!scriptCompiler.loadConfig(configPath, config, error)) {
return;
}
fs::path scriptsRoot = config.scriptsDir;
if (!scriptsRoot.is_absolute()) {
scriptsRoot = projectManager.currentProject.projectPath / scriptsRoot;
}
std::error_code ec;
if (!fs::exists(scriptsRoot, ec)) {
return;
}
const std::unordered_set<std::string> validExt = {
".cpp", ".cc", ".cxx", ".c", ".hpp", ".h", ".inl"
};
for (auto it = fs::recursive_directory_iterator(scriptsRoot, ec);
it != fs::recursive_directory_iterator(); ++it) {
if (it->is_directory()) continue;
std::string ext = it->path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (validExt.find(ext) == validExt.end()) continue;
scriptingFileList.push_back(it->path());
}
std::sort(scriptingFileList.begin(), scriptingFileList.end());
std::unordered_set<std::string> uniqueSymbols;
for (const auto& scriptPath : scriptingFileList) {
std::ifstream file(scriptPath);
if (!file.is_open()) continue;
std::stringstream buffer;
buffer << file.rdbuf();
std::vector<std::string> symbols = buildSymbolList(buffer.str());
for (auto& symbol : symbols) {
uniqueSymbols.insert(symbol);
}
}
scriptingCompletions.assign(uniqueSymbols.begin(), uniqueSymbols.end());
std::sort(scriptingCompletions.begin(), scriptingCompletions.end());
}
void Engine::openScriptInEditor(const fs::path& path) {
if (path.empty()) return;
std::error_code ec;
fs::path absPath = fs::absolute(path, ec);
fs::path normalized = (ec ? path : absPath).lexically_normal();
std::ifstream file(normalized);
std::stringstream buffer;
if (file.is_open()) {
buffer << file.rdbuf();
}
scriptEditorState.filePath = normalized;
scriptEditorState.buffer = buffer.str();
if (!scriptTextEditorReady) {
auto lang = TextEditor::LanguageDefinition::CPlusPlus();
lang.mIdentifiers.insert({"Begin", {}});
lang.mIdentifiers.insert({"TickUpdate", {}});
lang.mIdentifiers.insert({"Spec", {}});
lang.mIdentifiers.insert({"TestEditor", {}});
lang.mIdentifiers.insert({"Update", {}});
scriptTextEditor.SetLanguageDefinition(lang);
auto palette = scriptTextEditor.GetPalette();
palette[(int)TextEditor::PaletteIndex::KnownIdentifier] = IM_COL32(220, 180, 70, 255);
scriptTextEditor.SetPalette(palette);
scriptTextEditor.SetShowWhitespaces(true);
scriptTextEditor.SetAllowTabInput(false);
scriptTextEditor.SetSmartTabDelete(true);
scriptTextEditorReady = true;
}
scriptTextEditor.SetText(scriptEditorState.buffer);
scriptEditorState.dirty = false;
scriptEditorState.hasWriteTime = false;
if (fs::exists(normalized, ec)) {
scriptEditorState.lastWriteTime = fs::last_write_time(normalized, ec);
scriptEditorState.hasWriteTime = !ec;
}
showScriptingWindow = true;
}
void Engine::renderScriptingWindow() {
if (!showScriptingWindow) return;
if (scriptingFilesDirty) {
refreshScriptingFileList();
scriptingFilesDirty = false;
}
ImGui::Begin("Scripting", &showScriptingWindow);
if (!projectManager.currentProject.isLoaded) {
ImGui::TextDisabled("Load a project to edit scripts.");
ImGui::End();
return;
}
static std::vector<std::string> symbols;
static uint64_t symbolsHash = 0;
static std::vector<std::string> bufferIdentifiers;
static uint64_t identifiersHash = 0;
static std::vector<std::string> completionPool;
static std::vector<std::string> activeSuggestions;
static std::string activePrefix;
static bool completionPanelOpen = true;
ImGui::TextDisabled("C++ Script Editor");
ImGui::SameLine();
if (ImGui::Button("Refresh List")) {
scriptingFilesDirty = true;
}
ImGui::Separator();
float leftWidth = 240.0f;
ImGui::BeginChild("ScriptingFiles", ImVec2(leftWidth, 0.0f), true);
ImGui::TextDisabled("Scripts");
ImGui::InputTextWithHint("##ScriptFilter", "Filter", scriptingFilter, sizeof(scriptingFilter));
ImGui::Separator();
for (const auto& scriptPath : scriptingFileList) {
std::string label = scriptPath.filename().string();
std::string filter = scriptingFilter;
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
std::string lowerLabel = label;
std::transform(lowerLabel.begin(), lowerLabel.end(), lowerLabel.begin(), ::tolower);
if (!filter.empty() && lowerLabel.find(filter) == std::string::npos) {
continue;
}
bool selected = (scriptEditorState.filePath == scriptPath);
if (ImGui::Selectable(label.c_str(), selected)) {
openScriptInEditor(scriptPath);
}
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("ScriptingEditor", ImVec2(0.0f, 0.0f), false);
ImGui::TextDisabled("Active File");
ImGui::SameLine();
std::string fileLabel = scriptEditorState.filePath.empty()
? std::string("None")
: scriptEditorState.filePath.filename().string();
ImGui::TextUnformatted(fileLabel.c_str());
bool hasFile = !scriptEditorState.filePath.empty();
ImGui::SameLine();
if (!hasFile) {
ImGui::BeginDisabled();
}
if (ImGui::Button("Save")) {
std::ofstream out(scriptEditorState.filePath);
if (out.is_open()) {
scriptEditorState.buffer = scriptTextEditor.GetText();
out << scriptEditorState.buffer;
scriptEditorState.dirty = false;
std::error_code ec;
scriptEditorState.lastWriteTime = fs::last_write_time(scriptEditorState.filePath, ec);
scriptEditorState.hasWriteTime = !ec;
if (scriptEditorState.autoCompileOnSave) {
compileScriptFile(scriptEditorState.filePath);
}
}
}
ImGui::SameLine();
if (ImGui::Button("Compile")) {
compileScriptFile(scriptEditorState.filePath);
}
ImGui::SameLine();
ImGui::Checkbox("Auto-compile on save", &scriptEditorState.autoCompileOnSave);
if (!hasFile) {
ImGui::EndDisabled();
}
bool canReload = false;
if (hasFile && scriptEditorState.hasWriteTime) {
std::error_code ec;
if (fs::exists(scriptEditorState.filePath, ec)) {
auto diskTime = fs::last_write_time(scriptEditorState.filePath, ec);
if (!ec && diskTime > scriptEditorState.lastWriteTime) {
canReload = true;
}
}
}
if (canReload) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.35f, 1.0f), "File changed on disk");
ImGui::SameLine();
if (ImGui::Button("Reload")) {
openScriptInEditor(scriptEditorState.filePath);
}
}
ImGui::Separator();
if (ImGui::BeginTabBar("ScriptingTabs")) {
if (ImGui::BeginTabItem("Editor")) {
if (hasFile) {
if (!scriptTextEditorReady) {
auto lang = TextEditor::LanguageDefinition::CPlusPlus();
lang.mIdentifiers.insert({"Begin", {}});
lang.mIdentifiers.insert({"TickUpdate", {}});
lang.mIdentifiers.insert({"Spec", {}});
lang.mIdentifiers.insert({"TestEditor", {}});
lang.mIdentifiers.insert({"Update", {}});
scriptTextEditor.SetLanguageDefinition(lang);
auto palette = scriptTextEditor.GetPalette();
palette[(int)TextEditor::PaletteIndex::KnownIdentifier] = IM_COL32(220, 180, 70, 255);
scriptTextEditor.SetPalette(palette);
scriptTextEditor.SetShowWhitespaces(true);
scriptTextEditor.SetAllowTabInput(false);
scriptTextEditor.SetSmartTabDelete(true);
scriptTextEditorReady = true;
}
completionPool.clear();
std::unordered_set<std::string> poolSet;
for (const auto& kw : cppKeywordSet()) {
poolSet.insert(kw);
}
for (const auto& entry : scriptingCompletions) {
poolSet.insert(entry);
}
for (const auto& entry : symbols) {
poolSet.insert(entry);
}
uint64_t bufferHash = hashBuffer(scriptEditorState.buffer);
if (bufferHash != identifiersHash) {
identifiersHash = bufferHash;
bufferIdentifiers = extractIdentifiers(scriptEditorState.buffer);
}
for (const auto& entry : bufferIdentifiers) {
poolSet.insert(entry);
}
completionPool.assign(poolSet.begin(), poolSet.end());
std::sort(completionPool.begin(), completionPool.end());
TextEditor::Coordinates cursorBefore = scriptTextEditor.GetCursorPosition();
activePrefix = scriptTextEditor.GetWordAtPublic(cursorBefore);
if (activePrefix.empty() && cursorBefore.mColumn > 0) {
TextEditor::Coordinates prev(cursorBefore.mLine, cursorBefore.mColumn - 1);
activePrefix = scriptTextEditor.GetWordAtPublic(prev);
}
if (!activePrefix.empty() && activePrefix.size() >= 2) {
activeSuggestions = buildCompletionList(completionPool, activePrefix);
} else {
activeSuggestions.clear();
}
bool tabPressed = ImGui::IsKeyPressed(ImGuiKey_Tab);
bool canComplete = !activeSuggestions.empty() && !ImGui::GetIO().KeyShift;
scriptTextEditor.SetAllowTabInput(!canComplete);
float completionHeight = completionPanelOpen ? 140.0f : 0.0f;
float availHeight = ImGui::GetContentRegionAvail().y;
float editorHeight = std::max(120.0f, availHeight - completionHeight - 12.0f);
ImVec2 editorSize = ImVec2(0.0f, editorHeight);
scriptTextEditor.Render("##ScriptEditor", editorSize, false);
if (scriptTextEditor.IsTextChanged()) {
scriptEditorState.dirty = true;
scriptEditorState.buffer = scriptTextEditor.GetText();
}
uint64_t newHash = hashBuffer(scriptEditorState.buffer);
if (newHash != symbolsHash) {
symbolsHash = newHash;
symbols = buildSymbolList(scriptEditorState.buffer);
}
TextEditor::Coordinates cursorAfter = scriptTextEditor.GetCursorPosition();
activePrefix = scriptTextEditor.GetWordAtPublic(cursorAfter);
if (activePrefix.empty() && cursorAfter.mColumn > 0) {
TextEditor::Coordinates prev(cursorAfter.mLine, cursorAfter.mColumn - 1);
activePrefix = scriptTextEditor.GetWordAtPublic(prev);
}
if (!activePrefix.empty() && activePrefix.size() >= 2) {
activeSuggestions = buildCompletionList(completionPool, activePrefix);
} else {
activeSuggestions.clear();
}
bool canCompleteNow = !activeSuggestions.empty() && !ImGui::GetIO().KeyShift;
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
tabPressed && canCompleteNow) {
TextEditor::Coordinates cursor = scriptTextEditor.GetCursorPosition();
TextEditor::Coordinates start(cursor.mLine,
std::max(0, cursor.mColumn - static_cast<int>(activePrefix.size())));
scriptTextEditor.SetSelection(start, cursor);
scriptTextEditor.Delete();
scriptTextEditor.InsertText(activeSuggestions.front().c_str());
scriptEditorState.dirty = true;
scriptEditorState.buffer = scriptTextEditor.GetText();
}
if (completionPanelOpen) {
ImGui::Separator();
ImGui::TextDisabled("Completions");
ImGui::SameLine();
ImGui::Checkbox("Show", &completionPanelOpen);
ImGui::BeginChild("CompletionList", ImVec2(0.0f, completionHeight), true);
if (activeSuggestions.empty()) {
ImGui::TextDisabled("No suggestions");
} else {
for (const auto& suggestion : activeSuggestions) {
if (ImGui::Selectable(suggestion.c_str())) {
TextEditor::Coordinates cursor = scriptTextEditor.GetCursorPosition();
TextEditor::Coordinates start(cursor.mLine,
std::max(0, cursor.mColumn - static_cast<int>(activePrefix.size())));
scriptTextEditor.SetSelection(start, cursor);
scriptTextEditor.Delete();
scriptTextEditor.InsertText(suggestion.c_str());
scriptEditorState.dirty = true;
scriptEditorState.buffer = scriptTextEditor.GetText();
}
}
}
ImGui::EndChild();
}
if (canCompleteNow && scriptTextEditor.HasCursorScreenPosition()) {
const std::string& suggestion = activeSuggestions.front();
if (suggestion.size() > activePrefix.size() &&
suggestion.rfind(activePrefix, 0) == 0) {
std::string ghost = suggestion.substr(activePrefix.size());
ImVec2 ghostPos = scriptTextEditor.GetCursorScreenPositionPublic();
ImU32 ghostColor = IM_COL32(180, 180, 180, 110);
ImGui::GetWindowDrawList()->AddText(ghostPos, ghostColor, ghost.c_str());
}
}
} else {
ImGui::TextDisabled("Select a script file to start editing.");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Intellisense")) {
ScriptBuildConfig config;
std::string error;
fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject);
bool hasConfig = scriptCompiler.loadConfig(configPath, config, error);
if (hasConfig) {
packageManager.applyToBuildConfig(config);
ImGui::TextDisabled("Compiler");
ImGui::Text("Standard: %s", config.cppStandard.c_str());
ImGui::Separator();
ImGui::TextDisabled("Include Dirs");
for (const auto& includeDir : config.includeDirs) {
ImGui::BulletText("%s", includeDir.string().c_str());
}
ImGui::Separator();
ImGui::TextDisabled("Defines");
for (const auto& def : config.defines) {
ImGui::BulletText("%s", def.c_str());
}
} else {
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.55f, 1.0f), "Scripts.modu not loaded");
if (!error.empty()) {
ImGui::TextWrapped("%s", error.c_str());
}
}
ImGui::Separator();
ImGui::TextDisabled("Outline");
if (!symbols.empty()) {
for (const auto& symbol : symbols) {
ImGui::BulletText("%s", symbol.c_str());
}
} else {
ImGui::TextDisabled("No symbols detected.");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Build")) {
ImGui::TextDisabled("Compile Status");
ImGui::Text("%s", lastCompileStatus.c_str());
if (!lastCompileLog.empty()) {
ImGui::Separator();
ImGui::TextDisabled("Output");
ImGui::BeginChild("CompileLog", ImVec2(0.0f, 0.0f), true);
ImGui::TextUnformatted(lastCompileLog.c_str());
ImGui::EndChild();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndChild();
ImGui::End();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@
#include "PhysicsSystem.h"
#include "AudioSystem.h"
#include "PackageManager.h"
#include "ManagedScriptRuntime.h"
#include "ThirdParty/ImGuiColorTextEdit/TextEditor.h"
#include "../include/Window/Window.h"
#include <unordered_map>
#include <unordered_set>
@@ -22,6 +24,7 @@
#include <thread>
void window_size_callback(GLFWwindow* window, int width, int height);
fs::path resolveScriptsConfigPath(const Project& project);
class Engine {
private:
@@ -74,7 +77,15 @@ private:
bool showConsole = true;
bool showProjectBrowser = true; // Now merged into file browser
bool showMeshBuilder = false;
bool showBuildSettings = false;
bool showStyleEditor = false;
bool showScriptingWindow = false;
bool firstFrame = true;
bool playerMode = false;
bool autoStartRequested = false;
bool autoStartPlayerMode = false;
std::string autoStartProjectPath;
std::string autoStartSceneName;
std::vector<std::string> consoleLog;
int draggedObjectId = -1;
@@ -103,8 +114,34 @@ private:
char fileBrowserSearch[256] = "";
float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range
float fileBrowserSidebarWidth = 220.0f;
bool showFileBrowserSidebar = true;
std::vector<fs::path> fileBrowserFavorites;
std::string uiStylePresetName = "Current";
enum class UIAnimationMode {
Off = 0,
Snappy = 1,
Fluid = 2
};
enum class WorkspaceMode {
Default = 0,
Animation = 1,
Scripting = 2
};
UIAnimationMode uiAnimationMode = UIAnimationMode::Off;
WorkspaceMode currentWorkspace = WorkspaceMode::Default;
bool workspaceLayoutDirty = false;
bool pendingWorkspaceReload = false;
fs::path pendingWorkspaceIniPath;
bool editorSettingsDirty = false;
bool showEnvironmentWindow = true;
bool showCameraWindow = true;
bool showAnimationWindow = false;
int animationTargetId = -1;
int animationSelectedKey = -1;
float animationCurrentTime = 0.0f;
bool animationIsPlaying = false;
float animationLastAppliedTime = -1.0f;
bool hierarchyShowTexturePreview = false;
bool hierarchyPreviewNearest = false;
std::unordered_map<std::string, bool> texturePreviewFilterOverrides;
@@ -121,6 +158,8 @@ private:
bool gameViewportFocused = false;
bool showGameProfiler = true;
bool showCanvasOverlay = false;
bool showUIWorldGrid = true;
bool showSceneGrid3D = false;
int gameViewportResolutionIndex = 0;
int gameViewportCustomWidth = 1920;
int gameViewportCustomHeight = 1080;
@@ -139,10 +178,41 @@ private:
std::vector<int> meshEditSelectedVertices;
std::vector<int> meshEditSelectedEdges; // indices into generated edge list
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
struct UIAnimationState {
float hover = 0.0f;
float active = 0.0f;
float sliderValue = 0.0f;
bool initialized = false;
};
std::unordered_map<int, UIAnimationState> uiAnimationStates;
struct UIWorldCamera2D {
glm::vec2 position = glm::vec2(0.0f);
float zoom = 100.0f; // pixels per world unit
glm::vec2 viewportSize = glm::vec2(0.0f);
glm::vec2 WorldToScreen(const glm::vec2& world) const {
return glm::vec2(
(world.x - position.x) * zoom + viewportSize.x * 0.5f,
(position.y - world.y) * zoom + viewportSize.y * 0.5f
);
}
glm::vec2 ScreenToWorld(const glm::vec2& screen) const {
return glm::vec2(
(screen.x - viewportSize.x * 0.5f) / zoom + position.x,
position.y - (screen.y - viewportSize.y * 0.5f) / zoom
);
}
};
bool uiWorldMode = false;
bool uiWorldPanning = false;
UIWorldCamera2D uiWorldCamera;
bool consoleWrapText = true;
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
ScriptCompiler scriptCompiler;
ScriptRuntime scriptRuntime;
ManagedScriptRuntime managedRuntime;
PhysicsSystem physics;
AudioSystem audio;
bool showCompilePopup = false;
@@ -151,8 +221,58 @@ private:
bool lastCompileSuccess = false;
std::string lastCompileStatus;
std::string lastCompileLog;
float compileProgress = 0.0f;
std::string compileStage;
enum class BuildPlatform {
Windows = 0,
Linux = 1,
Android = 2
};
struct BuildSceneEntry {
std::string name;
bool enabled = true;
};
struct BuildSettings {
BuildPlatform platform = BuildPlatform::Windows;
std::string architecture = "x86_64";
bool developmentBuild = false;
bool autoConnectProfiler = false;
bool scriptDebugging = false;
bool deepProfiling = false;
bool scriptsOnlyBuild = false;
bool serverBuild = false;
std::string compressionMethod = "Default";
std::vector<BuildSceneEntry> scenes;
};
BuildSettings buildSettings;
int buildSettingsSelectedIndex = -1;
bool buildSettingsDirty = false;
struct ExportJobResult {
bool success = false;
std::string message;
fs::path outputDir;
};
struct ExportJobState {
bool active = false;
bool done = false;
bool success = false;
bool cancelled = false;
float progress = 0.0f;
std::string status;
std::string log;
fs::path outputDir;
bool runAfter = false;
std::future<ExportJobResult> future;
};
ExportJobState exportJob;
std::atomic<bool> exportCancelRequested = false;
std::mutex exportMutex;
bool showExportDialog = false;
bool exportRunAfter = false;
char exportOutputPath[512] = "";
struct ScriptCompileJobResult {
bool success = false;
bool isManaged = false;
fs::path scriptPath;
fs::path binaryPath;
std::string compiledSource;
@@ -168,6 +288,7 @@ private:
std::unordered_map<std::string, fs::file_time_type> scriptLastAutoCompileTime;
std::deque<fs::path> autoCompileQueue;
std::unordered_set<std::string> autoCompileQueued;
bool managedAutoCompileQueued = false;
double scriptAutoCompileLastCheck = 0.0;
double scriptAutoCompileInterval = 0.5;
struct ProjectLoadResult {
@@ -180,6 +301,16 @@ private:
double projectLoadStartTime = 0.0;
std::string projectLoadPath;
std::future<ProjectLoadResult> projectLoadFuture;
bool sceneLoadInProgress = false;
float sceneLoadProgress = 0.0f;
std::string sceneLoadStatus;
std::string sceneLoadSceneName;
std::vector<SceneObject> sceneLoadObjects;
std::vector<size_t> sceneLoadAssetIndices;
size_t sceneLoadAssetsDone = 0;
int sceneLoadNextId = 0;
int sceneLoadVersion = 9;
float sceneLoadTimeOfDay = -1.0f;
bool specMode = false;
bool testMode = false;
bool collisionWireframe = false;
@@ -192,6 +323,21 @@ private:
};
std::vector<UIStylePreset> uiStylePresets;
int uiStylePresetIndex = 0;
struct ScriptEditorState {
fs::path filePath;
std::string buffer;
bool dirty = false;
bool autoCompileOnSave = true;
bool hasWriteTime = false;
fs::file_time_type lastWriteTime;
};
ScriptEditorState scriptEditorState;
std::vector<fs::path> scriptingFileList;
std::vector<std::string> scriptingCompletions;
TextEditor scriptTextEditor;
bool scriptTextEditorReady = false;
char scriptingFilter[128] = "";
bool scriptingFilesDirty = true;
// Private methods
SceneObject* getSelectedObject();
glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
@@ -222,6 +368,7 @@ private:
void renderPlayControlsBar();
void renderEnvironmentWindow();
void renderCameraWindow();
void renderAnimationWindow();
void renderHierarchyPanel();
void renderObjectNode(SceneObject& obj, const std::string& filter,
std::vector<bool>& ancestorHasNext, bool isLast, int depth);
@@ -230,12 +377,16 @@ private:
void renderInspectorPanel();
void renderConsolePanel();
void renderViewport();
void renderPlayerViewport();
void renderGameViewportWindow();
void renderBuildSettingsWindow();
void renderScriptingWindow();
void renderDialogs();
void updateCompileJob();
void renderProjectBrowserPanel();
void renderScriptEditorWindows();
void refreshScriptEditorWindows();
void refreshScriptingFileList();
Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath);
void updateAutoCompileScripts();
@@ -244,13 +395,38 @@ private:
void startProjectLoad(const std::string& path);
void pollProjectLoad();
void finishProjectLoad(ProjectLoadResult& result);
void beginDeferredSceneLoad(const std::string& sceneName);
void pollSceneLoad();
void finalizeDeferredSceneLoad();
void syncPlayerCamera();
void updateScripts(float delta);
void updatePlayerController(float delta);
void updateRigidbody2D(float delta);
void updateCameraFollow2D(float delta);
void updateSkeletalAnimations(float delta);
void updateSkinningMatrices();
void rebuildSkeletalBindings();
void initUIStylePresets();
int findUIStylePreset(const std::string& name) const;
const UIStylePreset* getUIStylePreset(const std::string& name) const;
void registerUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace);
bool applyUIStylePresetByName(const std::string& name);
void applyWorkspacePreset(WorkspaceMode mode, bool rebuildLayout);
void buildWorkspaceLayout(WorkspaceMode mode);
fs::path getEditorUserSettingsPath() const;
fs::path getEditorLayoutPath() const;
fs::path getWorkspaceLayoutPath(WorkspaceMode mode) const;
void loadEditorUserSettings();
void saveEditorUserSettings() const;
void exportEditorThemeLayout();
void resetBuildSettings();
void loadBuildSettings();
void saveBuildSettings();
bool addSceneToBuildSettings(const std::string& sceneName, bool enabled);
void loadAutoStartConfig();
void applyAutoStartMode();
void startExportBuild(const fs::path& outputDir, bool runAfter);
void pollExportBuild();
void renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb();
@@ -258,6 +434,7 @@ private:
void renderFileBrowserListView();
void renderFileContextMenu(const fs::directory_entry& entry);
void handleFileDoubleClick(const fs::directory_entry& entry);
void openScriptInEditor(const fs::path& path);
ImVec4 getFileCategoryColor(FileCategory category) const;
const char* getFileCategoryIconText(FileCategory category) const;
@@ -308,6 +485,10 @@ public:
SceneObject* findObjectByName(const std::string& name);
SceneObject* findObjectById(int id);
fs::path resolveScriptBinary(const fs::path& sourcePath);
fs::path resolveManagedAssembly(const fs::path& sourcePath);
fs::path getManagedProjectPath() const;
fs::path getManagedOutputDll() const;
void compileManagedScripts();
void markProjectDirty();
// Script-accessible logging wrapper
void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type);

145
src/ManagedBindings.cpp Normal file
View File

@@ -0,0 +1,145 @@
#include "ManagedBindings.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
int modu_ctx_get_object_id(ScriptContext* ctx) {
return (ctx && ctx->object) ? ctx->object->id : -1;
}
void modu_ctx_get_position(ScriptContext* ctx, float* x, float* y, float* z) {
if (!ctx || !ctx->object || !x || !y || !z) return;
*x = ctx->object->position.x;
*y = ctx->object->position.y;
*z = ctx->object->position.z;
}
void modu_ctx_set_position(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return;
ctx->SetPosition(glm::vec3(x, y, z));
}
void modu_ctx_get_rotation(ScriptContext* ctx, float* x, float* y, float* z) {
if (!ctx || !ctx->object || !x || !y || !z) return;
*x = ctx->object->rotation.x;
*y = ctx->object->rotation.y;
*z = ctx->object->rotation.z;
}
void modu_ctx_set_rotation(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return;
ctx->SetRotation(glm::vec3(x, y, z));
}
void modu_ctx_get_scale(ScriptContext* ctx, float* x, float* y, float* z) {
if (!ctx || !ctx->object || !x || !y || !z) return;
*x = ctx->object->scale.x;
*y = ctx->object->scale.y;
*z = ctx->object->scale.z;
}
void modu_ctx_set_scale(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return;
ctx->SetScale(glm::vec3(x, y, z));
}
int modu_ctx_has_rigidbody(ScriptContext* ctx) {
return (ctx && ctx->HasRigidbody()) ? 1 : 0;
}
int modu_ctx_ensure_rigidbody(ScriptContext* ctx, int useGravity, int kinematic) {
if (!ctx) return 0;
return ctx->EnsureRigidbody(useGravity != 0, kinematic != 0) ? 1 : 0;
}
int modu_ctx_set_rigidbody_velocity(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return 0;
return ctx->SetRigidbodyVelocity(glm::vec3(x, y, z)) ? 1 : 0;
}
int modu_ctx_get_rigidbody_velocity(ScriptContext* ctx, float* x, float* y, float* z) {
if (!ctx || !x || !y || !z) return 0;
glm::vec3 velocity(0.0f);
if (!ctx->GetRigidbodyVelocity(velocity)) return 0;
*x = velocity.x;
*y = velocity.y;
*z = velocity.z;
return 1;
}
int modu_ctx_add_rigidbody_force(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return 0;
return ctx->AddRigidbodyForce(glm::vec3(x, y, z)) ? 1 : 0;
}
int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z) {
if (!ctx) return 0;
return ctx->AddRigidbodyImpulse(glm::vec3(x, y, z)) ? 1 : 0;
}
float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback) {
if (!ctx || !key) return fallback;
return ctx->GetSettingFloat(key, fallback);
}
int modu_ctx_get_setting_bool(ScriptContext* ctx, const char* key, int fallback) {
if (!ctx || !key) return fallback ? 1 : 0;
return ctx->GetSettingBool(key, fallback != 0) ? 1 : 0;
}
void modu_ctx_get_setting_string(ScriptContext* ctx, const char* key, const char* fallback,
char* outBuffer, int outBufferSize) {
if (!outBuffer || outBufferSize <= 0) return;
std::string value;
if (!ctx || !key) {
value = fallback ? fallback : "";
} else {
value = ctx->GetSetting(key, fallback ? fallback : "");
}
std::snprintf(outBuffer, static_cast<size_t>(outBufferSize), "%s", value.c_str());
}
void modu_ctx_set_setting_float(ScriptContext* ctx, const char* key, float value) {
if (!ctx || !key) return;
ctx->SetSettingFloat(key, value);
}
void modu_ctx_set_setting_bool(ScriptContext* ctx, const char* key, int value) {
if (!ctx || !key) return;
ctx->SetSettingBool(key, value != 0);
}
void modu_ctx_set_setting_string(ScriptContext* ctx, const char* key, const char* value) {
if (!ctx || !key) return;
ctx->SetSetting(key, value ? value : "");
}
void modu_ctx_add_console_message(ScriptContext* ctx, const char* message, int type) {
if (!ctx || !message) return;
ctx->AddConsoleMessage(message, static_cast<ConsoleMessageType>(type));
}
ManagedNativeApi BuildManagedNativeApi() {
ManagedNativeApi api;
api.getObjectId = modu_ctx_get_object_id;
api.getPosition = modu_ctx_get_position;
api.setPosition = modu_ctx_set_position;
api.getRotation = modu_ctx_get_rotation;
api.setRotation = modu_ctx_set_rotation;
api.getScale = modu_ctx_get_scale;
api.setScale = modu_ctx_set_scale;
api.hasRigidbody = modu_ctx_has_rigidbody;
api.ensureRigidbody = modu_ctx_ensure_rigidbody;
api.setRigidbodyVelocity = modu_ctx_set_rigidbody_velocity;
api.getRigidbodyVelocity = modu_ctx_get_rigidbody_velocity;
api.addRigidbodyForce = modu_ctx_add_rigidbody_force;
api.addRigidbodyImpulse = modu_ctx_add_rigidbody_impulse;
api.getSettingFloat = modu_ctx_get_setting_float;
api.getSettingBool = modu_ctx_get_setting_bool;
api.getSettingString = modu_ctx_get_setting_string;
api.setSettingFloat = modu_ctx_set_setting_float;
api.setSettingBool = modu_ctx_set_setting_bool;
api.setSettingString = modu_ctx_set_setting_string;
api.addConsoleMessage = modu_ctx_add_console_message;
return api;
}

55
src/ManagedBindings.h Normal file
View File

@@ -0,0 +1,55 @@
#pragma once
#include "ScriptRuntime.h"
#include <cstdint>
extern "C" {
int modu_ctx_get_object_id(ScriptContext* ctx);
void modu_ctx_get_position(ScriptContext* ctx, float* x, float* y, float* z);
void modu_ctx_set_position(ScriptContext* ctx, float x, float y, float z);
void modu_ctx_get_rotation(ScriptContext* ctx, float* x, float* y, float* z);
void modu_ctx_set_rotation(ScriptContext* ctx, float x, float y, float z);
void modu_ctx_get_scale(ScriptContext* ctx, float* x, float* y, float* z);
void modu_ctx_set_scale(ScriptContext* ctx, float x, float y, float z);
int modu_ctx_has_rigidbody(ScriptContext* ctx);
int modu_ctx_ensure_rigidbody(ScriptContext* ctx, int useGravity, int kinematic);
int modu_ctx_set_rigidbody_velocity(ScriptContext* ctx, float x, float y, float z);
int modu_ctx_get_rigidbody_velocity(ScriptContext* ctx, float* x, float* y, float* z);
int modu_ctx_add_rigidbody_force(ScriptContext* ctx, float x, float y, float z);
int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z);
float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback);
int modu_ctx_get_setting_bool(ScriptContext* ctx, const char* key, int fallback);
void modu_ctx_get_setting_string(ScriptContext* ctx, const char* key, const char* fallback,
char* outBuffer, int outBufferSize);
void modu_ctx_set_setting_float(ScriptContext* ctx, const char* key, float value);
void modu_ctx_set_setting_bool(ScriptContext* ctx, const char* key, int value);
void modu_ctx_set_setting_string(ScriptContext* ctx, const char* key, const char* value);
void modu_ctx_add_console_message(ScriptContext* ctx, const char* message, int type);
}
struct ManagedNativeApi {
uint32_t version = 1;
int (*getObjectId)(ScriptContext* ctx) = nullptr;
void (*getPosition)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr;
void (*setPosition)(ScriptContext* ctx, float x, float y, float z) = nullptr;
void (*getRotation)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr;
void (*setRotation)(ScriptContext* ctx, float x, float y, float z) = nullptr;
void (*getScale)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr;
void (*setScale)(ScriptContext* ctx, float x, float y, float z) = nullptr;
int (*hasRigidbody)(ScriptContext* ctx) = nullptr;
int (*ensureRigidbody)(ScriptContext* ctx, int useGravity, int kinematic) = nullptr;
int (*setRigidbodyVelocity)(ScriptContext* ctx, float x, float y, float z) = nullptr;
int (*getRigidbodyVelocity)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr;
int (*addRigidbodyForce)(ScriptContext* ctx, float x, float y, float z) = nullptr;
int (*addRigidbodyImpulse)(ScriptContext* ctx, float x, float y, float z) = nullptr;
float (*getSettingFloat)(ScriptContext* ctx, const char* key, float fallback) = nullptr;
int (*getSettingBool)(ScriptContext* ctx, const char* key, int fallback) = nullptr;
void (*getSettingString)(ScriptContext* ctx, const char* key, const char* fallback,
char* outBuffer, int outBufferSize) = nullptr;
void (*setSettingFloat)(ScriptContext* ctx, const char* key, float value) = nullptr;
void (*setSettingBool)(ScriptContext* ctx, const char* key, int value) = nullptr;
void (*setSettingString)(ScriptContext* ctx, const char* key, const char* value) = nullptr;
void (*addConsoleMessage)(ScriptContext* ctx, const char* message, int type) = nullptr;
};
ManagedNativeApi BuildManagedNativeApi();

View File

@@ -0,0 +1,452 @@
#include "ManagedScriptRuntime.h"
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
#if MODULARITY_USE_MONO
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/mono-config.h>
#include <mono/metadata/threads.h>
#endif
#if MODULARITY_USE_MONO
namespace {
std::string trim(std::string value) {
auto is_space = [](unsigned char c) { return std::isspace(c) != 0; };
while (!value.empty() && is_space(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
}
while (!value.empty() && is_space(static_cast<unsigned char>(value.back()))) {
value.pop_back();
}
return value;
}
std::string stripAssemblyQualifier(const std::string& typeName) {
auto comma = typeName.find(',');
if (comma == std::string::npos) {
return trim(typeName);
}
return trim(typeName.substr(0, comma));
}
bool splitNamespaceAndName(const std::string& fullName, std::string& nameSpace, std::string& name) {
auto dot = fullName.rfind('.');
if (dot == std::string::npos) {
nameSpace.clear();
name = fullName;
return !name.empty();
}
nameSpace = fullName.substr(0, dot);
name = fullName.substr(dot + 1);
return !name.empty();
}
std::string monoExceptionToString(MonoObject* exc) {
if (!exc) return "Unknown managed exception";
MonoString* strObj = mono_object_to_string(exc, nullptr);
if (!strObj) return "Managed exception (no ToString)";
char* utf8 = mono_string_to_utf8(strObj);
std::string out = utf8 ? utf8 : "Managed exception (utf8 conversion failed)";
mono_free(utf8);
return out;
}
fs::path resolveMonoRoot() {
const char* env = std::getenv("MODU_MONO_ROOT");
if (env && env[0] != '\0') {
return fs::path(env);
}
std::vector<fs::path> candidates;
fs::path cwd = fs::current_path();
candidates.push_back(cwd / "src" / "ThirdParty" / "mono");
candidates.push_back(cwd / ".." / "src" / "ThirdParty" / "mono");
candidates.push_back(cwd / "ThirdParty" / "mono");
candidates.push_back(cwd / ".." / "ThirdParty" / "mono");
for (const auto& path : candidates) {
std::error_code ec;
if (fs::exists(path, ec)) {
return path;
}
}
return {};
}
void configureMonoFromRoot(const fs::path& root) {
fs::path libDir = root / "lib";
fs::path etcDir = root / "etc";
mono_set_dirs(libDir.string().c_str(), etcDir.string().c_str());
fs::path assembliesDir = root / "lib" / "mono" / "4.5";
if (fs::exists(assembliesDir)) {
mono_set_assemblies_path(assembliesDir.string().c_str());
}
mono_config_parse(nullptr);
}
std::string toKey(const fs::path& assemblyPath, const std::string& typeName) {
return assemblyPath.lexically_normal().string() + "|" + typeName;
}
} // namespace
struct ManagedScriptRuntime::MonoState {
MonoDomain* rootDomain = nullptr;
MonoDomain* scriptDomain = nullptr;
fs::path monoRoot;
};
ManagedScriptRuntime::~ManagedScriptRuntime() {
unloadAll();
monoState.reset();
}
void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const {
if (!state) return;
if (state->rootDomain) {
if (state->scriptDomain) {
mono_domain_set(state->rootDomain, false);
mono_domain_unload(state->scriptDomain);
state->scriptDomain = nullptr;
}
mono_jit_cleanup(state->rootDomain);
}
delete state;
}
bool ManagedScriptRuntime::ensureHost(const fs::path& assemblyPath) {
(void)assemblyPath;
if (monoState && monoState->rootDomain && monoState->scriptDomain) return true;
if (!monoState) {
fs::path monoRoot = resolveMonoRoot();
if (monoRoot.empty()) {
lastError = "Mono root not found. Set MODU_MONO_ROOT or vendor Mono in src/ThirdParty/mono.";
return false;
}
auto* state = new MonoState();
state->monoRoot = monoRoot;
configureMonoFromRoot(monoRoot);
state->rootDomain = mono_jit_init_version("Modularity", "v4.0.30319");
if (!state->rootDomain) {
delete state;
lastError = "Failed to initialize Mono JIT";
return false;
}
monoState.reset(state);
std::cerr << "[Managed] Mono root: " << monoRoot << std::endl;
}
if (!monoState->scriptDomain) {
monoState->scriptDomain = mono_domain_create_appdomain(const_cast<char*>("ModularityScripts"), nullptr);
if (!monoState->scriptDomain) {
lastError = "Failed to create Mono appdomain";
return false;
}
}
mono_domain_set(monoState->scriptDomain, false);
mono_thread_attach(monoState->scriptDomain);
return true;
}
static MonoAssembly* loadAssembly(MonoDomain* domain, const fs::path& assemblyPath, MonoImage** outImage,
std::string& error) {
if (!outImage) {
error = "Internal error: missing image output";
return nullptr;
}
*outImage = nullptr;
std::error_code ec;
if (!fs::exists(assemblyPath, ec)) {
error = "Missing managed assembly: " + assemblyPath.string();
return nullptr;
}
mono_domain_set(domain, false);
MonoAssembly* assembly = mono_domain_assembly_open(domain, assemblyPath.string().c_str());
if (!assembly) {
error = "Mono failed to load assembly: " + assemblyPath.string();
return nullptr;
}
MonoImage* image = mono_assembly_get_image(assembly);
if (!image) {
error = "Mono failed to get image: " + assemblyPath.string();
return nullptr;
}
*outImage = image;
return assembly;
}
bool ManagedScriptRuntime::ensureApiInjected(const fs::path& assemblyPath) {
if (apiInjected) return true;
if (!monoState || !monoState->scriptDomain) return false;
std::string error;
MonoImage* image = nullptr;
MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error);
if (!assembly || !image) {
lastError = error;
return false;
}
MonoClass* hostClass = mono_class_from_name(image, "ModuCPP", "Host");
if (!hostClass) {
lastError = "Managed class ModuCPP.Host not found";
return false;
}
MonoMethod* setApiMethod = mono_class_get_method_from_name(hostClass, "SetNativeApi", 1);
if (!setApiMethod) {
lastError = "Managed method ModuCPP.Host.SetNativeApi not found";
return false;
}
mono_domain_set(monoState->scriptDomain, false);
mono_thread_attach(monoState->scriptDomain);
intptr_t apiPtr = reinterpret_cast<intptr_t>(&api);
void* args[1] = { &apiPtr };
MonoObject* exc = nullptr;
mono_runtime_invoke(setApiMethod, nullptr, args, &exc);
if (exc) {
lastError = monoExceptionToString(exc);
return false;
}
apiInjected = true;
return true;
}
bool ManagedScriptRuntime::loadModuleMethods(Module& mod, const fs::path& assemblyPath,
const std::string& typeName) {
if (!monoState || !monoState->scriptDomain || typeName.empty()) {
lastError = "Managed script type is required";
return false;
}
std::string error;
MonoImage* image = nullptr;
MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error);
if (!assembly || !image) {
lastError = error;
return false;
}
std::string normalized = stripAssemblyQualifier(typeName);
std::string nameSpace;
std::string className;
if (!splitNamespaceAndName(normalized, nameSpace, className)) {
lastError = "Managed script type name is invalid";
return false;
}
MonoClass* klass = mono_class_from_name(image, nameSpace.c_str(), className.c_str());
if (!klass) {
lastError = "Managed type not found: " + normalized;
return false;
}
MonoMethod* inspector = mono_class_get_method_from_name(klass, "Script_OnInspector", 1);
MonoMethod* begin = mono_class_get_method_from_name(klass, "Script_Begin", 2);
MonoMethod* spec = mono_class_get_method_from_name(klass, "Script_Spec", 2);
MonoMethod* testEditor = mono_class_get_method_from_name(klass, "Script_TestEditor", 2);
MonoMethod* update = mono_class_get_method_from_name(klass, "Script_Update", 2);
MonoMethod* tickUpdate = mono_class_get_method_from_name(klass, "Script_TickUpdate", 2);
mod.inspectorMethod = inspector;
mod.beginMethod = begin;
mod.specMethod = spec;
mod.testEditorMethod = testEditor;
mod.updateMethod = update;
mod.tickUpdateMethod = tickUpdate;
if (!inspector && !begin && !spec && !testEditor && !update && !tickUpdate) {
lastError = "No managed script exports found";
return false;
}
return true;
}
ManagedScriptRuntime::Module* ManagedScriptRuntime::getModule(const fs::path& assemblyPath,
const std::string& typeName) {
lastError.clear();
if (assemblyPath.empty()) {
lastError = "Managed assembly path is empty";
return nullptr;
}
std::string key = toKey(assemblyPath, typeName);
auto it = modules.find(key);
if (it != modules.end()) return &it->second;
if (!ensureHost(assemblyPath)) return nullptr;
if (!ensureApiInjected(assemblyPath)) return nullptr;
Module mod;
mod.assemblyPath = assemblyPath;
mod.typeName = typeName;
if (!loadModuleMethods(mod, assemblyPath, typeName)) return nullptr;
modules[key] = mod;
return &modules[key];
}
static bool invokeMonoMethod(MonoDomain* domain, MonoMethod* method, ScriptContext* ctx,
float deltaTime, bool hasDelta, std::string& error) {
if (!method) return false;
mono_domain_set(domain, false);
mono_thread_attach(domain);
intptr_t ctxPtr = reinterpret_cast<intptr_t>(ctx);
void* argsWithDelta[2] = { &ctxPtr, &deltaTime };
void* argsNoDelta[1] = { &ctxPtr };
MonoObject* exc = nullptr;
mono_runtime_invoke(method, nullptr, hasDelta ? argsWithDelta : argsNoDelta, &exc);
if (exc) {
error = monoExceptionToString(exc);
return false;
}
return true;
}
bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName,
ScriptContext& ctx) {
Module* mod = getModule(assemblyPath, typeName);
if (!mod) return false;
MonoMethod* inspector = reinterpret_cast<MonoMethod*>(mod->inspectorMethod);
if (!inspector) {
lastError.clear();
return false;
}
std::string error;
bool ok = invokeMonoMethod(monoState->scriptDomain, inspector, &ctx, 0.0f, false, error);
if (!ok) {
lastError = error;
}
return ok;
}
bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) {
Module* mod = getModule(assemblyPath, typeName);
if (!mod) return false;
if (!mod->inspectorMethod) {
lastError.clear();
return false;
}
return true;
}
void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName,
ScriptContext& ctx, float deltaTime,
bool runSpec, bool runTest) {
Module* mod = getModule(assemblyPath, typeName);
if (!mod) return;
int objId = ctx.object ? ctx.object->id : -1;
MonoMethod* begin = reinterpret_cast<MonoMethod*>(mod->beginMethod);
if (objId >= 0 && begin && mod->beginCalledObjects.find(objId) == mod->beginCalledObjects.end()) {
std::string error;
if (!invokeMonoMethod(monoState->scriptDomain, begin, &ctx, deltaTime, true, error)) {
lastError = error;
return;
}
mod->beginCalledObjects.insert(objId);
}
MonoMethod* tickUpdate = reinterpret_cast<MonoMethod*>(mod->tickUpdateMethod);
MonoMethod* update = reinterpret_cast<MonoMethod*>(mod->updateMethod);
if (tickUpdate || update) {
std::string error;
if (!invokeMonoMethod(monoState->scriptDomain, tickUpdate ? tickUpdate : update,
&ctx, deltaTime, true, error)) {
lastError = error;
return;
}
}
if (runSpec) {
MonoMethod* spec = reinterpret_cast<MonoMethod*>(mod->specMethod);
if (spec) {
std::string error;
if (!invokeMonoMethod(monoState->scriptDomain, spec, &ctx, deltaTime, true, error)) {
lastError = error;
return;
}
}
}
if (runTest) {
MonoMethod* test = reinterpret_cast<MonoMethod*>(mod->testEditorMethod);
if (test) {
std::string error;
if (!invokeMonoMethod(monoState->scriptDomain, test, &ctx, deltaTime, true, error)) {
lastError = error;
return;
}
}
}
}
void ManagedScriptRuntime::unloadAll() {
modules.clear();
apiInjected = false;
lastError.clear();
if (monoState && monoState->rootDomain && monoState->scriptDomain) {
mono_domain_set(monoState->rootDomain, false);
mono_domain_unload(monoState->scriptDomain);
monoState->scriptDomain = nullptr;
}
}
#else
ManagedScriptRuntime::~ManagedScriptRuntime() = default;
struct ManagedScriptRuntime::MonoState {};
void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const {
delete state;
}
bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) {
(void)assemblyPath;
(void)typeName;
lastError = "Managed scripts disabled (Mono not built).";
return false;
}
bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx) {
(void)assemblyPath;
(void)typeName;
(void)ctx;
lastError = "Managed scripts disabled (Mono not built).";
return false;
}
void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName,
ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest) {
(void)assemblyPath;
(void)typeName;
(void)ctx;
(void)deltaTime;
(void)runSpec;
(void)runTest;
lastError = "Managed scripts disabled (Mono not built).";
}
void ManagedScriptRuntime::unloadAll() {
modules.clear();
monoState.reset();
apiInjected = false;
lastError.clear();
}
#endif

View File

@@ -0,0 +1,50 @@
#pragma once
#include "ManagedBindings.h"
#include "ScriptRuntime.h"
#include <filesystem>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
class ManagedScriptRuntime {
public:
~ManagedScriptRuntime();
bool hasInspector(const fs::path& assemblyPath, const std::string& typeName);
bool invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx);
void tickModule(const fs::path& assemblyPath, const std::string& typeName,
ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest);
void unloadAll();
const std::string& getLastError() const { return lastError; }
struct Module {
fs::path assemblyPath;
std::string typeName;
void* inspectorMethod = nullptr;
void* beginMethod = nullptr;
void* specMethod = nullptr;
void* testEditorMethod = nullptr;
void* updateMethod = nullptr;
void* tickUpdateMethod = nullptr;
std::unordered_set<int> beginCalledObjects;
};
struct MonoState;
struct MonoStateDeleter {
void operator()(MonoState* state) const;
};
private:
Module* getModule(const fs::path& assemblyPath, const std::string& typeName);
bool ensureHost(const fs::path& assemblyPath);
bool ensureApiInjected(const fs::path& assemblyPath);
bool loadModuleMethods(Module& mod, const fs::path& assemblyPath, const std::string& typeName);
std::unordered_map<std::string, Module> modules;
std::string lastError;
std::unique_ptr<MonoState, MonoStateDeleter> monoState;
bool apiInjected = false;
ManagedNativeApi api = BuildManagedNativeApi();
};

View File

@@ -4,6 +4,10 @@
#include <fstream>
#include <cstdint>
#include <cstring>
#include <unordered_set>
#include <functional>
#include <assimp/material.h>
#include "ThirdParty/glm/gtc/quaternion.hpp"
ModelLoader& ModelLoader::getInstance() {
static ModelLoader instance;
@@ -11,6 +15,13 @@ ModelLoader& ModelLoader::getInstance() {
}
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out);
static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
std::vector<OBJLoader::LoadedMesh>& loadedMeshes,
ModelSceneData& out, std::string& errorMsg);
static void buildSceneNodes(const aiScene* scene,
const std::vector<int>& meshIndices,
ModelSceneData& out);
static glm::mat4 aiToGlm(const aiMatrix4x4& m);
ModelLoader& getModelLoader() {
return ModelLoader::getInstance();
@@ -91,6 +102,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
if (loadedMeshes[i].path == filepath) {
result.success = true;
result.meshIndex = static_cast<int>(i);
result.meshIndices.push_back(result.meshIndex);
const auto& mesh = loadedMeshes[i];
result.vertexCount = mesh.vertexCount;
result.faceCount = mesh.faceCount;
@@ -273,6 +285,92 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
return result;
}
bool ModelLoader::loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg) {
out = ModelSceneData();
if (!isSupported(filepath)) {
errorMsg = "Unsupported file format: " + fs::path(filepath).extension().string();
return false;
}
auto cached = cachedScenes.find(filepath);
if (cached != cachedScenes.end()) {
out = cached->second;
return true;
}
unsigned int importFlags =
aiProcess_Triangulate |
aiProcess_GenSmoothNormals |
aiProcess_FlipUVs |
aiProcess_CalcTangentSpace |
aiProcess_JoinIdenticalVertices |
aiProcess_SortByPType |
aiProcess_ValidateDataStructure;
const aiScene* scene = importer.ReadFile(filepath, importFlags);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
errorMsg = "Assimp error: " + std::string(importer.GetErrorString());
return false;
}
if (!buildSceneMeshes(filepath, scene, loadedMeshes, out, errorMsg)) {
return false;
}
buildSceneNodes(scene, out.meshIndices, out);
out.animations.clear();
if (scene->mNumAnimations > 0) {
out.animations.reserve(scene->mNumAnimations);
for (unsigned int i = 0; i < scene->mNumAnimations; ++i) {
aiAnimation* anim = scene->mAnimations[i];
ModelSceneData::AnimationClip clip;
clip.name = anim->mName.C_Str();
if (clip.name.empty()) {
clip.name = "Clip_" + std::to_string(i);
}
clip.duration = anim->mDuration;
clip.ticksPerSecond = anim->mTicksPerSecond != 0.0 ? anim->mTicksPerSecond : 25.0;
clip.channels.reserve(anim->mNumChannels);
for (unsigned int c = 0; c < anim->mNumChannels; ++c) {
aiNodeAnim* ch = anim->mChannels[c];
ModelSceneData::AnimChannel channel;
channel.nodeName = ch->mNodeName.C_Str();
channel.positions.reserve(ch->mNumPositionKeys);
for (unsigned int k = 0; k < ch->mNumPositionKeys; ++k) {
const auto& key = ch->mPositionKeys[k];
ModelSceneData::AnimVecKey vk;
vk.time = static_cast<float>(key.mTime);
vk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z);
channel.positions.push_back(vk);
}
channel.rotations.reserve(ch->mNumRotationKeys);
for (unsigned int k = 0; k < ch->mNumRotationKeys; ++k) {
const auto& key = ch->mRotationKeys[k];
ModelSceneData::AnimQuatKey qk;
qk.time = static_cast<float>(key.mTime);
qk.value = glm::quat(key.mValue.w, key.mValue.x, key.mValue.y, key.mValue.z);
channel.rotations.push_back(qk);
}
channel.scales.reserve(ch->mNumScalingKeys);
for (unsigned int k = 0; k < ch->mNumScalingKeys; ++k) {
const auto& key = ch->mScalingKeys[k];
ModelSceneData::AnimVecKey sk;
sk.time = static_cast<float>(key.mTime);
sk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z);
channel.scales.push_back(sk);
}
clip.channels.push_back(std::move(channel));
}
out.animations.push_back(std::move(clip));
}
}
cachedScenes[filepath] = out;
return true;
}
bool ModelLoader::exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg) {
fs::path inPath(inputFile);
if (!fs::exists(inPath)) {
@@ -350,17 +448,51 @@ bool ModelLoader::loadRawMesh(const std::string& filepath, RawMeshAsset& out, st
return false;
}
in.seekg(0, std::ios::end);
std::streamoff fileSize = in.tellg();
in.seekg(sizeof(header), std::ios::beg);
in.read(reinterpret_cast<char*>(&out.boundsMin.x), sizeof(float) * 3);
in.read(reinterpret_cast<char*>(&out.boundsMax.x), sizeof(float) * 3);
const std::streamoff payloadSize = fileSize - sizeof(header) - sizeof(float) * 6;
const std::streamoff positionsSize = static_cast<std::streamoff>(sizeof(glm::vec3)) * header.vertexCount;
const std::streamoff normalsSize = static_cast<std::streamoff>(sizeof(glm::vec3)) * header.vertexCount;
const std::streamoff uvsSize = static_cast<std::streamoff>(sizeof(glm::vec2)) * header.vertexCount;
const std::streamoff facesSize = static_cast<std::streamoff>(sizeof(glm::u32vec3)) * header.faceCount;
bool hasNormals = false;
bool hasUVs = false;
if (payloadSize == positionsSize + normalsSize + uvsSize + facesSize) {
hasNormals = true;
hasUVs = true;
} else if (payloadSize == positionsSize + normalsSize + facesSize) {
hasNormals = true;
} else if (payloadSize == positionsSize + uvsSize + facesSize) {
hasUVs = true;
} else if (payloadSize == positionsSize + facesSize) {
// legacy raw meshes without normals/uvs
} else if (payloadSize < positionsSize + facesSize) {
errorMsg = "Raw mesh data is truncated";
return false;
}
out.positions.resize(header.vertexCount);
out.normals.resize(header.vertexCount);
out.uvs.resize(header.vertexCount);
out.faces.resize(header.faceCount);
in.read(reinterpret_cast<char*>(out.positions.data()), sizeof(glm::vec3) * out.positions.size());
in.read(reinterpret_cast<char*>(out.normals.data()), sizeof(glm::vec3) * out.normals.size());
in.read(reinterpret_cast<char*>(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size());
if (hasNormals) {
out.normals.resize(header.vertexCount);
in.read(reinterpret_cast<char*>(out.normals.data()), sizeof(glm::vec3) * out.normals.size());
} else {
out.normals.assign(header.vertexCount, glm::vec3(0.0f));
}
if (hasUVs) {
out.uvs.resize(header.vertexCount);
in.read(reinterpret_cast<char*>(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size());
} else {
out.uvs.assign(header.vertexCount, glm::vec2(0.0f));
}
in.read(reinterpret_cast<char*>(out.faces.data()), sizeof(glm::u32vec3) * out.faces.size());
if (!in.good()) {
@@ -435,6 +567,18 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file
outPath.replace_extension(".rmesh");
}
std::vector<glm::vec3> normalsData;
normalsData.resize(asset.positions.size(), glm::vec3(0.0f));
if (asset.normals.size() == asset.positions.size()) {
normalsData = asset.normals;
}
std::vector<glm::vec2> uvsData;
uvsData.resize(asset.positions.size(), glm::vec2(0.0f));
if (asset.uvs.size() == asset.positions.size()) {
uvsData = asset.uvs;
}
struct Header {
char magic[6] = {'R','M','E','S','H','\0'};
uint32_t version = 1;
@@ -455,8 +599,8 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file
out.write(reinterpret_cast<const char*>(&asset.boundsMin.x), sizeof(float) * 3);
out.write(reinterpret_cast<const char*>(&asset.boundsMax.x), sizeof(float) * 3);
out.write(reinterpret_cast<const char*>(asset.positions.data()), sizeof(glm::vec3) * asset.positions.size());
out.write(reinterpret_cast<const char*>(asset.normals.data()), sizeof(glm::vec3) * asset.normals.size());
out.write(reinterpret_cast<const char*>(asset.uvs.data()), sizeof(glm::vec2) * asset.uvs.size());
out.write(reinterpret_cast<const char*>(normalsData.data()), sizeof(glm::vec3) * normalsData.size());
out.write(reinterpret_cast<const char*>(uvsData.data()), sizeof(glm::vec2) * uvsData.size());
out.write(reinterpret_cast<const char*>(asset.faces.data()), sizeof(glm::u32vec3) * asset.faces.size());
if (!out.good()) {
@@ -544,6 +688,84 @@ bool ModelLoader::updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::s
return true;
}
int ModelLoader::addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath,
const std::string& name, std::string& errorMsg) {
if (asset.positions.empty() || asset.faces.empty()) {
errorMsg = "Raw mesh is empty";
return -1;
}
std::vector<float> vertices;
vertices.reserve(asset.faces.size() * 3 * 8);
std::vector<glm::vec3> triPositions;
triPositions.reserve(asset.faces.size() * 3);
auto getPos = [&](uint32_t idx) -> const glm::vec3& { return asset.positions[idx]; };
auto getNorm = [&](uint32_t idx) -> glm::vec3 {
if (idx < asset.normals.size()) return asset.normals[idx];
return glm::vec3(0.0f);
};
auto getUV = [&](uint32_t idx) -> glm::vec2 {
if (idx < asset.uvs.size()) return asset.uvs[idx];
return glm::vec2(0.0f);
};
for (const auto& face : asset.faces) {
const uint32_t idx[3] = { face.x, face.y, face.z };
glm::vec3 faceNormal(0.0f);
if (!asset.hasNormals) {
const glm::vec3& a = getPos(idx[0]);
const glm::vec3& b = getPos(idx[1]);
const glm::vec3& c = getPos(idx[2]);
faceNormal = glm::normalize(glm::cross(b - a, c - a));
}
for (int i = 0; i < 3; i++) {
glm::vec3 pos = getPos(idx[i]);
glm::vec3 n = asset.hasNormals ? getNorm(idx[i]) : faceNormal;
glm::vec2 uv = asset.hasUVs ? getUV(idx[i]) : glm::vec2(0.0f);
triPositions.push_back(pos);
vertices.push_back(pos.x);
vertices.push_back(pos.y);
vertices.push_back(pos.z);
vertices.push_back(n.x);
vertices.push_back(n.y);
vertices.push_back(n.z);
vertices.push_back(uv.x);
vertices.push_back(uv.y);
}
}
if (vertices.empty()) {
errorMsg = "No vertices generated for GPU upload";
return -1;
}
OBJLoader::LoadedMesh loaded;
loaded.path = sourcePath;
loaded.name = name.empty() ? "StaticBatch" : name;
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
loaded.faceCount = static_cast<int>(asset.faces.size());
loaded.hasNormals = asset.hasNormals;
loaded.hasTexCoords = asset.hasUVs;
loaded.boundsMin = asset.boundsMin;
loaded.boundsMax = asset.boundsMax;
loaded.triangleVertices = std::move(triPositions);
loaded.positions = asset.positions;
loaded.triangleIndices.clear();
loaded.triangleIndices.reserve(asset.faces.size() * 3);
for (const auto& face : asset.faces) {
loaded.triangleIndices.push_back(face.x);
loaded.triangleIndices.push_back(face.y);
loaded.triangleIndices.push_back(face.z);
}
int newIndex = static_cast<int>(loadedMeshes.size());
loadedMeshes.push_back(std::move(loaded));
return newIndex;
}
static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
return glm::mat4(
m.a1, m.b1, m.c1, m.d1,
@@ -553,6 +775,303 @@ static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
);
}
static glm::vec3 quatToEulerDegrees(const aiQuaternion& q) {
glm::quat gq(q.w, q.x, q.y, q.z);
glm::vec3 euler = glm::degrees(glm::eulerAngles(gq));
return euler;
}
static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
std::vector<OBJLoader::LoadedMesh>& loadedMeshes,
ModelSceneData& out, std::string& errorMsg) {
out.meshIndices.assign(scene->mNumMeshes, -1);
out.meshMaterialIndices.assign(scene->mNumMeshes, -1);
out.materials.clear();
out.materials.reserve(scene->mNumMaterials);
for (unsigned int i = 0; i < scene->mNumMaterials; ++i) {
aiMaterial* mat = scene->mMaterials[i];
ModelMaterialInfo info;
info.name = mat->GetName().C_Str();
if (info.name.empty()) {
info.name = "Material_" + std::to_string(i);
}
aiColor3D diffuse(1.0f, 1.0f, 1.0f);
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse)) {
info.props.color = glm::vec3(diffuse.r, diffuse.g, diffuse.b);
}
aiColor3D specular(0.0f, 0.0f, 0.0f);
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, specular)) {
float avg = (specular.r + specular.g + specular.b) / 3.0f;
info.props.specularStrength = avg;
}
float shininess = info.props.shininess;
if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shininess)) {
info.props.shininess = shininess;
}
aiString tex;
if (AI_SUCCESS == mat->GetTexture(aiTextureType_DIFFUSE, 0, &tex)) {
info.albedoPath = tex.C_Str();
}
if (AI_SUCCESS == mat->GetTexture(aiTextureType_NORMALS, 0, &tex)) {
info.normalPath = tex.C_Str();
} else if (AI_SUCCESS == mat->GetTexture(aiTextureType_HEIGHT, 0, &tex)) {
info.normalPath = tex.C_Str();
}
if (!info.albedoPath.empty()) {
info.props.textureMix = 1.0f;
}
out.materials.push_back(info);
}
for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[i];
if (!mesh || mesh->mNumVertices == 0 || mesh->mNumFaces == 0) {
continue;
}
std::vector<float> vertices;
struct BoneVertex {
int ids[4];
float weights[4];
};
std::vector<BoneVertex> boneVertices;
std::vector<glm::ivec4> vertexBoneIds(mesh->mNumVertices, glm::ivec4(0));
std::vector<glm::vec4> vertexBoneWeights(mesh->mNumVertices, glm::vec4(0.0f));
std::vector<glm::vec3> triPositions;
std::vector<glm::vec3> positions;
std::vector<uint32_t> triangleIndices;
vertices.reserve(mesh->mNumFaces * 3 * 8);
boneVertices.reserve(mesh->mNumFaces * 3);
triPositions.reserve(mesh->mNumFaces * 3);
positions.reserve(mesh->mNumVertices);
triangleIndices.reserve(mesh->mNumFaces * 3);
glm::vec3 boundsMin(FLT_MAX);
glm::vec3 boundsMax(-FLT_MAX);
std::vector<std::string> boneNames;
std::vector<glm::mat4> inverseBindMatrices;
if (mesh->mNumBones > 0) {
boneNames.reserve(mesh->mNumBones);
inverseBindMatrices.reserve(mesh->mNumBones);
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
aiBone* bone = mesh->mBones[b];
int boneIndex = static_cast<int>(boneNames.size());
boneNames.push_back(bone->mName.C_Str());
inverseBindMatrices.push_back(aiToGlm(bone->mOffsetMatrix));
for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
unsigned int vId = bone->mWeights[w].mVertexId;
float weight = bone->mWeights[w].mWeight;
if (vId >= vertexBoneWeights.size()) continue;
glm::vec4& weights = vertexBoneWeights[vId];
glm::ivec4& ids = vertexBoneIds[vId];
int replaceIndex = -1;
float minWeight = weight;
for (int k = 0; k < 4; ++k) {
if (weights[k] == 0.0f) {
replaceIndex = k;
break;
}
if (weights[k] < minWeight) {
minWeight = weights[k];
replaceIndex = k;
}
}
if (replaceIndex >= 0) {
weights[replaceIndex] = weight;
ids[replaceIndex] = boneIndex;
}
}
}
}
for (unsigned int v = 0; v < mesh->mNumVertices; ++v) {
glm::vec3 pos(mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z);
positions.push_back(pos);
boundsMin.x = std::min(boundsMin.x, pos.x);
boundsMin.y = std::min(boundsMin.y, pos.y);
boundsMin.z = std::min(boundsMin.z, pos.z);
boundsMax.x = std::max(boundsMax.x, pos.x);
boundsMax.y = std::max(boundsMax.y, pos.y);
boundsMax.z = std::max(boundsMax.z, pos.z);
}
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
const aiFace& face = mesh->mFaces[f];
if (face.mNumIndices != 3) continue;
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[0]));
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[1]));
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[2]));
for (unsigned int j = 0; j < 3; ++j) {
unsigned int index = face.mIndices[j];
glm::vec3 pos(mesh->mVertices[index].x,
mesh->mVertices[index].y,
mesh->mVertices[index].z);
vertices.push_back(pos.x);
vertices.push_back(pos.y);
vertices.push_back(pos.z);
triPositions.push_back(pos);
if (mesh->mNormals) {
glm::vec3 n(mesh->mNormals[index].x,
mesh->mNormals[index].y,
mesh->mNormals[index].z);
vertices.push_back(n.x);
vertices.push_back(n.y);
vertices.push_back(n.z);
} else {
vertices.push_back(0.0f);
vertices.push_back(1.0f);
vertices.push_back(0.0f);
}
if (mesh->mTextureCoords[0]) {
vertices.push_back(mesh->mTextureCoords[0][index].x);
vertices.push_back(mesh->mTextureCoords[0][index].y);
} else {
vertices.push_back(0.0f);
vertices.push_back(0.0f);
}
BoneVertex bv{};
glm::ivec4 ids = vertexBoneIds[index];
glm::vec4 weights = vertexBoneWeights[index];
float weightSum = weights.x + weights.y + weights.z + weights.w;
if (weightSum > 0.0f) {
weights /= weightSum;
}
bv.ids[0] = ids.x;
bv.ids[1] = ids.y;
bv.ids[2] = ids.z;
bv.ids[3] = ids.w;
bv.weights[0] = weights.x;
bv.weights[1] = weights.y;
bv.weights[2] = weights.z;
bv.weights[3] = weights.w;
boneVertices.push_back(bv);
}
}
if (vertices.empty()) {
continue;
}
OBJLoader::LoadedMesh loaded;
loaded.path = filepath;
loaded.name = mesh->mName.C_Str();
if (loaded.name.empty()) {
loaded.name = fs::path(filepath).stem().string() + "_mesh" + std::to_string(i);
}
bool isSkinned = mesh->mNumBones > 0 && boneVertices.size() == vertices.size() / 8;
if (isSkinned) {
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float), true,
boneVertices.data(), boneVertices.size() * sizeof(BoneVertex));
} else {
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
}
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
loaded.faceCount = static_cast<int>(mesh->mNumFaces);
loaded.hasNormals = mesh->mNormals != nullptr;
loaded.hasTexCoords = mesh->mTextureCoords[0] != nullptr;
loaded.boundsMin = boundsMin;
loaded.boundsMax = boundsMax;
loaded.triangleVertices = std::move(triPositions);
loaded.positions = std::move(positions);
loaded.triangleIndices = std::move(triangleIndices);
loaded.isSkinned = isSkinned;
loaded.boneNames = std::move(boneNames);
loaded.inverseBindMatrices = std::move(inverseBindMatrices);
if (isSkinned) {
loaded.boneIds.reserve(boneVertices.size());
loaded.boneWeights.reserve(boneVertices.size());
for (const auto& bv : boneVertices) {
loaded.boneIds.emplace_back(bv.ids[0], bv.ids[1], bv.ids[2], bv.ids[3]);
loaded.boneWeights.emplace_back(bv.weights[0], bv.weights[1], bv.weights[2], bv.weights[3]);
}
loaded.baseVertices = vertices;
}
out.meshMaterialIndices[i] = mesh->mMaterialIndex < (int)out.materials.size()
? static_cast<int>(mesh->mMaterialIndex)
: -1;
out.meshIndices[i] = static_cast<int>(loadedMeshes.size());
loadedMeshes.push_back(std::move(loaded));
}
bool anyMesh = false;
for (int idx : out.meshIndices) {
if (idx >= 0) { anyMesh = true; break; }
}
if (!anyMesh) {
errorMsg = "No meshes found in model file";
return false;
}
return true;
}
static void buildSceneNodes(const aiScene* scene,
const std::vector<int>& meshIndices,
ModelSceneData& out) {
std::unordered_set<std::string> boneNames;
for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[i];
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
boneNames.insert(mesh->mBones[b]->mName.C_Str());
}
}
std::function<void(aiNode*, int)> walk = [&](aiNode* node, int parentIndex) {
ModelNodeInfo info;
info.name = node->mName.C_Str();
if (info.name.empty()) {
info.name = "Node_" + std::to_string(out.nodes.size());
}
info.parentIndex = parentIndex;
info.isBone = boneNames.find(info.name) != boneNames.end();
aiVector3D scaling(1.0f, 1.0f, 1.0f);
aiVector3D position(0.0f, 0.0f, 0.0f);
aiQuaternion rotation;
node->mTransformation.Decompose(scaling, rotation, position);
info.localPosition = glm::vec3(position.x, position.y, position.z);
info.localScale = glm::vec3(scaling.x, scaling.y, scaling.z);
info.localRotation = quatToEulerDegrees(rotation);
for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
unsigned int meshIndex = node->mMeshes[i];
if (meshIndex < meshIndices.size()) {
info.meshIndices.push_back(static_cast<int>(meshIndex));
}
}
int thisIndex = static_cast<int>(out.nodes.size());
out.nodes.push_back(info);
for (unsigned int c = 0; c < node->mNumChildren; ++c) {
walk(node->mChildren[c], thisIndex);
}
};
out.nodes.clear();
if (scene->mRootNode) {
walk(scene->mRootNode, -1);
}
}
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out) {
aiMatrix4x4 current = parentTransform * node->mTransformation;
glm::mat4 gTransform = aiToGlm(current);
@@ -608,6 +1127,89 @@ static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatri
}
}
bool ModelLoader::buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg,
glm::vec3* outRootPos, glm::vec3* outRootRot, glm::vec3* outRootScale) {
out = RawMeshAsset();
fs::path inPath(filepath);
if (!fs::exists(inPath)) {
errorMsg = "File not found: " + filepath;
return false;
}
if (!isSupported(filepath)) {
errorMsg = "Unsupported file format for raw mesh build";
return false;
}
Assimp::Importer localImporter;
unsigned int importFlags =
aiProcess_Triangulate |
aiProcess_JoinIdenticalVertices |
aiProcess_GenSmoothNormals |
aiProcess_FlipUVs;
const aiScene* scene = localImporter.ReadFile(filepath, importFlags);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
errorMsg = "Assimp error: " + std::string(localImporter.GetErrorString());
return false;
}
aiMatrix4x4 parent;
parent = aiMatrix4x4();
if (scene->mRootNode && (outRootPos || outRootRot || outRootScale)) {
aiVector3D scaling(1.0f, 1.0f, 1.0f);
aiVector3D position(0.0f, 0.0f, 0.0f);
aiQuaternion rotation;
scene->mRootNode->mTransformation.Decompose(scaling, rotation, position);
if (outRootPos) *outRootPos = glm::vec3(position.x, position.y, position.z);
if (outRootScale) *outRootScale = glm::vec3(scaling.x, scaling.y, scaling.z);
if (outRootRot) *outRootRot = quatToEulerDegrees(rotation);
aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation;
rootTransform.Inverse();
parent = rootTransform;
}
collectRawMeshData(scene->mRootNode, scene, parent, out);
if (out.positions.empty() || out.faces.empty()) {
errorMsg = "No geometry found to build raw mesh";
return false;
}
out.hasNormals = false;
for (const auto& n : out.normals) {
if (glm::length(n) > 1e-4f) { out.hasNormals = true; break; }
}
out.hasUVs = false;
for (const auto& uv : out.uvs) {
if (std::abs(uv.x) > 1e-6f || std::abs(uv.y) > 1e-6f) { out.hasUVs = true; break; }
}
if (!out.hasNormals) {
out.normals.assign(out.positions.size(), glm::vec3(0.0f));
std::vector<glm::vec3> accum(out.positions.size(), glm::vec3(0.0f));
for (const auto& face : out.faces) {
const glm::vec3& a = out.positions[face.x];
const glm::vec3& b = out.positions[face.y];
const glm::vec3& c = out.positions[face.z];
glm::vec3 n = glm::normalize(glm::cross(b - a, c - a));
accum[face.x] += n;
accum[face.y] += n;
accum[face.z] += n;
}
for (size_t i = 0; i < accum.size(); i++) {
if (glm::length(accum[i]) > 1e-6f) {
out.normals[i] = glm::normalize(accum[i]);
}
}
out.hasNormals = true;
}
return true;
}
void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform,
std::vector<float>& vertices, std::vector<glm::vec3>& triPositions,
std::vector<glm::vec3>& positions, std::vector<uint32_t>& indices,
@@ -720,6 +1322,7 @@ const std::vector<OBJLoader::LoadedMesh>& ModelLoader::getAllMeshes() const {
void ModelLoader::clear() {
loadedMeshes.clear();
cachedScenes.clear();
}
size_t ModelLoader::getMeshCount() const {

View File

@@ -3,6 +3,7 @@
#include "Common.h"
#include "Rendering.h"
#include <cstdint>
#include <unordered_map>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
@@ -19,6 +20,7 @@ struct ModelFormat {
struct ModelLoadResult {
bool success = false;
int meshIndex = -1;
std::vector<int> meshIndices;
std::string errorMessage;
int vertexCount = 0;
int faceCount = 0;
@@ -41,6 +43,51 @@ struct RawMeshAsset {
bool hasUVs = false;
};
struct ModelMaterialInfo {
std::string name;
MaterialProperties props;
std::string albedoPath;
std::string normalPath;
};
struct ModelNodeInfo {
std::string name;
int parentIndex = -1;
std::vector<int> meshIndices;
glm::vec3 localPosition = glm::vec3(0.0f);
glm::vec3 localRotation = glm::vec3(0.0f);
glm::vec3 localScale = glm::vec3(1.0f);
bool isBone = false;
};
struct ModelSceneData {
std::vector<ModelNodeInfo> nodes;
std::vector<ModelMaterialInfo> materials;
std::vector<int> meshIndices;
std::vector<int> meshMaterialIndices;
struct AnimVecKey {
float time = 0.0f;
glm::vec3 value = glm::vec3(0.0f);
};
struct AnimQuatKey {
float time = 0.0f;
glm::quat value = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
};
struct AnimChannel {
std::string nodeName;
std::vector<AnimVecKey> positions;
std::vector<AnimQuatKey> rotations;
std::vector<AnimVecKey> scales;
};
struct AnimationClip {
std::string name;
double duration = 0.0;
double ticksPerSecond = 0.0;
std::vector<AnimChannel> channels;
};
std::vector<AnimationClip> animations;
};
class ModelLoader {
public:
// Singleton access
@@ -48,6 +95,9 @@ public:
// Load a model file (FBX, OBJ, GLTF, etc.)
ModelLoadResult loadModel(const std::string& filepath);
// Load a model scene with node hierarchy and per-mesh materials
bool loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg);
// Get mesh by index
Mesh* getMesh(int index);
@@ -72,6 +122,16 @@ public:
// Update an already-loaded raw mesh in GPU memory
bool updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg);
// Build a raw mesh asset from a model scene without writing to disk
bool buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg,
glm::vec3* outRootPos = nullptr,
glm::vec3* outRootRot = nullptr,
glm::vec3* outRootScale = nullptr);
// Add a raw mesh asset to the GPU cache and return its mesh index
int addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath,
const std::string& name, std::string& errorMsg);
// Get list of supported formats
static std::vector<ModelFormat> getSupportedFormats();
@@ -100,6 +160,7 @@ private:
// Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure)
std::vector<OBJLoader::LoadedMesh> loadedMeshes;
std::unordered_map<std::string, ModelSceneData> cachedScenes;
// Assimp importer (kept for resource management)
Assimp::Importer importer;

View File

@@ -122,9 +122,9 @@ void PhysicsSystem::createGroundPlane() {
bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector<PxVec3>& vertices, std::vector<uint32_t>& indices) const {
const OBJLoader::LoadedMesh* meshInfo = nullptr;
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
}
if (!meshInfo) {
@@ -215,21 +215,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
s->setRestOffset(rest);
};
switch (obj.type) {
case ObjectType::Cube: {
switch (obj.renderType) {
case RenderType::Cube: {
PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f)));
shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break;
}
case ObjectType::Sphere: {
case RenderType::Sphere: {
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;
}
case ObjectType::Capsule: {
case RenderType::Capsule: {
float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f;
radius = std::max(radius, 0.01f);
float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f);
@@ -242,21 +242,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic);
break;
}
case ObjectType::Plane: {
case RenderType::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::Sprite: {
case RenderType::Sprite: {
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: {
case RenderType::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);
@@ -302,9 +302,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
minDim = std::min(radius * 2.0f, halfHeight * 2.0f);
} else {
const OBJLoader::LoadedMesh* meshInfo = nullptr;
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
}
if (!meshInfo) {
@@ -491,7 +491,6 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!isReady()) return;
clearActors();
createGroundPlane();
struct MeshCookInfo {
std::string name;
@@ -506,9 +505,9 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!obj.enabled || !obj.hasCollider || !obj.collider.enabled) continue;
if (obj.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue;
const OBJLoader::LoadedMesh* meshInfo = nullptr;
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
}
if (!meshInfo) continue;

File diff suppressed because it is too large Load Diff

View File

@@ -56,10 +56,18 @@ class SceneSerializer {
public:
static bool saveScene(const fs::path& filePath,
const std::vector<SceneObject>& objects,
int nextId);
int nextId,
float timeOfDay);
static bool loadScene(const fs::path& filePath,
std::vector<SceneObject>& objects,
int& nextId,
int& outVersion);
int& outVersion,
float* outTimeOfDay = nullptr);
static bool loadSceneDeferred(const fs::path& filePath,
std::vector<SceneObject>& objects,
int& nextId,
int& outVersion,
float* outTimeOfDay = nullptr);
};

View File

@@ -13,12 +13,12 @@ OBJLoader g_objLoader;
// Cube vertex data
float vertices[] = {
// Back face (z = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
// Front face (z = 0.5f)
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
@@ -37,12 +37,12 @@ float vertices[] = {
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Right face (x = 0.5f)
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
// Bottom face (y = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
@@ -54,11 +54,11 @@ float vertices[] = {
// Top face (y = 0.5f)
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
};
float mirrorPlaneVertices[] = {
@@ -287,6 +287,7 @@ std::vector<float> generateTorus(int segments, int sides) {
// Mesh implementation
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float));
strideFloats = 8;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
@@ -308,9 +309,52 @@ Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
glBindVertexArray(0);
}
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage,
const void* boneData, size_t boneDataBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float));
strideFloats = 8;
dynamic = dynamicUsage;
hasBones = boneData && boneDataBytes > 0;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, dynamicUsage ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
if (hasBones) {
glGenBuffers(1, &boneVBO);
glBindBuffer(GL_ARRAY_BUFFER, boneVBO);
glBufferData(GL_ARRAY_BUFFER, boneDataBytes, boneData, GL_STATIC_DRAW);
glVertexAttribIPointer(3, 4, GL_INT, sizeof(int) * 4 + sizeof(float) * 4, (void*)0);
glEnableVertexAttribArray(3);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(int) * 4 + sizeof(float) * 4,
(void*)(sizeof(int) * 4));
glEnableVertexAttribArray(4);
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
Mesh::~Mesh() {
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
if (boneVBO) {
glDeleteBuffers(1, &boneVBO);
}
}
void Mesh::draw() const {
@@ -319,6 +363,56 @@ void Mesh::draw() const {
glBindVertexArray(0);
}
void Mesh::updateVertices(const float* vertexData, size_t dataSizeBytes) {
if (!dynamic) return;
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, dataSizeBytes, vertexData);
glBindBuffer(GL_ARRAY_BUFFER, 0);
vertexCount = dataSizeBytes / (strideFloats * sizeof(float));
}
static void applyCpuSkinning(OBJLoader::LoadedMesh& meshInfo, const std::vector<glm::mat4>& bones, int maxBones) {
if (!meshInfo.mesh || !meshInfo.isSkinned) return;
if (meshInfo.baseVertices.empty() || meshInfo.boneIds.empty() || meshInfo.boneWeights.empty()) return;
if (!meshInfo.mesh->isDynamic()) return;
size_t vertexCount = meshInfo.baseVertices.size() / 8;
if (vertexCount == 0 || meshInfo.boneIds.size() != vertexCount || meshInfo.boneWeights.size() != vertexCount) {
return;
}
std::vector<float> skinned = meshInfo.baseVertices;
int boneLimit = std::min<int>(static_cast<int>(bones.size()), maxBones);
for (size_t i = 0; i < vertexCount; ++i) {
glm::vec3 basePos(skinned[i * 8 + 0], skinned[i * 8 + 1], skinned[i * 8 + 2]);
glm::vec3 baseNorm(skinned[i * 8 + 3], skinned[i * 8 + 4], skinned[i * 8 + 5]);
glm::ivec4 ids = meshInfo.boneIds[i];
glm::vec4 weights = meshInfo.boneWeights[i];
glm::vec4 skinnedPos(0.0f);
glm::vec3 skinnedNorm(0.0f);
for (int k = 0; k < 4; ++k) {
int id = ids[k];
float w = weights[k];
if (w <= 0.0f || id < 0 || id >= boneLimit) continue;
const glm::mat4& m = bones[id];
skinnedPos += w * (m * glm::vec4(basePos, 1.0f));
skinnedNorm += w * glm::mat3(m) * baseNorm;
}
skinned[i * 8 + 0] = skinnedPos.x;
skinned[i * 8 + 1] = skinnedPos.y;
skinned[i * 8 + 2] = skinnedPos.z;
if (glm::length(skinnedNorm) > 1e-6f) {
skinnedNorm = glm::normalize(skinnedNorm);
}
skinned[i * 8 + 3] = skinnedNorm.x;
skinned[i * 8 + 4] = skinnedNorm.y;
skinned[i * 8 + 5] = skinnedNorm.z;
}
meshInfo.mesh->updateVertices(skinned.data(), skinned.size() * sizeof(float));
}
// OBJLoader implementation
int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
// Check if already loaded
@@ -821,7 +915,7 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
};
for (const auto& obj : sceneObjects) {
if (!obj.enabled || obj.type != ObjectType::Mirror) continue;
if (!obj.enabled || !obj.hasRenderer || obj.renderType != RenderType::Mirror) continue;
active.insert(obj.id);
RenderTarget& target = mirrorTargets[obj.id];
@@ -1017,7 +1111,7 @@ void Renderer::renderObject(const SceneObject& obj) {
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
shader->setBool("unlit", obj.type == ObjectType::Mirror || obj.type == ObjectType::Sprite);
shader->setBool("unlit", obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite);
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
@@ -1026,7 +1120,7 @@ void Renderer::renderObject(const SceneObject& obj) {
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) {
if (obj.renderType == RenderType::Mirror) {
auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1);
@@ -1054,29 +1148,29 @@ void Renderer::renderObject(const SceneObject& obj) {
}
shader->setBool("hasNormalMap", normalUsed);
switch (obj.type) {
case ObjectType::Cube:
switch (obj.renderType) {
case RenderType::Cube:
cubeMesh->draw();
break;
case ObjectType::Sphere:
case RenderType::Sphere:
sphereMesh->draw();
break;
case ObjectType::Capsule:
case RenderType::Capsule:
capsuleMesh->draw();
break;
case ObjectType::Plane:
case RenderType::Plane:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Mirror:
case RenderType::Mirror:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Sprite:
case RenderType::Sprite:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Torus:
case RenderType::Torus:
if (torusMesh) torusMesh->draw();
break;
case ObjectType::OBJMesh:
case RenderType::OBJMesh:
if (obj.meshId >= 0) {
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
if (objMesh) {
@@ -1084,7 +1178,7 @@ void Renderer::renderObject(const SceneObject& obj) {
}
}
break;
case ObjectType::Model:
case RenderType::Model:
if (obj.meshId >= 0) {
Mesh* modelMesh = getModelLoader().getMesh(obj.meshId);
if (modelMesh) {
@@ -1092,26 +1186,8 @@ void Renderer::renderObject(const SceneObject& obj) {
}
}
break;
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight:
// Lights are not rendered as geometry
break;
case ObjectType::DirectionalLight:
// Not rendered as geometry
break;
case ObjectType::Camera:
// Cameras are editor helpers only
break;
case ObjectType::PostFXNode:
break;
case ObjectType::Sprite2D:
case ObjectType::Canvas:
case ObjectType::UIImage:
case ObjectType::UISlider:
case ObjectType::UIButton:
case ObjectType::UIText:
// UI types are rendered via ImGui, not here.
case RenderType::None:
default:
break;
}
}
@@ -1157,8 +1233,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
candidates.reserve(sceneObjects.size());
for (const auto& obj : sceneObjects) {
if (!obj.enabled || !obj.light.enabled) continue;
if (obj.type == ObjectType::DirectionalLight) {
if (!obj.enabled || !obj.hasLight || !obj.light.enabled) continue;
if (obj.light.type == LightType::Directional) {
LightUniform l;
l.type = 0;
l.dir = forwardFromRotation(obj);
@@ -1166,7 +1242,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
l.intensity = obj.light.intensity;
lights.push_back(l);
if (lights.size() >= kMaxLights) break;
} else if (obj.type == ObjectType::SpotLight) {
} else if (obj.light.type == LightType::Spot) {
LightUniform l;
l.type = 2;
l.pos = obj.position;
@@ -1182,7 +1258,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
c.distSq = glm::dot(delta, delta);
c.id = obj.id;
candidates.push_back(c);
} else if (obj.type == ObjectType::PointLight) {
} else if (obj.light.type == LightType::Point) {
LightUniform l;
l.type = 1;
l.pos = obj.position;
@@ -1195,7 +1271,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
c.distSq = glm::dot(delta, delta);
c.id = obj.id;
candidates.push_back(c);
} else if (obj.type == ObjectType::AreaLight) {
} else if (obj.light.type == LightType::Area) {
LightUniform l;
l.type = 3; // area
l.pos = obj.position;
@@ -1227,23 +1303,48 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
}
}
glm::mat4 view = camera.getViewMatrix();
glm::mat4 proj = glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane);
GLboolean cullFace = glIsEnabled(GL_CULL_FACE);
GLint prevCullMode = GL_BACK;
glGetIntegerv(GL_CULL_FACE_MODE, &prevCullMode);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue;
// Skip light gizmo-only types and camera helpers
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode || obj.type == ObjectType::Canvas || obj.type == ObjectType::UIImage || obj.type == ObjectType::UISlider || obj.type == ObjectType::UIButton || obj.type == ObjectType::UIText || obj.type == ObjectType::Sprite2D) {
continue;
}
if (!drawMirrorObjects && obj.hasRenderer && obj.renderType == RenderType::Mirror) continue;
if (!HasRendererComponent(obj)) continue;
Shader* active = getShader(obj.vertexShaderPath, obj.fragmentShaderPath);
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, obj.scale);
std::string vertPath = obj.vertexShaderPath;
std::string fragPath = obj.fragmentShaderPath;
int boneLimit = obj.skeletal.maxBones;
int availableBones = static_cast<int>(obj.skeletal.finalMatrices.size());
bool needsFallback = obj.hasSkeletalAnimation && obj.skeletal.enabled &&
obj.skeletal.allowCpuFallback &&
boneLimit > 0 && availableBones > boneLimit;
bool wantsGpuSkinning = obj.hasSkeletalAnimation && obj.skeletal.enabled &&
obj.skeletal.useGpuSkinning && !needsFallback;
if (vertPath.empty() && wantsGpuSkinning) {
vertPath = skinnedVertPath;
}
Shader* active = getShader(vertPath, fragPath);
if (!active) continue;
shader = active;
shader->use();
shader->setMat4("view", camera.getViewMatrix());
shader->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane));
shader->setMat4("view", view);
shader->setMat4("projection", proj);
shader->setVec3("viewPos", camera.position);
shader->setBool("unlit", obj.type == ObjectType::Mirror);
shader->setBool("unlit", obj.renderType == RenderType::Mirror);
shader->setVec3("ambientColor", ambientColor);
shader->setVec3("ambientColor", ambientColor);
@@ -1263,13 +1364,6 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
}
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, obj.scale);
shader->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength);
@@ -1277,6 +1371,20 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
if (obj.hasSkeletalAnimation && obj.skeletal.enabled) {
int safeLimit = std::max(0, boneLimit);
int boneCount = std::min<int>(availableBones, safeLimit);
if (wantsGpuSkinning && boneCount > 0) {
shader->setInt("boneCount", boneCount);
shader->setMat4Array("bones", obj.skeletal.finalMatrices.data(), boneCount);
shader->setBool("useSkinning", true);
} else {
shader->setBool("useSkinning", false);
}
} else {
shader->setBool("useSkinning", false);
}
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
@@ -1284,7 +1392,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) {
if (obj.renderType == RenderType::Mirror) {
auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1);
@@ -1313,31 +1421,51 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setBool("hasNormalMap", normalUsed);
Mesh* meshToDraw = nullptr;
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::Sprite) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
if (obj.renderType == RenderType::Cube) meshToDraw = cubeMesh;
else if (obj.renderType == RenderType::Sphere) meshToDraw = sphereMesh;
else if (obj.renderType == RenderType::Capsule) meshToDraw = capsuleMesh;
else if (obj.renderType == RenderType::Plane) meshToDraw = planeMesh;
else if (obj.renderType == RenderType::Mirror) meshToDraw = planeMesh;
else if (obj.renderType == RenderType::Sprite) meshToDraw = planeMesh;
else if (obj.renderType == RenderType::Torus) meshToDraw = torusMesh;
else if (obj.renderType == RenderType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId != -1) {
} else if (obj.renderType == RenderType::Model && obj.meshId != -1) {
meshToDraw = getModelLoader().getMesh(obj.meshId);
}
if (obj.renderType == RenderType::Model && obj.meshId != -1 &&
obj.hasSkeletalAnimation && obj.skeletal.enabled && !wantsGpuSkinning) {
const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId);
if (meshInfo) {
applyCpuSkinning(*const_cast<OBJLoader::LoadedMesh*>(meshInfo),
obj.skeletal.finalMatrices,
obj.skeletal.maxBones);
}
}
bool doubleSided = (obj.renderType == RenderType::Sprite || obj.renderType == RenderType::Mirror);
if (doubleSided) {
glDisable(GL_CULL_FACE);
} else {
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
if (meshToDraw) {
recordMeshDraw();
meshToDraw->draw();
}
}
if (skybox) {
glm::mat4 view = camera.getViewMatrix();
glm::mat4 proj = glm::perspective(glm::radians(fovDeg),
(float)width / height,
nearPlane, farPlane);
if (!cullFace) {
glDisable(GL_CULL_FACE);
} else {
glEnable(GL_CULL_FACE);
glCullFace(prevCullMode);
}
if (skybox) {
recordDrawCall();
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
}
@@ -1351,7 +1479,7 @@ PostFXSettings Renderer::gatherPostFX(const std::vector<SceneObject>& sceneObjec
PostFXSettings combined;
combined.enabled = false;
for (const auto& obj : sceneObjects) {
if (obj.type != ObjectType::PostFXNode) continue;
if (!obj.hasPostFX) continue;
if (!obj.postFx.enabled) continue;
combined = obj.postFx; // Last enabled node wins for now
combined.enabled = true;
@@ -1601,9 +1729,9 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
break;
case ColliderType::Mesh:
case ColliderType::ConvexMesh:
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
meshToDraw = getModelLoader().getMesh(obj.meshId);
} else {
meshToDraw = nullptr;
@@ -1612,29 +1740,29 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
break;
}
} else {
switch (obj.type) {
case ObjectType::Cube:
switch (obj.renderType) {
case RenderType::Cube:
meshToDraw = cubeMesh;
break;
case ObjectType::Sphere:
case RenderType::Sphere:
meshToDraw = sphereMesh;
break;
case ObjectType::Capsule:
case RenderType::Capsule:
meshToDraw = capsuleMesh;
break;
case ObjectType::Plane:
case RenderType::Plane:
meshToDraw = planeMesh;
break;
case ObjectType::Sprite:
case RenderType::Sprite:
meshToDraw = planeMesh;
break;
case ObjectType::Torus:
case RenderType::Torus:
meshToDraw = sphereMesh;
break;
case ObjectType::OBJMesh:
case RenderType::OBJMesh:
if (obj.meshId >= 0) meshToDraw = g_objLoader.getMesh(obj.meshId);
break;
case ObjectType::Model:
case RenderType::Model:
if (obj.meshId >= 0) meshToDraw = getModelLoader().getMesh(obj.meshId);
break;
default:
@@ -1673,31 +1801,30 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
}
if (!selectedObj || !selectedObj->enabled) return;
if (selectedObj->type == ObjectType::PointLight ||
selectedObj->type == ObjectType::SpotLight ||
selectedObj->type == ObjectType::AreaLight ||
selectedObj->type == ObjectType::Camera ||
selectedObj->type == ObjectType::PostFXNode ||
selectedObj->type == ObjectType::Canvas ||
selectedObj->type == ObjectType::UIImage ||
selectedObj->type == ObjectType::UISlider ||
selectedObj->type == ObjectType::UIButton ||
selectedObj->type == ObjectType::UIText ||
selectedObj->type == ObjectType::Sprite2D) {
if (!HasRendererComponent(*selectedObj)) {
return;
}
bool wantsGpuSkinning = selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled &&
selectedObj->skeletal.useGpuSkinning;
int boneLimit = selectedObj->skeletal.maxBones;
int availableBones = static_cast<int>(selectedObj->skeletal.finalMatrices.size());
if (selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled &&
selectedObj->skeletal.allowCpuFallback && boneLimit > 0 && availableBones > boneLimit) {
wantsGpuSkinning = false;
}
Mesh* meshToDraw = nullptr;
if (selectedObj->type == ObjectType::Cube) meshToDraw = cubeMesh;
else if (selectedObj->type == ObjectType::Sphere) meshToDraw = sphereMesh;
else if (selectedObj->type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (selectedObj->type == ObjectType::Plane) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Mirror) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Sprite) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Torus) meshToDraw = torusMesh;
else if (selectedObj->type == ObjectType::OBJMesh && selectedObj->meshId != -1) {
if (selectedObj->renderType == RenderType::Cube) meshToDraw = cubeMesh;
else if (selectedObj->renderType == RenderType::Sphere) meshToDraw = sphereMesh;
else if (selectedObj->renderType == RenderType::Capsule) meshToDraw = capsuleMesh;
else if (selectedObj->renderType == RenderType::Plane) meshToDraw = planeMesh;
else if (selectedObj->renderType == RenderType::Mirror) meshToDraw = planeMesh;
else if (selectedObj->renderType == RenderType::Sprite) meshToDraw = planeMesh;
else if (selectedObj->renderType == RenderType::Torus) meshToDraw = torusMesh;
else if (selectedObj->renderType == RenderType::OBJMesh && selectedObj->meshId != -1) {
meshToDraw = g_objLoader.getMesh(selectedObj->meshId);
} else if (selectedObj->type == ObjectType::Model && selectedObj->meshId != -1) {
} else if (selectedObj->renderType == RenderType::Model && selectedObj->meshId != -1) {
meshToDraw = getModelLoader().getMesh(selectedObj->meshId);
}
if (!meshToDraw) return;
@@ -1764,6 +1891,16 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
baseModel = glm::scale(baseModel, selectedObj->scale);
if (selectedObj->renderType == RenderType::Model && selectedObj->meshId != -1 &&
selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled && !wantsGpuSkinning) {
const auto* meshInfo = getModelLoader().getMeshInfo(selectedObj->meshId);
if (meshInfo) {
applyCpuSkinning(*const_cast<OBJLoader::LoadedMesh*>(meshInfo),
selectedObj->skeletal.finalMatrices,
selectedObj->skeletal.maxBones);
}
}
// Mark the object in the stencil buffer.
glEnable(GL_STENCIL_TEST);
glStencilMask(0xFF);

View File

@@ -19,14 +19,23 @@ std::vector<float> generateTorus(int segments = 32, int sides = 16);
class Mesh {
private:
unsigned int VAO, VBO;
unsigned int boneVBO = 0;
int vertexCount;
int strideFloats = 8;
bool dynamic = false;
bool hasBones = false;
public:
Mesh(const float* vertexData, size_t dataSizeBytes);
Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage,
const void* boneData, size_t boneDataBytes);
~Mesh();
void draw() const;
void updateVertices(const float* vertexData, size_t dataSizeBytes);
int getVertexCount() const { return vertexCount; }
bool isDynamic() const { return dynamic; }
bool usesBones() const { return hasBones; }
};
class OBJLoader {
@@ -44,6 +53,12 @@ public:
std::vector<glm::vec3> triangleVertices; // positions duplicated per-triangle for picking
std::vector<glm::vec3> positions; // unique vertex positions for physics
std::vector<uint32_t> triangleIndices; // triangle indices into positions
bool isSkinned = false;
std::vector<std::string> boneNames;
std::vector<glm::mat4> inverseBindMatrices;
std::vector<glm::ivec4> boneIds;
std::vector<glm::vec4> boneWeights;
std::vector<float> baseVertices;
};
private:
@@ -104,6 +119,7 @@ private:
};
std::unordered_map<std::string, ShaderEntry> shaderCache;
std::string defaultVertPath = "Resources/Shaders/vert.glsl";
std::string skinnedVertPath = "Resources/Shaders/skinned_vert.glsl";
std::string defaultFragPath = "Resources/Shaders/frag.glsl";
std::string postVertPath = "Resources/Shaders/postfx_vert.glsl";
std::string postFragPath = "Resources/Shaders/postfx_frag.glsl";

View File

@@ -23,7 +23,31 @@ enum class ObjectType {
UIImage = 17,
UISlider = 18,
UIButton = 19,
UIText = 20
UIText = 20,
Empty = 21
};
enum class RenderType {
None = 0,
Cube = 1,
Sphere = 2,
Capsule = 3,
OBJMesh = 4,
Model = 5,
Mirror = 6,
Plane = 7,
Torus = 8,
Sprite = 9
};
enum class UIElementType {
None = 0,
Canvas = 1,
Image = 2,
Slider = 3,
Button = 4,
Text = 5,
Sprite2D = 6
};
struct MaterialProperties {
@@ -60,6 +84,76 @@ enum class UIButtonStyle {
Outline = 1
};
enum class ReverbPreset {
Room = 0,
LivingRoom = 1,
Hall = 2,
Forest = 3,
Custom = 4
};
enum class ReverbZoneShape {
Box = 0,
Sphere = 1
};
enum class AudioRolloffMode {
Logarithmic = 0,
Linear = 1,
Exponential = 2,
Custom = 3
};
enum class AnimationInterpolation {
Linear = 0,
SmoothStep = 1,
EaseIn = 2,
EaseOut = 3,
EaseInOut = 4
};
enum class AnimationCurveMode {
Preset = 0,
Bezier = 1
};
struct AnimationKeyframe {
float time = 0.0f;
glm::vec3 position = glm::vec3(0.0f);
glm::vec3 rotation = glm::vec3(0.0f);
glm::vec3 scale = glm::vec3(1.0f);
AnimationInterpolation interpolation = AnimationInterpolation::SmoothStep;
AnimationCurveMode curveMode = AnimationCurveMode::Preset;
glm::vec2 bezierIn = glm::vec2(0.25f, 0.0f);
glm::vec2 bezierOut = glm::vec2(0.75f, 1.0f);
};
struct AnimationComponent {
bool enabled = true;
float clipLength = 2.0f;
float playSpeed = 1.0f;
bool loop = true;
bool applyOnScrub = true;
std::vector<AnimationKeyframe> keyframes;
};
struct SkeletalAnimationComponent {
bool enabled = true;
bool useGpuSkinning = true;
bool allowCpuFallback = true;
bool useAnimation = true;
int clipIndex = 0;
float time = 0.0f;
float playSpeed = 1.0f;
bool loop = true;
int skeletonRootId = -1;
int maxBones = 128;
std::vector<std::string> boneNames;
std::vector<int> boneNodeIds;
std::vector<glm::mat4> inverseBindMatrices;
std::vector<glm::mat4> finalMatrices;
};
struct LightComponent {
LightType type = LightType::Point;
glm::vec3 color = glm::vec3(1.0f);
@@ -85,6 +179,8 @@ struct CameraComponent {
float nearClip = NEAR_PLANE;
float farClip = FAR_PLANE;
bool applyPostFX = true;
bool use2D = false;
float pixelsPerUnit = 100.0f;
};
struct PostFXSettings {
@@ -122,9 +218,16 @@ struct ScriptSetting {
std::string value;
};
enum class ScriptLanguage {
Cpp = 0,
CSharp = 1
};
struct ScriptComponent {
bool enabled = true;
ScriptLanguage language = ScriptLanguage::Cpp;
std::string path;
std::string managedType;
std::vector<ScriptSetting> settings;
std::string lastBinaryPath;
std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start
@@ -169,9 +272,11 @@ struct PlayerControllerComponent {
};
struct UIElementComponent {
UIElementType type = UIElementType::None;
UIAnchor anchor = UIAnchor::Center;
glm::vec2 position = glm::vec2(0.0f); // offset in pixels from anchor
glm::vec2 size = glm::vec2(160.0f, 40.0f);
float rotation = 0.0f;
float sliderValue = 0.5f;
float sliderMin = 0.0f;
float sliderMax = 1.0f;
@@ -193,6 +298,37 @@ struct Rigidbody2DComponent {
glm::vec2 velocity = glm::vec2(0.0f);
};
enum class Collider2DType {
Box = 0,
Polygon = 1,
Edge = 2
};
struct Collider2DComponent {
bool enabled = true;
Collider2DType type = Collider2DType::Box;
glm::vec2 boxSize = glm::vec2(1.0f);
std::vector<glm::vec2> points;
bool closed = false;
float edgeThickness = 0.05f;
};
struct ParallaxLayer2DComponent {
bool enabled = true;
int order = 0;
float factor = 1.0f; // 1 = world locked, 0 = camera locked
bool repeatX = false;
bool repeatY = false;
glm::vec2 repeatSpacing = glm::vec2(0.0f);
};
struct CameraFollow2DComponent {
bool enabled = true;
int targetId = -1;
glm::vec2 offset = glm::vec2(0.0f);
float smoothTime = 0.0f; // seconds; 0 snaps to target
};
struct AudioSourceComponent {
bool enabled = true;
std::string clipPath;
@@ -202,6 +338,36 @@ struct AudioSourceComponent {
bool spatial = true;
float minDistance = 1.0f;
float maxDistance = 25.0f;
AudioRolloffMode rolloffMode = AudioRolloffMode::Logarithmic;
float rolloff = 1.0f;
float customMidDistance = 0.5f;
float customMidGain = 0.6f;
float customEndGain = 0.0f;
};
struct ReverbZoneComponent {
bool enabled = true;
ReverbPreset preset = ReverbPreset::Room;
ReverbZoneShape shape = ReverbZoneShape::Box;
glm::vec3 boxSize = glm::vec3(6.0f);
float radius = 6.0f;
float blendDistance = 1.0f;
float minDistance = 1.0f;
float maxDistance = 15.0f;
float room = -1000.0f; // dB
float roomHF = -100.0f; // dB
float roomLF = 0.0f; // dB
float decayTime = 1.49f; // s
float decayHFRatio = 0.83f; // 0.1..2
float reflections = -2602.0f; // dB
float reflectionsDelay = 0.007f; // s
float reverb = 200.0f; // dB
float reverbDelay = 0.011f; // s
float hfReference = 5000.0f; // Hz
float lfReference = 250.0f; // Hz
float roomRolloffFactor = 0.0f;
float diffusion = 100.0f; // 0..100
float density = 100.0f; // 0..100
};
class SceneObject {
@@ -211,6 +377,12 @@ public:
bool enabled = true;
int layer = 0;
std::string tag = "Untagged";
bool hasRenderer = false;
RenderType renderType = RenderType::None;
bool hasLight = false;
bool hasCamera = false;
bool hasPostFX = false;
bool hasUI = false;
glm::vec3 position;
glm::vec3 rotation;
glm::vec3 scale;
@@ -224,6 +396,7 @@ public:
bool isExpanded = true;
std::string meshPath; // Path to imported model file
int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader)
int meshSourceIndex = -1; // Source mesh index for multi-mesh models
MaterialProperties material;
std::string materialPath; // Optional external material asset
std::string albedoTexturePath;
@@ -241,12 +414,24 @@ public:
RigidbodyComponent rigidbody;
bool hasRigidbody2D = false;
Rigidbody2DComponent rigidbody2D;
bool hasCollider2D = false;
Collider2DComponent collider2D;
bool hasParallaxLayer2D = false;
ParallaxLayer2DComponent parallaxLayer2D;
bool hasCameraFollow2D = false;
CameraFollow2DComponent cameraFollow2D;
bool hasCollider = false;
ColliderComponent collider;
bool hasPlayerController = false;
PlayerControllerComponent playerController;
bool hasAudioSource = false;
AudioSourceComponent audioSource;
bool hasReverbZone = false;
ReverbZoneComponent reverbZone;
bool hasAnimation = false;
AnimationComponent animation;
bool hasSkeletalAnimation = false;
SkeletalAnimationComponent skeletal;
UIElementComponent ui;
SceneObject(const std::string& name, ObjectType type, int id)
@@ -261,3 +446,11 @@ public:
localInitialized(true),
id(id) {}
};
inline bool HasRendererComponent(const SceneObject& obj) {
return obj.hasRenderer && obj.renderType != RenderType::None;
}
inline bool HasUIComponent(const SceneObject& obj) {
return obj.hasUI && obj.ui.type != UIElementType::None;
}

View File

@@ -173,6 +173,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
relToScripts.clear();
}
auto hasDotDot = [](const fs::path& path) {
for (const auto& part : path) {
if (part == "..") return true;
}
return false;
};
if (relToScripts.empty() || relToScripts.is_absolute() || hasDotDot(relToScripts)) {
relToScripts.clear();
}
fs::path relativeParent = relToScripts.has_parent_path() ? relToScripts.parent_path() : fs::path();
std::string baseName = scriptAbs.stem().string();
fs::path objectPath = config.outDir / relativeParent / (baseName + ".o");
@@ -246,10 +256,24 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
FunctionSpec testEditorSpec = detectFunction(scriptSource, "TestEditor");
FunctionSpec updateSpec = detectFunction(scriptSource, "Update");
FunctionSpec tickUpdateSpec = detectFunction(scriptSource, "TickUpdate");
FunctionSpec inspectorSpec = detectFunction(scriptSource, "Script_OnInspector");
auto hasExternCInspector = [&]() {
try {
std::regex direct(R"(extern\s+"C"\s+void\s+Script_OnInspector\s*\()");
if (std::regex_search(scriptSource, direct)) return true;
std::regex block(R"(extern\s+"C"\s*\{[\s\S]*?\bScript_OnInspector\b)");
return std::regex_search(scriptSource, block);
} catch (...) {
return false;
}
};
bool inspectorExtern = hasExternCInspector();
bool needsInspectorWrap = inspectorSpec.present && !inspectorExtern;
fs::path wrapperPath;
bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present
|| updateSpec.present || tickUpdateSpec.present;
|| updateSpec.present || tickUpdateSpec.present || needsInspectorWrap;
fs::path sourceToCompile = scriptAbs;
if (useWrapper) {
@@ -264,8 +288,15 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
}
std::string includePath = scriptAbs.lexically_normal().generic_string();
if (needsInspectorWrap) {
wrapper << "#define Script_OnInspector Script_OnInspector_Impl\n";
}
wrapper << "#include \"ScriptRuntime.h\"\n";
wrapper << "#include \"" << includePath << "\"\n\n";
wrapper << "#include \"" << includePath << "\"\n";
if (needsInspectorWrap) {
wrapper << "#undef Script_OnInspector\n";
}
wrapper << "\n";
wrapper << "extern \"C\" {\n";
auto emitWrapper = [&](const char* exportedName, const char* implName,
@@ -293,6 +324,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
emitWrapper("Script_TestEditor", "TestEditor", testEditorSpec);
emitWrapper("Script_Update", "Update", updateSpec);
emitWrapper("Script_TickUpdate", "TickUpdate", tickUpdateSpec);
if (needsInspectorWrap) {
wrapper << "void Script_OnInspector(ScriptContext& ctx) {\n";
if (inspectorSpec.takesContext) {
wrapper << " Script_OnInspector_Impl(ctx);\n";
} else {
wrapper << " (void)ctx;\n";
wrapper << " Script_OnInspector_Impl();\n";
}
wrapper << "}\n\n";
}
wrapper << "}\n";
sourceToCompile = wrapperPath;

View File

@@ -43,13 +43,8 @@ std::string trimString(const std::string& input) {
return input.substr(start, end - start);
}
bool isUIObjectType(ObjectType type) {
return type == ObjectType::Canvas ||
type == ObjectType::UIImage ||
type == ObjectType::UISlider ||
type == ObjectType::UIButton ||
type == ObjectType::UIText ||
type == ObjectType::Sprite2D;
bool isUIObject(const SceneObject* obj) {
return obj && HasUIComponent(*obj);
}
}
@@ -345,7 +340,7 @@ void ScriptContext::TickStandaloneMovement(StandaloneMovementState& state, Stand
}
bool ScriptContext::IsUIButtonPressed() const {
return object && object->type == ObjectType::UIButton && object->ui.buttonPressed;
return object && object->hasUI && object->ui.type == UIElementType::Button && object->ui.buttonPressed;
}
bool ScriptContext::IsUIInteractable() const {
@@ -361,12 +356,12 @@ void ScriptContext::SetUIInteractable(bool interactable) {
}
float ScriptContext::GetUISliderValue() const {
if (!object || object->type != ObjectType::UISlider) return 0.0f;
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return 0.0f;
return object->ui.sliderValue;
}
void ScriptContext::SetUISliderValue(float value) {
if (!object || object->type != ObjectType::UISlider) return;
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return;
float clamped = std::clamp(value, object->ui.sliderMin, object->ui.sliderMax);
if (object->ui.sliderValue != clamped) {
object->ui.sliderValue = clamped;
@@ -375,7 +370,7 @@ void ScriptContext::SetUISliderValue(float value) {
}
void ScriptContext::SetUISliderRange(float minValue, float maxValue) {
if (!object || object->type != ObjectType::UISlider) return;
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return;
if (maxValue < minValue) std::swap(minValue, maxValue);
object->ui.sliderMin = minValue;
object->ui.sliderMax = maxValue;
@@ -400,12 +395,12 @@ void ScriptContext::SetUIColor(const glm::vec4& color) {
}
float ScriptContext::GetUITextScale() const {
if (!object || object->type != ObjectType::UIText) return 1.0f;
if (!object || !object->hasUI || object->ui.type != UIElementType::Text) return 1.0f;
return object->ui.textScale;
}
void ScriptContext::SetUITextScale(float scale) {
if (!object || object->type != ObjectType::UIText) return;
if (!object || !object->hasUI || object->ui.type != UIElementType::Text) return;
float clamped = std::max(0.1f, scale);
if (object->ui.textScale != clamped) {
object->ui.textScale = clamped;
@@ -414,7 +409,7 @@ void ScriptContext::SetUITextScale(float scale) {
}
void ScriptContext::SetUISliderStyle(UISliderStyle style) {
if (!object || object->type != ObjectType::UISlider) return;
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return;
if (object->ui.sliderStyle != style) {
object->ui.sliderStyle = style;
MarkDirty();
@@ -422,7 +417,7 @@ void ScriptContext::SetUISliderStyle(UISliderStyle style) {
}
void ScriptContext::SetUIButtonStyle(UIButtonStyle style) {
if (!object || object->type != ObjectType::UIButton) return;
if (!object || !object->hasUI || object->ui.type != UIElementType::Button) return;
if (object->ui.buttonStyle != style) {
object->ui.buttonStyle = style;
MarkDirty();
@@ -454,7 +449,7 @@ bool ScriptContext::HasRigidbody() const {
}
bool ScriptContext::HasRigidbody2D() const {
return object && isUIObjectType(object->type) && object->hasRigidbody2D && object->rigidbody2D.enabled;
return isUIObject(object) && object->hasRigidbody2D && object->rigidbody2D.enabled;
}
bool ScriptContext::EnsureCapsuleCollider(float height, float radius) {

View File

@@ -138,3 +138,9 @@ void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
}
void Shader::setMat4Array(const std::string &name, const glm::mat4 *data, int count) const
{
if (count <= 0 || !data) return;
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), count, GL_FALSE, glm::value_ptr(data[0]));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,401 @@
#pragma once
#include <string>
#include <vector>
#include <array>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <regex>
#include "imgui.h"
class TextEditor
{
public:
enum class PaletteIndex
{
Default,
Keyword,
Number,
String,
CharLiteral,
Punctuation,
Preprocessor,
Identifier,
KnownIdentifier,
PreprocIdentifier,
Comment,
MultiLineComment,
Background,
Cursor,
Selection,
ErrorMarker,
Breakpoint,
LineNumber,
CurrentLineFill,
CurrentLineFillInactive,
CurrentLineEdge,
Max
};
enum class SelectionMode
{
Normal,
Word,
Line
};
struct Breakpoint
{
int mLine;
bool mEnabled;
std::string mCondition;
Breakpoint()
: mLine(-1)
, mEnabled(false)
{}
};
// Represents a character coordinate from the user's point of view,
// i. e. consider an uniform grid (assuming fixed-width font) on the
// screen as it is rendered, and each cell has its own coordinate, starting from 0.
// Tabs are counted as [1..mTabSize] count empty spaces, depending on
// how many space is necessary to reach the next tab stop.
// For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4,
// because it is rendered as " ABC" on the screen.
struct Coordinates
{
int mLine, mColumn;
Coordinates() : mLine(0), mColumn(0) {}
Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn)
{
assert(aLine >= 0);
assert(aColumn >= 0);
}
static Coordinates Invalid() { static Coordinates invalid(-1, -1); return invalid; }
bool operator ==(const Coordinates& o) const
{
return
mLine == o.mLine &&
mColumn == o.mColumn;
}
bool operator !=(const Coordinates& o) const
{
return
mLine != o.mLine ||
mColumn != o.mColumn;
}
bool operator <(const Coordinates& o) const
{
if (mLine != o.mLine)
return mLine < o.mLine;
return mColumn < o.mColumn;
}
bool operator >(const Coordinates& o) const
{
if (mLine != o.mLine)
return mLine > o.mLine;
return mColumn > o.mColumn;
}
bool operator <=(const Coordinates& o) const
{
if (mLine != o.mLine)
return mLine < o.mLine;
return mColumn <= o.mColumn;
}
bool operator >=(const Coordinates& o) const
{
if (mLine != o.mLine)
return mLine > o.mLine;
return mColumn >= o.mColumn;
}
};
struct Identifier
{
Coordinates mLocation;
std::string mDeclaration;
};
typedef std::string String;
typedef std::unordered_map<std::string, Identifier> Identifiers;
typedef std::unordered_set<std::string> Keywords;
typedef std::map<int, std::string> ErrorMarkers;
typedef std::unordered_set<int> Breakpoints;
typedef std::array<ImU32, (unsigned)PaletteIndex::Max> Palette;
typedef uint8_t Char;
struct Glyph
{
Char mChar;
PaletteIndex mColorIndex = PaletteIndex::Default;
bool mComment : 1;
bool mMultiLineComment : 1;
bool mPreprocessor : 1;
Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex),
mComment(false), mMultiLineComment(false), mPreprocessor(false) {}
};
typedef std::vector<Glyph> Line;
typedef std::vector<Line> Lines;
struct LanguageDefinition
{
typedef std::pair<std::string, PaletteIndex> TokenRegexString;
typedef std::vector<TokenRegexString> TokenRegexStrings;
typedef bool(*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex);
std::string mName;
Keywords mKeywords;
Identifiers mIdentifiers;
Identifiers mPreprocIdentifiers;
std::string mCommentStart, mCommentEnd, mSingleLineComment;
char mPreprocChar;
bool mAutoIndentation;
TokenizeCallback mTokenize;
TokenRegexStrings mTokenRegexStrings;
bool mCaseSensitive;
LanguageDefinition()
: mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true)
{
}
static const LanguageDefinition& CPlusPlus();
static const LanguageDefinition& HLSL();
static const LanguageDefinition& GLSL();
static const LanguageDefinition& C();
static const LanguageDefinition& SQL();
static const LanguageDefinition& AngelScript();
static const LanguageDefinition& Lua();
};
TextEditor();
~TextEditor();
void SetLanguageDefinition(const LanguageDefinition& aLanguageDef);
const LanguageDefinition& GetLanguageDefinition() const { return mLanguageDefinition; }
const Palette& GetPalette() const { return mPaletteBase; }
void SetPalette(const Palette& aValue);
void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; }
void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; }
void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false);
void SetText(const std::string& aText);
std::string GetText() const;
void SetTextLines(const std::vector<std::string>& aLines);
std::vector<std::string> GetTextLines() const;
std::string GetSelectedText() const;
std::string GetCurrentLineText()const;
int GetTotalLines() const { return (int)mLines.size(); }
bool IsOverwrite() const { return mOverwrite; }
void SetReadOnly(bool aValue);
bool IsReadOnly() const { return mReadOnly; }
bool IsTextChanged() const { return mTextChanged; }
bool IsCursorPositionChanged() const { return mCursorPositionChanged; }
bool IsColorizerEnabled() const { return mColorizerEnabled; }
void SetColorizerEnable(bool aValue);
Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); }
void SetCursorPosition(const Coordinates& aPosition);
inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;}
inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; }
inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;}
inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; }
inline void SetAllowTabInput(bool aValue) { mAllowTabInput = aValue; }
inline bool IsTabInputAllowed() const { return mAllowTabInput; }
inline void SetSmartTabDelete(bool aValue) { mSmartTabDelete = aValue; }
inline bool IsSmartTabDeleteEnabled() const { return mSmartTabDelete; }
inline void SetImGuiChildIgnored (bool aValue){ mIgnoreImGuiChild = aValue;}
inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; }
inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; }
inline bool IsShowingWhitespaces() const { return mShowWhitespaces; }
void SetTabSize(int aValue);
inline int GetTabSize() const { return mTabSize; }
void InsertText(const std::string& aValue);
void InsertText(const char* aValue);
void MoveUp(int aAmount = 1, bool aSelect = false);
void MoveDown(int aAmount = 1, bool aSelect = false);
void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false);
void MoveTop(bool aSelect = false);
void MoveBottom(bool aSelect = false);
void MoveHome(bool aSelect = false);
void MoveEnd(bool aSelect = false);
void SetSelectionStart(const Coordinates& aPosition);
void SetSelectionEnd(const Coordinates& aPosition);
void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal);
std::string GetWordUnderCursorPublic() const;
std::string GetWordAtPublic(const Coordinates& aCoords) const;
ImVec2 GetCursorScreenPositionPublic() const { return mCursorScreenPos; }
bool HasCursorScreenPosition() const { return mCursorScreenPosValid; }
void SelectWordUnderCursor();
void SelectAll();
bool HasSelection() const;
void Copy();
void Cut();
void Paste();
void Delete();
bool CanUndo() const;
bool CanRedo() const;
void Undo(int aSteps = 1);
void Redo(int aSteps = 1);
static const Palette& GetDarkPalette();
static const Palette& GetLightPalette();
static const Palette& GetRetroBluePalette();
private:
typedef std::vector<std::pair<std::regex, PaletteIndex>> RegexList;
struct EditorState
{
Coordinates mSelectionStart;
Coordinates mSelectionEnd;
Coordinates mCursorPosition;
};
class UndoRecord
{
public:
UndoRecord() {}
~UndoRecord() {}
UndoRecord(
const std::string& aAdded,
const TextEditor::Coordinates aAddedStart,
const TextEditor::Coordinates aAddedEnd,
const std::string& aRemoved,
const TextEditor::Coordinates aRemovedStart,
const TextEditor::Coordinates aRemovedEnd,
TextEditor::EditorState& aBefore,
TextEditor::EditorState& aAfter);
void Undo(TextEditor* aEditor);
void Redo(TextEditor* aEditor);
std::string mAdded;
Coordinates mAddedStart;
Coordinates mAddedEnd;
std::string mRemoved;
Coordinates mRemovedStart;
Coordinates mRemovedEnd;
EditorState mBefore;
EditorState mAfter;
};
typedef std::vector<UndoRecord> UndoBuffer;
void ProcessInputs();
void Colorize(int aFromLine = 0, int aCount = -1);
void ColorizeRange(int aFromLine = 0, int aToLine = 0);
void ColorizeInternal();
float TextDistanceToLineStart(const Coordinates& aFrom) const;
void EnsureCursorVisible();
int GetPageSize() const;
std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const;
Coordinates GetActualCursorCoordinates() const;
Coordinates SanitizeCoordinates(const Coordinates& aValue) const;
void Advance(Coordinates& aCoordinates) const;
void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd);
int InsertTextAt(Coordinates& aWhere, const char* aValue);
void AddUndo(UndoRecord& aValue);
Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const;
Coordinates FindWordStart(const Coordinates& aFrom) const;
Coordinates FindWordEnd(const Coordinates& aFrom) const;
Coordinates FindNextWord(const Coordinates& aFrom) const;
int GetCharacterIndex(const Coordinates& aCoordinates) const;
int GetCharacterColumn(int aLine, int aIndex) const;
int GetLineCharacterCount(int aLine) const;
int GetLineMaxColumn(int aLine) const;
bool IsOnWordBoundary(const Coordinates& aAt) const;
void RemoveLine(int aStart, int aEnd);
void RemoveLine(int aIndex);
Line& InsertLine(int aIndex);
void EnterCharacter(ImWchar aChar, bool aShift);
void Backspace();
void DeleteSelection();
std::string GetWordUnderCursor() const;
std::string GetWordAt(const Coordinates& aCoords) const;
ImU32 GetGlyphColor(const Glyph& aGlyph) const;
void HandleKeyboardInputs();
void HandleMouseInputs();
void Render();
float mLineSpacing;
Lines mLines;
EditorState mState;
UndoBuffer mUndoBuffer;
int mUndoIndex;
int mTabSize;
bool mOverwrite;
bool mReadOnly;
bool mWithinRender;
bool mScrollToCursor;
bool mScrollToTop;
bool mTextChanged;
bool mColorizerEnabled;
float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor.
int mLeftMargin;
bool mCursorPositionChanged;
int mColorRangeMin, mColorRangeMax;
SelectionMode mSelectionMode;
bool mHandleKeyboardInputs;
bool mHandleMouseInputs;
bool mAllowTabInput;
bool mSmartTabDelete;
ImVec2 mCursorScreenPos;
bool mCursorScreenPosValid;
bool mIgnoreImGuiChild;
bool mShowWhitespaces;
Palette mPaletteBase;
Palette mPalette;
LanguageDefinition mLanguageDefinition;
RegexList mRegexList;
bool mCheckComments;
Breakpoints mBreakpoints;
ErrorMarkers mErrorMarkers;
ImVec2 mCharAdvance;
Coordinates mInteractiveStart, mInteractiveEnd;
std::string mLineBuffer;
uint64_t mStartTime;
float mLastClick;
};

View File

@@ -1288,6 +1288,7 @@ static const float DOCKING_TRANSPARENT_PAYLOAD_ALPHA = 0.50f; // For u
static void SetCurrentWindow(ImGuiWindow* window);
static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags);
static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window);
static float DockAnimEaseSmooth(float t);
static void AddWindowToSortBuffer(ImVector<ImGuiWindow*>* out_sorted_windows, ImGuiWindow* window);
@@ -4603,6 +4604,12 @@ ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NUL
FontWindowScale = FontWindowScaleParents = 1.0f;
SettingsOffset = -1;
DockOrder = -1;
DockAnimStartTime = 0.0f;
DockAnimDuration = 0.0f;
DockAnimActive = false;
DockAnimOvershoot = false;
DockAnimGrabRatio = ImVec2(0.5f, 0.5f);
DockAnimOvershootStrength = 1.0f;
DrawList = &DrawListInst;
DrawList->_OwnerName = Name;
DrawList->_SetDrawListSharedData(&Ctx->DrawListSharedData);
@@ -5252,6 +5259,12 @@ ImDrawListSharedData* ImGui::GetDrawListSharedData()
return &GImGui->DrawListSharedData;
}
namespace ImGui
{
ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node);
void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window);
}
void ImGui::StartMouseMovingWindow(ImGuiWindow* window)
{
// Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows.
@@ -5294,7 +5307,12 @@ void ImGui::StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* nod
const bool clicked = IsMouseClicked(0);
const bool dragging = IsMouseDragging(0);
if (can_undock_node && dragging)
DockContextQueueUndockNode(&g, node); // Will lead to DockNodeStartMouseMovingWindow() -> StartMouseMovingWindow() being called next frame
{
ImGuiDockNode* undocked = DockContextProcessUndockNode(&g, node);
if (undocked && undocked->Windows.Size > 0)
DockNodeStartMouseMovingWindow(undocked, undocked->Windows[0]);
return;
}
else if (!can_undock_node && (clicked || dragging) && g.MovingWindow != window)
StartMouseMovingWindow(window);
}
@@ -5348,7 +5366,30 @@ void ImGui::UpdateMouseMovingWindowNewFrame()
const bool window_disappeared = (!moving_window->WasActive && !moving_window->Active);
if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos) && !window_disappeared)
{
ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset;
ImVec2 pos_target = g.IO.MousePos - g.ActiveIdClickOffset;
ImVec2 pos = pos_target;
if (moving_window->DockAnimActive && moving_window->DockAnimOvershoot)
{
const float elapsed = (float)(g.Time - moving_window->DockAnimStartTime);
const float duration = ImMax(0.001f, moving_window->DockAnimDuration);
float t = ImSaturate(elapsed / duration);
const float ease_out = DockAnimEaseSmooth(t);
const float s = 1.70158f * moving_window->DockAnimOvershootStrength;
const float t1 = t - 1.0f;
float ease_t = 1.0f + (t1 * t1) * ((s + 1.0f) * t1 + s);
ImVec2 anim_size = ImLerp(moving_window->DockAnimFromSize, moving_window->DockAnimToSize, ease_t);
if (anim_size.x > 0.0f && anim_size.y > 0.0f)
{
moving_window->SizeFull = anim_size;
moving_window->Size = anim_size;
ImVec2 pos_target_anim = g.IO.MousePos - moving_window->DockAnimGrabRatio * anim_size;
pos = ImLerp(moving_window->DockAnimFromPos, pos_target_anim, ease_out);
}
else
{
pos = ImLerp(moving_window->DockAnimFromPos, pos_target, ease_out);
}
}
if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y)
{
SetWindowPos(moving_window, pos, ImGuiCond_Always);
@@ -8141,6 +8182,29 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)
}
window->Pos = ImTrunc(window->Pos);
if (window->DockAnimActive)
{
const float elapsed = (float)(g.Time - window->DockAnimStartTime);
const float duration = ImMax(0.001f, window->DockAnimDuration);
float t = ImSaturate(elapsed / duration);
float ease_t = DockAnimEaseSmooth(t);
if (window->DockAnimOvershoot)
{
const float s = 1.70158f * window->DockAnimOvershootStrength;
const float t1 = t - 1.0f;
ease_t = 1.0f + (t1 * t1) * ((s + 1.0f) * t1 + s);
}
const ImVec2 from_center = window->DockAnimFromPos + window->DockAnimFromSize * 0.5f;
const ImVec2 to_center = window->DockAnimToPos + window->DockAnimToSize * 0.5f;
const ImVec2 center = ImLerp(from_center, to_center, t);
const ImVec2 size = ImLerp(window->DockAnimFromSize, window->DockAnimToSize, ease_t);
window->SizeFull = size;
window->Pos = center - size * 0.5f;
window->Size = (window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow)) ? window->TitleBarRect().GetSize() : window->SizeFull;
if (t >= 1.0f)
window->DockAnimActive = false;
}
// Lock window rounding for the frame (so that altering them doesn't cause inconsistencies)
// Large values tend to lead to variety of artifacts and are not recommended.
if (window->ViewportOwned || window->DockIsActive)
@@ -17540,6 +17604,7 @@ namespace ImGui
static void DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node);
static void DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node);
static void DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req);
ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node);
static void DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx);
static ImGuiDockNode* DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window);
static void DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count);
@@ -17562,7 +17627,7 @@ namespace ImGui
static void DockNodeRemoveTabBar(ImGuiDockNode* node);
static void DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar);
static void DockNodeUpdateVisibleFlag(ImGuiDockNode* node);
static void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window);
void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window);
static bool DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* payload_window);
static void DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* preview_data, bool is_explicit_target, bool is_outer_docking);
static void DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, const ImGuiDockPreviewData* preview_data);
@@ -18003,6 +18068,27 @@ void ImGui::DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode*
req.Type = ImGuiDockRequestType_None;
}
static float DockAnimEaseSmooth(float t)
{
t = ImSaturate(t);
return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
}
static void DockWindowQueueScaleAnim(ImGuiContext* ctx, ImGuiWindow* window, const ImVec2& from_pos, const ImVec2& from_size, const ImVec2& to_pos, const ImVec2& to_size, bool overshoot, float duration, float overshoot_strength)
{
if (to_size.x <= 0.0f || to_size.y <= 0.0f)
return;
window->DockAnimFromPos = from_pos;
window->DockAnimFromSize = from_size;
window->DockAnimToPos = to_pos;
window->DockAnimToSize = to_size;
window->DockAnimStartTime = (float)ctx->Time;
window->DockAnimDuration = duration;
window->DockAnimActive = true;
window->DockAnimOvershoot = overshoot;
window->DockAnimOvershootStrength = overshoot_strength;
}
void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req)
{
IM_ASSERT((req->Type == ImGuiDockRequestType_Dock && req->DockPayload != NULL) || (req->Type == ImGuiDockRequestType_Split && req->DockPayload == NULL));
@@ -18014,6 +18100,15 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req)
ImGuiWindow* payload_window = req->DockPayload; // Optional
ImGuiWindow* target_window = req->DockTargetWindow;
ImGuiDockNode* node = req->DockTargetNode;
ImVector<ImGuiWindow*> payload_windows;
if (payload_window)
{
payload_windows.push_back(payload_window);
}
else if (req->DockTargetNode == NULL && req->DockTargetWindow != NULL && req->DockSplitDir != ImGuiDir_None)
{
// no-op, payload_node path handled below
}
if (payload_window)
IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X target '%s' dock window '%s', split_dir %d\n", node ? node->ID : 0, target_window ? target_window->Name : "NULL", payload_window->Name, req->DockSplitDir);
else
@@ -18030,6 +18125,11 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req)
next_selected_id = payload_node->TabBar->NextSelectedTabId ? payload_node->TabBar->NextSelectedTabId : payload_node->TabBar->SelectedTabId;
if (payload_node == NULL)
next_selected_id = payload_window->TabId;
if (payload_node)
{
for (ImGuiWindow* window : payload_node->Windows)
payload_windows.push_back(window);
}
}
// FIXME-DOCK: When we are trying to dock an existing single-window node into a loose window, transfer Node ID as well
@@ -18138,6 +18238,24 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req)
// Update selection immediately
if (ImGuiTabBar* tab_bar = node->TabBar)
tab_bar->NextSelectedTabId = next_selected_id;
const ImVec2 target_pos = node->Pos;
const ImVec2 target_size = node->Size;
for (ImGuiWindow* window : payload_windows)
{
const ImVec2 from_pos = window->Pos;
const ImVec2 from_size = window->SizeFull;
const ImVec2 size_delta = target_size - from_size;
const ImVec2 pos_delta = target_pos - from_pos;
const float dist = size_delta.x * size_delta.x + size_delta.y * size_delta.y + pos_delta.x * pos_delta.x + pos_delta.y * pos_delta.y;
if (dist > 1.0f)
DockWindowQueueScaleAnim(ctx, window, from_pos, from_size, target_pos, target_size, true, 0.20f, 0.75f);
else
{
const ImVec2 scaled = target_size * 0.92f;
const ImVec2 centered = target_pos + (target_size - scaled) * 0.5f;
DockWindowQueueScaleAnim(ctx, window, centered, scaled, target_pos, target_size, true, 0.20f, 0.75f);
}
}
MarkIniSettingsDirty();
}
@@ -18168,6 +18286,12 @@ void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* windo
{
ImGuiContext& g = *ctx;
IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockWindow window '%s', clear_persistent_docking_ref = %d\n", window->Name, clear_persistent_docking_ref);
const ImVec2 from_pos = window->Pos;
const ImVec2 from_size = window->SizeFull;
if (from_size.x > 0.0f && from_size.y > 0.0f)
window->DockAnimGrabRatio = g.ActiveIdClickOffset / from_size;
else
window->DockAnimGrabRatio = ImVec2(0.5f, 0.5f);
if (window->DockNode)
DockNodeRemoveWindow(window->DockNode, window, clear_persistent_docking_ref ? 0 : window->DockId);
else
@@ -18176,11 +18300,17 @@ void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* windo
window->DockIsActive = false;
window->DockNodeIsVisible = window->DockTabIsVisible = false;
window->Size = window->SizeFull = FixLargeWindowsWhenUndocking(window->SizeFull, window->Viewport);
ImVec2 to_pos = window->Pos;
ImVec2 to_size = window->SizeFull;
ImVec2 size_delta = to_size - from_size;
ImVec2 pos_delta = to_pos - from_pos;
const float dist = size_delta.x * size_delta.x + size_delta.y * size_delta.y + pos_delta.x * pos_delta.x + pos_delta.y * pos_delta.y;
DockWindowQueueScaleAnim(ctx, window, from_pos, from_size, to_pos, to_size, true, 0.28f, 1.05f);
MarkIniSettingsDirty();
}
void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node)
ImGuiDockNode* ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node)
{
ImGuiContext& g = *ctx;
IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockNode node %08X\n", node->ID);
@@ -18219,6 +18349,7 @@ void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node)
node->Size = FixLargeWindowsWhenUndocking(node->Size, node->Windows[0]->Viewport);
node->WantMouseMove = true;
MarkIniSettingsDirty();
return node;
}
// This is mostly used for automation.
@@ -18669,7 +18800,7 @@ static void ImGui::DockNodeUpdateVisibleFlag(ImGuiDockNode* node)
node->IsVisible = is_visible;
}
static void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window)
void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window)
{
ImGuiContext& g = *GImGui;
IM_ASSERT(node->WantMouseMove == true);
@@ -19711,7 +19842,9 @@ static void ImGui::DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDock
overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(root_payload->Viewport);
// Draw main preview rectangle
const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.60f : 0.40f);
const float pulse = 0.5f + 0.5f * sinf((float)g.Time * 3.2f);
const float pulse_alpha = ImLerp(0.65f, 1.35f, pulse);
const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, (is_transparent_payload ? 0.60f : 0.40f) * pulse_alpha);
const ImU32 overlay_col_drop = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.90f : 0.70f);
const ImU32 overlay_col_drop_hovered = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 1.20f : 1.00f);
const ImU32 overlay_col_lines = GetColorU32(ImGuiCol_NavWindowingHighlight, is_transparent_payload ? 0.80f : 0.60f);

View File

@@ -2838,6 +2838,16 @@ struct IMGUI_API ImGuiWindow
ImVec2 Pos; // Position (always rounded-up to nearest pixel)
ImVec2 Size; // Current size (==SizeFull or collapsed title bar size)
ImVec2 SizeFull; // Size when non collapsed
ImVec2 DockAnimFromPos; // Docking animation start position
ImVec2 DockAnimFromSize; // Docking animation start size
ImVec2 DockAnimToPos; // Docking animation target position
ImVec2 DockAnimToSize; // Docking animation target size
ImVec2 DockAnimGrabRatio; // Docking animation grab ratio during undock drag
float DockAnimStartTime; // Docking animation start time
float DockAnimDuration; // Docking animation duration
bool DockAnimActive; // Docking animation active
bool DockAnimOvershoot; // Docking animation overshoot (undock)
float DockAnimOvershootStrength; // Docking animation overshoot strength
ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding.
ImVec2 ContentSizeIdeal;
ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize().
@@ -3005,6 +3015,7 @@ struct ImGuiTabItem
int LastFrameVisible;
int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance
float Offset; // Position relative to beginning of tab
float OffsetAnim; // Animated position relative to beginning of tab
float Width; // Width currently displayed
float ContentWidth; // Width of label + padding, stored during BeginTabItem() call (misnamed as "Content" would normally imply width of label only)
float RequestedWidth; // Width optionally requested by caller, -1.0f is unused
@@ -3013,7 +3024,7 @@ struct ImGuiTabItem
ImS16 IndexDuringLayout; // Index only used during TabBarLayout(). Tabs gets reordered so 'Tabs[n].IndexDuringLayout == n' but may mismatch during additions.
bool WantClose; // Marked as closed by SetTabItemClosed()
ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; }
ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; OffsetAnim = -1.0f; }
};
// Storage for a tab bar (sizeof() 160 bytes)
@@ -3670,7 +3681,7 @@ namespace ImGui
IMGUI_API void DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window);
IMGUI_API void DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node);
IMGUI_API void DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref = true);
IMGUI_API void DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node);
IMGUI_API ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node);
IMGUI_API bool DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos);
IMGUI_API ImGuiDockNode*DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id);
IMGUI_API void DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar);

View File

@@ -9724,6 +9724,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
{
ImGuiContext& g = *GImGui;
tab_bar->WantLayout = false;
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
// Track selected tab when resizing our parent down
const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
@@ -9927,11 +9928,20 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
section_tab_index += section->TabCount;
}
const float anim_t = ImSaturate(g.IO.DeltaTime * 14.0f);
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
{
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
if (tab->OffsetAnim < 0.0f || tab_bar_appearing)
tab->OffsetAnim = tab->Offset;
else
tab->OffsetAnim = ImLerp(tab->OffsetAnim, tab->Offset, anim_t);
}
// Clear name buffers
tab_bar->TabsNames.Buf.resize(0);
// If we have lost the selected tab, select the next most recently active one
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
if (found_selected_tab_id == false && !tab_bar_appearing)
tab_bar->SelectedTabId = 0;
if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
@@ -10551,10 +10561,11 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
// Layout
const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
size.x = tab->Width;
const float render_offset = (tab->OffsetAnim >= 0.0f) ? tab->OffsetAnim : tab->Offset;
if (is_central_section)
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(render_offset - tab_bar->ScrollingAnim), 0.0f);
else
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(render_offset, 0.0f);
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, pos + size);

View File

@@ -39,6 +39,7 @@ GLFWwindow *Window::makeWindow() {
}
glfwMakeContextCurrent(window);
glfwSwapInterval(0);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cerr << "Failed to initialize GLAD\n";

52
src/main_player.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "Engine.h"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#else
#include <unistd.h>
#endif
static std::filesystem::path getExecutableDir() {
#if defined(_WIN32)
char pathBuf[MAX_PATH] = {};
DWORD len = GetModuleFileNameA(nullptr, pathBuf, MAX_PATH);
if (len == 0 || len == MAX_PATH) {return {};}
return std::filesystem::path(pathBuf).parent_path();
#elif defined(__APPLE__)
uint32_t size = 0;
if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) {return {};}
std::string buf(size, '\0');
if (_NSGetExecutablePath(buf.data(), &size) != 0) {return {};}
return std::filesystem::path(buf).lexically_normal().parent_path();
#else
std::vector<char> buf(4096, '\0');
ssize_t len = readlink("/proc/self/exe", buf.data(), buf.size() - 1);
if (len <= 0) {return {};}
buf[static_cast<size_t>(len)] = '\0';
return std::filesystem::path(buf.data()).parent_path();
#endif
}
int main() {
if (auto exeDir = getExecutableDir(); !exeDir.empty()) {
std::error_code ec;
std::filesystem::current_path(exeDir, ec);
if (ec) {
std::cerr << "[WARN] Failed to set working dir to executable: "
<< ec.message() << std::endl;
}
}
Engine engine;
if (!engine.init()) {return -1;}
engine.run();
engine.shutdown();
return 0;
}