Compare commits

..

10 Commits

Author SHA1 Message Date
303b835ba7 First Commit on new Git-Base, yey! 2026-01-22 12:30:53 -05:00
Anemunt
2061d588e7 Added Hot loading and fixed the engine freezing when compiling scripts, Yey! 2026-01-01 18:58:13 -05:00
Anemunt
b5bbbc2937 Better Physics a little, New UI! And not only that, More simple scripting, Yey!!!! 2026-01-01 00:35:51 -05:00
Anemunt
ac1fab021c Adding notes in commonly edited scripts to clarify where functions with // notes to know what the script does in better detail. 2025-12-30 09:24:16 -05:00
Anemunt
67e6ece953 A few updates to the Project layout + some compatibility stuff lol.
New Project layout i added:
ProjectName/
├─ Assets/
│ ├─ Scenes/
│ ├─ Scripts/
│ │ ├─ Runtime/
│ │ └─ Editor/
│ ├─ Models/
│ ├─ Shaders/
│ └─ Materials/
├─ Library/ (aka CacheLibrary)
│ ├─ CompiledScripts/
│ ├─ InstalledPackages/
│ ├─ ScriptTemp/
│ └─ Temp/
├─ ProjectUserSettings/
│ ├─ ProjectLayout/
│ ├─ ScriptSettings/
│ └─ UserPrefs/ (optional)
├─ packages.modu
├─ project.modu
└─ scripts.modu
2025-12-30 08:52:52 -05:00
Anemunt
ee30412c9b fix building 2025-12-28 16:25:54 -05:00
Anemunt
9ce4b41e39 fixed up a few lighting issues + made modularity boot quicker without taking past 100 MB of ram. 2025-12-28 16:01:45 -05:00
5e1d352289 Merge branch 'main' of https://git.shockinteractive.xyz/Shock-Interactive-LLC/Modularity 2025-12-28 03:48:37 +01:00
920f201432 Removed stupid vs 2026 checker for windows build because i have 2022 not 2026! 2025-12-28 03:48:28 +01:00
Anemunt
1bedff2aff fixed light flickering when rotating an object. 2025-12-27 21:46:33 -05:00
107 changed files with 20607 additions and 1426 deletions

1
.gitignore vendored
View File

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

View File

@@ -51,12 +51,35 @@ endif()
# ==================== Optional PhysX ==================== # ==================== Optional PhysX ====================
option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON) option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON)
option(MODULARITY_BUILD_EDITOR "Build the Modularity editor target" ON)
# ==================== Third-party libraries ==================== # ==================== Third-party libraries ====================
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL) add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
find_package(OpenGL REQUIRED) 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 # GLAD
add_library(glad STATIC src/ThirdParty/glad/glad.c) add_library(glad STATIC src/ThirdParty/glad/glad.c)
target_include_directories(glad PUBLIC src/ThirdParty/glad) target_include_directories(glad PUBLIC src/ThirdParty/glad)
@@ -105,11 +128,16 @@ file(GLOB_RECURSE ENGINE_HEADERS CONFIGURE_DEPENDS
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/assimp/.*") list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/PhysX/.*") 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/assimp/.*")
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*") list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
add_library(core STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS}) add_library(core STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "Disable Assimp warnings as errors" FORCE) set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "Disable Assimp warnings as errors" FORCE)
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "Disable Assimp tests" FORCE)
set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "Disable Assimp samples" FORCE)
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "Disable Assimp tools" FORCE)
set(ASSIMP_INSTALL OFF CACHE BOOL "Disable Assimp install targets" FORCE)
add_subdirectory(src/ThirdParty/assimp EXCLUDE_FROM_ALL) add_subdirectory(src/ThirdParty/assimp EXCLUDE_FROM_ALL)
target_link_libraries(core PUBLIC assimp) target_link_libraries(core PUBLIC assimp)
target_include_directories(core PUBLIC target_include_directories(core PUBLIC
@@ -118,8 +146,29 @@ target_include_directories(core PUBLIC
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include ${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
) )
target_link_libraries(core PUBLIC glad glm imgui imguizmo) 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}) 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) if(MODULARITY_ENABLE_PHYSX)
set(PHYSX_ROOT_DIR ${PROJECT_SOURCE_DIR}/src/ThirdParty/PhysX/physx CACHE PATH "PhysX root directory") 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)") set(TARGET_BUILD_PLATFORM "linux" CACHE STRING "PhysX build platform (linux/windows)")
@@ -130,19 +179,47 @@ if(MODULARITY_ENABLE_PHYSX)
target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include) target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include)
target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB) target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
target_link_libraries(core PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking) 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() endif()
# ==================== Executable ==================== # ==================== Executable ====================
add_executable(Modularity src/main.cpp) if(MODULARITY_BUILD_EDITOR)
target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS}) 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 # Link order matters on Linux
if(NOT WIN32) if(NOT WIN32)
find_package(X11 REQUIRED) 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 target_include_directories(ModularityPlayer PRIVATE ${X11_INCLUDE_DIR})
core target_link_libraries(ModularityPlayer PRIVATE
core_player
imgui imgui
imguizmo imguizmo
glad glad
@@ -157,15 +234,25 @@ if(NOT WIN32)
Xinerama Xinerama
Xcursor Xcursor
) )
# Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols. target_link_options(ModularityPlayer PRIVATE "-rdynamic")
target_link_options(Modularity PRIVATE "-rdynamic")
else() 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() endif()
# ==================== Copy Resources folder after build ==================== # ==================== 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 COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Resources ${CMAKE_SOURCE_DIR}/Resources
$<TARGET_FILE_DIR:Modularity>/Resources $<TARGET_FILE_DIR:ModularityPlayer>/Resources
) )

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,9 @@ uniform float threshold = 1.0;
void main() { void main() {
vec3 c = texture(sceneTex, TexCoord).rgb; vec3 c = texture(sceneTex, TexCoord).rgb;
float luma = dot(c, vec3(0.2125, 0.7154, 0.0721)); float luma = dot(c, vec3(0.2125, 0.7154, 0.0721));
float bright = max(luma - threshold, 0.0); float knee = 0.25;
vec3 masked = c * step(0.0, bright); float w = clamp((luma - threshold) / max(knee, 1e-4), 0.0, 1.0);
w = w * w * (3.0 - 2.0 * w);
vec3 masked = c * w;
FragColor = vec4(masked, 1.0); FragColor = vec4(masked, 1.0);
} }

View File

@@ -42,17 +42,8 @@ vec3 applyColorAdjust(vec3 color) {
return color; return color;
} }
vec3 sampleCombined(vec2 uv) { vec3 sampleBase(vec2 uv) {
vec3 c = texture(sceneTex, uv).rgb; return applyColorAdjust(texture(sceneTex, uv).rgb);
if (enableBloom) {
vec3 glow = texture(bloomTex, uv).rgb * bloomIntensity;
c += glow;
}
return c;
}
vec3 sampleAdjusted(vec2 uv) {
return applyColorAdjust(sampleCombined(uv));
} }
float luminance(vec3 c) { float luminance(vec3 c) {
@@ -66,24 +57,24 @@ float computeVignette(vec2 uv) {
} }
vec3 applyChromatic(vec2 uv) { vec3 applyChromatic(vec2 uv) {
vec3 base = sampleAdjusted(uv); vec3 base = sampleBase(uv);
vec2 dir = uv - vec2(0.5); vec2 dir = uv - vec2(0.5);
float dist = max(length(dir), 0.0001); float dist = max(length(dir), 0.0001);
vec2 offset = normalize(dir) * chromaticAmount * (1.0 + dist * 2.0); vec2 offset = normalize(dir) * chromaticAmount * (1.0 + dist * 2.0);
vec3 rSample = sampleAdjusted(uv + offset); vec3 rSample = sampleBase(uv + offset);
vec3 bSample = sampleAdjusted(uv - offset); vec3 bSample = sampleBase(uv - offset);
vec3 ca = vec3(rSample.r, base.g, bSample.b); vec3 ca = vec3(rSample.r, base.g, bSample.b);
return mix(base, ca, 0.85); return mix(base, ca, 0.85);
} }
float computeAOFactor(vec2 uv) { float computeAOFactor(vec2 uv) {
vec3 centerColor = sampleAdjusted(uv); vec3 centerColor = sampleBase(uv);
float centerLum = luminance(centerColor); float centerLum = luminance(centerColor);
float occlusion = 0.0; float occlusion = 0.0;
vec2 directions[4] = vec2[](vec2(1.0, 0.0), vec2(-1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, -1.0)); vec2 directions[4] = vec2[](vec2(1.0, 0.0), vec2(-1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, -1.0));
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
vec2 sampleUv = uv + directions[i] * aoRadius; vec2 sampleUv = uv + directions[i] * aoRadius;
vec3 sampleColor = sampleAdjusted(sampleUv); vec3 sampleColor = sampleBase(sampleUv);
float sampleLum = luminance(sampleColor); float sampleLum = luminance(sampleColor);
occlusion += max(0.0, centerLum - sampleLum); occlusion += max(0.0, centerLum - sampleLum);
} }
@@ -92,7 +83,7 @@ float computeAOFactor(vec2 uv) {
} }
void main() { void main() {
vec3 color = sampleAdjusted(TexCoord); vec3 color = sampleBase(TexCoord);
if (enableChromatic) { if (enableChromatic) {
color = applyChromatic(TexCoord); color = applyChromatic(TexCoord);
@@ -132,5 +123,10 @@ void main() {
} }
} }
if (enableBloom) {
vec3 glow = texture(bloomTex, TexCoord).rgb * bloomIntensity;
color += glow;
}
FragColor = vec4(color, 1.0); FragColor = vec4(color, 1.0);
} }

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 Collapsed=0
[Window][DockSpace] [Window][DockSpace]
Pos=0,23 Pos=0,24
Size=1920,983 Size=1900,998
Collapsed=0 Collapsed=0
[Window][Viewport] [Window][Viewport]
Pos=306,46 Pos=304,48
Size=1265,739 Size=1249,747
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x0000000F,0
[Window][Hierarchy] [Window][Hierarchy]
Pos=0,46 Pos=0,48
Size=304,739 Size=304,747
Collapsed=0 Collapsed=0
DockId=0x00000001,0 DockId=0x00000007,0
[Window][Inspector] [Window][Inspector]
Pos=1573,46 Pos=1553,48
Size=347,960 Size=347,747
Collapsed=0 Collapsed=0
DockId=0x00000008,0 DockId=0x00000010,0
[Window][File Browser] [Window][File Browser]
Pos=756,836 Pos=756,836
Size=753,221 Size=753,221
Collapsed=0 Collapsed=0
DockId=0x00000006,1 DockId=0xD71539A0,1
[Window][Console] [Window][Console]
Pos=0,787 Pos=939,795
Size=785,219 Size=961,227
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000014,0
[Window][Project] [Window][Project]
Pos=787,787 Pos=0,795
Size=784,219 Size=939,227
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000013,0
[Window][Launcher] [Window][Launcher]
Pos=0,0 Pos=0,0
@@ -60,43 +60,64 @@ Size=1000,800
Collapsed=0 Collapsed=0
[Window][Camera] [Window][Camera]
Pos=0,46 Pos=0,48
Size=304,739 Size=304,747
Collapsed=0 Collapsed=0
DockId=0x00000001,1 DockId=0x00000007,1
[Window][Environment] [Window][Environment]
Pos=1573,46 Pos=1553,48
Size=347,960 Size=347,747
Collapsed=0 Collapsed=0
DockId=0x00000008,1 DockId=0x00000010,1
[Window][Project Manager] [Window][Project Manager]
Pos=787,785 Pos=787,785
Size=784,221 Size=784,221
Collapsed=0 Collapsed=0
DockId=0x00000006,1 DockId=0xD71539A0,1
[Window][Game Viewport] [Window][Game Viewport]
Pos=306,46 Pos=304,48
Size=1265,739 Size=1249,747
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x0000000F,1
[Window][Project Settings] [Window][Project Settings]
Pos=306,46 Pos=304,48
Size=1265,739 Size=1249,747
Collapsed=0 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] [Docking][Data]
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y
DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867
DockNode ID=0x00000003 Parent=0x00000007 SizeRef=1858,739 Split=X DockNode ID=0x00000001 Parent=0x00000005 SizeRef=1900,747 Split=X Selected=0xE9044848
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=304,758 Selected=0xBABDAE5E DockNode ID=0x00000007 Parent=0x00000001 SizeRef=304,486 Selected=0xBABDAE5E
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=694,758 CentralNode=1 Selected=0xC450F867 DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1596,486 Split=X Selected=0xE9044848
DockNode ID=0x00000004 Parent=0x00000007 SizeRef=1858,219 Split=X Selected=0xEA83D666 DockNode ID=0x0000000F Parent=0x00000008 SizeRef=1249,486 Selected=0xE9044848
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=929,221 Selected=0xEA83D666 DockNode ID=0x00000010 Parent=0x00000008 SizeRef=347,486 Selected=0x36DC96AB
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0x9C21DE82 DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1900,227 Split=X Selected=0x3F1379AF
DockNode ID=0x00000008 Parent=0xD71539A0 SizeRef=347,1015 Selected=0x36DC96AB 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;
}

44
Scripts/FPSDisplay.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <string>
#include <algorithm>
namespace {
bool clampTo120 = false;
float smoothFps = 0.0f;
float smoothing = 0.15f;
}
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ctx.AutoSetting("ClampFPS120", clampTo120);
if (ctx.script) {
std::string saved = ctx.GetSetting("FpsSmoothing", "");
if (!saved.empty()) {
try { smoothing = std::stof(saved); } catch (...) {}
}
}
bool changed = false;
ImGui::TextUnformatted("FPS Display");
ImGui::Separator();
changed |= ImGui::Checkbox("Clamp FPS to 120", &clampTo120);
changed |= ImGui::DragFloat("Smoothing", &smoothing, 0.01f, 0.0f, 1.0f, "%.2f");
if (changed) {
ctx.SetFPSCap(clampTo120, 120.0f);
ctx.SetSetting("FpsSmoothing", std::to_string(smoothing));
ctx.SaveAutoSettings();
}
ImGui::TextDisabled("Attach to a UI Text object.");
}
void TickUpdate(ScriptContext& ctx, float deltaTime) {
if (!ctx.object || ctx.object->type != ObjectType::UIText) return;
float fps = (deltaTime > 1e-6f) ? (1.0f / deltaTime) : 0.0f;
float k = std::clamp(smoothing, 0.0f, 1.0f);
if (smoothFps <= 0.0f) smoothFps = fps;
smoothFps = smoothFps + (fps - smoothFps) * k;
ctx.object->ui.label = "FPS: " + std::to_string(static_cast<int>(smoothFps + 0.5f));
}

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

@@ -36,15 +36,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextUnformatted("RigidbodyTest"); ImGui::TextUnformatted("RigidbodyTest");
ImGui::Separator(); ImGui::Separator();
bool changed = false; ImGui::Checkbox("Launch on Begin", &autoLaunch);
changed |= ImGui::Checkbox("Launch on Begin", &autoLaunch); ImGui::Checkbox("Show Velocity Readback", &showVelocity);
changed |= ImGui::Checkbox("Show Velocity Readback", &showVelocity); ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
changed |= ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f); ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
changed |= ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
if (changed) {
ctx.SaveAutoSettings();
}
if (ImGui::Button("Launch Now")) { if (ImGui::Button("Launch Now")) {
Launch(ctx); Launch(ctx);
@@ -77,12 +72,3 @@ void Begin(ScriptContext& ctx, float /*deltaTime*/) {
Launch(ctx); Launch(ctx);
} }
} }
void Spec(ScriptContext& ctx, float /*deltaTime*/) {
}
void TestEditor(ScriptContext& ctx, float /*deltaTime*/) {
}
void TickUpdate(ScriptContext& ctx, float /*deltaTime*/) {
}

View File

@@ -3,13 +3,11 @@
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
namespace { namespace {
// Script state (persisted by AutoSetting binder)
bool autoRotate = false; bool autoRotate = false;
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
char targetName[128] = "MyTarget"; char targetName[128] = "MyTarget";
// Runtime behavior
static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) { static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) {
if (!autoRotate || !ctx.object) return; if (!autoRotate || !ctx.object) return;
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime); ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
@@ -17,7 +15,6 @@ namespace {
} }
extern "C" void Script_OnInspector(ScriptContext& ctx) { extern "C" void Script_OnInspector(ScriptContext& ctx) {
// Auto settings (loaded once, saved only when changed)
ctx.AutoSetting("autoRotate", autoRotate); ctx.AutoSetting("autoRotate", autoRotate);
ctx.AutoSetting("spinSpeed", spinSpeed); ctx.AutoSetting("spinSpeed", spinSpeed);
ctx.AutoSetting("offset", offset); ctx.AutoSetting("offset", offset);
@@ -26,15 +23,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextUnformatted("SampleInspector"); ImGui::TextUnformatted("SampleInspector");
ImGui::Separator(); ImGui::Separator();
bool changed = false; ImGui::Checkbox("Auto Rotate", &autoRotate);
changed |= ImGui::Checkbox("Auto Rotate", &autoRotate); ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
changed |= ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f); ImGui::DragFloat3("Offset", &offset.x, 0.1f);
changed |= ImGui::DragFloat3("Offset", &offset.x, 0.1f); ImGui::InputText("Target Name", targetName, sizeof(targetName));
changed |= ImGui::InputText("Target Name", targetName, sizeof(targetName));
if (changed) {
ctx.SaveAutoSettings();
}
if (ctx.object) { if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id); ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
@@ -44,15 +36,12 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
} }
} }
if (ImGui::Button("Nudge Target")) { if (ImGui::Button("Nudge Target")) {
if (SceneObject* target = ctx.FindObjectByName(targetName)) { if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
target->position += offset; target->position += offset;
} }
} }
} }
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
}
void Spec(ScriptContext& ctx, float deltaTime) { void Spec(ScriptContext& ctx, float deltaTime) {
ApplyAutoRotate(ctx, deltaTime); ApplyAutoRotate(ctx, deltaTime);
} }

View File

@@ -8,101 +8,41 @@
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
#include <string>
#include <algorithm>
#include <sstream>
namespace { namespace {
bool autoRotate = false; bool autoRotate = false;
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f);
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
char targetName[128] = "MyTarget"; char targetName[128] = "MyTarget";
int settingsLoadedForId = -1;
ScriptComponent* settingsLoadedForScript = nullptr;
void setSetting(ScriptContext& ctx, const std::string& key, const std::string& value) { void bindSettings(ScriptContext& ctx) {
if (!ctx.script) return; ctx.AutoSetting("autoRotate", autoRotate);
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(), ctx.AutoSetting("spinSpeed", spinSpeed);
[&](const ScriptSetting& s) { return s.key == key; }); ctx.AutoSetting("offset", offset);
if (it != ctx.script->settings.end()) { ctx.AutoSetting("targetName", targetName, sizeof(targetName));
it->value = value;
} else {
ctx.script->settings.push_back({key, value});
}
}
std::string getSetting(const ScriptContext& ctx, const std::string& key, const std::string& fallback = "") {
if (!ctx.script) return fallback;
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
[&](const ScriptSetting& s) { return s.key == key; });
return (it != ctx.script->settings.end()) ? it->value : fallback;
}
void loadSettings(ScriptContext& ctx) {
if (!ctx.script || !ctx.object) return;
if (settingsLoadedForId == ctx.object->id && settingsLoadedForScript == ctx.script) return;Segmentation fault (core dumped)
settingsLoadedForId = ctx.object->id;
settingsLoadedForScript = ctx.script;
auto parseBool = [](const std::string& v, bool def) {
if (v == "1" || v == "true") return true;
if (v == "0" || v == "false") return false;
return def;
};
auto parseVec3 = [](const std::string& v, const glm::vec3& def) {
glm::vec3 out = def;
std::stringstream ss(v);
std::string part;
for (int i = 0; i < 3 && std::getline(ss, part, ','); ++i) {
try { out[i] = std::stof(part); } catch (...) {}
}
return out;
};
autoRotate = parseBool(getSetting(ctx, "autoRotate", autoRotate ? "1" : "0"), autoRotate);
spinSpeed = parseVec3(getSetting(ctx, "spinSpeed", ""), spinSpeed);
offset = parseVec3(getSetting(ctx, "offset", ""), offset);
std::string tgt = getSetting(ctx, "targetName", targetName);
if (!tgt.empty()) {
std::snprintf(targetName, sizeof(targetName), "%s", tgt.c_str());
}
}
void persistSettings(ScriptContext& ctx) {
setSetting(ctx, "autoRotate", autoRotate ? "1" : "0");
setSetting(ctx, "spinSpeed",
std::to_string(spinSpeed.x) + "," + std::to_string(spinSpeed.y) + "," + std::to_string(spinSpeed.z));
setSetting(ctx, "offset",
std::to_string(offset.x) + "," + std::to_string(offset.y) + "," + std::to_string(offset.z));
setSetting(ctx, "targetName", targetName);
ctx.MarkDirty();
} }
void applyAutoRotate(ScriptContext& ctx, float deltaTime) { void applyAutoRotate(ScriptContext& ctx, float deltaTime) {
if (!autoRotate || !ctx.object) return; if (!autoRotate || !ctx.object) return;
if (ctx.HasRigidbody() && !ctx.object->rigidbody.isKinematic) {
if (ctx.SetRigidbodyAngularVelocity(glm::radians(spinSpeed))) {
return;
}
}
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime); ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
} }
} // namespace } // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) { extern "C" void Script_OnInspector(ScriptContext& ctx) {
loadSettings(ctx); bindSettings(ctx);
ImGui::TextUnformatted("SampleInspector"); ImGui::TextUnformatted("SampleInspector");
ImGui::Separator(); ImGui::Separator();
if (ImGui::Checkbox("Auto Rotate", &autoRotate)) { ImGui::Checkbox("Auto Rotate", &autoRotate);
persistSettings(ctx); ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
} ImGui::DragFloat3("Offset", &offset.x, 0.1f);
if (ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f)) {
persistSettings(ctx);
}
if (ImGui::DragFloat3("Offset", &offset.x, 0.1f)) {
persistSettings(ctx);
}
ImGui::InputText("Target Name", targetName, sizeof(targetName)); ImGui::InputText("Target Name", targetName, sizeof(targetName));
persistSettings(ctx);
if (ctx.object) { if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id); ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
@@ -113,7 +53,7 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
} }
if (ImGui::Button("Nudge Target")) { if (ImGui::Button("Nudge Target")) {
if (SceneObject* target = ctx.FindObjectByName(targetName)) { if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
target->position += offset; target->position += offset;
} }
} }
@@ -122,7 +62,7 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
// New lifecycle hooks supported by the compiler wrapper. These are optional stubs demonstrating usage. // New lifecycle hooks supported by the compiler wrapper. These are optional stubs demonstrating usage.
void Begin(ScriptContext& ctx, float /*deltaTime*/) { void Begin(ScriptContext& ctx, float /*deltaTime*/) {
// Initialize per-script state here. // Initialize per-script state here.
loadSettings(ctx); bindSettings(ctx);
} }
void Spec(ScriptContext& ctx, float deltaTime) { void Spec(ScriptContext& ctx, float deltaTime) {

View File

@@ -2,203 +2,55 @@
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
#include <unordered_map> #include <unordered_map>
namespace
namespace { {
struct ControllerState { struct ControllerState
float pitch = 0.0f; {
float yaw = 0.0f; ScriptContext::StandaloneMovementState movement; ScriptContext::StandaloneMovementDebug debug;
float verticalVelocity = 0.0f; bool initialized = false;
bool initialized = false; };
}; std::unordered_map<int, ControllerState> g_states;
ScriptContext::StandaloneMovementSettings g_settings;
std::unordered_map<int, ControllerState> g_states; ControllerState& getState(int id) {return g_states[id];}
// aliases for readability
glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f); // walk speed, run speed, jump glm::vec3& moveTuning = g_settings.moveTuning;
glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f); // sensitivity, max delta clamp, reserved glm::vec3& lookTuning = g_settings.lookTuning;
glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f); // height, radius, ground snap glm::vec3& capsuleTuning = g_settings.capsuleTuning;
glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f); // gravity, probe extra, max fall speed glm::vec3& gravityTuning = g_settings.gravityTuning;
bool enableMouseLook = true; bool& enableMouseLook = g_settings.enableMouseLook;
bool requireMouseButton = false; bool& requireMouseButton = g_settings.requireMouseButton;
bool enforceCollider = true; bool& enforceCollider = g_settings.enforceCollider;
bool enforceRigidbody = true; bool& enforceRigidbody = g_settings.enforceRigidbody;
bool showDebug = false;
ControllerState& getState(int id) {
return g_states[id];
} }
extern "C" void Script_OnInspector(ScriptContext& ctx)
void bindSettings(ScriptContext& ctx) { {
ctx.AutoSetting("moveTuning", moveTuning); ctx.BindStandaloneMovementSettings(g_settings);
ctx.AutoSetting("lookTuning", lookTuning);
ctx.AutoSetting("capsuleTuning", capsuleTuning);
ctx.AutoSetting("gravityTuning", gravityTuning);
ctx.AutoSetting("enableMouseLook", enableMouseLook);
ctx.AutoSetting("requireMouseButton", requireMouseButton);
ctx.AutoSetting("enforceCollider", enforceCollider);
ctx.AutoSetting("enforceRigidbody", enforceRigidbody);
ctx.AutoSetting("showDebug", showDebug);
}
void ensureComponents(ScriptContext& ctx, float height, float radius) {
if (!ctx.object) return;
if (enforceCollider) {
ctx.object->hasCollider = true;
ctx.object->collider.enabled = true;
ctx.object->collider.type = ColliderType::Capsule;
ctx.object->collider.convex = true;
ctx.object->collider.boxSize = glm::vec3(radius * 2.0f, height, radius * 2.0f);
}
if (enforceRigidbody) {
ctx.object->hasRigidbody = true;
ctx.object->rigidbody.enabled = true;
ctx.object->rigidbody.useGravity = true;
ctx.object->rigidbody.isKinematic = false;
}
}
} // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) {
bindSettings(ctx);
ImGui::TextUnformatted("Standalone Movement Controller"); ImGui::TextUnformatted("Standalone Movement Controller");
ImGui::Separator(); ImGui::Separator();
ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
bool changed = false; ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
changed |= ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f"); ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
changed |= ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f"); ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
changed |= ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f"); ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
changed |= ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f"); ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
changed |= ImGui::Checkbox("Enable Mouse Look", &enableMouseLook); ImGui::Checkbox("Force Collider", &enforceCollider);
changed |= ImGui::Checkbox("Hold RMB to Look", &requireMouseButton); ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
changed |= ImGui::Checkbox("Force Collider", &enforceCollider);
changed |= ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
changed |= ImGui::Checkbox("Show Debug", &showDebug);
if (changed) {
ctx.SaveAutoSettings();
}
} }
void Begin(ScriptContext& ctx, float /*deltaTime*/) { void Begin(ScriptContext& ctx, float)
if (!ctx.object) return; {
bindSettings(ctx); if (!ctx.object) return; ControllerState& s = getState(ctx.object->id);
ControllerState& state = getState(ctx.object->id); if (!s.initialized)
if (!state.initialized) { {
state.pitch = ctx.object->rotation.x; s.movement.pitch = ctx.object->rotation.x;
state.yaw = ctx.object->rotation.y; s.movement.yaw = ctx.object->rotation.y;
state.verticalVelocity = 0.0f; s.initialized = true;
state.initialized = true;
} }
ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y); if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
} }
void TickUpdate(ScriptContext& ctx, float deltaTime) { void TickUpdate(ScriptContext& ctx, float dt)
if (!ctx.object) return; {
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id); ctx.TickStandaloneMovement(s.movement, g_settings, dt, nullptr);
ControllerState& state = getState(ctx.object->id);
ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y);
const float walkSpeed = moveTuning.x;
const float runSpeed = moveTuning.y;
const float jumpStrength = moveTuning.z;
const float lookSensitivity = lookTuning.x;
const float maxMouseDelta = glm::max(5.0f, lookTuning.y);
const float height = capsuleTuning.x;
const float radius = capsuleTuning.y;
const float groundSnap = capsuleTuning.z;
const float gravity = gravityTuning.x;
const float probeExtra = gravityTuning.y;
const float maxFall = glm::max(1.0f, gravityTuning.z);
if (enableMouseLook) {
bool allowLook = !requireMouseButton || ImGui::IsMouseDown(ImGuiMouseButton_Right);
if (allowLook) {
ImGuiIO& io = ImGui::GetIO();
glm::vec2 delta(io.MouseDelta.x, io.MouseDelta.y);
float len = glm::length(delta);
if (len > maxMouseDelta) {
delta *= (maxMouseDelta / len);
}
state.yaw -= delta.x * 50.0f * lookSensitivity * deltaTime;
state.pitch -= delta.y * 50.0f * lookSensitivity * deltaTime;
state.pitch = std::clamp(state.pitch, -89.0f, 89.0f);
}
}
glm::quat q = glm::quat(glm::radians(glm::vec3(state.pitch, state.yaw, 0.0f)));
glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f));
glm::vec3 planarForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
glm::vec3 planarRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
if (!std::isfinite(planarForward.x) || glm::length(planarForward) < 1e-3f) {
planarForward = glm::vec3(0.0f, 0.0f, -1.0f);
}
if (!std::isfinite(planarRight.x) || glm::length(planarRight) < 1e-3f) {
planarRight = glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 move(0.0f);
if (ImGui::IsKeyDown(ImGuiKey_W)) move += planarForward;
if (ImGui::IsKeyDown(ImGuiKey_S)) move -= planarForward;
if (ImGui::IsKeyDown(ImGuiKey_D)) move += planarRight;
if (ImGui::IsKeyDown(ImGuiKey_A)) move -= planarRight;
if (glm::length(move) > 0.001f) move = glm::normalize(move);
bool sprint = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
float targetSpeed = sprint ? runSpeed : walkSpeed;
glm::vec3 velocity = move * targetSpeed;
float capsuleHalf = std::max(0.1f, height * 0.5f);
glm::vec3 physVel;
bool havePhysVel = ctx.GetRigidbodyVelocity(physVel);
if (havePhysVel) state.verticalVelocity = physVel.y;
glm::vec3 hitPos(0.0f);
glm::vec3 hitNormal(0.0f, 1.0f, 0.0f);
float hitDist = 0.0f;
float probeDist = capsuleHalf + probeExtra;
glm::vec3 rayStart = ctx.object->position + glm::vec3(0.0f, 0.1f, 0.0f);
bool hitGround = ctx.RaycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist,
&hitPos, &hitNormal, &hitDist);
bool grounded = hitGround && hitNormal.y > 0.25f &&
hitDist <= capsuleHalf + groundSnap &&
state.verticalVelocity <= 0.35f;
if (!hitGround) {
grounded = ctx.object->position.y <= capsuleHalf + 0.12f && state.verticalVelocity <= 0.35f;
}
if (grounded) {
state.verticalVelocity = 0.0f;
if (!havePhysVel) {
if (hitGround) {
ctx.object->position.y = std::max(ctx.object->position.y, hitPos.y + capsuleHalf);
} else {
ctx.object->position.y = capsuleHalf;
}
}
if (ImGui::IsKeyDown(ImGuiKey_Space)) {
state.verticalVelocity = jumpStrength;
}
} else {
state.verticalVelocity += gravity * deltaTime;
}
state.verticalVelocity = std::clamp(state.verticalVelocity, -maxFall, maxFall);
velocity.y = state.verticalVelocity;
glm::vec3 rotation(state.pitch, state.yaw, 0.0f);
ctx.object->rotation = rotation;
ctx.SetRigidbodyRotation(rotation);
if (!ctx.SetRigidbodyVelocity(velocity)) {
ctx.object->position += velocity * deltaTime;
}
if (showDebug) {
ImGui::Text("Move (%.2f, %.2f, %.2f)", velocity.x, velocity.y, velocity.z);
ImGui::Text("Grounded: %s", grounded ? "yes" : "no");
}
} }
void Spec(ScriptContext& /*ctx*/, float /*deltaTime*/) {}
void TestEditor(ScriptContext& /*ctx*/, float /*deltaTime*/) {}

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

@@ -17,7 +17,7 @@ mkdir build
cd build cd build
echo [INFO] Configuring with CMake (Visual Studio 18 2026)... echo [INFO] Configuring with CMake (Visual Studio 18 2026)...
cmake -G "Visual Studio 18 2026" -A x64 .. cmake -A x64 ..
if errorlevel 1 ( if errorlevel 1 (
echo. echo.

View File

@@ -23,18 +23,56 @@ trap finish EXIT
echo -e "================================\n Modularity - Native Linux Builder\n================================" 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 git submodule update --init --recursive
if [ -d "build" ]; then if [ -d "build" ] && [ $clean_build -eq 1 ]; then
echo -e "[i]: Oh! We found an existing build directory.\nRemoving existing folder..." echo -e "[i]: Cleaning existing build directory..."
rm -rf build/ rm -rf build/
echo -e "[i]: Build Has been Removed\nContinuing build" echo -e "[i]: Build Has been Removed\nContinuing build"
fi fi
mkdir -p build mkdir -p build
cd build cd build
cmake .. cmake .. -DMONO_ROOT=/usr
cmake --build . -- -j"$(nproc)" 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 -r ../Resources .
cp Resources/imgui.ini . cp Resources/imgui.ini .
ln -sf build/compile_commands.json compile_commands.json ln -sf build/compile_commands.json compile_commands.json

View File

@@ -1,117 +1,424 @@
# Modularity C++ Scripting Quickstart ---
title: C++ Scripting
description: Hot-compiled native C++ scripts, per-object state, ImGui inspectors, and runtime/editor hooks.
---
## Project setup # C++ Scripting
- Scripts live under `Scripts/` (configurable via `Scripts.modu`). Scripts in Modularity are native C++ code compiled into shared libraries and loaded at runtime. They run per scene object and can optionally draw ImGui UI in the inspector and in custom editor windows.
- The engine generates a wrapper per script when compiling. It exports fixed entry points with `extern "C"` linkage:
- `Script_OnInspector(ScriptContext&)` > Notes up front:
- `Script_Begin(ScriptContext&, float deltaTime)` > - Scripts are not sandboxed. They can crash the editor/game if they dereference bad pointers or do unsafe work.
- `Script_Spec(ScriptContext&, float deltaTime)` > - Always null-check `ctx.object` (objects can be deleted, disabled, or scripts can be detached).
- `Script_TestEditor(ScriptContext&, float deltaTime)`
- `Script_Update(ScriptContext&, float deltaTime)` (fallback if TickUpdate is absent) ## Table of contents
- `Script_TickUpdate(ScriptContext&, float deltaTime)` - [Quickstart](#quickstart)
- Build config file: `Scripts.modu` (auto-created per project). Keys: - [C# managed scripting (experimental)](#c-managed-scripting-experimental)
- `scriptsDir`, `outDir`, `includeDir=...`, `define=...`, `linux.linkLib`, `win.linkLib`, `cppStandard`. - [Scripts.modu](#scriptsmodu)
- [How compilation works](#how-compilation-works)
- [Lifecycle hooks](#lifecycle-hooks)
- [ScriptContext](#scriptcontext)
- [ImGui in scripts](#imgui-in-scripts)
- [Per-script settings](#per-script-settings)
- [UI scripting](#ui-scripting)
- [IEnum tasks](#ienum-tasks)
- [Logging](#logging)
- [Scripted editor windows](#scripted-editor-windows)
- [Manual compile (CLI)](#manual-compile-cli)
- [Troubleshooting](#troubleshooting)
- [Templates](#templates)
## Quickstart
1. Create a script file under `Scripts/` (e.g. `Scripts/MyScript.cpp`).
2. Select an object in the scene.
3. In the Inspector, add/enable a script component and set its path:
- In the **Scripts** section, set `Path` OR click **Use Selection** after selecting the file in the File Browser.
4. Compile the script:
- In the File Browser, right-click the script file and choose **Compile Script**, or
- 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.
Common keys:
- `scriptsDir` - where script source files live (default: `Scripts`)
- `outDir` - where compiled binaries go (default: `Cache/ScriptBin`)
- `includeDir=...` - add include directories (repeatable)
- `define=...` - add preprocessor defines (repeatable)
- `linux.linkLib=...` - comma-separated link libs/flags for Linux (e.g. `dl,pthread`)
- `win.linkLib=...` - comma-separated link libs for Windows (e.g. `User32,Advapi32`)
- `cppStandard` - C++ standard (e.g. `c++20`)
Example:
```ini
scriptsDir=Scripts
outDir=Cache/ScriptBin
includeDir=../src
includeDir=../include
cppStandard=c++20
linux.linkLib=dl,pthread
win.linkLib=User32,Advapi32
```
## How compilation works
Modularity compiles scripts into shared libraries and loads them by symbol name.
- Source lives under `Scripts/`.
- Output binaries are written to `Cache/ScriptBin/`.
- Binaries are platform-specific:
- Windows: `.dll`
- Linux: `.so`
### Wrapper generation (important)
To reduce boilerplate, Modularity auto-generates a wrapper for these hook names **if it detects them in your script**:
- `Begin`
- `TickUpdate`
- `Update`
- `Spec`
- `TestEditor`
That wrapper exports `Script_Begin`, `Script_TickUpdate`, etc. This means you can usually write plain functions like:
```cpp
void TickUpdate(ScriptContext& ctx, float dt) {
(void)dt;
if (!ctx.object) return;
}
```
However:
- `Script_OnInspector` is **not** wrapper-generated. If you want inspector UI, you must export it explicitly with `extern "C"`.
- Scripted editor windows (`RenderEditorWindow`, `ExitRenderEditorWindow`) are also **not** wrapper-generated; export them explicitly with `extern "C"`.
## Lifecycle hooks ## Lifecycle hooks
- **Inspector**: `Script_OnInspector(ScriptContext&)` is called when the script is inspected in the UI. All hooks are optional. If a hook is missing, it is simply not called.
- **Begin**: `Script_Begin` runs once per object instance before ticking.
- **Spec/Test**: `Script_Spec` and `Script_TestEditor` run every frame when the global “Spec Mode” / “Test Mode” toggles are enabled (Scripts menu). Hook list:
- **Tick**: `Script_TickUpdate` runs every frame for each script; `Script_Update` is a fallback if TickUpdate is missing. - `Script_OnInspector(ScriptContext&)` (manual export required)
- All tick-style hooks receive `deltaTime` (seconds) and the `ScriptContext`. - `Script_Begin(ScriptContext&, float deltaTime)` (wrapper-generated from `Begin`)
- `Script_TickUpdate(ScriptContext&, float deltaTime)` (wrapper-generated from `TickUpdate`)
- `Script_Update(ScriptContext&, float deltaTime)` (wrapper-generated from `Update`, used only if TickUpdate missing)
- `Script_Spec(ScriptContext&, float deltaTime)` (wrapper-generated from `Spec`)
- `Script_TestEditor(ScriptContext&, float deltaTime)` (wrapper-generated from `TestEditor`)
Runtime notes:
- `Begin` runs once per object instance (per script component instance).
- `TickUpdate` runs every frame (preferred).
- `Update` runs only if `TickUpdate` is not exported.
- `Spec/TestEditor` run every frame only while their global toggles are enabled (main menu -> Scripts).
## ScriptContext
`ScriptContext` is passed into most hooks and provides access to the engine, the owning object, and helper APIs.
## ScriptContext helpers
Available methods:
- `FindObjectByName`, `FindObjectById`
- `SetPosition`, `SetRotation`, `SetScale`
- `HasRigidbody`
- `SetRigidbodyVelocity`, `GetRigidbodyVelocity`
- `SetRigidbodyAngularVelocity`, `GetRigidbodyAngularVelocity`
- `AddRigidbodyForce`, `AddRigidbodyImpulse`
- `AddRigidbodyTorque`, `AddRigidbodyAngularImpulse`
- `SetRigidbodyRotation`, `TeleportRigidbody`
- `MarkDirty` (flags the project as having unsaved changes)
Fields: Fields:
- `engine`: pointer to the Engine - `engine` (`Engine*`) - engine pointer
- `object`: pointer to the owning `SceneObject` - `object` (`SceneObject*`) - owning object pointer (may be null)
- `script`: pointer to the owning `ScriptComponent` (gives access to per-script `settings`) - `script` (`ScriptComponent*`) - owning script component (settings storage)
## Persisting per-script settings ### Object lookup
- Each `ScriptComponent` has `settings` (key/value strings) serialized with the scene. - `FindObjectByName(const std::string&)`
- You can read/write them via `ctx.script->settings` or helper functions in your script. - `FindObjectById(int)`
- After mutating settings or object transforms, call `ctx.MarkDirty()` so Ctrl+S captures changes.
## Example pattern (simplified) ### Transform helpers
- `SetPosition(const glm::vec3&)`
- `SetRotation(const glm::vec3&)` (degrees)
- `SetScale(const glm::vec3&)`
- `SetPosition2D(const glm::vec2&)` (UI position in pixels)
### UI helpers (Buttons/Sliders)
- `IsUIButtonPressed()`
- `IsUIInteractable()`, `SetUIInteractable(bool)`
- `GetUISliderValue()`, `SetUISliderValue(float)`
- `SetUISliderRange(float min, float max)`
- `SetUILabel(const std::string&)`, `SetUIColor(const glm::vec4&)`
- `GetUITextScale()`, `SetUITextScale(float)`
- `SetUISliderStyle(UISliderStyle)`
- `SetUIButtonStyle(UIButtonStyle)`
- `SetUIStylePreset(const std::string&)`
- `RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false)`
### Rigidbody helpers (3D)
- `HasRigidbody()`
- `SetRigidbodyVelocity(const glm::vec3&)`, `GetRigidbodyVelocity(glm::vec3& out)`
- `SetRigidbodyAngularVelocity(const glm::vec3&)`, `GetRigidbodyAngularVelocity(glm::vec3& out)`
- `AddRigidbodyForce(const glm::vec3&)`, `AddRigidbodyImpulse(const glm::vec3&)`
- `AddRigidbodyTorque(const glm::vec3&)`, `AddRigidbodyAngularImpulse(const glm::vec3&)`
- `SetRigidbodyRotation(const glm::vec3& rotDeg)`
- `TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)`
### Rigidbody2D helpers (UI/canvas only)
- `HasRigidbody2D()`
- `SetRigidbody2DVelocity(const glm::vec2&)`, `GetRigidbody2DVelocity(glm::vec2& out)`
### Audio helpers
- `HasAudioSource()`
- `PlayAudio()`, `StopAudio()`
- `SetAudioLoop(bool)`
- `SetAudioVolume(float)`
- `SetAudioClip(const std::string& path)`
### Settings + utility
- `GetSetting(key, fallback)`, `SetSetting(key, value)`
- `GetSettingBool(key, fallback)`, `SetSettingBool(key, value)`
- `GetSettingVec3(key, fallback)`, `SetSettingVec3(key, value)`
- `AutoSetting(key, bool|glm::vec3|buffer)`, `SaveAutoSettings()`
- `AddConsoleMessage(text, type)`
- `MarkDirty()`
## ImGui in scripts
Modularity uses Dear ImGui for editor UI. Scripts can draw ImGui in two places:
### Inspector UI (per object)
Export `Script_OnInspector(ScriptContext&)`:
```cpp ```cpp
static bool autoRotate = false; #include "ScriptRuntime.h"
static glm::vec3 speed = {0, 45, 0}; #include "ThirdParty/imgui/imgui.h"
void Script_OnInspector(ScriptContext& ctx) { static bool autoRotate = false;
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::Checkbox("Auto Rotate", &autoRotate); ImGui::Checkbox("Auto Rotate", &autoRotate);
ImGui::DragFloat3("Speed", &speed.x, 1.f, -360.f, 360.f);
ctx.MarkDirty(); ctx.MarkDirty();
} }
```
void Script_Begin(ScriptContext& ctx, float) { > Tip: `Script_OnInspector` must be exported exactly with `extern "C"` (it is not wrapper-generated).
ctx.MarkDirty(); // ensure initial state is saved > Important: Do not call ImGui functions (e.g., `ImGui::Text`) from `TickUpdate` or other runtime hooks. Those run before the ImGui frame is active and outside any window, which can crash.
### Scripted editor windows (custom tabs)
See [Scripted editor windows](#scripted-editor-windows).
## Per-script settings
Each `ScriptComponent` owns serialized key/value strings (`ctx.script->settings`). Use them to persist state with the scene.
### Direct settings
```cpp
void TickUpdate(ScriptContext& ctx, float) {
if (!ctx.script) return;
ctx.SetSetting("mode", "hard");
ctx.MarkDirty();
} }
```
void Script_TickUpdate(ScriptContext& ctx, float dt) { ### AutoSetting (recommended for inspector UI)
if (autoRotate && ctx.object) { `AutoSetting` binds a variable to a key and loads/saves automatically when you call `SaveAutoSettings()`.
ctx.SetRotation(ctx.object->rotation + speed * dt); ```cpp
extern "C" void Script_OnInspector(ScriptContext& ctx) {
static bool enabled = false;
ctx.AutoSetting("enabled", enabled);
ImGui::Checkbox("Enabled", &enabled);
ctx.SaveAutoSettings();
}
```
## UI scripting
UI elements are scene objects (Create -> 2D/UI). They render in the **Game Viewport** overlay.
### Button clicks
`IsUIButtonPressed()` is true only on the frame the click happens.
```cpp
void TickUpdate(ScriptContext& ctx, float) {
if (ctx.IsUIButtonPressed()) {
ctx.AddConsoleMessage("Button clicked!");
} }
} }
``` ```
## Runtime behavior ### Sliders as meters (health/ammo)
- Scripts tick for all objects every frame, even if not selected. Set `Interactable` to false to make a slider read-only.
- Spec/Test toggles are global (main menu → Scripts).
- Compile scripts via the UI “Compile Script” button or run the build command; wrapper generation is automatic.
## Rigidbody helper usage
- `SetRigidbodyAngularVelocity(vec3)` sets angular velocity in radians/sec for dynamic, non-kinematic bodies.
```cpp ```cpp
ctx.SetRigidbodyAngularVelocity({0.0f, 3.0f, 0.0f}); void TickUpdate(ScriptContext& ctx, float) {
``` ctx.SetUIInteractable(false);
- `GetRigidbodyAngularVelocity(out vec3)` reads current angular velocity into `out`. Returns false if unavailable. ctx.SetUISliderStyle(UISliderStyle::Fill);
```cpp ctx.SetUISliderRange(0.0f, 100.0f);
glm::vec3 angVel; ctx.SetUISliderValue(health);
if (ctx.GetRigidbodyAngularVelocity(angVel)) {
ctx.AddConsoleMessage("AngVel Y: " + std::to_string(angVel.y));
} }
``` ```
- `AddRigidbodyForce(vec3)` applies continuous force (mass-aware).
### Style presets
You can register custom ImGui style presets in code and then select them per UI element in the Inspector.
```cpp ```cpp
ctx.AddRigidbodyForce({0.0f, 0.0f, 25.0f}); void Begin(ScriptContext& ctx, float) {
ImGuiStyle style = ImGui::GetStyle();
style.Colors[ImGuiCol_Button] = ImVec4(0.20f, 0.50f, 0.90f, 1.00f);
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.25f, 0.60f, 1.00f, 1.00f);
ctx.RegisterUIStylePreset("Ocean", style, true);
}
``` ```
- `AddRigidbodyImpulse(vec3)` applies an instant impulse (mass-aware). Then select **UI -> Style Preset** on a button or slider.
### Finding other UI objects
```cpp ```cpp
ctx.AddRigidbodyImpulse({0.0f, 6.5f, 0.0f}); void TickUpdate(ScriptContext& ctx, float) {
if (SceneObject* other = ctx.FindObjectByName("UI Button 3")) {
if (other->type == ObjectType::UIButton && other->ui.buttonPressed) {
ctx.AddConsoleMessage("Other button clicked!");
}
}
}
``` ```
- `AddRigidbodyTorque(vec3)` applies continuous torque.
## IEnum tasks
Modularity provides lightweight, opt-in “tasks” you can start/stop per script component instance.
Important: In this version, an IEnum task is **just a function** with signature `void(ScriptContext&, float)` that is called every frame while its registered.
Start/stop macros:
- `IEnum_Start(fn)` / `IEnum_Stop(fn)` / `IEnum_Ensure(fn)`
Example (toggle rotation without cluttering TickUpdate):
```cpp ```cpp
ctx.AddRigidbodyTorque({0.0f, 15.0f, 0.0f}); static bool autoRotate = false;
``` static glm::vec3 speed = {0, 45, 0};
- `AddRigidbodyAngularImpulse(vec3)` applies an instant angular impulse.
```cpp static void RotateTask(ScriptContext& ctx, float dt) {
ctx.AddRigidbodyAngularImpulse({0.0f, 4.0f, 0.0f}); if (!ctx.object) return;
``` ctx.SetRotation(ctx.object->rotation + speed * dt);
- `SetRigidbodyRotation(vec3 degrees)` teleports the rigidbody rotation. }
```cpp
ctx.SetRigidbodyRotation({0.0f, 90.0f, 0.0f}); extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::Checkbox("Auto Rotate", &autoRotate);
if (autoRotate) IEnum_Ensure(RotateTask);
else IEnum_Stop(RotateTask);
ctx.MarkDirty();
}
``` ```
Notes: Notes:
- These return false if the object has no enabled rigidbody or is kinematic. - Tasks are stored per `ScriptComponent` instance.
- Use force/torque for continuous input and impulses for bursty actions. - Dont spam logs every frame inside a task; use “warn once” patterns.
- `SetRigidbodyRotation` is authoritative; use it sparingly during gameplay.
## Logging
Use `ctx.AddConsoleMessage(text, type)` to write to the editor console.
Typical types:
- `ConsoleMessageType::Info`
- `ConsoleMessageType::Success`
- `ConsoleMessageType::Warning`
- `ConsoleMessageType::Error`
Warn-once pattern:
```cpp
static bool warned = false;
if (!warned) {
ctx.AddConsoleMessage("[MyScript] Something looks off", ConsoleMessageType::Warning);
warned = true;
}
```
## Scripted editor windows
Scripts can expose ImGui-powered editor tabs by exporting:
- `RenderEditorWindow(ScriptContext& ctx)` (called every frame while tab is open)
- `ExitRenderEditorWindow(ScriptContext& ctx)` (called once when tab closes)
Example:
```cpp
#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
ImGui::TextUnformatted("Hello from script!");
if (ImGui::Button("Log")) {
ctx.AddConsoleMessage("Editor window clicked");
}
}
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
(void)ctx;
}
```
How to open:
1. Compile the script so the binary is updated under `Cache/ScriptBin/`.
2. In the main menu, go to **View -> Scripted Windows** and toggle the entry.
## Manual compile (CLI) ## Manual compile (CLI)
Linux example: Linux:
```bash ```bash
g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o
g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread
``` ```
Windows example:
Windows:
```bat ```bat
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj
link /nologo /DLL ..\\Cache\\ScriptBin\\SampleInspector.obj /OUT:..\\Cache\\ScriptBin\\SampleInspector.dll User32.lib Advapi32.lib link /nologo /DLL ..\\Cache\\ScriptBin\\SampleInspector.obj /OUT:..\\Cache\\ScriptBin\\SampleInspector.dll User32.lib Advapi32.lib
``` ```
## Troubleshooting
- **Script not running**
- Ensure the object is enabled and the script component is enabled.
- Ensure the script path points to a real file and the compiled binary exists.
- **No inspector UI**
- `Script_OnInspector` must be exported with `extern "C"` (no wrapper is generated for it).
- **Changes not saved**
- Call `ctx.MarkDirty()` after mutating transforms/settings you want to persist.
- **Editor window not showing**
- Ensure `RenderEditorWindow` is exported with `extern "C"` and the binary is up to date.
- **Custom UI style preset not listed**
- Ensure `RegisterUIStylePreset(...)` ran (e.g. in `Begin`) before selecting it in the Inspector.
- **Hard crash**
- Add null checks, avoid static pointers to scene objects, and dont hold references across frames unless you can validate them.
## Templates
### Minimal runtime script (wrapper-based)
```cpp
#include "ScriptRuntime.h"
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
if (!ctx.object) return;
}
```
### Minimal script with inspector (manual export)
```cpp
#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
if (!ctx.object) return;
}
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextDisabled("Hello from inspector!");
(void)ctx;
}
```
### Text
Use **UI Text** objects for on-screen text. Update their `label` and size from scripts:
```cpp
void TickUpdate(ScriptContext& ctx, float) {
if (SceneObject* text = ctx.FindObjectByName("UI Text 2")) {
if (text->type == ObjectType::UIText) {
text->ui.label = "Speed: 12.4";
text->ui.textScale = 1.4f;
ctx.MarkDirty();
}
}
}
```
### FPS display example
Attach `Scripts/FPSDisplay.cpp` to a **UI Text** object to show FPS. The inspector exposes a checkbox to clamp FPS to 120.

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 setVec2(const std::string &name, const glm::vec2 &value) const;
void setVec3(const std::string &name, const glm::vec3 &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 setMat4(const std::string &name, const glm::mat4 &mat) const;
void setMat4Array(const std::string &name, const glm::mat4 *data, int count) const;
private: private:
std::string readShaderFile(const char* filePath); std::string readShaderFile(const char* filePath);

View File

@@ -2,10 +2,143 @@
#include "../include/ThirdParty/miniaudio.h" #include "../include/ThirdParty/miniaudio.h"
#include "AudioSystem.h" #include "AudioSystem.h"
#include <cmath> #include <cmath>
#include <atomic>
#include <array>
namespace { namespace {
constexpr size_t kPreviewBuckets = 800; constexpr size_t kPreviewBuckets = 800;
constexpr ma_uint32 kPreviewChunkFrames = 2048; 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() { bool AudioSystem::init() {
@@ -15,6 +148,65 @@ bool AudioSystem::init() {
std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n"; std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n";
return false; 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; initialized = true;
return true; return true;
} }
@@ -22,6 +214,7 @@ bool AudioSystem::init() {
void AudioSystem::shutdown() { void AudioSystem::shutdown() {
stopPreview(); stopPreview();
destroyActiveSounds(); destroyActiveSounds();
shutdownReverbGraph();
if (initialized) { if (initialized) {
ma_engine_uninit(&engine); ma_engine_uninit(&engine);
initialized = false; initialized = false;
@@ -81,7 +274,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
&engine, &engine,
obj.audioSource.clipPath.c_str(), obj.audioSource.clipPath.c_str(),
MA_SOUND_FLAG_STREAM, MA_SOUND_FLAG_STREAM,
nullptr, reverbReady ? &reverbGroup : nullptr,
nullptr, nullptr,
&snd->sound &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_looping(&snd.sound, obj.audioSource.loop ? MA_TRUE : MA_FALSE);
ma_sound_set_volume(&snd.sound, obj.audioSource.volume); ma_sound_set_volume(&snd.sound, obj.audioSource.volume);
ma_sound_set_spatialization_enabled(&snd.sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE); 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_min_distance(&snd.sound, minDist);
ma_sound_set_max_distance(&snd.sound, maxDist); ma_sound_set_max_distance(&snd.sound, maxDist);
ma_sound_set_position(&snd.sound, obj.position.x, obj.position.y, obj.position.z); 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_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_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); ma_engine_listener_set_world_up(&engine, 0, listenerCamera.up.x, listenerCamera.up.y, listenerCamera.up.z);
updateReverb(objects, listenerCamera.position);
if (!playing) { if (!playing) {
destroyActiveSounds(); destroyActiveSounds();
@@ -144,6 +358,10 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
if (ensureSoundFor(obj)) { if (ensureSoundFor(obj)) {
refreshSoundParams(obj, *activeSounds[obj.id]); 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; 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 AudioSystem::loadPreview(const std::string& path) {
AudioClipPreview preview; AudioClipPreview preview;
preview.path = path; preview.path = path;

View File

@@ -42,7 +42,51 @@ public:
bool setObjectLoop(const SceneObject& obj, bool loop); bool setObjectLoop(const SceneObject& obj, bool loop);
bool setObjectVolume(const SceneObject& obj, float volume); 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: 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 { struct ActiveSound {
ma_sound sound; ma_sound sound;
std::string clipPath; std::string clipPath;
@@ -56,6 +100,12 @@ private:
std::unordered_map<std::string, AudioClipPreview> previewCache; std::unordered_map<std::string, AudioClipPreview> previewCache;
std::unordered_set<std::string> missingClips; std::unordered_set<std::string> missingClips;
SimpleReverbNode reverbNode{};
ma_splitter_node reverbSplitter{};
ma_sound_group reverbGroup{};
bool reverbReady = false;
ReverbSettings currentReverb{};
ma_sound previewSound{}; ma_sound previewSound{};
bool previewActive = false; bool previewActive = false;
std::string previewPath; std::string previewPath;
@@ -63,5 +113,10 @@ private:
void destroyActiveSounds(); void destroyActiveSounds();
bool ensureSoundFor(const SceneObject& obj); bool ensureSoundFor(const SceneObject& obj);
void refreshSoundParams(const SceneObject& obj, ActiveSound& snd); void refreshSoundParams(const SceneObject& obj, ActiveSound& snd);
float computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const;
AudioClipPreview loadPreview(const std::string& path); 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

@@ -1,6 +1,6 @@
#include "EditorUI.h" #include "EditorUI.h"
// FileBrowser implementation #pragma region File Browser
FileBrowser::FileBrowser() { FileBrowser::FileBrowser() {
currentPath = fs::current_path(); currentPath = fs::current_path();
projectRoot = currentPath; projectRoot = currentPath;
@@ -96,8 +96,9 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
// Model files // Model files
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" || if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" || ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".b3d" ||
ext == ".stl" || ext == ".x" || ext == ".md5mesh" || ext == ".rmesh") { ext == ".ply" || ext == ".stl" || ext == ".x" || ext == ".md5mesh" ||
ext == ".rmesh") {
return FileCategory::Model; return FileCategory::Model;
} }
@@ -183,10 +184,66 @@ bool FileBrowser::matchesFilter(const fs::directory_entry& entry) const {
return filenameLower.find(filterLower) != std::string::npos; return filenameLower.find(filterLower) != std::string::npos;
} }
#pragma endregion
#pragma region ImGui Theme
void applyModernTheme() { void applyModernTheme() {
ImGuiStyle& style = ImGui::GetStyle(); ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors; 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 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f); ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f);
@@ -251,20 +308,85 @@ void applyModernTheme() {
colors[ImGuiCol_NavHighlight] = accent; colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f); colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f); 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.ChildRounding = 12.0f;
style.FrameRounding = 10.0f; style.FrameRounding = 12.0f;
style.PopupRounding = 12.0f; style.PopupRounding = 12.0f;
style.GrabRounding = 12.0f;
style.ScrollbarSize = 11.0f;
style.ScrollbarRounding = 10.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.TabRounding = 10.0f;
style.WindowPadding = ImVec2(12.0f, 12.0f); style.TableAngledHeadersAngle = 35.0f;
style.FramePadding = ImVec2(10.0f, 6.0f); style.TableAngledHeadersTextAlign = ImVec2(0.50f, 0.00f);
style.ItemSpacing = ImVec2(10.0f, 8.0f);
style.ItemInnerSpacing = ImVec2(8.0f, 6.0f); style.TreeLinesFlags = ImGuiTreeNodeFlags_DrawLinesNone;
style.IndentSpacing = 18.0f; 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.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f; style.FrameBorderSize = 1.0f;
@@ -272,6 +394,31 @@ void applyModernTheme() {
style.TabBorderSize = 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
// Call once per frame before rendering editor panels.
void setupDockspace(const std::function<void()>& menuBarContent) { void setupDockspace(const std::function<void()>& menuBarContent) {
static bool dockspaceOpen = true; static bool dockspaceOpen = true;
static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_None; static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_None;
@@ -304,3 +451,4 @@ void setupDockspace(const std::function<void()>& menuBarContent) {
ImGui::End(); ImGui::End();
} }
#pragma endregion

View File

@@ -3,6 +3,8 @@
#include <functional> #include <functional>
#include "Common.h" #include "Common.h"
#pragma region File Browser Enums
enum class FileBrowserViewMode { enum class FileBrowserViewMode {
List, List,
Grid Grid
@@ -20,6 +22,9 @@ enum class FileCategory {
Text, Text,
Unknown Unknown
}; };
#pragma endregion
#pragma region File Browser
class FileBrowser { class FileBrowser {
public: public:
@@ -40,6 +45,7 @@ public:
FileBrowser(); FileBrowser();
// Call refresh after mutating currentPath/searchFilter/showHiddenFiles.
void refresh(); void refresh();
void navigateUp(); void navigateUp();
void navigateTo(const fs::path& path); void navigateTo(const fs::path& path);
@@ -57,9 +63,16 @@ public:
// Legacy compatibility // Legacy compatibility
bool isOBJFile(const fs::directory_entry& entry) const; bool isOBJFile(const fs::directory_entry& entry) const;
}; };
#pragma endregion
#pragma region Editor UI Helpers
// Apply the modern dark theme to ImGui // Apply the modern dark theme to ImGui
void applyModernTheme(); void applyModernTheme();
void applyEditorLayoutPreset(ImGuiStyle& style);
void applyPixelStyle(ImGuiStyle& style);
void applySuperRoundStyle(ImGuiStyle& style);
// Setup ImGui dockspace for the editor // Setup ImGui dockspace for the editor
void setupDockspace(const std::function<void()>& menuBarContent = nullptr); void setupDockspace(const std::function<void()>& menuBarContent = nullptr);
#pragma endregion

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

@@ -18,6 +18,7 @@
#include <shlobj.h> #include <shlobj.h>
#endif #endif
#pragma region Environment Window
void Engine::renderEnvironmentWindow() { void Engine::renderEnvironmentWindow() {
if (!showEnvironmentWindow) return; if (!showEnvironmentWindow) return;
ImGui::Begin("Environment", &showEnvironmentWindow); ImGui::Begin("Environment", &showEnvironmentWindow);
@@ -74,7 +75,9 @@ void Engine::renderEnvironmentWindow() {
ImGui::End(); ImGui::End();
} }
#pragma endregion
#pragma region Camera Window
void Engine::renderCameraWindow() { void Engine::renderCameraWindow() {
if (!showCameraWindow) return; if (!showCameraWindow) return;
ImGui::Begin("Camera", &showCameraWindow); ImGui::Begin("Camera", &showCameraWindow);
@@ -96,3 +99,4 @@ void Engine::renderCameraWindow() {
ImGui::End(); ImGui::End();
} }
#pragma endregion

View File

@@ -20,6 +20,7 @@
#include <shellapi.h> #include <shellapi.h>
#endif #endif
#pragma region File Icons
namespace FileIcons { namespace FileIcons {
namespace { namespace {
ImU32 BlendColor(ImU32 a, ImU32 b, float t) { ImU32 BlendColor(ImU32 a, ImU32 b, float t) {
@@ -401,7 +402,9 @@ namespace FileIcons {
} }
} }
} }
#pragma endregion
#pragma region File Actions
namespace { namespace {
enum class CreateKind { enum class CreateKind {
Folder, Folder,
@@ -475,8 +478,10 @@ namespace {
#endif #endif
} }
} }
#pragma endregion
#pragma region File Browser Panel
// Uses FileBrowser state for navigation, selection, and drag-drop.
void Engine::renderFileBrowserPanel() { void Engine::renderFileBrowserPanel() {
ImGui::Begin("Project", &showFileBrowser); ImGui::Begin("Project", &showFileBrowser);
ImGuiStyle& style = ImGui::GetStyle(); ImGuiStyle& style = ImGui::GetStyle();
@@ -496,6 +501,7 @@ void Engine::renderFileBrowserPanel() {
static fs::path pendingDeletePath; static fs::path pendingDeletePath;
static fs::path pendingRenamePath; static fs::path pendingRenamePath;
static char renameName[256] = ""; static char renameName[256] = "";
bool settingsDirty = false;
auto openEntry = [&](const fs::directory_entry& entry) { auto openEntry = [&](const fs::directory_entry& entry) {
if (entry.is_directory()) { if (entry.is_directory()) {
@@ -531,6 +537,10 @@ void Engine::renderFileBrowserPanel() {
logToConsole("Loaded scene: " + sceneName); logToConsole("Loaded scene: " + sceneName);
return; return;
} }
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
openScriptInEditor(entry.path());
return;
}
openPathInShell(entry.path()); openPathInShell(entry.path());
}; };
@@ -619,6 +629,15 @@ void Engine::renderFileBrowserPanel() {
return false; 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 // Get colors for categories
auto getCategoryColor = [](FileCategory cat) -> ImU32 { auto getCategoryColor = [](FileCategory cat) -> ImU32 {
switch (cat) { switch (cat) {
@@ -737,16 +756,24 @@ void Engine::renderFileBrowserPanel() {
ImGui::TextDisabled("Size"); ImGui::TextDisabled("Size");
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(90); 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); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
ImGui::SameLine(); ImGui::SameLine();
} }
if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) { if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid; fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
settingsDirty = true;
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View"); if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
ImGui::SameLine(); 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::EndChild();
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
@@ -761,6 +788,159 @@ void Engine::renderFileBrowserPanel() {
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f); contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg); ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
ImGui::BeginChild("FileContent", ImVec2(0, 0), true); 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(); ImDrawList* drawList = ImGui::GetWindowDrawList();
@@ -1163,9 +1343,14 @@ void Engine::renderFileBrowserPanel() {
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::EndChild();
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
if (settingsDirty) {
saveEditorUserSettings();
}
if (triggerDeletePopup) { if (triggerDeletePopup) {
ImGui::OpenPopup("Confirm Delete"); ImGui::OpenPopup("Confirm Delete");
triggerDeletePopup = false; triggerDeletePopup = false;
@@ -1242,3 +1427,4 @@ void Engine::renderFileBrowserPanel() {
ImGui::End(); ImGui::End();
} }
#pragma endregion

View File

@@ -18,6 +18,7 @@
#include <shlobj.h> #include <shlobj.h>
#endif #endif
#pragma region ImGui Helpers
namespace ImGui { namespace ImGui {
// Animated progress bar that keeps circles moving while work happens in the background. // Animated progress bar that keeps circles moving while work happens in the background.
@@ -102,8 +103,38 @@ bool Spinner(const char* label, float radius, int thickness, const ImU32& color)
return true; return true;
} }
} // namespace ImGui 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
#pragma region Package Task State
namespace { namespace {
struct PackageTaskResult { struct PackageTaskResult {
bool success = false; bool success = false;
@@ -117,8 +148,9 @@ struct PackageTaskState {
std::future<PackageTaskResult> future; std::future<PackageTaskResult> future;
}; };
} // namespace } // namespace
#pragma endregion
#pragma region Launcher
void Engine::renderLauncher() { void Engine::renderLauncher() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImVec2 displaySize = io.DisplaySize; ImVec2 displaySize = io.DisplaySize;
@@ -184,7 +216,7 @@ void Engine::renderLauncher() {
ImGui::SetWindowFontScale(1.4f); ImGui::SetWindowFontScale(1.4f);
ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity"); ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity");
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Debug Build V0.7.0"); ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V6.3");
ImGui::EndChild(); ImGui::EndChild();
@@ -345,13 +377,10 @@ void Engine::renderLauncher() {
ImGui::Spacing(); ImGui::Spacing();
} }
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextDisabled("Modularity Engine - Beta V6.3");
ImGui::TextDisabled("Modularity Engine - Version 0.6.8");
ImGui::EndChild(); ImGui::EndChild();
} }
@@ -363,8 +392,74 @@ void Engine::renderLauncher() {
renderNewProjectDialog(); renderNewProjectDialog();
if (projectManager.showOpenProjectDialog) if (projectManager.showOpenProjectDialog)
renderOpenProjectDialog(); renderOpenProjectDialog();
}
if (projectLoadInProgress || sceneLoadInProgress) {
float elapsed = static_cast<float>(glfwGetTime() - projectLoadStartTime);
if (elapsed > 0.15f) {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(io.DisplaySize);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.65f));
ImGui::Begin("ProjectLoadOverlay", nullptr,
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoInputs);
ImGui::End();
ImGui::PopStyleColor();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(420, 160));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 20.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.13f, 0.18f, 0.98f));
ImGui::Begin("ProjectLoadCard", nullptr,
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
const char* headline = sceneLoadInProgress ? "Loading scene..." : "Loading project...";
ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "%s", headline);
ImGui::Spacing();
if (sceneLoadInProgress && !sceneLoadStatus.empty()) {
ImGui::TextDisabled("%s", sceneLoadStatus.c_str());
} else if (!projectLoadPath.empty()) {
ImGui::TextDisabled("%s", projectLoadPath.c_str());
}
ImGui::Spacing();
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();
ImGui::PopStyleVar(2);
}
}
}
#pragma endregion
#pragma region New Project Dialog
void Engine::renderNewProjectDialog() { void Engine::renderNewProjectDialog() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
@@ -441,7 +536,9 @@ void Engine::renderNewProjectDialog() {
} }
ImGui::End(); ImGui::End();
} }
#pragma endregion
#pragma region Open Project Dialog
void Engine::renderOpenProjectDialog() { void Engine::renderOpenProjectDialog() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
@@ -497,7 +594,9 @@ void Engine::renderOpenProjectDialog() {
} }
ImGui::End(); ImGui::End();
} }
#pragma endregion
#pragma region Project Browser Panel
void Engine::renderProjectBrowserPanel() { void Engine::renderProjectBrowserPanel() {
ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f); ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f);
ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f); ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f);
@@ -793,3 +892,4 @@ void Engine::renderProjectBrowserPanel() {
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
} }
#pragma endregion

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,10 +12,19 @@
#include "PhysicsSystem.h" #include "PhysicsSystem.h"
#include "AudioSystem.h" #include "AudioSystem.h"
#include "PackageManager.h" #include "PackageManager.h"
#include "ManagedScriptRuntime.h"
#include "ThirdParty/ImGuiColorTextEdit/TextEditor.h"
#include "../include/Window/Window.h" #include "../include/Window/Window.h"
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <atomic>
#include <deque>
#include <future>
#include <mutex>
#include <thread>
void window_size_callback(GLFWwindow* window, int width, int height); void window_size_callback(GLFWwindow* window, int width, int height);
fs::path resolveScriptsConfigPath(const Project& project);
class Engine { class Engine {
private: private:
@@ -68,7 +77,15 @@ private:
bool showConsole = true; bool showConsole = true;
bool showProjectBrowser = true; // Now merged into file browser bool showProjectBrowser = true; // Now merged into file browser
bool showMeshBuilder = false; bool showMeshBuilder = false;
bool showBuildSettings = false;
bool showStyleEditor = false;
bool showScriptingWindow = false;
bool firstFrame = true; bool firstFrame = true;
bool playerMode = false;
bool autoStartRequested = false;
bool autoStartPlayerMode = false;
std::string autoStartProjectPath;
std::string autoStartSceneName;
std::vector<std::string> consoleLog; std::vector<std::string> consoleLog;
int draggedObjectId = -1; int draggedObjectId = -1;
@@ -97,8 +114,34 @@ private:
char fileBrowserSearch[256] = ""; char fileBrowserSearch[256] = "";
float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range 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 showEnvironmentWindow = true;
bool showCameraWindow = 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 hierarchyShowTexturePreview = false;
bool hierarchyPreviewNearest = false; bool hierarchyPreviewNearest = false;
std::unordered_map<std::string, bool> texturePreviewFilterOverrides; std::unordered_map<std::string, bool> texturePreviewFilterOverrides;
@@ -113,32 +156,188 @@ private:
int previewCameraId = -1; int previewCameraId = -1;
bool gameViewCursorLocked = false; bool gameViewCursorLocked = false;
bool gameViewportFocused = false; bool gameViewportFocused = false;
bool showUITextOverlay = false; bool showGameProfiler = true;
bool showCanvasOverlay = false; bool showCanvasOverlay = false;
bool showUIWorldGrid = true;
bool showSceneGrid3D = false;
int gameViewportResolutionIndex = 0;
int gameViewportCustomWidth = 1920;
int gameViewportCustomHeight = 1080;
float gameViewportZoom = 1.0f;
bool gameViewportAutoFit = true;
int activePlayerId = -1; int activePlayerId = -1;
MeshBuilder meshBuilder; MeshBuilder meshBuilder;
char meshBuilderPath[260] = ""; char meshBuilderPath[260] = "";
char meshBuilderFaceInput[128] = ""; char meshBuilderFaceInput[128] = "";
bool meshEditMode = false; bool meshEditMode = false;
bool meshEditLoaded = false; bool meshEditLoaded = false;
bool meshEditDirty = false;
bool meshEditExtrudeMode = false;
std::string meshEditPath; std::string meshEditPath;
RawMeshAsset meshEditAsset; RawMeshAsset meshEditAsset;
std::vector<int> meshEditSelectedVertices; std::vector<int> meshEditSelectedVertices;
std::vector<int> meshEditSelectedEdges; // indices into generated edge list std::vector<int> meshEditSelectedEdges; // indices into generated edge list
std::vector<int> meshEditSelectedFaces; // indices into mesh faces 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 }; enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex; MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
ScriptCompiler scriptCompiler; ScriptCompiler scriptCompiler;
ScriptRuntime scriptRuntime; ScriptRuntime scriptRuntime;
ManagedScriptRuntime managedRuntime;
PhysicsSystem physics; PhysicsSystem physics;
AudioSystem audio; AudioSystem audio;
bool showCompilePopup = false; bool showCompilePopup = false;
bool compilePopupOpened = false;
double compilePopupHideTime = 0.0;
bool lastCompileSuccess = false; bool lastCompileSuccess = false;
std::string lastCompileStatus; std::string lastCompileStatus;
std::string lastCompileLog; 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;
std::string compileLog;
std::string linkLog;
std::string error;
};
std::atomic<bool> compileInProgress = false;
std::atomic<bool> compileResultReady = false;
std::thread compileWorker;
std::mutex compileMutex;
ScriptCompileJobResult compileResult;
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 {
bool success = false;
Project project;
std::string error;
std::string path;
};
bool projectLoadInProgress = false;
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 specMode = false;
bool testMode = false; bool testMode = false;
bool collisionWireframe = false; bool collisionWireframe = false;
bool fpsCapEnabled = false;
float fpsCap = 120.0f;
struct UIStylePreset {
std::string name;
ImGuiStyle style;
bool builtin = false;
};
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 // Private methods
SceneObject* getSelectedObject(); SceneObject* getSelectedObject();
glm::vec3 getSelectionCenterWorld(bool worldSpace) const; glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
@@ -154,8 +353,10 @@ private:
void importOBJToScene(const std::string& filepath, const std::string& objectName); void importOBJToScene(const std::string& filepath, const std::string& objectName);
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
void convertModelToRawMesh(const std::string& filepath); void convertModelToRawMesh(const std::string& filepath);
void createRMeshPrimitive(const std::string& primitiveName);
bool ensureMeshEditTarget(SceneObject* obj); bool ensureMeshEditTarget(SceneObject* obj);
bool syncMeshEditToGPU(SceneObject* obj); bool syncMeshEditToGPU(SceneObject* obj);
bool saveMeshEditAsset(std::string& error);
void handleKeyboardShortcuts(); void handleKeyboardShortcuts();
void OpenProjectPath(const std::string& path); void OpenProjectPath(const std::string& path);
@@ -167,6 +368,7 @@ private:
void renderPlayControlsBar(); void renderPlayControlsBar();
void renderEnvironmentWindow(); void renderEnvironmentWindow();
void renderCameraWindow(); void renderCameraWindow();
void renderAnimationWindow();
void renderHierarchyPanel(); void renderHierarchyPanel();
void renderObjectNode(SceneObject& obj, const std::string& filter, void renderObjectNode(SceneObject& obj, const std::string& filter,
std::vector<bool>& ancestorHasNext, bool isLast, int depth); std::vector<bool>& ancestorHasNext, bool isLast, int depth);
@@ -175,15 +377,56 @@ private:
void renderInspectorPanel(); void renderInspectorPanel();
void renderConsolePanel(); void renderConsolePanel();
void renderViewport(); void renderViewport();
void renderPlayerViewport();
void renderGameViewportWindow(); void renderGameViewportWindow();
void renderBuildSettingsWindow();
void renderScriptingWindow();
void renderDialogs(); void renderDialogs();
void updateCompileJob();
void renderProjectBrowserPanel(); void renderProjectBrowserPanel();
void renderScriptEditorWindows(); void renderScriptEditorWindows();
void refreshScriptEditorWindows(); void refreshScriptEditorWindows();
void refreshScriptingFileList();
Camera makeCameraFromObject(const SceneObject& obj) const; Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath); void compileScriptFile(const fs::path& scriptPath);
void updateAutoCompileScripts();
void processAutoCompileQueue();
void queueAutoCompile(const fs::path& scriptPath, const fs::file_time_type& sourceTime);
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 updateScripts(float delta);
void updatePlayerController(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 renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb(); void renderFileBrowserBreadcrumb();
@@ -191,6 +434,7 @@ private:
void renderFileBrowserListView(); void renderFileBrowserListView();
void renderFileContextMenu(const fs::directory_entry& entry); void renderFileContextMenu(const fs::directory_entry& entry);
void handleFileDoubleClick(const fs::directory_entry& entry); void handleFileDoubleClick(const fs::directory_entry& entry);
void openScriptInEditor(const fs::path& path);
ImVec4 getFileCategoryColor(FileCategory category) const; ImVec4 getFileCategoryColor(FileCategory category) const;
const char* getFileCategoryIconText(FileCategory category) const; const char* getFileCategoryIconText(FileCategory category) const;
@@ -241,6 +485,10 @@ public:
SceneObject* findObjectByName(const std::string& name); SceneObject* findObjectByName(const std::string& name);
SceneObject* findObjectById(int id); SceneObject* findObjectById(int id);
fs::path resolveScriptBinary(const fs::path& sourcePath); 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(); void markProjectDirty();
// Script-accessible logging wrapper // Script-accessible logging wrapper
void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type); void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type);
@@ -265,4 +513,7 @@ public:
bool setAudioVolumeFromScript(int id, float volume); bool setAudioVolumeFromScript(int id, float volume);
bool setAudioClipFromScript(int id, const std::string& path); bool setAudioClipFromScript(int id, const std::string& path);
void syncLocalTransform(SceneObject& obj); void syncLocalTransform(SceneObject& obj);
const std::vector<UIStylePreset>& getUIStylePresets() const { return uiStylePresets; }
void registerUIStylePresetFromScript(const std::string& name, const ImGuiStyle& style, bool replace = false);
void setFrameRateCapFromScript(bool enabled, float cap);
}; };

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

@@ -1,5 +1,6 @@
#include "MeshBuilder.h" #include "MeshBuilder.h"
#pragma region State Reset
void MeshBuilder::clear() { void MeshBuilder::clear() {
mesh = RawMeshAsset(); mesh = RawMeshAsset();
hasMesh = false; hasMesh = false;
@@ -7,7 +8,9 @@ void MeshBuilder::clear() {
selectedVertex = -1; selectedVertex = -1;
loadedPath.clear(); loadedPath.clear();
} }
#pragma endregion
#pragma region File IO
bool MeshBuilder::load(const std::string& path, std::string& error) { bool MeshBuilder::load(const std::string& path, std::string& error) {
auto& loader = getModelLoader(); auto& loader = getModelLoader();
RawMeshAsset loaded; RawMeshAsset loaded;
@@ -35,7 +38,9 @@ bool MeshBuilder::save(const std::string& path, std::string& error) {
dirty = false; dirty = false;
return true; return true;
} }
#pragma endregion
#pragma region Geometry Editing
void MeshBuilder::recomputeNormals() { void MeshBuilder::recomputeNormals() {
if (mesh.positions.empty()) return; if (mesh.positions.empty()) return;
mesh.normals.assign(mesh.positions.size(), glm::vec3(0.0f)); mesh.normals.assign(mesh.positions.size(), glm::vec3(0.0f));
@@ -103,3 +108,4 @@ bool MeshBuilder::addFace(const std::vector<uint32_t>& indices, std::string& err
dirty = true; dirty = true;
return true; return true;
} }
#pragma endregion

View File

@@ -3,6 +3,7 @@
#include "Common.h" #include "Common.h"
#include "ModelLoader.h" #include "ModelLoader.h"
#pragma region Mesh Builder State
// Lightweight mesh editing state used by the MeshBuilder panel. // Lightweight mesh editing state used by the MeshBuilder panel.
class MeshBuilder { class MeshBuilder {
public: public:
@@ -11,7 +12,9 @@ public:
std::string loadedPath; std::string loadedPath;
bool dirty = false; bool dirty = false;
int selectedVertex = -1; int selectedVertex = -1;
#pragma endregion
#pragma region Mesh Builder API
bool load(const std::string& path, std::string& error); bool load(const std::string& path, std::string& error);
bool save(const std::string& path, std::string& error); bool save(const std::string& path, std::string& error);
void clear(); void clear();
@@ -20,3 +23,4 @@ public:
// Add a new face defined by vertex indices (3 = triangle, 4 = quad fan). // Add a new face defined by vertex indices (3 = triangle, 4 = quad fan).
bool addFace(const std::vector<uint32_t>& indices, std::string& error); bool addFace(const std::vector<uint32_t>& indices, std::string& error);
}; };
#pragma endregion

View File

@@ -4,6 +4,10 @@
#include <fstream> #include <fstream>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <unordered_set>
#include <functional>
#include <assimp/material.h>
#include "ThirdParty/glm/gtc/quaternion.hpp"
ModelLoader& ModelLoader::getInstance() { ModelLoader& ModelLoader::getInstance() {
static ModelLoader instance; static ModelLoader instance;
@@ -11,6 +15,13 @@ ModelLoader& ModelLoader::getInstance() {
} }
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out); 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() { ModelLoader& getModelLoader() {
return ModelLoader::getInstance(); return ModelLoader::getInstance();
@@ -91,6 +102,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
if (loadedMeshes[i].path == filepath) { if (loadedMeshes[i].path == filepath) {
result.success = true; result.success = true;
result.meshIndex = static_cast<int>(i); result.meshIndex = static_cast<int>(i);
result.meshIndices.push_back(result.meshIndex);
const auto& mesh = loadedMeshes[i]; const auto& mesh = loadedMeshes[i];
result.vertexCount = mesh.vertexCount; result.vertexCount = mesh.vertexCount;
result.faceCount = mesh.faceCount; result.faceCount = mesh.faceCount;
@@ -273,6 +285,92 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
return result; 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) { bool ModelLoader::exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg) {
fs::path inPath(inputFile); fs::path inPath(inputFile);
if (!fs::exists(inPath)) { if (!fs::exists(inPath)) {
@@ -350,17 +448,51 @@ bool ModelLoader::loadRawMesh(const std::string& filepath, RawMeshAsset& out, st
return false; 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.boundsMin.x), sizeof(float) * 3);
in.read(reinterpret_cast<char*>(&out.boundsMax.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.positions.resize(header.vertexCount);
out.normals.resize(header.vertexCount);
out.uvs.resize(header.vertexCount);
out.faces.resize(header.faceCount); 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.positions.data()), sizeof(glm::vec3) * out.positions.size());
in.read(reinterpret_cast<char*>(out.normals.data()), sizeof(glm::vec3) * out.normals.size()); if (hasNormals) {
in.read(reinterpret_cast<char*>(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size()); 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()); in.read(reinterpret_cast<char*>(out.faces.data()), sizeof(glm::u32vec3) * out.faces.size());
if (!in.good()) { if (!in.good()) {
@@ -435,6 +567,18 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file
outPath.replace_extension(".rmesh"); 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 { struct Header {
char magic[6] = {'R','M','E','S','H','\0'}; char magic[6] = {'R','M','E','S','H','\0'};
uint32_t version = 1; 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.boundsMin.x), sizeof(float) * 3);
out.write(reinterpret_cast<const char*>(&asset.boundsMax.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.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*>(normalsData.data()), sizeof(glm::vec3) * normalsData.size());
out.write(reinterpret_cast<const char*>(asset.uvs.data()), sizeof(glm::vec2) * asset.uvs.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()); out.write(reinterpret_cast<const char*>(asset.faces.data()), sizeof(glm::u32vec3) * asset.faces.size());
if (!out.good()) { if (!out.good()) {
@@ -544,6 +688,84 @@ bool ModelLoader::updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::s
return true; 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) { static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
return glm::mat4( return glm::mat4(
m.a1, m.b1, m.c1, m.d1, m.a1, m.b1, m.c1, m.d1,
@@ -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) { static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out) {
aiMatrix4x4 current = parentTransform * node->mTransformation; aiMatrix4x4 current = parentTransform * node->mTransformation;
glm::mat4 gTransform = aiToGlm(current); 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, void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform,
std::vector<float>& vertices, std::vector<glm::vec3>& triPositions, std::vector<float>& vertices, std::vector<glm::vec3>& triPositions,
std::vector<glm::vec3>& positions, std::vector<uint32_t>& indices, 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() { void ModelLoader::clear() {
loadedMeshes.clear(); loadedMeshes.clear();
cachedScenes.clear();
} }
size_t ModelLoader::getMeshCount() const { size_t ModelLoader::getMeshCount() const {

View File

@@ -3,6 +3,7 @@
#include "Common.h" #include "Common.h"
#include "Rendering.h" #include "Rendering.h"
#include <cstdint> #include <cstdint>
#include <unordered_map>
#include <assimp/Importer.hpp> #include <assimp/Importer.hpp>
#include <assimp/scene.h> #include <assimp/scene.h>
@@ -19,6 +20,7 @@ struct ModelFormat {
struct ModelLoadResult { struct ModelLoadResult {
bool success = false; bool success = false;
int meshIndex = -1; int meshIndex = -1;
std::vector<int> meshIndices;
std::string errorMessage; std::string errorMessage;
int vertexCount = 0; int vertexCount = 0;
int faceCount = 0; int faceCount = 0;
@@ -41,6 +43,51 @@ struct RawMeshAsset {
bool hasUVs = false; 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 { class ModelLoader {
public: public:
// Singleton access // Singleton access
@@ -49,6 +96,9 @@ public:
// Load a model file (FBX, OBJ, GLTF, etc.) // Load a model file (FBX, OBJ, GLTF, etc.)
ModelLoadResult loadModel(const std::string& filepath); 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 // Get mesh by index
Mesh* getMesh(int index); Mesh* getMesh(int index);
@@ -73,6 +123,16 @@ public:
// Update an already-loaded raw mesh in GPU memory // Update an already-loaded raw mesh in GPU memory
bool updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg); 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 // Get list of supported formats
static std::vector<ModelFormat> getSupportedFormats(); static std::vector<ModelFormat> getSupportedFormats();
@@ -100,6 +160,7 @@ private:
// Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure) // Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure)
std::vector<OBJLoader::LoadedMesh> loadedMeshes; std::vector<OBJLoader::LoadedMesh> loadedMeshes;
std::unordered_map<std::string, ModelSceneData> cachedScenes;
// Assimp importer (kept for resource management) // Assimp importer (kept for resource management)
Assimp::Importer importer; Assimp::Importer importer;

View File

@@ -5,6 +5,7 @@
#include <cstdio> #include <cstdio>
#include <sstream> #include <sstream>
#pragma region Local Path Helpers
namespace { namespace {
fs::path normalizePath(const fs::path& p) { fs::path normalizePath(const fs::path& p) {
std::error_code ec; std::error_code ec;
@@ -59,7 +60,9 @@ fs::path guessIncludeDir(const fs::path& repoRoot, const std::string& includeRel
return normalizePath(repoRoot); return normalizePath(repoRoot);
} }
} // namespace } // namespace
#pragma endregion
#pragma region Lifecycle
PackageManager::PackageManager() { PackageManager::PackageManager() {
buildRegistry(); buildRegistry();
} }
@@ -70,7 +73,9 @@ void PackageManager::setProjectRoot(const fs::path& root) {
manifestPath = projectRoot / "packages.modu"; manifestPath = projectRoot / "packages.modu";
loadManifest(); loadManifest();
} }
#pragma endregion
#pragma region Install / Remove
bool PackageManager::isInstalled(const std::string& id) const { bool PackageManager::isInstalled(const std::string& id) const {
return std::find(installedIds.begin(), installedIds.end(), id) != installedIds.end(); return std::find(installedIds.begin(), installedIds.end(), id) != installedIds.end();
} }
@@ -135,7 +140,9 @@ bool PackageManager::remove(const std::string& id) {
saveManifest(); saveManifest();
return true; return true;
} }
#pragma endregion
#pragma region Build Config
void PackageManager::applyToBuildConfig(ScriptBuildConfig& config) const { void PackageManager::applyToBuildConfig(ScriptBuildConfig& config) const {
std::unordered_set<std::string> defineSet(config.defines.begin(), config.defines.end()); std::unordered_set<std::string> defineSet(config.defines.begin(), config.defines.end());
std::unordered_set<std::string> linuxLibSet(config.linuxLinkLibs.begin(), config.linuxLinkLibs.end()); std::unordered_set<std::string> linuxLibSet(config.linuxLinkLibs.begin(), config.linuxLinkLibs.end());
@@ -167,7 +174,9 @@ void PackageManager::applyToBuildConfig(ScriptBuildConfig& config) const {
} }
} }
} }
#pragma endregion
#pragma region Registry
void PackageManager::buildRegistry() { void PackageManager::buildRegistry() {
registry.clear(); registry.clear();
fs::path engineRoot = fs::current_path(); fs::path engineRoot = fs::current_path();
@@ -230,7 +239,9 @@ void PackageManager::buildRegistry() {
miniaudio.includeDirs = { engineRoot / "include/ThirdParty" }; miniaudio.includeDirs = { engineRoot / "include/ThirdParty" };
add(miniaudio); add(miniaudio);
} }
#pragma endregion
#pragma region Manifest IO
void PackageManager::loadManifest() { void PackageManager::loadManifest() {
installedIds.clear(); installedIds.clear();
for (const auto& pkg : registry) { for (const auto& pkg : registry) {
@@ -336,7 +347,9 @@ void PackageManager::saveManifest() const {
file << join(pkg->windowsLibs, ';') << "\n"; file << join(pkg->windowsLibs, ';') << "\n";
} }
} }
#pragma endregion
#pragma region Registry Lookup
const PackageInfo* PackageManager::findPackage(const std::string& id) const { const PackageInfo* PackageManager::findPackage(const std::string& id) const {
auto it = std::find_if(registry.begin(), registry.end(), [&](const PackageInfo& p) { auto it = std::find_if(registry.begin(), registry.end(), [&](const PackageInfo& p) {
return p.id == id; return p.id == id;
@@ -348,7 +361,9 @@ bool PackageManager::isBuiltIn(const std::string& id) const {
const PackageInfo* pkg = findPackage(id); const PackageInfo* pkg = findPackage(id);
return pkg && pkg->builtIn; return pkg && pkg->builtIn;
} }
#pragma endregion
#pragma region Utility Helpers
std::string PackageManager::trim(const std::string& value) { std::string PackageManager::trim(const std::string& value) {
size_t start = 0; size_t start = 0;
while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) start++; while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) start++;
@@ -406,8 +421,14 @@ std::string PackageManager::join(const std::vector<std::string>& vals, char deli
} }
return oss.str(); return oss.str();
} }
#pragma endregion
#pragma region External Packages
fs::path PackageManager::packagesFolder() const { fs::path PackageManager::packagesFolder() const {
fs::path newFolder = projectRoot / "Library" / "InstalledPackages";
if (fs::exists(newFolder) || fs::exists(projectRoot / "scripts.modu")) {
return newFolder;
}
return projectRoot / "Packages"; return projectRoot / "Packages";
} }
@@ -512,3 +533,4 @@ bool PackageManager::updateGitPackage(const std::string& id, std::string& outLog
} }
return true; return true;
} }
#pragma endregion

View File

@@ -16,8 +16,12 @@ PxVec3 ToPxVec3(const glm::vec3& v) {
} }
PxQuat ToPxQuat(const glm::vec3& eulerDeg) { PxQuat ToPxQuat(const glm::vec3& eulerDeg) {
glm::vec3 radians = glm::radians(eulerDeg); glm::vec3 r = glm::radians(eulerDeg);
glm::quat q = glm::quat(radians); glm::mat4 m(1.0f);
m = glm::rotate(m, r.x, glm::vec3(1.0f, 0.0f, 0.0f));
m = glm::rotate(m, r.y, glm::vec3(0.0f, 1.0f, 0.0f));
m = glm::rotate(m, r.z, glm::vec3(0.0f, 0.0f, 1.0f));
glm::quat q = glm::quat_cast(glm::mat3(m));
return PxQuat(q.x, q.y, q.z, q.w); return PxQuat(q.x, q.y, q.z, q.w);
} }
@@ -25,9 +29,20 @@ glm::vec3 ToGlmVec3(const PxVec3& v) {
return glm::vec3(v.x, v.y, v.z); return glm::vec3(v.x, v.y, v.z);
} }
glm::vec3 ExtractEulerXYZ(const glm::mat3& m) {
float T1 = std::atan2(m[2][1], m[2][2]);
float C2 = std::sqrt(m[0][0] * m[0][0] + m[1][0] * m[1][0]);
float T2 = std::atan2(-m[2][0], C2);
float S1 = std::sin(T1);
float C1 = std::cos(T1);
float T3 = std::atan2(S1 * m[0][2] - C1 * m[0][1], C1 * m[1][1] - S1 * m[1][2]);
return glm::vec3(-T1, -T2, -T3);
}
glm::vec3 ToGlmEulerDeg(const PxQuat& q) { glm::vec3 ToGlmEulerDeg(const PxQuat& q) {
glm::quat gq(q.w, q.x, q.y, q.z); glm::quat gq(q.w, q.x, q.y, q.z);
return glm::degrees(glm::eulerAngles(gq)); glm::mat3 m = glm::mat3_cast(gq);
return glm::degrees(ExtractEulerXYZ(m));
} }
} // namespace } // namespace
@@ -107,9 +122,9 @@ void PhysicsSystem::createGroundPlane() {
bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector<PxVec3>& vertices, std::vector<uint32_t>& indices) const { bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector<PxVec3>& vertices, std::vector<uint32_t>& indices) const {
const OBJLoader::LoadedMesh* meshInfo = nullptr; 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); 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); meshInfo = getModelLoader().getMeshInfo(obj.meshId);
} }
if (!meshInfo) { if (!meshInfo) {
@@ -200,21 +215,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
s->setRestOffset(rest); s->setRestOffset(rest);
}; };
switch (obj.type) { switch (obj.renderType) {
case ObjectType::Cube: { case RenderType::Cube: {
PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f))); PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f)));
shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true); shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break; break;
} }
case ObjectType::Sphere: { case RenderType::Sphere: {
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
radius = std::max(radius, 0.01f); radius = std::max(radius, 0.01f);
shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true); shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true);
tuneShape(shape, radius * 2.0f, isDynamic); tuneShape(shape, radius * 2.0f, isDynamic);
break; break;
} }
case ObjectType::Capsule: { case RenderType::Capsule: {
float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f; float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f;
radius = std::max(radius, 0.01f); radius = std::max(radius, 0.01f);
float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f); float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f);
@@ -227,14 +242,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic); tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic);
break; break;
} }
case ObjectType::Plane: { case RenderType::Plane: {
glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f)); glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f));
halfExtents.z = std::max(halfExtents.z, 0.01f); halfExtents.z = std::max(halfExtents.z, 0.01f);
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true); shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break; break;
} }
case ObjectType::Torus: { 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 RenderType::Torus: {
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
radius = std::max(radius, 0.01f); radius = std::max(radius, 0.01f);
shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true); shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true);
@@ -280,9 +302,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
minDim = std::min(radius * 2.0f, halfHeight * 2.0f); minDim = std::min(radius * 2.0f, halfHeight * 2.0f);
} else { } else {
const OBJLoader::LoadedMesh* meshInfo = nullptr; 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); 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); meshInfo = getModelLoader().getMeshInfo(obj.meshId);
} }
if (!meshInfo) { if (!meshInfo) {
@@ -469,7 +491,6 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!isReady()) return; if (!isReady()) return;
clearActors(); clearActors();
createGroundPlane();
struct MeshCookInfo { struct MeshCookInfo {
std::string name; std::string name;
@@ -484,9 +505,9 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!obj.enabled || !obj.hasCollider || !obj.collider.enabled) continue; if (!obj.enabled || !obj.hasCollider || !obj.collider.enabled) continue;
if (obj.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue; if (obj.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue;
const OBJLoader::LoadedMesh* meshInfo = nullptr; 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); 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); meshInfo = getModelLoader().getMeshInfo(obj.meshId);
} }
if (!meshInfo) continue; if (!meshInfo) continue;

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ public:
std::string currentSceneName; std::string currentSceneName;
bool isLoaded = false; bool isLoaded = false;
bool hasUnsavedChanges = false; bool hasUnsavedChanges = false;
bool usesNewLayout = false;
Project() = default; Project() = default;
Project(const std::string& projectName, const fs::path& basePath); Project(const std::string& projectName, const fs::path& basePath);
@@ -55,10 +56,18 @@ class SceneSerializer {
public: public:
static bool saveScene(const fs::path& filePath, static bool saveScene(const fs::path& filePath,
const std::vector<SceneObject>& objects, const std::vector<SceneObject>& objects,
int nextId); int nextId,
float timeOfDay);
static bool loadScene(const fs::path& filePath, static bool loadScene(const fs::path& filePath,
std::vector<SceneObject>& objects, std::vector<SceneObject>& objects,
int& nextId, 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 // Cube vertex data
float vertices[] = { float vertices[] = {
// Back face (z = -0.5f) // 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, 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, 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) // Front 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, 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, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Right face (x = 0.5f) // 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, 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, 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) // Bottom 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, 0.0f, 1.0f,
@@ -54,11 +54,11 @@ float vertices[] = {
// Top face (y = 0.5f) // 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, 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, 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[] = { float mirrorPlaneVertices[] = {
@@ -287,6 +287,7 @@ std::vector<float> generateTorus(int segments, int sides) {
// Mesh implementation // Mesh implementation
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) { Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float)); vertexCount = dataSizeBytes / (8 * sizeof(float));
strideFloats = 8;
glGenVertexArrays(1, &VAO); glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO); glGenBuffers(1, &VBO);
@@ -308,9 +309,52 @@ Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
glBindVertexArray(0); 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() { Mesh::~Mesh() {
glDeleteVertexArrays(1, &VAO); glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &VBO);
if (boneVBO) {
glDeleteBuffers(1, &boneVBO);
}
} }
void Mesh::draw() const { void Mesh::draw() const {
@@ -319,6 +363,56 @@ void Mesh::draw() const {
glBindVertexArray(0); 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 // OBJLoader implementation
int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) { int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
// Check if already loaded // Check if already loaded
@@ -537,6 +631,7 @@ Renderer::~Renderer() {
if (rbo) glDeleteRenderbuffers(1, &rbo); if (rbo) glDeleteRenderbuffers(1, &rbo);
if (quadVBO) glDeleteBuffers(1, &quadVBO); if (quadVBO) glDeleteBuffers(1, &quadVBO);
if (quadVAO) glDeleteVertexArrays(1, &quadVAO); if (quadVAO) glDeleteVertexArrays(1, &quadVAO);
if (debugWhiteTexture) glDeleteTextures(1, &debugWhiteTexture);
} }
Texture* Renderer::getTexture(const std::string& path) { Texture* Renderer::getTexture(const std::string& path) {
@@ -619,6 +714,17 @@ void Renderer::initialize() {
texture1 = new Texture("Resources/Textures/container.jpg"); texture1 = new Texture("Resources/Textures/container.jpg");
texture2 = new Texture("Resources/Textures/awesomeface.png"); texture2 = new Texture("Resources/Textures/awesomeface.png");
if (debugWhiteTexture == 0) {
unsigned char white[4] = { 255, 255, 255, 255 };
glGenTextures(1, &debugWhiteTexture);
glBindTexture(GL_TEXTURE_2D, debugWhiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
}
cubeMesh = new Mesh(vertices, sizeof(vertices)); cubeMesh = new Mesh(vertices, sizeof(vertices));
@@ -809,7 +915,7 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
}; };
for (const auto& obj : sceneObjects) { 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); active.insert(obj.id);
RenderTarget& target = mirrorTargets[obj.id]; RenderTarget& target = mirrorTargets[obj.id];
@@ -886,12 +992,36 @@ void Renderer::ensureQuad() {
} }
void Renderer::drawFullscreenQuad() { void Renderer::drawFullscreenQuad() {
recordFullscreenDraw();
if (quadVAO == 0) ensureQuad(); if (quadVAO == 0) ensureQuad();
glBindVertexArray(quadVAO); glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6); glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0); glBindVertexArray(0);
} }
void Renderer::resetStats(RenderStats& stats) {
stats.drawCalls = 0;
stats.meshDraws = 0;
stats.fullscreenDraws = 0;
}
void Renderer::recordDrawCall() {
if (!activeStats) return;
activeStats->drawCalls += 1;
}
void Renderer::recordMeshDraw() {
if (!activeStats) return;
activeStats->drawCalls += 1;
activeStats->meshDraws += 1;
}
void Renderer::recordFullscreenDraw() {
if (!activeStats) return;
activeStats->drawCalls += 1;
activeStats->fullscreenDraws += 1;
}
void Renderer::clearHistory() { void Renderer::clearHistory() {
historyValid = false; historyValid = false;
if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) { if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) {
@@ -981,7 +1111,7 @@ void Renderer::renderObject(const SceneObject& obj) {
shader->setFloat("specularStrength", obj.material.specularStrength); shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess); shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix); shader->setFloat("mixAmount", obj.material.textureMix);
shader->setBool("unlit", obj.type == ObjectType::Mirror); shader->setBool("unlit", obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite);
Texture* baseTex = texture1; Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) { if (!obj.albedoTexturePath.empty()) {
@@ -990,7 +1120,7 @@ void Renderer::renderObject(const SceneObject& obj) {
if (baseTex) baseTex->Bind(GL_TEXTURE0); if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false; bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) { if (obj.renderType == RenderType::Mirror) {
auto it = mirrorTargets.find(obj.id); auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) { if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1); glActiveTexture(GL_TEXTURE1);
@@ -1018,26 +1148,29 @@ void Renderer::renderObject(const SceneObject& obj) {
} }
shader->setBool("hasNormalMap", normalUsed); shader->setBool("hasNormalMap", normalUsed);
switch (obj.type) { switch (obj.renderType) {
case ObjectType::Cube: case RenderType::Cube:
cubeMesh->draw(); cubeMesh->draw();
break; break;
case ObjectType::Sphere: case RenderType::Sphere:
sphereMesh->draw(); sphereMesh->draw();
break; break;
case ObjectType::Capsule: case RenderType::Capsule:
capsuleMesh->draw(); capsuleMesh->draw();
break; break;
case ObjectType::Plane: case RenderType::Plane:
if (planeMesh) planeMesh->draw(); if (planeMesh) planeMesh->draw();
break; break;
case ObjectType::Mirror: case RenderType::Mirror:
if (planeMesh) planeMesh->draw(); if (planeMesh) planeMesh->draw();
break; break;
case ObjectType::Torus: case RenderType::Sprite:
if (planeMesh) planeMesh->draw();
break;
case RenderType::Torus:
if (torusMesh) torusMesh->draw(); if (torusMesh) torusMesh->draw();
break; break;
case ObjectType::OBJMesh: case RenderType::OBJMesh:
if (obj.meshId >= 0) { if (obj.meshId >= 0) {
Mesh* objMesh = g_objLoader.getMesh(obj.meshId); Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
if (objMesh) { if (objMesh) {
@@ -1045,7 +1178,7 @@ void Renderer::renderObject(const SceneObject& obj) {
} }
} }
break; break;
case ObjectType::Model: case RenderType::Model:
if (obj.meshId >= 0) { if (obj.meshId >= 0) {
Mesh* modelMesh = getModelLoader().getMesh(obj.meshId); Mesh* modelMesh = getModelLoader().getMesh(obj.meshId);
if (modelMesh) { if (modelMesh) {
@@ -1053,18 +1186,8 @@ void Renderer::renderObject(const SceneObject& obj) {
} }
} }
break; break;
case ObjectType::PointLight: case RenderType::None:
case ObjectType::SpotLight: default:
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; break;
} }
} }
@@ -1110,8 +1233,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
candidates.reserve(sceneObjects.size()); candidates.reserve(sceneObjects.size());
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled || !obj.light.enabled) continue; if (!obj.enabled || !obj.hasLight || !obj.light.enabled) continue;
if (obj.type == ObjectType::DirectionalLight) { if (obj.light.type == LightType::Directional) {
LightUniform l; LightUniform l;
l.type = 0; l.type = 0;
l.dir = forwardFromRotation(obj); l.dir = forwardFromRotation(obj);
@@ -1119,7 +1242,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
l.intensity = obj.light.intensity; l.intensity = obj.light.intensity;
lights.push_back(l); lights.push_back(l);
if (lights.size() >= kMaxLights) break; if (lights.size() >= kMaxLights) break;
} else if (obj.type == ObjectType::SpotLight) { } else if (obj.light.type == LightType::Spot) {
LightUniform l; LightUniform l;
l.type = 2; l.type = 2;
l.pos = obj.position; l.pos = obj.position;
@@ -1135,7 +1258,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
c.distSq = glm::dot(delta, delta); c.distSq = glm::dot(delta, delta);
c.id = obj.id; c.id = obj.id;
candidates.push_back(c); candidates.push_back(c);
} else if (obj.type == ObjectType::PointLight) { } else if (obj.light.type == LightType::Point) {
LightUniform l; LightUniform l;
l.type = 1; l.type = 1;
l.pos = obj.position; l.pos = obj.position;
@@ -1148,7 +1271,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
c.distSq = glm::dot(delta, delta); c.distSq = glm::dot(delta, delta);
c.id = obj.id; c.id = obj.id;
candidates.push_back(c); candidates.push_back(c);
} else if (obj.type == ObjectType::AreaLight) { } else if (obj.light.type == LightType::Area) {
LightUniform l; LightUniform l;
l.type = 3; // area l.type = 3; // area
l.pos = obj.position; l.pos = obj.position;
@@ -1180,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) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue; if (!obj.enabled) continue;
if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue; if (!drawMirrorObjects && obj.hasRenderer && obj.renderType == RenderType::Mirror) continue;
// Skip light gizmo-only types and camera helpers if (!HasRendererComponent(obj)) continue;
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) {
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; if (!active) continue;
shader = active; shader = active;
shader->use(); shader->use();
shader->setMat4("view", camera.getViewMatrix()); shader->setMat4("view", view);
shader->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane)); shader->setMat4("projection", proj);
shader->setVec3("viewPos", camera.position); 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);
shader->setVec3("ambientColor", ambientColor); shader->setVec3("ambientColor", ambientColor);
@@ -1216,13 +1364,6 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade); 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->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color); shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength); shader->setFloat("ambientStrength", obj.material.ambientStrength);
@@ -1230,6 +1371,20 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("shininess", obj.material.shininess); shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix); 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; Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) { if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t; if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
@@ -1237,7 +1392,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
if (baseTex) baseTex->Bind(GL_TEXTURE0); if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false; bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) { if (obj.renderType == RenderType::Mirror) {
auto it = mirrorTargets.find(obj.id); auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) { if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1); glActiveTexture(GL_TEXTURE1);
@@ -1266,29 +1421,52 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setBool("hasNormalMap", normalUsed); shader->setBool("hasNormalMap", normalUsed);
Mesh* meshToDraw = nullptr; Mesh* meshToDraw = nullptr;
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh; if (obj.renderType == RenderType::Cube) meshToDraw = cubeMesh;
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh; else if (obj.renderType == RenderType::Sphere) meshToDraw = sphereMesh;
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh; else if (obj.renderType == RenderType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh; else if (obj.renderType == RenderType::Plane) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh; else if (obj.renderType == RenderType::Mirror) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh; else if (obj.renderType == RenderType::Sprite) meshToDraw = planeMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) { else if (obj.renderType == RenderType::Torus) meshToDraw = torusMesh;
else if (obj.renderType == RenderType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId); 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); 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) { if (meshToDraw) {
recordMeshDraw();
meshToDraw->draw(); meshToDraw->draw();
} }
} }
if (skybox) { if (!cullFace) {
glm::mat4 view = camera.getViewMatrix(); glDisable(GL_CULL_FACE);
glm::mat4 proj = glm::perspective(glm::radians(fovDeg), } else {
(float)width / height, glEnable(GL_CULL_FACE);
nearPlane, farPlane); glCullFace(prevCullMode);
}
if (skybox) {
recordDrawCall();
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj)); skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
} }
@@ -1301,7 +1479,7 @@ PostFXSettings Renderer::gatherPostFX(const std::vector<SceneObject>& sceneObjec
PostFXSettings combined; PostFXSettings combined;
combined.enabled = false; combined.enabled = false;
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (obj.type != ObjectType::PostFXNode) continue; if (!obj.hasPostFX) continue;
if (!obj.postFx.enabled) continue; if (!obj.postFx.enabled) continue;
combined = obj.postFx; // Last enabled node wins for now combined = obj.postFx; // Last enabled node wins for now
combined.enabled = true; combined.enabled = true;
@@ -1449,16 +1627,31 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
return target.texture; return target.texture;
} }
void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int /*selectedId*/, float fovDeg, float nearPlane, float farPlane) { void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane, bool drawColliders) {
resetStats(viewportStats);
activeStats = &viewportStats;
updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane); updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true); renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true);
if (drawColliders) {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight);
renderCollisionOverlay(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane);
unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true); unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true);
displayTexture = result ? result : viewportTexture; displayTexture = result ? result : viewportTexture;
activeStats = nullptr;
} }
unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) { unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) {
resetStats(previewStats);
activeStats = &previewStats;
ensureRenderTarget(previewTarget, width, height); ensureRenderTarget(previewTarget, width, height);
if (previewTarget.fbo == 0) return 0; if (previewTarget.fbo == 0) {
activeStats = nullptr;
return 0;
}
glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo); glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo);
glViewport(0, 0, width, height); glViewport(0, 0, width, height);
@@ -1468,12 +1661,290 @@ unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vecto
updateMirrorTargets(camera, sceneObjects, width, height, fovDeg, nearPlane, farPlane); updateMirrorTargets(camera, sceneObjects, width, height, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true); renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true);
if (!applyPostFX) { if (!applyPostFX) {
activeStats = nullptr;
return previewTarget.texture; return previewTarget.texture;
} }
unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false); unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false);
activeStats = nullptr;
return processed ? processed : previewTarget.texture; return processed ? processed : previewTarget.texture;
} }
void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane) {
if (!defaultShader || width <= 0 || height <= 0) return;
GLint prevPoly[2] = { GL_FILL, GL_FILL };
glGetIntegerv(GL_POLYGON_MODE, prevPoly);
GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean depthMask = GL_TRUE;
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
GLboolean cullFace = glIsEnabled(GL_CULL_FACE);
GLint prevCullMode = GL_BACK;
glGetIntegerv(GL_CULL_FACE_MODE, &prevCullMode);
GLboolean polyOffsetLine = glIsEnabled(GL_POLYGON_OFFSET_LINE);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDisable(GL_CULL_FACE);
glEnable(GL_POLYGON_OFFSET_LINE);
glPolygonOffset(-1.0f, -1.0f);
Shader* active = defaultShader;
active->use();
active->setMat4("view", camera.getViewMatrix());
active->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane));
active->setVec3("viewPos", camera.position);
active->setBool("unlit", true);
active->setBool("hasOverlay", false);
active->setBool("hasNormalMap", false);
active->setInt("lightCount", 0);
active->setFloat("mixAmount", 0.0f);
active->setVec3("materialColor", glm::vec3(0.2f, 1.0f, 0.2f));
active->setFloat("ambientStrength", 1.0f);
active->setFloat("specularStrength", 0.0f);
active->setFloat("shininess", 1.0f);
active->setInt("texture1", 0);
active->setInt("overlayTex", 1);
active->setInt("normalMap", 2);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, debugWhiteTexture ? debugWhiteTexture : (texture1 ? texture1->GetID() : 0));
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (!(obj.hasCollider && obj.collider.enabled) && !(obj.hasRigidbody && obj.rigidbody.enabled)) continue;
Mesh* meshToDraw = nullptr;
glm::vec3 scale = obj.scale;
glm::vec3 position = obj.position;
glm::vec3 rotation = obj.rotation;
if (obj.hasCollider && obj.collider.enabled) {
switch (obj.collider.type) {
case ColliderType::Box:
meshToDraw = cubeMesh;
scale = obj.collider.boxSize;
break;
case ColliderType::Capsule:
meshToDraw = capsuleMesh;
scale = obj.collider.boxSize;
break;
case ColliderType::Mesh:
case ColliderType::ConvexMesh:
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
meshToDraw = getModelLoader().getMesh(obj.meshId);
} else {
meshToDraw = nullptr;
}
scale = obj.scale;
break;
}
} else {
switch (obj.renderType) {
case RenderType::Cube:
meshToDraw = cubeMesh;
break;
case RenderType::Sphere:
meshToDraw = sphereMesh;
break;
case RenderType::Capsule:
meshToDraw = capsuleMesh;
break;
case RenderType::Plane:
meshToDraw = planeMesh;
break;
case RenderType::Sprite:
meshToDraw = planeMesh;
break;
case RenderType::Torus:
meshToDraw = sphereMesh;
break;
case RenderType::OBJMesh:
if (obj.meshId >= 0) meshToDraw = g_objLoader.getMesh(obj.meshId);
break;
case RenderType::Model:
if (obj.meshId >= 0) meshToDraw = getModelLoader().getMesh(obj.meshId);
break;
default:
break;
}
}
if (!meshToDraw) continue;
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, position);
model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, scale);
active->setMat4("model", model);
meshToDraw->draw();
}
if (!polyOffsetLine) glDisable(GL_POLYGON_OFFSET_LINE);
if (cullFace) glEnable(GL_CULL_FACE);
if (depthTest) glEnable(GL_DEPTH_TEST);
glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]);
}
void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane) {
if (!defaultShader || selectedId < 0 || currentWidth <= 0 || currentHeight <= 0) return;
const SceneObject* selectedObj = nullptr;
for (const auto& obj : sceneObjects) {
if (obj.id == selectedId) {
selectedObj = &obj;
break;
}
}
if (!selectedObj || !selectedObj->enabled) return;
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->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->renderType == RenderType::Model && selectedObj->meshId != -1) {
meshToDraw = getModelLoader().getMesh(selectedObj->meshId);
}
if (!meshToDraw) return;
GLint prevPoly[2] = { GL_FILL, GL_FILL };
glGetIntegerv(GL_POLYGON_MODE, prevPoly);
GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean depthMask = GL_TRUE;
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
GLboolean cullFace = glIsEnabled(GL_CULL_FACE);
GLint prevCullMode = GL_BACK;
glGetIntegerv(GL_CULL_FACE_MODE, &prevCullMode);
GLboolean stencilTest = glIsEnabled(GL_STENCIL_TEST);
GLint prevStencilFunc = GL_ALWAYS;
GLint prevStencilRef = 0;
GLint prevStencilValueMask = 0xFF;
GLint prevStencilFail = GL_KEEP;
GLint prevStencilZFail = GL_KEEP;
GLint prevStencilZPass = GL_KEEP;
GLint prevStencilWriteMask = 0xFF;
glGetIntegerv(GL_STENCIL_FUNC, &prevStencilFunc);
glGetIntegerv(GL_STENCIL_REF, &prevStencilRef);
glGetIntegerv(GL_STENCIL_VALUE_MASK, &prevStencilValueMask);
glGetIntegerv(GL_STENCIL_FAIL, &prevStencilFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &prevStencilZFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &prevStencilZPass);
glGetIntegerv(GL_STENCIL_WRITEMASK, &prevStencilWriteMask);
GLboolean prevColorMask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE };
glGetBooleanv(GL_COLOR_WRITEMASK, prevColorMask);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight);
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
Shader* active = defaultShader;
active->use();
active->setMat4("view", camera.getViewMatrix());
active->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)currentWidth / (float)currentHeight, nearPlane, farPlane));
active->setVec3("viewPos", camera.position);
active->setBool("unlit", true);
active->setBool("hasOverlay", false);
active->setBool("hasNormalMap", false);
active->setInt("lightCount", 0);
active->setFloat("mixAmount", 0.0f);
active->setVec3("materialColor", glm::vec3(1.0f, 0.5f, 0.1f));
active->setFloat("ambientStrength", 1.0f);
active->setFloat("specularStrength", 0.0f);
active->setFloat("shininess", 1.0f);
active->setInt("texture1", 0);
active->setInt("overlayTex", 1);
active->setInt("normalMap", 2);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, debugWhiteTexture ? debugWhiteTexture : (texture1 ? texture1->GetID() : 0));
glm::mat4 baseModel = glm::mat4(1.0f);
baseModel = glm::translate(baseModel, selectedObj->position);
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
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);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
if (cullFace) {
glEnable(GL_CULL_FACE);
glCullFace(prevCullMode);
} else {
glDisable(GL_CULL_FACE);
}
active->setMat4("model", baseModel);
meshToDraw->draw();
// Draw the scaled outline where stencil is not marked.
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
const float outlineScale = 1.03f;
glm::mat4 outlineModel = glm::scale(baseModel, glm::vec3(outlineScale));
active->setMat4("model", outlineModel);
meshToDraw->draw();
if (!cullFace) {
glDisable(GL_CULL_FACE);
} else {
glCullFace(prevCullMode);
}
glDepthMask(depthMask);
if (depthTest) glEnable(GL_DEPTH_TEST);
else glDisable(GL_DEPTH_TEST);
glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]);
glColorMask(prevColorMask[0], prevColorMask[1], prevColorMask[2], prevColorMask[3]);
glStencilFunc(prevStencilFunc, prevStencilRef, prevStencilValueMask);
glStencilOp(prevStencilFail, prevStencilZFail, prevStencilZPass);
glStencilMask(prevStencilWriteMask);
if (!stencilTest) glDisable(GL_STENCIL_TEST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Renderer::endRender() { void Renderer::endRender() {
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }

View File

@@ -19,14 +19,23 @@ std::vector<float> generateTorus(int segments = 32, int sides = 16);
class Mesh { class Mesh {
private: private:
unsigned int VAO, VBO; unsigned int VAO, VBO;
unsigned int boneVBO = 0;
int vertexCount; int vertexCount;
int strideFloats = 8;
bool dynamic = false;
bool hasBones = false;
public: public:
Mesh(const float* vertexData, size_t dataSizeBytes); Mesh(const float* vertexData, size_t dataSizeBytes);
Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage,
const void* boneData, size_t boneDataBytes);
~Mesh(); ~Mesh();
void draw() const; void draw() const;
void updateVertices(const float* vertexData, size_t dataSizeBytes);
int getVertexCount() const { return vertexCount; } int getVertexCount() const { return vertexCount; }
bool isDynamic() const { return dynamic; }
bool usesBones() const { return hasBones; }
}; };
class OBJLoader { class OBJLoader {
@@ -44,6 +53,12 @@ public:
std::vector<glm::vec3> triangleVertices; // positions duplicated per-triangle for picking std::vector<glm::vec3> triangleVertices; // positions duplicated per-triangle for picking
std::vector<glm::vec3> positions; // unique vertex positions for physics std::vector<glm::vec3> positions; // unique vertex positions for physics
std::vector<uint32_t> triangleIndices; // triangle indices into positions 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: private:
@@ -61,6 +76,13 @@ public:
class Camera; class Camera;
class Renderer { class Renderer {
public:
struct RenderStats {
int drawCalls = 0;
int meshDraws = 0;
int fullscreenDraws = 0;
};
private: private:
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0; unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
int currentWidth = 800, currentHeight = 600; int currentWidth = 800, currentHeight = 600;
@@ -84,6 +106,7 @@ private:
Shader* blurShader = nullptr; Shader* blurShader = nullptr;
Texture* texture1 = nullptr; Texture* texture1 = nullptr;
Texture* texture2 = nullptr; Texture* texture2 = nullptr;
unsigned int debugWhiteTexture = 0;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCache; std::unordered_map<std::string, std::unique_ptr<Texture>> textureCache;
std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheLinear; std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheLinear;
std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheNearest; std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheNearest;
@@ -96,6 +119,7 @@ private:
}; };
std::unordered_map<std::string, ShaderEntry> shaderCache; std::unordered_map<std::string, ShaderEntry> shaderCache;
std::string defaultVertPath = "Resources/Shaders/vert.glsl"; std::string defaultVertPath = "Resources/Shaders/vert.glsl";
std::string skinnedVertPath = "Resources/Shaders/skinned_vert.glsl";
std::string defaultFragPath = "Resources/Shaders/frag.glsl"; std::string defaultFragPath = "Resources/Shaders/frag.glsl";
std::string postVertPath = "Resources/Shaders/postfx_vert.glsl"; std::string postVertPath = "Resources/Shaders/postfx_vert.glsl";
std::string postFragPath = "Resources/Shaders/postfx_frag.glsl"; std::string postFragPath = "Resources/Shaders/postfx_frag.glsl";
@@ -114,6 +138,9 @@ private:
unsigned int displayTexture = 0; unsigned int displayTexture = 0;
bool historyValid = false; bool historyValid = false;
std::unordered_map<int, RenderTarget> mirrorTargets; std::unordered_map<int, RenderTarget> mirrorTargets;
RenderStats viewportStats;
RenderStats previewStats;
RenderStats* activeStats = nullptr;
void setupFBO(); void setupFBO();
void ensureRenderTarget(RenderTarget& target, int w, int h); void ensureRenderTarget(RenderTarget& target, int w, int h);
@@ -121,6 +148,10 @@ private:
void updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane); void updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void ensureQuad(); void ensureQuad();
void drawFullscreenQuad(); void drawFullscreenQuad();
void resetStats(RenderStats& stats);
void recordDrawCall();
void recordMeshDraw();
void recordFullscreenDraw();
void clearHistory(); void clearHistory();
void clearTarget(RenderTarget& target); void clearTarget(RenderTarget& target);
void renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects); void renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects);
@@ -145,10 +176,14 @@ public:
void beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos); void beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos);
void renderSkybox(const glm::mat4& view, const glm::mat4& proj); void renderSkybox(const glm::mat4& view, const glm::mat4& proj);
void renderObject(const SceneObject& obj); void renderObject(const SceneObject& obj);
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE); void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE, bool drawColliders = false);
void renderSelectionOutline(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane);
unsigned int renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX = false); unsigned int renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX = false);
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void endRender(); void endRender();
Skybox* getSkybox() { return skybox; } Skybox* getSkybox() { return skybox; }
unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; } unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; }
const RenderStats& getLastViewportStats() const { return viewportStats; }
const RenderStats& getLastPreviewStats() const { return previewStats; }
}; };

View File

@@ -3,20 +3,51 @@
#include "Common.h" #include "Common.h"
enum class ObjectType { enum class ObjectType {
Cube, Cube = 0,
Sphere, Sphere = 1,
Capsule, Capsule = 2,
OBJMesh, OBJMesh = 3,
Model, // New type for Assimp-loaded models (FBX, GLTF, etc.) Model = 4, // New type for Assimp-loaded models (FBX, GLTF, etc.)
DirectionalLight, DirectionalLight = 5,
PointLight, PointLight = 6,
SpotLight, SpotLight = 7,
AreaLight, AreaLight = 8,
Camera, Camera = 9,
PostFXNode, PostFXNode = 10,
Mirror, Mirror = 11,
Plane, Plane = 12,
Torus Torus = 13,
Sprite = 14, // 3D quad sprite (lit/unlit with material)
Sprite2D = 15, // Screen-space sprite
Canvas = 16, // UI canvas root
UIImage = 17,
UISlider = 18,
UIButton = 19,
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 { struct MaterialProperties {
@@ -34,6 +65,95 @@ enum class LightType {
Area = 3 Area = 3
}; };
enum class UIAnchor {
Center = 0,
TopLeft = 1,
TopRight = 2,
BottomLeft = 3,
BottomRight = 4
};
enum class UISliderStyle {
ImGui = 0,
Fill = 1,
Circle = 2
};
enum class UIButtonStyle {
ImGui = 0,
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 { struct LightComponent {
LightType type = LightType::Point; LightType type = LightType::Point;
glm::vec3 color = glm::vec3(1.0f); glm::vec3 color = glm::vec3(1.0f);
@@ -59,6 +179,8 @@ struct CameraComponent {
float nearClip = NEAR_PLANE; float nearClip = NEAR_PLANE;
float farClip = FAR_PLANE; float farClip = FAR_PLANE;
bool applyPostFX = true; bool applyPostFX = true;
bool use2D = false;
float pixelsPerUnit = 100.0f;
}; };
struct PostFXSettings { struct PostFXSettings {
@@ -96,9 +218,16 @@ struct ScriptSetting {
std::string value; std::string value;
}; };
enum class ScriptLanguage {
Cpp = 0,
CSharp = 1
};
struct ScriptComponent { struct ScriptComponent {
bool enabled = true; bool enabled = true;
ScriptLanguage language = ScriptLanguage::Cpp;
std::string path; std::string path;
std::string managedType;
std::vector<ScriptSetting> settings; std::vector<ScriptSetting> settings;
std::string lastBinaryPath; std::string lastBinaryPath;
std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start
@@ -142,6 +271,64 @@ struct PlayerControllerComponent {
float yaw = 0.0f; float yaw = 0.0f;
}; };
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;
std::string label = "UI Element";
bool buttonPressed = false;
glm::vec4 color = glm::vec4(1.0f);
bool interactable = true;
UISliderStyle sliderStyle = UISliderStyle::ImGui;
UIButtonStyle buttonStyle = UIButtonStyle::ImGui;
std::string stylePreset = "Default";
float textScale = 1.0f;
};
struct Rigidbody2DComponent {
bool enabled = true;
bool useGravity = false;
float gravityScale = 1.0f;
float linearDamping = 0.0f;
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 { struct AudioSourceComponent {
bool enabled = true; bool enabled = true;
std::string clipPath; std::string clipPath;
@@ -151,6 +338,36 @@ struct AudioSourceComponent {
bool spatial = true; bool spatial = true;
float minDistance = 1.0f; float minDistance = 1.0f;
float maxDistance = 25.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 { class SceneObject {
@@ -160,6 +377,12 @@ public:
bool enabled = true; bool enabled = true;
int layer = 0; int layer = 0;
std::string tag = "Untagged"; 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 position;
glm::vec3 rotation; glm::vec3 rotation;
glm::vec3 scale; glm::vec3 scale;
@@ -173,6 +396,7 @@ public:
bool isExpanded = true; bool isExpanded = true;
std::string meshPath; // Path to imported model file std::string meshPath; // Path to imported model file
int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader) int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader)
int meshSourceIndex = -1; // Source mesh index for multi-mesh models
MaterialProperties material; MaterialProperties material;
std::string materialPath; // Optional external material asset std::string materialPath; // Optional external material asset
std::string albedoTexturePath; std::string albedoTexturePath;
@@ -188,12 +412,27 @@ public:
std::vector<std::string> additionalMaterialPaths; std::vector<std::string> additionalMaterialPaths;
bool hasRigidbody = false; bool hasRigidbody = false;
RigidbodyComponent rigidbody; 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; bool hasCollider = false;
ColliderComponent collider; ColliderComponent collider;
bool hasPlayerController = false; bool hasPlayerController = false;
PlayerControllerComponent playerController; PlayerControllerComponent playerController;
bool hasAudioSource = false; bool hasAudioSource = false;
AudioSourceComponent audioSource; 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) SceneObject(const std::string& name, ObjectType type, int id)
: name(name), : name(name),
@@ -207,3 +446,11 @@ public:
localInitialized(true), localInitialized(true),
id(id) {} 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(); 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(); fs::path relativeParent = relToScripts.has_parent_path() ? relToScripts.parent_path() : fs::path();
std::string baseName = scriptAbs.stem().string(); std::string baseName = scriptAbs.stem().string();
fs::path objectPath = config.outDir / relativeParent / (baseName + ".o"); 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 testEditorSpec = detectFunction(scriptSource, "TestEditor");
FunctionSpec updateSpec = detectFunction(scriptSource, "Update"); FunctionSpec updateSpec = detectFunction(scriptSource, "Update");
FunctionSpec tickUpdateSpec = detectFunction(scriptSource, "TickUpdate"); 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; fs::path wrapperPath;
bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present
|| updateSpec.present || tickUpdateSpec.present; || updateSpec.present || tickUpdateSpec.present || needsInspectorWrap;
fs::path sourceToCompile = scriptAbs; fs::path sourceToCompile = scriptAbs;
if (useWrapper) { if (useWrapper) {
@@ -264,8 +288,15 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
} }
std::string includePath = scriptAbs.lexically_normal().generic_string(); 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 \"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"; wrapper << "extern \"C\" {\n";
auto emitWrapper = [&](const char* exportedName, const char* implName, 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_TestEditor", "TestEditor", testEditorSpec);
emitWrapper("Script_Update", "Update", updateSpec); emitWrapper("Script_Update", "Update", updateSpec);
emitWrapper("Script_TickUpdate", "TickUpdate", tickUpdateSpec); 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"; wrapper << "}\n";
sourceToCompile = wrapperPath; sourceToCompile = wrapperPath;

View File

@@ -1,7 +1,10 @@
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "Engine.h" #include "Engine.h"
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cctype>
#include <iterator> #include <iterator>
#include <unordered_map> #include <unordered_map>
@@ -27,6 +30,22 @@ std::string makeScriptInstanceKey(const ScriptContext& ctx) {
} }
return key; return key;
} }
std::string trimString(const std::string& input) {
size_t start = 0;
while (start < input.size() && std::isspace(static_cast<unsigned char>(input[start]))) {
++start;
}
size_t end = input.size();
while (end > start && std::isspace(static_cast<unsigned char>(input[end - 1]))) {
--end;
}
return input.substr(start, end - start);
}
bool isUIObject(const SceneObject* obj) {
return obj && HasUIComponent(*obj);
}
} }
SceneObject* ScriptContext::FindObjectByName(const std::string& name) { SceneObject* ScriptContext::FindObjectByName(const std::string& name) {
@@ -39,6 +58,31 @@ SceneObject* ScriptContext::FindObjectById(int id) {
return engine->findObjectById(id); return engine->findObjectById(id);
} }
SceneObject* ScriptContext::ResolveObjectRef(const std::string& ref) {
if (ref.empty()) return nullptr;
std::string trimmed = trimString(ref);
if (trimmed == "ObjectSelf") return object;
const std::string namePrefix = "Object.";
const std::string idPrefix = "Object.ID-";
if (trimmed.rfind(idPrefix, 0) == 0) {
std::string idStr = trimmed.substr(idPrefix.size());
if (idStr.empty()) return nullptr;
try {
int id = std::stoi(idStr);
return FindObjectById(id);
} catch (...) {
return nullptr;
}
}
if (trimmed.rfind(namePrefix, 0) == 0) {
std::string name = trimmed.substr(namePrefix.size());
if (name.empty()) return nullptr;
return FindObjectByName(name);
}
return nullptr;
}
bool ScriptContext::IsObjectEnabled() const { bool ScriptContext::IsObjectEnabled() const {
return object ? object->enabled : false; return object ? object->enabled : false;
} }
@@ -97,6 +141,12 @@ void ScriptContext::SetPosition(const glm::vec3& pos) {
} }
} }
void ScriptContext::SetPosition2D(const glm::vec2& pos) {
if (!object) return;
object->ui.position = pos;
MarkDirty();
}
void ScriptContext::SetRotation(const glm::vec3& rot) { void ScriptContext::SetRotation(const glm::vec3& rot) {
if (object) { if (object) {
object->rotation = NormalizeEulerDegrees(rot); object->rotation = NormalizeEulerDegrees(rot);
@@ -126,10 +176,352 @@ void ScriptContext::SetScale(const glm::vec3& scl) {
} }
} }
void ScriptContext::GetPlanarYawPitchVectors(float pitchDeg, float yawDeg,
glm::vec3& outForward, glm::vec3& outRight) const {
glm::quat q = glm::quat(glm::radians(glm::vec3(pitchDeg, yawDeg, 0.0f)));
glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f));
outForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
outRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
if (!std::isfinite(outForward.x) || glm::length(outForward) < 1e-3f) {
outForward = glm::vec3(0.0f, 0.0f, -1.0f);
}
if (!std::isfinite(outRight.x) || glm::length(outRight) < 1e-3f) {
outRight = glm::vec3(1.0f, 0.0f, 0.0f);
}
}
glm::vec3 ScriptContext::GetMoveInputWASD(float pitchDeg, float yawDeg) const {
glm::vec3 forward(0.0f);
glm::vec3 right(0.0f);
glm::vec3 move(0.0f);
GetPlanarYawPitchVectors(pitchDeg, yawDeg, forward, right);
if (ImGui::IsKeyDown(ImGuiKey_W)) move += forward;
if (ImGui::IsKeyDown(ImGuiKey_S)) move -= forward;
if (ImGui::IsKeyDown(ImGuiKey_D)) move += right;
if (ImGui::IsKeyDown(ImGuiKey_A)) move -= right;
if (glm::length(move) > 0.001f) move = glm::normalize(move);
return move;
}
bool ScriptContext::ApplyMouseLook(float& pitchDeg, float& yawDeg, float sensitivity, float maxDelta,
float deltaTime, bool requireMouseButton) const {
if (requireMouseButton && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) return false;
ImGuiIO& io = ImGui::GetIO();
glm::vec2 delta(io.MouseDelta.x, io.MouseDelta.y);
float len = glm::length(delta);
if (len > maxDelta) delta *= (maxDelta / len);
yawDeg -= delta.x * 50.0f * sensitivity * deltaTime;
pitchDeg -= delta.y * 50.0f * sensitivity * deltaTime;
pitchDeg = std::clamp(pitchDeg, -89.0f, 89.0f);
return true;
}
bool ScriptContext::IsSprintDown() const {
return ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
}
bool ScriptContext::IsJumpDown() const {
return ImGui::IsKeyDown(ImGuiKey_Space);
}
bool ScriptContext::ResolveGround(float capsuleHalf, float probeExtra, float groundSnap, float verticalVelocity,
glm::vec3* outHitPos, bool* outHitGround) const {
if (!object) return false;
glm::vec3 hitPos(0.0f);
glm::vec3 hitNormal(0.0f, 1.0f, 0.0f);
float hitDist = 0.0f;
float probeDist = capsuleHalf + probeExtra;
glm::vec3 rayStart = object->position + glm::vec3(0.0f, 0.1f, 0.0f);
bool hitGround = RaycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist,
&hitPos, &hitNormal, &hitDist);
bool grounded = hitGround && hitNormal.y > 0.25f &&
hitDist <= capsuleHalf + groundSnap &&
verticalVelocity <= 0.35f;
if (!hitGround) {
grounded = object->position.y <= capsuleHalf + 0.12f && verticalVelocity <= 0.35f;
}
if (outHitPos) *outHitPos = hitPos;
if (outHitGround) *outHitGround = hitGround;
return grounded;
}
void ScriptContext::ApplyVelocity(const glm::vec3& velocity, float deltaTime) {
if (!object) return;
if (!SetRigidbodyVelocity(velocity)) {
object->position += velocity * deltaTime;
}
}
void ScriptContext::BindStandaloneMovementSettings(StandaloneMovementSettings& settings) {
AutoSetting("moveTuning", settings.moveTuning);
AutoSetting("lookTuning", settings.lookTuning);
AutoSetting("capsuleTuning", settings.capsuleTuning);
AutoSetting("gravityTuning", settings.gravityTuning);
AutoSetting("enableMouseLook", settings.enableMouseLook);
AutoSetting("requireMouseButton", settings.requireMouseButton);
AutoSetting("enforceCollider", settings.enforceCollider);
AutoSetting("enforceRigidbody", settings.enforceRigidbody);
}
void ScriptContext::DrawStandaloneMovementInspector(StandaloneMovementSettings& settings, bool* showDebug) {
BindStandaloneMovementSettings(settings);
ImGui::TextUnformatted("Standalone Movement Controller");
ImGui::Separator();
ImGui::DragFloat3("Walk/Run/Jump", &settings.moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
ImGui::DragFloat2("Look Sens/Clamp", &settings.lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
ImGui::DragFloat3("Height/Radius/Snap", &settings.capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
ImGui::DragFloat3("Gravity/Probe/MaxFall", &settings.gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
ImGui::Checkbox("Enable Mouse Look", &settings.enableMouseLook);
ImGui::Checkbox("Hold RMB to Look", &settings.requireMouseButton);
ImGui::Checkbox("Force Collider", &settings.enforceCollider);
ImGui::Checkbox("Force Rigidbody", &settings.enforceRigidbody);
if (showDebug) {
AutoSetting("showDebug", *showDebug);
ImGui::Checkbox("Show Debug", showDebug);
}
}
void ScriptContext::TickStandaloneMovement(StandaloneMovementState& state, StandaloneMovementSettings& settings,
float deltaTime, StandaloneMovementDebug* debug) {
if (!object) return;
BindStandaloneMovementSettings(settings);
if (settings.enforceCollider) EnsureCapsuleCollider(settings.capsuleTuning.x, settings.capsuleTuning.y);
if (settings.enforceRigidbody) EnsureRigidbody(true, false);
const float walkSpeed = settings.moveTuning.x;
const float runSpeed = settings.moveTuning.y;
const float jumpStrength = settings.moveTuning.z;
const float lookSensitivity = settings.lookTuning.x;
const float maxMouseDelta = glm::max(5.0f, settings.lookTuning.y);
const float height = settings.capsuleTuning.x;
const float groundSnap = settings.capsuleTuning.z;
const float gravity = settings.gravityTuning.x;
const float probeExtra = settings.gravityTuning.y;
const float maxFall = glm::max(1.0f, settings.gravityTuning.z);
if (settings.enableMouseLook) {
ApplyMouseLook(state.pitch, state.yaw, lookSensitivity, maxMouseDelta, deltaTime, settings.requireMouseButton);
}
glm::vec3 move = GetMoveInputWASD(state.pitch, state.yaw);
float targetSpeed = IsSprintDown() ? runSpeed : walkSpeed;
glm::vec3 velocity = move * targetSpeed;
float capsuleHalf = std::max(0.1f, height * 0.5f);
glm::vec3 physVel;
bool havePhysVel = GetRigidbodyVelocity(physVel);
if (havePhysVel) state.verticalVelocity = physVel.y;
glm::vec3 hitPos(0.0f);
bool hitGround = false;
bool grounded = ResolveGround(capsuleHalf, probeExtra, groundSnap, state.verticalVelocity, &hitPos, &hitGround);
if (grounded) {
state.verticalVelocity = 0.0f;
if (!havePhysVel) {
object->position.y = hitGround ? std::max(object->position.y, hitPos.y + capsuleHalf) : capsuleHalf;
}
if (IsJumpDown()) state.verticalVelocity = jumpStrength;
} else {
state.verticalVelocity += gravity * deltaTime;
}
state.verticalVelocity = std::clamp(state.verticalVelocity, -maxFall, maxFall);
velocity.y = state.verticalVelocity;
glm::vec3 rotation(state.pitch, state.yaw, 0.0f);
SetRotation(rotation);
SetRigidbodyRotation(rotation);
ApplyVelocity(velocity, deltaTime);
if (debug) {
debug->velocity = velocity;
debug->grounded = grounded;
}
}
bool ScriptContext::IsUIButtonPressed() const {
return object && object->hasUI && object->ui.type == UIElementType::Button && object->ui.buttonPressed;
}
bool ScriptContext::IsUIInteractable() const {
return object ? object->ui.interactable : false;
}
void ScriptContext::SetUIInteractable(bool interactable) {
if (!object) return;
if (object->ui.interactable != interactable) {
object->ui.interactable = interactable;
MarkDirty();
}
}
float ScriptContext::GetUISliderValue() const {
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return 0.0f;
return object->ui.sliderValue;
}
void ScriptContext::SetUISliderValue(float value) {
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;
MarkDirty();
}
}
void ScriptContext::SetUISliderRange(float minValue, float maxValue) {
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;
object->ui.sliderValue = std::clamp(object->ui.sliderValue, minValue, maxValue);
MarkDirty();
}
void ScriptContext::SetUILabel(const std::string& label) {
if (!object) return;
if (object->ui.label != label) {
object->ui.label = label;
MarkDirty();
}
}
void ScriptContext::SetUIColor(const glm::vec4& color) {
if (!object) return;
if (object->ui.color != color) {
object->ui.color = color;
MarkDirty();
}
}
float ScriptContext::GetUITextScale() const {
if (!object || !object->hasUI || object->ui.type != UIElementType::Text) return 1.0f;
return object->ui.textScale;
}
void ScriptContext::SetUITextScale(float scale) {
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;
MarkDirty();
}
}
void ScriptContext::SetUISliderStyle(UISliderStyle style) {
if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return;
if (object->ui.sliderStyle != style) {
object->ui.sliderStyle = style;
MarkDirty();
}
}
void ScriptContext::SetUIButtonStyle(UIButtonStyle style) {
if (!object || !object->hasUI || object->ui.type != UIElementType::Button) return;
if (object->ui.buttonStyle != style) {
object->ui.buttonStyle = style;
MarkDirty();
}
}
void ScriptContext::SetUIStylePreset(const std::string& name) {
if (!object || name.empty()) return;
if (object->ui.stylePreset != name) {
object->ui.stylePreset = name;
MarkDirty();
}
}
void ScriptContext::RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace) {
if (engine) {
engine->registerUIStylePresetFromScript(name, style, replace);
}
}
void ScriptContext::SetFPSCap(bool enabled, float cap) {
if (engine) {
engine->setFrameRateCapFromScript(enabled, cap);
}
}
bool ScriptContext::HasRigidbody() const { bool ScriptContext::HasRigidbody() const {
return object && object->hasRigidbody && object->rigidbody.enabled; return object && object->hasRigidbody && object->rigidbody.enabled;
} }
bool ScriptContext::HasRigidbody2D() const {
return isUIObject(object) && object->hasRigidbody2D && object->rigidbody2D.enabled;
}
bool ScriptContext::EnsureCapsuleCollider(float height, float radius) {
if (!object) return false;
bool changed = false;
if (!object->hasCollider) {
object->hasCollider = true;
changed = true;
}
ColliderComponent& col = object->collider;
if (!col.enabled) {
col.enabled = true;
changed = true;
}
if (col.type != ColliderType::Capsule) {
col.type = ColliderType::Capsule;
changed = true;
}
if (!col.convex) {
col.convex = true;
changed = true;
}
glm::vec3 size(radius * 2.0f, height, radius * 2.0f);
if (col.boxSize != size) {
col.boxSize = size;
changed = true;
}
if (changed) {
MarkDirty();
}
return true;
}
bool ScriptContext::EnsureRigidbody(bool useGravity, bool kinematic) {
if (!object) return false;
bool changed = false;
if (!object->hasRigidbody) {
object->hasRigidbody = true;
changed = true;
}
RigidbodyComponent& rb = object->rigidbody;
if (!rb.enabled) {
rb.enabled = true;
changed = true;
}
if (rb.useGravity != useGravity) {
rb.useGravity = useGravity;
changed = true;
}
if (rb.isKinematic != kinematic) {
rb.isKinematic = kinematic;
changed = true;
}
if (changed) {
MarkDirty();
}
return true;
}
bool ScriptContext::SetRigidbody2DVelocity(const glm::vec2& velocity) {
if (!object || !HasRigidbody2D()) return false;
object->rigidbody2D.velocity = velocity;
MarkDirty();
return true;
}
bool ScriptContext::GetRigidbody2DVelocity(glm::vec2& outVelocity) const {
if (!object || !HasRigidbody2D()) return false;
outVelocity = object->rigidbody2D.velocity;
return true;
}
bool ScriptContext::SetRigidbodyVelocity(const glm::vec3& velocity) { bool ScriptContext::SetRigidbodyVelocity(const glm::vec3& velocity) {
if (!engine || !object || !HasRigidbody()) return false; if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyVelocityFromScript(object->id, velocity); return engine->setRigidbodyVelocityFromScript(object->id, velocity);
@@ -140,6 +532,12 @@ bool ScriptContext::GetRigidbodyVelocity(glm::vec3& outVelocity) const {
return engine->getRigidbodyVelocityFromScript(object->id, outVelocity); return engine->getRigidbodyVelocityFromScript(object->id, outVelocity);
} }
bool ScriptContext::AddRigidbodyVelocity(const glm::vec3& deltaVelocity) {
glm::vec3 current;
if (!GetRigidbodyVelocity(current)) return false;
return SetRigidbodyVelocity(current + deltaVelocity);
}
bool ScriptContext::SetRigidbodyAngularVelocity(const glm::vec3& velocity) { bool ScriptContext::SetRigidbodyAngularVelocity(const glm::vec3& velocity) {
if (!engine || !object || !HasRigidbody()) return false; if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity); return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity);
@@ -265,6 +663,17 @@ void ScriptContext::SetSettingBool(const std::string& key, bool value) {
SetSetting(key, value ? "1" : "0"); SetSetting(key, value ? "1" : "0");
} }
float ScriptContext::GetSettingFloat(const std::string& key, float fallback) const {
std::string v = GetSetting(key, "");
if (v.empty()) return fallback;
try { return std::stof(v); } catch (...) {}
return fallback;
}
void ScriptContext::SetSettingFloat(const std::string& key, float value) {
SetSetting(key, std::to_string(value));
}
glm::vec3 ScriptContext::GetSettingVec3(const std::string& key, const glm::vec3& fallback) const { glm::vec3 ScriptContext::GetSettingVec3(const std::string& key, const glm::vec3& fallback) const {
std::string v = GetSetting(key, ""); std::string v = GetSetting(key, "");
if (v.empty()) return fallback; if (v.empty()) return fallback;
@@ -315,6 +724,31 @@ void ScriptContext::AutoSetting(const std::string& key, bool& value) {
autoSettings.push_back(entry); autoSettings.push_back(entry);
} }
void ScriptContext::AutoSetting(const std::string& key, float& value) {
if (!script) return;
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
[&](const AutoSettingEntry& e){ return e.key == key; })) return;
static std::unordered_map<std::string, float> defaults;
std::string scriptId = makeScriptInstanceKey(*this);
std::string id = scriptId + "|" + key;
float defaultVal = value;
auto itDef = defaults.find(id);
if (itDef != defaults.end()) {
defaultVal = itDef->second;
} else {
defaults[id] = defaultVal;
}
value = GetSettingFloat(key, defaultVal);
AutoSettingEntry entry;
entry.type = AutoSettingType::Float;
entry.key = key;
entry.ptr = &value;
entry.initialFloat = value;
autoSettings.push_back(entry);
}
void ScriptContext::AutoSetting(const std::string& key, glm::vec3& value) { void ScriptContext::AutoSetting(const std::string& key, glm::vec3& value) {
if (!script) return; if (!script) return;
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(), if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
@@ -378,6 +812,12 @@ void ScriptContext::SaveAutoSettings() {
newVal = cur ? "1" : "0"; newVal = cur ? "1" : "0";
break; break;
} }
case AutoSettingType::Float: {
float cur = *static_cast<float*>(e.ptr);
if (std::abs(cur - e.initialFloat) < 1e-6f) continue;
newVal = std::to_string(cur);
break;
}
case AutoSettingType::Vec3: { case AutoSettingType::Vec3: {
glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr); glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr);
if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue; if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue;

View File

@@ -10,13 +10,14 @@ struct ScriptContext {
Engine* engine = nullptr; Engine* engine = nullptr;
SceneObject* object = nullptr; SceneObject* object = nullptr;
ScriptComponent* script = nullptr; ScriptComponent* script = nullptr;
enum class AutoSettingType { Bool, Vec3, StringBuf }; enum class AutoSettingType { Bool, Float, Vec3, StringBuf };
struct AutoSettingEntry { struct AutoSettingEntry {
AutoSettingType type; AutoSettingType type;
std::string key; std::string key;
void* ptr = nullptr; void* ptr = nullptr;
size_t bufSize = 0; size_t bufSize = 0;
bool initialBool = false; bool initialBool = false;
float initialFloat = 0.0f;
glm::vec3 initialVec3 = glm::vec3(0.0f); glm::vec3 initialVec3 = glm::vec3(0.0f);
std::string initialString; std::string initialString;
}; };
@@ -25,6 +26,7 @@ struct ScriptContext {
// Convenience helpers for scripts // Convenience helpers for scripts
SceneObject* FindObjectByName(const std::string& name); SceneObject* FindObjectByName(const std::string& name);
SceneObject* FindObjectById(int id); SceneObject* FindObjectById(int id);
SceneObject* ResolveObjectRef(const std::string& ref);
bool IsObjectEnabled() const; bool IsObjectEnabled() const;
void SetObjectEnabled(bool enabled); void SetObjectEnabled(bool enabled);
int GetLayer() const; int GetLayer() const;
@@ -34,11 +36,66 @@ struct ScriptContext {
bool HasTag(const std::string& tag) const; bool HasTag(const std::string& tag) const;
bool IsInLayer(int layer) const; bool IsInLayer(int layer) const;
void SetPosition(const glm::vec3& pos); void SetPosition(const glm::vec3& pos);
void SetPosition2D(const glm::vec2& pos);
void SetRotation(const glm::vec3& rot); void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl); void SetScale(const glm::vec3& scl);
void GetPlanarYawPitchVectors(float pitchDeg, float yawDeg, glm::vec3& outForward, glm::vec3& outRight) const;
glm::vec3 GetMoveInputWASD(float pitchDeg, float yawDeg) const;
bool ApplyMouseLook(float& pitchDeg, float& yawDeg, float sensitivity, float maxDelta, float deltaTime,
bool requireMouseButton) const;
bool IsSprintDown() const;
bool IsJumpDown() const;
bool ResolveGround(float capsuleHalf, float probeExtra, float groundSnap, float verticalVelocity,
glm::vec3* outHitPos = nullptr, bool* outHitGround = nullptr) const;
void ApplyVelocity(const glm::vec3& velocity, float deltaTime);
struct StandaloneMovementSettings {
glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f);
glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f);
glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f);
glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f);
bool enableMouseLook = true;
bool requireMouseButton = false;
bool enforceCollider = true;
bool enforceRigidbody = true;
};
struct StandaloneMovementState {
float pitch = 0.0f;
float yaw = 0.0f;
float verticalVelocity = 0.0f;
};
struct StandaloneMovementDebug {
glm::vec3 velocity = glm::vec3(0.0f);
bool grounded = false;
};
void BindStandaloneMovementSettings(StandaloneMovementSettings& settings);
void DrawStandaloneMovementInspector(StandaloneMovementSettings& settings, bool* showDebug = nullptr);
void TickStandaloneMovement(StandaloneMovementState& state, StandaloneMovementSettings& settings,
float deltaTime, StandaloneMovementDebug* debug = nullptr);
// UI helpers
bool IsUIButtonPressed() const;
bool IsUIInteractable() const;
void SetUIInteractable(bool interactable);
float GetUISliderValue() const;
void SetUISliderValue(float value);
void SetUISliderRange(float minValue, float maxValue);
void SetUILabel(const std::string& label);
void SetUIColor(const glm::vec4& color);
float GetUITextScale() const;
void SetUITextScale(float scale);
void SetUISliderStyle(UISliderStyle style);
void SetUIButtonStyle(UIButtonStyle style);
void SetUIStylePreset(const std::string& name);
void RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false);
void SetFPSCap(bool enabled, float cap = 120.0f);
bool HasRigidbody() const; bool HasRigidbody() const;
bool HasRigidbody2D() const;
bool EnsureCapsuleCollider(float height, float radius);
bool EnsureRigidbody(bool useGravity = true, bool kinematic = false);
bool SetRigidbody2DVelocity(const glm::vec2& velocity);
bool GetRigidbody2DVelocity(glm::vec2& outVelocity) const;
bool SetRigidbodyVelocity(const glm::vec3& velocity); bool SetRigidbodyVelocity(const glm::vec3& velocity);
bool GetRigidbodyVelocity(glm::vec3& outVelocity) const; bool GetRigidbodyVelocity(glm::vec3& outVelocity) const;
bool AddRigidbodyVelocity(const glm::vec3& deltaVelocity);
bool SetRigidbodyAngularVelocity(const glm::vec3& velocity); bool SetRigidbodyAngularVelocity(const glm::vec3& velocity);
bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const; bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const;
bool AddRigidbodyForce(const glm::vec3& force); bool AddRigidbodyForce(const glm::vec3& force);
@@ -63,12 +120,15 @@ struct ScriptContext {
void SetSetting(const std::string& key, const std::string& value); void SetSetting(const std::string& key, const std::string& value);
bool GetSettingBool(const std::string& key, bool fallback = false) const; bool GetSettingBool(const std::string& key, bool fallback = false) const;
void SetSettingBool(const std::string& key, bool value); void SetSettingBool(const std::string& key, bool value);
float GetSettingFloat(const std::string& key, float fallback = 0.0f) const;
void SetSettingFloat(const std::string& key, float value);
glm::vec3 GetSettingVec3(const std::string& key, const glm::vec3& fallback = glm::vec3(0.0f)) const; glm::vec3 GetSettingVec3(const std::string& key, const glm::vec3& fallback = glm::vec3(0.0f)) const;
void SetSettingVec3(const std::string& key, const glm::vec3& value); void SetSettingVec3(const std::string& key, const glm::vec3& value);
// Console helper // Console helper
void AddConsoleMessage(const std::string& message, ConsoleMessageType type = ConsoleMessageType::Info); void AddConsoleMessage(const std::string& message, ConsoleMessageType type = ConsoleMessageType::Info);
// Auto-binding helpers: bind once per call, optionally load stored value, then SaveAutoSettings() writes back on change. // Auto-binding helpers: bind once per call, optionally load stored value.
void AutoSetting(const std::string& key, bool& value); void AutoSetting(const std::string& key, bool& value);
void AutoSetting(const std::string& key, float& value);
void AutoSetting(const std::string& key, glm::vec3& value); void AutoSetting(const std::string& key, glm::vec3& value);
void AutoSetting(const std::string& key, char* buffer, size_t bufferSize); void AutoSetting(const std::string& key, char* buffer, size_t bufferSize);
void SaveAutoSettings(); void SaveAutoSettings();

View File

@@ -47,12 +47,34 @@ void Shader::compileShaders(const char* vertexSource, const char* fragmentSource
glCompileShader(fragment); glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT"); checkCompileErrors(fragment, "FRAGMENT");
int success = 0;
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success) {
glDeleteShader(vertex);
glDeleteShader(fragment);
ID = 0;
return;
}
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success) {
glDeleteShader(vertex);
glDeleteShader(fragment);
ID = 0;
return;
}
ID = glCreateProgram(); ID = glCreateProgram();
glAttachShader(ID, vertex); glAttachShader(ID, vertex);
glAttachShader(ID, fragment); glAttachShader(ID, fragment);
glLinkProgram(ID); glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM"); checkCompileErrors(ID, "PROGRAM");
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if (!success) {
glDeleteProgram(ID);
ID = 0;
}
glDeleteShader(vertex); glDeleteShader(vertex);
glDeleteShader(fragment); glDeleteShader(fragment);
} }
@@ -116,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)); 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

Some files were not shown because too many files have changed in this diff Show More