Compare commits
10 Commits
d05286cb50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 303b835ba7 | |||
|
|
2061d588e7 | ||
|
|
b5bbbc2937 | ||
|
|
ac1fab021c | ||
|
|
67e6ece953 | ||
|
|
ee30412c9b | ||
|
|
9ce4b41e39 | ||
| 5e1d352289 | |||
| 920f201432 | |||
|
|
1bedff2aff |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
build/
|
||||
.cache/
|
||||
Images-thingy/
|
||||
@@ -51,12 +51,35 @@ endif()
|
||||
|
||||
# ==================== Optional PhysX ====================
|
||||
option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON)
|
||||
option(MODULARITY_BUILD_EDITOR "Build the Modularity editor target" ON)
|
||||
|
||||
# ==================== Third-party libraries ====================
|
||||
|
||||
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
# ==================== Mono (managed scripting) ====================
|
||||
option(MODULARITY_USE_MONO "Enable Mono embedding for managed scripts" ON)
|
||||
|
||||
if(MODULARITY_USE_MONO)
|
||||
set(MONO_ROOT ${PROJECT_SOURCE_DIR}/src/ThirdParty/mono CACHE PATH "Mono root directory")
|
||||
find_path(MONO_INCLUDE_DIR mono/jit/jit.h
|
||||
HINTS
|
||||
${MONO_ROOT}/include/mono-2.0
|
||||
${MONO_ROOT}/include
|
||||
)
|
||||
find_library(MONO_LIBRARY
|
||||
NAMES mono-2.0-sgen mono-2.0 monosgen-2.0
|
||||
HINTS
|
||||
${MONO_ROOT}/lib
|
||||
${MONO_ROOT}/lib64
|
||||
)
|
||||
if(NOT MONO_INCLUDE_DIR OR NOT MONO_LIBRARY)
|
||||
message(WARNING "Mono not found. Disabling MODULARITY_USE_MONO. Set MONO_ROOT to a Mono runtime with include/mono-2.0 and libmono-2.0-sgen.")
|
||||
set(MODULARITY_USE_MONO OFF CACHE BOOL "Enable Mono embedding for managed scripts" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# GLAD
|
||||
add_library(glad STATIC src/ThirdParty/glad/glad.c)
|
||||
target_include_directories(glad PUBLIC src/ThirdParty/glad)
|
||||
@@ -105,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/PhysX/.*")
|
||||
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/main_player.cpp")
|
||||
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
|
||||
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
|
||||
|
||||
add_library(core STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
|
||||
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)
|
||||
target_link_libraries(core PUBLIC assimp)
|
||||
target_include_directories(core PUBLIC
|
||||
@@ -118,8 +146,29 @@ target_include_directories(core PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
|
||||
)
|
||||
target_link_libraries(core PUBLIC glad glm imgui imguizmo)
|
||||
if(MODULARITY_USE_MONO)
|
||||
target_include_directories(core PUBLIC ${MONO_INCLUDE_DIR})
|
||||
target_link_libraries(core PUBLIC ${MONO_LIBRARY})
|
||||
endif()
|
||||
target_compile_definitions(core PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
|
||||
target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
add_library(core_player STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_PLAYER)
|
||||
target_link_libraries(core_player PUBLIC assimp)
|
||||
target_include_directories(core_player PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/src
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
|
||||
)
|
||||
target_link_libraries(core_player PUBLIC glad glm imgui imguizmo)
|
||||
if(MODULARITY_USE_MONO)
|
||||
target_include_directories(core_player PUBLIC ${MONO_INCLUDE_DIR})
|
||||
target_link_libraries(core_player PUBLIC ${MONO_LIBRARY})
|
||||
endif()
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
|
||||
target_compile_options(core_player PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
if(MODULARITY_ENABLE_PHYSX)
|
||||
set(PHYSX_ROOT_DIR ${PROJECT_SOURCE_DIR}/src/ThirdParty/PhysX/physx CACHE PATH "PhysX root directory")
|
||||
set(TARGET_BUILD_PLATFORM "linux" CACHE STRING "PhysX build platform (linux/windows)")
|
||||
@@ -130,17 +179,24 @@ if(MODULARITY_ENABLE_PHYSX)
|
||||
target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include)
|
||||
target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
|
||||
target_link_libraries(core PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking)
|
||||
target_include_directories(core_player PUBLIC ${PHYSX_ROOT_DIR}/include)
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
|
||||
target_link_libraries(core_player PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking)
|
||||
endif()
|
||||
|
||||
# ==================== Executable ====================
|
||||
if(MODULARITY_BUILD_EDITOR)
|
||||
add_executable(Modularity src/main.cpp)
|
||||
target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
endif()
|
||||
add_executable(ModularityPlayer src/main_player.cpp)
|
||||
target_compile_options(ModularityPlayer PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
# Link order matters on Linux
|
||||
if(NOT WIN32)
|
||||
find_package(X11 REQUIRED)
|
||||
if(MODULARITY_BUILD_EDITOR)
|
||||
target_include_directories(Modularity PRIVATE ${X11_INCLUDE_DIR})
|
||||
|
||||
target_link_libraries(Modularity PRIVATE
|
||||
core
|
||||
imgui
|
||||
@@ -159,13 +215,44 @@ if(NOT WIN32)
|
||||
)
|
||||
# Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols.
|
||||
target_link_options(Modularity PRIVATE "-rdynamic")
|
||||
endif()
|
||||
|
||||
target_include_directories(ModularityPlayer PRIVATE ${X11_INCLUDE_DIR})
|
||||
target_link_libraries(ModularityPlayer PRIVATE
|
||||
core_player
|
||||
imgui
|
||||
imguizmo
|
||||
glad
|
||||
glm
|
||||
glfw
|
||||
OpenGL::GL
|
||||
pthread
|
||||
dl
|
||||
${X11_LIBRARIES}
|
||||
Xrandr
|
||||
Xi
|
||||
Xinerama
|
||||
Xcursor
|
||||
)
|
||||
target_link_options(ModularityPlayer PRIVATE "-rdynamic")
|
||||
else()
|
||||
if(MODULARITY_BUILD_EDITOR)
|
||||
target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL)
|
||||
endif()
|
||||
target_link_libraries(ModularityPlayer PRIVATE core_player glfw OpenGL::GL)
|
||||
endif()
|
||||
|
||||
# ==================== Copy Resources folder after build ====================
|
||||
if(MODULARITY_BUILD_EDITOR)
|
||||
add_custom_command(TARGET Modularity POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Resources
|
||||
$<TARGET_FILE_DIR:Modularity>/Resources
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET ModularityPlayer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Resources
|
||||
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
|
||||
)
|
||||
|
||||
BIN
Resources/Fonts/TheSunset.ttf
Normal file
BIN
Resources/Fonts/TheSunset.ttf
Normal file
Binary file not shown.
BIN
Resources/Fonts/Thesunsethd-Regular (1).ttf
Normal file
BIN
Resources/Fonts/Thesunsethd-Regular (1).ttf
Normal file
Binary file not shown.
@@ -9,7 +9,9 @@ uniform float threshold = 1.0;
|
||||
void main() {
|
||||
vec3 c = texture(sceneTex, TexCoord).rgb;
|
||||
float luma = dot(c, vec3(0.2125, 0.7154, 0.0721));
|
||||
float bright = max(luma - threshold, 0.0);
|
||||
vec3 masked = c * step(0.0, bright);
|
||||
float knee = 0.25;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -42,17 +42,8 @@ vec3 applyColorAdjust(vec3 color) {
|
||||
return color;
|
||||
}
|
||||
|
||||
vec3 sampleCombined(vec2 uv) {
|
||||
vec3 c = 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));
|
||||
vec3 sampleBase(vec2 uv) {
|
||||
return applyColorAdjust(texture(sceneTex, uv).rgb);
|
||||
}
|
||||
|
||||
float luminance(vec3 c) {
|
||||
@@ -66,24 +57,24 @@ float computeVignette(vec2 uv) {
|
||||
}
|
||||
|
||||
vec3 applyChromatic(vec2 uv) {
|
||||
vec3 base = sampleAdjusted(uv);
|
||||
vec3 base = sampleBase(uv);
|
||||
vec2 dir = uv - vec2(0.5);
|
||||
float dist = max(length(dir), 0.0001);
|
||||
vec2 offset = normalize(dir) * chromaticAmount * (1.0 + dist * 2.0);
|
||||
vec3 rSample = sampleAdjusted(uv + offset);
|
||||
vec3 bSample = sampleAdjusted(uv - offset);
|
||||
vec3 rSample = sampleBase(uv + offset);
|
||||
vec3 bSample = sampleBase(uv - offset);
|
||||
vec3 ca = vec3(rSample.r, base.g, bSample.b);
|
||||
return mix(base, ca, 0.85);
|
||||
}
|
||||
|
||||
float computeAOFactor(vec2 uv) {
|
||||
vec3 centerColor = sampleAdjusted(uv);
|
||||
vec3 centerColor = sampleBase(uv);
|
||||
float centerLum = luminance(centerColor);
|
||||
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));
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
vec2 sampleUv = uv + directions[i] * aoRadius;
|
||||
vec3 sampleColor = sampleAdjusted(sampleUv);
|
||||
vec3 sampleColor = sampleBase(sampleUv);
|
||||
float sampleLum = luminance(sampleColor);
|
||||
occlusion += max(0.0, centerLum - sampleLum);
|
||||
}
|
||||
@@ -92,7 +83,7 @@ float computeAOFactor(vec2 uv) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 color = sampleAdjusted(TexCoord);
|
||||
vec3 color = sampleBase(TexCoord);
|
||||
|
||||
if (enableChromatic) {
|
||||
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);
|
||||
}
|
||||
|
||||
44
Resources/Shaders/skinned_vert.glsl
Normal file
44
Resources/Shaders/skinned_vert.glsl
Normal 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
125
Resources/anim.ini
Normal 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
|
||||
|
||||
@@ -14,45 +14,45 @@ Size=500,250
|
||||
Collapsed=0
|
||||
|
||||
[Window][DockSpace]
|
||||
Pos=0,23
|
||||
Size=1920,983
|
||||
Pos=0,24
|
||||
Size=1900,998
|
||||
Collapsed=0
|
||||
|
||||
[Window][Viewport]
|
||||
Pos=306,46
|
||||
Size=1265,739
|
||||
Pos=304,48
|
||||
Size=1249,747
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
DockId=0x0000000F,0
|
||||
|
||||
[Window][Hierarchy]
|
||||
Pos=0,46
|
||||
Size=304,739
|
||||
Pos=0,48
|
||||
Size=304,747
|
||||
Collapsed=0
|
||||
DockId=0x00000001,0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][Inspector]
|
||||
Pos=1573,46
|
||||
Size=347,960
|
||||
Pos=1553,48
|
||||
Size=347,747
|
||||
Collapsed=0
|
||||
DockId=0x00000008,0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][File Browser]
|
||||
Pos=756,836
|
||||
Size=753,221
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
DockId=0xD71539A0,1
|
||||
|
||||
[Window][Console]
|
||||
Pos=0,787
|
||||
Size=785,219
|
||||
Pos=939,795
|
||||
Size=961,227
|
||||
Collapsed=0
|
||||
DockId=0x00000005,0
|
||||
DockId=0x00000014,0
|
||||
|
||||
[Window][Project]
|
||||
Pos=787,787
|
||||
Size=784,219
|
||||
Pos=0,795
|
||||
Size=939,227
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
DockId=0x00000013,0
|
||||
|
||||
[Window][Launcher]
|
||||
Pos=0,0
|
||||
@@ -60,43 +60,64 @@ Size=1000,800
|
||||
Collapsed=0
|
||||
|
||||
[Window][Camera]
|
||||
Pos=0,46
|
||||
Size=304,739
|
||||
Pos=0,48
|
||||
Size=304,747
|
||||
Collapsed=0
|
||||
DockId=0x00000001,1
|
||||
DockId=0x00000007,1
|
||||
|
||||
[Window][Environment]
|
||||
Pos=1573,46
|
||||
Size=347,960
|
||||
Pos=1553,48
|
||||
Size=347,747
|
||||
Collapsed=0
|
||||
DockId=0x00000008,1
|
||||
DockId=0x00000010,1
|
||||
|
||||
[Window][Project Manager]
|
||||
Pos=787,785
|
||||
Size=784,221
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
DockId=0xD71539A0,1
|
||||
|
||||
[Window][Game Viewport]
|
||||
Pos=306,46
|
||||
Size=1265,739
|
||||
Pos=304,48
|
||||
Size=1249,747
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
DockId=0x0000000F,1
|
||||
|
||||
[Window][Project Settings]
|
||||
Pos=306,46
|
||||
Size=1265,739
|
||||
Pos=304,48
|
||||
Size=1249,747
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
DockId=0x0000000F,2
|
||||
|
||||
[Window][Animation]
|
||||
Pos=583,795
|
||||
Size=738,227
|
||||
Collapsed=0
|
||||
DockId=0x00000013,0
|
||||
|
||||
[Window][Scripting]
|
||||
Pos=304,48
|
||||
Size=1249,747
|
||||
Collapsed=0
|
||||
DockId=0x0000000F,3
|
||||
|
||||
[Table][0xFF88847C,2]
|
||||
RefScale=16
|
||||
Column 0 Width=220
|
||||
Column 1 Weight=1.0000
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X
|
||||
DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y
|
||||
DockNode ID=0x00000003 Parent=0x00000007 SizeRef=1858,739 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000003 SizeRef=304,758 Selected=0xBABDAE5E
|
||||
DockNode ID=0x00000002 Parent=0x00000003 SizeRef=694,758 CentralNode=1 Selected=0xC450F867
|
||||
DockNode ID=0x00000004 Parent=0x00000007 SizeRef=1858,219 Split=X Selected=0xEA83D666
|
||||
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=929,221 Selected=0xEA83D666
|
||||
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0x9C21DE82
|
||||
DockNode ID=0x00000008 Parent=0xD71539A0 SizeRef=347,1015 Selected=0x36DC96AB
|
||||
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867
|
||||
DockNode ID=0x00000001 Parent=0x00000005 SizeRef=1900,747 Split=X Selected=0xE9044848
|
||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=304,486 Selected=0xBABDAE5E
|
||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1596,486 Split=X Selected=0xE9044848
|
||||
DockNode ID=0x0000000F Parent=0x00000008 SizeRef=1249,486 Selected=0xE9044848
|
||||
DockNode ID=0x00000010 Parent=0x00000008 SizeRef=347,486 Selected=0x36DC96AB
|
||||
DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1900,227 Split=X Selected=0x3F1379AF
|
||||
DockNode ID=0x00000013 Parent=0x00000002 SizeRef=939,488 CentralNode=1 Selected=0x9C21DE82
|
||||
DockNode ID=0x00000014 Parent=0x00000002 SizeRef=961,488 Selected=0xEA83D666
|
||||
DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82
|
||||
DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82
|
||||
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666
|
||||
|
||||
|
||||
121
Resources/scripter.ini
Normal file
121
Resources/scripter.ini
Normal 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
282
Scripts/AnimationWindow.cpp
Normal 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", ¤tTime, 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
44
Scripts/FPSDisplay.cpp
Normal 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
297
Scripts/Managed/ModuCPP.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Scripts/Managed/ModuCPP.csproj
Normal file
10
Scripts/Managed/ModuCPP.csproj
Normal 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>
|
||||
68
Scripts/Managed/SampleInspector.cs
Normal file
68
Scripts/Managed/SampleInspector.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Scripts/Managed/SampleInspectorManaged.cs
Normal file
68
Scripts/Managed/SampleInspectorManaged.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json
Normal file
23
Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json
Normal 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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll
Normal file
BIN
Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll
Normal file
Binary file not shown.
BIN
Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb
Normal file
BIN
Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb
Normal file
Binary file not shown.
14
Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json
Normal file
14
Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json
Normal file
24
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json
Normal 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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll
Normal file
BIN
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll
Normal file
Binary file not shown.
BIN
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb
Normal file
BIN
Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb
Normal file
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
|
||||
22
Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs
Normal file
22
Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs
Normal 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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375
|
||||
@@ -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 =
|
||||
@@ -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;
|
||||
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache
Normal file
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
0579f849781bddafc2e55261290008c5a7fb6bddd064d155e3e9c2dd44aec502
|
||||
@@ -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
|
||||
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll
Normal file
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
b14c7a505f46d8314ef755360e8bbee5cc4a67ee7d033805e0a7f8e8d9b71b40
|
||||
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb
Normal file
BIN
Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}}
|
||||
BIN
Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll
Normal file
BIN
Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll
Normal file
Binary file not shown.
BIN
Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll
Normal file
BIN
Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll
Normal file
Binary file not shown.
@@ -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")]
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375
|
||||
@@ -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 =
|
||||
@@ -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;
|
||||
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache
Normal file
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
12a173d9ad34d74a13f6f07a58c9a75f8033484b726d3271d6b9bdffb23c227b
|
||||
@@ -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
|
||||
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll
Normal file
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll
Normal file
Binary file not shown.
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb
Normal file
BIN
Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}}
|
||||
70
Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json
Normal file
70
Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props
Normal file
15
Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props
Normal 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>
|
||||
6
Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets
Normal file
6
Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets
Normal 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>
|
||||
247
Scripts/Managed/obj/project.assets.json
Normal file
247
Scripts/Managed/obj/project.assets.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Scripts/Managed/obj/project.nuget.cache
Normal file
11
Scripts/Managed/obj/project.nuget.cache
Normal 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": []
|
||||
}
|
||||
@@ -36,15 +36,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("RigidbodyTest");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::Checkbox("Launch on Begin", &autoLaunch);
|
||||
changed |= ImGui::Checkbox("Show Velocity Readback", &showVelocity);
|
||||
changed |= ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
|
||||
changed |= ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
ImGui::Checkbox("Launch on Begin", &autoLaunch);
|
||||
ImGui::Checkbox("Show Velocity Readback", &showVelocity);
|
||||
ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
|
||||
ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
|
||||
|
||||
if (ImGui::Button("Launch Now")) {
|
||||
Launch(ctx);
|
||||
@@ -77,12 +72,3 @@ void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
Launch(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void Spec(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
}
|
||||
|
||||
void TestEditor(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
namespace {
|
||||
// Script state (persisted by AutoSetting binder)
|
||||
bool autoRotate = false;
|
||||
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec
|
||||
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
char targetName[128] = "MyTarget";
|
||||
|
||||
// Runtime behavior
|
||||
static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) {
|
||||
if (!autoRotate || !ctx.object) return;
|
||||
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
|
||||
@@ -17,7 +15,6 @@ namespace {
|
||||
}
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
// Auto settings (loaded once, saved only when changed)
|
||||
ctx.AutoSetting("autoRotate", autoRotate);
|
||||
ctx.AutoSetting("spinSpeed", spinSpeed);
|
||||
ctx.AutoSetting("offset", offset);
|
||||
@@ -26,15 +23,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("SampleInspector");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
changed |= ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
changed |= ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
changed |= ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
|
||||
if (ctx.object) {
|
||||
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 (SceneObject* target = ctx.FindObjectByName(targetName)) {
|
||||
if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
|
||||
target->position += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
}
|
||||
|
||||
void Spec(ScriptContext& ctx, float deltaTime) {
|
||||
ApplyAutoRotate(ctx, deltaTime);
|
||||
}
|
||||
|
||||
@@ -8,101 +8,41 @@
|
||||
#include "ScriptRuntime.h"
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
bool autoRotate = false;
|
||||
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f);
|
||||
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
char targetName[128] = "MyTarget";
|
||||
int settingsLoadedForId = -1;
|
||||
ScriptComponent* settingsLoadedForScript = nullptr;
|
||||
|
||||
void setSetting(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (!ctx.script) return;
|
||||
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
|
||||
[&](const ScriptSetting& s) { return s.key == key; });
|
||||
if (it != ctx.script->settings.end()) {
|
||||
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 bindSettings(ScriptContext& ctx) {
|
||||
ctx.AutoSetting("autoRotate", autoRotate);
|
||||
ctx.AutoSetting("spinSpeed", spinSpeed);
|
||||
ctx.AutoSetting("offset", offset);
|
||||
ctx.AutoSetting("targetName", targetName, sizeof(targetName));
|
||||
}
|
||||
|
||||
void applyAutoRotate(ScriptContext& ctx, float deltaTime) {
|
||||
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);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
loadSettings(ctx);
|
||||
bindSettings(ctx);
|
||||
|
||||
ImGui::TextUnformatted("SampleInspector");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("Auto Rotate", &autoRotate)) {
|
||||
persistSettings(ctx);
|
||||
}
|
||||
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::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
persistSettings(ctx);
|
||||
|
||||
if (ctx.object) {
|
||||
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 (SceneObject* target = ctx.FindObjectByName(targetName)) {
|
||||
if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
|
||||
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.
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
// Initialize per-script state here.
|
||||
loadSettings(ctx);
|
||||
bindSettings(ctx);
|
||||
}
|
||||
|
||||
void Spec(ScriptContext& ctx, float deltaTime) {
|
||||
|
||||
@@ -2,203 +2,55 @@
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
struct ControllerState {
|
||||
float pitch = 0.0f;
|
||||
float yaw = 0.0f;
|
||||
float verticalVelocity = 0.0f;
|
||||
namespace
|
||||
{
|
||||
struct ControllerState
|
||||
{
|
||||
ScriptContext::StandaloneMovementState movement; ScriptContext::StandaloneMovementDebug debug;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
std::unordered_map<int, ControllerState> g_states;
|
||||
|
||||
glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f); // walk speed, run speed, jump
|
||||
glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f); // sensitivity, max delta clamp, reserved
|
||||
glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f); // height, radius, ground snap
|
||||
glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f); // gravity, probe extra, max fall speed
|
||||
bool enableMouseLook = true;
|
||||
bool requireMouseButton = false;
|
||||
bool enforceCollider = true;
|
||||
bool enforceRigidbody = true;
|
||||
bool showDebug = false;
|
||||
|
||||
ControllerState& getState(int id) {
|
||||
return g_states[id];
|
||||
ScriptContext::StandaloneMovementSettings g_settings;
|
||||
ControllerState& getState(int id) {return g_states[id];}
|
||||
// aliases for readability
|
||||
glm::vec3& moveTuning = g_settings.moveTuning;
|
||||
glm::vec3& lookTuning = g_settings.lookTuning;
|
||||
glm::vec3& capsuleTuning = g_settings.capsuleTuning;
|
||||
glm::vec3& gravityTuning = g_settings.gravityTuning;
|
||||
bool& enableMouseLook = g_settings.enableMouseLook;
|
||||
bool& requireMouseButton = g_settings.requireMouseButton;
|
||||
bool& enforceCollider = g_settings.enforceCollider;
|
||||
bool& enforceRigidbody = g_settings.enforceRigidbody;
|
||||
}
|
||||
|
||||
void bindSettings(ScriptContext& ctx) {
|
||||
ctx.AutoSetting("moveTuning", moveTuning);
|
||||
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);
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx)
|
||||
{
|
||||
ctx.BindStandaloneMovementSettings(g_settings);
|
||||
ImGui::TextUnformatted("Standalone Movement Controller");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
|
||||
changed |= ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
|
||||
changed |= ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
|
||||
changed |= ImGui::Checkbox("Force Collider", &enforceCollider);
|
||||
changed |= ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
|
||||
changed |= ImGui::Checkbox("Show Debug", &showDebug);
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
|
||||
ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
|
||||
ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
|
||||
ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
|
||||
ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
|
||||
ImGui::Checkbox("Force Collider", &enforceCollider);
|
||||
ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
bindSettings(ctx);
|
||||
ControllerState& state = getState(ctx.object->id);
|
||||
if (!state.initialized) {
|
||||
state.pitch = ctx.object->rotation.x;
|
||||
state.yaw = ctx.object->rotation.y;
|
||||
state.verticalVelocity = 0.0f;
|
||||
state.initialized = true;
|
||||
void Begin(ScriptContext& ctx, float)
|
||||
{
|
||||
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id);
|
||||
if (!s.initialized)
|
||||
{
|
||||
s.movement.pitch = ctx.object->rotation.x;
|
||||
s.movement.yaw = ctx.object->rotation.y;
|
||||
s.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) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
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);
|
||||
void TickUpdate(ScriptContext& ctx, float dt)
|
||||
{
|
||||
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id); ctx.TickStandaloneMovement(s.movement, g_settings, dt, nullptr);
|
||||
}
|
||||
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*/) {}
|
||||
|
||||
74
Scripts/TopDownMovement2D.cpp
Normal file
74
Scripts/TopDownMovement2D.cpp
Normal 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
BIN
TheSunset.ttf
Normal file
Binary file not shown.
BIN
Thesunsethd-Regular (1).ttf
Normal file
BIN
Thesunsethd-Regular (1).ttf
Normal file
Binary file not shown.
@@ -17,7 +17,7 @@ mkdir build
|
||||
cd build
|
||||
|
||||
echo [INFO] Configuring with CMake (Visual Studio 18 2026)...
|
||||
cmake -G "Visual Studio 18 2026" -A x64 ..
|
||||
cmake -A x64 ..
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
|
||||
44
build.sh
44
build.sh
@@ -23,18 +23,56 @@ trap finish EXIT
|
||||
|
||||
echo -e "================================\n Modularity - Native Linux Builder\n================================"
|
||||
|
||||
clean_build=0
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--clean" ]; then
|
||||
clean_build=1
|
||||
fi
|
||||
done
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
if [ -d "build" ]; then
|
||||
echo -e "[i]: Oh! We found an existing build directory.\nRemoving existing folder..."
|
||||
if [ -d "build" ] && [ $clean_build -eq 1 ]; then
|
||||
echo -e "[i]: Cleaning existing build directory..."
|
||||
rm -rf build/
|
||||
echo -e "[i]: Build Has been Removed\nContinuing build"
|
||||
fi
|
||||
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake ..
|
||||
cmake .. -DMONO_ROOT=/usr
|
||||
cmake --build . -- -j"$(nproc)"
|
||||
|
||||
mkdir -p Packages/ThirdParty
|
||||
find . -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
|
||||
-not -path "./Packages/*" -exec cp -f {} Packages/ThirdParty/ \;
|
||||
|
||||
mkdir -p Packages/Engine
|
||||
find . -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
|
||||
-not -path "./Packages/*" -exec cp -f {} Packages/Engine/ \;
|
||||
|
||||
cd ..
|
||||
|
||||
player_cache_dir="build/player-cache"
|
||||
if [ $clean_build -eq 1 ] && [ -d "$player_cache_dir" ]; then
|
||||
echo -e "[i]: Cleaning player cache build directory..."
|
||||
rm -rf "$player_cache_dir"
|
||||
fi
|
||||
|
||||
mkdir -p "$player_cache_dir"
|
||||
cmake -S . -B "$player_cache_dir" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE=Release -DMODULARITY_BUILD_EDITOR=OFF
|
||||
cmake --build "$player_cache_dir" --target ModularityPlayer -- -j"$(nproc)"
|
||||
|
||||
mkdir -p "$player_cache_dir/Packages/ThirdParty"
|
||||
find "$player_cache_dir" -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
|
||||
-not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/ThirdParty/" \;
|
||||
|
||||
mkdir -p "$player_cache_dir/Packages/Engine"
|
||||
find "$player_cache_dir" -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
|
||||
-not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/Engine/" \;
|
||||
|
||||
cd build
|
||||
|
||||
cp -r ../Resources .
|
||||
cp Resources/imgui.ini .
|
||||
ln -sf build/compile_commands.json compile_commands.json
|
||||
@@ -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
|
||||
- Scripts live under `Scripts/` (configurable via `Scripts.modu`).
|
||||
- The engine generates a wrapper per script when compiling. It exports fixed entry points with `extern "C"` linkage:
|
||||
- `Script_OnInspector(ScriptContext&)`
|
||||
- `Script_Begin(ScriptContext&, float deltaTime)`
|
||||
- `Script_Spec(ScriptContext&, float deltaTime)`
|
||||
- `Script_TestEditor(ScriptContext&, float deltaTime)`
|
||||
- `Script_Update(ScriptContext&, float deltaTime)` (fallback if TickUpdate is absent)
|
||||
- `Script_TickUpdate(ScriptContext&, float deltaTime)`
|
||||
- Build config file: `Scripts.modu` (auto-created per project). Keys:
|
||||
- `scriptsDir`, `outDir`, `includeDir=...`, `define=...`, `linux.linkLib`, `win.linkLib`, `cppStandard`.
|
||||
# C++ Scripting
|
||||
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.
|
||||
|
||||
> Notes up front:
|
||||
> - Scripts are not sandboxed. They can crash the editor/game if they dereference bad pointers or do unsafe work.
|
||||
> - Always null-check `ctx.object` (objects can be deleted, disabled, or scripts can be detached).
|
||||
|
||||
## Table of contents
|
||||
- [Quickstart](#quickstart)
|
||||
- [C# managed scripting (experimental)](#c-managed-scripting-experimental)
|
||||
- [Scripts.modu](#scriptsmodu)
|
||||
- [How compilation works](#how-compilation-works)
|
||||
- [Lifecycle hooks](#lifecycle-hooks)
|
||||
- [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 Inspector’s 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
|
||||
- **Inspector**: `Script_OnInspector(ScriptContext&)` is called when the script is inspected in the UI.
|
||||
- **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).
|
||||
- **Tick**: `Script_TickUpdate` runs every frame for each script; `Script_Update` is a fallback if TickUpdate is missing.
|
||||
- All tick-style hooks receive `deltaTime` (seconds) and the `ScriptContext`.
|
||||
All hooks are optional. If a hook is missing, it is simply not called.
|
||||
|
||||
Hook list:
|
||||
- `Script_OnInspector(ScriptContext&)` (manual export required)
|
||||
- `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:
|
||||
- `engine`: pointer to the Engine
|
||||
- `object`: pointer to the owning `SceneObject`
|
||||
- `script`: pointer to the owning `ScriptComponent` (gives access to per-script `settings`)
|
||||
- `engine` (`Engine*`) - engine pointer
|
||||
- `object` (`SceneObject*`) - owning object pointer (may be null)
|
||||
- `script` (`ScriptComponent*`) - owning script component (settings storage)
|
||||
|
||||
## Persisting per-script settings
|
||||
- Each `ScriptComponent` has `settings` (key/value strings) serialized with the scene.
|
||||
- You can read/write them via `ctx.script->settings` or helper functions in your script.
|
||||
- After mutating settings or object transforms, call `ctx.MarkDirty()` so Ctrl+S captures changes.
|
||||
### Object lookup
|
||||
- `FindObjectByName(const std::string&)`
|
||||
- `FindObjectById(int)`
|
||||
|
||||
## 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
|
||||
#include "ScriptRuntime.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
static bool autoRotate = false;
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: `Script_OnInspector` must be exported exactly with `extern "C"` (it is not wrapper-generated).
|
||||
> 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();
|
||||
}
|
||||
```
|
||||
|
||||
### AutoSetting (recommended for inspector UI)
|
||||
`AutoSetting` binds a variable to a key and loads/saves automatically when you call `SaveAutoSettings()`.
|
||||
```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!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sliders as meters (health/ammo)
|
||||
Set `Interactable` to false to make a slider read-only.
|
||||
```cpp
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
ctx.SetUIInteractable(false);
|
||||
ctx.SetUISliderStyle(UISliderStyle::Fill);
|
||||
ctx.SetUISliderRange(0.0f, 100.0f);
|
||||
ctx.SetUISliderValue(health);
|
||||
}
|
||||
```
|
||||
|
||||
### Style presets
|
||||
You can register custom ImGui style presets in code and then select them per UI element in the Inspector.
|
||||
```cpp
|
||||
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);
|
||||
}
|
||||
```
|
||||
Then select **UI -> Style Preset** on a button or slider.
|
||||
|
||||
### Finding other UI objects
|
||||
```cpp
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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 it’s registered.
|
||||
|
||||
Start/stop macros:
|
||||
- `IEnum_Start(fn)` / `IEnum_Stop(fn)` / `IEnum_Ensure(fn)`
|
||||
|
||||
Example (toggle rotation without cluttering TickUpdate):
|
||||
```cpp
|
||||
static bool autoRotate = false;
|
||||
static glm::vec3 speed = {0, 45, 0};
|
||||
|
||||
void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Speed", &speed.x, 1.f, -360.f, 360.f);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
|
||||
void Script_Begin(ScriptContext& ctx, float) {
|
||||
ctx.MarkDirty(); // ensure initial state is saved
|
||||
}
|
||||
|
||||
void Script_TickUpdate(ScriptContext& ctx, float dt) {
|
||||
if (autoRotate && ctx.object) {
|
||||
static void RotateTask(ScriptContext& ctx, float dt) {
|
||||
if (!ctx.object) return;
|
||||
ctx.SetRotation(ctx.object->rotation + speed * dt);
|
||||
}
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
if (autoRotate) IEnum_Ensure(RotateTask);
|
||||
else IEnum_Stop(RotateTask);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime behavior
|
||||
- Scripts tick for all objects every frame, even if not selected.
|
||||
- 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
|
||||
ctx.SetRigidbodyAngularVelocity({0.0f, 3.0f, 0.0f});
|
||||
```
|
||||
- `GetRigidbodyAngularVelocity(out vec3)` reads current angular velocity into `out`. Returns false if unavailable.
|
||||
```cpp
|
||||
glm::vec3 angVel;
|
||||
if (ctx.GetRigidbodyAngularVelocity(angVel)) {
|
||||
ctx.AddConsoleMessage("AngVel Y: " + std::to_string(angVel.y));
|
||||
}
|
||||
```
|
||||
- `AddRigidbodyForce(vec3)` applies continuous force (mass-aware).
|
||||
```cpp
|
||||
ctx.AddRigidbodyForce({0.0f, 0.0f, 25.0f});
|
||||
```
|
||||
- `AddRigidbodyImpulse(vec3)` applies an instant impulse (mass-aware).
|
||||
```cpp
|
||||
ctx.AddRigidbodyImpulse({0.0f, 6.5f, 0.0f});
|
||||
```
|
||||
- `AddRigidbodyTorque(vec3)` applies continuous torque.
|
||||
```cpp
|
||||
ctx.AddRigidbodyTorque({0.0f, 15.0f, 0.0f});
|
||||
```
|
||||
- `AddRigidbodyAngularImpulse(vec3)` applies an instant angular impulse.
|
||||
```cpp
|
||||
ctx.AddRigidbodyAngularImpulse({0.0f, 4.0f, 0.0f});
|
||||
```
|
||||
- `SetRigidbodyRotation(vec3 degrees)` teleports the rigidbody rotation.
|
||||
```cpp
|
||||
ctx.SetRigidbodyRotation({0.0f, 90.0f, 0.0f});
|
||||
```
|
||||
Notes:
|
||||
- These return false if the object has no enabled rigidbody or is kinematic.
|
||||
- Use force/torque for continuous input and impulses for bursty actions.
|
||||
- `SetRigidbodyRotation` is authoritative; use it sparingly during gameplay.
|
||||
- Tasks are stored per `ScriptComponent` instance.
|
||||
- Don’t spam logs every frame inside a task; use “warn once” patterns.
|
||||
|
||||
## 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)
|
||||
Linux example:
|
||||
Linux:
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Windows example:
|
||||
|
||||
Windows:
|
||||
```bat
|
||||
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
|
||||
```
|
||||
|
||||
## 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 don’t 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
17
docs/mono-embedding.md
Normal 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`.
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
void setVec2(const std::string &name, const glm::vec2 &value) const;
|
||||
void setVec3(const std::string &name, const glm::vec3 &value) const;
|
||||
void setMat4(const std::string &name, const glm::mat4 &mat) const;
|
||||
void setMat4Array(const std::string &name, const glm::mat4 *data, int count) const;
|
||||
|
||||
private:
|
||||
std::string readShaderFile(const char* filePath);
|
||||
|
||||
@@ -2,10 +2,143 @@
|
||||
#include "../include/ThirdParty/miniaudio.h"
|
||||
#include "AudioSystem.h"
|
||||
#include <cmath>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kPreviewBuckets = 800;
|
||||
constexpr ma_uint32 kPreviewChunkFrames = 2048;
|
||||
constexpr float kReverbSmoothing = 0.12f;
|
||||
constexpr size_t kReverbCombCount = 4;
|
||||
constexpr size_t kReverbAllpassCount = 2;
|
||||
constexpr float kReverbPreDelayMaxSeconds = 0.2f;
|
||||
constexpr float kReverbReflectionsMaxSeconds = 0.1f;
|
||||
|
||||
float DbToLinear(float db) {
|
||||
return std::pow(10.0f, db / 20.0f);
|
||||
}
|
||||
|
||||
struct ReverbNodeVTable {
|
||||
ma_node_vtable vtable;
|
||||
};
|
||||
|
||||
static void reverb_node_process(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn,
|
||||
float** ppFramesOut, ma_uint32* pFrameCountOut) {
|
||||
if (!pNode || !ppFramesIn || !ppFramesOut) return;
|
||||
auto* node = reinterpret_cast<AudioSystem::SimpleReverbNode*>(pNode);
|
||||
(void)pFrameCountIn;
|
||||
const float* input = ppFramesIn[0];
|
||||
float* output = ppFramesOut[0];
|
||||
if (!input || !output) return;
|
||||
|
||||
ma_uint32 frameCount = *pFrameCountOut;
|
||||
int channels = node->channels;
|
||||
float decayTime = std::max(0.1f, node->decayTime);
|
||||
float diffusion = std::clamp(node->diffusion, 0.0f, 100.0f);
|
||||
float density = std::clamp(node->density, 0.0f, 100.0f);
|
||||
float preDelaySeconds = std::clamp(node->preDelaySeconds, 0.0f, kReverbPreDelayMaxSeconds);
|
||||
float reflectionsDelaySeconds = std::clamp(node->reflectionsDelaySeconds, 0.0f, kReverbReflectionsMaxSeconds);
|
||||
size_t preDelayFrames = static_cast<size_t>(preDelaySeconds * static_cast<float>(node->sampleRate));
|
||||
size_t reflectionsDelayFrames = static_cast<size_t>(reflectionsDelaySeconds * static_cast<float>(node->sampleRate));
|
||||
float wetGain = std::clamp(node->wetGain, 0.0f, 2.0f);
|
||||
float reflectionsGain = std::clamp(node->reflectionsGain, 0.0f, 2.0f);
|
||||
|
||||
float diffusionNorm = diffusion / 100.0f;
|
||||
float densityNorm = density / 100.0f;
|
||||
float allpassGain = 0.2f + 0.55f * diffusionNorm;
|
||||
float densityScale = 0.6f + 0.4f * densityNorm;
|
||||
float combGain = 1.0f / static_cast<float>(node->combBuffers.size());
|
||||
std::array<float, kReverbCombCount> combFeedback{};
|
||||
for (size_t i = 0; i < node->combBuffers.size(); ++i) {
|
||||
float delaySec = static_cast<float>(node->combBuffers[i].size() / channels) / static_cast<float>(node->sampleRate);
|
||||
combFeedback[i] = std::pow(10.0f, (-3.0f * delaySec) / decayTime) * densityScale;
|
||||
}
|
||||
|
||||
float cutoffHz = std::clamp(node->hfReference * node->decayHFRatio, 500.0f, 20000.0f);
|
||||
float lpAlpha = std::exp(-2.0f * PI * cutoffHz / static_cast<float>(node->sampleRate));
|
||||
|
||||
for (ma_uint32 frame = 0; frame < frameCount; ++frame) {
|
||||
size_t preReadIndex = node->preDelayMaxFrames > 0
|
||||
? (node->preDelayIndex + node->preDelayMaxFrames - preDelayFrames) % node->preDelayMaxFrames
|
||||
: 0;
|
||||
size_t reflectionsReadIndex = node->reflectionsMaxFrames > 0
|
||||
? (node->reflectionsIndex + node->reflectionsMaxFrames - reflectionsDelayFrames) % node->reflectionsMaxFrames
|
||||
: 0;
|
||||
|
||||
for (int ch = 0; ch < channels; ++ch) {
|
||||
float inSample = input[frame * channels + ch];
|
||||
float preSample = inSample;
|
||||
if (!node->preDelayBuffer.empty()) {
|
||||
size_t writeBase = node->preDelayIndex * channels;
|
||||
size_t readBase = preReadIndex * channels;
|
||||
preSample = node->preDelayBuffer[readBase + ch];
|
||||
node->preDelayBuffer[writeBase + ch] = inSample;
|
||||
}
|
||||
|
||||
float reflectionsSample = 0.0f;
|
||||
if (!node->reflectionsBuffer.empty()) {
|
||||
size_t writeBase = node->reflectionsIndex * channels;
|
||||
size_t readBase = reflectionsReadIndex * channels;
|
||||
reflectionsSample = node->reflectionsBuffer[readBase + ch];
|
||||
node->reflectionsBuffer[writeBase + ch] = preSample;
|
||||
}
|
||||
|
||||
float combSum = 0.0f;
|
||||
for (size_t i = 0; i < node->combBuffers.size(); ++i) {
|
||||
auto& buffer = node->combBuffers[i];
|
||||
size_t idx = node->combIndex[i];
|
||||
size_t base = idx * channels + ch;
|
||||
float y = buffer[base];
|
||||
buffer[base] = preSample + y * combFeedback[i];
|
||||
combSum += y;
|
||||
}
|
||||
|
||||
combSum *= combGain;
|
||||
float apOut = combSum;
|
||||
for (size_t i = 0; i < node->allpassBuffers.size(); ++i) {
|
||||
auto& buffer = node->allpassBuffers[i];
|
||||
size_t idx = node->allpassIndex[i];
|
||||
size_t base = idx * channels + ch;
|
||||
float buf = buffer[base];
|
||||
float y = -allpassGain * apOut + buf;
|
||||
buffer[base] = apOut + buf * allpassGain;
|
||||
apOut = y;
|
||||
}
|
||||
|
||||
float wetSample = apOut * wetGain + reflectionsSample * reflectionsGain;
|
||||
float lp = node->lpState.empty() ? wetSample : (lpAlpha * node->lpState[ch] + (1.0f - lpAlpha) * wetSample);
|
||||
if (!node->lpState.empty()) node->lpState[ch] = lp;
|
||||
output[frame * channels + ch] = lp;
|
||||
}
|
||||
|
||||
if (!node->preDelayBuffer.empty()) {
|
||||
node->preDelayIndex = (node->preDelayIndex + 1) % node->preDelayMaxFrames;
|
||||
}
|
||||
if (!node->reflectionsBuffer.empty()) {
|
||||
node->reflectionsIndex = (node->reflectionsIndex + 1) % node->reflectionsMaxFrames;
|
||||
}
|
||||
for (size_t i = 0; i < node->combIndex.size(); ++i) {
|
||||
node->combIndex[i] = (node->combIndex[i] + 1) % (node->combBuffers[i].size() / channels);
|
||||
}
|
||||
for (size_t i = 0; i < node->allpassIndex.size(); ++i) {
|
||||
node->allpassIndex[i] = (node->allpassIndex[i] + 1) % (node->allpassBuffers[i].size() / channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ma_result reverb_node_get_required_input_frames(ma_node* pNode, ma_uint32 outputFrameCount, ma_uint32* pInputFrameCount) {
|
||||
(void)pNode;
|
||||
if (pInputFrameCount) *pInputFrameCount = outputFrameCount;
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
static ma_node_vtable g_reverb_node_vtable = {
|
||||
reverb_node_process,
|
||||
reverb_node_get_required_input_frames,
|
||||
1,
|
||||
1,
|
||||
0
|
||||
};
|
||||
}
|
||||
|
||||
bool AudioSystem::init() {
|
||||
@@ -15,6 +148,65 @@ bool AudioSystem::init() {
|
||||
std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n";
|
||||
return false;
|
||||
}
|
||||
ma_uint32 channels = ma_engine_get_channels(&engine);
|
||||
ma_uint32 sampleRate = ma_engine_get_sample_rate(&engine);
|
||||
ma_splitter_node_config splitterConfig = ma_splitter_node_config_init(channels);
|
||||
res = ma_splitter_node_init(ma_engine_get_node_graph(&engine), &splitterConfig, nullptr, &reverbSplitter);
|
||||
if (res == MA_SUCCESS) {
|
||||
ma_node_config nodeConfig = ma_node_config_init();
|
||||
nodeConfig.vtable = &g_reverb_node_vtable;
|
||||
nodeConfig.pInputChannels = reinterpret_cast<const ma_uint32*>(&channels);
|
||||
nodeConfig.pOutputChannels = reinterpret_cast<const ma_uint32*>(&channels);
|
||||
res = ma_node_init(ma_engine_get_node_graph(&engine), &nodeConfig, nullptr, reinterpret_cast<ma_node*>(&reverbNode));
|
||||
if (res == MA_SUCCESS) {
|
||||
reverbNode.channels = static_cast<int>(channels);
|
||||
reverbNode.sampleRate = static_cast<int>(sampleRate);
|
||||
reverbNode.preDelayMaxFrames = static_cast<size_t>(kReverbPreDelayMaxSeconds * sampleRate);
|
||||
reverbNode.reflectionsMaxFrames = static_cast<size_t>(kReverbReflectionsMaxSeconds * sampleRate);
|
||||
reverbNode.preDelayBuffer.assign(reverbNode.preDelayMaxFrames * channels, 0.0f);
|
||||
reverbNode.reflectionsBuffer.assign(reverbNode.reflectionsMaxFrames * channels, 0.0f);
|
||||
reverbNode.lpState.assign(channels, 0.0f);
|
||||
|
||||
const float combDelayMs[kReverbCombCount] = { 29.7f, 37.1f, 41.1f, 43.7f };
|
||||
reverbNode.combBuffers.resize(kReverbCombCount);
|
||||
reverbNode.combIndex.assign(kReverbCombCount, 0);
|
||||
for (size_t i = 0; i < kReverbCombCount; ++i) {
|
||||
size_t frames = static_cast<size_t>((combDelayMs[i] / 1000.0f) * sampleRate);
|
||||
frames = std::max<size_t>(1, frames);
|
||||
reverbNode.combBuffers[i].assign(frames * channels, 0.0f);
|
||||
}
|
||||
|
||||
const float allpassDelayMs[kReverbAllpassCount] = { 5.0f, 1.7f };
|
||||
reverbNode.allpassBuffers.resize(kReverbAllpassCount);
|
||||
reverbNode.allpassIndex.assign(kReverbAllpassCount, 0);
|
||||
for (size_t i = 0; i < kReverbAllpassCount; ++i) {
|
||||
size_t frames = static_cast<size_t>((allpassDelayMs[i] / 1000.0f) * sampleRate);
|
||||
frames = std::max<size_t>(1, frames);
|
||||
reverbNode.allpassBuffers[i].assign(frames * channels, 0.0f);
|
||||
}
|
||||
|
||||
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbSplitter), 0, ma_engine_get_endpoint(&engine), 0);
|
||||
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbSplitter), 1, reinterpret_cast<ma_node*>(&reverbNode), 0);
|
||||
ma_node_attach_output_bus(reinterpret_cast<ma_node*>(&reverbNode), 0, ma_engine_get_endpoint(&engine), 0);
|
||||
ma_sound_group_config groupConfig = ma_sound_group_config_init_2(&engine);
|
||||
groupConfig.pInitialAttachment = reinterpret_cast<ma_node*>(&reverbSplitter);
|
||||
groupConfig.initialAttachmentInputBusIndex = 0;
|
||||
res = ma_sound_group_init_ex(&engine, &groupConfig, &reverbGroup);
|
||||
if (res == MA_SUCCESS) {
|
||||
reverbReady = true;
|
||||
ma_sound_group_set_spatialization_enabled(&reverbGroup, MA_FALSE);
|
||||
ma_sound_group_set_attenuation_model(&reverbGroup, ma_attenuation_model_none);
|
||||
ma_sound_group_start(&reverbGroup);
|
||||
ma_node_set_output_bus_volume(reinterpret_cast<ma_node*>(&reverbSplitter), 0, 1.0f);
|
||||
} else {
|
||||
ma_node_uninit(reinterpret_cast<ma_node*>(&reverbNode), nullptr);
|
||||
ma_splitter_node_uninit(&reverbSplitter, nullptr);
|
||||
}
|
||||
} else {
|
||||
ma_splitter_node_uninit(&reverbSplitter, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
return true;
|
||||
}
|
||||
@@ -22,6 +214,7 @@ bool AudioSystem::init() {
|
||||
void AudioSystem::shutdown() {
|
||||
stopPreview();
|
||||
destroyActiveSounds();
|
||||
shutdownReverbGraph();
|
||||
if (initialized) {
|
||||
ma_engine_uninit(&engine);
|
||||
initialized = false;
|
||||
@@ -81,7 +274,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
|
||||
&engine,
|
||||
obj.audioSource.clipPath.c_str(),
|
||||
MA_SOUND_FLAG_STREAM,
|
||||
nullptr,
|
||||
reverbReady ? &reverbGroup : nullptr,
|
||||
nullptr,
|
||||
&snd->sound
|
||||
);
|
||||
@@ -104,6 +297,26 @@ void AudioSystem::refreshSoundParams(const SceneObject& obj, ActiveSound& snd) {
|
||||
ma_sound_set_looping(&snd.sound, obj.audioSource.loop ? MA_TRUE : MA_FALSE);
|
||||
ma_sound_set_volume(&snd.sound, obj.audioSource.volume);
|
||||
ma_sound_set_spatialization_enabled(&snd.sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE);
|
||||
if (obj.audioSource.spatial) {
|
||||
switch (obj.audioSource.rolloffMode) {
|
||||
case AudioRolloffMode::Linear:
|
||||
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_linear);
|
||||
break;
|
||||
case AudioRolloffMode::Exponential:
|
||||
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_exponential);
|
||||
break;
|
||||
case AudioRolloffMode::Custom:
|
||||
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none);
|
||||
break;
|
||||
case AudioRolloffMode::Logarithmic:
|
||||
default:
|
||||
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_inverse);
|
||||
break;
|
||||
}
|
||||
ma_sound_set_rolloff(&snd.sound, std::max(0.01f, obj.audioSource.rolloff));
|
||||
} else {
|
||||
ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none);
|
||||
}
|
||||
ma_sound_set_min_distance(&snd.sound, minDist);
|
||||
ma_sound_set_max_distance(&snd.sound, maxDist);
|
||||
ma_sound_set_position(&snd.sound, obj.position.x, obj.position.y, obj.position.z);
|
||||
@@ -120,6 +333,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
ma_engine_listener_set_position(&engine, 0, listenerCamera.position.x, listenerCamera.position.y, listenerCamera.position.z);
|
||||
ma_engine_listener_set_direction(&engine, 0, listenerCamera.front.x, listenerCamera.front.y, listenerCamera.front.z);
|
||||
ma_engine_listener_set_world_up(&engine, 0, listenerCamera.up.x, listenerCamera.up.y, listenerCamera.up.z);
|
||||
updateReverb(objects, listenerCamera.position);
|
||||
|
||||
if (!playing) {
|
||||
destroyActiveSounds();
|
||||
@@ -144,6 +358,10 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
|
||||
if (ensureSoundFor(obj)) {
|
||||
refreshSoundParams(obj, *activeSounds[obj.id]);
|
||||
if (obj.audioSource.spatial && obj.audioSource.rolloffMode == AudioRolloffMode::Custom) {
|
||||
float attenuation = computeCustomAttenuation(obj, listenerCamera.position);
|
||||
ma_sound_set_volume(&activeSounds[obj.id]->sound, obj.audioSource.volume * attenuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +471,152 @@ bool AudioSystem::setObjectVolume(const SceneObject& obj, float volume) {
|
||||
return true;
|
||||
}
|
||||
|
||||
float AudioSystem::computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const {
|
||||
float minDist = std::max(0.1f, obj.audioSource.minDistance);
|
||||
float maxDist = std::max(obj.audioSource.maxDistance, minDist + 0.5f);
|
||||
float dist = glm::length(listenerPos - obj.position);
|
||||
if (dist <= minDist) return 1.0f;
|
||||
if (dist >= maxDist) return std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f);
|
||||
|
||||
float range = maxDist - minDist;
|
||||
float midRatio = std::clamp(obj.audioSource.customMidDistance, 0.0f, 1.0f);
|
||||
float midDist = minDist + range * midRatio;
|
||||
float midGain = std::clamp(obj.audioSource.customMidGain, 0.0f, 1.0f);
|
||||
float endGain = std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f);
|
||||
|
||||
if (dist <= midDist) {
|
||||
float t = (dist - minDist) / std::max(0.001f, midDist - minDist);
|
||||
return std::clamp(1.0f + (midGain - 1.0f) * t, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float t = (dist - midDist) / std::max(0.001f, maxDist - midDist);
|
||||
return std::clamp(midGain + (endGain - midGain) * t, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void AudioSystem::updateReverb(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos) {
|
||||
if (!reverbReady) return;
|
||||
float blend = 0.0f;
|
||||
ReverbSettings target = getReverbTarget(objects, listenerPos, blend);
|
||||
applyReverbSettings(target, blend);
|
||||
}
|
||||
|
||||
AudioSystem::ReverbSettings AudioSystem::getReverbTarget(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos, float& outBlend) const {
|
||||
ReverbSettings target{};
|
||||
float bestBlend = 0.0f;
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled || !obj.hasReverbZone || !obj.reverbZone.enabled) continue;
|
||||
const auto& zone = obj.reverbZone;
|
||||
float blend = 0.0f;
|
||||
|
||||
if (zone.shape == ReverbZoneShape::Sphere) {
|
||||
float minDist = std::max(0.0f, zone.minDistance);
|
||||
float maxDist = std::max(zone.maxDistance, minDist + 0.01f);
|
||||
float radius = std::max(0.01f, zone.radius);
|
||||
float dist = glm::length(listenerPos - obj.position);
|
||||
if (dist > radius) continue;
|
||||
maxDist = std::min(maxDist, radius);
|
||||
if (dist >= maxDist) continue;
|
||||
if (dist <= minDist) {
|
||||
blend = 1.0f;
|
||||
} else {
|
||||
blend = std::clamp((maxDist - dist) / (maxDist - minDist), 0.0f, 1.0f);
|
||||
}
|
||||
} else {
|
||||
glm::vec3 halfSize = glm::max(zone.boxSize * 0.5f, glm::vec3(0.01f));
|
||||
glm::vec3 delta = glm::abs(listenerPos - obj.position);
|
||||
if (delta.x > halfSize.x || delta.y > halfSize.y || delta.z > halfSize.z) continue;
|
||||
float edgeDistance = std::min({halfSize.x - delta.x, halfSize.y - delta.y, halfSize.z - delta.z});
|
||||
if (zone.blendDistance <= 0.001f) {
|
||||
blend = 1.0f;
|
||||
} else {
|
||||
blend = std::clamp(edgeDistance / zone.blendDistance, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (blend > bestBlend) {
|
||||
bestBlend = blend;
|
||||
target.room = zone.room;
|
||||
target.roomHF = zone.roomHF;
|
||||
target.roomLF = zone.roomLF;
|
||||
target.decayTime = zone.decayTime;
|
||||
target.decayHFRatio = zone.decayHFRatio;
|
||||
target.reflections = zone.reflections;
|
||||
target.reflectionsDelay = zone.reflectionsDelay;
|
||||
target.reverb = zone.reverb;
|
||||
target.reverbDelay = zone.reverbDelay;
|
||||
target.hfReference = zone.hfReference;
|
||||
target.lfReference = zone.lfReference;
|
||||
target.roomRolloffFactor = zone.roomRolloffFactor;
|
||||
target.diffusion = zone.diffusion;
|
||||
target.density = zone.density;
|
||||
}
|
||||
}
|
||||
|
||||
outBlend = bestBlend;
|
||||
return target;
|
||||
}
|
||||
|
||||
void AudioSystem::applyReverbSettings(const ReverbSettings& target, float blend) {
|
||||
ReverbSettings mixed{};
|
||||
mixed.room = target.room;
|
||||
mixed.roomHF = target.roomHF;
|
||||
mixed.roomLF = target.roomLF;
|
||||
mixed.decayTime = std::max(0.1f, target.decayTime);
|
||||
mixed.decayHFRatio = std::clamp(target.decayHFRatio, 0.1f, 2.0f);
|
||||
mixed.reflections = target.reflections;
|
||||
mixed.reflectionsDelay = std::clamp(target.reflectionsDelay, 0.0f, kReverbReflectionsMaxSeconds);
|
||||
mixed.reverb = target.reverb;
|
||||
mixed.reverbDelay = std::clamp(target.reverbDelay, 0.0f, kReverbPreDelayMaxSeconds);
|
||||
mixed.hfReference = std::clamp(target.hfReference, 1000.0f, 20000.0f);
|
||||
mixed.lfReference = std::clamp(target.lfReference, 20.0f, 1000.0f);
|
||||
mixed.roomRolloffFactor = std::max(0.0f, target.roomRolloffFactor);
|
||||
mixed.diffusion = std::clamp(target.diffusion, 0.0f, 100.0f);
|
||||
mixed.density = std::clamp(target.density, 0.0f, 100.0f);
|
||||
|
||||
currentReverb.room = currentReverb.room + (mixed.room - currentReverb.room) * kReverbSmoothing;
|
||||
currentReverb.roomHF = currentReverb.roomHF + (mixed.roomHF - currentReverb.roomHF) * kReverbSmoothing;
|
||||
currentReverb.roomLF = currentReverb.roomLF + (mixed.roomLF - currentReverb.roomLF) * kReverbSmoothing;
|
||||
currentReverb.decayTime = currentReverb.decayTime + (mixed.decayTime - currentReverb.decayTime) * kReverbSmoothing;
|
||||
currentReverb.decayHFRatio = currentReverb.decayHFRatio + (mixed.decayHFRatio - currentReverb.decayHFRatio) * kReverbSmoothing;
|
||||
currentReverb.reflections = currentReverb.reflections + (mixed.reflections - currentReverb.reflections) * kReverbSmoothing;
|
||||
currentReverb.reflectionsDelay = currentReverb.reflectionsDelay + (mixed.reflectionsDelay - currentReverb.reflectionsDelay) * kReverbSmoothing;
|
||||
currentReverb.reverb = currentReverb.reverb + (mixed.reverb - currentReverb.reverb) * kReverbSmoothing;
|
||||
currentReverb.reverbDelay = currentReverb.reverbDelay + (mixed.reverbDelay - currentReverb.reverbDelay) * kReverbSmoothing;
|
||||
currentReverb.hfReference = currentReverb.hfReference + (mixed.hfReference - currentReverb.hfReference) * kReverbSmoothing;
|
||||
currentReverb.lfReference = currentReverb.lfReference + (mixed.lfReference - currentReverb.lfReference) * kReverbSmoothing;
|
||||
currentReverb.roomRolloffFactor = currentReverb.roomRolloffFactor + (mixed.roomRolloffFactor - currentReverb.roomRolloffFactor) * kReverbSmoothing;
|
||||
currentReverb.diffusion = currentReverb.diffusion + (mixed.diffusion - currentReverb.diffusion) * kReverbSmoothing;
|
||||
currentReverb.density = currentReverb.density + (mixed.density - currentReverb.density) * kReverbSmoothing;
|
||||
|
||||
constexpr float kDbSoftening = 0.5f;
|
||||
constexpr float kWetScale = 0.25f;
|
||||
float reflectionsGain = DbToLinear((currentReverb.reflections + currentReverb.room) * kDbSoftening) * (blend * kWetScale);
|
||||
float reverbGain = DbToLinear((currentReverb.reverb + currentReverb.room) * kDbSoftening) * (blend * kWetScale);
|
||||
float dry = std::clamp(1.0f - blend * (currentReverb.roomRolloffFactor * 0.05f), 0.2f, 1.0f);
|
||||
|
||||
ma_node_set_output_bus_volume(reinterpret_cast<ma_node*>(&reverbSplitter), 0, dry);
|
||||
reverbNode.wetGain = std::clamp(reverbGain, 0.0f, 1.0f);
|
||||
reverbNode.reflectionsGain = std::clamp(reflectionsGain, 0.0f, 1.0f);
|
||||
reverbNode.decayTime = currentReverb.decayTime;
|
||||
reverbNode.decayHFRatio = currentReverb.decayHFRatio;
|
||||
reverbNode.diffusion = currentReverb.diffusion;
|
||||
reverbNode.density = currentReverb.density;
|
||||
reverbNode.hfReference = currentReverb.hfReference;
|
||||
reverbNode.preDelaySeconds = currentReverb.reverbDelay;
|
||||
reverbNode.reflectionsDelaySeconds = currentReverb.reflectionsDelay;
|
||||
}
|
||||
|
||||
void AudioSystem::shutdownReverbGraph() {
|
||||
if (reverbReady) {
|
||||
ma_sound_group_uninit(&reverbGroup);
|
||||
ma_node_uninit(reinterpret_cast<ma_node*>(&reverbNode), nullptr);
|
||||
ma_splitter_node_uninit(&reverbSplitter, nullptr);
|
||||
reverbReady = false;
|
||||
}
|
||||
currentReverb = ReverbSettings{};
|
||||
}
|
||||
|
||||
AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
|
||||
AudioClipPreview preview;
|
||||
preview.path = path;
|
||||
|
||||
@@ -42,7 +42,51 @@ public:
|
||||
bool setObjectLoop(const SceneObject& obj, bool loop);
|
||||
bool setObjectVolume(const SceneObject& obj, float volume);
|
||||
|
||||
struct SimpleReverbNode {
|
||||
ma_node_base baseNode;
|
||||
int channels = 0;
|
||||
int sampleRate = 0;
|
||||
std::vector<std::vector<float>> combBuffers;
|
||||
std::vector<size_t> combIndex;
|
||||
std::vector<std::vector<float>> allpassBuffers;
|
||||
std::vector<size_t> allpassIndex;
|
||||
std::vector<float> preDelayBuffer;
|
||||
size_t preDelayIndex = 0;
|
||||
std::vector<float> reflectionsBuffer;
|
||||
size_t reflectionsIndex = 0;
|
||||
std::vector<float> lpState;
|
||||
float wetGain = 0.0f;
|
||||
float reflectionsGain = 0.0f;
|
||||
float decayTime = 1.5f;
|
||||
float decayHFRatio = 0.5f;
|
||||
float diffusion = 100.0f;
|
||||
float density = 100.0f;
|
||||
float hfReference = 5000.0f;
|
||||
float preDelaySeconds = 0.01f;
|
||||
float reflectionsDelaySeconds = 0.01f;
|
||||
size_t preDelayMaxFrames = 0;
|
||||
size_t reflectionsMaxFrames = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
struct ReverbSettings {
|
||||
float room = -10000.0f;
|
||||
float roomHF = -10000.0f;
|
||||
float roomLF = -10000.0f;
|
||||
float decayTime = 1.5f;
|
||||
float decayHFRatio = 0.5f;
|
||||
float reflections = -10000.0f;
|
||||
float reflectionsDelay = 0.01f;
|
||||
float reverb = -10000.0f;
|
||||
float reverbDelay = 0.01f;
|
||||
float hfReference = 5000.0f;
|
||||
float lfReference = 250.0f;
|
||||
float roomRolloffFactor = 0.0f;
|
||||
float diffusion = 100.0f;
|
||||
float density = 100.0f;
|
||||
float dry = 1.0f;
|
||||
};
|
||||
|
||||
struct ActiveSound {
|
||||
ma_sound sound;
|
||||
std::string clipPath;
|
||||
@@ -56,6 +100,12 @@ private:
|
||||
std::unordered_map<std::string, AudioClipPreview> previewCache;
|
||||
std::unordered_set<std::string> missingClips;
|
||||
|
||||
SimpleReverbNode reverbNode{};
|
||||
ma_splitter_node reverbSplitter{};
|
||||
ma_sound_group reverbGroup{};
|
||||
bool reverbReady = false;
|
||||
ReverbSettings currentReverb{};
|
||||
|
||||
ma_sound previewSound{};
|
||||
bool previewActive = false;
|
||||
std::string previewPath;
|
||||
@@ -63,5 +113,10 @@ private:
|
||||
void destroyActiveSounds();
|
||||
bool ensureSoundFor(const SceneObject& obj);
|
||||
void refreshSoundParams(const SceneObject& obj, ActiveSound& snd);
|
||||
float computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const;
|
||||
AudioClipPreview loadPreview(const std::string& path);
|
||||
void updateReverb(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos);
|
||||
ReverbSettings getReverbTarget(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos, float& outBlend) const;
|
||||
void applyReverbSettings(const ReverbSettings& target, float blend);
|
||||
void shutdownReverbGraph();
|
||||
};
|
||||
|
||||
170
src/EditorUI.cpp
170
src/EditorUI.cpp
@@ -1,6 +1,6 @@
|
||||
#include "EditorUI.h"
|
||||
|
||||
// FileBrowser implementation
|
||||
#pragma region File Browser
|
||||
FileBrowser::FileBrowser() {
|
||||
currentPath = fs::current_path();
|
||||
projectRoot = currentPath;
|
||||
@@ -96,8 +96,9 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
|
||||
|
||||
// Model files
|
||||
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
|
||||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" ||
|
||||
ext == ".stl" || ext == ".x" || ext == ".md5mesh" || ext == ".rmesh") {
|
||||
ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".b3d" ||
|
||||
ext == ".ply" || ext == ".stl" || ext == ".x" || ext == ".md5mesh" ||
|
||||
ext == ".rmesh") {
|
||||
return FileCategory::Model;
|
||||
}
|
||||
|
||||
@@ -183,10 +184,66 @@ bool FileBrowser::matchesFilter(const fs::directory_entry& entry) const {
|
||||
|
||||
return filenameLower.find(filterLower) != std::string::npos;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region ImGui Theme
|
||||
void applyModernTheme() {
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec4* colors = style.Colors;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
const float fontSizeBase = 18.0f;
|
||||
const float fontSizeOffset = -2.5f;
|
||||
const float fontSize = std::max(1.0f, fontSizeBase + fontSizeOffset);
|
||||
ImFont* editorFont = nullptr;
|
||||
fs::path primaryFontPath;
|
||||
const fs::path fontCandidates[] = {
|
||||
fs::path("Resources") / "Fonts" / "TheSunset.ttf",
|
||||
fs::path("Resources") / "Fonts" / "Thesunsethd-Regular (1).ttf",
|
||||
fs::path("TheSunset.ttf"),
|
||||
fs::path("Thesunsethd-Regular (1).ttf")
|
||||
};
|
||||
for (const auto& fontPath : fontCandidates) {
|
||||
if (!fs::exists(fontPath)) {
|
||||
continue;
|
||||
}
|
||||
const std::string fontPathStr = fontPath.string();
|
||||
editorFont = io.Fonts->AddFontFromFileTTF(fontPathStr.c_str(), fontSize);
|
||||
if (editorFont) {
|
||||
primaryFontPath = fontPath;
|
||||
io.FontDefault = editorFont;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!editorFont) {
|
||||
std::cerr << "[WARN] Failed to load editor font (TheSunset) from Resources/Fonts."
|
||||
<< std::endl;
|
||||
} else {
|
||||
const fs::path fallbackCandidates[] = {
|
||||
fs::path("Resources") / "Fonts" / "TheSunset.ttf",
|
||||
fs::path("TheSunset.ttf")
|
||||
};
|
||||
if (primaryFontPath.filename() != "TheSunset.ttf") {
|
||||
for (const auto& fallbackPath : fallbackCandidates) {
|
||||
if (!fs::exists(fallbackPath)) {
|
||||
continue;
|
||||
}
|
||||
const std::string fallbackPathStr = fallbackPath.string();
|
||||
ImFontConfig mergeConfig;
|
||||
mergeConfig.MergeMode = true;
|
||||
ImFont* fallbackFont = io.Fonts->AddFontFromFileTTF(
|
||||
fallbackPathStr.c_str(),
|
||||
fontSize,
|
||||
&mergeConfig,
|
||||
io.Fonts->GetGlyphRangesDefault()
|
||||
);
|
||||
if (!fallbackFont) {
|
||||
std::cerr << "[WARN] Failed to merge fallback font: "
|
||||
<< fallbackPathStr << std::endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImVec4 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f);
|
||||
ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f);
|
||||
@@ -251,20 +308,85 @@ void applyModernTheme() {
|
||||
colors[ImGuiCol_NavHighlight] = accent;
|
||||
colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f);
|
||||
applyEditorLayoutPreset(style);
|
||||
}
|
||||
|
||||
style.WindowRounding = 10.0f;
|
||||
void applyEditorLayoutPreset(ImGuiStyle& style) {
|
||||
style.WindowPadding = ImVec2(3.0f, 3.0f);
|
||||
style.FramePadding = ImVec2(4.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(10.0f, 5.0f);
|
||||
style.ItemInnerSpacing = ImVec2(2.0f, 2.0f);
|
||||
style.CellPadding = ImVec2(4.0f, 2.0f);
|
||||
style.TouchExtraPadding = ImVec2(0.0f, 0.0f);
|
||||
style.IndentSpacing = 11.0f;
|
||||
style.GrabMinSize = 8.0f;
|
||||
|
||||
style.WindowBorderSize = 0.0f;
|
||||
style.ChildBorderSize = 1.0f;
|
||||
style.PopupBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
|
||||
style.WindowRounding = 12.0f;
|
||||
style.ChildRounding = 12.0f;
|
||||
style.FrameRounding = 10.0f;
|
||||
style.FrameRounding = 12.0f;
|
||||
style.PopupRounding = 12.0f;
|
||||
style.GrabRounding = 12.0f;
|
||||
|
||||
style.ScrollbarSize = 11.0f;
|
||||
style.ScrollbarRounding = 10.0f;
|
||||
style.GrabRounding = 8.0f;
|
||||
style.ScrollbarPadding = 1.0f;
|
||||
|
||||
style.TabBorderSize = 1.0f;
|
||||
style.TabBarBorderSize = 1.0f;
|
||||
style.TabBarOverlineSize = 1.0f;
|
||||
style.TabMinWidthBase = 1.0f;
|
||||
style.TabMinWidthShrink = 80.0f;
|
||||
style.TabCloseButtonMinWidthSelected = -1.0f;
|
||||
style.TabCloseButtonMinWidthUnselected = 0.0f;
|
||||
style.TabRounding = 10.0f;
|
||||
|
||||
style.WindowPadding = ImVec2(12.0f, 12.0f);
|
||||
style.FramePadding = ImVec2(10.0f, 6.0f);
|
||||
style.ItemSpacing = ImVec2(10.0f, 8.0f);
|
||||
style.ItemInnerSpacing = ImVec2(8.0f, 6.0f);
|
||||
style.IndentSpacing = 18.0f;
|
||||
style.TableAngledHeadersAngle = 35.0f;
|
||||
style.TableAngledHeadersTextAlign = ImVec2(0.50f, 0.00f);
|
||||
|
||||
style.TreeLinesFlags = ImGuiTreeNodeFlags_DrawLinesNone;
|
||||
style.TreeLinesSize = 1.0f;
|
||||
style.TreeLinesRounding = 0.0f;
|
||||
|
||||
style.WindowTitleAlign = ImVec2(0.50f, 0.50f);
|
||||
style.WindowBorderHoverPadding = 6.0f;
|
||||
style.WindowMenuButtonPosition = ImGuiDir_None;
|
||||
|
||||
style.ColorButtonPosition = ImGuiDir_Right;
|
||||
style.ButtonTextAlign = ImVec2(0.50f, 0.50f);
|
||||
style.SelectableTextAlign = ImVec2(0.00f, 0.00f);
|
||||
style.SeparatorTextBorderSize = 2.0f;
|
||||
style.SeparatorTextAlign = ImVec2(0.50f, 0.50f);
|
||||
style.SeparatorTextPadding = ImVec2(4.0f, 0.0f);
|
||||
style.LogSliderDeadzone = 4.0f;
|
||||
style.ImageBorderSize = 0.0f;
|
||||
|
||||
style.DockingNodeHasCloseButton = true;
|
||||
style.DockingSeparatorSize = 0.0f;
|
||||
|
||||
style.DisplayWindowPadding = ImVec2(19.0f, 19.0f);
|
||||
style.DisplaySafeAreaPadding = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void applyPixelStyle(ImGuiStyle& style) {
|
||||
applyEditorLayoutPreset(style);
|
||||
style.WindowRounding = 0.0f;
|
||||
style.ChildRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.ScrollbarRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
|
||||
style.WindowPadding = ImVec2(8.0f, 6.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.IndentSpacing = 14.0f;
|
||||
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
@@ -272,6 +394,31 @@ void applyModernTheme() {
|
||||
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) {
|
||||
static bool dockspaceOpen = true;
|
||||
static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_None;
|
||||
@@ -304,3 +451,4 @@ void setupDockspace(const std::function<void()>& menuBarContent) {
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <functional>
|
||||
#include "Common.h"
|
||||
|
||||
#pragma region File Browser Enums
|
||||
|
||||
enum class FileBrowserViewMode {
|
||||
List,
|
||||
Grid
|
||||
@@ -20,6 +22,9 @@ enum class FileCategory {
|
||||
Text,
|
||||
Unknown
|
||||
};
|
||||
#pragma endregion
|
||||
|
||||
#pragma region File Browser
|
||||
|
||||
class FileBrowser {
|
||||
public:
|
||||
@@ -40,6 +45,7 @@ public:
|
||||
|
||||
FileBrowser();
|
||||
|
||||
// Call refresh after mutating currentPath/searchFilter/showHiddenFiles.
|
||||
void refresh();
|
||||
void navigateUp();
|
||||
void navigateTo(const fs::path& path);
|
||||
@@ -57,9 +63,16 @@ public:
|
||||
// Legacy compatibility
|
||||
bool isOBJFile(const fs::directory_entry& entry) const;
|
||||
};
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Editor UI Helpers
|
||||
|
||||
// Apply the modern dark theme to ImGui
|
||||
void applyModernTheme();
|
||||
void applyEditorLayoutPreset(ImGuiStyle& style);
|
||||
void applyPixelStyle(ImGuiStyle& style);
|
||||
void applySuperRoundStyle(ImGuiStyle& style);
|
||||
|
||||
// Setup ImGui dockspace for the editor
|
||||
void setupDockspace(const std::function<void()>& menuBarContent = nullptr);
|
||||
#pragma endregion
|
||||
|
||||
554
src/EditorWindows/AnimationWindow.cpp
Normal file
554
src/EditorWindows/AnimationWindow.cpp
Normal 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();
|
||||
}
|
||||
253
src/EditorWindows/BuildSettingsWindow.cpp
Normal file
253
src/EditorWindows/BuildSettingsWindow.cpp
Normal 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();
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
#pragma region Environment Window
|
||||
void Engine::renderEnvironmentWindow() {
|
||||
if (!showEnvironmentWindow) return;
|
||||
ImGui::Begin("Environment", &showEnvironmentWindow);
|
||||
@@ -74,7 +75,9 @@ void Engine::renderEnvironmentWindow() {
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Camera Window
|
||||
void Engine::renderCameraWindow() {
|
||||
if (!showCameraWindow) return;
|
||||
ImGui::Begin("Camera", &showCameraWindow);
|
||||
@@ -96,3 +99,4 @@ void Engine::renderCameraWindow() {
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
#pragma region File Icons
|
||||
namespace FileIcons {
|
||||
namespace {
|
||||
ImU32 BlendColor(ImU32 a, ImU32 b, float t) {
|
||||
@@ -401,7 +402,9 @@ namespace FileIcons {
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region File Actions
|
||||
namespace {
|
||||
enum class CreateKind {
|
||||
Folder,
|
||||
@@ -475,8 +478,10 @@ namespace {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
|
||||
#pragma region File Browser Panel
|
||||
// Uses FileBrowser state for navigation, selection, and drag-drop.
|
||||
void Engine::renderFileBrowserPanel() {
|
||||
ImGui::Begin("Project", &showFileBrowser);
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
@@ -496,6 +501,7 @@ void Engine::renderFileBrowserPanel() {
|
||||
static fs::path pendingDeletePath;
|
||||
static fs::path pendingRenamePath;
|
||||
static char renameName[256] = "";
|
||||
bool settingsDirty = false;
|
||||
|
||||
auto openEntry = [&](const fs::directory_entry& entry) {
|
||||
if (entry.is_directory()) {
|
||||
@@ -531,6 +537,10 @@ void Engine::renderFileBrowserPanel() {
|
||||
logToConsole("Loaded scene: " + sceneName);
|
||||
return;
|
||||
}
|
||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
|
||||
openScriptInEditor(entry.path());
|
||||
return;
|
||||
}
|
||||
openPathInShell(entry.path());
|
||||
};
|
||||
|
||||
@@ -619,6 +629,15 @@ void Engine::renderFileBrowserPanel() {
|
||||
return false;
|
||||
};
|
||||
|
||||
auto normalizePath = [](const fs::path& path) {
|
||||
std::error_code ec;
|
||||
fs::path canonical = fs::weakly_canonical(path, ec);
|
||||
if (!ec) {
|
||||
return canonical;
|
||||
}
|
||||
return path.lexically_normal();
|
||||
};
|
||||
|
||||
// Get colors for categories
|
||||
auto getCategoryColor = [](FileCategory cat) -> ImU32 {
|
||||
switch (cat) {
|
||||
@@ -737,16 +756,24 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::TextDisabled("Size");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(90);
|
||||
ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx");
|
||||
if (ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx")) {
|
||||
settingsDirty = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
|
||||
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
|
||||
settingsDirty = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(showFileBrowserSidebar ? "Side" : "Side", ImVec2(52, 0))) {
|
||||
showFileBrowserSidebar = !showFileBrowserSidebar;
|
||||
settingsDirty = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle sidebar");
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
@@ -761,6 +788,159 @@ void Engine::renderFileBrowserPanel() {
|
||||
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
|
||||
ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
|
||||
if (showFileBrowserSidebar) {
|
||||
float minSidebarWidth = 160.0f;
|
||||
float maxSidebarWidth = std::max(minSidebarWidth, ImGui::GetContentRegionAvail().x * 0.5f);
|
||||
fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth);
|
||||
|
||||
ImGui::BeginChild("FileSidebar", ImVec2(fileBrowserSidebarWidth, 0), true);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
|
||||
ImGui::TextDisabled("Favorites");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+")) {
|
||||
fs::path current = normalizePath(fileBrowser.currentPath);
|
||||
bool exists = false;
|
||||
for (const auto& fav : fileBrowserFavorites) {
|
||||
if (normalizePath(fav) == current) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
fileBrowserFavorites.push_back(current);
|
||||
settingsDirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add current folder");
|
||||
|
||||
fs::path baseRoot = fileBrowser.projectRoot.empty()
|
||||
? projectManager.currentProject.projectPath
|
||||
: fileBrowser.projectRoot;
|
||||
fs::path normalizedCurrent = normalizePath(fileBrowser.currentPath);
|
||||
|
||||
for (size_t i = 0; i < fileBrowserFavorites.size(); ++i) {
|
||||
fs::path fav = fileBrowserFavorites[i];
|
||||
std::string label;
|
||||
std::error_code ec;
|
||||
fs::path rel = fs::relative(fav, baseRoot, ec);
|
||||
std::string relStr = rel.generic_string();
|
||||
if (!ec && !rel.empty() && relStr.find("..") != 0) {
|
||||
label = relStr;
|
||||
if (label.empty() || label == ".") {
|
||||
label = "Project";
|
||||
}
|
||||
} else {
|
||||
label = fav.filename().string();
|
||||
if (label.empty()) {
|
||||
label = fav.string();
|
||||
}
|
||||
}
|
||||
|
||||
bool exists = fs::exists(fav);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (!exists) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Selectable(label.c_str(), normalizePath(fav) == normalizedCurrent)) {
|
||||
if (exists) {
|
||||
fileBrowser.navigateTo(fav);
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem("FavContext")) {
|
||||
if (ImGui::MenuItem("Remove")) {
|
||||
fileBrowserFavorites.erase(fileBrowserFavorites.begin() + static_cast<int>(i));
|
||||
settingsDirty = true;
|
||||
ImGui::EndPopup();
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
if (exists && ImGui::MenuItem("Open in File Explorer")) {
|
||||
openPathInFileManager(fav);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Folders");
|
||||
ImGui::BeginChild("FolderTree", ImVec2(0, 0), false);
|
||||
|
||||
auto drawFolderTree = [&](auto&& self, const fs::path& path) -> void {
|
||||
if (!fs::exists(path)) {
|
||||
return;
|
||||
}
|
||||
std::string name = path.filename().string();
|
||||
if (name.empty()) {
|
||||
name = "Project";
|
||||
}
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_OpenOnDoubleClick |
|
||||
ImGuiTreeNodeFlags_SpanFullWidth;
|
||||
if (fileBrowser.currentPath == path) {
|
||||
flags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
ImGui::PushID(path.string().c_str());
|
||||
bool open = ImGui::TreeNodeEx(name.c_str(), flags);
|
||||
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
|
||||
fileBrowser.navigateTo(path);
|
||||
}
|
||||
if (open) {
|
||||
std::vector<fs::path> dirs;
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::directory_iterator(path, ec)) {
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
if (!entry.is_directory()) {
|
||||
continue;
|
||||
}
|
||||
std::string dirName = entry.path().filename().string();
|
||||
if (!fileBrowser.showHiddenFiles && !dirName.empty() && dirName[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
dirs.push_back(entry.path());
|
||||
}
|
||||
std::sort(dirs.begin(), dirs.end(), [](const fs::path& a, const fs::path& b) {
|
||||
return a.filename().string() < b.filename().string();
|
||||
});
|
||||
for (const auto& dir : dirs) {
|
||||
self(self, dir);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
};
|
||||
|
||||
if (!baseRoot.empty()) {
|
||||
drawFolderTree(drawFolderTree, baseRoot);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
float splitterHeight = ImGui::GetContentRegionAvail().y;
|
||||
if (splitterHeight < 1.0f) {
|
||||
splitterHeight = 1.0f;
|
||||
}
|
||||
ImGui::InvisibleButton("SidebarSplitter", ImVec2(4.0f, splitterHeight));
|
||||
if (ImGui::IsItemHovered() || ImGui::IsItemActive()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
|
||||
}
|
||||
if (ImGui::IsItemActive()) {
|
||||
fileBrowserSidebarWidth += ImGui::GetIO().MouseDelta.x;
|
||||
fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth);
|
||||
settingsDirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
ImGui::BeginChild("FileMain", ImVec2(0, 0), false);
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
@@ -1163,9 +1343,14 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (settingsDirty) {
|
||||
saveEditorUserSettings();
|
||||
}
|
||||
|
||||
if (triggerDeletePopup) {
|
||||
ImGui::OpenPopup("Confirm Delete");
|
||||
triggerDeletePopup = false;
|
||||
@@ -1242,3 +1427,4 @@ void Engine::renderFileBrowserPanel() {
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
#pragma region ImGui Helpers
|
||||
namespace ImGui {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
} // 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 {
|
||||
struct PackageTaskResult {
|
||||
bool success = false;
|
||||
@@ -117,8 +148,9 @@ struct PackageTaskState {
|
||||
std::future<PackageTaskResult> future;
|
||||
};
|
||||
} // namespace
|
||||
#pragma endregion
|
||||
|
||||
|
||||
#pragma region Launcher
|
||||
void Engine::renderLauncher() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 displaySize = io.DisplaySize;
|
||||
@@ -184,7 +216,7 @@ void Engine::renderLauncher() {
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity");
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Debug Build V0.7.0");
|
||||
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V6.3");
|
||||
|
||||
|
||||
ImGui::EndChild();
|
||||
@@ -345,13 +377,10 @@ void Engine::renderLauncher() {
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextDisabled("Modularity Engine - Version 0.6.8");
|
||||
|
||||
ImGui::TextDisabled("Modularity Engine - Beta V6.3");
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -363,8 +392,74 @@ void Engine::renderLauncher() {
|
||||
renderNewProjectDialog();
|
||||
if (projectManager.showOpenProjectDialog)
|
||||
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() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
||||
@@ -441,7 +536,9 @@ void Engine::renderNewProjectDialog() {
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Open Project Dialog
|
||||
void Engine::renderOpenProjectDialog() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
||||
@@ -497,7 +594,9 @@ void Engine::renderOpenProjectDialog() {
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Project Browser Panel
|
||||
void Engine::renderProjectBrowserPanel() {
|
||||
ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f);
|
||||
ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f);
|
||||
@@ -793,3 +892,4 @@ void Engine::renderProjectBrowserPanel() {
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
538
src/EditorWindows/ScriptingWindow.cpp
Normal file
538
src/EditorWindows/ScriptingWindow.cpp
Normal 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
3545
src/Engine.cpp
3545
src/Engine.cpp
File diff suppressed because it is too large
Load Diff
253
src/Engine.h
253
src/Engine.h
@@ -12,10 +12,19 @@
|
||||
#include "PhysicsSystem.h"
|
||||
#include "AudioSystem.h"
|
||||
#include "PackageManager.h"
|
||||
#include "ManagedScriptRuntime.h"
|
||||
#include "ThirdParty/ImGuiColorTextEdit/TextEditor.h"
|
||||
#include "../include/Window/Window.h"
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
void window_size_callback(GLFWwindow* window, int width, int height);
|
||||
fs::path resolveScriptsConfigPath(const Project& project);
|
||||
|
||||
class Engine {
|
||||
private:
|
||||
@@ -68,7 +77,15 @@ private:
|
||||
bool showConsole = true;
|
||||
bool showProjectBrowser = true; // Now merged into file browser
|
||||
bool showMeshBuilder = false;
|
||||
bool showBuildSettings = false;
|
||||
bool showStyleEditor = false;
|
||||
bool showScriptingWindow = false;
|
||||
bool firstFrame = true;
|
||||
bool playerMode = false;
|
||||
bool autoStartRequested = false;
|
||||
bool autoStartPlayerMode = false;
|
||||
std::string autoStartProjectPath;
|
||||
std::string autoStartSceneName;
|
||||
std::vector<std::string> consoleLog;
|
||||
int draggedObjectId = -1;
|
||||
|
||||
@@ -97,8 +114,34 @@ private:
|
||||
|
||||
char fileBrowserSearch[256] = "";
|
||||
float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range
|
||||
float fileBrowserSidebarWidth = 220.0f;
|
||||
bool showFileBrowserSidebar = true;
|
||||
std::vector<fs::path> fileBrowserFavorites;
|
||||
std::string uiStylePresetName = "Current";
|
||||
enum class UIAnimationMode {
|
||||
Off = 0,
|
||||
Snappy = 1,
|
||||
Fluid = 2
|
||||
};
|
||||
enum class WorkspaceMode {
|
||||
Default = 0,
|
||||
Animation = 1,
|
||||
Scripting = 2
|
||||
};
|
||||
UIAnimationMode uiAnimationMode = UIAnimationMode::Off;
|
||||
WorkspaceMode currentWorkspace = WorkspaceMode::Default;
|
||||
bool workspaceLayoutDirty = false;
|
||||
bool pendingWorkspaceReload = false;
|
||||
fs::path pendingWorkspaceIniPath;
|
||||
bool editorSettingsDirty = false;
|
||||
bool showEnvironmentWindow = true;
|
||||
bool showCameraWindow = true;
|
||||
bool showAnimationWindow = false;
|
||||
int animationTargetId = -1;
|
||||
int animationSelectedKey = -1;
|
||||
float animationCurrentTime = 0.0f;
|
||||
bool animationIsPlaying = false;
|
||||
float animationLastAppliedTime = -1.0f;
|
||||
bool hierarchyShowTexturePreview = false;
|
||||
bool hierarchyPreviewNearest = false;
|
||||
std::unordered_map<std::string, bool> texturePreviewFilterOverrides;
|
||||
@@ -113,32 +156,188 @@ private:
|
||||
int previewCameraId = -1;
|
||||
bool gameViewCursorLocked = false;
|
||||
bool gameViewportFocused = false;
|
||||
bool showUITextOverlay = false;
|
||||
bool showGameProfiler = true;
|
||||
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;
|
||||
MeshBuilder meshBuilder;
|
||||
char meshBuilderPath[260] = "";
|
||||
char meshBuilderFaceInput[128] = "";
|
||||
bool meshEditMode = false;
|
||||
bool meshEditLoaded = false;
|
||||
bool meshEditDirty = false;
|
||||
bool meshEditExtrudeMode = false;
|
||||
std::string meshEditPath;
|
||||
RawMeshAsset meshEditAsset;
|
||||
std::vector<int> meshEditSelectedVertices;
|
||||
std::vector<int> meshEditSelectedEdges; // indices into generated edge list
|
||||
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
|
||||
struct UIAnimationState {
|
||||
float hover = 0.0f;
|
||||
float active = 0.0f;
|
||||
float sliderValue = 0.0f;
|
||||
bool initialized = false;
|
||||
};
|
||||
std::unordered_map<int, UIAnimationState> uiAnimationStates;
|
||||
struct UIWorldCamera2D {
|
||||
glm::vec2 position = glm::vec2(0.0f);
|
||||
float zoom = 100.0f; // pixels per world unit
|
||||
glm::vec2 viewportSize = glm::vec2(0.0f);
|
||||
|
||||
glm::vec2 WorldToScreen(const glm::vec2& world) const {
|
||||
return glm::vec2(
|
||||
(world.x - position.x) * zoom + viewportSize.x * 0.5f,
|
||||
(position.y - world.y) * zoom + viewportSize.y * 0.5f
|
||||
);
|
||||
}
|
||||
|
||||
glm::vec2 ScreenToWorld(const glm::vec2& screen) const {
|
||||
return glm::vec2(
|
||||
(screen.x - viewportSize.x * 0.5f) / zoom + position.x,
|
||||
position.y - (screen.y - viewportSize.y * 0.5f) / zoom
|
||||
);
|
||||
}
|
||||
};
|
||||
bool uiWorldMode = false;
|
||||
bool uiWorldPanning = false;
|
||||
UIWorldCamera2D uiWorldCamera;
|
||||
bool consoleWrapText = true;
|
||||
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
|
||||
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
||||
ScriptCompiler scriptCompiler;
|
||||
ScriptRuntime scriptRuntime;
|
||||
ManagedScriptRuntime managedRuntime;
|
||||
PhysicsSystem physics;
|
||||
AudioSystem audio;
|
||||
bool showCompilePopup = false;
|
||||
bool compilePopupOpened = false;
|
||||
double compilePopupHideTime = 0.0;
|
||||
bool lastCompileSuccess = false;
|
||||
std::string lastCompileStatus;
|
||||
std::string lastCompileLog;
|
||||
float compileProgress = 0.0f;
|
||||
std::string compileStage;
|
||||
enum class BuildPlatform {
|
||||
Windows = 0,
|
||||
Linux = 1,
|
||||
Android = 2
|
||||
};
|
||||
struct BuildSceneEntry {
|
||||
std::string name;
|
||||
bool enabled = true;
|
||||
};
|
||||
struct BuildSettings {
|
||||
BuildPlatform platform = BuildPlatform::Windows;
|
||||
std::string architecture = "x86_64";
|
||||
bool developmentBuild = false;
|
||||
bool autoConnectProfiler = false;
|
||||
bool scriptDebugging = false;
|
||||
bool deepProfiling = false;
|
||||
bool scriptsOnlyBuild = false;
|
||||
bool serverBuild = false;
|
||||
std::string compressionMethod = "Default";
|
||||
std::vector<BuildSceneEntry> scenes;
|
||||
};
|
||||
BuildSettings buildSettings;
|
||||
int buildSettingsSelectedIndex = -1;
|
||||
bool buildSettingsDirty = false;
|
||||
struct ExportJobResult {
|
||||
bool success = false;
|
||||
std::string message;
|
||||
fs::path outputDir;
|
||||
};
|
||||
struct ExportJobState {
|
||||
bool active = false;
|
||||
bool done = false;
|
||||
bool success = false;
|
||||
bool cancelled = false;
|
||||
float progress = 0.0f;
|
||||
std::string status;
|
||||
std::string log;
|
||||
fs::path outputDir;
|
||||
bool runAfter = false;
|
||||
std::future<ExportJobResult> future;
|
||||
};
|
||||
ExportJobState exportJob;
|
||||
std::atomic<bool> exportCancelRequested = false;
|
||||
std::mutex exportMutex;
|
||||
bool showExportDialog = false;
|
||||
bool exportRunAfter = false;
|
||||
char exportOutputPath[512] = "";
|
||||
struct ScriptCompileJobResult {
|
||||
bool success = false;
|
||||
bool isManaged = false;
|
||||
fs::path scriptPath;
|
||||
fs::path binaryPath;
|
||||
std::string compiledSource;
|
||||
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 testMode = 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
|
||||
SceneObject* getSelectedObject();
|
||||
glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
|
||||
@@ -154,8 +353,10 @@ private:
|
||||
void importOBJToScene(const std::string& filepath, const std::string& objectName);
|
||||
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
|
||||
void convertModelToRawMesh(const std::string& filepath);
|
||||
void createRMeshPrimitive(const std::string& primitiveName);
|
||||
bool ensureMeshEditTarget(SceneObject* obj);
|
||||
bool syncMeshEditToGPU(SceneObject* obj);
|
||||
bool saveMeshEditAsset(std::string& error);
|
||||
void handleKeyboardShortcuts();
|
||||
void OpenProjectPath(const std::string& path);
|
||||
|
||||
@@ -167,6 +368,7 @@ private:
|
||||
void renderPlayControlsBar();
|
||||
void renderEnvironmentWindow();
|
||||
void renderCameraWindow();
|
||||
void renderAnimationWindow();
|
||||
void renderHierarchyPanel();
|
||||
void renderObjectNode(SceneObject& obj, const std::string& filter,
|
||||
std::vector<bool>& ancestorHasNext, bool isLast, int depth);
|
||||
@@ -175,15 +377,56 @@ private:
|
||||
void renderInspectorPanel();
|
||||
void renderConsolePanel();
|
||||
void renderViewport();
|
||||
void renderPlayerViewport();
|
||||
void renderGameViewportWindow();
|
||||
void renderBuildSettingsWindow();
|
||||
void renderScriptingWindow();
|
||||
void renderDialogs();
|
||||
void updateCompileJob();
|
||||
void renderProjectBrowserPanel();
|
||||
void renderScriptEditorWindows();
|
||||
void refreshScriptEditorWindows();
|
||||
void refreshScriptingFileList();
|
||||
Camera makeCameraFromObject(const SceneObject& obj) const;
|
||||
void compileScriptFile(const fs::path& scriptPath);
|
||||
void updateAutoCompileScripts();
|
||||
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 updatePlayerController(float delta);
|
||||
void updateRigidbody2D(float delta);
|
||||
void updateCameraFollow2D(float delta);
|
||||
void updateSkeletalAnimations(float delta);
|
||||
void updateSkinningMatrices();
|
||||
void rebuildSkeletalBindings();
|
||||
void initUIStylePresets();
|
||||
int findUIStylePreset(const std::string& name) const;
|
||||
const UIStylePreset* getUIStylePreset(const std::string& name) const;
|
||||
void registerUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace);
|
||||
bool applyUIStylePresetByName(const std::string& name);
|
||||
void applyWorkspacePreset(WorkspaceMode mode, bool rebuildLayout);
|
||||
void buildWorkspaceLayout(WorkspaceMode mode);
|
||||
fs::path getEditorUserSettingsPath() const;
|
||||
fs::path getEditorLayoutPath() const;
|
||||
fs::path getWorkspaceLayoutPath(WorkspaceMode mode) const;
|
||||
void loadEditorUserSettings();
|
||||
void saveEditorUserSettings() const;
|
||||
void exportEditorThemeLayout();
|
||||
void resetBuildSettings();
|
||||
void loadBuildSettings();
|
||||
void saveBuildSettings();
|
||||
bool addSceneToBuildSettings(const std::string& sceneName, bool enabled);
|
||||
void loadAutoStartConfig();
|
||||
void applyAutoStartMode();
|
||||
void startExportBuild(const fs::path& outputDir, bool runAfter);
|
||||
void pollExportBuild();
|
||||
|
||||
void renderFileBrowserToolbar();
|
||||
void renderFileBrowserBreadcrumb();
|
||||
@@ -191,6 +434,7 @@ private:
|
||||
void renderFileBrowserListView();
|
||||
void renderFileContextMenu(const fs::directory_entry& entry);
|
||||
void handleFileDoubleClick(const fs::directory_entry& entry);
|
||||
void openScriptInEditor(const fs::path& path);
|
||||
ImVec4 getFileCategoryColor(FileCategory category) const;
|
||||
const char* getFileCategoryIconText(FileCategory category) const;
|
||||
|
||||
@@ -241,6 +485,10 @@ public:
|
||||
SceneObject* findObjectByName(const std::string& name);
|
||||
SceneObject* findObjectById(int id);
|
||||
fs::path resolveScriptBinary(const fs::path& sourcePath);
|
||||
fs::path resolveManagedAssembly(const fs::path& sourcePath);
|
||||
fs::path getManagedProjectPath() const;
|
||||
fs::path getManagedOutputDll() const;
|
||||
void compileManagedScripts();
|
||||
void markProjectDirty();
|
||||
// Script-accessible logging wrapper
|
||||
void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type);
|
||||
@@ -265,4 +513,7 @@ public:
|
||||
bool setAudioVolumeFromScript(int id, float volume);
|
||||
bool setAudioClipFromScript(int id, const std::string& path);
|
||||
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
145
src/ManagedBindings.cpp
Normal 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
55
src/ManagedBindings.h
Normal 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();
|
||||
452
src/ManagedScriptRuntime.cpp
Normal file
452
src/ManagedScriptRuntime.cpp
Normal 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
|
||||
50
src/ManagedScriptRuntime.h
Normal file
50
src/ManagedScriptRuntime.h
Normal 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();
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MeshBuilder.h"
|
||||
|
||||
#pragma region State Reset
|
||||
void MeshBuilder::clear() {
|
||||
mesh = RawMeshAsset();
|
||||
hasMesh = false;
|
||||
@@ -7,7 +8,9 @@ void MeshBuilder::clear() {
|
||||
selectedVertex = -1;
|
||||
loadedPath.clear();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region File IO
|
||||
bool MeshBuilder::load(const std::string& path, std::string& error) {
|
||||
auto& loader = getModelLoader();
|
||||
RawMeshAsset loaded;
|
||||
@@ -35,7 +38,9 @@ bool MeshBuilder::save(const std::string& path, std::string& error) {
|
||||
dirty = false;
|
||||
return true;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Geometry Editing
|
||||
void MeshBuilder::recomputeNormals() {
|
||||
if (mesh.positions.empty()) return;
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Common.h"
|
||||
#include "ModelLoader.h"
|
||||
|
||||
#pragma region Mesh Builder State
|
||||
// Lightweight mesh editing state used by the MeshBuilder panel.
|
||||
class MeshBuilder {
|
||||
public:
|
||||
@@ -11,7 +12,9 @@ public:
|
||||
std::string loadedPath;
|
||||
bool dirty = false;
|
||||
int selectedVertex = -1;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Mesh Builder API
|
||||
bool load(const std::string& path, std::string& error);
|
||||
bool save(const std::string& path, std::string& error);
|
||||
void clear();
|
||||
@@ -20,3 +23,4 @@ public:
|
||||
// Add a new face defined by vertex indices (3 = triangle, 4 = quad fan).
|
||||
bool addFace(const std::vector<uint32_t>& indices, std::string& error);
|
||||
};
|
||||
#pragma endregion
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include <fstream>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
#include <assimp/material.h>
|
||||
#include "ThirdParty/glm/gtc/quaternion.hpp"
|
||||
|
||||
ModelLoader& ModelLoader::getInstance() {
|
||||
static ModelLoader instance;
|
||||
@@ -11,6 +15,13 @@ ModelLoader& ModelLoader::getInstance() {
|
||||
}
|
||||
|
||||
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out);
|
||||
static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
|
||||
std::vector<OBJLoader::LoadedMesh>& loadedMeshes,
|
||||
ModelSceneData& out, std::string& errorMsg);
|
||||
static void buildSceneNodes(const aiScene* scene,
|
||||
const std::vector<int>& meshIndices,
|
||||
ModelSceneData& out);
|
||||
static glm::mat4 aiToGlm(const aiMatrix4x4& m);
|
||||
|
||||
ModelLoader& getModelLoader() {
|
||||
return ModelLoader::getInstance();
|
||||
@@ -91,6 +102,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
||||
if (loadedMeshes[i].path == filepath) {
|
||||
result.success = true;
|
||||
result.meshIndex = static_cast<int>(i);
|
||||
result.meshIndices.push_back(result.meshIndex);
|
||||
const auto& mesh = loadedMeshes[i];
|
||||
result.vertexCount = mesh.vertexCount;
|
||||
result.faceCount = mesh.faceCount;
|
||||
@@ -273,6 +285,92 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ModelLoader::loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg) {
|
||||
out = ModelSceneData();
|
||||
|
||||
if (!isSupported(filepath)) {
|
||||
errorMsg = "Unsupported file format: " + fs::path(filepath).extension().string();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cached = cachedScenes.find(filepath);
|
||||
if (cached != cachedScenes.end()) {
|
||||
out = cached->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int importFlags =
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_FlipUVs |
|
||||
aiProcess_CalcTangentSpace |
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_SortByPType |
|
||||
aiProcess_ValidateDataStructure;
|
||||
|
||||
const aiScene* scene = importer.ReadFile(filepath, importFlags);
|
||||
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
|
||||
errorMsg = "Assimp error: " + std::string(importer.GetErrorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!buildSceneMeshes(filepath, scene, loadedMeshes, out, errorMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
buildSceneNodes(scene, out.meshIndices, out);
|
||||
|
||||
out.animations.clear();
|
||||
if (scene->mNumAnimations > 0) {
|
||||
out.animations.reserve(scene->mNumAnimations);
|
||||
for (unsigned int i = 0; i < scene->mNumAnimations; ++i) {
|
||||
aiAnimation* anim = scene->mAnimations[i];
|
||||
ModelSceneData::AnimationClip clip;
|
||||
clip.name = anim->mName.C_Str();
|
||||
if (clip.name.empty()) {
|
||||
clip.name = "Clip_" + std::to_string(i);
|
||||
}
|
||||
clip.duration = anim->mDuration;
|
||||
clip.ticksPerSecond = anim->mTicksPerSecond != 0.0 ? anim->mTicksPerSecond : 25.0;
|
||||
clip.channels.reserve(anim->mNumChannels);
|
||||
for (unsigned int c = 0; c < anim->mNumChannels; ++c) {
|
||||
aiNodeAnim* ch = anim->mChannels[c];
|
||||
ModelSceneData::AnimChannel channel;
|
||||
channel.nodeName = ch->mNodeName.C_Str();
|
||||
channel.positions.reserve(ch->mNumPositionKeys);
|
||||
for (unsigned int k = 0; k < ch->mNumPositionKeys; ++k) {
|
||||
const auto& key = ch->mPositionKeys[k];
|
||||
ModelSceneData::AnimVecKey vk;
|
||||
vk.time = static_cast<float>(key.mTime);
|
||||
vk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z);
|
||||
channel.positions.push_back(vk);
|
||||
}
|
||||
channel.rotations.reserve(ch->mNumRotationKeys);
|
||||
for (unsigned int k = 0; k < ch->mNumRotationKeys; ++k) {
|
||||
const auto& key = ch->mRotationKeys[k];
|
||||
ModelSceneData::AnimQuatKey qk;
|
||||
qk.time = static_cast<float>(key.mTime);
|
||||
qk.value = glm::quat(key.mValue.w, key.mValue.x, key.mValue.y, key.mValue.z);
|
||||
channel.rotations.push_back(qk);
|
||||
}
|
||||
channel.scales.reserve(ch->mNumScalingKeys);
|
||||
for (unsigned int k = 0; k < ch->mNumScalingKeys; ++k) {
|
||||
const auto& key = ch->mScalingKeys[k];
|
||||
ModelSceneData::AnimVecKey sk;
|
||||
sk.time = static_cast<float>(key.mTime);
|
||||
sk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z);
|
||||
channel.scales.push_back(sk);
|
||||
}
|
||||
clip.channels.push_back(std::move(channel));
|
||||
}
|
||||
out.animations.push_back(std::move(clip));
|
||||
}
|
||||
}
|
||||
|
||||
cachedScenes[filepath] = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ModelLoader::exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg) {
|
||||
fs::path inPath(inputFile);
|
||||
if (!fs::exists(inPath)) {
|
||||
@@ -350,17 +448,51 @@ bool ModelLoader::loadRawMesh(const std::string& filepath, RawMeshAsset& out, st
|
||||
return false;
|
||||
}
|
||||
|
||||
in.seekg(0, std::ios::end);
|
||||
std::streamoff fileSize = in.tellg();
|
||||
in.seekg(sizeof(header), std::ios::beg);
|
||||
|
||||
in.read(reinterpret_cast<char*>(&out.boundsMin.x), sizeof(float) * 3);
|
||||
in.read(reinterpret_cast<char*>(&out.boundsMax.x), sizeof(float) * 3);
|
||||
|
||||
const std::streamoff payloadSize = fileSize - sizeof(header) - sizeof(float) * 6;
|
||||
const std::streamoff positionsSize = static_cast<std::streamoff>(sizeof(glm::vec3)) * header.vertexCount;
|
||||
const std::streamoff normalsSize = static_cast<std::streamoff>(sizeof(glm::vec3)) * header.vertexCount;
|
||||
const std::streamoff uvsSize = static_cast<std::streamoff>(sizeof(glm::vec2)) * header.vertexCount;
|
||||
const std::streamoff facesSize = static_cast<std::streamoff>(sizeof(glm::u32vec3)) * header.faceCount;
|
||||
|
||||
bool hasNormals = false;
|
||||
bool hasUVs = false;
|
||||
if (payloadSize == positionsSize + normalsSize + uvsSize + facesSize) {
|
||||
hasNormals = true;
|
||||
hasUVs = true;
|
||||
} else if (payloadSize == positionsSize + normalsSize + facesSize) {
|
||||
hasNormals = true;
|
||||
} else if (payloadSize == positionsSize + uvsSize + facesSize) {
|
||||
hasUVs = true;
|
||||
} else if (payloadSize == positionsSize + facesSize) {
|
||||
// legacy raw meshes without normals/uvs
|
||||
} else if (payloadSize < positionsSize + facesSize) {
|
||||
errorMsg = "Raw mesh data is truncated";
|
||||
return false;
|
||||
}
|
||||
|
||||
out.positions.resize(header.vertexCount);
|
||||
out.normals.resize(header.vertexCount);
|
||||
out.uvs.resize(header.vertexCount);
|
||||
out.faces.resize(header.faceCount);
|
||||
|
||||
in.read(reinterpret_cast<char*>(out.positions.data()), sizeof(glm::vec3) * out.positions.size());
|
||||
if (hasNormals) {
|
||||
out.normals.resize(header.vertexCount);
|
||||
in.read(reinterpret_cast<char*>(out.normals.data()), sizeof(glm::vec3) * out.normals.size());
|
||||
} else {
|
||||
out.normals.assign(header.vertexCount, glm::vec3(0.0f));
|
||||
}
|
||||
if (hasUVs) {
|
||||
out.uvs.resize(header.vertexCount);
|
||||
in.read(reinterpret_cast<char*>(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size());
|
||||
} else {
|
||||
out.uvs.assign(header.vertexCount, glm::vec2(0.0f));
|
||||
}
|
||||
in.read(reinterpret_cast<char*>(out.faces.data()), sizeof(glm::u32vec3) * out.faces.size());
|
||||
|
||||
if (!in.good()) {
|
||||
@@ -435,6 +567,18 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file
|
||||
outPath.replace_extension(".rmesh");
|
||||
}
|
||||
|
||||
std::vector<glm::vec3> normalsData;
|
||||
normalsData.resize(asset.positions.size(), glm::vec3(0.0f));
|
||||
if (asset.normals.size() == asset.positions.size()) {
|
||||
normalsData = asset.normals;
|
||||
}
|
||||
|
||||
std::vector<glm::vec2> uvsData;
|
||||
uvsData.resize(asset.positions.size(), glm::vec2(0.0f));
|
||||
if (asset.uvs.size() == asset.positions.size()) {
|
||||
uvsData = asset.uvs;
|
||||
}
|
||||
|
||||
struct Header {
|
||||
char magic[6] = {'R','M','E','S','H','\0'};
|
||||
uint32_t version = 1;
|
||||
@@ -455,8 +599,8 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file
|
||||
out.write(reinterpret_cast<const char*>(&asset.boundsMin.x), sizeof(float) * 3);
|
||||
out.write(reinterpret_cast<const char*>(&asset.boundsMax.x), sizeof(float) * 3);
|
||||
out.write(reinterpret_cast<const char*>(asset.positions.data()), sizeof(glm::vec3) * asset.positions.size());
|
||||
out.write(reinterpret_cast<const char*>(asset.normals.data()), sizeof(glm::vec3) * asset.normals.size());
|
||||
out.write(reinterpret_cast<const char*>(asset.uvs.data()), sizeof(glm::vec2) * asset.uvs.size());
|
||||
out.write(reinterpret_cast<const char*>(normalsData.data()), sizeof(glm::vec3) * normalsData.size());
|
||||
out.write(reinterpret_cast<const char*>(uvsData.data()), sizeof(glm::vec2) * uvsData.size());
|
||||
out.write(reinterpret_cast<const char*>(asset.faces.data()), sizeof(glm::u32vec3) * asset.faces.size());
|
||||
|
||||
if (!out.good()) {
|
||||
@@ -544,6 +688,84 @@ bool ModelLoader::updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::s
|
||||
return true;
|
||||
}
|
||||
|
||||
int ModelLoader::addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath,
|
||||
const std::string& name, std::string& errorMsg) {
|
||||
if (asset.positions.empty() || asset.faces.empty()) {
|
||||
errorMsg = "Raw mesh is empty";
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<float> vertices;
|
||||
vertices.reserve(asset.faces.size() * 3 * 8);
|
||||
std::vector<glm::vec3> triPositions;
|
||||
triPositions.reserve(asset.faces.size() * 3);
|
||||
|
||||
auto getPos = [&](uint32_t idx) -> const glm::vec3& { return asset.positions[idx]; };
|
||||
auto getNorm = [&](uint32_t idx) -> glm::vec3 {
|
||||
if (idx < asset.normals.size()) return asset.normals[idx];
|
||||
return glm::vec3(0.0f);
|
||||
};
|
||||
auto getUV = [&](uint32_t idx) -> glm::vec2 {
|
||||
if (idx < asset.uvs.size()) return asset.uvs[idx];
|
||||
return glm::vec2(0.0f);
|
||||
};
|
||||
|
||||
for (const auto& face : asset.faces) {
|
||||
const uint32_t idx[3] = { face.x, face.y, face.z };
|
||||
glm::vec3 faceNormal(0.0f);
|
||||
if (!asset.hasNormals) {
|
||||
const glm::vec3& a = getPos(idx[0]);
|
||||
const glm::vec3& b = getPos(idx[1]);
|
||||
const glm::vec3& c = getPos(idx[2]);
|
||||
faceNormal = glm::normalize(glm::cross(b - a, c - a));
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
glm::vec3 pos = getPos(idx[i]);
|
||||
glm::vec3 n = asset.hasNormals ? getNorm(idx[i]) : faceNormal;
|
||||
glm::vec2 uv = asset.hasUVs ? getUV(idx[i]) : glm::vec2(0.0f);
|
||||
|
||||
triPositions.push_back(pos);
|
||||
vertices.push_back(pos.x);
|
||||
vertices.push_back(pos.y);
|
||||
vertices.push_back(pos.z);
|
||||
vertices.push_back(n.x);
|
||||
vertices.push_back(n.y);
|
||||
vertices.push_back(n.z);
|
||||
vertices.push_back(uv.x);
|
||||
vertices.push_back(uv.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.empty()) {
|
||||
errorMsg = "No vertices generated for GPU upload";
|
||||
return -1;
|
||||
}
|
||||
|
||||
OBJLoader::LoadedMesh loaded;
|
||||
loaded.path = sourcePath;
|
||||
loaded.name = name.empty() ? "StaticBatch" : name;
|
||||
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
|
||||
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
|
||||
loaded.faceCount = static_cast<int>(asset.faces.size());
|
||||
loaded.hasNormals = asset.hasNormals;
|
||||
loaded.hasTexCoords = asset.hasUVs;
|
||||
loaded.boundsMin = asset.boundsMin;
|
||||
loaded.boundsMax = asset.boundsMax;
|
||||
loaded.triangleVertices = std::move(triPositions);
|
||||
loaded.positions = asset.positions;
|
||||
loaded.triangleIndices.clear();
|
||||
loaded.triangleIndices.reserve(asset.faces.size() * 3);
|
||||
for (const auto& face : asset.faces) {
|
||||
loaded.triangleIndices.push_back(face.x);
|
||||
loaded.triangleIndices.push_back(face.y);
|
||||
loaded.triangleIndices.push_back(face.z);
|
||||
}
|
||||
|
||||
int newIndex = static_cast<int>(loadedMeshes.size());
|
||||
loadedMeshes.push_back(std::move(loaded));
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
|
||||
return glm::mat4(
|
||||
m.a1, m.b1, m.c1, m.d1,
|
||||
@@ -553,6 +775,303 @@ static glm::mat4 aiToGlm(const aiMatrix4x4& m) {
|
||||
);
|
||||
}
|
||||
|
||||
static glm::vec3 quatToEulerDegrees(const aiQuaternion& q) {
|
||||
glm::quat gq(q.w, q.x, q.y, q.z);
|
||||
glm::vec3 euler = glm::degrees(glm::eulerAngles(gq));
|
||||
return euler;
|
||||
}
|
||||
|
||||
static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
|
||||
std::vector<OBJLoader::LoadedMesh>& loadedMeshes,
|
||||
ModelSceneData& out, std::string& errorMsg) {
|
||||
out.meshIndices.assign(scene->mNumMeshes, -1);
|
||||
out.meshMaterialIndices.assign(scene->mNumMeshes, -1);
|
||||
|
||||
out.materials.clear();
|
||||
out.materials.reserve(scene->mNumMaterials);
|
||||
for (unsigned int i = 0; i < scene->mNumMaterials; ++i) {
|
||||
aiMaterial* mat = scene->mMaterials[i];
|
||||
ModelMaterialInfo info;
|
||||
info.name = mat->GetName().C_Str();
|
||||
if (info.name.empty()) {
|
||||
info.name = "Material_" + std::to_string(i);
|
||||
}
|
||||
|
||||
aiColor3D diffuse(1.0f, 1.0f, 1.0f);
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse)) {
|
||||
info.props.color = glm::vec3(diffuse.r, diffuse.g, diffuse.b);
|
||||
}
|
||||
|
||||
aiColor3D specular(0.0f, 0.0f, 0.0f);
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, specular)) {
|
||||
float avg = (specular.r + specular.g + specular.b) / 3.0f;
|
||||
info.props.specularStrength = avg;
|
||||
}
|
||||
|
||||
float shininess = info.props.shininess;
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shininess)) {
|
||||
info.props.shininess = shininess;
|
||||
}
|
||||
|
||||
aiString tex;
|
||||
if (AI_SUCCESS == mat->GetTexture(aiTextureType_DIFFUSE, 0, &tex)) {
|
||||
info.albedoPath = tex.C_Str();
|
||||
}
|
||||
if (AI_SUCCESS == mat->GetTexture(aiTextureType_NORMALS, 0, &tex)) {
|
||||
info.normalPath = tex.C_Str();
|
||||
} else if (AI_SUCCESS == mat->GetTexture(aiTextureType_HEIGHT, 0, &tex)) {
|
||||
info.normalPath = tex.C_Str();
|
||||
}
|
||||
|
||||
if (!info.albedoPath.empty()) {
|
||||
info.props.textureMix = 1.0f;
|
||||
}
|
||||
|
||||
out.materials.push_back(info);
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
|
||||
aiMesh* mesh = scene->mMeshes[i];
|
||||
if (!mesh || mesh->mNumVertices == 0 || mesh->mNumFaces == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<float> vertices;
|
||||
struct BoneVertex {
|
||||
int ids[4];
|
||||
float weights[4];
|
||||
};
|
||||
std::vector<BoneVertex> boneVertices;
|
||||
std::vector<glm::ivec4> vertexBoneIds(mesh->mNumVertices, glm::ivec4(0));
|
||||
std::vector<glm::vec4> vertexBoneWeights(mesh->mNumVertices, glm::vec4(0.0f));
|
||||
std::vector<glm::vec3> triPositions;
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<uint32_t> triangleIndices;
|
||||
vertices.reserve(mesh->mNumFaces * 3 * 8);
|
||||
boneVertices.reserve(mesh->mNumFaces * 3);
|
||||
triPositions.reserve(mesh->mNumFaces * 3);
|
||||
positions.reserve(mesh->mNumVertices);
|
||||
triangleIndices.reserve(mesh->mNumFaces * 3);
|
||||
|
||||
glm::vec3 boundsMin(FLT_MAX);
|
||||
glm::vec3 boundsMax(-FLT_MAX);
|
||||
|
||||
std::vector<std::string> boneNames;
|
||||
std::vector<glm::mat4> inverseBindMatrices;
|
||||
if (mesh->mNumBones > 0) {
|
||||
boneNames.reserve(mesh->mNumBones);
|
||||
inverseBindMatrices.reserve(mesh->mNumBones);
|
||||
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
|
||||
aiBone* bone = mesh->mBones[b];
|
||||
int boneIndex = static_cast<int>(boneNames.size());
|
||||
boneNames.push_back(bone->mName.C_Str());
|
||||
inverseBindMatrices.push_back(aiToGlm(bone->mOffsetMatrix));
|
||||
|
||||
for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
|
||||
unsigned int vId = bone->mWeights[w].mVertexId;
|
||||
float weight = bone->mWeights[w].mWeight;
|
||||
if (vId >= vertexBoneWeights.size()) continue;
|
||||
|
||||
glm::vec4& weights = vertexBoneWeights[vId];
|
||||
glm::ivec4& ids = vertexBoneIds[vId];
|
||||
int replaceIndex = -1;
|
||||
float minWeight = weight;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
if (weights[k] == 0.0f) {
|
||||
replaceIndex = k;
|
||||
break;
|
||||
}
|
||||
if (weights[k] < minWeight) {
|
||||
minWeight = weights[k];
|
||||
replaceIndex = k;
|
||||
}
|
||||
}
|
||||
if (replaceIndex >= 0) {
|
||||
weights[replaceIndex] = weight;
|
||||
ids[replaceIndex] = boneIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int v = 0; v < mesh->mNumVertices; ++v) {
|
||||
glm::vec3 pos(mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z);
|
||||
positions.push_back(pos);
|
||||
boundsMin.x = std::min(boundsMin.x, pos.x);
|
||||
boundsMin.y = std::min(boundsMin.y, pos.y);
|
||||
boundsMin.z = std::min(boundsMin.z, pos.z);
|
||||
boundsMax.x = std::max(boundsMax.x, pos.x);
|
||||
boundsMax.y = std::max(boundsMax.y, pos.y);
|
||||
boundsMax.z = std::max(boundsMax.z, pos.z);
|
||||
}
|
||||
|
||||
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
|
||||
const aiFace& face = mesh->mFaces[f];
|
||||
if (face.mNumIndices != 3) continue;
|
||||
|
||||
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[0]));
|
||||
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[1]));
|
||||
triangleIndices.push_back(static_cast<uint32_t>(face.mIndices[2]));
|
||||
|
||||
for (unsigned int j = 0; j < 3; ++j) {
|
||||
unsigned int index = face.mIndices[j];
|
||||
glm::vec3 pos(mesh->mVertices[index].x,
|
||||
mesh->mVertices[index].y,
|
||||
mesh->mVertices[index].z);
|
||||
vertices.push_back(pos.x);
|
||||
vertices.push_back(pos.y);
|
||||
vertices.push_back(pos.z);
|
||||
triPositions.push_back(pos);
|
||||
|
||||
if (mesh->mNormals) {
|
||||
glm::vec3 n(mesh->mNormals[index].x,
|
||||
mesh->mNormals[index].y,
|
||||
mesh->mNormals[index].z);
|
||||
vertices.push_back(n.x);
|
||||
vertices.push_back(n.y);
|
||||
vertices.push_back(n.z);
|
||||
} else {
|
||||
vertices.push_back(0.0f);
|
||||
vertices.push_back(1.0f);
|
||||
vertices.push_back(0.0f);
|
||||
}
|
||||
|
||||
if (mesh->mTextureCoords[0]) {
|
||||
vertices.push_back(mesh->mTextureCoords[0][index].x);
|
||||
vertices.push_back(mesh->mTextureCoords[0][index].y);
|
||||
} else {
|
||||
vertices.push_back(0.0f);
|
||||
vertices.push_back(0.0f);
|
||||
}
|
||||
|
||||
BoneVertex bv{};
|
||||
glm::ivec4 ids = vertexBoneIds[index];
|
||||
glm::vec4 weights = vertexBoneWeights[index];
|
||||
float weightSum = weights.x + weights.y + weights.z + weights.w;
|
||||
if (weightSum > 0.0f) {
|
||||
weights /= weightSum;
|
||||
}
|
||||
bv.ids[0] = ids.x;
|
||||
bv.ids[1] = ids.y;
|
||||
bv.ids[2] = ids.z;
|
||||
bv.ids[3] = ids.w;
|
||||
bv.weights[0] = weights.x;
|
||||
bv.weights[1] = weights.y;
|
||||
bv.weights[2] = weights.z;
|
||||
bv.weights[3] = weights.w;
|
||||
boneVertices.push_back(bv);
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
OBJLoader::LoadedMesh loaded;
|
||||
loaded.path = filepath;
|
||||
loaded.name = mesh->mName.C_Str();
|
||||
if (loaded.name.empty()) {
|
||||
loaded.name = fs::path(filepath).stem().string() + "_mesh" + std::to_string(i);
|
||||
}
|
||||
bool isSkinned = mesh->mNumBones > 0 && boneVertices.size() == vertices.size() / 8;
|
||||
if (isSkinned) {
|
||||
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float), true,
|
||||
boneVertices.data(), boneVertices.size() * sizeof(BoneVertex));
|
||||
} else {
|
||||
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
|
||||
}
|
||||
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
|
||||
loaded.faceCount = static_cast<int>(mesh->mNumFaces);
|
||||
loaded.hasNormals = mesh->mNormals != nullptr;
|
||||
loaded.hasTexCoords = mesh->mTextureCoords[0] != nullptr;
|
||||
loaded.boundsMin = boundsMin;
|
||||
loaded.boundsMax = boundsMax;
|
||||
loaded.triangleVertices = std::move(triPositions);
|
||||
loaded.positions = std::move(positions);
|
||||
loaded.triangleIndices = std::move(triangleIndices);
|
||||
loaded.isSkinned = isSkinned;
|
||||
loaded.boneNames = std::move(boneNames);
|
||||
loaded.inverseBindMatrices = std::move(inverseBindMatrices);
|
||||
if (isSkinned) {
|
||||
loaded.boneIds.reserve(boneVertices.size());
|
||||
loaded.boneWeights.reserve(boneVertices.size());
|
||||
for (const auto& bv : boneVertices) {
|
||||
loaded.boneIds.emplace_back(bv.ids[0], bv.ids[1], bv.ids[2], bv.ids[3]);
|
||||
loaded.boneWeights.emplace_back(bv.weights[0], bv.weights[1], bv.weights[2], bv.weights[3]);
|
||||
}
|
||||
loaded.baseVertices = vertices;
|
||||
}
|
||||
|
||||
out.meshMaterialIndices[i] = mesh->mMaterialIndex < (int)out.materials.size()
|
||||
? static_cast<int>(mesh->mMaterialIndex)
|
||||
: -1;
|
||||
|
||||
out.meshIndices[i] = static_cast<int>(loadedMeshes.size());
|
||||
loadedMeshes.push_back(std::move(loaded));
|
||||
}
|
||||
|
||||
bool anyMesh = false;
|
||||
for (int idx : out.meshIndices) {
|
||||
if (idx >= 0) { anyMesh = true; break; }
|
||||
}
|
||||
if (!anyMesh) {
|
||||
errorMsg = "No meshes found in model file";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void buildSceneNodes(const aiScene* scene,
|
||||
const std::vector<int>& meshIndices,
|
||||
ModelSceneData& out) {
|
||||
std::unordered_set<std::string> boneNames;
|
||||
for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
|
||||
aiMesh* mesh = scene->mMeshes[i];
|
||||
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
|
||||
boneNames.insert(mesh->mBones[b]->mName.C_Str());
|
||||
}
|
||||
}
|
||||
|
||||
std::function<void(aiNode*, int)> walk = [&](aiNode* node, int parentIndex) {
|
||||
ModelNodeInfo info;
|
||||
info.name = node->mName.C_Str();
|
||||
if (info.name.empty()) {
|
||||
info.name = "Node_" + std::to_string(out.nodes.size());
|
||||
}
|
||||
info.parentIndex = parentIndex;
|
||||
info.isBone = boneNames.find(info.name) != boneNames.end();
|
||||
|
||||
aiVector3D scaling(1.0f, 1.0f, 1.0f);
|
||||
aiVector3D position(0.0f, 0.0f, 0.0f);
|
||||
aiQuaternion rotation;
|
||||
node->mTransformation.Decompose(scaling, rotation, position);
|
||||
|
||||
info.localPosition = glm::vec3(position.x, position.y, position.z);
|
||||
info.localScale = glm::vec3(scaling.x, scaling.y, scaling.z);
|
||||
info.localRotation = quatToEulerDegrees(rotation);
|
||||
|
||||
for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
|
||||
unsigned int meshIndex = node->mMeshes[i];
|
||||
if (meshIndex < meshIndices.size()) {
|
||||
info.meshIndices.push_back(static_cast<int>(meshIndex));
|
||||
}
|
||||
}
|
||||
|
||||
int thisIndex = static_cast<int>(out.nodes.size());
|
||||
out.nodes.push_back(info);
|
||||
|
||||
for (unsigned int c = 0; c < node->mNumChildren; ++c) {
|
||||
walk(node->mChildren[c], thisIndex);
|
||||
}
|
||||
};
|
||||
|
||||
out.nodes.clear();
|
||||
if (scene->mRootNode) {
|
||||
walk(scene->mRootNode, -1);
|
||||
}
|
||||
}
|
||||
|
||||
static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out) {
|
||||
aiMatrix4x4 current = parentTransform * node->mTransformation;
|
||||
glm::mat4 gTransform = aiToGlm(current);
|
||||
@@ -608,6 +1127,89 @@ static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatri
|
||||
}
|
||||
}
|
||||
|
||||
bool ModelLoader::buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg,
|
||||
glm::vec3* outRootPos, glm::vec3* outRootRot, glm::vec3* outRootScale) {
|
||||
out = RawMeshAsset();
|
||||
|
||||
fs::path inPath(filepath);
|
||||
if (!fs::exists(inPath)) {
|
||||
errorMsg = "File not found: " + filepath;
|
||||
return false;
|
||||
}
|
||||
if (!isSupported(filepath)) {
|
||||
errorMsg = "Unsupported file format for raw mesh build";
|
||||
return false;
|
||||
}
|
||||
|
||||
Assimp::Importer localImporter;
|
||||
unsigned int importFlags =
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_FlipUVs;
|
||||
|
||||
const aiScene* scene = localImporter.ReadFile(filepath, importFlags);
|
||||
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
|
||||
errorMsg = "Assimp error: " + std::string(localImporter.GetErrorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
aiMatrix4x4 parent;
|
||||
parent = aiMatrix4x4();
|
||||
if (scene->mRootNode && (outRootPos || outRootRot || outRootScale)) {
|
||||
aiVector3D scaling(1.0f, 1.0f, 1.0f);
|
||||
aiVector3D position(0.0f, 0.0f, 0.0f);
|
||||
aiQuaternion rotation;
|
||||
scene->mRootNode->mTransformation.Decompose(scaling, rotation, position);
|
||||
if (outRootPos) *outRootPos = glm::vec3(position.x, position.y, position.z);
|
||||
if (outRootScale) *outRootScale = glm::vec3(scaling.x, scaling.y, scaling.z);
|
||||
if (outRootRot) *outRootRot = quatToEulerDegrees(rotation);
|
||||
|
||||
aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation;
|
||||
rootTransform.Inverse();
|
||||
parent = rootTransform;
|
||||
}
|
||||
|
||||
collectRawMeshData(scene->mRootNode, scene, parent, out);
|
||||
|
||||
if (out.positions.empty() || out.faces.empty()) {
|
||||
errorMsg = "No geometry found to build raw mesh";
|
||||
return false;
|
||||
}
|
||||
|
||||
out.hasNormals = false;
|
||||
for (const auto& n : out.normals) {
|
||||
if (glm::length(n) > 1e-4f) { out.hasNormals = true; break; }
|
||||
}
|
||||
|
||||
out.hasUVs = false;
|
||||
for (const auto& uv : out.uvs) {
|
||||
if (std::abs(uv.x) > 1e-6f || std::abs(uv.y) > 1e-6f) { out.hasUVs = true; break; }
|
||||
}
|
||||
|
||||
if (!out.hasNormals) {
|
||||
out.normals.assign(out.positions.size(), glm::vec3(0.0f));
|
||||
std::vector<glm::vec3> accum(out.positions.size(), glm::vec3(0.0f));
|
||||
for (const auto& face : out.faces) {
|
||||
const glm::vec3& a = out.positions[face.x];
|
||||
const glm::vec3& b = out.positions[face.y];
|
||||
const glm::vec3& c = out.positions[face.z];
|
||||
glm::vec3 n = glm::normalize(glm::cross(b - a, c - a));
|
||||
accum[face.x] += n;
|
||||
accum[face.y] += n;
|
||||
accum[face.z] += n;
|
||||
}
|
||||
for (size_t i = 0; i < accum.size(); i++) {
|
||||
if (glm::length(accum[i]) > 1e-6f) {
|
||||
out.normals[i] = glm::normalize(accum[i]);
|
||||
}
|
||||
}
|
||||
out.hasNormals = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform,
|
||||
std::vector<float>& vertices, std::vector<glm::vec3>& triPositions,
|
||||
std::vector<glm::vec3>& positions, std::vector<uint32_t>& indices,
|
||||
@@ -720,6 +1322,7 @@ const std::vector<OBJLoader::LoadedMesh>& ModelLoader::getAllMeshes() const {
|
||||
|
||||
void ModelLoader::clear() {
|
||||
loadedMeshes.clear();
|
||||
cachedScenes.clear();
|
||||
}
|
||||
|
||||
size_t ModelLoader::getMeshCount() const {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Common.h"
|
||||
#include "Rendering.h"
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/scene.h>
|
||||
@@ -19,6 +20,7 @@ struct ModelFormat {
|
||||
struct ModelLoadResult {
|
||||
bool success = false;
|
||||
int meshIndex = -1;
|
||||
std::vector<int> meshIndices;
|
||||
std::string errorMessage;
|
||||
int vertexCount = 0;
|
||||
int faceCount = 0;
|
||||
@@ -41,6 +43,51 @@ struct RawMeshAsset {
|
||||
bool hasUVs = false;
|
||||
};
|
||||
|
||||
struct ModelMaterialInfo {
|
||||
std::string name;
|
||||
MaterialProperties props;
|
||||
std::string albedoPath;
|
||||
std::string normalPath;
|
||||
};
|
||||
|
||||
struct ModelNodeInfo {
|
||||
std::string name;
|
||||
int parentIndex = -1;
|
||||
std::vector<int> meshIndices;
|
||||
glm::vec3 localPosition = glm::vec3(0.0f);
|
||||
glm::vec3 localRotation = glm::vec3(0.0f);
|
||||
glm::vec3 localScale = glm::vec3(1.0f);
|
||||
bool isBone = false;
|
||||
};
|
||||
|
||||
struct ModelSceneData {
|
||||
std::vector<ModelNodeInfo> nodes;
|
||||
std::vector<ModelMaterialInfo> materials;
|
||||
std::vector<int> meshIndices;
|
||||
std::vector<int> meshMaterialIndices;
|
||||
struct AnimVecKey {
|
||||
float time = 0.0f;
|
||||
glm::vec3 value = glm::vec3(0.0f);
|
||||
};
|
||||
struct AnimQuatKey {
|
||||
float time = 0.0f;
|
||||
glm::quat value = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
struct AnimChannel {
|
||||
std::string nodeName;
|
||||
std::vector<AnimVecKey> positions;
|
||||
std::vector<AnimQuatKey> rotations;
|
||||
std::vector<AnimVecKey> scales;
|
||||
};
|
||||
struct AnimationClip {
|
||||
std::string name;
|
||||
double duration = 0.0;
|
||||
double ticksPerSecond = 0.0;
|
||||
std::vector<AnimChannel> channels;
|
||||
};
|
||||
std::vector<AnimationClip> animations;
|
||||
};
|
||||
|
||||
class ModelLoader {
|
||||
public:
|
||||
// Singleton access
|
||||
@@ -49,6 +96,9 @@ public:
|
||||
// Load a model file (FBX, OBJ, GLTF, etc.)
|
||||
ModelLoadResult loadModel(const std::string& filepath);
|
||||
|
||||
// Load a model scene with node hierarchy and per-mesh materials
|
||||
bool loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg);
|
||||
|
||||
// Get mesh by index
|
||||
Mesh* getMesh(int index);
|
||||
|
||||
@@ -73,6 +123,16 @@ public:
|
||||
// Update an already-loaded raw mesh in GPU memory
|
||||
bool updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg);
|
||||
|
||||
// Build a raw mesh asset from a model scene without writing to disk
|
||||
bool buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg,
|
||||
glm::vec3* outRootPos = nullptr,
|
||||
glm::vec3* outRootRot = nullptr,
|
||||
glm::vec3* outRootScale = nullptr);
|
||||
|
||||
// Add a raw mesh asset to the GPU cache and return its mesh index
|
||||
int addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath,
|
||||
const std::string& name, std::string& errorMsg);
|
||||
|
||||
// Get list of supported formats
|
||||
static std::vector<ModelFormat> getSupportedFormats();
|
||||
|
||||
@@ -100,6 +160,7 @@ private:
|
||||
|
||||
// Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure)
|
||||
std::vector<OBJLoader::LoadedMesh> loadedMeshes;
|
||||
std::unordered_map<std::string, ModelSceneData> cachedScenes;
|
||||
|
||||
// Assimp importer (kept for resource management)
|
||||
Assimp::Importer importer;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
|
||||
#pragma region Local Path Helpers
|
||||
namespace {
|
||||
fs::path normalizePath(const fs::path& p) {
|
||||
std::error_code ec;
|
||||
@@ -59,7 +60,9 @@ fs::path guessIncludeDir(const fs::path& repoRoot, const std::string& includeRel
|
||||
return normalizePath(repoRoot);
|
||||
}
|
||||
} // namespace
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Lifecycle
|
||||
PackageManager::PackageManager() {
|
||||
buildRegistry();
|
||||
}
|
||||
@@ -70,7 +73,9 @@ void PackageManager::setProjectRoot(const fs::path& root) {
|
||||
manifestPath = projectRoot / "packages.modu";
|
||||
loadManifest();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Install / Remove
|
||||
bool PackageManager::isInstalled(const std::string& id) const {
|
||||
return std::find(installedIds.begin(), installedIds.end(), id) != installedIds.end();
|
||||
}
|
||||
@@ -135,7 +140,9 @@ bool PackageManager::remove(const std::string& id) {
|
||||
saveManifest();
|
||||
return true;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Build Config
|
||||
void PackageManager::applyToBuildConfig(ScriptBuildConfig& config) const {
|
||||
std::unordered_set<std::string> defineSet(config.defines.begin(), config.defines.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() {
|
||||
registry.clear();
|
||||
fs::path engineRoot = fs::current_path();
|
||||
@@ -230,7 +239,9 @@ void PackageManager::buildRegistry() {
|
||||
miniaudio.includeDirs = { engineRoot / "include/ThirdParty" };
|
||||
add(miniaudio);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Manifest IO
|
||||
void PackageManager::loadManifest() {
|
||||
installedIds.clear();
|
||||
for (const auto& pkg : registry) {
|
||||
@@ -336,7 +347,9 @@ void PackageManager::saveManifest() const {
|
||||
file << join(pkg->windowsLibs, ';') << "\n";
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Registry Lookup
|
||||
const PackageInfo* PackageManager::findPackage(const std::string& id) const {
|
||||
auto it = std::find_if(registry.begin(), registry.end(), [&](const PackageInfo& p) {
|
||||
return p.id == id;
|
||||
@@ -348,7 +361,9 @@ bool PackageManager::isBuiltIn(const std::string& id) const {
|
||||
const PackageInfo* pkg = findPackage(id);
|
||||
return pkg && pkg->builtIn;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Utility Helpers
|
||||
std::string PackageManager::trim(const std::string& value) {
|
||||
size_t start = 0;
|
||||
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();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region External Packages
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -512,3 +533,4 @@ bool PackageManager::updateGitPackage(const std::string& id, std::string& outLog
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -16,8 +16,12 @@ PxVec3 ToPxVec3(const glm::vec3& v) {
|
||||
}
|
||||
|
||||
PxQuat ToPxQuat(const glm::vec3& eulerDeg) {
|
||||
glm::vec3 radians = glm::radians(eulerDeg);
|
||||
glm::quat q = glm::quat(radians);
|
||||
glm::vec3 r = glm::radians(eulerDeg);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,9 +29,20 @@ glm::vec3 ToGlmVec3(const PxVec3& v) {
|
||||
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::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
|
||||
|
||||
@@ -107,9 +122,9 @@ void PhysicsSystem::createGroundPlane() {
|
||||
|
||||
bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector<PxVec3>& vertices, std::vector<uint32_t>& indices) const {
|
||||
const OBJLoader::LoadedMesh* meshInfo = nullptr;
|
||||
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
|
||||
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
|
||||
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
|
||||
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
|
||||
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
|
||||
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
|
||||
}
|
||||
if (!meshInfo) {
|
||||
@@ -200,21 +215,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
|
||||
s->setRestOffset(rest);
|
||||
};
|
||||
|
||||
switch (obj.type) {
|
||||
case ObjectType::Cube: {
|
||||
switch (obj.renderType) {
|
||||
case RenderType::Cube: {
|
||||
PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f)));
|
||||
shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true);
|
||||
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
|
||||
break;
|
||||
}
|
||||
case ObjectType::Sphere: {
|
||||
case RenderType::Sphere: {
|
||||
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
|
||||
radius = std::max(radius, 0.01f);
|
||||
shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true);
|
||||
tuneShape(shape, radius * 2.0f, isDynamic);
|
||||
break;
|
||||
}
|
||||
case ObjectType::Capsule: {
|
||||
case RenderType::Capsule: {
|
||||
float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f;
|
||||
radius = std::max(radius, 0.01f);
|
||||
float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f);
|
||||
@@ -227,14 +242,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
|
||||
tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic);
|
||||
break;
|
||||
}
|
||||
case ObjectType::Plane: {
|
||||
case RenderType::Plane: {
|
||||
glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f));
|
||||
halfExtents.z = std::max(halfExtents.z, 0.01f);
|
||||
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true);
|
||||
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
|
||||
break;
|
||||
}
|
||||
case ObjectType::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;
|
||||
radius = std::max(radius, 0.01f);
|
||||
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);
|
||||
} else {
|
||||
const OBJLoader::LoadedMesh* meshInfo = nullptr;
|
||||
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
|
||||
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
|
||||
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
|
||||
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
|
||||
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
|
||||
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
|
||||
}
|
||||
if (!meshInfo) {
|
||||
@@ -469,7 +491,6 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
if (!isReady()) return;
|
||||
|
||||
clearActors();
|
||||
createGroundPlane();
|
||||
|
||||
struct MeshCookInfo {
|
||||
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.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue;
|
||||
const OBJLoader::LoadedMesh* meshInfo = nullptr;
|
||||
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
|
||||
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
|
||||
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
|
||||
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
|
||||
} else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) {
|
||||
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
|
||||
}
|
||||
if (!meshInfo) continue;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ public:
|
||||
std::string currentSceneName;
|
||||
bool isLoaded = false;
|
||||
bool hasUnsavedChanges = false;
|
||||
bool usesNewLayout = false;
|
||||
|
||||
Project() = default;
|
||||
Project(const std::string& projectName, const fs::path& basePath);
|
||||
@@ -55,10 +56,18 @@ class SceneSerializer {
|
||||
public:
|
||||
static bool saveScene(const fs::path& filePath,
|
||||
const std::vector<SceneObject>& objects,
|
||||
int nextId);
|
||||
int nextId,
|
||||
float timeOfDay);
|
||||
|
||||
static bool loadScene(const fs::path& filePath,
|
||||
std::vector<SceneObject>& objects,
|
||||
int& nextId,
|
||||
int& outVersion);
|
||||
int& outVersion,
|
||||
float* outTimeOfDay = nullptr);
|
||||
|
||||
static bool loadSceneDeferred(const fs::path& filePath,
|
||||
std::vector<SceneObject>& objects,
|
||||
int& nextId,
|
||||
int& outVersion,
|
||||
float* outTimeOfDay = nullptr);
|
||||
};
|
||||
|
||||
@@ -13,12 +13,12 @@ OBJLoader g_objLoader;
|
||||
// Cube vertex data
|
||||
float vertices[] = {
|
||||
// Back face (z = -0.5f)
|
||||
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
|
||||
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
|
||||
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
|
||||
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
|
||||
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
|
||||
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
|
||||
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
|
||||
|
||||
// Front face (z = 0.5f)
|
||||
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||||
@@ -37,12 +37,12 @@ float vertices[] = {
|
||||
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
||||
|
||||
// Right face (x = 0.5f)
|
||||
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
|
||||
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
|
||||
|
||||
// Bottom face (y = -0.5f)
|
||||
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
||||
@@ -54,11 +54,11 @@ float vertices[] = {
|
||||
|
||||
// Top face (y = 0.5f)
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
||||
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
|
||||
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
|
||||
};
|
||||
|
||||
float mirrorPlaneVertices[] = {
|
||||
@@ -287,6 +287,7 @@ std::vector<float> generateTorus(int segments, int sides) {
|
||||
// Mesh implementation
|
||||
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
|
||||
vertexCount = dataSizeBytes / (8 * sizeof(float));
|
||||
strideFloats = 8;
|
||||
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
@@ -308,9 +309,52 @@ Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage,
|
||||
const void* boneData, size_t boneDataBytes) {
|
||||
vertexCount = dataSizeBytes / (8 * sizeof(float));
|
||||
strideFloats = 8;
|
||||
dynamic = dynamicUsage;
|
||||
hasBones = boneData && boneDataBytes > 0;
|
||||
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, dynamicUsage ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);
|
||||
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(6 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
|
||||
if (hasBones) {
|
||||
glGenBuffers(1, &boneVBO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, boneVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, boneDataBytes, boneData, GL_STATIC_DRAW);
|
||||
|
||||
glVertexAttribIPointer(3, 4, GL_INT, sizeof(int) * 4 + sizeof(float) * 4, (void*)0);
|
||||
glEnableVertexAttribArray(3);
|
||||
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(int) * 4 + sizeof(float) * 4,
|
||||
(void*)(sizeof(int) * 4));
|
||||
glEnableVertexAttribArray(4);
|
||||
}
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
Mesh::~Mesh() {
|
||||
glDeleteVertexArrays(1, &VAO);
|
||||
glDeleteBuffers(1, &VBO);
|
||||
if (boneVBO) {
|
||||
glDeleteBuffers(1, &boneVBO);
|
||||
}
|
||||
}
|
||||
|
||||
void Mesh::draw() const {
|
||||
@@ -319,6 +363,56 @@ void Mesh::draw() const {
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void Mesh::updateVertices(const float* vertexData, size_t dataSizeBytes) {
|
||||
if (!dynamic) return;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, dataSizeBytes, vertexData);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
vertexCount = dataSizeBytes / (strideFloats * sizeof(float));
|
||||
}
|
||||
|
||||
static void applyCpuSkinning(OBJLoader::LoadedMesh& meshInfo, const std::vector<glm::mat4>& bones, int maxBones) {
|
||||
if (!meshInfo.mesh || !meshInfo.isSkinned) return;
|
||||
if (meshInfo.baseVertices.empty() || meshInfo.boneIds.empty() || meshInfo.boneWeights.empty()) return;
|
||||
if (!meshInfo.mesh->isDynamic()) return;
|
||||
|
||||
size_t vertexCount = meshInfo.baseVertices.size() / 8;
|
||||
if (vertexCount == 0 || meshInfo.boneIds.size() != vertexCount || meshInfo.boneWeights.size() != vertexCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<float> skinned = meshInfo.baseVertices;
|
||||
int boneLimit = std::min<int>(static_cast<int>(bones.size()), maxBones);
|
||||
for (size_t i = 0; i < vertexCount; ++i) {
|
||||
glm::vec3 basePos(skinned[i * 8 + 0], skinned[i * 8 + 1], skinned[i * 8 + 2]);
|
||||
glm::vec3 baseNorm(skinned[i * 8 + 3], skinned[i * 8 + 4], skinned[i * 8 + 5]);
|
||||
glm::ivec4 ids = meshInfo.boneIds[i];
|
||||
glm::vec4 weights = meshInfo.boneWeights[i];
|
||||
|
||||
glm::vec4 skinnedPos(0.0f);
|
||||
glm::vec3 skinnedNorm(0.0f);
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
int id = ids[k];
|
||||
float w = weights[k];
|
||||
if (w <= 0.0f || id < 0 || id >= boneLimit) continue;
|
||||
const glm::mat4& m = bones[id];
|
||||
skinnedPos += w * (m * glm::vec4(basePos, 1.0f));
|
||||
skinnedNorm += w * glm::mat3(m) * baseNorm;
|
||||
}
|
||||
skinned[i * 8 + 0] = skinnedPos.x;
|
||||
skinned[i * 8 + 1] = skinnedPos.y;
|
||||
skinned[i * 8 + 2] = skinnedPos.z;
|
||||
if (glm::length(skinnedNorm) > 1e-6f) {
|
||||
skinnedNorm = glm::normalize(skinnedNorm);
|
||||
}
|
||||
skinned[i * 8 + 3] = skinnedNorm.x;
|
||||
skinned[i * 8 + 4] = skinnedNorm.y;
|
||||
skinned[i * 8 + 5] = skinnedNorm.z;
|
||||
}
|
||||
|
||||
meshInfo.mesh->updateVertices(skinned.data(), skinned.size() * sizeof(float));
|
||||
}
|
||||
|
||||
// OBJLoader implementation
|
||||
int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
|
||||
// Check if already loaded
|
||||
@@ -537,6 +631,7 @@ Renderer::~Renderer() {
|
||||
if (rbo) glDeleteRenderbuffers(1, &rbo);
|
||||
if (quadVBO) glDeleteBuffers(1, &quadVBO);
|
||||
if (quadVAO) glDeleteVertexArrays(1, &quadVAO);
|
||||
if (debugWhiteTexture) glDeleteTextures(1, &debugWhiteTexture);
|
||||
}
|
||||
|
||||
Texture* Renderer::getTexture(const std::string& path) {
|
||||
@@ -619,6 +714,17 @@ void Renderer::initialize() {
|
||||
|
||||
texture1 = new Texture("Resources/Textures/container.jpg");
|
||||
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));
|
||||
|
||||
@@ -809,7 +915,7 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
|
||||
};
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || obj.type != ObjectType::Mirror) continue;
|
||||
if (!obj.enabled || !obj.hasRenderer || obj.renderType != RenderType::Mirror) continue;
|
||||
active.insert(obj.id);
|
||||
|
||||
RenderTarget& target = mirrorTargets[obj.id];
|
||||
@@ -886,12 +992,36 @@ void Renderer::ensureQuad() {
|
||||
}
|
||||
|
||||
void Renderer::drawFullscreenQuad() {
|
||||
recordFullscreenDraw();
|
||||
if (quadVAO == 0) ensureQuad();
|
||||
glBindVertexArray(quadVAO);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
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() {
|
||||
historyValid = false;
|
||||
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("shininess", obj.material.shininess);
|
||||
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;
|
||||
if (!obj.albedoTexturePath.empty()) {
|
||||
@@ -990,7 +1120,7 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
if (baseTex) baseTex->Bind(GL_TEXTURE0);
|
||||
|
||||
bool overlayUsed = false;
|
||||
if (obj.type == ObjectType::Mirror) {
|
||||
if (obj.renderType == RenderType::Mirror) {
|
||||
auto it = mirrorTargets.find(obj.id);
|
||||
if (it != mirrorTargets.end() && it->second.texture != 0) {
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
@@ -1018,26 +1148,29 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
}
|
||||
shader->setBool("hasNormalMap", normalUsed);
|
||||
|
||||
switch (obj.type) {
|
||||
case ObjectType::Cube:
|
||||
switch (obj.renderType) {
|
||||
case RenderType::Cube:
|
||||
cubeMesh->draw();
|
||||
break;
|
||||
case ObjectType::Sphere:
|
||||
case RenderType::Sphere:
|
||||
sphereMesh->draw();
|
||||
break;
|
||||
case ObjectType::Capsule:
|
||||
case RenderType::Capsule:
|
||||
capsuleMesh->draw();
|
||||
break;
|
||||
case ObjectType::Plane:
|
||||
case RenderType::Plane:
|
||||
if (planeMesh) planeMesh->draw();
|
||||
break;
|
||||
case ObjectType::Mirror:
|
||||
case RenderType::Mirror:
|
||||
if (planeMesh) planeMesh->draw();
|
||||
break;
|
||||
case ObjectType::Torus:
|
||||
case RenderType::Sprite:
|
||||
if (planeMesh) planeMesh->draw();
|
||||
break;
|
||||
case RenderType::Torus:
|
||||
if (torusMesh) torusMesh->draw();
|
||||
break;
|
||||
case ObjectType::OBJMesh:
|
||||
case RenderType::OBJMesh:
|
||||
if (obj.meshId >= 0) {
|
||||
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
|
||||
if (objMesh) {
|
||||
@@ -1045,7 +1178,7 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ObjectType::Model:
|
||||
case RenderType::Model:
|
||||
if (obj.meshId >= 0) {
|
||||
Mesh* modelMesh = getModelLoader().getMesh(obj.meshId);
|
||||
if (modelMesh) {
|
||||
@@ -1053,18 +1186,8 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ObjectType::PointLight:
|
||||
case ObjectType::SpotLight:
|
||||
case ObjectType::AreaLight:
|
||||
// Lights are not rendered as geometry
|
||||
break;
|
||||
case ObjectType::DirectionalLight:
|
||||
// Not rendered as geometry
|
||||
break;
|
||||
case ObjectType::Camera:
|
||||
// Cameras are editor helpers only
|
||||
break;
|
||||
case ObjectType::PostFXNode:
|
||||
case RenderType::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1110,8 +1233,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
candidates.reserve(sceneObjects.size());
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || !obj.light.enabled) continue;
|
||||
if (obj.type == ObjectType::DirectionalLight) {
|
||||
if (!obj.enabled || !obj.hasLight || !obj.light.enabled) continue;
|
||||
if (obj.light.type == LightType::Directional) {
|
||||
LightUniform l;
|
||||
l.type = 0;
|
||||
l.dir = forwardFromRotation(obj);
|
||||
@@ -1119,7 +1242,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
l.intensity = obj.light.intensity;
|
||||
lights.push_back(l);
|
||||
if (lights.size() >= kMaxLights) break;
|
||||
} else if (obj.type == ObjectType::SpotLight) {
|
||||
} else if (obj.light.type == LightType::Spot) {
|
||||
LightUniform l;
|
||||
l.type = 2;
|
||||
l.pos = obj.position;
|
||||
@@ -1135,7 +1258,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
c.distSq = glm::dot(delta, delta);
|
||||
c.id = obj.id;
|
||||
candidates.push_back(c);
|
||||
} else if (obj.type == ObjectType::PointLight) {
|
||||
} else if (obj.light.type == LightType::Point) {
|
||||
LightUniform l;
|
||||
l.type = 1;
|
||||
l.pos = obj.position;
|
||||
@@ -1148,7 +1271,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
c.distSq = glm::dot(delta, delta);
|
||||
c.id = obj.id;
|
||||
candidates.push_back(c);
|
||||
} else if (obj.type == ObjectType::AreaLight) {
|
||||
} else if (obj.light.type == LightType::Area) {
|
||||
LightUniform l;
|
||||
l.type = 3; // area
|
||||
l.pos = obj.position;
|
||||
@@ -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) {
|
||||
if (!obj.enabled) continue;
|
||||
if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue;
|
||||
// Skip light gizmo-only types and camera helpers
|
||||
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) {
|
||||
continue;
|
||||
}
|
||||
if (!drawMirrorObjects && obj.hasRenderer && obj.renderType == RenderType::Mirror) continue;
|
||||
if (!HasRendererComponent(obj)) continue;
|
||||
|
||||
Shader* active = getShader(obj.vertexShaderPath, obj.fragmentShaderPath);
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, obj.position);
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
model = glm::scale(model, obj.scale);
|
||||
|
||||
std::string vertPath = obj.vertexShaderPath;
|
||||
std::string fragPath = obj.fragmentShaderPath;
|
||||
int boneLimit = obj.skeletal.maxBones;
|
||||
int availableBones = static_cast<int>(obj.skeletal.finalMatrices.size());
|
||||
bool needsFallback = obj.hasSkeletalAnimation && obj.skeletal.enabled &&
|
||||
obj.skeletal.allowCpuFallback &&
|
||||
boneLimit > 0 && availableBones > boneLimit;
|
||||
bool wantsGpuSkinning = obj.hasSkeletalAnimation && obj.skeletal.enabled &&
|
||||
obj.skeletal.useGpuSkinning && !needsFallback;
|
||||
if (vertPath.empty() && wantsGpuSkinning) {
|
||||
vertPath = skinnedVertPath;
|
||||
}
|
||||
Shader* active = getShader(vertPath, fragPath);
|
||||
if (!active) continue;
|
||||
shader = active;
|
||||
shader->use();
|
||||
|
||||
shader->setMat4("view", camera.getViewMatrix());
|
||||
shader->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane));
|
||||
shader->setMat4("view", view);
|
||||
shader->setMat4("projection", proj);
|
||||
shader->setVec3("viewPos", camera.position);
|
||||
shader->setBool("unlit", obj.type == ObjectType::Mirror);
|
||||
shader->setBool("unlit", obj.renderType == RenderType::Mirror);
|
||||
shader->setVec3("ambientColor", ambientColor);
|
||||
shader->setVec3("ambientColor", ambientColor);
|
||||
|
||||
@@ -1216,13 +1364,6 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
|
||||
}
|
||||
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, obj.position);
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
model = glm::scale(model, obj.scale);
|
||||
|
||||
shader->setMat4("model", model);
|
||||
shader->setVec3("materialColor", obj.material.color);
|
||||
shader->setFloat("ambientStrength", obj.material.ambientStrength);
|
||||
@@ -1230,6 +1371,20 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setFloat("shininess", obj.material.shininess);
|
||||
shader->setFloat("mixAmount", obj.material.textureMix);
|
||||
|
||||
if (obj.hasSkeletalAnimation && obj.skeletal.enabled) {
|
||||
int safeLimit = std::max(0, boneLimit);
|
||||
int boneCount = std::min<int>(availableBones, safeLimit);
|
||||
if (wantsGpuSkinning && boneCount > 0) {
|
||||
shader->setInt("boneCount", boneCount);
|
||||
shader->setMat4Array("bones", obj.skeletal.finalMatrices.data(), boneCount);
|
||||
shader->setBool("useSkinning", true);
|
||||
} else {
|
||||
shader->setBool("useSkinning", false);
|
||||
}
|
||||
} else {
|
||||
shader->setBool("useSkinning", false);
|
||||
}
|
||||
|
||||
Texture* baseTex = texture1;
|
||||
if (!obj.albedoTexturePath.empty()) {
|
||||
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
|
||||
@@ -1237,7 +1392,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
if (baseTex) baseTex->Bind(GL_TEXTURE0);
|
||||
|
||||
bool overlayUsed = false;
|
||||
if (obj.type == ObjectType::Mirror) {
|
||||
if (obj.renderType == RenderType::Mirror) {
|
||||
auto it = mirrorTargets.find(obj.id);
|
||||
if (it != mirrorTargets.end() && it->second.texture != 0) {
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
@@ -1266,29 +1421,52 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setBool("hasNormalMap", normalUsed);
|
||||
|
||||
Mesh* meshToDraw = nullptr;
|
||||
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
|
||||
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh;
|
||||
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
|
||||
else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh;
|
||||
else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh;
|
||||
else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh;
|
||||
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
|
||||
if (obj.renderType == RenderType::Cube) meshToDraw = cubeMesh;
|
||||
else if (obj.renderType == RenderType::Sphere) meshToDraw = sphereMesh;
|
||||
else if (obj.renderType == RenderType::Capsule) meshToDraw = capsuleMesh;
|
||||
else if (obj.renderType == RenderType::Plane) meshToDraw = planeMesh;
|
||||
else if (obj.renderType == RenderType::Mirror) meshToDraw = planeMesh;
|
||||
else if (obj.renderType == RenderType::Sprite) meshToDraw = planeMesh;
|
||||
else if (obj.renderType == RenderType::Torus) meshToDraw = torusMesh;
|
||||
else if (obj.renderType == RenderType::OBJMesh && obj.meshId != -1) {
|
||||
meshToDraw = g_objLoader.getMesh(obj.meshId);
|
||||
} else if (obj.type == ObjectType::Model && obj.meshId != -1) {
|
||||
} else if (obj.renderType == RenderType::Model && obj.meshId != -1) {
|
||||
meshToDraw = getModelLoader().getMesh(obj.meshId);
|
||||
}
|
||||
|
||||
if (obj.renderType == RenderType::Model && obj.meshId != -1 &&
|
||||
obj.hasSkeletalAnimation && obj.skeletal.enabled && !wantsGpuSkinning) {
|
||||
const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId);
|
||||
if (meshInfo) {
|
||||
applyCpuSkinning(*const_cast<OBJLoader::LoadedMesh*>(meshInfo),
|
||||
obj.skeletal.finalMatrices,
|
||||
obj.skeletal.maxBones);
|
||||
}
|
||||
}
|
||||
|
||||
bool doubleSided = (obj.renderType == RenderType::Sprite || obj.renderType == RenderType::Mirror);
|
||||
if (doubleSided) {
|
||||
glDisable(GL_CULL_FACE);
|
||||
} else {
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
if (meshToDraw) {
|
||||
recordMeshDraw();
|
||||
meshToDraw->draw();
|
||||
}
|
||||
}
|
||||
|
||||
if (skybox) {
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
glm::mat4 proj = glm::perspective(glm::radians(fovDeg),
|
||||
(float)width / height,
|
||||
nearPlane, farPlane);
|
||||
if (!cullFace) {
|
||||
glDisable(GL_CULL_FACE);
|
||||
} else {
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(prevCullMode);
|
||||
}
|
||||
|
||||
if (skybox) {
|
||||
recordDrawCall();
|
||||
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
|
||||
}
|
||||
|
||||
@@ -1301,7 +1479,7 @@ PostFXSettings Renderer::gatherPostFX(const std::vector<SceneObject>& sceneObjec
|
||||
PostFXSettings combined;
|
||||
combined.enabled = false;
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (obj.type != ObjectType::PostFXNode) continue;
|
||||
if (!obj.hasPostFX) continue;
|
||||
if (!obj.postFx.enabled) continue;
|
||||
combined = obj.postFx; // Last enabled node wins for now
|
||||
combined.enabled = true;
|
||||
@@ -1449,16 +1627,31 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
resetStats(previewStats);
|
||||
activeStats = &previewStats;
|
||||
ensureRenderTarget(previewTarget, width, height);
|
||||
if (previewTarget.fbo == 0) return 0;
|
||||
if (previewTarget.fbo == 0) {
|
||||
activeStats = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo);
|
||||
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);
|
||||
renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true);
|
||||
if (!applyPostFX) {
|
||||
activeStats = nullptr;
|
||||
return previewTarget.texture;
|
||||
}
|
||||
unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false);
|
||||
activeStats = nullptr;
|
||||
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() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
@@ -19,14 +19,23 @@ std::vector<float> generateTorus(int segments = 32, int sides = 16);
|
||||
class Mesh {
|
||||
private:
|
||||
unsigned int VAO, VBO;
|
||||
unsigned int boneVBO = 0;
|
||||
int vertexCount;
|
||||
int strideFloats = 8;
|
||||
bool dynamic = false;
|
||||
bool hasBones = false;
|
||||
|
||||
public:
|
||||
Mesh(const float* vertexData, size_t dataSizeBytes);
|
||||
Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage,
|
||||
const void* boneData, size_t boneDataBytes);
|
||||
~Mesh();
|
||||
|
||||
void draw() const;
|
||||
void updateVertices(const float* vertexData, size_t dataSizeBytes);
|
||||
int getVertexCount() const { return vertexCount; }
|
||||
bool isDynamic() const { return dynamic; }
|
||||
bool usesBones() const { return hasBones; }
|
||||
};
|
||||
|
||||
class OBJLoader {
|
||||
@@ -44,6 +53,12 @@ public:
|
||||
std::vector<glm::vec3> triangleVertices; // positions duplicated per-triangle for picking
|
||||
std::vector<glm::vec3> positions; // unique vertex positions for physics
|
||||
std::vector<uint32_t> triangleIndices; // triangle indices into positions
|
||||
bool isSkinned = false;
|
||||
std::vector<std::string> boneNames;
|
||||
std::vector<glm::mat4> inverseBindMatrices;
|
||||
std::vector<glm::ivec4> boneIds;
|
||||
std::vector<glm::vec4> boneWeights;
|
||||
std::vector<float> baseVertices;
|
||||
};
|
||||
|
||||
private:
|
||||
@@ -61,6 +76,13 @@ public:
|
||||
class Camera;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
struct RenderStats {
|
||||
int drawCalls = 0;
|
||||
int meshDraws = 0;
|
||||
int fullscreenDraws = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
|
||||
int currentWidth = 800, currentHeight = 600;
|
||||
@@ -84,6 +106,7 @@ private:
|
||||
Shader* blurShader = nullptr;
|
||||
Texture* texture1 = 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>> previewTextureCacheLinear;
|
||||
std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheNearest;
|
||||
@@ -96,6 +119,7 @@ private:
|
||||
};
|
||||
std::unordered_map<std::string, ShaderEntry> shaderCache;
|
||||
std::string defaultVertPath = "Resources/Shaders/vert.glsl";
|
||||
std::string skinnedVertPath = "Resources/Shaders/skinned_vert.glsl";
|
||||
std::string defaultFragPath = "Resources/Shaders/frag.glsl";
|
||||
std::string postVertPath = "Resources/Shaders/postfx_vert.glsl";
|
||||
std::string postFragPath = "Resources/Shaders/postfx_frag.glsl";
|
||||
@@ -114,6 +138,9 @@ private:
|
||||
unsigned int displayTexture = 0;
|
||||
bool historyValid = false;
|
||||
std::unordered_map<int, RenderTarget> mirrorTargets;
|
||||
RenderStats viewportStats;
|
||||
RenderStats previewStats;
|
||||
RenderStats* activeStats = nullptr;
|
||||
|
||||
void setupFBO();
|
||||
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 ensureQuad();
|
||||
void drawFullscreenQuad();
|
||||
void resetStats(RenderStats& stats);
|
||||
void recordDrawCall();
|
||||
void recordMeshDraw();
|
||||
void recordFullscreenDraw();
|
||||
void clearHistory();
|
||||
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);
|
||||
@@ -145,10 +176,14 @@ public:
|
||||
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 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);
|
||||
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
|
||||
void endRender();
|
||||
|
||||
Skybox* getSkybox() { return skybox; }
|
||||
unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; }
|
||||
const RenderStats& getLastViewportStats() const { return viewportStats; }
|
||||
const RenderStats& getLastPreviewStats() const { return previewStats; }
|
||||
};
|
||||
|
||||
@@ -3,20 +3,51 @@
|
||||
#include "Common.h"
|
||||
|
||||
enum class ObjectType {
|
||||
Cube,
|
||||
Sphere,
|
||||
Capsule,
|
||||
OBJMesh,
|
||||
Model, // New type for Assimp-loaded models (FBX, GLTF, etc.)
|
||||
DirectionalLight,
|
||||
PointLight,
|
||||
SpotLight,
|
||||
AreaLight,
|
||||
Camera,
|
||||
PostFXNode,
|
||||
Mirror,
|
||||
Plane,
|
||||
Torus
|
||||
Cube = 0,
|
||||
Sphere = 1,
|
||||
Capsule = 2,
|
||||
OBJMesh = 3,
|
||||
Model = 4, // New type for Assimp-loaded models (FBX, GLTF, etc.)
|
||||
DirectionalLight = 5,
|
||||
PointLight = 6,
|
||||
SpotLight = 7,
|
||||
AreaLight = 8,
|
||||
Camera = 9,
|
||||
PostFXNode = 10,
|
||||
Mirror = 11,
|
||||
Plane = 12,
|
||||
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 {
|
||||
@@ -34,6 +65,95 @@ enum class LightType {
|
||||
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 {
|
||||
LightType type = LightType::Point;
|
||||
glm::vec3 color = glm::vec3(1.0f);
|
||||
@@ -59,6 +179,8 @@ struct CameraComponent {
|
||||
float nearClip = NEAR_PLANE;
|
||||
float farClip = FAR_PLANE;
|
||||
bool applyPostFX = true;
|
||||
bool use2D = false;
|
||||
float pixelsPerUnit = 100.0f;
|
||||
};
|
||||
|
||||
struct PostFXSettings {
|
||||
@@ -96,9 +218,16 @@ struct ScriptSetting {
|
||||
std::string value;
|
||||
};
|
||||
|
||||
enum class ScriptLanguage {
|
||||
Cpp = 0,
|
||||
CSharp = 1
|
||||
};
|
||||
|
||||
struct ScriptComponent {
|
||||
bool enabled = true;
|
||||
ScriptLanguage language = ScriptLanguage::Cpp;
|
||||
std::string path;
|
||||
std::string managedType;
|
||||
std::vector<ScriptSetting> settings;
|
||||
std::string lastBinaryPath;
|
||||
std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start
|
||||
@@ -142,6 +271,64 @@ struct PlayerControllerComponent {
|
||||
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 {
|
||||
bool enabled = true;
|
||||
std::string clipPath;
|
||||
@@ -151,6 +338,36 @@ struct AudioSourceComponent {
|
||||
bool spatial = true;
|
||||
float minDistance = 1.0f;
|
||||
float maxDistance = 25.0f;
|
||||
AudioRolloffMode rolloffMode = AudioRolloffMode::Logarithmic;
|
||||
float rolloff = 1.0f;
|
||||
float customMidDistance = 0.5f;
|
||||
float customMidGain = 0.6f;
|
||||
float customEndGain = 0.0f;
|
||||
};
|
||||
|
||||
struct ReverbZoneComponent {
|
||||
bool enabled = true;
|
||||
ReverbPreset preset = ReverbPreset::Room;
|
||||
ReverbZoneShape shape = ReverbZoneShape::Box;
|
||||
glm::vec3 boxSize = glm::vec3(6.0f);
|
||||
float radius = 6.0f;
|
||||
float blendDistance = 1.0f;
|
||||
float minDistance = 1.0f;
|
||||
float maxDistance = 15.0f;
|
||||
float room = -1000.0f; // dB
|
||||
float roomHF = -100.0f; // dB
|
||||
float roomLF = 0.0f; // dB
|
||||
float decayTime = 1.49f; // s
|
||||
float decayHFRatio = 0.83f; // 0.1..2
|
||||
float reflections = -2602.0f; // dB
|
||||
float reflectionsDelay = 0.007f; // s
|
||||
float reverb = 200.0f; // dB
|
||||
float reverbDelay = 0.011f; // s
|
||||
float hfReference = 5000.0f; // Hz
|
||||
float lfReference = 250.0f; // Hz
|
||||
float roomRolloffFactor = 0.0f;
|
||||
float diffusion = 100.0f; // 0..100
|
||||
float density = 100.0f; // 0..100
|
||||
};
|
||||
|
||||
class SceneObject {
|
||||
@@ -160,6 +377,12 @@ public:
|
||||
bool enabled = true;
|
||||
int layer = 0;
|
||||
std::string tag = "Untagged";
|
||||
bool hasRenderer = false;
|
||||
RenderType renderType = RenderType::None;
|
||||
bool hasLight = false;
|
||||
bool hasCamera = false;
|
||||
bool hasPostFX = false;
|
||||
bool hasUI = false;
|
||||
glm::vec3 position;
|
||||
glm::vec3 rotation;
|
||||
glm::vec3 scale;
|
||||
@@ -173,6 +396,7 @@ public:
|
||||
bool isExpanded = true;
|
||||
std::string meshPath; // Path to imported model file
|
||||
int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader)
|
||||
int meshSourceIndex = -1; // Source mesh index for multi-mesh models
|
||||
MaterialProperties material;
|
||||
std::string materialPath; // Optional external material asset
|
||||
std::string albedoTexturePath;
|
||||
@@ -188,12 +412,27 @@ public:
|
||||
std::vector<std::string> additionalMaterialPaths;
|
||||
bool hasRigidbody = false;
|
||||
RigidbodyComponent rigidbody;
|
||||
bool hasRigidbody2D = false;
|
||||
Rigidbody2DComponent rigidbody2D;
|
||||
bool hasCollider2D = false;
|
||||
Collider2DComponent collider2D;
|
||||
bool hasParallaxLayer2D = false;
|
||||
ParallaxLayer2DComponent parallaxLayer2D;
|
||||
bool hasCameraFollow2D = false;
|
||||
CameraFollow2DComponent cameraFollow2D;
|
||||
bool hasCollider = false;
|
||||
ColliderComponent collider;
|
||||
bool hasPlayerController = false;
|
||||
PlayerControllerComponent playerController;
|
||||
bool hasAudioSource = false;
|
||||
AudioSourceComponent audioSource;
|
||||
bool hasReverbZone = false;
|
||||
ReverbZoneComponent reverbZone;
|
||||
bool hasAnimation = false;
|
||||
AnimationComponent animation;
|
||||
bool hasSkeletalAnimation = false;
|
||||
SkeletalAnimationComponent skeletal;
|
||||
UIElementComponent ui;
|
||||
|
||||
SceneObject(const std::string& name, ObjectType type, int id)
|
||||
: name(name),
|
||||
@@ -207,3 +446,11 @@ public:
|
||||
localInitialized(true),
|
||||
id(id) {}
|
||||
};
|
||||
|
||||
inline bool HasRendererComponent(const SceneObject& obj) {
|
||||
return obj.hasRenderer && obj.renderType != RenderType::None;
|
||||
}
|
||||
|
||||
inline bool HasUIComponent(const SceneObject& obj) {
|
||||
return obj.hasUI && obj.ui.type != UIElementType::None;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
relToScripts.clear();
|
||||
}
|
||||
|
||||
auto hasDotDot = [](const fs::path& path) {
|
||||
for (const auto& part : path) {
|
||||
if (part == "..") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (relToScripts.empty() || relToScripts.is_absolute() || hasDotDot(relToScripts)) {
|
||||
relToScripts.clear();
|
||||
}
|
||||
|
||||
fs::path relativeParent = relToScripts.has_parent_path() ? relToScripts.parent_path() : fs::path();
|
||||
std::string baseName = scriptAbs.stem().string();
|
||||
fs::path objectPath = config.outDir / relativeParent / (baseName + ".o");
|
||||
@@ -246,10 +256,24 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
FunctionSpec testEditorSpec = detectFunction(scriptSource, "TestEditor");
|
||||
FunctionSpec updateSpec = detectFunction(scriptSource, "Update");
|
||||
FunctionSpec tickUpdateSpec = detectFunction(scriptSource, "TickUpdate");
|
||||
FunctionSpec inspectorSpec = detectFunction(scriptSource, "Script_OnInspector");
|
||||
|
||||
auto hasExternCInspector = [&]() {
|
||||
try {
|
||||
std::regex direct(R"(extern\s+"C"\s+void\s+Script_OnInspector\s*\()");
|
||||
if (std::regex_search(scriptSource, direct)) return true;
|
||||
std::regex block(R"(extern\s+"C"\s*\{[\s\S]*?\bScript_OnInspector\b)");
|
||||
return std::regex_search(scriptSource, block);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
bool inspectorExtern = hasExternCInspector();
|
||||
bool needsInspectorWrap = inspectorSpec.present && !inspectorExtern;
|
||||
|
||||
fs::path wrapperPath;
|
||||
bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present
|
||||
|| updateSpec.present || tickUpdateSpec.present;
|
||||
|| updateSpec.present || tickUpdateSpec.present || needsInspectorWrap;
|
||||
fs::path sourceToCompile = scriptAbs;
|
||||
|
||||
if (useWrapper) {
|
||||
@@ -264,8 +288,15 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
}
|
||||
|
||||
std::string includePath = scriptAbs.lexically_normal().generic_string();
|
||||
if (needsInspectorWrap) {
|
||||
wrapper << "#define Script_OnInspector Script_OnInspector_Impl\n";
|
||||
}
|
||||
wrapper << "#include \"ScriptRuntime.h\"\n";
|
||||
wrapper << "#include \"" << includePath << "\"\n\n";
|
||||
wrapper << "#include \"" << includePath << "\"\n";
|
||||
if (needsInspectorWrap) {
|
||||
wrapper << "#undef Script_OnInspector\n";
|
||||
}
|
||||
wrapper << "\n";
|
||||
wrapper << "extern \"C\" {\n";
|
||||
|
||||
auto emitWrapper = [&](const char* exportedName, const char* implName,
|
||||
@@ -293,6 +324,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
emitWrapper("Script_TestEditor", "TestEditor", testEditorSpec);
|
||||
emitWrapper("Script_Update", "Update", updateSpec);
|
||||
emitWrapper("Script_TickUpdate", "TickUpdate", tickUpdateSpec);
|
||||
if (needsInspectorWrap) {
|
||||
wrapper << "void Script_OnInspector(ScriptContext& ctx) {\n";
|
||||
if (inspectorSpec.takesContext) {
|
||||
wrapper << " Script_OnInspector_Impl(ctx);\n";
|
||||
} else {
|
||||
wrapper << " (void)ctx;\n";
|
||||
wrapper << " Script_OnInspector_Impl();\n";
|
||||
}
|
||||
wrapper << "}\n\n";
|
||||
}
|
||||
|
||||
wrapper << "}\n";
|
||||
sourceToCompile = wrapperPath;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "ScriptRuntime.h"
|
||||
#include "Engine.h"
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <iterator>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -27,6 +30,22 @@ std::string makeScriptInstanceKey(const ScriptContext& ctx) {
|
||||
}
|
||||
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) {
|
||||
@@ -39,6 +58,31 @@ SceneObject* ScriptContext::FindObjectById(int 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 {
|
||||
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) {
|
||||
if (object) {
|
||||
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 {
|
||||
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) {
|
||||
if (!engine || !object || !HasRigidbody()) return false;
|
||||
return engine->setRigidbodyVelocityFromScript(object->id, velocity);
|
||||
@@ -140,6 +532,12 @@ bool ScriptContext::GetRigidbodyVelocity(glm::vec3& outVelocity) const {
|
||||
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) {
|
||||
if (!engine || !object || !HasRigidbody()) return false;
|
||||
return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity);
|
||||
@@ -265,6 +663,17 @@ void ScriptContext::SetSettingBool(const std::string& key, bool value) {
|
||||
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 {
|
||||
std::string v = GetSetting(key, "");
|
||||
if (v.empty()) return fallback;
|
||||
@@ -315,6 +724,31 @@ void ScriptContext::AutoSetting(const std::string& key, bool& value) {
|
||||
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) {
|
||||
if (!script) return;
|
||||
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
|
||||
@@ -378,6 +812,12 @@ void ScriptContext::SaveAutoSettings() {
|
||||
newVal = cur ? "1" : "0";
|
||||
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: {
|
||||
glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr);
|
||||
if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue;
|
||||
|
||||
@@ -10,13 +10,14 @@ struct ScriptContext {
|
||||
Engine* engine = nullptr;
|
||||
SceneObject* object = nullptr;
|
||||
ScriptComponent* script = nullptr;
|
||||
enum class AutoSettingType { Bool, Vec3, StringBuf };
|
||||
enum class AutoSettingType { Bool, Float, Vec3, StringBuf };
|
||||
struct AutoSettingEntry {
|
||||
AutoSettingType type;
|
||||
std::string key;
|
||||
void* ptr = nullptr;
|
||||
size_t bufSize = 0;
|
||||
bool initialBool = false;
|
||||
float initialFloat = 0.0f;
|
||||
glm::vec3 initialVec3 = glm::vec3(0.0f);
|
||||
std::string initialString;
|
||||
};
|
||||
@@ -25,6 +26,7 @@ struct ScriptContext {
|
||||
// Convenience helpers for scripts
|
||||
SceneObject* FindObjectByName(const std::string& name);
|
||||
SceneObject* FindObjectById(int id);
|
||||
SceneObject* ResolveObjectRef(const std::string& ref);
|
||||
bool IsObjectEnabled() const;
|
||||
void SetObjectEnabled(bool enabled);
|
||||
int GetLayer() const;
|
||||
@@ -34,11 +36,66 @@ struct ScriptContext {
|
||||
bool HasTag(const std::string& tag) const;
|
||||
bool IsInLayer(int layer) const;
|
||||
void SetPosition(const glm::vec3& pos);
|
||||
void SetPosition2D(const glm::vec2& pos);
|
||||
void SetRotation(const glm::vec3& rot);
|
||||
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 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 GetRigidbodyVelocity(glm::vec3& outVelocity) const;
|
||||
bool AddRigidbodyVelocity(const glm::vec3& deltaVelocity);
|
||||
bool SetRigidbodyAngularVelocity(const glm::vec3& velocity);
|
||||
bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const;
|
||||
bool AddRigidbodyForce(const glm::vec3& force);
|
||||
@@ -63,12 +120,15 @@ struct ScriptContext {
|
||||
void SetSetting(const std::string& key, const std::string& value);
|
||||
bool GetSettingBool(const std::string& key, bool fallback = false) const;
|
||||
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;
|
||||
void SetSettingVec3(const std::string& key, const glm::vec3& value);
|
||||
// Console helper
|
||||
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, float& value);
|
||||
void AutoSetting(const std::string& key, glm::vec3& value);
|
||||
void AutoSetting(const std::string& key, char* buffer, size_t bufferSize);
|
||||
void SaveAutoSettings();
|
||||
|
||||
@@ -47,12 +47,34 @@ void Shader::compileShaders(const char* vertexSource, const char* fragmentSource
|
||||
glCompileShader(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();
|
||||
glAttachShader(ID, vertex);
|
||||
glAttachShader(ID, fragment);
|
||||
glLinkProgram(ID);
|
||||
checkCompileErrors(ID, "PROGRAM");
|
||||
|
||||
glGetProgramiv(ID, GL_LINK_STATUS, &success);
|
||||
if (!success) {
|
||||
glDeleteProgram(ID);
|
||||
ID = 0;
|
||||
}
|
||||
|
||||
glDeleteShader(vertex);
|
||||
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));
|
||||
}
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
3224
src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp
vendored
Normal file
3224
src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp
vendored
Normal file
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
Reference in New Issue
Block a user