Added OGG Support + 2.5D support

This commit is contained in:
2026-02-28 17:13:48 -05:00
parent 123610af6c
commit 4e3823c4f9
33 changed files with 9273 additions and 507 deletions

View File

@@ -74,6 +74,10 @@ set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "Build Assimp documentation" FORCE)
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
find_package(OpenGL REQUIRED)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(SNDFILE QUIET IMPORTED_TARGET sndfile)
endif()
# ==================== Mono (managed scripting) ====================
option(MODULARITY_USE_MONO "Enable Mono embedding for managed scripts" ON)
@@ -170,7 +174,14 @@ 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)
# Force vendored zlib/minizip in assimp to avoid host minizip/unzip.h dependency on clean builds.
set(ASSIMP_BUILD_ZLIB ON CACHE BOOL "Build Assimp zlib/minizip from source" FORCE)
set(ASSIMP_BUILD_MINIZIP ON CACHE BOOL "Build Assimp minizip from source" FORCE)
# Some distros expose broken minizip pkg-config include dirs (/usr/include instead of /usr/include/minizip).
# Ensure assimp does not prefer that cached result.
set(UNZIP_FOUND FALSE CACHE INTERNAL "Force Assimp to use vendored unzip headers" FORCE)
add_subdirectory(src/ThirdParty/assimp EXCLUDE_FROM_ALL)
target_include_directories(assimp PRIVATE ${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/contrib/unzip)
target_link_libraries(core PUBLIC assimp)
target_include_directories(core PUBLIC
${PROJECT_SOURCE_DIR}/src
@@ -187,9 +198,13 @@ if(MODULARITY_USE_MONO)
endif()
target_compile_definitions(core PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
target_compile_definitions(core PUBLIC MODULARITY_HAS_VULKAN=$<BOOL:${MODULARITY_HAS_VULKAN}>)
target_compile_definitions(core PUBLIC MODULARITY_HAS_SNDFILE=$<BOOL:${SNDFILE_FOUND}>)
if(WIN32)
target_compile_definitions(core PUBLIC ${MODULARITY_WINDOWS_DEFINES})
endif()
if(SNDFILE_FOUND)
target_link_libraries(core PUBLIC PkgConfig::SNDFILE)
endif()
target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS})
add_library(core_player STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
@@ -210,9 +225,13 @@ if(MODULARITY_USE_MONO)
endif()
target_compile_definitions(core_player PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
target_compile_definitions(core_player PUBLIC MODULARITY_HAS_VULKAN=$<BOOL:${MODULARITY_HAS_VULKAN}>)
target_compile_definitions(core_player PUBLIC MODULARITY_HAS_SNDFILE=$<BOOL:${SNDFILE_FOUND}>)
if(WIN32)
target_compile_definitions(core_player PUBLIC ${MODULARITY_WINDOWS_DEFINES})
endif()
if(SNDFILE_FOUND)
target_link_libraries(core_player PUBLIC PkgConfig::SNDFILE)
endif()
target_compile_options(core_player PRIVATE ${MODULARITY_WARNING_FLAGS})
if(MODULARITY_ENABLE_PHYSX)
@@ -307,17 +326,24 @@ else()
target_link_libraries(ModularityPlayer PRIVATE core_player glfw OpenGL::GL)
endif()
add_custom_command(TARGET ModularityPlayer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Resources
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
)
# Copy resources once to avoid parallel target races writing the same directory.
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
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Template-Projects
$<TARGET_FILE_DIR:Modularity>/Template-Projects
)
else()
add_custom_command(TARGET ModularityPlayer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Resources
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/Template-Projects
$<TARGET_FILE_DIR:ModularityPlayer>/Template-Projects
)
endif()
@@ -374,6 +400,10 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/Resources
DESTINATION ./bin/
)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/Template-Projects
DESTINATION ./bin/
)
# ==================== Packaging (CPack) ====================
set(CPACK_PACKAGE_NAME "Modularity")

View File

@@ -15,6 +15,7 @@ uniform mat4 projection;
uniform mat4 bones[256];
uniform int boneCount;
uniform bool useSkinning;
uniform vec4 uvRect;
void main()
{
@@ -38,7 +39,7 @@ void main()
vec4 worldPos = model * localPos;
FragPos = vec3(worldPos);
Normal = mat3(transpose(inverse(model))) * localNormal;
TexCoord = aTexCoord;
TexCoord = uvRect.xy + aTexCoord * uvRect.zw;
gl_Position = projection * view * worldPos;
}

View File

@@ -10,12 +10,13 @@ out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform vec4 uvRect;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoord = aTexCoord;
TexCoord = uvRect.xy + aTexCoord * uvRect.zw;
gl_Position = projection * view * vec4(FragPos, 1.0);
}

View File

View File

@@ -0,0 +1,277 @@
# Scene File
version=19
nextId=3
timeOfDay=0
objectCount=3
[Object]
id=0
name=Cube 0
type=0
enabled=1
layer=0
tag=Untagged
hasRenderer=1
renderType=1
hasLight=0
hasCamera=0
hasPostFX=0
hasUI=0
uiType=0
parentId=-1
position=0,0,0
rotation=0,0,0
scale=1,1,1
hasRigidbody=0
hasRigidbody2D=0
hasCollider2D=0
hasParallaxLayer2D=0
hasCameraFollow2D=0
hasCollider=0
hasPlayerController=0
hasAudioSource=0
hasReverbZone=0
hasGroundBakedType=0
hasObsticleObject=0
hasAIAgent=0
hasAnimation=0
hasSkeletalAnimation=0
materialColor=1,1,1
materialAmbient=0.2
materialSpecular=0.5
materialShininess=32
materialTextureMix=0.3
materialTextureFilter=0
materialPath=
albedoTex=/home/anemunt/ModularityProjects/3D OpenGL Template/Assets/container.jpg
overlayTex=
normalMap=
vertexShader=
fragmentShader=
useOverlay=0
additionalMaterialCount=0
scripts=0
lightColor=1,1,1
lightIntensity=1
lightRange=10
lightEdgeFade=0.2
lightInner=15
lightOuter=25
lightSize=1,1
lightCastShadows=0
lightSoftShadows=1
lightShadowBias=0.02
lightShadowSoftness=0.04
lightEnabled=1
cameraType=0
cameraFov=45
cameraNear=0.1
cameraFar=100
cameraPostFX=1
cameraUse2D=0
cameraPixelsPerUnit=100
uiAnchor=0
uiPosition=0,0
uiRotation=0
uiSize=160,40
uiSliderValue=0.5
uiSliderMin=0
uiSliderMax=1
uiLabel=UI Element
uiColor=1,1,1,1
uiInteractable=1
uiSliderStyle=0
uiButtonStyle=0
uiStylePreset=Default
uiTextScale=1
uiRenderIn3D=0
uiRenderTargetSize=512,512
uiSpriteSheetEnabled=0
uiSpriteSheetGrid=1,1
uiSpriteSheetFrame=0
uiSpriteSheetFps=12
uiSpriteSheetLoop=1
scriptCount=0
children=
[Object]
id=1
name=Camera 1
type=9
enabled=1
layer=0
tag=Untagged
hasRenderer=0
renderType=0
hasLight=0
hasCamera=1
hasPostFX=0
hasUI=0
uiType=0
parentId=-1
position=2.9,2.5,3.5
rotation=340,40,0
scale=1,1,1
hasRigidbody=0
hasRigidbody2D=0
hasCollider2D=0
hasParallaxLayer2D=0
hasCameraFollow2D=0
hasCollider=0
hasPlayerController=0
hasAudioSource=0
hasReverbZone=0
hasGroundBakedType=0
hasObsticleObject=0
hasAIAgent=0
hasAnimation=0
hasSkeletalAnimation=0
materialColor=1,1,1
materialAmbient=0.2
materialSpecular=0.5
materialShininess=32
materialTextureMix=0.3
materialTextureFilter=0
materialPath=
albedoTex=
overlayTex=
normalMap=
vertexShader=
fragmentShader=
useOverlay=0
additionalMaterialCount=0
scripts=0
lightColor=1,1,1
lightIntensity=1
lightRange=10
lightEdgeFade=0.2
lightInner=15
lightOuter=25
lightSize=1,1
lightCastShadows=0
lightSoftShadows=1
lightShadowBias=0.02
lightShadowSoftness=0.04
lightEnabled=1
cameraType=1
cameraFov=60
cameraNear=0.1
cameraFar=100
cameraPostFX=1
cameraUse2D=0
cameraPixelsPerUnit=100
uiAnchor=0
uiPosition=0,0
uiRotation=0
uiSize=160,40
uiSliderValue=0.5
uiSliderMin=0
uiSliderMax=1
uiLabel=UI Element
uiColor=1,1,1,1
uiInteractable=1
uiSliderStyle=0
uiButtonStyle=0
uiStylePreset=Default
uiTextScale=1
uiRenderIn3D=0
uiRenderTargetSize=512,512
uiSpriteSheetEnabled=0
uiSpriteSheetGrid=1,1
uiSpriteSheetFrame=0
uiSpriteSheetFps=12
uiSpriteSheetLoop=1
scriptCount=0
children=
[Object]
id=2
name=Directional Light 2
type=5
enabled=1
layer=0
tag=Untagged
hasRenderer=0
renderType=0
hasLight=1
hasCamera=0
hasPostFX=0
hasUI=0
uiType=0
parentId=-1
position=-3.29997,3.99999,-3.9
rotation=241.457,35,45
scale=0.999999,1,1
hasRigidbody=0
hasRigidbody2D=0
hasCollider2D=0
hasParallaxLayer2D=0
hasCameraFollow2D=0
hasCollider=0
hasPlayerController=0
hasAudioSource=0
hasReverbZone=0
hasGroundBakedType=0
hasObsticleObject=0
hasAIAgent=0
hasAnimation=0
hasSkeletalAnimation=0
materialColor=1,1,1
materialAmbient=0.2
materialSpecular=0.5
materialShininess=32
materialTextureMix=0.3
materialTextureFilter=0
materialPath=
albedoTex=
overlayTex=
normalMap=
vertexShader=
fragmentShader=
useOverlay=0
additionalMaterialCount=0
scripts=0
lightColor=1,1,1
lightType=0
lightIntensity=1
lightRange=10
lightEdgeFade=0.2
lightInner=15
lightOuter=25
lightSize=1,1
lightCastShadows=0
lightSoftShadows=1
lightShadowBias=0.02
lightShadowSoftness=0.04
lightEnabled=1
cameraType=0
cameraFov=45
cameraNear=0.1
cameraFar=100
cameraPostFX=1
cameraUse2D=0
cameraPixelsPerUnit=100
uiAnchor=0
uiPosition=0,0
uiRotation=0
uiSize=160,40
uiSliderValue=0.5
uiSliderMin=0
uiSliderMax=1
uiLabel=UI Element
uiColor=1,1,1,1
uiInteractable=1
uiSliderStyle=0
uiButtonStyle=0
uiStylePreset=Default
uiTextScale=1
uiRenderIn3D=0
uiRenderTargetSize=512,512
uiSpriteSheetEnabled=0
uiSpriteSheetGrid=1,1
uiSpriteSheetFrame=0
uiSpriteSheetFps=12
uiSpriteSheetLoop=1
scriptCount=0
children=

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@@ -0,0 +1,102 @@
# Editor UI settings
uiStyle=Default
uiAnimationMode=Fluid
workspace=Default
fileBrowserIconScale=1.0000
fileBrowserViewMode=Grid
fileBrowserSidebarWidth=150.0000
fileBrowserSidebarVisible=1
consoleWrapText=1
showAnimationWindow=0
showAIPathfindingWindow=0
showSceneGizmos=1
gizmoShowCameraOverlays=1
gizmoShowCameraFrustumLabels=1
gizmoShowLightOverlays=1
gizmoShowLightIntensityLabels=1
sceneGizmoIconScale=1.0000
sceneGizmoOverlayScale=1.0000
showSceneGrid3D=0
showCanvasOverlay=0
showUIWorldGrid=1
showSpritePreviewPanel=1
pixelGridSnapEnabled=1
pixelGridSnapStep=1
showGameProfiler=1
collisionWireframe=0
fpsCapEnabled=0
fpsCap=120.0000
cameraMoveSpeed=5.0000
cameraSprintSpeed=10.0000
cameraSmoothMovement=1
cameraAcceleration=15.0000
cameraMouseSensitivity=0.1000
gameViewportResolutionIndex=0
gameViewportCustomWidth=1920
gameViewportCustomHeight=1080
gameViewportAutoFit=1
gameViewportZoom=1.0000
scriptAutoCompileInterval=0.5000
scriptAutoCompileOnSave=1
color.Text=0.9200,0.9300,0.9700,1.0000
color.TextDisabled=0.6000,0.6200,0.7000,1.0000
color.WindowBg=0.1100,0.1200,0.1900,1.0000
color.ChildBg=0.1600,0.1600,0.2400,1.0000
color.PopupBg=0.1000,0.1100,0.1700,0.9800
color.Border=0.2200,0.2300,0.3400,0.7000
color.BorderShadow=0.0000,0.0000,0.0000,0.0000
color.FrameBg=0.2000,0.2100,0.3000,1.0000
color.FrameBgHovered=0.2600,0.2800,0.4000,1.0000
color.FrameBgActive=0.3000,0.3400,0.4600,1.0000
color.TitleBg=0.1100,0.1200,0.1800,1.0000
color.TitleBgActive=0.1600,0.1700,0.2400,1.0000
color.TitleBgCollapsed=0.0900,0.1000,0.1500,1.0000
color.MenuBarBg=0.0900,0.1000,0.1600,1.0000
color.ScrollbarBg=0.1100,0.1200,0.1800,1.0000
color.ScrollbarGrab=0.2400,0.2600,0.3600,1.0000
color.ScrollbarGrabHovered=0.3200,0.3500,0.4800,1.0000
color.ScrollbarGrabActive=0.3600,0.4200,0.5800,1.0000
color.CheckMark=0.4800,0.5600,0.8600,1.0000
color.SliderGrab=0.4800,0.5600,0.8600,1.0000
color.SliderGrabActive=0.3800,0.4600,0.7400,1.0000
color.Button=0.2200,0.2300,0.3200,1.0000
color.ButtonHovered=0.2800,0.3000,0.4200,1.0000
color.ButtonActive=0.3300,0.3600,0.4800,1.0000
color.Header=0.2200,0.2300,0.3400,1.0000
color.HeaderHovered=0.2600,0.2800,0.3800,1.0000
color.HeaderActive=0.2800,0.3000,0.4200,1.0000
color.Separator=0.2200,0.2300,0.3400,1.0000
color.SeparatorHovered=0.3400,0.3600,0.5200,1.0000
color.SeparatorActive=0.4400,0.5000,0.7000,1.0000
color.ResizeGrip=0.2800,0.3000,0.4200,1.0000
color.ResizeGripHovered=0.3800,0.4400,0.6000,0.8000
color.ResizeGripActive=0.4800,0.5600,0.8600,1.0000
color.InputTextCursor=1.0000,1.0000,1.0000,1.0000
color.TabHovered=0.3000,0.3400,0.4800,1.0000
color.Tab=0.1500,0.1600,0.2400,1.0000
color.TabSelected=0.2000,0.2200,0.3200,1.0000
color.TabSelectedOverline=0.2600,0.5900,0.9800,1.0000
color.TabDimmed=0.1100,0.1200,0.1800,1.0000
color.TabDimmedSelected=0.1600,0.1800,0.2600,1.0000
color.TabDimmedSelectedOverline=0.5000,0.5000,0.5000,0.0000
color.DockingPreview=0.4800,0.5600,0.8600,0.4500
color.DockingEmptyBg=0.0800,0.0900,0.1400,1.0000
color.PlotLines=0.6100,0.6100,0.6100,1.0000
color.PlotLinesHovered=1.0000,0.4300,0.3500,1.0000
color.PlotHistogram=0.9000,0.7000,0.0000,1.0000
color.PlotHistogramHovered=1.0000,0.6000,0.0000,1.0000
color.TableHeaderBg=0.2000,0.2200,0.3200,1.0000
color.TableBorderStrong=0.3100,0.3100,0.3500,1.0000
color.TableBorderLight=0.2300,0.2300,0.2500,1.0000
color.TableRowBg=0.0000,0.0000,0.0000,0.0000
color.TableRowBgAlt=1.0000,1.0000,1.0000,0.0600
color.TextLink=0.2600,0.5900,0.9800,1.0000
color.TextSelectedBg=0.4800,0.5600,0.8600,0.2400
color.TreeLines=0.4300,0.4300,0.5000,0.5000
color.DragDropTarget=1.0000,1.0000,0.0000,0.9000
color.DragDropTargetBg=0.0000,0.0000,0.0000,0.0000
color.UnsavedMarker=1.0000,1.0000,1.0000,1.0000
color.NavCursor=0.4800,0.5600,0.8600,1.0000
color.NavWindowingHighlight=1.0000,1.0000,1.0000,0.7000
color.NavWindowingDimBg=0.8000,0.8000,0.8000,0.2000
color.ModalWindowDimBg=0.0500,0.0600,0.0900,0.7000

View File

@@ -0,0 +1,85 @@
[Window][Sprite Timeline]
Collapsed=0
DockId=0x00000006
[Window][DockSpace]
Pos=0,24
Size=1908,1004
Collapsed=0
[Window][Hierarchy]
Pos=0,48
Size=268,980
Collapsed=0
DockId=0x00000001,0
[Window][Inspector]
Pos=1628,48
Size=280,980
Collapsed=0
DockId=0x00000004,0
[Window][Project]
Pos=268,799
Size=1360,229
Collapsed=0
DockId=0x00000006,0
[Window][Environment]
Pos=1628,48
Size=280,980
Collapsed=0
DockId=0x00000004,1
[Window][Camera]
Pos=0,48
Size=268,980
Collapsed=0
DockId=0x00000001,1
[Window][Project Settings]
Pos=268,799
Size=1360,229
Collapsed=0
DockId=0x00000006,1
[Window][Viewport]
Pos=268,48
Size=1360,751
Collapsed=0
DockId=0x00000005,0
[Window][Sprite Preview]
Pos=1628,48
Size=280,980
Collapsed=0
DockId=0x00000004,2
[Window][Game Viewport]
Pos=268,48
Size=1360,751
Collapsed=0
DockId=0x00000005,1
[Window][Launcher]
Pos=0,0
Size=1342,841
Collapsed=0
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Table][0x3AEBACB6,5]
RefScale=16
[Docking][Data]
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1908,980 Split=X
DockNode ID=0x00000001 Parent=0xD71539A0 SizeRef=268,817 Selected=0xBABDAE5E
DockNode ID=0x00000002 Parent=0xD71539A0 SizeRef=1640,817 Split=X
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=1360,817 Split=Y
DockNode ID=0x00000005 Parent=0x00000003 SizeRef=794,751 CentralNode=1 Selected=0xC450F867
DockNode ID=0x00000006 Parent=0x00000003 SizeRef=794,229 Selected=0x9C21DE82
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=280,817 Selected=0x36DC96AB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,24 @@
# build.modu
platform=Linux
architecture=x86_64
companyName=DefaultCompany
buildName=3D OpenGL Template
version=0.1.0
splashImage=
splashEnabled=0
splashDuration=2.5
packageStandaloneArchive=1
developmentBuild=0
autoConnectProfiler=0
scriptDebugging=0
deepProfiling=0
scriptsOnlyBuild=0
serverBuild=0
compressionMethod=Default
rendererAmbient=0.2,0.2,0.2
rendererShadowResolution=512
rendererAutoReloadShaders=1
editorCameraFov=45
editorCameraNear=0.1
editorCameraFar=100
scene=Main,1

View File

@@ -0,0 +1,7 @@
# Modularity package manifest
# Add optional script-time dependencies here
package=glm
package=imgui
package=imguizmo
package=miniaudio
git=soft-chimp-locomotion|Soft Chimp Locomotion|https://github.com/darkresident55/Soft-Chimp-Locomotion.git|Packages/soft-chimp-locomotion|Packages/soft-chimp-locomotion/.git|||

View File

@@ -0,0 +1,4 @@
name=3D OpenGL Template
lastScene=Main
pipeline=3D
renderer=OpenGL

View File

@@ -0,0 +1,14 @@
# scripts.modu
cppStandard=c++20
scriptsDir=Assets/Scripts
outDir=Library/CompiledScripts
includeDir=/home/anemunt/Git-base/Modularity/build/src
includeDir=/home/anemunt/Git-base/Modularity/build/include
includeDir=/home/anemunt/Git-base/Modularity/build/src/ThirdParty
includeDir=/home/anemunt/Git-base/Modularity/build/src/ThirdParty/glm
define=MODU_SCRIPTING=1
define=MODU_PROJECT_NAME="3D OpenGL Template"
linux.linkLib=pthread
linux.linkLib=dl
win.linkLib=User32.lib
win.linkLib=Advapi32.lib

827
build.sh
View File

@@ -1,91 +1,782 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
start_time=$(date +%s)
start_time="$(date +%s)"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
build_dir="${script_dir}/build"
player_cache_dir="${build_dir}/player-cache"
last_step="bootstrap"
current_step=0
total_steps=0
build_started=0
status_line_active=0
status_current_label=""
status_current_spinner="-"
status_current_elapsed=0
finish() {
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
declare -a build_warning_messages=()
declare -a build_error_messages=()
if [ $exit_code -eq 0 ]; then
echo -e "================================\n Modularity - Native Linux Build Complete\n================================"
echo -e "[Complete]: Your Modularity Build Completed in ${duration}s!\nThe build should be located under Modularity within another folder called 'Build'"
else
echo -e "================================\n Modularity - Native Linux Build Failed\n================================"
echo "[Failed]: Your Modularity Build Failed after ${duration}s (exit code ${exit_code})."
fi
if [[ -t 1 ]]; then
C_RESET=$'\033[0m'
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'
C_RED=$'\033[31m'
C_CYAN=$'\033[36m'
ICON_INFO=""
ICON_WARN="⚠"
ICON_ERROR="✖"
ICON_OK="✔"
else
C_RESET=''
C_BOLD=''
C_DIM=''
C_GREEN=''
C_YELLOW=''
C_RED=''
C_CYAN=''
ICON_INFO="[i]"
ICON_WARN="[!]"
ICON_ERROR="[x]"
ICON_OK="[ok]"
fi
exit $exit_code
log_info() { printf "%s%s%s %s\n" "${C_CYAN}" "${ICON_INFO}" "${C_RESET}" "$*"; }
log_warn() { printf "%s%s%s %s\n" "${C_YELLOW}" "${ICON_WARN}" "${C_RESET}" "$*"; record_warning "$*"; }
log_error() { printf "%s%s%s %s\n" "${C_RED}" "${ICON_ERROR}" "${C_RESET}" "$*"; record_error "$*"; }
log_ok() { printf "%s%s%s %s\n" "${C_GREEN}" "${ICON_OK}" "${C_RESET}" "$*"; }
record_warning() {
build_warning_messages+=("$1")
}
trap finish EXIT
record_error() {
build_error_messages+=("$1")
}
echo -e "================================\n Modularity - Native Linux Builder\n================================"
repeat_char() {
local char="$1"
local count="$2"
if (( count <= 0 )); then
return
fi
printf "%${count}s" "" | tr ' ' "$char"
}
progress_prefix() {
local width=24
local filled=$((current_step * width / total_steps))
local empty=$((width - filled))
printf "%s[%02d/%02d]%s [%s%s%s%s%s]" \
"${C_DIM}" "${current_step}" "${total_steps}" "${C_RESET}" \
"${C_GREEN}" "$(repeat_char "#" "${filled}")" "${C_DIM}" "$(repeat_char "-" "${empty}")" "${C_RESET}"
}
clear_status_line() {
if [[ -t 1 && "${status_line_active}" -eq 1 ]]; then
printf "\r\033[2K"
status_line_active=0
fi
}
render_status_line() {
local label="$1"
local spinner="$2"
local elapsed="$3"
if [[ ! -t 1 ]]; then
return
fi
status_current_label="${label}"
status_current_spinner="${spinner}"
status_current_elapsed="${elapsed}"
printf "\r\033[2K%s %s%s%s %s %s(%ss)%s" \
"$(progress_prefix)" \
"${C_CYAN}" "${spinner}" "${C_RESET}" \
"${label}" \
"${C_DIM}" "${elapsed}" "${C_RESET}"
status_line_active=1
}
emit_scrolling_event() {
local level="$1"
local message="$2"
local had_status="${status_line_active}"
clear_status_line
case "${level}" in
info)
printf "%s%s%s %s\n" "${C_CYAN}" "${ICON_INFO}" "${C_RESET}" "${message}"
;;
warn)
printf "%s%s%s %s\n" "${C_YELLOW}" "${ICON_WARN}" "${C_RESET}" "${message}"
;;
error)
printf "%s%s%s %s\n" "${C_RED}" "${ICON_ERROR}" "${C_RESET}" "${message}"
;;
ok)
printf "%s%s%s %s\n" "${C_GREEN}" "${ICON_OK}" "${C_RESET}" "${message}"
;;
*)
printf "%s\n" "${message}"
;;
esac
if [[ -t 1 && "${had_status}" -eq 1 ]]; then
render_status_line "${status_current_label}" "${status_current_spinner}" "${status_current_elapsed}"
fi
}
is_interesting_info_line() {
local lower="$1"
[[ "${lower}" == *"built target"* ]] ||
[[ "${lower}" == *"configuring done"* ]] ||
[[ "${lower}" == *"generating done"* ]] ||
[[ "${lower}" == *"build files have been written"* ]] ||
[[ "${lower}" == *"installing:"* ]] ||
[[ "${lower}" == *"up-to-date:"* ]] ||
[[ "${lower}" == *"linking cxx executable"* ]] ||
[[ "${lower}" == *"linking cxx static library"* ]] ||
[[ "${lower}" == *"copying"* ]]
}
process_build_output_line() {
local step="$1"
local line="$2"
line="${line%$'\r'}"
if [[ -z "${line//[[:space:]]/}" ]]; then
return
fi
local lower="${line,,}"
local issue_text="[${step}] ${line}"
if [[ "${lower}" == *"0 errors generated"* ]]; then
return
fi
if [[ "${lower}" == *"warning:"* ]] || [[ "${lower}" == *" cmake warning"* ]] || [[ "${lower}" == *"warning "* ]]; then
record_warning "${issue_text}"
emit_scrolling_event "warn" "${issue_text}"
return
fi
if [[ "${lower}" == *"error:"* ]] || [[ "${lower}" == *"fatal"* ]] || [[ "${lower}" == *"undefined reference"* ]] || [[ "${lower}" == *"collect2: error"* ]] || [[ "${lower}" == *"ld: cannot"* ]]; then
record_error "${issue_text}"
emit_scrolling_event "error" "${issue_text}"
return
fi
if is_interesting_info_line "${lower}"; then
emit_scrolling_event "info" "[${step}] ${line}"
fi
}
print_issue_summary() {
local max_items=8
local i
if [[ "${#build_warning_messages[@]}" -gt 0 ]]; then
printf "\n%sWarnings (%d):%s\n" "${C_YELLOW}" "${#build_warning_messages[@]}" "${C_RESET}"
for ((i = 0; i < ${#build_warning_messages[@]} && i < max_items; i++)); do
printf " %s %s\n" "${ICON_WARN}" "${build_warning_messages[$i]}"
done
if [[ "${#build_warning_messages[@]}" -gt "${max_items}" ]]; then
printf " %s ... and %d more warning(s)\n" "${ICON_WARN}" "$(( ${#build_warning_messages[@]} - max_items ))"
fi
fi
if [[ "${#build_error_messages[@]}" -gt 0 ]]; then
printf "\n%sErrors (%d):%s\n" "${C_RED}" "${#build_error_messages[@]}" "${C_RESET}"
for ((i = 0; i < ${#build_error_messages[@]} && i < max_items; i++)); do
printf " %s %s\n" "${ICON_ERROR}" "${build_error_messages[$i]}"
done
if [[ "${#build_error_messages[@]}" -gt "${max_items}" ]]; then
printf " %s ... and %d more error(s)\n" "${ICON_ERROR}" "$(( ${#build_error_messages[@]} - max_items ))"
fi
fi
}
advance_step() {
local label="$1"
current_step=$((current_step + 1))
last_step="${label}"
}
run_step() {
local label="$1"
shift
advance_step "${label}"
clear_status_line
printf "\n%s %s\n" "$(progress_prefix)" "${label}"
"$@"
}
run_long_step() {
local label="$1"
shift
advance_step "${label}"
if [[ ! -t 1 ]]; then
printf "\n%s %s\n" "$(progress_prefix)" "${label}"
"$@"
return
fi
local log_file
log_file="$(mktemp "/tmp/modularity-build-step-${current_step}.XXXX.log")"
local start_step
start_step="$(date +%s)"
local spinner_frames='-\|/'
local spinner_index=0
local processed_lines=0
set +e
"$@" >"${log_file}" 2>&1 &
local cmd_pid=$!
render_status_line "${label}" "-" 0
while kill -0 "${cmd_pid}" >/dev/null 2>&1; do
local total_lines
total_lines="$(wc -l < "${log_file}" 2>/dev/null || echo 0)"
if (( total_lines > processed_lines )); then
while IFS= read -r line; do
process_build_output_line "${label}" "${line}"
done < <(sed -n "$((processed_lines + 1)),${total_lines}p" "${log_file}")
processed_lines="${total_lines}"
fi
local now elapsed frame
now="$(date +%s)"
elapsed=$((now - start_step))
frame="${spinner_frames:spinner_index:1}"
render_status_line "${label}" "${frame}" "${elapsed}"
spinner_index=$(((spinner_index + 1) % 4))
sleep 0.2
done
wait "${cmd_pid}"
local exit_code=$?
set -e
local total_lines
total_lines="$(wc -l < "${log_file}" 2>/dev/null || echo 0)"
if (( total_lines > processed_lines )); then
while IFS= read -r line; do
process_build_output_line "${label}" "${line}"
done < <(sed -n "$((processed_lines + 1)),${total_lines}p" "${log_file}")
processed_lines="${total_lines}"
fi
local now elapsed
now="$(date +%s)"
elapsed=$((now - start_step))
if [[ "${exit_code}" -ne 0 ]]; then
clear_status_line
record_error "[${label}] Step failed with exit code ${exit_code}. Log: ${log_file}"
emit_scrolling_event "error" "$(progress_prefix) ${label} (failed)"
log_error "Detailed log kept at: ${log_file}"
return "${exit_code}"
fi
clear_status_line
emit_scrolling_event "ok" "$(progress_prefix) ${label} (done in ${elapsed}s)"
rm -f "${log_file}"
}
finish() {
local exit_code="$1"
set +e
if [[ "${build_started}" -eq 0 ]]; then
return
fi
clear_status_line
local end_time
end_time="$(date +%s)"
local duration=$((end_time - start_time))
echo
if [[ "${exit_code}" -eq 0 ]]; then
printf "%s%s================================%s\n" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
printf "%s Modularity - Native Linux Build Complete%s\n" "${C_BOLD}" "${C_RESET}"
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
log_ok "Build completed in ${duration}s."
log_info "Artifacts: ${build_dir}"
print_issue_summary
else
printf "%s%s================================%s\n" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
printf "%s Modularity - Native Linux Build Failed%s\n" "${C_BOLD}" "${C_RESET}"
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
log_error "Build failed after ${duration}s at step: ${last_step} (exit code ${exit_code})."
print_issue_summary
fi
}
trap 'finish $?' EXIT
usage() {
cat <<'EOF'
Usage: ./build.sh [options]
Options:
--clean Remove existing build directories first
--build-type=<type> CMake build type (default: Release)
--generator=<name> Force CMake generator (e.g. Ninja, "Unix Makefiles")
--skip-deps Skip automatic dependency checks/install
--help Show this help message
EOF
}
clean_build=0
build_type="Release"
skip_deps=0
jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 4)"
preferred_generator=""
cmake_generator=""
for arg in "$@"; do
if [ "$arg" = "--clean" ]; then
clean_build=1
elif [[ "$arg" == --build-type=* ]]; then
build_type="${arg#*=}"
fi
case "$arg" in
--clean)
clean_build=1
;;
--build-type=*)
build_type="${arg#*=}"
;;
--generator=*)
preferred_generator="${arg#*=}"
;;
--skip-deps)
skip_deps=1
;;
--help)
usage
exit 0
;;
*)
log_warn "Unknown argument: ${arg}"
usage
exit 1
;;
esac
done
git submodule update --init --recursive
pkg_manager=""
pkg_prefix=()
pkg_index_updated=0
if command -v ccache >/dev/null 2>&1; then
export CCACHE_BASEDIR="$script_dir"
export CCACHE_NOHASHDIR=1
echo -e "[i]: ccache detected. Normalizing paths for cross-build cache hits."
detect_package_manager() {
if command -v apt-get >/dev/null 2>&1; then
pkg_manager="apt"
elif command -v dnf >/dev/null 2>&1; then
pkg_manager="dnf"
elif command -v pacman >/dev/null 2>&1; then
pkg_manager="pacman"
elif command -v zypper >/dev/null 2>&1; then
pkg_manager="zypper"
else
pkg_manager=""
fi
if [[ "$(id -u)" -eq 0 ]]; then
pkg_prefix=()
elif command -v sudo >/dev/null 2>&1; then
pkg_prefix=(sudo)
else
pkg_prefix=()
fi
}
detect_cmake_generator() {
if [[ -n "${preferred_generator}" ]]; then
cmake_generator="${preferred_generator}"
return
fi
if [[ -n "${CMAKE_GENERATOR:-}" ]]; then
cmake_generator="${CMAKE_GENERATOR}"
return
fi
if [[ "${clean_build}" -eq 0 && -f "${build_dir}/CMakeCache.txt" ]]; then
cmake_generator="$(sed -n 's/^CMAKE_GENERATOR:INTERNAL=//p' "${build_dir}/CMakeCache.txt" | head -n1)"
if [[ -n "${cmake_generator}" ]]; then
return
fi
fi
if command -v ninja >/dev/null 2>&1; then
cmake_generator="Ninja"
return
fi
cmake_generator=""
}
admin_cmd() {
if [[ "${#pkg_prefix[@]}" -gt 0 ]]; then
"${pkg_prefix[@]}" "$@"
else
"$@"
fi
}
update_pkg_index_once() {
if [[ "${pkg_index_updated}" -eq 1 ]]; then
return
fi
case "${pkg_manager}" in
apt)
admin_cmd apt-get update
;;
pacman)
admin_cmd pacman -Sy --noconfirm
;;
dnf|zypper)
;;
esac
pkg_index_updated=1
}
install_packages() {
local -a packages=("$@")
if [[ "${#packages[@]}" -eq 0 ]]; then
return
fi
update_pkg_index_once
case "${pkg_manager}" in
apt)
admin_cmd apt-get install -y "${packages[@]}"
;;
dnf)
admin_cmd dnf install -y "${packages[@]}"
;;
pacman)
admin_cmd pacman -S --noconfirm --needed "${packages[@]}"
;;
zypper)
admin_cmd zypper --non-interactive install --no-recommends "${packages[@]}"
;;
*)
return 1
;;
esac
}
install_optional_first_hit() {
local -a candidates=("$@")
local candidate
for candidate in "${candidates[@]}"; do
if install_packages "${candidate}" >/dev/null 2>&1; then
log_ok "Installed optional package: ${candidate}"
return 0
fi
done
return 1
}
ensure_linux_dependencies() {
detect_package_manager
if [[ -z "${pkg_manager}" ]]; then
log_warn "No supported package manager detected (apt/dnf/pacman/zypper). Skipping auto-install."
return
fi
if [[ "$(id -u)" -ne 0 && "${#pkg_prefix[@]}" -eq 0 ]]; then
log_warn "Auto-install requires root or sudo. Skipping dependency installation."
return
fi
local need_install=0
local -a missing=()
local cmd
for cmd in git cmake pkg-config c++; do
if ! command -v "${cmd}" >/dev/null 2>&1; then
need_install=1
missing+=("${cmd}")
fi
done
if command -v pkg-config >/dev/null 2>&1; then
local module
for module in x11 xrandr xi xinerama xcursor gl; do
if ! pkg-config --exists "${module}"; then
need_install=1
missing+=("${module}-dev")
fi
done
if ! pkg-config --exists vulkan; then
need_install=1
missing+=("vulkan-dev")
fi
fi
if ! command -v glslc >/dev/null 2>&1; then
need_install=1
missing+=("glslc")
fi
if ! command -v vulkaninfo >/dev/null 2>&1; then
need_install=1
missing+=("vulkan-tools")
fi
local -a required_pkgs=()
local -a core_pkgs=()
local -a x11_pkgs=()
local -a graphics_pkgs=()
local -a vulkan_pkgs=()
local -a optional_pkgs=()
local -a glslc_candidates=()
case "${pkg_manager}" in
apt)
core_pkgs=(build-essential cmake pkg-config git zlib1g-dev)
x11_pkgs=(libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev)
graphics_pkgs=(libgl1-mesa-dev libegl1-mesa-dev libwayland-dev)
vulkan_pkgs=(libvulkan-dev vulkan-tools glslang-tools)
optional_pkgs=(ccache)
glslc_candidates=(glslc shaderc)
;;
dnf)
core_pkgs=(gcc gcc-c++ make cmake pkgconf-pkg-config git zlib-devel)
x11_pkgs=(libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel)
graphics_pkgs=(mesa-libGL-devel mesa-libEGL-devel wayland-devel)
vulkan_pkgs=(vulkan-loader-devel vulkan-tools glslang)
optional_pkgs=(ccache)
glslc_candidates=(shaderc shaderc-devel)
;;
pacman)
core_pkgs=(base-devel cmake pkgconf git zlib)
x11_pkgs=(libx11 libxrandr libxinerama libxcursor libxi)
graphics_pkgs=(mesa wayland)
vulkan_pkgs=(vulkan-headers vulkan-tools glslang)
optional_pkgs=(ccache)
glslc_candidates=(shaderc)
;;
zypper)
core_pkgs=(gcc gcc-c++ make cmake pkg-config git zlib-devel)
x11_pkgs=(libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel)
graphics_pkgs=(Mesa-libGL-devel Mesa-libEGL-devel wayland-devel)
vulkan_pkgs=(vulkan-devel vulkan-tools glslang)
optional_pkgs=(ccache)
glslc_candidates=(shaderc shaderc-devel)
;;
esac
required_pkgs=(
"${core_pkgs[@]}"
"${x11_pkgs[@]}"
"${graphics_pkgs[@]}"
"${vulkan_pkgs[@]}"
"${optional_pkgs[@]}"
)
log_info "Dependency hierarchy (${pkg_manager}):"
echo " +-- Core toolchain"
for pkg in "${core_pkgs[@]}"; do echo " | +-- ${pkg}"; done
echo " +-- X11/Windowing"
for pkg in "${x11_pkgs[@]}"; do echo " | +-- ${pkg}"; done
echo " +-- OpenGL/EGL/Wayland"
for pkg in "${graphics_pkgs[@]}"; do echo " | +-- ${pkg}"; done
echo " +-- Vulkan stack"
for pkg in "${vulkan_pkgs[@]}"; do echo " | +-- ${pkg}"; done
echo " +-- Optional acceleration"
for pkg in "${optional_pkgs[@]}"; do echo " +-- ${pkg}"; done
if [[ "${need_install}" -eq 0 ]]; then
log_ok "Build dependencies already present."
return
fi
log_info "Missing prerequisites detected: ${missing[*]}"
log_info "Installing packages using ${pkg_manager}..."
install_packages "${required_pkgs[@]}"
if ! command -v glslc >/dev/null 2>&1; then
install_optional_first_hit "${glslc_candidates[@]}" || log_warn "glslc package candidate not found; Vulkan runtime shader compile may fail."
fi
if ! command -v glslc >/dev/null 2>&1; then
log_warn "glslc is still missing."
fi
if ! command -v vulkaninfo >/dev/null 2>&1; then
log_warn "vulkaninfo is still missing."
fi
}
sync_submodules() {
git -C "${script_dir}" submodule update --init --recursive
}
configure_ccache() {
if command -v ccache >/dev/null 2>&1; then
export CCACHE_BASEDIR="${script_dir}"
export CCACHE_NOHASHDIR=1
log_info "ccache detected. Normalizing paths for cross-build cache hits."
fi
}
clean_editor_build() {
if [[ -d "${build_dir}" ]]; then
rm -rf "${build_dir}"
log_ok "Removed ${build_dir}"
fi
}
clean_player_cache() {
if [[ -d "${player_cache_dir}" ]]; then
rm -rf "${player_cache_dir}"
log_ok "Removed ${player_cache_dir}"
fi
}
configure_editor_build() {
local -a generator_args=()
if [[ ! -f "${build_dir}/CMakeCache.txt" && -n "${cmake_generator}" ]]; then
generator_args=(-G "${cmake_generator}")
fi
cmake "${generator_args[@]}" -S "${script_dir}" -B "${build_dir}" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="${build_type}"
}
build_editor_targets() {
cmake --build "${build_dir}" --parallel "${jobs}"
}
install_editor_targets() {
cmake --install "${build_dir}" --prefix "${build_dir}/install"
}
copy_third_party_libraries() {
local target_dir="$1"
mkdir -p "${target_dir}/Packages/ThirdParty"
find "${target_dir}" -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
-not -path "${target_dir}/Packages/*" -exec cp -f {} "${target_dir}/Packages/ThirdParty/" \;
}
copy_engine_libraries() {
local target_dir="$1"
mkdir -p "${target_dir}/Packages/Engine"
find "${target_dir}" -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
-not -path "${target_dir}/Packages/*" -exec cp -f {} "${target_dir}/Packages/Engine/" \;
}
configure_player_build() {
local -a generator_args=()
if [[ ! -f "${player_cache_dir}/CMakeCache.txt" && -n "${cmake_generator}" ]]; then
generator_args=(-G "${cmake_generator}")
fi
cmake "${generator_args[@]}" -S "${script_dir}" -B "${player_cache_dir}" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="${build_type}" -DMODULARITY_BUILD_EDITOR=OFF
}
build_player_target() {
cmake --build "${player_cache_dir}" --target ModularityPlayer --parallel "${jobs}"
}
finalize_packaging() {
rm -rf "${build_dir}/Template-Projects"
cp -r "${script_dir}/Resources" "${build_dir}/"
if [[ -d "${script_dir}/Template-Projects" ]]; then
cp -r "${script_dir}/Template-Projects" "${build_dir}/"
else
mkdir -p "${build_dir}/Template-Projects"
fi
if [[ -f "${build_dir}/Resources/imgui.ini" ]]; then
cp "${build_dir}/Resources/imgui.ini" "${build_dir}/"
fi
ln -sfn "${build_dir}/compile_commands.json" "${script_dir}/compile_commands.json"
(cd "${build_dir}" && cpack)
}
show_stage_hierarchy() {
local -a stages=()
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
stages+=("Check/install system dependencies")
fi
if [[ "${clean_build}" -eq 1 ]]; then
stages+=("Clean editor build directory")
stages+=("Clean player cache directory")
fi
stages+=(
"Sync git submodules"
"Configure editor build"
"Build editor + engine targets"
"Install editor artifacts"
"Collect editor third-party libraries"
"Collect editor engine libraries"
"Configure player-only cache build"
"Build ModularityPlayer target"
"Collect player third-party libraries"
"Collect player engine libraries"
"Package artifacts and resources"
)
log_info "Build stage hierarchy:"
local i
local stage_count="${#stages[@]}"
for ((i = 0; i < stage_count; i++)); do
local branch="|--"
if [[ $((i + 1)) -eq "${stage_count}" ]]; then
branch='`--'
fi
printf " %s [%02d/%02d] %s\n" "${branch}" "$((i + 1))" "${stage_count}" "${stages[$i]}"
done
}
base_steps=11
total_steps="${base_steps}"
if [[ "${clean_build}" -eq 1 ]]; then
total_steps=$((total_steps + 2))
fi
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
total_steps=$((total_steps + 1))
fi
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"
build_started=1
detect_cmake_generator
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
printf "%s Modularity - Native Linux Builder%s\n" "${C_BOLD}" "${C_RESET}"
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
log_info "Build type: ${build_type} | Jobs: ${jobs}"
if [[ -n "${cmake_generator}" ]]; then
log_info "CMake generator: ${cmake_generator}"
fi
show_stage_hierarchy
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
run_step "Checking and installing system dependencies (Vulkan/OpenGL/X11/toolchain)" ensure_linux_dependencies
elif [[ "${skip_deps}" -eq 0 ]]; then
log_warn "Auto dependency install is only implemented for Linux."
fi
mkdir -p build
cd build
cmake .. -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="$build_type"
cmake --build . -- -j"$(nproc)"
cmake --install . --prefix install
configure_ccache
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"
if [[ "${clean_build}" -eq 1 ]]; then
run_long_step "Cleaning editor build directory" clean_editor_build
run_long_step "Cleaning player cache directory" clean_player_cache
fi
mkdir -p "$player_cache_dir"
cmake -S . -B "$player_cache_dir" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="$build_type" -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 compile_commands.json ../compile_commands.json
cpack
run_long_step "Syncing git submodules" sync_submodules
run_long_step "Configuring editor build (CMake)" configure_editor_build
run_long_step "Building editor + engine targets" build_editor_targets
run_long_step "Installing editor artifacts" install_editor_targets
run_long_step "Collecting editor third-party libraries" copy_third_party_libraries "${build_dir}"
run_long_step "Collecting editor engine libraries" copy_engine_libraries "${build_dir}"
run_long_step "Configuring player-only cache build" configure_player_build
run_long_step "Building ModularityPlayer target" build_player_target
run_long_step "Collecting player third-party libraries" copy_third_party_libraries "${player_cache_dir}"
run_long_step "Collecting player engine libraries" copy_engine_libraries "${player_cache_dir}"
run_long_step "Packaging artifacts and resources" finalize_packaging

View File

@@ -1 +1 @@
compile_commands.json
/home/anemunt/Git-base/Modularity/build/compile_commands.json

View File

@@ -18,6 +18,7 @@ public:
void setFloat(const std::string &name, float value) const;
void setVec2(const std::string &name, const glm::vec2 &value) const;
void setVec3(const std::string &name, const glm::vec3 &value) const;
void setVec4(const std::string &name, const glm::vec4 &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;

5584
include/ThirdParty/stb_vorbis.c vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,23 @@
#define STB_VORBIS_HEADER_ONLY
#include "../include/ThirdParty/stb_vorbis.c"
#define MINIAUDIO_IMPLEMENTATION
#include "../include/ThirdParty/miniaudio.h"
#include "AudioSystem.h"
#include <cmath>
#include <atomic>
#include <array>
#if MODULARITY_HAS_SNDFILE
#include <sndfile.h>
#endif
#undef STB_VORBIS_HEADER_ONLY
#include "../include/ThirdParty/stb_vorbis.c"
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
namespace {
constexpr size_t kPreviewBuckets = 800;
@@ -14,6 +28,58 @@ constexpr size_t kReverbAllpassCount = 2;
constexpr float kReverbPreDelayMaxSeconds = 0.2f;
constexpr float kReverbReflectionsMaxSeconds = 0.1f;
void BuildWaveformPreview(AudioClipPreview& preview, const float* samples, ma_uint64 totalFrames) {
if (!samples || totalFrames == 0 || preview.channels == 0) {
return;
}
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
if (preview.channels >= 2) {
preview.waveformLeft.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
preview.waveformRight.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
}
size_t bucketIndex = 0;
ma_uint64 bucketCursor = 0;
float bucketMax = 0.0f;
float bucketMaxLeft = 0.0f;
float bucketMaxRight = 0.0f;
for (ma_uint64 frame = 0; frame < totalFrames && bucketIndex < preview.waveform.size(); ++frame) {
const size_t frameOffset = static_cast<size_t>(frame * preview.channels);
for (ma_uint32 channel = 0; channel < preview.channels; ++channel) {
bucketMax = std::max(bucketMax, std::fabs(samples[frameOffset + channel]));
}
if (preview.channels >= 2) {
bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(samples[frameOffset]));
bucketMaxRight = std::max(bucketMaxRight, std::fabs(samples[frameOffset + 1]));
}
bucketCursor++;
if (bucketCursor >= framesPerBucket) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
bucketIndex++;
bucketCursor = 0;
bucketMax = 0.0f;
bucketMaxLeft = 0.0f;
bucketMaxRight = 0.0f;
}
}
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
}
}
float DbToLinear(float db) {
return std::pow(10.0f, db / 20.0f);
}
@@ -225,6 +291,7 @@ void AudioSystem::destroyActiveSounds() {
for (auto& kv : activeSounds) {
if (kv.second) {
ma_sound_uninit(&kv.second->sound);
releaseDecodedAudio(kv.second->decodedData);
}
}
activeSounds.clear();
@@ -255,6 +322,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
}
if (it->second) {
ma_sound_uninit(&it->second->sound);
releaseDecodedAudio(it->second->decodedData);
}
activeSounds.erase(it);
}
@@ -270,16 +338,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
if (!initialized && !init()) return false;
auto snd = std::make_unique<ActiveSound>();
ma_result res = ma_sound_init_from_file(
&engine,
obj.audioSource.clipPath.c_str(),
MA_SOUND_FLAG_STREAM,
reverbReady ? &reverbGroup : nullptr,
nullptr,
&snd->sound
);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: failed to load " << obj.audioSource.clipPath << " (" << res << ")\n";
if (!initSoundFromPath(obj.audioSource.clipPath, MA_SOUND_FLAG_STREAM, reverbReady ? &reverbGroup : nullptr, snd->sound, snd->decodedData)) {
return false;
}
@@ -350,6 +409,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
if (eraseIt != activeSounds.end()) {
if (eraseIt->second) {
ma_sound_uninit(&eraseIt->second->sound);
releaseDecodedAudio(eraseIt->second->decodedData);
}
activeSounds.erase(eraseIt);
}
@@ -369,6 +429,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
if (stillPresent.find(it->first) == stillPresent.end()) {
if (it->second) {
ma_sound_uninit(&it->second->sound);
releaseDecodedAudio(it->second->decodedData);
}
it = activeSounds.erase(it);
} else {
@@ -382,9 +443,8 @@ bool AudioSystem::playPreview(const std::string& path, float volume, bool loop)
if (!initialized && !init()) return false;
stopPreview();
ma_result res = ma_sound_init_from_file(&engine, path.c_str(), MA_SOUND_FLAG_STREAM, nullptr, nullptr, &previewSound);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: preview load failed for " << path << " (" << res << ")\n";
if (!initSoundFromPath(path, MA_SOUND_FLAG_STREAM, nullptr, previewSound, previewDecodedData)) {
std::cerr << "AudioSystem: preview load failed for " << path << "\n";
return false;
}
ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE);
@@ -394,6 +454,7 @@ bool AudioSystem::playPreview(const std::string& path, float volume, bool loop)
previewActive = ma_sound_start(&previewSound) == MA_SUCCESS;
if (!previewActive) {
ma_sound_uninit(&previewSound);
releaseDecodedAudio(previewDecodedData);
}
return previewActive;
}
@@ -403,6 +464,7 @@ void AudioSystem::stopPreview() {
ma_sound_stop(&previewSound);
ma_sound_uninit(&previewSound);
}
releaseDecodedAudio(previewDecodedData);
previewActive = false;
previewPath.clear();
}
@@ -413,25 +475,67 @@ bool AudioSystem::isPreviewing(const std::string& path) const {
bool AudioSystem::getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const {
if (!previewActive || previewPath != path) return false;
float cur = 0.0f;
float len = 0.0f;
if (ma_sound_get_cursor_in_seconds(&previewSound, &cur) != MA_SUCCESS) return false;
if (ma_sound_get_length_in_seconds(&previewSound, &len) != MA_SUCCESS) return false;
cursorSeconds = static_cast<double>(cur);
durationSeconds = static_cast<double>(len);
ma_uint32 sampleRate = 0;
if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS || sampleRate == 0) {
return false;
}
ma_uint64 cursorFrames = 0;
if (ma_sound_get_cursor_in_pcm_frames(&previewSound, &cursorFrames) != MA_SUCCESS) return false;
cursorSeconds = static_cast<double>(cursorFrames) / static_cast<double>(sampleRate);
durationSeconds = 0.0;
auto it = previewCache.find(path);
if (it != previewCache.end() && it->second.loaded && std::isfinite(it->second.durationSeconds) && it->second.durationSeconds > 0.0) {
durationSeconds = it->second.durationSeconds;
} else if (previewDecodedData && previewDecodedData->sampleRate > 0 && previewDecodedData->frameCount > 0) {
durationSeconds = static_cast<double>(previewDecodedData->frameCount) / static_cast<double>(previewDecodedData->sampleRate);
} else {
ma_uint64 lengthFrames = 0;
if (ma_sound_get_length_in_pcm_frames(&previewSound, &lengthFrames) == MA_SUCCESS && lengthFrames > 0) {
durationSeconds = static_cast<double>(lengthFrames) / static_cast<double>(sampleRate);
}
}
if (!std::isfinite(cursorSeconds) || !std::isfinite(durationSeconds) || durationSeconds <= 0.0) {
return false;
}
cursorSeconds = std::clamp(cursorSeconds, 0.0, durationSeconds);
return true;
}
bool AudioSystem::seekPreview(const std::string& path, double seconds) {
if (!previewActive || previewPath != path) return false;
ma_uint32 sampleRate = 0;
if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS) {
ma_uint32 sourceSampleRate = 0;
auto it = previewCache.find(path);
if (it != previewCache.end() && it->second.loaded && it->second.sampleRate > 0) {
sourceSampleRate = it->second.sampleRate;
} else if (previewDecodedData && previewDecodedData->sampleRate > 0) {
sourceSampleRate = previewDecodedData->sampleRate;
} else if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sourceSampleRate, nullptr, 0) != MA_SUCCESS || sourceSampleRate == 0) {
return false;
}
float lenSec = 0.0f;
ma_sound_get_length_in_seconds(&previewSound, &lenSec);
seconds = std::clamp(seconds, 0.0, static_cast<double>(lenSec));
ma_uint64 targetFrame = static_cast<ma_uint64>(seconds * static_cast<double>(sampleRate));
double maxSeconds = 0.0;
if (it != previewCache.end() && it->second.loaded && std::isfinite(it->second.durationSeconds) && it->second.durationSeconds > 0.0) {
maxSeconds = it->second.durationSeconds;
} else if (previewDecodedData && previewDecodedData->sampleRate > 0 && previewDecodedData->frameCount > 0) {
maxSeconds = static_cast<double>(previewDecodedData->frameCount) / static_cast<double>(previewDecodedData->sampleRate);
} else {
ma_uint64 lengthFrames = 0;
if (ma_sound_get_length_in_pcm_frames(&previewSound, &lengthFrames) == MA_SUCCESS && lengthFrames > 0) {
maxSeconds = static_cast<double>(lengthFrames) / static_cast<double>(sourceSampleRate);
}
}
if (maxSeconds > 0.0 && std::isfinite(maxSeconds)) {
seconds = std::clamp(seconds, 0.0, maxSeconds);
} else {
seconds = std::max(0.0, seconds);
}
ma_uint64 targetFrame = static_cast<ma_uint64>(seconds * static_cast<double>(sourceSampleRate));
ma_result res = ma_sound_seek_to_pcm_frame(&previewSound, targetFrame);
return res == MA_SUCCESS;
}
@@ -617,12 +721,94 @@ void AudioSystem::shutdownReverbGraph() {
currentReverb = ReverbSettings{};
}
std::shared_ptr<AudioSystem::DecodedAudioData> AudioSystem::decodeClipToMemory(const std::string& path) {
#if MODULARITY_HAS_SNDFILE
SF_INFO info{};
SNDFILE* file = sf_open(path.c_str(), SFM_READ, &info);
if (!file) {
return nullptr;
}
if (info.frames <= 0 || info.channels <= 0 || info.samplerate <= 0) {
sf_close(file);
return nullptr;
}
auto decoded = std::make_shared<DecodedAudioData>();
decoded->channels = static_cast<ma_uint32>(info.channels);
decoded->sampleRate = static_cast<ma_uint32>(info.samplerate);
decoded->frameCount = static_cast<ma_uint64>(info.frames);
decoded->pcmFrames.resize(static_cast<size_t>(decoded->frameCount * decoded->channels));
const sf_count_t framesRead = sf_readf_float(file, decoded->pcmFrames.data(), info.frames);
sf_close(file);
if (framesRead <= 0) {
return nullptr;
}
decoded->frameCount = static_cast<ma_uint64>(framesRead);
decoded->pcmFrames.resize(static_cast<size_t>(decoded->frameCount * decoded->channels));
if (ma_audio_buffer_ref_init(ma_format_f32, decoded->channels, decoded->pcmFrames.data(), decoded->frameCount, &decoded->buffer) != MA_SUCCESS) {
return nullptr;
}
decoded->initialized = true;
return decoded;
#else
(void)path;
return nullptr;
#endif
}
bool AudioSystem::initSoundFromPath(const std::string& path, ma_uint32 flags, ma_sound_group* group, ma_sound& sound,
std::shared_ptr<DecodedAudioData>& decodedData) {
ma_result res = ma_sound_init_from_file(&engine, path.c_str(), flags, group, nullptr, &sound);
if (res == MA_SUCCESS) {
return true;
}
decodedData = decodeClipToMemory(path);
if (!decodedData) {
std::cerr << "AudioSystem: miniaudio load failed for " << path << " (" << res << ")\n";
return false;
}
res = ma_sound_init_from_data_source(&engine, &decodedData->buffer.ds, flags & ~MA_SOUND_FLAG_STREAM, group, &sound);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: decoded fallback load failed for " << path << " (" << res << ")\n";
releaseDecodedAudio(decodedData);
return false;
}
return true;
}
void AudioSystem::releaseDecodedAudio(std::shared_ptr<DecodedAudioData>& decodedData) {
if (decodedData && decodedData->initialized) {
ma_audio_buffer_ref_uninit(&decodedData->buffer);
decodedData->initialized = false;
}
decodedData.reset();
}
AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
AudioClipPreview preview;
preview.path = path;
ma_decoder decoder;
if (ma_decoder_init_file(path.c_str(), nullptr, &decoder) != MA_SUCCESS) {
auto decoded = decodeClipToMemory(path);
if (!decoded) {
return preview;
}
preview.channels = decoded->channels;
preview.sampleRate = decoded->sampleRate;
preview.durationSeconds = (preview.sampleRate > 0)
? static_cast<double>(decoded->frameCount) / static_cast<double>(preview.sampleRate)
: 0.0;
BuildWaveformPreview(preview, decoded->pcmFrames.data(), decoded->frameCount);
preview.loaded = true;
releaseDecodedAudio(decoded);
return preview;
}
@@ -637,22 +823,12 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
return preview;
}
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
if (preview.channels >= 2) {
preview.waveformLeft.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
preview.waveformRight.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
}
std::vector<float> temp(kPreviewChunkFrames * preview.channels);
std::vector<float> pcmFrames;
pcmFrames.reserve(static_cast<size_t>(totalFrames * preview.channels));
ma_uint64 frameCursor = 0;
size_t bucketIndex = 0;
ma_uint64 bucketCursor = 0;
float bucketMax = 0.0f;
float bucketMaxLeft = 0.0f;
float bucketMaxRight = 0.0f;
while (frameCursor < totalFrames && bucketIndex < preview.waveform.size()) {
while (frameCursor < totalFrames) {
ma_uint64 framesToRead = std::min<ma_uint64>(kPreviewChunkFrames, totalFrames - frameCursor);
ma_uint64 framesRead = 0;
ma_result readResult = ma_decoder_read_pcm_frames(&decoder, temp.data(), framesToRead, &framesRead);
@@ -660,48 +836,15 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
break;
}
if (framesRead == 0) break;
for (ma_uint64 f = 0; f < framesRead; ++f) {
size_t frameOffset = static_cast<size_t>(f * preview.channels);
for (ma_uint32 c = 0; c < preview.channels; ++c) {
float sample = temp[frameOffset + c];
bucketMax = std::max(bucketMax, std::fabs(sample));
}
if (preview.channels >= 2) {
float leftSample = temp[frameOffset];
float rightSample = temp[frameOffset + 1];
bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(leftSample));
bucketMaxRight = std::max(bucketMaxRight, std::fabs(rightSample));
}
bucketCursor++;
frameCursor++;
if (bucketCursor >= framesPerBucket) {
if (bucketIndex < preview.waveform.size()) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
bucketIndex++;
}
bucketCursor = 0;
bucketMax = 0.0f;
bucketMaxLeft = 0.0f;
bucketMaxRight = 0.0f;
}
}
}
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
if (preview.channels >= 2) {
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
}
pcmFrames.insert(pcmFrames.end(), temp.begin(), temp.begin() + static_cast<std::ptrdiff_t>(framesRead * preview.channels));
frameCursor += framesRead;
}
ma_decoder_uninit(&decoder);
if (frameCursor == 0) {
return preview;
}
BuildWaveformPreview(preview, pcmFrames.data(), frameCursor);
preview.loaded = true;
return preview;
}

View File

@@ -6,6 +6,7 @@
#include "../include/ThirdParty/miniaudio.h"
#include <unordered_map>
#include <unordered_set>
#include <memory>
struct AudioClipPreview {
bool loaded = false;
@@ -20,6 +21,15 @@ struct AudioClipPreview {
class AudioSystem {
public:
struct DecodedAudioData {
std::vector<float> pcmFrames;
ma_audio_buffer_ref buffer{};
ma_uint32 channels = 0;
ma_uint32 sampleRate = 0;
ma_uint64 frameCount = 0;
bool initialized = false;
};
bool init();
void shutdown();
bool isReady() const { return initialized; }
@@ -92,6 +102,7 @@ private:
std::string clipPath;
bool spatial = true;
bool started = false; // prevents auto-restart after manual stop
std::shared_ptr<DecodedAudioData> decodedData;
};
ma_engine engine{};
@@ -109,12 +120,17 @@ private:
ma_sound previewSound{};
bool previewActive = false;
std::string previewPath;
std::shared_ptr<DecodedAudioData> previewDecodedData;
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);
std::shared_ptr<DecodedAudioData> decodeClipToMemory(const std::string& path);
bool initSoundFromPath(const std::string& path, ma_uint32 flags, ma_sound_group* group, ma_sound& sound,
std::shared_ptr<DecodedAudioData>& decodedData);
void releaseDecodedAudio(std::shared_ptr<DecodedAudioData>& decodedData);
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);

View File

@@ -1,4 +1,69 @@
#include "EditorUI.h"
#include <unordered_map>
namespace {
struct TouchSwipeWindowState {
ImVec2 virtualScroll = ImVec2(0.0f, 0.0f);
ImVec2 velocity = ImVec2(0.0f, 0.0f);
bool initialized = false;
bool touchedThisFrame = false;
bool isDragging = false;
};
struct TouchSwipeRuntimeState {
std::unordered_map<ImGuiID, TouchSwipeWindowState> windowStates;
ImGuiID activeWindowId = 0;
ImVec2 dragStartPos = ImVec2(0.0f, 0.0f);
ImVec2 lastPointerPos = ImVec2(0.0f, 0.0f);
bool dragging = false;
};
bool hasScrollableAxis(const ImGuiWindow* window, int axis) {
if (!window || axis < 0 || axis > 1) {
return false;
}
if ((window->Flags & ImGuiWindowFlags_NoInputs) != 0) {
return false;
}
if ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) != 0) {
return false;
}
return window->ScrollMax[axis] > 0.0f;
}
bool isTouchScrollableWindow(const ImGuiWindow* window) {
if (!window || !window->Active || window->Collapsed || window->SkipItems) {
return false;
}
return hasScrollableAxis(window, 0) || hasScrollableAxis(window, 1);
}
ImGuiWindow* findScrollableWindowFromHover(ImGuiWindow* hovered) {
for (ImGuiWindow* window = hovered; window != nullptr; window = window->ParentWindow) {
if (isTouchScrollableWindow(window)) {
return window;
}
}
return nullptr;
}
float applyEdgeResistance(float value, float minValue, float maxValue, float resistance) {
if (value < minValue) {
return minValue + (value - minValue) * resistance;
}
if (value > maxValue) {
return maxValue + (value - maxValue) * resistance;
}
return value;
}
float computeElasticOverscrollLimit(float axisExtent, float scrollMax) {
const float byViewport = axisExtent * 0.14f;
const float byRange = scrollMax * 0.35f + 6.0f;
return ImClamp(std::min(byViewport, byRange), 6.0f, 26.0f);
}
}
#pragma region File Browser
FileBrowser::FileBrowser() {
@@ -453,3 +518,285 @@ ImGuiID setupDockspace(const std::function<void()>& menuBarContent) {
return dockspaceId;
}
#pragma endregion
#pragma region Touch Swipe Scroll
void updateTouchSwipeScrolling() {
ImGuiContext* context = ImGui::GetCurrentContext();
if (!context) {
return;
}
ImGuiContext& g = *context;
ImGuiIO& io = ImGui::GetIO();
static TouchSwipeRuntimeState runtime;
const bool touchScreenMode = (io.ConfigFlags & ImGuiConfigFlags_IsTouchScreen) != 0;
const float dt = std::max(io.DeltaTime, 1.0f / 240.0f);
const float dragThresholdSqr = 16.0f;
const float edgeResistance = 0.18f;
const float wheelEdgeResistance = 0.52f;
const float freeScrollFriction = 5.8f;
const float overscrollReturnRate = 2.2f;
const float overscrollVelocityDamping = 9.0f;
const float settleVelocityEpsilon = 0.35f;
const float settlePositionEpsilon = 0.20f;
for (auto& [id, state] : runtime.windowStates) {
state.touchedThisFrame = false;
if (id != runtime.activeWindowId) {
state.isDragging = false;
}
}
for (ImGuiWindow* window : g.Windows) {
if (!isTouchScrollableWindow(window)) {
continue;
}
TouchSwipeWindowState& state = runtime.windowStates[window->ID];
state.touchedThisFrame = true;
if (!state.initialized) {
state.initialized = true;
state.virtualScroll = window->Scroll;
state.velocity = ImVec2(0.0f, 0.0f);
continue;
}
const bool stateIsIdle = !state.isDragging &&
std::abs(state.velocity.x) < 1.0f &&
std::abs(state.velocity.y) < 1.0f;
if (stateIsIdle) {
state.virtualScroll = window->Scroll;
}
}
if (runtime.activeWindowId != 0) {
ImGuiWindow* activeWindow = ImGui::FindWindowByID(runtime.activeWindowId);
auto it = runtime.windowStates.find(runtime.activeWindowId);
if (!activeWindow || !isTouchScrollableWindow(activeWindow) ||
it == runtime.windowStates.end() || !it->second.touchedThisFrame) {
runtime.activeWindowId = 0;
runtime.dragging = false;
}
}
ImVec2 wheel(io.MouseWheelH, io.MouseWheel);
if (io.MouseWheelRequestAxisSwap) {
wheel = ImVec2(wheel.y, 0.0f);
}
if ((std::abs(wheel.x) > 0.0001f || std::abs(wheel.y) > 0.0001f) && g.MovingWindow == nullptr) {
ImGuiWindow* baseWindow = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow;
ImGuiWindow* wheelWindow = findScrollableWindowFromHover(baseWindow);
if (wheelWindow) {
TouchSwipeWindowState& state = runtime.windowStates[wheelWindow->ID];
state.touchedThisFrame = true;
if (!state.initialized) {
state.initialized = true;
state.virtualScroll = wheelWindow->Scroll;
state.velocity = ImVec2(0.0f, 0.0f);
}
for (int axis = 0; axis < 2; ++axis) {
const float wheelDelta = (axis == 0) ? wheel.x : wheel.y;
if (!hasScrollableAxis(wheelWindow, axis) || std::abs(wheelDelta) < 0.0001f) {
continue;
}
const float maxScroll = wheelWindow->ScrollMax[axis];
const float currentScroll = wheelWindow->Scroll[axis];
const bool pushingMin = wheelDelta > 0.0f;
const bool pushingMax = wheelDelta < 0.0f;
const bool atMin = currentScroll <= 0.5f;
const bool atMax = currentScroll >= maxScroll - 0.5f;
const bool outside = state.virtualScroll[axis] < 0.0f || state.virtualScroll[axis] > maxScroll;
if (!outside && !((pushingMin && atMin) || (pushingMax && atMax))) {
continue;
}
const float maxStep = (axis == 0)
? (wheelWindow->InnerRect.GetWidth() * 0.67f)
: (wheelWindow->InnerRect.GetHeight() * 0.67f);
const float baseStep = (axis == 0)
? (2.0f * wheelWindow->FontRefSize)
: (5.0f * wheelWindow->FontRefSize);
const float scrollStep = ImTrunc(ImMin(baseStep, maxStep));
const float delta = -wheelDelta * scrollStep;
const float axisExtent = (axis == 0)
? wheelWindow->InnerRect.GetWidth()
: wheelWindow->InnerRect.GetHeight();
const float overscrollLimit = computeElasticOverscrollLimit(axisExtent, maxScroll);
const float clampedValue = ImClamp(state.virtualScroll[axis], 0.0f, maxScroll);
const float overshoot = std::abs(state.virtualScroll[axis] - clampedValue);
const float remainingFactor = ImClamp(
1.0f - (overshoot / std::max(overscrollLimit, 0.001f)),
0.12f,
1.0f);
const float previousValue = state.virtualScroll[axis];
const float stretchedValue = applyEdgeResistance(
previousValue + delta * 0.50f * remainingFactor,
0.0f,
maxScroll,
wheelEdgeResistance);
state.virtualScroll[axis] = stretchedValue;
const float frameVelocity = (stretchedValue - previousValue) / dt;
state.velocity[axis] = ImLerp(state.velocity[axis], frameVelocity, 0.35f);
}
}
}
if (touchScreenMode) {
if (io.MouseClicked[0] && runtime.activeWindowId == 0 &&
g.ActiveId == 0 && g.MovingWindow == nullptr) {
ImGuiWindow* hovered = findScrollableWindowFromHover(g.HoveredWindow);
if (hovered) {
const bool clickInTitleBar = hovered->TitleBarHeight > 0.0f &&
hovered->TitleBarRect().Contains(io.MouseClickedPos[0]);
if (!clickInTitleBar) {
runtime.activeWindowId = hovered->ID;
runtime.dragStartPos = io.MouseClickedPos[0];
runtime.lastPointerPos = io.MousePos;
runtime.dragging = false;
TouchSwipeWindowState& state = runtime.windowStates[hovered->ID];
state.isDragging = false;
state.velocity = ImVec2(0.0f, 0.0f);
state.virtualScroll = hovered->Scroll;
state.initialized = true;
}
}
}
if (!io.MouseDown[0]) {
if (runtime.activeWindowId != 0) {
auto it = runtime.windowStates.find(runtime.activeWindowId);
if (it != runtime.windowStates.end()) {
it->second.isDragging = false;
}
}
runtime.activeWindowId = 0;
runtime.dragging = false;
} else if (runtime.activeWindowId != 0) {
ImGuiWindow* activeWindow = ImGui::FindWindowByID(runtime.activeWindowId);
auto it = runtime.windowStates.find(runtime.activeWindowId);
if (activeWindow && it != runtime.windowStates.end()) {
TouchSwipeWindowState& state = it->second;
const ImVec2 totalDragDelta(
io.MousePos.x - runtime.dragStartPos.x,
io.MousePos.y - runtime.dragStartPos.y);
if (!runtime.dragging && ImLengthSqr(totalDragDelta) >= dragThresholdSqr) {
runtime.dragging = true;
state.isDragging = true;
}
const ImVec2 pointerDelta(
io.MousePos.x - runtime.lastPointerPos.x,
io.MousePos.y - runtime.lastPointerPos.y);
runtime.lastPointerPos = io.MousePos;
if (runtime.dragging && g.ActiveId == 0 && g.MovingWindow == nullptr) {
for (int axis = 0; axis < 2; ++axis) {
if (!hasScrollableAxis(activeWindow, axis)) {
continue;
}
const float maxScroll = activeWindow->ScrollMax[axis];
const float previousValue = state.virtualScroll[axis];
const float draggedValue = previousValue - pointerDelta[axis];
state.virtualScroll[axis] = applyEdgeResistance(
draggedValue, 0.0f, maxScroll, edgeResistance);
const float frameVelocity = (state.virtualScroll[axis] - previousValue) / dt;
state.velocity[axis] = ImLerp(state.velocity[axis], frameVelocity, 0.65f);
}
}
} else {
runtime.activeWindowId = 0;
runtime.dragging = false;
}
}
} else {
if (runtime.activeWindowId != 0) {
auto it = runtime.windowStates.find(runtime.activeWindowId);
if (it != runtime.windowStates.end()) {
it->second.isDragging = false;
}
}
runtime.activeWindowId = 0;
runtime.dragging = false;
}
for (auto& [windowId, state] : runtime.windowStates) {
if (!state.touchedThisFrame) {
continue;
}
ImGuiWindow* window = ImGui::FindWindowByID(windowId);
if (!window) {
continue;
}
const bool draggingThisWindow = runtime.dragging &&
runtime.activeWindowId == windowId &&
io.MouseDown[0] &&
state.isDragging;
for (int axis = 0; axis < 2; ++axis) {
if (!hasScrollableAxis(window, axis)) {
state.virtualScroll[axis] = 0.0f;
state.velocity[axis] = 0.0f;
continue;
}
const float maxScroll = window->ScrollMax[axis];
if (!draggingThisWindow) {
if (!touchScreenMode &&
state.virtualScroll[axis] >= -0.05f &&
state.virtualScroll[axis] <= maxScroll + 0.05f) {
state.virtualScroll[axis] = window->Scroll[axis];
state.velocity[axis] *= 0.5f;
}
float value = state.virtualScroll[axis];
float velocity = state.velocity[axis];
value += velocity * dt;
const float clampedValue = ImClamp(value, 0.0f, maxScroll);
const float stretch = value - clampedValue;
if (std::abs(stretch) > 0.0f) {
const float returnBlend = 1.0f - std::exp(-overscrollReturnRate * dt);
value = ImLerp(value, clampedValue, returnBlend);
velocity *= std::exp(-overscrollVelocityDamping * dt);
} else {
velocity *= std::exp(-freeScrollFriction * dt);
}
if (std::abs(velocity) < settleVelocityEpsilon &&
std::abs(value - clampedValue) < settlePositionEpsilon) {
value = clampedValue;
velocity = 0.0f;
}
state.virtualScroll[axis] = value;
state.velocity[axis] = velocity;
}
const float axisExtent = (axis == 0) ? window->InnerRect.GetWidth() : window->InnerRect.GetHeight();
const float overscrollLimit = computeElasticOverscrollLimit(axisExtent, maxScroll);
const float targetScroll = ImClamp(
state.virtualScroll[axis],
-overscrollLimit,
maxScroll + overscrollLimit);
state.virtualScroll[axis] = targetScroll;
if (axis == 0) {
ImGui::SetScrollX(window, targetScroll);
} else {
ImGui::SetScrollY(window, targetScroll);
}
}
}
for (auto it = runtime.windowStates.begin(); it != runtime.windowStates.end();) {
if (!it->second.touchedThisFrame && it->first != runtime.activeWindowId) {
it = runtime.windowStates.erase(it);
} else {
++it;
}
}
}
#pragma endregion

View File

@@ -75,4 +75,7 @@ void applySuperRoundStyle(ImGuiStyle& style);
// Setup ImGui dockspace for the editor and return its stable dockspace ID.
ImGuiID setupDockspace(const std::function<void()>& menuBarContent = nullptr);
// Apply touch-style swipe scrolling with inertial motion and elastic edge return.
void updateTouchSwipeScrolling();
#pragma endregion

File diff suppressed because it is too large Load Diff

View File

@@ -732,6 +732,7 @@ void Engine::renderHierarchyPanel() {
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
if (ImGui::MenuItem("Sprite (Quad)")) addObject(ObjectType::Sprite, "Sprite");
if (ImGui::MenuItem("2.5D Object")) addObject(ObjectType::Sprite25D, "2.5D Object");
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
ImGui::EndMenu();
}
@@ -1795,7 +1796,9 @@ void Engine::renderInspectorPanel() {
ImGui::Text("Type:");
ImGui::SameLine();
const char* typeLabel = "Empty";
if (obj.hasRenderer) {
if (obj.type == ObjectType::Sprite25D) {
typeLabel = "2.5D Object";
} else if (obj.hasRenderer) {
switch (obj.renderType) {
case RenderType::Cube: typeLabel = "Cube"; break;
case RenderType::Sphere: typeLabel = "Sphere"; break;
@@ -1865,7 +1868,9 @@ void Engine::renderInspectorPanel() {
if (obj.hasPostFX) {
ImGui::TextDisabled("Transform is ignored for post-processing nodes.");
}
if (isUIObject(obj)) {
if (obj.type == ObjectType::Sprite25D) {
ImGui::TextDisabled("2.5D objects use the transform for 3D placement and the UI section for sprite content.");
} else if (isUIObject(obj)) {
ImGui::TextDisabled("UI objects use the UI section for positioning.");
}
@@ -1919,15 +1924,19 @@ void Engine::renderInspectorPanel() {
ImGui::PushID("UI");
ImGui::Indent(10.0f);
const char* anchors[] = { "Center", "Top Left", "Top Right", "Bottom Left", "Bottom Right" };
int anchor = static_cast<int>(obj.ui.anchor);
if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) {
obj.ui.anchor = static_cast<UIAnchor>(anchor);
changed = true;
}
if (obj.type != ObjectType::Sprite25D) {
const char* anchors[] = { "Center", "Top Left", "Top Right", "Bottom Left", "Bottom Right" };
int anchor = static_cast<int>(obj.ui.anchor);
if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) {
obj.ui.anchor = static_cast<UIAnchor>(anchor);
changed = true;
}
if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) {
changed = true;
if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) {
changed = true;
}
} else {
ImGui::TextDisabled("Anchor and UI position are ignored for projected 2.5D sprites.");
}
if (ImGui::DragFloat("Rotation (deg)", &obj.ui.rotation, 0.5f, -360.0f, 360.0f)) {
@@ -1949,6 +1958,9 @@ void Engine::renderInspectorPanel() {
changed = true;
}
if (obj.ui.renderIn3D) {
if (ImGui::Checkbox("Face Camera", &obj.faceCamera)) {
changed = true;
}
int size[2] = { obj.ui.renderTargetSize.x, obj.ui.renderTargetSize.y };
if (ImGui::DragInt2("Render Target (px)", size, 1.0f, 16, 4096)) {
obj.ui.renderTargetSize.x = std::max(16, size[0]);
@@ -3564,6 +3576,45 @@ void Engine::renderInspectorPanel() {
materialChanged |= textureField("Detail Map", "ObjOverlay", obj.overlayTexturePath);
materialChanged |= textureField("Normal Map", "ObjNormal", obj.normalMapPath);
if (obj.renderType == RenderType::Sprite) {
if (!obj.albedoTexturePath.empty()) {
if (ImGui::SmallButton("Import Sheet##WorldSpriteSheet")) {
pendingSpriteSheetPath = obj.albedoTexturePath;
std::snprintf(importSpriteSheetName, sizeof(importSpriteSheetName), "%s", obj.name.c_str());
importSpriteSheetAsSprite2D = false;
showImportSpriteSheetDialog = true;
}
}
if (ImGui::CollapsingHeader("Sprite Sheet", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Enable Sprite Sheet", &obj.ui.spriteSheetEnabled)) {
materialChanged = true;
}
ImGui::BeginDisabled(!obj.ui.spriteSheetEnabled);
if (ImGui::DragInt("Columns", &obj.ui.spriteSheetColumns, 1.0f, 1, 1024)) {
obj.ui.spriteSheetColumns = std::max(1, obj.ui.spriteSheetColumns);
materialChanged = true;
}
if (ImGui::DragInt("Rows", &obj.ui.spriteSheetRows, 1.0f, 1, 1024)) {
obj.ui.spriteSheetRows = std::max(1, obj.ui.spriteSheetRows);
materialChanged = true;
}
int frameCount = std::max(1, obj.ui.spriteSheetColumns * obj.ui.spriteSheetRows);
if (ImGui::SliderInt("Frame", &obj.ui.spriteSheetFrame, 0, frameCount - 1)) {
obj.ui.spriteSheetFrame = std::clamp(obj.ui.spriteSheetFrame, 0, frameCount - 1);
materialChanged = true;
}
if (ImGui::DragFloat("FPS", &obj.ui.spriteSheetFps, 0.1f, 1.0f, 120.0f, "%.1f")) {
obj.ui.spriteSheetFps = std::clamp(obj.ui.spriteSheetFps, 1.0f, 120.0f);
materialChanged = true;
}
if (ImGui::Checkbox("Loop", &obj.ui.spriteSheetLoop)) {
materialChanged = true;
}
ImGui::EndDisabled();
}
}
ImGui::Spacing();
ImGui::TextDisabled("Shader");
const char* shaderPresetOptions[] = { "Custom", "Engine Lit (Default)", "Scrolling UV" };
@@ -4501,6 +4552,7 @@ void Engine::renderInspectorPanel() {
addEntry("Renderer/Sprite (Quad)", true, [&]() {
obj.hasRenderer = true;
obj.renderType = RenderType::Sprite;
obj.faceCamera = false;
obj.scale = glm::vec3(1.0f, 1.0f, 0.05f);
obj.material.ambientStrength = 1.0f;
syncLocalTransform(obj);

View File

@@ -21,6 +21,59 @@
#include <shlobj.h>
#endif
namespace {
bool ProjectWorldToOverlayPoint(const glm::vec3& worldPos,
const glm::mat4& view,
const glm::mat4& proj,
const ImVec2& overlayPos,
const ImVec2& overlaySize,
ImVec2& outScreen) {
glm::vec4 clip = proj * view * glm::vec4(worldPos, 1.0f);
if (clip.w <= 0.0001f) {
return false;
}
glm::vec3 ndc = glm::vec3(clip) / clip.w;
if (ndc.z < -1.0f || ndc.z > 1.0f) {
return false;
}
outScreen.x = overlayPos.x + (ndc.x * 0.5f + 0.5f) * overlaySize.x;
outScreen.y = overlayPos.y + (1.0f - (ndc.y * 0.5f + 0.5f)) * overlaySize.y;
return true;
}
bool ResolveProjectedSprite25DRect(const SceneObject& obj,
const glm::mat4& view,
const glm::mat4& proj,
const ImVec2& overlayPos,
const ImVec2& overlaySize,
ImVec2& outMin,
ImVec2& outMax) {
glm::mat4 invView = glm::inverse(view);
glm::vec3 cameraRight = glm::normalize(glm::vec3(invView[0]));
glm::vec3 cameraUp = glm::normalize(glm::vec3(invView[1]));
glm::vec2 baseSize = glm::max(obj.ui.size, glm::vec2(1.0f));
glm::vec3 objectScale = glm::max(glm::abs(obj.scale), glm::vec3(0.01f));
glm::vec2 worldHalfExtents = glm::vec2(baseSize.x * objectScale.x, baseSize.y * objectScale.y) * 0.005f;
ImVec2 center;
ImVec2 rightPoint;
ImVec2 upPoint;
if (!ProjectWorldToOverlayPoint(obj.position, view, proj, overlayPos, overlaySize, center) ||
!ProjectWorldToOverlayPoint(obj.position + cameraRight * worldHalfExtents.x, view, proj, overlayPos, overlaySize, rightPoint) ||
!ProjectWorldToOverlayPoint(obj.position + cameraUp * worldHalfExtents.y, view, proj, overlayPos, overlaySize, upPoint)) {
return false;
}
float halfWidth = std::max(1.0f, std::abs(rightPoint.x - center.x));
float halfHeight = std::max(1.0f, std::abs(upPoint.y - center.y));
outMin = ImVec2(center.x - halfWidth, center.y - halfHeight);
outMax = ImVec2(center.x + halfWidth, center.y + halfHeight);
return true;
}
}
#pragma region Gizmo Toolbar
namespace GizmoToolbar {
enum class Icon {
@@ -1219,6 +1272,18 @@ void Engine::renderGameViewportWindow() {
if (useWorldUi) {
uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y);
}
Camera projectedUiCamera = playerCam ? makeCameraFromObject(*playerCam) : Camera{};
glm::mat4 projectedUiView(1.0f);
glm::mat4 projectedUiProj(1.0f);
bool hasProjectedUiCamera = false;
if (playerCam && !playerCam->camera.use2D) {
projectedUiView = projectedUiCamera.getViewMatrix();
projectedUiProj = glm::perspective(glm::radians(playerCam->camera.fov),
std::max(0.1f, overlaySize.x / std::max(1.0f, overlaySize.y)),
playerCam->camera.nearClip,
playerCam->camera.farClip);
hasProjectedUiCamera = true;
}
auto worldToScreen = [&](const glm::vec2& world) {
glm::vec2 local = uiWorldCamera.WorldToScreen(world);
return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y);
@@ -1247,6 +1312,9 @@ void Engine::renderGameViewportWindow() {
return uiWorldCamera.position * (1.0f - factor);
};
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
if (obj.type == ObjectType::Sprite25D && hasProjectedUiCamera) {
return ResolveProjectedSprite25DRect(obj, projectedUiView, projectedUiProj, overlayPos, overlaySize, outMin, outMax);
}
glm::vec2 parentOffset = getWorldParentOffset(obj);
glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj);
glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y);
@@ -1257,6 +1325,7 @@ void Engine::renderGameViewportWindow() {
ImVec2 s1 = worldToScreen(worldMax);
outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y));
outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y));
return true;
};
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
@@ -1408,8 +1477,8 @@ void Engine::renderGameViewportWindow() {
for (SceneObject* objPtr : uiDrawList) {
SceneObject& obj = *objPtr;
ImVec2 rectMin, rectMax;
if (useWorldUi) {
resolveUIRectWorld(obj, rectMin, rectMax);
if (useWorldUi || obj.type == ObjectType::Sprite25D) {
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
} else {
resolveUIRect(obj, rectMin, rectMax);
}
@@ -1710,11 +1779,13 @@ void Engine::renderGameViewportWindow() {
if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
ImVec2 rectMin, rectMax;
ImVec2 parentMin, parentMax;
if (useWorldUi) {
resolveUIRectWorld(*selected, rectMin, rectMax);
bool haveRect = true;
if (useWorldUi || selected->type == ObjectType::Sprite25D) {
haveRect = resolveUIRectWorld(*selected, rectMin, rectMax);
} else {
resolveUIRect(*selected, rectMin, rectMax, &parentMin, &parentMax);
}
if (haveRect) {
ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE;
@@ -1800,6 +1871,7 @@ void Engine::renderGameViewportWindow() {
projectManager.currentProject.hasUnsavedChanges = true;
gizmoUsed = true;
}
}
}
}
@@ -1993,6 +2065,7 @@ struct DockDrawerState {
bool collapsed = false;
float openAmount = 1.0f;
float expandedExtent = 0.0f;
ImGuiID pendingTabFocusId = 0;
};
void addRotatedText90CW(ImDrawList* drawList,
@@ -2134,22 +2207,84 @@ DockTabInteractionState queryDockTabInteraction(const DockDrawerTarget& target,
return out;
}
void queueDrawerTabFocus(DockDrawerState& state, ImGuiTabBar* tabBar, ImGuiID tabId) {
if (!tabBar || tabId == 0) return;
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
ImGuiTabItem* tab = &tabBar->Tabs[i];
if (tab->ID != tabId) continue;
ImGui::TabBarQueueFocus(tabBar, tab);
state.pendingTabFocusId = tabId;
return;
}
}
void applyPendingDrawerTabFocus(DockDrawerState& state, ImGuiTabBar* tabBar) {
if (!tabBar || state.pendingTabFocusId == 0) return;
bool found = false;
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
ImGuiTabItem* tab = &tabBar->Tabs[i];
if (tab->ID != state.pendingTabFocusId) continue;
found = true;
if (tabBar->SelectedTabId == tab->ID || tabBar->VisibleTabId == tab->ID) {
state.pendingTabFocusId = 0;
return;
}
ImGui::TabBarQueueFocus(tabBar, tab);
break;
}
if (!found) {
state.pendingTabFocusId = 0;
}
}
void renderCollapsedSideDockRail(DockDrawerState& state,
const DockDrawerTarget& target,
DockDrawerSide side,
float railWidth) {
float railWidth,
float revealAmount) {
if (side == DockDrawerSide::Bottom) return;
if (!target.drawerBranch || !target.splitParent) return;
ImGuiTabBar* tabBar = target.drawerBranch->TabBar;
if (!tabBar || tabBar->Tabs.Size <= 0) return;
const float reveal = std::clamp(revealAmount, 0.0f, 1.0f);
if (reveal <= 0.001f) return;
ImVec2 railPos = target.drawerBranch->Pos;
const float splitMinX = target.splitParent->Pos.x;
const float splitMaxX = target.splitParent->Pos.x + target.splitParent->Size.x;
const float splitWidth = ImMax(1.0f, splitMaxX - splitMinX);
const float fullRailWidth = std::clamp(ImMax(railWidth, 22.0f), 8.0f, splitWidth);
const float visibleRailWidth = ImMax(1.0f, fullRailWidth * reveal);
const float branchMinX = target.drawerBranch->Pos.x;
const float branchMaxX = target.drawerBranch->Pos.x + target.drawerBranch->Size.x;
const float branchMinY = target.drawerBranch->Pos.y;
const float branchMaxY = target.drawerBranch->Pos.y + target.drawerBranch->Size.y;
ImVec2 railPos(branchMinX, branchMinY);
ImVec2 railSize = target.drawerBranch->Size;
const float desiredRailWidth = ImMax(railWidth, 22.0f);
railSize.x = ImMin(desiredRailWidth, railSize.x);
if (side == DockDrawerSide::Right) {
railPos.x = target.drawerBranch->Pos.x + target.drawerBranch->Size.x - railSize.x;
railSize.x = visibleRailWidth;
const bool hasValidBarRect = tabBar->BarRect.GetWidth() > 1.0f && tabBar->BarRect.GetHeight() > 1.0f;
const float hingeX = [&]() {
if (side == DockDrawerSide::Left) {
const float preferred = hasValidBarRect ? tabBar->BarRect.Max.x : branchMaxX;
return std::clamp(preferred, splitMinX, splitMaxX);
}
const float preferred = hasValidBarRect ? tabBar->BarRect.Min.x : branchMinX;
return std::clamp(preferred, splitMinX, splitMaxX);
}();
railPos.x = (side == DockDrawerSide::Left) ? (hingeX - visibleRailWidth) : hingeX;
railPos.x = std::clamp(railPos.x, splitMinX, splitMaxX - railSize.x);
float railTopY = branchMinY;
if (hasValidBarRect) {
railTopY = ImMax(railTopY, tabBar->BarRect.Max.y - 1.0f);
}
railPos.y = railTopY;
railSize.y = ImMax(1.0f, branchMaxY - railTopY);
char railWindowName[64];
std::snprintf(railWindowName, sizeof(railWindowName), "##DockRail_%c_%08X",
@@ -2169,62 +2304,92 @@ void renderCollapsedSideDockRail(DockDrawerState& state,
if (target.drawerBranch->HostWindow) {
ImGui::SetNextWindowViewport(target.drawerBranch->HostWindow->ViewportId);
}
ImGui::SetNextWindowBgAlpha(0.94f);
ImGui::SetNextWindowBgAlpha(0.94f * reveal);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
if (ImGui::Begin(railWindowName, nullptr, railFlags)) {
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(1.0f, 1.0f));
ImDrawList* draw = ImGui::GetWindowDrawList();
const ImGuiStyle& style = ImGui::GetStyle();
const ImVec2 avail = ImGui::GetContentRegionAvail();
const int tabCount = tabBar->Tabs.Size;
const float slotSpacing = style.ItemSpacing.y;
const float minSlotHeight = ImGui::GetFrameHeight() + 8.0f;
const float preferredSlotHeight = ImGui::GetFrameHeight() + 55.0f;
const float availableSlotHeight = (tabCount > 0)
? ImMax(minSlotHeight, (avail.y - slotSpacing * (tabCount - 1)) / static_cast<float>(tabCount))
: preferredSlotHeight;
const float slotHeight = std::clamp(preferredSlotHeight, minSlotHeight, availableSlotHeight);
const float slotWidth = ImMax(12.0f, avail.x);
const float usedHeight = tabCount * slotHeight + ImMax(0, tabCount - 1) * slotSpacing;
const float topPad = ImMax(0.0f, (avail.y - usedHeight) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + topPad);
const float slotSpacing = ImMax(1.0f, style.ItemInnerSpacing.y);
const ImVec2 railMin = ImGui::GetWindowPos();
const ImVec2 railMax(railMin.x + ImGui::GetWindowSize().x, railMin.y + ImGui::GetWindowSize().y);
const ImRect railRect(railMin, railMax);
draw->AddRectFilled(railRect.Min, railRect.Max, ImGui::GetColorU32(ImGuiCol_Tab));
draw->AddRect(railRect.Min, railRect.Max, ImGui::GetColorU32(ImGuiCol_Border));
const bool tabBarFocused = (tabBar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
const float minSlotHeight = ImGui::GetFrameHeight();
const float maxSlotHeight = ImGui::GetFrameHeight() * 3.2f;
const float slotWidth = ImMax(3.0f, ImGui::GetContentRegionAvail().x);
float cursorY = ImGui::GetCursorPosY() + style.FramePadding.y;
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
ImGuiTabItem* tab = &tabBar->Tabs[i];
const char* tabName = ImGui::TabBarGetTabName(tabBar, tab);
const bool selected = (tabBar->SelectedTabId == tab->ID) || (tabBar->VisibleTabId == tab->ID);
const ImVec2 labelSize = ImGui::CalcTextSize(tabName);
const float slotHeight = std::clamp(labelSize.x + style.FramePadding.x * 2.0f, minSlotHeight, maxSlotHeight);
ImGui::PushID(static_cast<int>(tab->ID));
ImGui::SetCursorPosY(cursorY);
ImVec2 slotPos = ImGui::GetCursorScreenPos();
ImVec2 slotSize(slotWidth, slotHeight);
if (ImGui::InvisibleButton("##SideTab", slotSize)) {
ImGui::TabBarQueueFocus(tabBar, tab);
queueDrawerTabFocus(state, tabBar, tab->ID);
state.collapsed = false;
}
const bool hovered = ImGui::IsItemHovered();
if (hovered) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
const ImRect slotRect(slotPos, ImVec2(slotPos.x + slotSize.x, slotPos.y + slotSize.y));
ImU32 bg = 0;
if (selected) bg = ImGui::GetColorU32(ImGuiCol_TabActive);
else if (hovered) bg = ImGui::GetColorU32(ImGuiCol_TabHovered);
else bg = ImGui::GetColorU32(ImGuiCol_Tab);
draw->AddRectFilled(slotRect.Min, slotRect.Max, bg, style.TabRounding);
draw->AddRect(slotRect.Min, slotRect.Max, ImGui::GetColorU32(ImGuiCol_Border), style.TabRounding);
const ImU32 textCol = ImGui::GetColorU32(ImGuiCol_Text);
const ImU32 bg = selected
? ImGui::GetColorU32(tabBarFocused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected)
: (hovered ? ImGui::GetColorU32(ImGuiCol_TabHovered)
: ImGui::GetColorU32(tabBarFocused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
const ImU32 border = ImGui::GetColorU32(ImGuiCol_Border);
const ImU32 overline = ImGui::GetColorU32(
tabBarFocused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
ImDrawFlags roundFlags = (side == DockDrawerSide::Left)
? (ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomRight)
: (ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBottomLeft);
draw->AddRectFilled(slotRect.Min, slotRect.Max, bg, style.TabRounding, roundFlags);
draw->AddRect(slotRect.Min, slotRect.Max, border, style.TabRounding, roundFlags);
if (selected) {
const float overlineThickness = ImMax(1.0f, style.TabBarOverlineSize);
if (side == DockDrawerSide::Left) {
draw->AddRectFilled(ImVec2(slotRect.Max.x - overlineThickness, slotRect.Min.y + 1.0f),
ImVec2(slotRect.Max.x, slotRect.Max.y - 1.0f),
overline);
} else {
draw->AddRectFilled(ImVec2(slotRect.Min.x, slotRect.Min.y + 1.0f),
ImVec2(slotRect.Min.x + overlineThickness, slotRect.Max.y - 1.0f),
overline);
}
}
const ImU32 textCol = ImGui::GetColorU32((selected || hovered) ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const float baseFontSize = ImGui::GetFontSize();
const float minRailFont = baseFontSize * 0.75f;
const float maxRailFont = baseFontSize * 1.35f;
float railFontSize = std::clamp(baseFontSize * 1.15f, minRailFont, maxRailFont);
const float minRailFont = baseFontSize * 0.70f;
const float maxRailFont = baseFontSize * 1.05f;
float railFontSize = std::clamp(baseFontSize * 0.95f, minRailFont, maxRailFont);
const ImVec2 unscaledText = ImGui::GetFont()->CalcTextSizeA(railFontSize, FLT_MAX, 0.0f, tabName);
if (unscaledText.x > 1.0f) {
const float fitScale = (slotRect.GetHeight() - 6.0f) / unscaledText.x;
if (unscaledText.x > 0.5f && unscaledText.y > 0.5f) {
const float fitToWidth = (slotRect.GetWidth() - 4.0f) / unscaledText.y;
const float fitToHeight = (slotRect.GetHeight() - 6.0f) / unscaledText.x;
const float fitScale = ImMin(fitToWidth, fitToHeight);
railFontSize = std::clamp(railFontSize * fitScale, minRailFont, maxRailFont);
}
addRotatedText90CW(draw, ImGui::GetFont(), railFontSize, slotRect, textCol, tabName);
if (hovered) {
ImGui::SetItemTooltip("%s", tabName);
}
ImGui::PopID();
cursorY += slotHeight + slotSpacing;
}
ImGui::PopStyleVar();
}
@@ -2289,6 +2454,7 @@ void updateDockDrawerAnimation(DockDrawerState& state,
state.collapsed = false;
state.openAmount = 1.0f;
state.expandedExtent = 0.0f;
state.pendingTabFocusId = 0;
return;
}
@@ -2299,9 +2465,11 @@ void updateDockDrawerAnimation(DockDrawerState& state,
state.expandedExtent =
(side == DockDrawerSide::Bottom) ? ImMax(0.0f, target.drawerBranch->Size.y)
: ImMax(0.0f, target.drawerBranch->Size.x);
state.pendingTabFocusId = 0;
}
ImGuiTabBar* tabBar = target.drawerBranch->TabBar;
applyPendingDrawerTabFocus(state, tabBar);
constexpr float kCollapsedSideRailWidth = 25.0f;
float collapsedExtent = (side == DockDrawerSide::Bottom)
? (ImGui::GetFrameHeight() + 8.0f)
@@ -2382,9 +2550,10 @@ void updateDockDrawerAnimation(DockDrawerState& state,
}
if (side != DockDrawerSide::Bottom) {
const bool useSideRail = state.openAmount <= 0.02f;
const float railReveal = std::clamp(1.0f - state.openAmount, 0.0f, 1.0f);
const bool useSideRail = railReveal > 0.01f;
ImGuiDockNodeFlags desiredFlags = target.drawerBranch->LocalFlags;
if (useSideRail) {
if (state.collapsed) {
desiredFlags |= ImGuiDockNodeFlags_HiddenTabBar;
} else {
desiredFlags &= ~ImGuiDockNodeFlags_HiddenTabBar;
@@ -2393,7 +2562,7 @@ void updateDockDrawerAnimation(DockDrawerState& state,
target.drawerBranch->SetLocalFlags(desiredFlags);
}
if (useSideRail) {
renderCollapsedSideDockRail(state, target, side, collapsedExtent);
renderCollapsedSideDockRail(state, target, side, collapsedExtent, railReveal);
}
}
}
@@ -2598,6 +2767,7 @@ void Engine::renderMainMenuBar() {
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
if (ImGui::MenuItem("2.5D Object")) addObject(ObjectType::Sprite25D, "2.5D Object");
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera");
if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
@@ -3321,6 +3491,68 @@ void Engine::renderViewport() {
blockSelection = true;
}
auto drawProjected25DSceneSprites = [&]() {
auto brightenTint = [](const ImVec4& c, float k) {
return ImVec4(std::clamp(c.x * k, 0.0f, 1.0f),
std::clamp(c.y * k, 0.0f, 1.0f),
std::clamp(c.z * k, 0.0f, 1.0f),
c.w);
};
for (auto& obj : sceneObjects) {
if (!obj.enabled || obj.type != ObjectType::Sprite25D || !obj.hasUI || obj.ui.type != UIElementType::Sprite2D) {
continue;
}
ImVec2 rectMin, rectMax;
if (!ResolveProjectedSprite25DRect(obj, view, proj, imageMin, ImVec2(imageMax.x - imageMin.x, imageMax.y - imageMin.y), rectMin, rectMax)) {
continue;
}
ImVec2 drawSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
if (drawSize.x <= 1.0f || drawSize.y <= 1.0f) continue;
unsigned int texId = 0;
if (rendererInitialized && !obj.albedoTexturePath.empty()) {
if (auto* tex = renderer.getTexture(obj.albedoTexturePath)) {
texId = tex->GetID();
}
}
std::array<ImVec2, 4> uvQuad = buildSpriteSheetUvs(obj);
ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a);
float angle = glm::radians(obj.ui.rotation);
if (std::abs(angle) > 1e-4f) {
ImVec2 center((rectMin.x + rectMax.x) * 0.5f, (rectMin.y + rectMax.y) * 0.5f);
ImVec2 half(drawSize.x * 0.5f, drawSize.y * 0.5f);
float c = std::cos(angle);
float s = std::sin(angle);
auto rotPt = [&](float x, float y) {
return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c);
};
ImVec2 p0 = rotPt(-half.x, -half.y);
ImVec2 p1 = rotPt( half.x, -half.y);
ImVec2 p2 = rotPt( half.x, half.y);
ImVec2 p3 = rotPt(-half.x, half.y);
if (texId != 0) {
viewportDrawList->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3,
uvQuad[0], uvQuad[1], uvQuad[2], uvQuad[3],
ImGui::GetColorU32(tint));
} else {
ImU32 fill = ImGui::GetColorU32(tint);
ImU32 border = ImGui::GetColorU32(brightenTint(tint, 0.85f));
viewportDrawList->AddQuadFilled(p0, p1, p2, p3, fill);
viewportDrawList->AddQuad(p0, p1, p2, p3, border, 1.5f);
}
} else if (texId != 0) {
viewportDrawList->AddImage((ImTextureID)(intptr_t)texId, rectMin, rectMax, uvQuad[0], uvQuad[2], ImGui::GetColorU32(tint));
} else {
ImU32 fill = ImGui::GetColorU32(tint);
ImU32 border = ImGui::GetColorU32(brightenTint(tint, 0.85f));
viewportDrawList->AddRectFilled(rectMin, rectMax, fill, 4.0f);
viewportDrawList->AddRect(rectMin, rectMax, border, 4.0f, 0, 1.5f);
}
}
};
drawProjected25DSceneSprites();
bool uiWorldCameraActive = false;
if (worldUiEditing) {
auto find3DCanvasId = [&](const SceneObject& target) -> int {
@@ -3345,6 +3577,8 @@ void Engine::renderViewport() {
editCanvas3DId = find3DCanvasId(*selected);
}
auto isUIType = [&](const SceneObject& target) {
if (target.type == ObjectType::Sprite25D) return true;
if (!worldUiEditing) return false;
if (!target.hasUI || target.ui.type == UIElementType::None) return false;
int canvasId = find3DCanvasId(target);
return (canvasId < 0) || (canvasId == editCanvas3DId);
@@ -3403,6 +3637,9 @@ void Engine::renderViewport() {
return uiWorldCamera.position * (1.0f - factor);
};
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
if (obj.type == ObjectType::Sprite25D) {
return ResolveProjectedSprite25DRect(obj, view, proj, overlayPos, overlaySize, outMin, outMax);
}
glm::vec2 parentOffset = getWorldParentOffset(obj);
glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj);
glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y);
@@ -3420,6 +3657,7 @@ void Engine::renderViewport() {
ImVec2 s1 = worldToScreen(worldMax);
outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y));
outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y));
return true;
};
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
@@ -3565,7 +3803,7 @@ void Engine::renderViewport() {
for (SceneObject* objPtr : uiDrawList) {
SceneObject& obj = *objPtr;
ImVec2 rectMin, rectMax;
resolveUIRectWorld(obj, rectMin, rectMax);
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
if (rectSize.x <= 1.0f || rectSize.y <= 1.0f) continue;
if (rectOutsideOverlay(rectMin, rectMax)) continue;
@@ -3871,7 +4109,7 @@ void Engine::renderViewport() {
}
bool gizmoUsed = false;
if (uiWorldHover && !uiWorldCameraActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
if (worldUiEditing && uiWorldHover && !uiWorldCameraActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGuizmo::IsUsing() && !ImGuizmo::IsOver()) {
ImVec2 mouse = ImGui::GetIO().MousePos;
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
@@ -3880,7 +4118,7 @@ void Engine::renderViewport() {
const SceneObject& obj = *it;
if (!obj.enabled || !isUIType(obj) || obj.ui.type == UIElementType::Canvas) continue;
ImVec2 rectMin, rectMax;
resolveUIRectWorld(obj, rectMin, rectMax);
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
if (mouse.x >= rectMin.x && mouse.x <= rectMax.x &&
mouse.y >= rectMin.y && mouse.y <= rectMax.y) {
hitId = obj.id;
@@ -3896,9 +4134,9 @@ void Engine::renderViewport() {
}
SceneObject* selected = getSelectedObject();
if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
if (worldUiEditing && selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
ImVec2 rectMin, rectMax;
resolveUIRectWorld(*selected, rectMin, rectMax);
if (resolveUIRectWorld(*selected, rectMin, rectMax)) {
ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
if (rectSize.x > 1.0f && rectSize.y > 1.0f) {
ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE;
@@ -3979,12 +4217,13 @@ void Engine::renderViewport() {
gizmoUsed = true;
}
}
}
}
ImGui::EndChild();
ImGui::PopStyleVar();
if (ImGui::IsAnyItemActive() || uiWorldCameraActive || gizmoUsed) {
if ((worldUiEditing && ImGui::IsAnyItemActive()) || uiWorldCameraActive || gizmoUsed) {
blockSelection = true;
}
}
@@ -4963,6 +5202,7 @@ void Engine::renderViewport() {
break;
case ObjectType::Mirror:
case ObjectType::Sprite:
case ObjectType::Sprite25D:
gizmoBoundsMin = glm::vec3(-0.5f, -0.5f, -0.02f);
gizmoBoundsMax = glm::vec3(0.5f, 0.5f, 0.02f);
break;
@@ -5468,7 +5708,7 @@ void Engine::renderViewport() {
case UIElementType::Slider: return ObjectType::UISlider;
case UIElementType::Button: return ObjectType::UIButton;
case UIElementType::Text: return ObjectType::UIText;
case UIElementType::Sprite2D: return ObjectType::Sprite2D;
case UIElementType::Sprite2D: return obj.type == ObjectType::Sprite25D ? ObjectType::Sprite25D : ObjectType::Sprite2D;
case UIElementType::None: break;
}
}
@@ -6104,6 +6344,9 @@ void Engine::renderViewport() {
auto ray = makeRay(mousePos);
float closest = FLT_MAX;
int hitId = -1;
glm::mat4 invView = glm::inverse(view);
glm::vec3 cameraPos = glm::vec3(invView[3]);
glm::vec3 cameraUp = glm::normalize(glm::vec3(invView[1]));
for (const auto& obj : sceneObjects) {
glm::vec3 aabbMin(-0.5f);
@@ -6111,10 +6354,26 @@ void Engine::renderViewport() {
glm::mat4 model(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
if (obj.type == ObjectType::Sprite25D || (obj.renderType == RenderType::Sprite && obj.faceCamera)) {
glm::vec3 forward = cameraPos - obj.position;
if (glm::dot(forward, forward) < 1e-6f) forward = glm::vec3(0.0f, 0.0f, 1.0f);
else forward = glm::normalize(forward);
glm::vec3 right = glm::cross(cameraUp, forward);
if (glm::dot(right, right) < 1e-6f) {
right = glm::cross(glm::vec3(0.0f, 0.0f, 1.0f), forward);
}
right = glm::normalize(right);
glm::vec3 up = glm::normalize(glm::cross(forward, right));
glm::vec3 scale = glm::max(glm::abs(obj.scale), glm::vec3(0.0001f));
model[0] = glm::vec4(right * scale.x * (obj.scale.x < 0.0f ? -1.0f : 1.0f), 0.0f);
model[1] = glm::vec4(up * scale.y * (obj.scale.y < 0.0f ? -1.0f : 1.0f), 0.0f);
model[2] = glm::vec4(forward * scale.z * (obj.scale.z < 0.0f ? -1.0f : 1.0f), 0.0f);
} else {
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
}
glm::mat4 invModel = glm::inverse(model);
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(ray.first, 1.0f));
@@ -6139,6 +6398,7 @@ void Engine::renderViewport() {
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT);
break;
case ObjectType::Sprite:
case ObjectType::Sprite25D:
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT);
break;
case ObjectType::Torus:
@@ -7190,6 +7450,12 @@ void Engine::renderPlayerViewport() {
if (useWorldUi) {
uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y);
}
glm::mat4 projectedUiView = camera.getViewMatrix();
glm::mat4 projectedUiProj = glm::perspective(glm::radians(runtimeFov),
std::max(0.1f, overlaySize.x / std::max(1.0f, overlaySize.y)),
runtimeNear,
runtimeFar);
bool hasProjectedUiCamera = true;
auto worldToScreen = [&](const glm::vec2& world) {
glm::vec2 local = uiWorldCamera.WorldToScreen(world);
return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y);
@@ -7213,6 +7479,9 @@ void Engine::renderPlayerViewport() {
return offset;
};
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
if (obj.type == ObjectType::Sprite25D && hasProjectedUiCamera) {
return ResolveProjectedSprite25DRect(obj, projectedUiView, projectedUiProj, overlayPos, overlaySize, outMin, outMax);
}
glm::vec2 parentOffset = getWorldParentOffset(obj);
glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y);
glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y);
@@ -7223,6 +7492,7 @@ void Engine::renderPlayerViewport() {
ImVec2 s1 = worldToScreen(worldMax);
outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y));
outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y));
return true;
};
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
@@ -7350,8 +7620,8 @@ void Engine::renderPlayerViewport() {
for (auto& obj : sceneObjects) {
if (!obj.enabled || !isUIType(obj)) continue;
ImVec2 rectMin, rectMax;
if (useWorldUi) {
resolveUIRectWorld(obj, rectMin, rectMax);
if (useWorldUi || obj.type == ObjectType::Sprite25D) {
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
} else {
resolveUIRect(obj, rectMin, rectMax);
}

View File

@@ -256,6 +256,13 @@ void ApplyObjectPreset(SceneObject& obj, ObjectType preset) {
obj.scale = glm::vec3(1.0f, 1.0f, 0.05f);
obj.material.ambientStrength = 1.0f;
break;
case ObjectType::Sprite25D:
obj.hasUI = true;
obj.ui.type = UIElementType::Sprite2D;
obj.ui.label = "2.5D Object";
obj.ui.size = glm::vec2(128.0f, 128.0f);
obj.scale = glm::vec3(1.0f);
break;
case ObjectType::DirectionalLight:
obj.hasLight = true;
obj.light.type = LightType::Directional;
@@ -367,7 +374,7 @@ void Engine::applyProjectPipelineDefaults(bool force) {
}
int Engine::resolveSpriteSheetFrame(const SceneObject& obj) const {
if (!obj.hasUI || !obj.ui.spriteSheetEnabled) {
if (!obj.ui.spriteSheetEnabled) {
return 0;
}
int columns = std::max(1, obj.ui.spriteSheetColumns);
@@ -384,7 +391,7 @@ std::array<ImVec2, 4> Engine::buildSpriteSheetUvs(const SceneObject& obj) const
ImVec2(1.0f, 0.0f),
ImVec2(0.0f, 0.0f)
};
if (!obj.hasUI || !obj.ui.spriteSheetEnabled) {
if (!obj.ui.spriteSheetEnabled) {
return uvs;
}
@@ -680,6 +687,107 @@ bool copyDirectoryRecursive(const fs::path& from, const fs::path& to, std::strin
return true;
}
fs::path resolveRecentProjectRoot(const std::string& recentPath) {
fs::path path(recentPath);
if (path.empty()) return {};
if (path.extension() == ".modu") {
return path.parent_path();
}
if (fs::is_directory(path)) {
return path;
}
return path.has_parent_path() ? path.parent_path() : path;
}
bool importInstalledPackagesFromRecent(const ProjectManager& projectManager,
const fs::path& targetProjectRoot,
std::string& outSourceProjectName,
std::string& outError) {
std::error_code ec;
fs::path targetCanonical = fs::weakly_canonical(targetProjectRoot, ec);
if (ec || targetCanonical.empty()) {
ec.clear();
targetCanonical = fs::absolute(targetProjectRoot, ec);
ec.clear();
}
for (const auto& rp : projectManager.recentProjects) {
fs::path sourceRoot = resolveRecentProjectRoot(rp.path);
if (sourceRoot.empty()) continue;
fs::path sourceCanonical = fs::weakly_canonical(sourceRoot, ec);
if (ec || sourceCanonical.empty()) {
ec.clear();
sourceCanonical = fs::absolute(sourceRoot, ec);
ec.clear();
}
if (!sourceCanonical.empty() && !targetCanonical.empty() && sourceCanonical == targetCanonical) {
continue;
}
fs::path sourceManifest = sourceRoot / "packages.modu";
if (!fs::exists(sourceManifest)) continue;
fs::path targetManifest = targetProjectRoot / "packages.modu";
fs::copy_file(sourceManifest, targetManifest, fs::copy_options::overwrite_existing, ec);
if (ec) {
outError = "Failed to copy package manifest from " + sourceRoot.string() + ": " + ec.message();
return false;
}
std::string copyError;
if (!copyDirectoryRecursive(sourceRoot / "Library" / "InstalledPackages",
targetProjectRoot / "Library" / "InstalledPackages",
copyError)) {
outError = copyError;
return false;
}
outSourceProjectName = rp.name.empty() ? sourceRoot.filename().string() : rp.name;
return true;
}
outError = "No recent project with a package manifest was found.";
return false;
}
bool applyTemplateProject(const fs::path& templateProjectRoot,
Project& project,
const std::string& projectName,
ProjectPipeline pipeline,
Modularity::GraphicsBackend rendererBackend,
std::string& outError) {
if (templateProjectRoot.empty()) {
return true;
}
fs::path templateProjectFile = templateProjectRoot / "project.modu";
if (!fs::exists(templateProjectFile)) {
outError = "Template is missing project.modu: " + templateProjectRoot.string();
return false;
}
std::string copyError;
if (!copyDirectoryRecursive(templateProjectRoot, project.projectPath, copyError)) {
outError = "Failed to copy template project: " + copyError;
return false;
}
if (!project.load(project.projectPath / "project.modu")) {
outError = "Failed to load copied template project.";
return false;
}
project.name = projectName;
project.pipeline = pipeline;
project.rendererBackend = rendererBackend;
if (project.currentSceneName.empty()) {
project.currentSceneName = "Main";
}
project.saveProjectFile();
return true;
}
bool copyPrecompiledPackages(const fs::path& buildRoot, const fs::path& outDir, std::string& error) {
std::error_code ec;
if (!fs::exists(buildRoot)) return true;
@@ -930,6 +1038,15 @@ void cleanExportOutput(const fs::path& exportRoot, const char* exeBaseName, std:
}
}
fs::path templatesDir = exportRoot / "Template-Projects";
if (fs::exists(templatesDir)) {
fs::remove_all(templatesDir, ec);
if (ec) {
error = "Failed to remove existing template projects.";
return;
}
}
fs::path autostart = exportRoot / "autostart.modu";
if (fs::exists(autostart)) {
fs::remove(autostart, ec);
@@ -1709,6 +1826,7 @@ void Engine::run() {
std::cerr << "[DEBUG] First frame: UI rendering complete, finalizing frame..." << std::endl;
}
updateTouchSwipeScrolling();
autosaveWorkspaceLayout();
renderUiCanvas3DTargets();
ImGui::Render();
@@ -4561,6 +4679,10 @@ void Engine::startExportBuild(const fs::path& outputDir, bool runAfter) {
result.message = copyError;
return result;
}
if (!copyDirectoryRecursive(sourceRoot / "Template-Projects", exportRoot / "Template-Projects", copyError)) {
result.message = copyError;
return result;
}
setStatus(0.78f, "Collecting precompiled packages...");
if (!copyPrecompiledPackages(buildRoot, exportRoot / "Packages" / "ThirdParty", copyError)) {
@@ -4985,6 +5107,34 @@ void Engine::createNewProject(const char* name, const char* location) {
}
#endif
if (newProject.create()) {
if (!projectManager.newProjectTemplatePath.empty()) {
std::string templateError;
if (!applyTemplateProject(fs::path(projectManager.newProjectTemplatePath),
newProject,
name,
newProject.pipeline,
newProject.rendererBackend,
templateError)) {
projectManager.errorMessage = templateError;
return;
}
}
bool importedPackages = false;
std::string importedFromProject;
if (projectManager.newProjectImportLastPackages) {
std::string importError;
if (importInstalledPackagesFromRecent(projectManager,
newProject.projectPath,
importedFromProject,
importError)) {
importedPackages = true;
} else if (!importError.empty()) {
addConsoleMessage("Installed package import skipped: " + importError,
ConsoleMessageType::Warning);
}
}
projectManager.currentProject = newProject;
projectManager.addToRecentProjects(name,
(newProject.projectPath / "project.modu").string());
@@ -5033,6 +5183,9 @@ void Engine::createNewProject(const char* name, const char* location) {
addConsoleMessage("Created new project: " + std::string(name), ConsoleMessageType::Success);
addConsoleMessage("Project location: " + newProject.projectPath.string(), ConsoleMessageType::Info);
addConsoleMessage("Pipeline: " + std::string(isProject2DPipeline() ? "2D" : "3D"), ConsoleMessageType::Info);
if (importedPackages) {
addConsoleMessage("Imported installed packages from: " + importedFromProject, ConsoleMessageType::Info);
}
saveCurrentScene();
loadBuildSettings();
@@ -6175,6 +6328,9 @@ void Engine::setupImGui() {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
#if defined(__ANDROID__)
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
#endif
if (usingVulkan()) {
io.ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable;
} else {

View File

@@ -3,10 +3,36 @@
#include "ModelLoader.h"
#include <cmath>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <unordered_map>
ObjectType GetLegacyTypeFromComponents(const SceneObject& obj);
namespace {
std::string TrimCopy(const std::string& value) {
const size_t first = value.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return "";
const size_t last = value.find_last_not_of(" \t\r\n");
return value.substr(first, last - first + 1);
}
std::string GetPlatformDefaultProjectsPath() {
#ifdef _WIN32
const char* userProfile = std::getenv("USERPROFILE");
if (userProfile && *userProfile) {
return (fs::path(userProfile) / "Documents" / "ModularityProjects").string();
}
#else
const char* home = std::getenv("HOME");
if (home && *home) {
return (fs::path(home) / "ModularityProjects").string();
}
#endif
return (fs::current_path() / "Projects").string();
}
} // namespace
// Project implementation
Project::Project(const std::string& projectName, const fs::path& basePath)
: name(projectName) {
@@ -192,9 +218,9 @@ ProjectManager::ProjectManager() {
fs::create_directories(appDataPath);
loadRecentProjects();
loadLauncherSettings();
std::string defaultPath = (fs::current_path() / "Projects").string();
strncpy(newProjectLocation, defaultPath.c_str(), sizeof(newProjectLocation) - 1);
std::snprintf(newProjectLocation, sizeof(newProjectLocation), "%s", defaultProjectLocation);
}
void ProjectManager::loadRecentProjects() {
@@ -269,6 +295,45 @@ void ProjectManager::saveRecentProjects() {
file.close();
}
void ProjectManager::loadLauncherSettings() {
defaultProjectLocation[0] = '\0';
fs::path settingsFile = appDataPath / "launcher_settings.modu";
if (fs::exists(settingsFile)) {
std::ifstream file(settingsFile);
std::string line;
while (std::getline(file, line)) {
const std::string cleaned = TrimCopy(line);
if (cleaned.empty() || cleaned[0] == '#') continue;
const size_t eq = cleaned.find('=');
if (eq == std::string::npos) continue;
const std::string key = TrimCopy(cleaned.substr(0, eq));
const std::string value = TrimCopy(cleaned.substr(eq + 1));
if (key == "defaultProjectLocation" && !value.empty()) {
std::snprintf(defaultProjectLocation, sizeof(defaultProjectLocation), "%s", value.c_str());
}
}
}
if (defaultProjectLocation[0] == '\0') {
const std::string fallback = GetPlatformDefaultProjectsPath();
std::snprintf(defaultProjectLocation, sizeof(defaultProjectLocation), "%s", fallback.c_str());
}
}
void ProjectManager::saveLauncherSettings() const {
fs::path settingsFile = appDataPath / "launcher_settings.modu";
std::ofstream file(settingsFile);
if (!file.is_open()) {
return;
}
file << "# Modularity launcher settings\n";
file << "defaultProjectLocation=" << defaultProjectLocation << "\n";
}
void ProjectManager::addToRecentProjects(const std::string& name, const std::string& path) {
std::string absolutePath = path;
try {
@@ -339,6 +404,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "tag=" << obj.tag << "\n";
file << "hasRenderer=" << (obj.hasRenderer ? 1 : 0) << "\n";
file << "renderType=" << static_cast<int>(obj.renderType) << "\n";
file << "faceCamera=" << (obj.faceCamera ? 1 : 0) << "\n";
file << "hasLight=" << (obj.hasLight ? 1 : 0) << "\n";
file << "hasCamera=" << (obj.hasCamera ? 1 : 0) << "\n";
file << "hasPostFX=" << (obj.hasPostFX ? 1 : 0) << "\n";
@@ -784,6 +850,10 @@ void ApplyLegacyTypePreset(SceneObject& obj, ObjectType legacyType) {
obj.hasRenderer = true;
obj.renderType = RenderType::Sprite;
break;
case ObjectType::Sprite25D:
obj.hasUI = true;
obj.ui.type = UIElementType::Sprite2D;
break;
case ObjectType::DirectionalLight:
obj.hasLight = true;
obj.light.type = LightType::Directional;
@@ -856,6 +926,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
obj.hasRenderer = true;
}
}},
{"faceCamera", +[](SceneObject& obj, const std::string& value) { obj.faceCamera = std::stoi(value) != 0; }},
{"hasLight", +[](SceneObject& obj, const std::string& value) { obj.hasLight = std::stoi(value) != 0; }},
{"hasCamera", +[](SceneObject& obj, const std::string& value) { obj.hasCamera = std::stoi(value) != 0; }},
{"hasPostFX", +[](SceneObject& obj, const std::string& value) { obj.hasPostFX = std::stoi(value) != 0; }},
@@ -1188,6 +1259,9 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
} // namespace
ObjectType GetLegacyTypeFromComponents(const SceneObject& obj) {
if (obj.type == ObjectType::Sprite25D) {
return ObjectType::Sprite25D;
}
if (obj.hasRenderer) {
switch (obj.renderType) {
case RenderType::Cube: return ObjectType::Cube;
@@ -1209,7 +1283,7 @@ ObjectType GetLegacyTypeFromComponents(const SceneObject& obj) {
case UIElementType::Slider: return ObjectType::UISlider;
case UIElementType::Button: return ObjectType::UIButton;
case UIElementType::Text: return ObjectType::UIText;
case UIElementType::Sprite2D: return ObjectType::Sprite2D;
case UIElementType::Sprite2D: return obj.type == ObjectType::Sprite25D ? ObjectType::Sprite25D : ObjectType::Sprite2D;
case UIElementType::None: break;
}
}

View File

@@ -45,11 +45,15 @@ public:
fs::path appDataPath;
char newProjectName[128] = "";
char newProjectLocation[512] = "";
char defaultProjectLocation[512] = "";
char openProjectPath[512] = "";
bool showNewProjectDialog = false;
bool showOpenProjectDialog = false;
int newProjectPipelineMode = 0;
int newProjectRendererMode = 0;
bool newProjectImportLastPackages = true;
std::string newProjectTemplatePath;
std::string newProjectTemplateName;
std::string errorMessage;
Project currentProject;
@@ -57,6 +61,8 @@ public:
void loadRecentProjects();
void saveRecentProjects();
void loadLauncherSettings();
void saveLauncherSettings() const;
void addToRecentProjects(const std::string& name, const std::string& path);
bool loadProject(const std::string& path);
};

View File

@@ -8,6 +8,65 @@
#define TINYOBJLOADER_IMPLEMENTATION
#include "../include/ThirdParty/tiny_obj_loader.h"
namespace {
glm::vec4 BuildSpriteUvRect(const SceneObject& obj) {
if (!obj.ui.spriteSheetEnabled) {
return glm::vec4(0.0f, 0.0f, 1.0f, 1.0f);
}
const int columns = std::max(1, obj.ui.spriteSheetColumns);
const int rows = std::max(1, obj.ui.spriteSheetRows);
const int total = std::max(1, columns * rows);
const int frame = std::clamp(obj.ui.spriteSheetFrame, 0, total - 1);
const int col = frame % columns;
const int row = frame / columns;
const float u0 = static_cast<float>(col) / static_cast<float>(columns);
const float v0 = static_cast<float>(row) / static_cast<float>(rows);
const float uSize = 1.0f / static_cast<float>(columns);
const float vSize = 1.0f / static_cast<float>(rows);
return glm::vec4(u0, v0, uSize, vSize);
}
glm::mat4 BuildSceneObjectModelMatrix(const SceneObject& obj, const glm::vec3* cameraPosition = nullptr, const glm::vec3* cameraUp = nullptr) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
if (obj.renderType == RenderType::Sprite && obj.faceCamera && cameraPosition != nullptr) {
glm::vec3 forward = *cameraPosition - obj.position;
if (glm::dot(forward, forward) < 1e-6f) {
forward = glm::vec3(0.0f, 0.0f, 1.0f);
} else {
forward = glm::normalize(forward);
}
glm::vec3 up = (cameraUp != nullptr && glm::dot(*cameraUp, *cameraUp) > 1e-6f)
? glm::normalize(*cameraUp)
: glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 right = glm::cross(up, forward);
if (glm::dot(right, right) < 1e-6f) {
up = glm::vec3(0.0f, 0.0f, 1.0f);
right = glm::cross(up, forward);
}
right = glm::normalize(right);
up = glm::normalize(glm::cross(forward, right));
const glm::vec3 signedScale = obj.scale;
const glm::vec3 absScale = glm::max(glm::abs(signedScale), glm::vec3(0.0001f));
model[0] = glm::vec4(right * absScale.x * (signedScale.x < 0.0f ? -1.0f : 1.0f), 0.0f);
model[1] = glm::vec4(up * absScale.y * (signedScale.y < 0.0f ? -1.0f : 1.0f), 0.0f);
model[2] = glm::vec4(forward * absScale.z * (signedScale.z < 0.0f ? -1.0f : 1.0f), 0.0f);
return model;
}
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);
return model;
}
} // namespace
// Global OBJ loader instance
OBJLoader g_objLoader;
@@ -1189,12 +1248,7 @@ void Renderer::renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
}
void Renderer::renderObject(const SceneObject& obj) {
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, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
glm::mat4 model = BuildSceneObjectModelMatrix(obj);
bool hasMaterialAsset = !obj.materialPath.empty();
bool hasCustomShader = !obj.vertexShaderPath.empty() || !obj.fragmentShaderPath.empty();
@@ -1207,6 +1261,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->setVec4("uvRect", BuildSpriteUvRect(obj));
shader->setBool("unlit", obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite || missingMaterialAndShader);
Texture* baseTex = texture1;
@@ -1327,14 +1382,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
}
return f;
};
auto buildModelMatrix = [](const SceneObject& obj) {
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);
return model;
auto buildModelMatrix = [&](const SceneObject& obj) {
return BuildSceneObjectModelMatrix(obj, &camera.position, &camera.up);
};
auto selectMeshForObject = [&](const SceneObject& obj) -> Mesh* {
if (obj.renderType == RenderType::Cube) return cubeMesh;
@@ -1683,6 +1732,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
shader->setVec4("uvRect", BuildSpriteUvRect(obj));
if (obj.hasSkeletalAnimation && obj.skeletal.enabled) {
int safeLimit = std::max(0, boneLimit);
@@ -2112,12 +2162,7 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
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);
glm::mat4 model = BuildSceneObjectModelMatrix(obj, &camera.position, &camera.up);
active->setMat4("model", model);
meshToDraw->draw();
@@ -2355,12 +2400,7 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
glCullFace(GL_BACK);
}
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, drawItem.obj->position);
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, drawItem.obj->scale);
glm::mat4 model = BuildSceneObjectModelMatrix(*drawItem.obj, &camera.position, &camera.up);
maskShader->use();
maskShader->setMat4("view", view);

View File

@@ -24,7 +24,8 @@ enum class ObjectType {
UISlider = 18,
UIButton = 19,
UIText = 20,
Empty = 21
Empty = 21,
Sprite25D = 22
};
enum class RenderType {
@@ -460,6 +461,7 @@ public:
std::string tag = "Untagged";
bool hasRenderer = false;
RenderType renderType = RenderType::None;
bool faceCamera = false;
bool hasLight = false;
bool hasCamera = false;
bool hasPostFX = false;

View File

@@ -1,9 +1,12 @@
#include "ScriptCompiler.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#include <fstream>
#include <optional>
#include <sstream>
#include <regex>
#if defined(_WIN32)
@@ -32,6 +35,58 @@ namespace {
}
return value.substr(start, end - start);
}
bool writeTextFileIfChanged(const fs::path& path, const std::string& text,
std::string& error) {
std::error_code ec;
if (fs::exists(path, ec) && !ec) {
std::ifstream existing(path, std::ios::binary);
if (existing.is_open()) {
std::ostringstream ss;
ss << existing.rdbuf();
if (ss.str() == text) {
return true;
}
}
}
fs::create_directories(path.parent_path(), ec);
ec.clear();
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out.is_open()) {
error = "Unable to write wrapper file: " + path.string();
return false;
}
out << text;
out.close();
if (!out.good()) {
error = "Failed to flush wrapper file: " + path.string();
return false;
}
return true;
}
std::optional<fs::file_time_type> getFileWriteTime(const fs::path& path) {
if (path.empty()) return std::nullopt;
std::error_code ec;
if (!fs::exists(path, ec) || ec) return std::nullopt;
auto t = fs::last_write_time(path, ec);
if (ec) return std::nullopt;
return t;
}
#if !defined(_WIN32)
std::string posixCompileDriver(bool cxx) {
static int ccacheAvailable = -1;
if (ccacheAvailable < 0) {
ccacheAvailable = (std::system("command -v ccache >/dev/null 2>&1") == 0) ? 1 : 0;
}
if (ccacheAvailable == 1) {
return cxx ? "ccache g++" : "ccache gcc";
}
return cxx ? "g++" : "gcc";
}
#endif
// why does windows need all of this :sob:
#if defined(_WIN32)
std::string getEnvValue(const char* name) {
@@ -457,14 +512,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
#else
secondaryObjectPath = config.outDir / relativeParent / (baseName + ".wrap.o");
#endif
std::error_code createErr;
fs::create_directories(wrapperPath.parent_path(), createErr);
std::ofstream wrapper(wrapperPath);
if (!wrapper.is_open()) {
error = "Unable to write C API wrapper file: " + wrapperPath.string();
return false;
}
std::ostringstream wrapper;
auto emitCImplDecl = [&](const char* name, const FunctionSpec& spec) {
if (!spec.present) return;
@@ -846,6 +894,9 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
emitEditorBridge("RenderEditorWindow", "Modu_RenderEditorWindow", editorRenderSpec);
emitEditorBridge("ExitRenderEditorWindow", "Modu_ExitRenderEditorWindow", editorExitSpec);
wrapper << "}\n";
if (!writeTextFileIfChanged(wrapperPath, wrapper.str(), error)) {
return false;
}
#ifdef _WIN32
compileCmd << "cl /nologo /TC /MD /Zi /Od";
@@ -861,11 +912,11 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
linkCmd << " " << lib;
}
#else
compileCmd << "gcc -std=c11 -fPIC -O0 -g";
compileCmd << posixCompileDriver(false) << " -std=c11 -fPIC -O0 -g";
appendPosixIncludesAndDefines(compileCmd);
compileCmd << " -c \"" << scriptAbs.string() << "\" -o \"" << objectPath.string() << "\"";
compileCmd << " && ";
compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g";
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
appendPosixIncludesAndDefines(compileCmd);
compileCmd << " -c \"" << wrapperPath.string() << "\" -o \"" << secondaryObjectPath.string() << "\"";
linkCmd << "g++ -shared \"" << objectPath.string() << "\" \"" << secondaryObjectPath.string()
@@ -895,14 +946,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
fs::path sourceToCompile = scriptAbs;
if (useWrapper) {
wrapperPath = config.outDir / relativeParent / (baseName + ".wrap.cpp");
std::error_code createErr;
fs::create_directories(wrapperPath.parent_path(), createErr);
std::ofstream wrapper(wrapperPath);
if (!wrapper.is_open()) {
error = "Unable to write wrapper file: " + wrapperPath.string();
return false;
}
std::ostringstream wrapper;
std::string includePath = scriptAbs.lexically_normal().generic_string();
if (needsInspectorWrap) {
@@ -980,6 +1024,9 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
}
wrapper << "}\n";
if (!writeTextFileIfChanged(wrapperPath, wrapper.str(), error)) {
return false;
}
sourceToCompile = wrapperPath;
}
@@ -993,7 +1040,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
linkCmd << " " << lib;
}
#else
compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g";
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
appendPosixIncludesAndDefines(compileCmd);
compileCmd << " -c \"" << sourceToCompile.string() << "\" -o \"" << objectPath.string() << "\"";
linkCmd << "g++ -shared \"" << objectPath.string() << "\" -o \"" << binaryPath.string() << "\"";
@@ -1029,6 +1076,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
outCommands.secondaryObjectPath = secondaryObjectPath;
outCommands.binaryPath = binaryPath;
outCommands.wrapperPath = wrapperPath;
outCommands.sourcePath = scriptAbs;
outCommands.usedWrapper = useWrapper;
return true;
}
@@ -1075,13 +1123,72 @@ bool ScriptCompiler::compile(const ScriptBuildCommands& commands, ScriptCompileO
fs::create_directories(commands.binaryPath.parent_path(), ec);
}
if (!runCommand(commands.compile + " 2>&1", output.compileLog)) {
error = "Compile failed";
return false;
bool needsCompile = true;
bool needsLink = true;
const auto sourceTime = getFileWriteTime(commands.sourcePath);
const auto wrapperTime = getFileWriteTime(commands.wrapperPath);
const auto objectTime = getFileWriteTime(commands.objectPath);
const bool hasSecondaryObject = !commands.secondaryObjectPath.empty();
const auto secondaryObjectTime = hasSecondaryObject
? getFileWriteTime(commands.secondaryObjectPath)
: std::optional<fs::file_time_type>{};
const auto binaryTime = getFileWriteTime(commands.binaryPath);
std::optional<fs::file_time_type> newestInput = sourceTime;
if (wrapperTime) {
newestInput = newestInput ? std::max(*newestInput, *wrapperTime) : wrapperTime;
}
if (!runCommand(commands.link + " 2>&1", output.linkLog)) {
error = "Link failed";
return false;
if (objectTime && newestInput) {
needsCompile = (*objectTime < *newestInput);
if (hasSecondaryObject) {
if (!secondaryObjectTime) {
needsCompile = true;
} else {
needsCompile = needsCompile || (*secondaryObjectTime < *newestInput);
}
}
} else {
needsCompile = true;
}
if (!needsCompile) {
output.compileLog += "Skipped compile (up-to-date)\n";
}
if (binaryTime && objectTime) {
fs::file_time_type newestObject = *objectTime;
if (hasSecondaryObject) {
if (!secondaryObjectTime) {
needsLink = true;
} else {
newestObject = std::max(newestObject, *secondaryObjectTime);
needsLink = (*binaryTime < newestObject);
}
} else {
needsLink = (*binaryTime < newestObject);
}
} else {
needsLink = true;
}
if (needsCompile) {
if (!runCommand(commands.compile + " 2>&1", output.compileLog)) {
error = "Compile failed";
return false;
}
needsLink = true;
}
if (needsLink) {
if (!runCommand(commands.link + " 2>&1", output.linkLog)) {
error = "Link failed";
return false;
}
} else {
output.linkLog += "Skipped link (up-to-date)\n";
}
return true;
}

View File

@@ -19,6 +19,7 @@ struct ScriptBuildCommands {
fs::path secondaryObjectPath;
fs::path binaryPath;
fs::path wrapperPath;
fs::path sourcePath;
bool usedWrapper = false;
};

View File

@@ -134,6 +134,11 @@ void Shader::setVec3(const std::string &name, const glm::vec3 &value) const
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void Shader::setVec4(const std::string &name, const glm::vec4 &value) const
{
glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
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));

View File

@@ -12406,6 +12406,8 @@ static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window)
{
ImVec2 scroll = window->Scroll;
ImVec2 decoration_size(window->DecoOuterSizeX1 + window->DecoInnerSizeX1 + window->DecoOuterSizeX2, window->DecoOuterSizeY1 + window->DecoInnerSizeY1 + window->DecoOuterSizeY2);
const bool allow_elastic_overscroll = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) == 0 &&
(window->Flags & ImGuiWindowFlags_NoMouseInputs) == 0;
for (int axis = 0; axis < 2; axis++)
{
if (window->ScrollTarget[axis] < FLT_MAX)
@@ -12420,9 +12422,18 @@ static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window)
}
scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]);
}
scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0f));
if (!window->Collapsed && !window->SkipItems)
scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]);
if (allow_elastic_overscroll && !window->Collapsed && !window->SkipItems)
{
const float axis_extent = (axis == 0) ? window->InnerRect.GetWidth() : window->InnerRect.GetHeight();
const float overscroll_limit = ImClamp(ImMin(axis_extent * 0.14f, window->ScrollMax[axis] * 0.35f + 6.0f), 6.0f, 26.0f);
scroll[axis] = ImClamp(ImRound64(scroll[axis]), -overscroll_limit, window->ScrollMax[axis] + overscroll_limit);
}
else
{
scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0f));
if (!window->Collapsed && !window->SkipItems)
scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]);
}
}
return scroll;
}