Compare commits
23
Commits
ad16daea7c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d26f61a16f | ||
|
|
6a85d17899 | ||
|
|
08acd84404 | ||
|
|
0fbc023018 | ||
|
|
de7e489cc0 | ||
|
|
9cf9242c2a | ||
|
|
118016bcc7 | ||
|
|
355dfa7851 | ||
|
|
aa3eef259c | ||
|
|
9c5a97cfbd | ||
|
|
b798ed0378 | ||
|
|
0b7cc38543 | ||
|
|
a3d3da4a67 | ||
|
|
f7e7c56392 | ||
|
|
f055bdcbf1 | ||
|
|
f99147b1a2 | ||
|
|
dde2c7daf9 | ||
|
|
e5d37965a1 | ||
|
|
3df0f89343 | ||
|
|
3a04c1884a | ||
|
|
92c1daf396 | ||
|
|
997844eaa7 | ||
|
|
36341bb5e0 |
@@ -54,3 +54,6 @@
|
|||||||
[submodule "destrum/third_party/imguizmo"]
|
[submodule "destrum/third_party/imguizmo"]
|
||||||
path = destrum/third_party/imguizmo
|
path = destrum/third_party/imguizmo
|
||||||
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
||||||
|
[submodule "destrum/third_party/openal-soft"]
|
||||||
|
path = destrum/third_party/openal-soft
|
||||||
|
url = https://github.com/kcat/openal-soft.git
|
||||||
|
|||||||
+18
-1
@@ -8,6 +8,12 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
include(CTest)
|
include(CTest)
|
||||||
|
|
||||||
option(DESTRUM_ENABLE_WARNINGS "Enable warnings for first-party targets" ON)
|
option(DESTRUM_ENABLE_WARNINGS "Enable warnings for first-party targets" ON)
|
||||||
|
option(DESTRUM_STRICT_ASSET_COOKING "Fail the build when an asset falls back to its source instead of being cooked" ON)
|
||||||
|
|
||||||
|
set(DESTRUM_CHEF_FLAGS)
|
||||||
|
if (DESTRUM_STRICT_ASSET_COOKING)
|
||||||
|
list(APPEND DESTRUM_CHEF_FLAGS --strict)
|
||||||
|
endif()
|
||||||
|
|
||||||
function(destrum_enable_warnings target)
|
function(destrum_enable_warnings target)
|
||||||
if (NOT DESTRUM_ENABLE_WARNINGS)
|
if (NOT DESTRUM_ENABLE_WARNINGS)
|
||||||
@@ -27,6 +33,7 @@ if (MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(destrum)
|
add_subdirectory(destrum)
|
||||||
|
add_subdirectory(editor)
|
||||||
add_subdirectory(lightkeeper)
|
add_subdirectory(lightkeeper)
|
||||||
add_subdirectory(TheChef)
|
add_subdirectory(TheChef)
|
||||||
|
|
||||||
@@ -37,6 +44,7 @@ endif()
|
|||||||
|
|
||||||
destrum_enable_warnings(destrum_assets)
|
destrum_enable_warnings(destrum_assets)
|
||||||
destrum_enable_warnings(destrum)
|
destrum_enable_warnings(destrum)
|
||||||
|
destrum_enable_warnings(destrum_editor)
|
||||||
destrum_enable_warnings(lightkeeper)
|
destrum_enable_warnings(lightkeeper)
|
||||||
|
|
||||||
add_custom_target(CleanupAssets
|
add_custom_target(CleanupAssets
|
||||||
@@ -53,10 +61,19 @@ add_dependencies(CookAssets
|
|||||||
_internal_cook_engine_assets
|
_internal_cook_engine_assets
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_custom_target(Export)
|
||||||
|
add_dependencies(Export _internal_export_lightkeeper)
|
||||||
|
|
||||||
|
add_custom_target(ValidateAssets)
|
||||||
|
add_dependencies(ValidateAssets
|
||||||
|
_internal_validate_game_assets
|
||||||
|
_internal_validate_engine_assets
|
||||||
|
)
|
||||||
|
|
||||||
option(ENABLE_SANITIZERS "Enable Address/Undefined sanitizers" OFF)
|
option(ENABLE_SANITIZERS "Enable Address/Undefined sanitizers" OFF)
|
||||||
|
|
||||||
if (ENABLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
if (ENABLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
||||||
foreach(target destrum lightkeeper)
|
foreach(target destrum destrum_editor lightkeeper)
|
||||||
target_compile_options(${target} PRIVATE
|
target_compile_options(${target} PRIVATE
|
||||||
-fsanitize=address,undefined
|
-fsanitize=address,undefined
|
||||||
-fno-omit-frame-pointer
|
-fno-omit-frame-pointer
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ Destrum is a C++20 Vulkan rendering/game-engine playground. It contains the core
|
|||||||
* Skeletal animation support
|
* Skeletal animation support
|
||||||
* Skybox/cubemap rendering
|
* Skybox/cubemap rendering
|
||||||
* Asset cooking pipeline for engine and game assets
|
* Asset cooking pipeline for engine and game assets
|
||||||
|
* Editor library: docked viewport, play/pause/stop, undo/redo, click to select, gizmos, asset browser
|
||||||
|
* Stable asset ids (`.meta` files) and material files (`.dmat`)
|
||||||
|
* HDR rendering with exposure and tonemapping (ACES, Khronos PBR Neutral)
|
||||||
|
* 3D audio through OpenAL Soft (`AudioSource` / `AudioListener`, WAV and OGG)
|
||||||
* CMake-based build setup with third-party dependencies as submodules
|
* CMake-based build setup with third-party dependencies as submodules
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
@@ -24,6 +28,8 @@ Destrum is a C++20 Vulkan rendering/game-engine playground. It contains the core
|
|||||||
│ ├── src/ # Engine implementation
|
│ ├── src/ # Engine implementation
|
||||||
│ ├── assets_src/ # Source engine assets
|
│ ├── assets_src/ # Source engine assets
|
||||||
│ └── third_party/# Vendored/submodule dependencies
|
│ └── third_party/# Vendored/submodule dependencies
|
||||||
|
├── editor/ # Editor library (destrum::editor), attached to a game at startup
|
||||||
|
├── docs/ # How the engine pieces work
|
||||||
├── lightkeeper/ # Demo/game executable using Destrum
|
├── lightkeeper/ # Demo/game executable using Destrum
|
||||||
│ ├── include/
|
│ ├── include/
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
@@ -52,7 +58,8 @@ Most dependencies are included as Git submodules:
|
|||||||
* GLM
|
* GLM
|
||||||
* nlohmann/json
|
* nlohmann/json
|
||||||
* spdlog
|
* spdlog
|
||||||
* stb
|
* stb (image, vorbis)
|
||||||
|
* OpenAL Soft
|
||||||
* tinygltf
|
* tinygltf
|
||||||
* tinyexr
|
* tinyexr
|
||||||
* Assimp
|
* Assimp
|
||||||
@@ -111,6 +118,16 @@ cmake --build build --target ValidateShaders
|
|||||||
`CookAssets` processes both engine and game assets. `CleanupAssets` removes generated runtime asset output.
|
`CookAssets` processes both engine and game assets. `CleanupAssets` removes generated runtime asset output.
|
||||||
`ValidateShaders` runs `spirv-val` over the cooked engine shaders when the Vulkan SDK provides it.
|
`ValidateShaders` runs `spirv-val` over the cooked engine shaders when the Vulkan SDK provides it.
|
||||||
|
|
||||||
|
Export a folder other people can run (the game, its DLLs and every cooked asset packed into one `assets.dpak`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build build --config Release --target Export
|
||||||
|
```
|
||||||
|
|
||||||
|
It ends up in `build/export/Release/lightkeeper/`, plus a `lightkeeper.zip` next to it. On Windows, `export.bat` does
|
||||||
|
the same and opens the folder. `TheChef --list build/export/Release/lightkeeper/assets.dpak` shows what takes up the
|
||||||
|
space. See `docs/CookedAssets.md`.
|
||||||
|
|
||||||
Install the application, engine library, public headers, and runtime assets:
|
Install the application, engine library, public headers, and runtime assets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -119,7 +136,7 @@ cmake --install build --config Release
|
|||||||
|
|
||||||
## Demo app
|
## Demo app
|
||||||
|
|
||||||
`lightkeeper` is the current demo application. It creates an SDL window, initializes the Destrum app, loads assets, sets up a scene, renders meshes, skyboxes, and a skinned animated character.
|
`lightkeeper` is the current demo application. It loads its level from `lightkeeper/assets_src/scenes/main.scene` and starts in the editor. Press F5 to play, or run `lightkeeper --game` to skip the editor. See `docs/Editor.md`.
|
||||||
|
|
||||||
## Development notes
|
## Development notes
|
||||||
|
|
||||||
@@ -134,9 +151,11 @@ destrum::destrum
|
|||||||
Game/demo targets can link against it:
|
Game/demo targets can link against it:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
target_link_libraries(my_game PRIVATE destrum::destrum)
|
target_link_libraries(my_game PRIVATE destrum::destrum destrum::editor)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The editor is optional, a game without it links only `destrum::destrum`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Do what you want dawgs :pray:
|
Do what you want dawgs :pray:
|
||||||
|
|||||||
+1
-1
Submodule TheChef updated: 0273340e3b...abcec12c90
+83
-5
@@ -4,6 +4,7 @@ add_subdirectory(third_party)
|
|||||||
add_library(destrum_assets
|
add_library(destrum_assets
|
||||||
"src/Assets/ModelFile.cpp"
|
"src/Assets/ModelFile.cpp"
|
||||||
"src/Assets/TextureFile.cpp"
|
"src/Assets/TextureFile.cpp"
|
||||||
|
"src/Assets/PackFile.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(destrum::assets ALIAS destrum_assets)
|
add_library(destrum::assets ALIAS destrum_assets)
|
||||||
@@ -36,10 +37,16 @@ set(SRC_FILES
|
|||||||
"src/Event.cpp"
|
"src/Event.cpp"
|
||||||
"src/Window.cpp"
|
"src/Window.cpp"
|
||||||
|
|
||||||
|
"src/Audio/AudioDecoder.cpp"
|
||||||
|
"src/Audio/AudioSystem.cpp"
|
||||||
|
|
||||||
"src/Components/Animator.cpp"
|
"src/Components/Animator.cpp"
|
||||||
|
"src/Components/AudioListener.cpp"
|
||||||
|
"src/Components/AudioSource.cpp"
|
||||||
"src/Components/CameraComponent.cpp"
|
"src/Components/CameraComponent.cpp"
|
||||||
"src/Components/LightComponent.cpp"
|
"src/Components/LightComponent.cpp"
|
||||||
"src/Components/MeshRendererComponent.cpp"
|
"src/Components/MeshRendererComponent.cpp"
|
||||||
|
"src/Components/MeshRendererInspector.cpp"
|
||||||
"src/Components/Rotator.cpp"
|
"src/Components/Rotator.cpp"
|
||||||
"src/Components/Spinner.cpp"
|
"src/Components/Spinner.cpp"
|
||||||
"src/Components/OrbitAndSpin.cpp"
|
"src/Components/OrbitAndSpin.cpp"
|
||||||
@@ -48,6 +55,10 @@ set(SRC_FILES
|
|||||||
"src/Components/Physics/BoxCollider.cpp"
|
"src/Components/Physics/BoxCollider.cpp"
|
||||||
"src/Components/Physics/CapsuleCollider.cpp"
|
"src/Components/Physics/CapsuleCollider.cpp"
|
||||||
"src/Components/Physics/Collider.cpp"
|
"src/Components/Physics/Collider.cpp"
|
||||||
|
"src/Components/Physics/ConvexHullCollider.cpp"
|
||||||
|
"src/Components/Physics/CylinderCollider.cpp"
|
||||||
|
"src/Components/Physics/MeshCollider.cpp"
|
||||||
|
"src/Components/Physics/MeshShapeCollider.cpp"
|
||||||
"src/Components/Physics/SphereCollider.cpp"
|
"src/Components/Physics/SphereCollider.cpp"
|
||||||
|
|
||||||
"src/Components/UI/Canvas.cpp"
|
"src/Components/UI/Canvas.cpp"
|
||||||
@@ -58,11 +69,13 @@ set(SRC_FILES
|
|||||||
"src/Graphics/BindlessSetManager.cpp"
|
"src/Graphics/BindlessSetManager.cpp"
|
||||||
"src/Graphics/Camera.cpp"
|
"src/Graphics/Camera.cpp"
|
||||||
"src/Graphics/DebugDraw.cpp"
|
"src/Graphics/DebugDraw.cpp"
|
||||||
|
"src/Graphics/DeletionQueue.cpp"
|
||||||
"src/Graphics/ComputePipeline.cpp"
|
"src/Graphics/ComputePipeline.cpp"
|
||||||
"src/Graphics/Frustum.cpp"
|
"src/Graphics/Frustum.cpp"
|
||||||
"src/Graphics/GfxDevice.cpp"
|
"src/Graphics/GfxDevice.cpp"
|
||||||
"src/Graphics/Caches/ImageCache.cpp"
|
"src/Graphics/Caches/ImageCache.cpp"
|
||||||
"src/Graphics/ImageLoader.cpp"
|
"src/Graphics/ImageLoader.cpp"
|
||||||
|
"src/Graphics/IndirectDrawList.cpp"
|
||||||
"src/Graphics/ImmediateExecuter.cpp"
|
"src/Graphics/ImmediateExecuter.cpp"
|
||||||
"src/Graphics/Init.cpp"
|
"src/Graphics/Init.cpp"
|
||||||
"src/Graphics/Caches/MaterialCache.cpp"
|
"src/Graphics/Caches/MaterialCache.cpp"
|
||||||
@@ -72,7 +85,6 @@ set(SRC_FILES
|
|||||||
"src/Graphics/Swapchain.cpp"
|
"src/Graphics/Swapchain.cpp"
|
||||||
"src/Graphics/Util.cpp"
|
"src/Graphics/Util.cpp"
|
||||||
"src/Graphics/RenderResources.cpp"
|
"src/Graphics/RenderResources.cpp"
|
||||||
"src/Graphics/ResourcesInspector.cpp"
|
|
||||||
|
|
||||||
"src/Graphics/Managers/VulkanInstanceManager.cpp"
|
"src/Graphics/Managers/VulkanInstanceManager.cpp"
|
||||||
"src/Graphics/Managers/MemoryManager.cpp"
|
"src/Graphics/Managers/MemoryManager.cpp"
|
||||||
@@ -96,6 +108,7 @@ set(SRC_FILES
|
|||||||
"src/Graphics/Pipelines/ShadowMapPipeline.cpp"
|
"src/Graphics/Pipelines/ShadowMapPipeline.cpp"
|
||||||
"src/Graphics/Pipelines/SkinningPipeline.cpp"
|
"src/Graphics/Pipelines/SkinningPipeline.cpp"
|
||||||
"src/Graphics/Pipelines/ImguiPass.cpp"
|
"src/Graphics/Pipelines/ImguiPass.cpp"
|
||||||
|
"src/Graphics/Pipelines/PostProcessPipeline.cpp"
|
||||||
|
|
||||||
"src/Input/InputManager.cpp"
|
"src/Input/InputManager.cpp"
|
||||||
|
|
||||||
@@ -108,26 +121,29 @@ set(SRC_FILES
|
|||||||
"src/Scene/Scene.cpp"
|
"src/Scene/Scene.cpp"
|
||||||
"src/Scene/SceneManager.cpp"
|
"src/Scene/SceneManager.cpp"
|
||||||
"src/Scene/ModelSpawner.cpp"
|
"src/Scene/ModelSpawner.cpp"
|
||||||
|
"src/Scene/ResourceCollector.cpp"
|
||||||
|
|
||||||
"src/FS/AssetFS.cpp"
|
"src/FS/AssetFS.cpp"
|
||||||
"src/FS/Manifest.cpp"
|
"src/FS/Manifest.cpp"
|
||||||
|
|
||||||
"src/Assets/AssetManager.cpp"
|
"src/Assets/AssetManager.cpp"
|
||||||
|
"src/Assets/AssetDatabase.cpp"
|
||||||
|
|
||||||
"src/Serialization/SceneSerializer.cpp"
|
"src/Serialization/SceneSerializer.cpp"
|
||||||
"src/Serialization/ObjectSerialization.cpp"
|
"src/Serialization/ObjectSerialization.cpp"
|
||||||
"src/Serialization/PrefabSerializer.cpp"
|
"src/Serialization/PrefabSerializer.cpp"
|
||||||
|
"src/Serialization/MaterialSerialization.cpp"
|
||||||
"src/Serialization/PrefabCache.cpp"
|
"src/Serialization/PrefabCache.cpp"
|
||||||
"src/Serialization/ComponentRegistry.cpp"
|
"src/Serialization/ComponentRegistry.cpp"
|
||||||
|
"src/Serialization/Reflect.cpp"
|
||||||
|
|
||||||
"src/Util/DeltaTime.cpp"
|
"src/Util/DeltaTime.cpp"
|
||||||
"src/Util/ImGuiUtils.cpp"
|
"src/Util/ImGuiUtils.cpp"
|
||||||
"src/Util/TransformGizmo.cpp"
|
|
||||||
"src/Util/RectGizmo.cpp"
|
"src/Threading/JobSystem.cpp"
|
||||||
"src/Util/SceneInspector.cpp"
|
|
||||||
|
|
||||||
"src/Physics/PhysicsWorld.cpp"
|
"src/Physics/PhysicsWorld.cpp"
|
||||||
"src/Physics/SimplePhysicsWorld.cpp"
|
"src/Physics/PhysicsMeshCache.cpp"
|
||||||
"src/Physics/PhysicsSceneBridge.cpp"
|
"src/Physics/PhysicsSceneBridge.cpp"
|
||||||
"src/Physics/JoltPhysicsWorld.cpp"
|
"src/Physics/JoltPhysicsWorld.cpp"
|
||||||
)
|
)
|
||||||
@@ -170,6 +186,8 @@ target_link_libraries(destrum
|
|||||||
PRIVATE
|
PRIVATE
|
||||||
freetype::freetype
|
freetype::freetype
|
||||||
tinyexr
|
tinyexr
|
||||||
|
OpenAL::OpenAL
|
||||||
|
stb::vorbis
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(destrum
|
target_compile_definitions(destrum
|
||||||
@@ -216,10 +234,21 @@ add_custom_target(_internal_cook_engine_assets
|
|||||||
COMMAND TheChef
|
COMMAND TheChef
|
||||||
--input "${ASSETS_SRC_DIR}"
|
--input "${ASSETS_SRC_DIR}"
|
||||||
--output "${ASSETS_RUNTIME_DIR}"
|
--output "${ASSETS_RUNTIME_DIR}"
|
||||||
|
${DESTRUM_CHEF_FLAGS}
|
||||||
DEPENDS TheChef ${ENGINE_ASSET_SOURCES}
|
DEPENDS TheChef ${ENGINE_ASSET_SOURCES}
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_custom_target(_internal_validate_engine_assets
|
||||||
|
COMMAND TheChef
|
||||||
|
--input "${ASSETS_SRC_DIR}"
|
||||||
|
--output "${ASSETS_RUNTIME_DIR}"
|
||||||
|
--validate
|
||||||
|
${DESTRUM_CHEF_FLAGS}
|
||||||
|
DEPENDS _internal_cook_engine_assets
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
find_program(SPIRV_VAL_EXECUTABLE NAMES spirv-val spirv-val.exe)
|
find_program(SPIRV_VAL_EXECUTABLE NAMES spirv-val spirv-val.exe)
|
||||||
if (SPIRV_VAL_EXECUTABLE)
|
if (SPIRV_VAL_EXECUTABLE)
|
||||||
file(GLOB_RECURSE ENGINE_SHADER_SOURCES CONFIGURE_DEPENDS
|
file(GLOB_RECURSE ENGINE_SHADER_SOURCES CONFIGURE_DEPENDS
|
||||||
@@ -232,6 +261,7 @@ if (SPIRV_VAL_EXECUTABLE)
|
|||||||
file(RELATIVE_PATH shader_relative "${ASSETS_SRC_DIR}" "${shader_source}")
|
file(RELATIVE_PATH shader_relative "${ASSETS_SRC_DIR}" "${shader_source}")
|
||||||
list(APPEND VALIDATE_SHADER_COMMANDS
|
list(APPEND VALIDATE_SHADER_COMMANDS
|
||||||
COMMAND "${SPIRV_VAL_EXECUTABLE}"
|
COMMAND "${SPIRV_VAL_EXECUTABLE}"
|
||||||
|
--target-env vulkan1.3 --scalar-block-layout
|
||||||
"${ASSETS_RUNTIME_DIR}/${shader_relative}.spv")
|
"${ASSETS_RUNTIME_DIR}/${shader_relative}.spv")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
@@ -269,6 +299,54 @@ function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
|
|||||||
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
|
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# A folder (and zip) other people can run: the game exe, its DLLs and every cooked asset packed into assets.dpak
|
||||||
|
function(destrum_add_export GAME_TARGET GAME_RUNTIME_DIR)
|
||||||
|
set(_engine_runtime "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_runtime")
|
||||||
|
set(_export_root "${CMAKE_BINARY_DIR}/export/$<CONFIG>")
|
||||||
|
set(_export_dir "${_export_root}/${GAME_TARGET}")
|
||||||
|
|
||||||
|
# The release VC++ runtime ships app-local, debug builds only run where Visual Studio is installed
|
||||||
|
set(_system_libs)
|
||||||
|
if (MSVC)
|
||||||
|
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
|
||||||
|
include(InstallRequiredSystemLibraries)
|
||||||
|
set(_system_libs "$<$<NOT:$<CONFIG:Debug>>:${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}>")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_target(_internal_export_${GAME_TARGET}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_export_dir}" "${_export_dir}.zip"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_export_dir}"
|
||||||
|
COMMAND TheChef
|
||||||
|
--pack "${_export_dir}/assets.dpak"
|
||||||
|
--mount "engine=${_engine_runtime}"
|
||||||
|
--mount "game=${GAME_RUNTIME_DIR}"
|
||||||
|
COMMAND TheChef --list "${_export_dir}/assets.dpak" --top 10
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy -t "${_export_dir}"
|
||||||
|
"$<TARGET_FILE:${GAME_TARGET}>"
|
||||||
|
"$<TARGET_RUNTIME_DLLS:${GAME_TARGET}>"
|
||||||
|
"${_system_libs}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E chdir "${_export_root}"
|
||||||
|
${CMAKE_COMMAND} -E tar cf "${GAME_TARGET}.zip" --format=zip "${GAME_TARGET}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E echo "Exported ${GAME_TARGET} to ${_export_dir}"
|
||||||
|
COMMAND_EXPAND_LISTS
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_dependencies(_internal_export_${GAME_TARGET} ${GAME_TARGET})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# OpenAL is a DLL, every exe that links the engine needs a copy next to it
|
||||||
|
function(destrum_copy_runtime_dlls TARGET)
|
||||||
|
if (WIN32)
|
||||||
|
add_custom_command(TARGET ${TARGET} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_RUNTIME_DLLS:${TARGET}> $<TARGET_FILE_DIR:${TARGET}>
|
||||||
|
COMMAND_EXPAND_LISTS
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
install(FILES $<TARGET_FILE:OpenAL::OpenAL> DESTINATION bin)
|
||||||
|
|
||||||
install(TARGETS destrum destrum_assets
|
install(TARGETS destrum destrum_assets
|
||||||
ARCHIVE DESTINATION lib
|
ARCHIVE DESTINATION lib
|
||||||
LIBRARY DESTINATION lib
|
LIBRARY DESTINATION lib
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"allowFallback": [
|
||||||
|
"run.fbx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"guid": "a09100c14a67b3840518b68b93fbadda"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"guid": "0407d177c8487bca092ffb7fd4d47aa2"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"guid": "78d91a725664c8b815531d55bc62683f"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef INSTANCE_DATA_GLSL
|
||||||
|
#define INSTANCE_DATA_GLSL
|
||||||
|
|
||||||
|
#extension GL_EXT_buffer_reference : require
|
||||||
|
#extension GL_EXT_scalar_block_layout : require
|
||||||
|
|
||||||
|
#include "vertex.glsl"
|
||||||
|
|
||||||
|
// Must match GPUInstanceData in IndirectDrawList.h
|
||||||
|
struct InstanceData {
|
||||||
|
mat4 transform;
|
||||||
|
VertexBuffer vertexBuffer;
|
||||||
|
uint materialID;
|
||||||
|
uint objectID;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout (buffer_reference, scalar) readonly buffer InstanceBuffer {
|
||||||
|
InstanceData instances[];
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INSTANCE_DATA_GLSL
|
||||||
@@ -12,9 +12,24 @@ layout (location = 0) in vec3 inPos;
|
|||||||
layout (location = 1) in vec2 inUV;
|
layout (location = 1) in vec2 inUV;
|
||||||
layout (location = 2) in vec3 inNormal;
|
layout (location = 2) in vec3 inNormal;
|
||||||
layout (location = 3) in vec4 inTangent;
|
layout (location = 3) in vec4 inTangent;
|
||||||
|
layout (location = 4) flat in uint inMaterialID;
|
||||||
|
layout (location = 5) flat in uint inObjectID;
|
||||||
|
|
||||||
layout (location = 0) out vec4 outFragColor;
|
layout (location = 0) out vec4 outFragColor;
|
||||||
|
|
||||||
|
// Stable per object, the same object keeps its color between frames and runs
|
||||||
|
vec3 selectionColor(uint objectID)
|
||||||
|
{
|
||||||
|
if (objectID == 0u) {
|
||||||
|
return vec3(0.0);
|
||||||
|
}
|
||||||
|
uint hash = objectID * 2654435761u;
|
||||||
|
hash ^= hash >> 15;
|
||||||
|
hash *= 2246822519u;
|
||||||
|
hash ^= hash >> 13;
|
||||||
|
return vec3(hash & 255u, (hash >> 8) & 255u, (hash >> 16) & 255u) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
vec3 calculateNormal(MaterialData material, uint samplerID)
|
vec3 calculateNormal(MaterialData material, uint samplerID)
|
||||||
{
|
{
|
||||||
vec3 n = normalize(inNormal);
|
vec3 n = normalize(inNormal);
|
||||||
@@ -62,7 +77,7 @@ float calculateShadow(Light light, vec3 fragPos, vec3 geometryNormal)
|
|||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
|
MaterialData material = pcs.sceneData.materials.data[inMaterialID];
|
||||||
uint samplerID = material.textureFilteringMode;
|
uint samplerID = material.textureFilteringMode;
|
||||||
|
|
||||||
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
|
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
|
||||||
@@ -126,6 +141,10 @@ void main()
|
|||||||
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
|
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pcs.sceneData.debugSelectionView != 0) {
|
||||||
|
fragColor = selectionColor(inObjectID & 0x7FFFFFFFu);
|
||||||
|
}
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
// TANGENT DEBUG
|
// TANGENT DEBUG
|
||||||
if (inTangent == vec4(0.0)) {
|
if (inTangent == vec4(0.0)) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ layout (location = 0) out vec3 outPos;
|
|||||||
layout (location = 1) out vec2 outUV;
|
layout (location = 1) out vec2 outUV;
|
||||||
layout (location = 2) out vec3 outNormal;
|
layout (location = 2) out vec3 outNormal;
|
||||||
layout (location = 3) out vec4 outTangent;
|
layout (location = 3) out vec4 outTangent;
|
||||||
|
layout (location = 4) flat out uint outMaterialID;
|
||||||
|
layout (location = 5) flat out uint outObjectID;
|
||||||
|
|
||||||
// The depth pre-pass reuses this shader, positions have to match bit for bit
|
// The depth pre-pass reuses this shader, positions have to match bit for bit
|
||||||
// so the main pass can depth test against it with LESS_OR_EQUAL
|
// so the main pass can depth test against it with LESS_OR_EQUAL
|
||||||
@@ -15,9 +17,11 @@ invariant gl_Position;
|
|||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
|
// gl_InstanceIndex already includes the indirect command's firstInstance
|
||||||
|
InstanceData instance = pcs.instanceBuffer.instances[gl_InstanceIndex];
|
||||||
|
Vertex v = instance.vertexBuffer.vertices[gl_VertexIndex];
|
||||||
|
|
||||||
vec4 worldPos = pcs.transform * vec4(v.position, 1.0f);
|
vec4 worldPos = instance.transform * vec4(v.position, 1.0f);
|
||||||
|
|
||||||
gl_Position = pcs.sceneData.viewProj * worldPos;
|
gl_Position = pcs.sceneData.viewProj * worldPos;
|
||||||
outPos = worldPos.xyz;
|
outPos = worldPos.xyz;
|
||||||
@@ -25,9 +29,11 @@ void main()
|
|||||||
// A bit inefficient, but okay - this is needed for non-uniform scale
|
// A bit inefficient, but okay - this is needed for non-uniform scale
|
||||||
// models. See: http://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
|
// models. See: http://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
|
||||||
// Simpler case, when everything is uniform
|
// Simpler case, when everything is uniform
|
||||||
// outNormal = (pcs.transform * vec4(v.normal, 0.0)).xyz;
|
// outNormal = (instance.transform * vec4(v.normal, 0.0)).xyz;
|
||||||
outNormal = mat3(transpose(inverse(pcs.transform))) * v.normal;
|
outNormal = mat3(transpose(inverse(instance.transform))) * v.normal;
|
||||||
|
|
||||||
// w is the bitangent sign, the fragment shader builds the TBN from the interpolated vectors
|
// w is the bitangent sign, the fragment shader builds the TBN from the interpolated vectors
|
||||||
outTangent = vec4(mat3(pcs.transform) * v.tangent.xyz, v.tangent.w);
|
outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w);
|
||||||
|
outMaterialID = instance.materialID;
|
||||||
|
outObjectID = instance.objectID;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,10 @@
|
|||||||
#extension GL_EXT_scalar_block_layout: require
|
#extension GL_EXT_scalar_block_layout: require
|
||||||
|
|
||||||
#include "scene_data.glsl"
|
#include "scene_data.glsl"
|
||||||
#include "vertex.glsl"
|
#include "instance_data.glsl"
|
||||||
|
|
||||||
layout (push_constant, scalar) uniform constants
|
layout (push_constant, scalar) uniform constants
|
||||||
{
|
{
|
||||||
mat4 transform;
|
|
||||||
SceneDataBuffer sceneData;
|
SceneDataBuffer sceneData;
|
||||||
VertexBuffer vertexBuffer;
|
InstanceBuffer instanceBuffer;
|
||||||
uint materialID;
|
|
||||||
uint padding;
|
|
||||||
} pcs;
|
} pcs;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#version 460
|
||||||
|
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
#extension GL_EXT_nonuniform_qualifier : enable
|
||||||
|
|
||||||
|
#include "bindless.glsl"
|
||||||
|
|
||||||
|
// The selection image is R32_UINT, read through the same bindless array as an unsigned texture
|
||||||
|
layout (set = 0, binding = 0) uniform utexture2D uintTextures[];
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 inUV;
|
||||||
|
layout (location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
// Must match PostProcessPipeline::PushConstants and the Tonemapper enum
|
||||||
|
#define TONEMAPPER_NONE 0
|
||||||
|
#define TONEMAPPER_ACES 1
|
||||||
|
#define TONEMAPPER_PBR_NEUTRAL 2
|
||||||
|
#define NO_TEXTURE 0xFFFFFFFFu
|
||||||
|
#define SELECTED_FLAG 0x80000000u
|
||||||
|
|
||||||
|
layout (push_constant) uniform PostPushConstants {
|
||||||
|
uint sceneColorTexture;
|
||||||
|
uint selectionTexture;
|
||||||
|
uint tonemapper;
|
||||||
|
float exposure;
|
||||||
|
ivec2 hoverPixel;
|
||||||
|
} pcs;
|
||||||
|
|
||||||
|
// Narkowicz 2015, a fit of the ACES filmic curve
|
||||||
|
vec3 tonemapAces(vec3 color)
|
||||||
|
{
|
||||||
|
const float a = 2.51;
|
||||||
|
const float b = 0.03;
|
||||||
|
const float c = 2.43;
|
||||||
|
const float d = 0.59;
|
||||||
|
const float e = 0.14;
|
||||||
|
return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Khronos PBR Neutral, keeps base colors as they are up to about 0.8 and only rolls off the highlights
|
||||||
|
vec3 tonemapPbrNeutral(vec3 color)
|
||||||
|
{
|
||||||
|
const float startCompression = 0.8 - 0.04;
|
||||||
|
const float desaturation = 0.15;
|
||||||
|
|
||||||
|
float x = min(color.r, min(color.g, color.b));
|
||||||
|
float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
||||||
|
color -= offset;
|
||||||
|
|
||||||
|
float peak = max(color.r, max(color.g, color.b));
|
||||||
|
if (peak < startCompression) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float d = 1.0 - startCompression;
|
||||||
|
float newPeak = 1.0 - d * d / (peak + d - startCompression);
|
||||||
|
color *= newPeak / peak;
|
||||||
|
|
||||||
|
float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
|
||||||
|
return mix(color, vec3(newPeak), g);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint objectAt(ivec2 pixel, ivec2 size)
|
||||||
|
{
|
||||||
|
pixel = clamp(pixel, ivec2(0), size - 1);
|
||||||
|
return texelFetch(nonuniformEXT(usampler2D(uintTextures[pcs.selectionTexture], samplers[NEAREST_SAMPLER_ID])), pixel, 0).r;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 applyOutlines(vec3 color, ivec2 pixel)
|
||||||
|
{
|
||||||
|
if (pcs.selectionTexture == NO_TEXTURE) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
ivec2 size = textureSize(nonuniformEXT(usampler2D(uintTextures[pcs.selectionTexture], samplers[NEAREST_SAMPLER_ID])), 0);
|
||||||
|
uint center = objectAt(pixel, size);
|
||||||
|
uint hovered = pcs.hoverPixel.x >= 0 ? (objectAt(pcs.hoverPixel, size) & ~SELECTED_FLAG) : 0u;
|
||||||
|
|
||||||
|
bool selectedEdge = false;
|
||||||
|
bool hoveredEdge = false;
|
||||||
|
for (int y = -2; y <= 2; ++y) {
|
||||||
|
for (int x = -2; x <= 2; ++x) {
|
||||||
|
if (abs(x) + abs(y) > 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint neighbor = objectAt(pixel + ivec2(x, y), size);
|
||||||
|
selectedEdge = selectedEdge || ((neighbor & SELECTED_FLAG) != 0u && (center & SELECTED_FLAG) == 0u);
|
||||||
|
hoveredEdge = hoveredEdge || (hovered != 0u && (neighbor & ~SELECTED_FLAG) == hovered &&
|
||||||
|
(center & ~SELECTED_FLAG) != hovered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEdge) {
|
||||||
|
return vec3(1.0, 0.55, 0.1);
|
||||||
|
}
|
||||||
|
if (hoveredEdge) {
|
||||||
|
return mix(color, vec3(1.0), 0.65);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
||||||
|
vec3 color = texelFetch(nonuniformEXT(sampler2D(textures[pcs.sceneColorTexture], samplers[NEAREST_SAMPLER_ID])), pixel, 0).rgb;
|
||||||
|
color *= pcs.exposure;
|
||||||
|
|
||||||
|
if (pcs.tonemapper == TONEMAPPER_ACES) {
|
||||||
|
color = tonemapAces(color);
|
||||||
|
} else if (pcs.tonemapper == TONEMAPPER_PBR_NEUTRAL) {
|
||||||
|
color = tonemapPbrNeutral(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
outColor = vec4(applyOutlines(clamp(color, 0.0, 1.0), pixel), 1.0);
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
|
|||||||
// directional shadows
|
// directional shadows
|
||||||
uint shadowMapId;
|
uint shadowMapId;
|
||||||
float shadowNormalOffset;
|
float shadowNormalOffset;
|
||||||
uint padding;
|
uint debugSelectionView;
|
||||||
mat4 lightViewProj;
|
mat4 lightViewProj;
|
||||||
|
|
||||||
// spot shadows
|
// spot shadows
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#version 460
|
||||||
|
|
||||||
|
// Same interface as mesh.vert, only the object ID is used
|
||||||
|
layout (location = 0) in vec3 inPos;
|
||||||
|
layout (location = 1) in vec2 inUV;
|
||||||
|
layout (location = 2) in vec3 inNormal;
|
||||||
|
layout (location = 3) in vec4 inTangent;
|
||||||
|
layout (location = 4) flat in uint inMaterialID;
|
||||||
|
layout (location = 5) flat in uint inObjectID;
|
||||||
|
|
||||||
|
layout (location = 0) out uint outObjectID;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
outObjectID = inObjectID;
|
||||||
|
}
|
||||||
@@ -4,17 +4,18 @@
|
|||||||
#extension GL_EXT_buffer_reference : require
|
#extension GL_EXT_buffer_reference : require
|
||||||
#extension GL_EXT_scalar_block_layout : require
|
#extension GL_EXT_scalar_block_layout : require
|
||||||
|
|
||||||
#include "vertex.glsl"
|
#include "instance_data.glsl"
|
||||||
|
|
||||||
layout (push_constant, scalar) uniform constants
|
layout (push_constant, scalar) uniform constants
|
||||||
{
|
{
|
||||||
mat4 modelToLightClip;
|
mat4 lightViewProj;
|
||||||
VertexBuffer vertexBuffer;
|
InstanceBuffer instanceBuffer;
|
||||||
} pcs;
|
} pcs;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
|
InstanceData instance = pcs.instanceBuffer.instances[gl_InstanceIndex];
|
||||||
|
Vertex v = instance.vertexBuffer.vertices[gl_VertexIndex];
|
||||||
|
|
||||||
gl_Position = pcs.modelToLightClip * vec4(v.position, 1.0f);
|
gl_Position = pcs.lightViewProj * instance.transform * vec4(v.position, 1.0f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"guid": "c2f1f9dcb185460c6d685c17d7ec4f82"
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
#ifndef APP_H
|
#ifndef APP_H
|
||||||
#define APP_H
|
#define APP_H
|
||||||
#include <filesystem>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "glm/vec2.hpp"
|
#include <glm/vec2.hpp>
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
|
||||||
|
|
||||||
|
#include <destrum/AppLayer.h>
|
||||||
#include <destrum/Window.h>
|
#include <destrum/Window.h>
|
||||||
|
#include <destrum/Graphics/Camera.h>
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
#include <destrum/Graphics/Renderer.h>
|
#include <destrum/Graphics/Renderer.h>
|
||||||
|
#include <destrum/Graphics/RenderResources.h>
|
||||||
#include <destrum/Graphics/Pipelines/ImguiPass.h>
|
#include <destrum/Graphics/Pipelines/ImguiPass.h>
|
||||||
#include <destrum/Graphics/ResourcesInspector.h>
|
|
||||||
|
|
||||||
#include <destrum/Input/InputManager.h>
|
#include <destrum/Input/InputManager.h>
|
||||||
|
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public:
|
public:
|
||||||
struct AppParams {
|
struct AppParams {
|
||||||
@@ -25,28 +25,57 @@ public:
|
|||||||
std::string appName{"Destrum App"};
|
std::string appName{"Destrum App"};
|
||||||
std::string windowTitle;
|
std::string windowTitle;
|
||||||
std::filesystem::path exeDir;
|
std::filesystem::path exeDir;
|
||||||
bool showDebugUi{true};
|
|
||||||
WindowMode windowMode{WindowMode::Windowed};
|
WindowMode windowMode{WindowMode::Windowed};
|
||||||
bool startMaximized{false};
|
bool startMaximized{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SimulationControls {
|
||||||
|
bool paused{false};
|
||||||
|
bool stepOnce{false};
|
||||||
|
float timeScale{1.0f};
|
||||||
|
int stepsLastFrame{0};
|
||||||
|
float accumulator{0.0f};
|
||||||
|
float fixedDeltaTime{0.0f};
|
||||||
|
};
|
||||||
|
|
||||||
App();
|
App();
|
||||||
virtual ~App();
|
virtual ~App();
|
||||||
|
|
||||||
|
App(const App&) = delete;
|
||||||
|
App& operator=(const App&) = delete;
|
||||||
|
|
||||||
void init(const AppParams& params);
|
void init(const AppParams& params);
|
||||||
|
void attachLayer(std::unique_ptr<AppLayer> layer);
|
||||||
void run();
|
void run();
|
||||||
void cleanup();
|
void cleanup();
|
||||||
|
|
||||||
virtual void customInit() = 0;
|
virtual void customInit() = 0;
|
||||||
virtual void customUpdate(float dt) = 0;
|
virtual void customUpdate(float) {}
|
||||||
virtual void customDraw() = 0;
|
virtual void customFixedUpdate(float) {}
|
||||||
|
virtual void customRender(GameRenderer&) {}
|
||||||
virtual void customCleanup() {}
|
virtual void customCleanup() {}
|
||||||
virtual void customFixedUpdate(float dt) = 0;
|
|
||||||
|
|
||||||
virtual void onWindowResize(int, int) {};
|
|
||||||
|
|
||||||
// Called inside the "Windows" menu of the main menu bar, add toggles for the game's own windows here
|
|
||||||
virtual void customWindowsMenu() {}
|
virtual void customWindowsMenu() {}
|
||||||
|
virtual void onWindowResize(int, int) {}
|
||||||
|
virtual void onPlayModeChanged(bool) {}
|
||||||
|
[[nodiscard]] virtual Camera* gameCamera() { return nullptr; }
|
||||||
|
|
||||||
|
[[nodiscard]] bool isPlaying() const;
|
||||||
|
[[nodiscard]] bool isPaused() const;
|
||||||
|
|
||||||
|
[[nodiscard]] Window& getWindow() { return window; }
|
||||||
|
[[nodiscard]] GfxDevice& getGfxDevice() { return gfxDevice; }
|
||||||
|
[[nodiscard]] GameRenderer& getRenderer() { return renderer; }
|
||||||
|
[[nodiscard]] RenderResources& getResources() { return resources; }
|
||||||
|
[[nodiscard]] ImguiPass& getImguiPass() { return imguiPass; }
|
||||||
|
[[nodiscard]] const AppParams& getParams() const { return m_params; }
|
||||||
|
[[nodiscard]] SimulationControls& getSimulation() { return simulation; }
|
||||||
|
[[nodiscard]] float getAverageFps() const { return avgFPS; }
|
||||||
|
[[nodiscard]] float getRenderAspectRatio() const;
|
||||||
|
|
||||||
|
[[nodiscard]] Camera& getViewCamera();
|
||||||
|
void resizeRenderTarget(const glm::ivec2& size);
|
||||||
|
void collectUnusedResources();
|
||||||
|
void requestQuit() { isRunning = false; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Window window;
|
Window window;
|
||||||
@@ -55,44 +84,29 @@ protected:
|
|||||||
GfxDevice gfxDevice;
|
GfxDevice gfxDevice;
|
||||||
GameRenderer renderer;
|
GameRenderer renderer;
|
||||||
RenderResources resources;
|
RenderResources resources;
|
||||||
|
|
||||||
ImguiPass imguiPass;
|
ImguiPass imguiPass;
|
||||||
|
|
||||||
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
bool cleanedUp{false};
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initDevices();
|
||||||
|
void processEvents();
|
||||||
|
void runFixedSteps();
|
||||||
|
void handleSwapchainResize(bool& skipFrame);
|
||||||
|
void renderFrame();
|
||||||
|
|
||||||
|
std::unique_ptr<AppLayer> layer;
|
||||||
|
Camera fallbackCamera{glm::vec3{0.0f, 2.0f, -5.0f}, glm::vec3{0.0f, 1.0f, 0.0f}};
|
||||||
|
SimulationControls simulation{};
|
||||||
|
|
||||||
bool isRunning{false};
|
bool isRunning{false};
|
||||||
bool cleanedUp{false};
|
|
||||||
bool cleaningUp{false};
|
bool cleaningUp{false};
|
||||||
bool customInitStarted{false};
|
bool customInitStarted{false};
|
||||||
bool gamePaused{false};
|
|
||||||
|
|
||||||
bool frameLimit{false};
|
bool frameLimit{false};
|
||||||
float frameTime{0.f};
|
float avgFPS{0.0f};
|
||||||
float avgFPS{0.f};
|
|
||||||
|
|
||||||
bool resizePending = false;
|
bool resizePending{false};
|
||||||
std::chrono::steady_clock::time_point lastResizeTime{};
|
std::chrono::steady_clock::time_point lastResizeTime{};
|
||||||
|
|
||||||
bool m_PhysicsPaused = false;
|
|
||||||
bool m_PhysicsStepOnce = false;
|
|
||||||
|
|
||||||
float m_PhysicsTimeScale = 1.0f;
|
|
||||||
int m_PhysicsStepsLastFrame = 0;
|
|
||||||
|
|
||||||
struct DebugWindows {
|
|
||||||
bool stats{true};
|
|
||||||
bool physics{true};
|
|
||||||
bool inspector{true};
|
|
||||||
bool imageCache{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
DebugWindows m_DebugWindows{};
|
|
||||||
ResourcesInspector m_ResourcesInspector;
|
|
||||||
|
|
||||||
void drawWindowsMenu();
|
|
||||||
void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif // APP_H
|
||||||
#endif //APP_H
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef DESTRUM_APPLAYER_H
|
||||||
|
#define DESTRUM_APPLAYER_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <destrum/Components/UI/CanvasFrame.h>
|
||||||
|
|
||||||
|
class Camera;
|
||||||
|
class GameRenderer;
|
||||||
|
|
||||||
|
class AppLayer {
|
||||||
|
public:
|
||||||
|
struct InputFocus {
|
||||||
|
bool mouse{false};
|
||||||
|
bool keyboard{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual ~AppLayer() = default;
|
||||||
|
|
||||||
|
virtual void onAttach() {}
|
||||||
|
virtual void onDetach() {}
|
||||||
|
|
||||||
|
virtual void onBeginFrame() {}
|
||||||
|
virtual void onImGuiBegin(float) {}
|
||||||
|
virtual void onImGui(float) {}
|
||||||
|
virtual void onEndFrame() {}
|
||||||
|
virtual void onRender(GameRenderer&) {}
|
||||||
|
|
||||||
|
[[nodiscard]] virtual Camera* viewCamera() { return nullptr; }
|
||||||
|
[[nodiscard]] virtual bool hostsViewport() const { return false; }
|
||||||
|
[[nodiscard]] virtual InputFocus gameInputFocus() const { return {}; }
|
||||||
|
[[nodiscard]] virtual std::optional<CanvasFrame> canvasFrame() const { return std::nullopt; }
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_APPLAYER_H
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#ifndef DESTRUM_ASSETDATABASE_H
|
||||||
|
#define DESTRUM_ASSETDATABASE_H
|
||||||
|
|
||||||
|
#include <compare>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
#include <destrum/Singleton.h>
|
||||||
|
|
||||||
|
struct AssetGuid {
|
||||||
|
std::uint64_t high{0};
|
||||||
|
std::uint64_t low{0};
|
||||||
|
|
||||||
|
[[nodiscard]] static AssetGuid Generate();
|
||||||
|
[[nodiscard]] static std::optional<AssetGuid> Parse(std::string_view text);
|
||||||
|
[[nodiscard]] std::string ToString() const;
|
||||||
|
[[nodiscard]] bool IsValid() const { return high != 0 || low != 0; }
|
||||||
|
|
||||||
|
auto operator<=>(const AssetGuid&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct std::hash<AssetGuid> {
|
||||||
|
std::size_t operator()(const AssetGuid& guid) const noexcept
|
||||||
|
{
|
||||||
|
return std::hash<std::uint64_t>{}(guid.high ^ (guid.low * 0x9E3779B97F4A7C15ull));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetDatabase final : public Singleton<AssetDatabase> {
|
||||||
|
public:
|
||||||
|
class MetaFileScope {
|
||||||
|
public:
|
||||||
|
MetaFileScope();
|
||||||
|
~MetaFileScope();
|
||||||
|
MetaFileScope(const MetaFileScope&) = delete;
|
||||||
|
MetaFileScope& operator=(const MetaFileScope&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Refresh();
|
||||||
|
void Clear();
|
||||||
|
void Register(std::string_view path, const AssetGuid& guid);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<AssetGuid> FindGuid(std::string_view path) const;
|
||||||
|
[[nodiscard]] std::optional<std::string> FindPath(const AssetGuid& guid) const;
|
||||||
|
std::optional<AssetGuid> GuidForSave(std::string_view path);
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json WriteReference(std::string_view path, std::string_view subresource = {});
|
||||||
|
[[nodiscard]] std::optional<AssetReference> ReadReference(const nlohmann::json& reference) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, AssetGuid> m_Guids;
|
||||||
|
std::unordered_map<AssetGuid, std::string> m_Paths;
|
||||||
|
int m_MetaFileScopes{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_ASSETDATABASE_H
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef DESTRUM_ASSETTYPES_H
|
||||||
|
#define DESTRUM_ASSETTYPES_H
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace AssetTypes {
|
||||||
|
inline constexpr std::string_view MetaExtension = ".meta";
|
||||||
|
inline constexpr std::string_view MaterialExtension = ".dmat";
|
||||||
|
inline constexpr std::string_view SceneExtension = ".scene";
|
||||||
|
inline constexpr std::string_view PrefabExtension = ".prefab";
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string Extension(std::string_view path)
|
||||||
|
{
|
||||||
|
std::string extension = std::filesystem::path{path}.extension().string();
|
||||||
|
std::ranges::transform(extension, extension.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline bool HasExtension(std::string_view path, std::initializer_list<std::string_view> extensions)
|
||||||
|
{
|
||||||
|
const std::string extension = Extension(path);
|
||||||
|
return std::ranges::find(extensions, extension) != extensions.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline bool IsModel(std::string_view path) { return HasExtension(path, {".fbx", ".glb", ".gltf", ".obj"}); }
|
||||||
|
[[nodiscard]] inline bool IsImage(std::string_view path) { return HasExtension(path, {".png", ".jpg", ".jpeg", ".tga", ".bmp"}); }
|
||||||
|
[[nodiscard]] inline bool IsAudio(std::string_view path) { return HasExtension(path, {".wav", ".ogg"}); }
|
||||||
|
[[nodiscard]] inline bool IsMaterial(std::string_view path) { return HasExtension(path, {MaterialExtension}); }
|
||||||
|
[[nodiscard]] inline bool IsScene(std::string_view path) { return HasExtension(path, {SceneExtension}); }
|
||||||
|
[[nodiscard]] inline bool IsPrefab(std::string_view path) { return HasExtension(path, {PrefabExtension}); }
|
||||||
|
[[nodiscard]] inline bool IsMeta(std::string_view path) { return HasExtension(path, {MetaExtension}); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_ASSETTYPES_H
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#ifndef DESTRUM_PACKFILE_H
|
||||||
|
#define DESTRUM_PACKFILE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace PackFile {
|
||||||
|
inline constexpr std::uint32_t Version = 1;
|
||||||
|
inline constexpr std::string_view Extension = ".dpak";
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::string path;
|
||||||
|
std::uint64_t offset{0};
|
||||||
|
std::uint64_t size{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Source {
|
||||||
|
std::string path;
|
||||||
|
std::filesystem::path file;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Paths are "<scheme>/<relative path>", e.g. "engine/shaders/mesh.vert.spv"
|
||||||
|
void Write(const std::vector<Source>& files, const std::filesystem::path& path);
|
||||||
|
|
||||||
|
class Reader {
|
||||||
|
public:
|
||||||
|
explicit Reader(std::filesystem::path path);
|
||||||
|
|
||||||
|
[[nodiscard]] const Entry* Find(std::string_view path) const;
|
||||||
|
[[nodiscard]] std::vector<std::uint8_t> Read(const Entry& entry) const;
|
||||||
|
[[nodiscard]] const std::vector<Entry>& Entries() const { return m_Entries; }
|
||||||
|
[[nodiscard]] const std::filesystem::path& Path() const { return m_Path; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path m_Path;
|
||||||
|
std::vector<Entry> m_Entries;
|
||||||
|
std::unordered_map<std::string, std::size_t> m_ByPath;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_PACKFILE_H
|
||||||
@@ -14,6 +14,7 @@ namespace TextureFile {
|
|||||||
|
|
||||||
enum class PixelFormat : std::uint32_t {
|
enum class PixelFormat : std::uint32_t {
|
||||||
RGBA16Float,
|
RGBA16Float,
|
||||||
|
RGBA8,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Texture {
|
struct Texture {
|
||||||
@@ -26,6 +27,7 @@ namespace TextureFile {
|
|||||||
[[nodiscard]] std::size_t BytesPerPixel(PixelFormat format);
|
[[nodiscard]] std::size_t BytesPerPixel(PixelFormat format);
|
||||||
|
|
||||||
[[nodiscard]] Texture FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba);
|
[[nodiscard]] Texture FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba);
|
||||||
|
[[nodiscard]] Texture FromRGBA8(std::uint32_t width, std::uint32_t height, std::span<const std::uint8_t> rgba);
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::uint8_t> Write(const Texture& texture);
|
[[nodiscard]] std::vector<std::uint8_t> Write(const Texture& texture);
|
||||||
[[nodiscard]] Texture Read(std::span<const std::uint8_t> bytes);
|
[[nodiscard]] Texture Read(std::span<const std::uint8_t> bytes);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef DESTRUM_AUDIODECODER_H
|
||||||
|
#define DESTRUM_AUDIODECODER_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct AudioData {
|
||||||
|
int channels{0};
|
||||||
|
int sampleRate{0};
|
||||||
|
std::vector<std::int16_t> samples;
|
||||||
|
|
||||||
|
[[nodiscard]] float Duration() const
|
||||||
|
{
|
||||||
|
if (channels <= 0 || sampleRate <= 0) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
return static_cast<float>(samples.size() / static_cast<std::size_t>(channels)) / static_cast<float>(sampleRate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace AudioDecoder {
|
||||||
|
[[nodiscard]] std::optional<AudioData> DecodeWav(std::span<const std::uint8_t> bytes);
|
||||||
|
[[nodiscard]] std::optional<AudioData> DecodeOgg(std::span<const std::uint8_t> bytes);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<AudioData> Decode(std::span<const std::uint8_t> bytes, std::string_view path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_AUDIODECODER_H
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#ifndef DESTRUM_AUDIOSYSTEM_H
|
||||||
|
#define DESTRUM_AUDIOSYSTEM_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Singleton.h>
|
||||||
|
|
||||||
|
struct AudioData;
|
||||||
|
struct ALCdevice;
|
||||||
|
struct ALCcontext;
|
||||||
|
class Camera;
|
||||||
|
|
||||||
|
class AudioClip {
|
||||||
|
public:
|
||||||
|
AudioClip(std::string path, std::uint32_t buffer, int channels, int sampleRate, float duration);
|
||||||
|
~AudioClip();
|
||||||
|
|
||||||
|
AudioClip(const AudioClip&) = delete;
|
||||||
|
AudioClip& operator=(const AudioClip&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string& GetPath() const { return m_Path; }
|
||||||
|
[[nodiscard]] std::uint32_t GetBuffer() const { return m_Buffer; }
|
||||||
|
[[nodiscard]] int GetChannels() const { return m_Channels; }
|
||||||
|
[[nodiscard]] int GetSampleRate() const { return m_SampleRate; }
|
||||||
|
[[nodiscard]] float GetDuration() const { return m_Duration; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_Path;
|
||||||
|
std::uint32_t m_Buffer{0};
|
||||||
|
int m_Channels{0};
|
||||||
|
int m_SampleRate{0};
|
||||||
|
float m_Duration{0.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] glm::vec3 SmoothAudioVelocity(const glm::vec3& current, const glm::vec3& previous,
|
||||||
|
const glm::vec3& smoothed, float dt);
|
||||||
|
|
||||||
|
struct OneShotParams {
|
||||||
|
float volume{1.0f};
|
||||||
|
float pitch{1.0f};
|
||||||
|
bool spatial{true};
|
||||||
|
float minDistance{1.0f};
|
||||||
|
float maxDistance{50.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioSystem final : public Singleton<AudioSystem> {
|
||||||
|
public:
|
||||||
|
friend class Singleton<AudioSystem>;
|
||||||
|
|
||||||
|
~AudioSystem() override;
|
||||||
|
|
||||||
|
bool Init();
|
||||||
|
void Shutdown();
|
||||||
|
[[nodiscard]] bool IsAvailable() const { return m_Context != nullptr; }
|
||||||
|
|
||||||
|
std::shared_ptr<const AudioClip> LoadClip(std::string_view vpath);
|
||||||
|
std::shared_ptr<const AudioClip> CreateClip(std::string path, const AudioData& data);
|
||||||
|
void ReleaseUnusedClips();
|
||||||
|
|
||||||
|
std::uint32_t CreateSource();
|
||||||
|
void DestroySource(std::uint32_t source);
|
||||||
|
|
||||||
|
void PlayOneShot(std::string_view vpath, const glm::vec3& position, const OneShotParams& params = {});
|
||||||
|
|
||||||
|
void SetListener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up);
|
||||||
|
|
||||||
|
void Update(float dt, bool paused, const Camera* fallbackListener);
|
||||||
|
|
||||||
|
void SetMasterVolume(float volume);
|
||||||
|
[[nodiscard]] float GetMasterVolume() const { return m_MasterVolume; }
|
||||||
|
[[nodiscard]] bool IsPaused() const { return m_Paused; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioSystem() = default;
|
||||||
|
|
||||||
|
void SetPaused(bool paused);
|
||||||
|
|
||||||
|
struct OneShot {
|
||||||
|
std::uint32_t source{0};
|
||||||
|
std::shared_ptr<const AudioClip> clip;
|
||||||
|
};
|
||||||
|
|
||||||
|
ALCdevice* m_Device{nullptr};
|
||||||
|
ALCcontext* m_Context{nullptr};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<const AudioClip>> m_Clips;
|
||||||
|
std::unordered_set<std::uint32_t> m_Sources;
|
||||||
|
std::vector<std::uint32_t> m_PausedSources;
|
||||||
|
std::vector<OneShot> m_OneShots;
|
||||||
|
|
||||||
|
glm::vec3 m_ListenerPosition{0.0f};
|
||||||
|
glm::vec3 m_ListenerForward{0.0f, 0.0f, -1.0f};
|
||||||
|
glm::vec3 m_ListenerUp{0.0f, 1.0f, 0.0f};
|
||||||
|
glm::vec3 m_LastListenerPosition{0.0f};
|
||||||
|
glm::vec3 m_ListenerVelocity{0.0f};
|
||||||
|
bool m_HasLastListenerPosition{false};
|
||||||
|
bool m_ListenerSetThisFrame{false};
|
||||||
|
|
||||||
|
float m_MasterVolume{1.0f};
|
||||||
|
bool m_Paused{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_AUDIOSYSTEM_H
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef DESTRUM_AUDIOLISTENER_H
|
||||||
|
#define DESTRUM_AUDIOLISTENER_H
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
|
||||||
|
class AudioListener final : public Component {
|
||||||
|
public:
|
||||||
|
explicit AudioListener(GameObject& parent);
|
||||||
|
|
||||||
|
AudioListener(const AudioListener& other) = delete;
|
||||||
|
AudioListener(AudioListener&& other) noexcept = delete;
|
||||||
|
AudioListener& operator=(const AudioListener& other) = delete;
|
||||||
|
AudioListener& operator=(AudioListener&& other) noexcept = delete;
|
||||||
|
|
||||||
|
void LateUpdate(float dt) override;
|
||||||
|
|
||||||
|
std::string GetTypeName() const override { return "AudioListener"; }
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_AUDIOLISTENER_H
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#ifndef DESTRUM_AUDIOSOURCE_H
|
||||||
|
#define DESTRUM_AUDIOSOURCE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
|
||||||
|
class AudioClip;
|
||||||
|
|
||||||
|
class AudioSource final : public Component {
|
||||||
|
public:
|
||||||
|
explicit AudioSource(GameObject& parent, std::string clipPath = {});
|
||||||
|
~AudioSource() override;
|
||||||
|
|
||||||
|
AudioSource(const AudioSource& other) = delete;
|
||||||
|
AudioSource(AudioSource&& other) noexcept = delete;
|
||||||
|
AudioSource& operator=(const AudioSource& other) = delete;
|
||||||
|
AudioSource& operator=(AudioSource&& other) noexcept = delete;
|
||||||
|
|
||||||
|
void Update(float dt) override;
|
||||||
|
void LateUpdate(float dt) override;
|
||||||
|
void SetEnabled(bool enabled) override;
|
||||||
|
|
||||||
|
std::string GetTypeName() const override { return "AudioSource"; }
|
||||||
|
|
||||||
|
void Reflect(FieldVisitor& v) override;
|
||||||
|
nlohmann::json Serialize() const override;
|
||||||
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
void Play();
|
||||||
|
void Pause();
|
||||||
|
void Stop();
|
||||||
|
[[nodiscard]] bool IsPlaying() const;
|
||||||
|
|
||||||
|
void PlayOneShot(const std::string& clipPath, float volumeScale = 1.0f) const;
|
||||||
|
|
||||||
|
void SetClip(const std::string& clipPath);
|
||||||
|
[[nodiscard]] const std::string& GetClip() const { return m_ClipPath; }
|
||||||
|
|
||||||
|
void SetVolume(float volume);
|
||||||
|
[[nodiscard]] float GetVolume() const { return m_Volume; }
|
||||||
|
|
||||||
|
void SetPitch(float pitch);
|
||||||
|
[[nodiscard]] float GetPitch() const { return m_Pitch; }
|
||||||
|
|
||||||
|
void SetLoop(bool loop);
|
||||||
|
[[nodiscard]] bool GetLoop() const { return m_Loop; }
|
||||||
|
|
||||||
|
void SetPlayOnStart(bool playOnStart) { m_PlayOnStart = playOnStart; }
|
||||||
|
[[nodiscard]] bool GetPlayOnStart() const { return m_PlayOnStart; }
|
||||||
|
|
||||||
|
void SetSpatial(bool spatial);
|
||||||
|
[[nodiscard]] bool GetSpatial() const { return m_Spatial; }
|
||||||
|
|
||||||
|
void SetDistances(float minDistance, float maxDistance);
|
||||||
|
[[nodiscard]] float GetMinDistance() const { return m_MinDistance; }
|
||||||
|
[[nodiscard]] float GetMaxDistance() const { return m_MaxDistance; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureSource();
|
||||||
|
void ApplyProperties();
|
||||||
|
void SyncPosition(float dt);
|
||||||
|
void ReleaseSource();
|
||||||
|
|
||||||
|
std::string m_ClipPath;
|
||||||
|
float m_Volume{1.0f};
|
||||||
|
float m_Pitch{1.0f};
|
||||||
|
bool m_Loop{false};
|
||||||
|
bool m_PlayOnStart{true};
|
||||||
|
bool m_Spatial{true};
|
||||||
|
float m_MinDistance{1.0f};
|
||||||
|
float m_MaxDistance{50.0f};
|
||||||
|
|
||||||
|
std::uint32_t m_Source{0};
|
||||||
|
std::shared_ptr<const AudioClip> m_Clip;
|
||||||
|
bool m_AutoPlayed{false};
|
||||||
|
glm::vec3 m_LastPosition{0.0f};
|
||||||
|
glm::vec3 m_Velocity{0.0f};
|
||||||
|
bool m_HasLastPosition{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_AUDIOSOURCE_H
|
||||||
@@ -17,8 +17,7 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "CameraComponent"; }
|
std::string GetTypeName() const override { return "CameraComponent"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
// Places the camera at this object's world transform, looking down its local -Z.
|
// Places the camera at this object's world transform, looking down its local -Z.
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "LightComponent"; }
|
std::string GetTypeName() const override { return "LightComponent"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
void SetType(LightType type) { m_Type = type; }
|
void SetType(LightType type) { m_Type = type; }
|
||||||
[[nodiscard]] LightType GetType() const { return m_Type; }
|
[[nodiscard]] LightType GetType() const { return m_Type; }
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
#include <destrum/ObjectModel/Component.h>
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class GameRenderer;
|
||||||
|
class RenderResources;
|
||||||
|
|
||||||
class MeshRendererComponent final: public Component {
|
class MeshRendererComponent final: public Component {
|
||||||
public:
|
public:
|
||||||
@@ -30,6 +37,18 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void DrawMeshField(RenderResources& resources);
|
||||||
|
void DrawPrimitiveSettings(RenderResources& resources);
|
||||||
|
void DrawMaterialField(RenderResources& resources, GameRenderer& renderer);
|
||||||
|
void DrawMaterialEditor(RenderResources& resources, GameRenderer& renderer);
|
||||||
|
void AssignPrimitive(RenderResources& resources, const PrimitiveMeshes::Desc& desc);
|
||||||
|
void LoadDroppedModel(RenderResources& resources, const std::string& path);
|
||||||
|
|
||||||
|
std::string m_MeshSearch;
|
||||||
|
std::string m_MaterialSearch;
|
||||||
|
std::optional<PrimitiveMeshes::Desc> m_PrimitiveDraft;
|
||||||
|
std::string m_PrimitiveDraftKey;
|
||||||
|
|
||||||
MeshID meshID{NULL_MESH_ID};
|
MeshID meshID{NULL_MESH_ID};
|
||||||
MaterialID materialID{NULL_MATERIAL_ID};
|
MaterialID materialID{NULL_MATERIAL_ID};
|
||||||
std::string meshKey;
|
std::string meshKey;
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "OrbitAndSpin"; }
|
std::string GetTypeName() const override { return "OrbitAndSpin"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
void OnFieldsEdited() override;
|
||||||
|
|
||||||
// optional setters
|
// optional setters
|
||||||
void SetRadius(float r) { m_Radius = r; }
|
void SetRadius(float r) { m_Radius = r; }
|
||||||
@@ -36,6 +37,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void BuildOrbitBasis(); // builds m_U/m_V from m_OrbitAxis
|
void BuildOrbitBasis(); // builds m_U/m_V from m_OrbitAxis
|
||||||
|
void Sanitize();
|
||||||
|
|
||||||
float m_Radius = 5.0f;
|
float m_Radius = 5.0f;
|
||||||
glm::vec3 m_Center{0.0f};
|
glm::vec3 m_Center{0.0f};
|
||||||
|
|||||||
@@ -11,16 +11,13 @@ public:
|
|||||||
|
|
||||||
void Update(float) override {}
|
void Update(float) override {}
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
{
|
{
|
||||||
return "BoxCollider";
|
return "BoxCollider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void DrawDebugLines(const glm::vec4& color) const override;
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
||||||
|
|||||||
@@ -17,24 +17,7 @@ public:
|
|||||||
return "CapsuleCollider";
|
return "CapsuleCollider";
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImGuiInspector() override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override {
|
|
||||||
auto data = SerializeCollider();
|
|
||||||
data["radius"] = m_Radius;
|
|
||||||
data["height"] = m_Height;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Deserialize(const nlohmann::json& data) override {
|
|
||||||
DeserializeCollider(data);
|
|
||||||
if (data.contains("radius")) {
|
|
||||||
SetRadius(data.at("radius").get<float>());
|
|
||||||
}
|
|
||||||
if (data.contains("height")) {
|
|
||||||
SetHeight(data.at("height").get<float>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrawDebugLines(const glm::vec4& color) const override;
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] bool AffectsPhysics() const override { return true; }
|
[[nodiscard]] bool AffectsPhysics() const override { return true; }
|
||||||
|
|
||||||
void ImGuiInspector() override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
|
|
||||||
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
|
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
|
||||||
virtual void DrawDebugLines(const glm::vec4& color) const = 0;
|
virtual void DrawDebugLines(const glm::vec4& color) const = 0;
|
||||||
@@ -30,18 +30,6 @@ public:
|
|||||||
void SetCenterOffset(const glm::vec3& offset) { m_CenterOffset = offset; NotifyPhysicsChanged(); }
|
void SetCenterOffset(const glm::vec3& offset) { m_CenterOffset = offset; NotifyPhysicsChanged(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
[[nodiscard]] nlohmann::json SerializeCollider() const {
|
|
||||||
return {
|
|
||||||
{"centerOffset", m_CenterOffset},
|
|
||||||
{"isTrigger", m_IsTrigger}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void DeserializeCollider(const nlohmann::json& data) {
|
|
||||||
m_CenterOffset = data.value("centerOffset", m_CenterOffset);
|
|
||||||
m_IsTrigger = data.value("isTrigger", m_IsTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool m_IsTrigger{false};
|
bool m_IsTrigger{false};
|
||||||
glm::vec3 m_CenterOffset{0.0f};
|
glm::vec3 m_CenterOffset{0.0f};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
|
||||||
|
class ConvexHullCollider final : public MeshShapeCollider {
|
||||||
|
public:
|
||||||
|
static constexpr int MinVertices = 4;
|
||||||
|
static constexpr int MaxVertices = 256;
|
||||||
|
|
||||||
|
explicit ConvexHullCollider(GameObject& owner, int maxVertices = 64)
|
||||||
|
: MeshShapeCollider(owner)
|
||||||
|
, m_MaxVertices(std::clamp(maxVertices, MinVertices, MaxVertices)) {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "ConvexHullCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reflect(FieldVisitor& v) override;
|
||||||
|
|
||||||
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
||||||
|
return BuildConvexHullShape(m_MaxVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int GetMaxVertices() const {
|
||||||
|
return m_MaxVertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetMaxVertices(int maxVertices) {
|
||||||
|
m_MaxVertices = std::clamp(maxVertices, MinVertices, MaxVertices);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_MaxVertices{64};
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
|
class CylinderCollider final : public Collider {
|
||||||
|
public:
|
||||||
|
explicit CylinderCollider(GameObject& owner, float radius = 0.5f, float height = 1.0f)
|
||||||
|
: Collider(owner)
|
||||||
|
, m_Radius(std::max(radius, 0.0001f))
|
||||||
|
, m_Height(std::max(height, 0.0001f)) {}
|
||||||
|
|
||||||
|
void Update(float) override {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "CylinderCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reflect(FieldVisitor& v) override;
|
||||||
|
|
||||||
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
||||||
|
return PhysicsShapeDesc::Cylinder(m_Radius, m_Height, m_CenterOffset, m_IsTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] float GetRadius() const {
|
||||||
|
return m_Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRadius(float radius) {
|
||||||
|
m_Radius = std::max(radius, 0.0001f);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] float GetHeight() const {
|
||||||
|
return m_Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetHeight(float height) {
|
||||||
|
m_Height = std::max(height, 0.0001f);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_Radius{0.5f};
|
||||||
|
float m_Height{1.0f};
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
|
||||||
|
// Exact triangle mesh for static and kinematic bodies. Dynamic bodies can't use one, they get a convex hull instead.
|
||||||
|
class MeshCollider final : public MeshShapeCollider {
|
||||||
|
public:
|
||||||
|
static constexpr int DynamicHullVertices = 64;
|
||||||
|
|
||||||
|
explicit MeshCollider(GameObject& owner)
|
||||||
|
: MeshShapeCollider(owner) {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "MeshCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool UsesConvexFallback() const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
|
// Base for colliders shaped by the MeshRendererComponent on the same object. Unlike the primitive colliders,
|
||||||
|
// the mesh is in local units, so the transform's world scale is applied.
|
||||||
|
class MeshShapeCollider : public Collider {
|
||||||
|
public:
|
||||||
|
explicit MeshShapeCollider(GameObject& owner)
|
||||||
|
: Collider(owner) {}
|
||||||
|
|
||||||
|
void Update(float) override {}
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
// Used instead of the MeshRenderer mesh, not serialized
|
||||||
|
void SetCustomMesh(std::shared_ptr<const PhysicsMeshData> mesh) {
|
||||||
|
m_CustomMesh = std::move(mesh);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> GetSourceMesh() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] glm::vec3 GetShapeScale() const;
|
||||||
|
[[nodiscard]] glm::mat4 GetShapeMatrix() const;
|
||||||
|
|
||||||
|
// Falls back to a box around the mesh when it has no volume
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildConvexHullShape(int maxVertices) const;
|
||||||
|
void DrawConvexHull(int maxVertices, const glm::vec4& color) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<const PhysicsMeshData> m_CustomMesh;
|
||||||
|
};
|
||||||
@@ -21,8 +21,7 @@ public:
|
|||||||
void Start() override;
|
void Start() override;
|
||||||
void Destroy() override;
|
void Destroy() override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ public:
|
|||||||
|
|
||||||
void Update(float) override {}
|
void Update(float) override {}
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
{
|
{
|
||||||
return "SphereCollider";
|
return "SphereCollider";
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "Rotator"; }
|
std::string GetTypeName() const override { return "Rotator"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
void SetDistance(float distance);
|
void SetDistance(float distance);
|
||||||
void SetSpeed(float speed) { m_Speed = speed; }
|
void SetSpeed(float speed) { m_Speed = speed; }
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "Spinner"; }
|
std::string GetTypeName() const override { return "Spinner"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
void SetAxis(const glm::vec3& axis) {
|
void SetAxis(const glm::vec3& axis) {
|
||||||
m_Axis = glm::dot(axis, axis) > 0.000001f
|
m_Axis = glm::dot(axis, axis) > 0.000001f
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef DESTRUM_CANVAS_H
|
#ifndef DESTRUM_CANVAS_H
|
||||||
#define DESTRUM_CANVAS_H
|
#define DESTRUM_CANVAS_H
|
||||||
|
|
||||||
|
#include <destrum/Components/UI/CanvasFrame.h>
|
||||||
#include <destrum/Components/UI/RectTransform.h>
|
#include <destrum/Components/UI/RectTransform.h>
|
||||||
#include <destrum/ObjectModel/Component.h>
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
|
||||||
@@ -22,10 +23,9 @@ public:
|
|||||||
void Start() override;
|
void Start() override;
|
||||||
void Render(const RenderContext& ctx) override;
|
void Render(const RenderContext& ctx) override;
|
||||||
void Draw(const glm::vec2& screenSize);
|
void Draw(const glm::vec2& screenSize);
|
||||||
|
void Draw(const CanvasFrame& frame);
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
[[nodiscard]] float GetScale(const glm::vec2& screenSize) const;
|
[[nodiscard]] float GetScale(const glm::vec2& screenSize) const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef DESTRUM_CANVASFRAME_H
|
||||||
|
#define DESTRUM_CANVASFRAME_H
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
// Where canvases lay out, in draw image pixels: a resolution sized screen at min, drawn zoom times larger
|
||||||
|
struct CanvasFrame {
|
||||||
|
glm::vec2 min{0.0f};
|
||||||
|
glm::vec2 resolution{0.0f};
|
||||||
|
float zoom{1.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_CANVASFRAME_H
|
||||||
@@ -22,8 +22,7 @@ public:
|
|||||||
|
|
||||||
std::string GetTypeName() const override { return "RectTransform"; }
|
std::string GetTypeName() const override { return "RectTransform"; }
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
[[nodiscard]] UIRect ComputeRect(const UIRect& parentRect, float scale) const;
|
[[nodiscard]] UIRect ComputeRect(const UIRect& parentRect, float scale) const;
|
||||||
@@ -44,6 +43,8 @@ public:
|
|||||||
// Where the canvas last drew this object
|
// Where the canvas last drew this object
|
||||||
[[nodiscard]] const UIRect& GetScreenRect() const { return m_ScreenRect; }
|
[[nodiscard]] const UIRect& GetScreenRect() const { return m_ScreenRect; }
|
||||||
[[nodiscard]] const UIRect& GetParentRect() const { return m_ParentRect; }
|
[[nodiscard]] const UIRect& GetParentRect() const { return m_ParentRect; }
|
||||||
|
// Screen pixels per canvas unit at the last draw
|
||||||
|
[[nodiscard]] float GetScreenScale() const { return m_LastScale; }
|
||||||
void SetScreenRect(const UIRect& rect, const UIRect& parentRect, float scale)
|
void SetScreenRect(const UIRect& rect, const UIRect& parentRect, float scale)
|
||||||
{
|
{
|
||||||
m_ScreenRect = rect;
|
m_ScreenRect = rect;
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ public:
|
|||||||
|
|
||||||
void DrawUI(const UIRect& rect, float scale) override;
|
void DrawUI(const UIRect& rect, float scale) override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
void SetShape(UIImageShape shape) { m_Shape = shape; }
|
void SetShape(UIImageShape shape) { m_Shape = shape; }
|
||||||
void SetOutline(float thickness) { m_Outline = thickness; }
|
void SetOutline(float thickness) { m_Outline = thickness; }
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ public:
|
|||||||
|
|
||||||
void DrawUI(const UIRect& rect, float scale) override;
|
void DrawUI(const UIRect& rect, float scale) override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
|
||||||
|
|
||||||
void SetText(std::string text) { m_Text = std::move(text); }
|
void SetText(std::string text) { m_Text = std::move(text); }
|
||||||
void SetFont(std::string assetPath) { m_FontPath = std::move(assetPath); }
|
void SetFont(std::string assetPath) { m_FontPath = std::move(assetPath); }
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <destrum/Singleton.h>
|
#include <destrum/Singleton.h>
|
||||||
|
#include <destrum/Assets/PackFile.h>
|
||||||
|
|
||||||
#include <destrum/FS/Manifest.h>
|
#include <destrum/FS/Manifest.h>
|
||||||
|
|
||||||
@@ -17,23 +19,42 @@ struct FSMount {
|
|||||||
std::string scheme; // "engine", "game"
|
std::string scheme; // "engine", "game"
|
||||||
std::filesystem::path root;
|
std::filesystem::path root;
|
||||||
std::optional<AssetManifest> manifest;
|
std::optional<AssetManifest> manifest;
|
||||||
|
std::optional<std::filesystem::path> sourceRoot;
|
||||||
|
std::shared_ptr<const PackFile::Reader> pack;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AssetFS final: public Singleton<AssetFS> {
|
class AssetFS final: public Singleton<AssetFS> {
|
||||||
public:
|
public:
|
||||||
|
static constexpr std::string_view PackName = "assets.dpak";
|
||||||
|
|
||||||
void Init(std::filesystem::path exeDir);
|
void Init(std::filesystem::path exeDir);
|
||||||
void Reset();
|
void Reset();
|
||||||
void Mount(std::string scheme, std::filesystem::path root);
|
void Mount(std::string scheme, std::filesystem::path root);
|
||||||
|
void MountPack(const std::filesystem::path& packPath, const std::filesystem::path& root);
|
||||||
|
void ReloadManifest(std::string_view scheme);
|
||||||
|
|
||||||
std::vector<uint8_t> ReadBytes(std::string_view vpath);
|
std::vector<uint8_t> ReadBytes(std::string_view vpath);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath) const;
|
||||||
|
[[nodiscard]] bool Exists(const std::filesystem::path& fullPath) const;
|
||||||
|
[[nodiscard]] std::vector<std::filesystem::path> ListFiles(const FSMount& mount) const;
|
||||||
|
[[nodiscard]] bool IsPacked() const;
|
||||||
|
|
||||||
[[nodiscard]] std::filesystem::path GetFullPath(std::string_view vpath) const;
|
[[nodiscard]] std::filesystem::path GetFullPath(std::string_view vpath) const;
|
||||||
|
|
||||||
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
|
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> ToVirtualPath(const std::filesystem::path& fullPath) const;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<FSMount>& GetMounts() const { return mounts; }
|
||||||
|
|
||||||
|
void SetSourceRoot(std::string_view scheme, std::filesystem::path sourceRoot);
|
||||||
|
[[nodiscard]] std::optional<std::filesystem::path> GetSourcePath(std::string_view vpath) const;
|
||||||
|
|
||||||
|
bool WriteFile(std::string_view vpath, std::string_view contents);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);
|
void AddMount(std::string scheme, std::filesystem::path root, std::shared_ptr<const PackFile::Reader> pack);
|
||||||
|
|
||||||
std::vector<FSMount> mounts;
|
std::vector<FSMount> mounts;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -29,7 +30,7 @@ struct AssetManifest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
namespace FS {
|
namespace FS {
|
||||||
AssetManifest LoadAssetManifest(const std::filesystem::path& manifestPath);
|
AssetManifest ParseAssetManifest(std::span<const std::uint8_t> bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif //MANIFEST_H
|
#endif //MANIFEST_H
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@
|
|||||||
#include <destrum/Graphics/GPUImage.h>
|
#include <destrum/Graphics/GPUImage.h>
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
#include <destrum/Graphics/TextureIntent.h>
|
#include <destrum/Graphics/TextureIntent.h>
|
||||||
|
#include <destrum/Graphics/imageLoader.h>
|
||||||
|
|
||||||
class GfxDevice;
|
class GfxDevice;
|
||||||
|
|
||||||
@@ -18,6 +22,13 @@ class ImageCache {
|
|||||||
friend class ResourcesInspector;
|
friend class ResourcesInspector;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
struct LoadedImageInfo {
|
||||||
|
std::filesystem::path path;
|
||||||
|
TextureIntent intent;
|
||||||
|
VkImageUsageFlags usage;
|
||||||
|
bool mipMap;
|
||||||
|
};
|
||||||
|
|
||||||
explicit ImageCache(GfxDevice& gfxDevice);
|
explicit ImageCache(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
ImageID loadImageFromFile(
|
ImageID loadImageFromFile(
|
||||||
@@ -25,9 +36,19 @@ public:
|
|||||||
VkImageUsageFlags usage,
|
VkImageUsageFlags usage,
|
||||||
bool mipMap, TextureIntent intent = TextureIntent::ColorSrgb);
|
bool mipMap, TextureIntent intent = TextureIntent::ColorSrgb);
|
||||||
|
|
||||||
|
// Reserves the ID now and decodes on a job worker, the slot is filled in by finishPendingLoads
|
||||||
|
ImageID requestImage(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
VkImageUsageFlags usage,
|
||||||
|
bool mipMap, TextureIntent intent = TextureIntent::ColorSrgb);
|
||||||
|
void finishPendingLoads();
|
||||||
|
[[nodiscard]] std::size_t getPendingLoadCount() const { return pendingImages.size(); }
|
||||||
|
|
||||||
ImageID addImage(GPUImage image);
|
ImageID addImage(GPUImage image);
|
||||||
ImageID addImage(ImageID id, GPUImage image);
|
ImageID addImage(ImageID id, GPUImage image);
|
||||||
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
||||||
|
[[nodiscard]] const LoadedImageInfo* findLoadedImageInfo(ImageID id) const;
|
||||||
|
void removeImage(ImageID id);
|
||||||
|
|
||||||
[[nodiscard]] ImageID getFreeImageId() const;
|
[[nodiscard]] ImageID getFreeImageId() const;
|
||||||
[[nodiscard]] std::uint32_t getMaxImageCount() const {
|
[[nodiscard]] std::uint32_t getMaxImageCount() const {
|
||||||
@@ -51,15 +72,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct PendingImage {
|
||||||
|
ImageID id{NULL_IMAGE_ID};
|
||||||
|
std::future<ImageData> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<ImageID> findLoadedImage(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
VkImageUsageFlags usage,
|
||||||
|
bool mipMap,
|
||||||
|
TextureIntent intent) const;
|
||||||
|
ImageID reserveImageId();
|
||||||
|
void finishPendingLoad(ImageID id);
|
||||||
|
void completePendingLoad(PendingImage pending);
|
||||||
|
|
||||||
std::vector<GPUImage> images;
|
std::vector<GPUImage> images;
|
||||||
|
std::vector<PendingImage> pendingImages;
|
||||||
|
std::shared_ptr<std::vector<ImageID>> freeIds = std::make_shared<std::vector<ImageID>>();
|
||||||
GfxDevice& gfxDevice;
|
GfxDevice& gfxDevice;
|
||||||
|
|
||||||
struct LoadedImageInfo {
|
|
||||||
std::filesystem::path path;
|
|
||||||
TextureIntent intent;
|
|
||||||
VkImageUsageFlags usage;
|
|
||||||
bool mipMap;
|
|
||||||
};
|
|
||||||
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
||||||
ImageID errorImageId{NULL_IMAGE_ID};
|
ImageID errorImageId{NULL_IMAGE_ID};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#ifndef MATERIALCACHE_H
|
#ifndef MATERIALCACHE_H
|
||||||
#define MATERIALCACHE_H
|
#define MATERIALCACHE_H
|
||||||
|
|
||||||
#include <vector>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
#include <destrum/Graphics/Material.h>
|
#include <destrum/Graphics/Material.h>
|
||||||
@@ -27,13 +28,16 @@ public:
|
|||||||
|
|
||||||
void cleanup(GfxDevice& gfxDevice);
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
MaterialID addMaterial(Material material);
|
MaterialID addMaterial(Material material, std::string key = {});
|
||||||
|
void removeMaterial(MaterialID id);
|
||||||
|
[[nodiscard]] bool isValid(MaterialID id) const { return id < materialKeys.size() && !materialKeys[id].empty(); }
|
||||||
MaterialID addSimpleColorMaterial(
|
MaterialID addSimpleColorMaterial(
|
||||||
glm::vec3 color,
|
glm::vec3 color,
|
||||||
std::string name = {});
|
std::string name = {});
|
||||||
MaterialID addSimpleTextureMaterial(ImageID textureID);
|
MaterialID addSimpleTextureMaterial(ImageID textureID);
|
||||||
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
|
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
|
||||||
[[nodiscard]] const std::string& getMaterialKey(MaterialID id) const;
|
[[nodiscard]] const std::string& getMaterialKey(MaterialID id) const;
|
||||||
|
[[nodiscard]] std::size_t getMaterialCount() const { return materials.size(); }
|
||||||
[[nodiscard]] std::optional<MaterialID> findMaterialByKey(std::string_view key) const;
|
[[nodiscard]] std::optional<MaterialID> findMaterialByKey(std::string_view key) const;
|
||||||
[[nodiscard]] std::optional<MaterialID> findMaterialByName(
|
[[nodiscard]] std::optional<MaterialID> findMaterialByName(
|
||||||
std::string_view name,
|
std::string_view name,
|
||||||
@@ -47,13 +51,14 @@ public:
|
|||||||
|
|
||||||
|
|
||||||
Material& getMaterialMutable(MaterialID id);
|
Material& getMaterialMutable(MaterialID id);
|
||||||
void updateMaterialGPU(MaterialID id);
|
[[nodiscard]] MaterialData getMaterialData(MaterialID id) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] MaterialData toMaterialData(const Material& material) const;
|
[[nodiscard]] MaterialData toMaterialData(const Material& material) const;
|
||||||
|
|
||||||
std::vector<Material> materials;
|
std::vector<Material> materials;
|
||||||
std::vector<std::string> materialKeys;
|
std::vector<std::string> materialKeys;
|
||||||
|
std::shared_ptr<std::vector<MaterialID>> freeIds = std::make_shared<std::vector<MaterialID>>();
|
||||||
|
|
||||||
static constexpr auto MAX_MATERIALS = 1000;
|
static constexpr auto MAX_MATERIALS = 1000;
|
||||||
GPUBuffer materialDataBuffer;
|
GPUBuffer materialDataBuffer;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/Resources/Mesh.h>
|
#include <destrum/Graphics/Resources/Mesh.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "../ids.h"
|
#include "../ids.h"
|
||||||
|
|
||||||
@@ -15,17 +17,44 @@ public:
|
|||||||
void cleanup(GfxDevice& gfxDevice);
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
||||||
|
void removeMesh(GfxDevice& gfxDevice, MeshID id);
|
||||||
|
[[nodiscard]] bool isValid(MeshID id) const { return id < meshKeys.size() && !meshKeys[id].empty(); }
|
||||||
|
[[nodiscard]] std::size_t getSlotCount() const { return meshes.size(); }
|
||||||
const GPUMesh& getMesh(MeshID id) const;
|
const GPUMesh& getMesh(MeshID id) const;
|
||||||
|
[[nodiscard]] const std::vector<GPUMesh>& getMeshes() const { return meshes; }
|
||||||
const CPUMesh& getCPUMesh(MeshID id) const;
|
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||||
[[nodiscard]] const std::string& getMeshKey(MeshID id) const;
|
[[nodiscard]] const std::string& getMeshKey(MeshID id) const;
|
||||||
[[nodiscard]] std::optional<MeshID> findMeshByKey(std::string_view key) const;
|
[[nodiscard]] std::optional<MeshID> findMeshByKey(std::string_view key) const;
|
||||||
|
|
||||||
|
[[nodiscard]] const GPUBuffer& getIndexBuffer() const { return indexBuffer; }
|
||||||
|
[[nodiscard]] std::uint32_t getIndexCount() const { return indexCount; }
|
||||||
|
[[nodiscard]] std::uint32_t getIndexCapacity() const { return indexCapacity; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
|
struct IndexRange {
|
||||||
|
std::uint32_t first{0};
|
||||||
|
std::uint32_t count{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RecycleBin {
|
||||||
|
std::vector<MeshID> ids;
|
||||||
|
std::vector<IndexRange> indexRanges;
|
||||||
|
};
|
||||||
|
|
||||||
|
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh);
|
||||||
|
void ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices);
|
||||||
|
[[nodiscard]] std::uint32_t allocateIndices(GfxDevice& gfxDevice, std::uint32_t count);
|
||||||
|
|
||||||
|
static constexpr std::uint32_t InitialIndexCapacity = 1u << 20;
|
||||||
|
|
||||||
|
GPUBuffer indexBuffer;
|
||||||
|
std::uint32_t indexCount{0};
|
||||||
|
std::uint32_t indexCapacity{0};
|
||||||
|
|
||||||
std::vector<GPUMesh> meshes;
|
std::vector<GPUMesh> meshes;
|
||||||
std::vector<CPUMesh> cpuMeshes;
|
std::vector<CPUMesh> cpuMeshes;
|
||||||
std::vector<std::string> meshKeys;
|
std::vector<std::string> meshKeys;
|
||||||
|
std::shared_ptr<RecycleBin> recycleBin = std::make_shared<RecycleBin>();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //MESHCACHE_H
|
#endif //MESHCACHE_H
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace DebugDraw {
|
namespace DebugDraw {
|
||||||
void Arc(const glm::vec3& center, const glm::vec3& axisA, const glm::vec3& axisB, float radius,
|
void Arc(const glm::vec3& center, const glm::vec3& axisA, const glm::vec3& axisB, float radius,
|
||||||
float fromAngle, float toAngle, const glm::vec4& color, int segments = 16);
|
float fromAngle, float toAngle, const glm::vec4& color, int segments = 16);
|
||||||
@@ -14,6 +17,12 @@ namespace DebugDraw {
|
|||||||
void Sphere(const glm::vec3& center, float radius, const glm::vec4& color);
|
void Sphere(const glm::vec3& center, float radius, const glm::vec4& color);
|
||||||
|
|
||||||
void Capsule(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color);
|
void Capsule(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color);
|
||||||
|
void Cylinder(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color);
|
||||||
|
|
||||||
|
// Wireframe of an indexed triangle list, stops after maxTriangles
|
||||||
|
void Triangles(const glm::mat4& transform, const std::vector<glm::vec3>& points,
|
||||||
|
const std::vector<std::uint32_t>& indices, const glm::vec4& color,
|
||||||
|
std::size_t maxTriangles = 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // DEBUGDRAW_H
|
#endif // DEBUGDRAW_H
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef DELETIONQUEUE_H
|
||||||
|
#define DELETIONQUEUE_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class DeletionQueue {
|
||||||
|
public:
|
||||||
|
void push(std::uint64_t retiredInFrame, std::function<void()>&& deleter);
|
||||||
|
|
||||||
|
void flush(std::uint64_t lastCompletedFrame);
|
||||||
|
|
||||||
|
void flushAll();
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t size() const { return entries.size(); }
|
||||||
|
[[nodiscard]] bool empty() const { return entries.empty(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
std::uint64_t frame;
|
||||||
|
std::function<void()> deleter;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::deque<Entry> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DELETIONQUEUE_H
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef FRAMECONFIG_H
|
||||||
|
#define FRAMECONFIG_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
inline constexpr std::uint32_t FRAMES_IN_FLIGHT = 2;
|
||||||
|
|
||||||
|
#endif // FRAMECONFIG_H
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <glm/vec2.hpp>
|
#include <glm/vec2.hpp>
|
||||||
#include <glm/vec4.hpp>
|
#include <glm/vec4.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/DeletionQueue.h>
|
||||||
#include <destrum/Graphics/GPUImage.h>
|
#include <destrum/Graphics/GPUImage.h>
|
||||||
#include <destrum/Graphics/Swapchain.h>
|
#include <destrum/Graphics/Swapchain.h>
|
||||||
#include <destrum/Graphics/Resources/Buffer.h>
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
@@ -49,8 +50,8 @@ public:
|
|||||||
struct EndFrameProps
|
struct EndFrameProps
|
||||||
{
|
{
|
||||||
VkClearColorValue clearColor{{0.f, 0.f, 0.f, 1.f}};
|
VkClearColorValue clearColor{{0.f, 0.f, 0.f, 1.f}};
|
||||||
glm::ivec4 drawImageBlitRect{};
|
|
||||||
ImguiPass* imguiPass{nullptr};
|
ImguiPass* imguiPass{nullptr};
|
||||||
|
bool copyDrawImageToSwapchain{true};
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -113,7 +114,8 @@ public:
|
|||||||
return memoryManager.createBuffer(allocSize, usage, memoryUsage);
|
return memoryManager.createBuffer(allocSize, usage, memoryUsage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void destroyBuffer(GPUBuffer& buffer) const
|
void destroyBuffer(GPUBuffer& buffer);
|
||||||
|
void destroyBufferNow(GPUBuffer& buffer) const
|
||||||
{
|
{
|
||||||
memoryManager.destroyBuffer(buffer);
|
memoryManager.destroyBuffer(buffer);
|
||||||
}
|
}
|
||||||
@@ -139,6 +141,15 @@ public:
|
|||||||
return imageManager.loadImageFromFile(path, usage, mipMap, intent);
|
return imageManager.loadImageFromFile(path, usage, mipMap, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<GPUImage> createImageFromData(
|
||||||
|
const ImageData& data,
|
||||||
|
VkImageUsageFlags usage,
|
||||||
|
bool mipMap,
|
||||||
|
const std::string& debugName) const
|
||||||
|
{
|
||||||
|
return imageManager.createImageFromData(data, usage, mipMap, debugName);
|
||||||
|
}
|
||||||
|
|
||||||
void uploadImageDataSized(
|
void uploadImageDataSized(
|
||||||
const GPUImage& image,
|
const GPUImage& image,
|
||||||
const void* pixelData,
|
const void* pixelData,
|
||||||
@@ -148,11 +159,24 @@ public:
|
|||||||
imageManager.uploadImageData(image, pixelData, byteSize, layer);
|
imageManager.uploadImageData(image, pixelData, byteSize, layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void destroyImage(GPUImage& image) const
|
void destroyImage(GPUImage& image);
|
||||||
|
void destroyImageNow(GPUImage& image) const
|
||||||
{
|
{
|
||||||
imageManager.destroyImage(image);
|
imageManager.destroyImage(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void deferDestroy(std::function<void()>&& deleter);
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t getFrameNumber() const
|
||||||
|
{
|
||||||
|
return frameManager.getFrameNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t getPendingDeletionCount() const
|
||||||
|
{
|
||||||
|
return deletionQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] VkDevice getVkDevice() const
|
[[nodiscard]] VkDevice getVkDevice() const
|
||||||
{
|
{
|
||||||
return instanceManager.getDevice();
|
return instanceManager.getDevice();
|
||||||
@@ -228,6 +252,7 @@ private:
|
|||||||
ImageManager imageManager;
|
ImageManager imageManager;
|
||||||
|
|
||||||
VulkanImmediateExecutor executor;
|
VulkanImmediateExecutor executor;
|
||||||
|
DeletionQueue deletionQueue;
|
||||||
|
|
||||||
Swapchain swapchain;
|
Swapchain swapchain;
|
||||||
VkFormat swapchainFormat{VK_FORMAT_UNDEFINED};
|
VkFormat swapchainFormat{VK_FORMAT_UNDEFINED};
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#ifndef INDIRECTDRAWLIST_H
|
||||||
|
#define INDIRECTDRAWLIST_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/mat4x4.hpp>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/FrameConfig.h>
|
||||||
|
#include <destrum/Graphics/Frustum.h>
|
||||||
|
#include <destrum/Graphics/MeshDrawCommand.h>
|
||||||
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
|
|
||||||
|
class GfxDevice;
|
||||||
|
struct GPUMesh;
|
||||||
|
|
||||||
|
// Must match InstanceData in instance_data.glsl
|
||||||
|
struct GPUInstanceData {
|
||||||
|
glm::mat4 transform;
|
||||||
|
VkDeviceAddress vertexBuffer;
|
||||||
|
std::uint32_t materialId;
|
||||||
|
std::uint32_t objectId;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GPUInstanceData) == 80);
|
||||||
|
|
||||||
|
class IndirectDrawList {
|
||||||
|
public:
|
||||||
|
struct View {
|
||||||
|
std::uint32_t firstCommand{0};
|
||||||
|
std::uint32_t commandCount{0};
|
||||||
|
std::uint32_t instanceCount{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Filter {
|
||||||
|
All,
|
||||||
|
ShadowCasters,
|
||||||
|
};
|
||||||
|
|
||||||
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
|
void begin(std::uint32_t frameIndex);
|
||||||
|
|
||||||
|
View addView(
|
||||||
|
const std::vector<GPUMesh>& meshes,
|
||||||
|
const std::vector<MeshDrawCommand>& drawCommands,
|
||||||
|
const std::vector<std::size_t>& sortedDrawCommands,
|
||||||
|
const Frustum& frustum,
|
||||||
|
Filter filter);
|
||||||
|
|
||||||
|
void upload(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
|
void draw(VkCommandBuffer cmd, const GPUBuffer& indexBuffer, const View& view) const;
|
||||||
|
|
||||||
|
[[nodiscard]] VkDeviceAddress getInstanceBufferAddress() const;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<GPUInstanceData>& getInstances() const { return instances; }
|
||||||
|
[[nodiscard]] const std::vector<VkDrawIndexedIndirectCommand>& getCommands() const { return commands; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PerFrameBuffers {
|
||||||
|
GPUBuffer instances;
|
||||||
|
GPUBuffer commands;
|
||||||
|
std::size_t instanceCapacity{0};
|
||||||
|
std::size_t commandCapacity{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::size_t InitialInstanceCapacity = 1024;
|
||||||
|
static constexpr std::size_t InitialCommandCapacity = 256;
|
||||||
|
|
||||||
|
std::array<PerFrameBuffers, FRAMES_IN_FLIGHT> frames{};
|
||||||
|
std::uint32_t currentFrame{0};
|
||||||
|
|
||||||
|
std::vector<GPUInstanceData> instances;
|
||||||
|
std::vector<VkDrawIndexedIndirectCommand> commands;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INDIRECTDRAWLIST_H
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/FrameConfig.h>
|
||||||
|
|
||||||
class FrameManager {
|
class FrameManager {
|
||||||
public:
|
public:
|
||||||
struct FrameData {
|
struct FrameData {
|
||||||
@@ -27,10 +29,12 @@ public:
|
|||||||
[[nodiscard]] VkCommandBuffer beginFrame();
|
[[nodiscard]] VkCommandBuffer beginFrame();
|
||||||
void endFrame(VkCommandBuffer cmd);
|
void endFrame(VkCommandBuffer cmd);
|
||||||
|
|
||||||
void waitIdle() const;
|
|
||||||
|
|
||||||
[[nodiscard]] std::uint32_t getCurrentFrameIndex() const {
|
[[nodiscard]] std::uint32_t getCurrentFrameIndex() const {
|
||||||
return frameNumber % 2;
|
return static_cast<std::uint32_t>(frameNumber % FRAMES_IN_FLIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t getFrameNumber() const {
|
||||||
|
return frameNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] FrameData& getCurrentFrame() {
|
[[nodiscard]] FrameData& getCurrentFrame() {
|
||||||
@@ -48,9 +52,8 @@ public:
|
|||||||
void nextFrame();
|
void nextFrame();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t FRAMES_IN_FLIGHT = 2;
|
|
||||||
std::array<FrameData, FRAMES_IN_FLIGHT> frames{};
|
std::array<FrameData, FRAMES_IN_FLIGHT> frames{};
|
||||||
std::uint32_t frameNumber{0};
|
std::uint64_t frameNumber{0};
|
||||||
VkDevice device{VK_NULL_HANDLE};
|
VkDevice device{VK_NULL_HANDLE};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
class VulkanImmediateExecutor;
|
class VulkanImmediateExecutor;
|
||||||
class MemoryManager;
|
class MemoryManager;
|
||||||
|
struct ImageData;
|
||||||
|
|
||||||
class ImageManager {
|
class ImageManager {
|
||||||
public:
|
public:
|
||||||
@@ -41,6 +42,12 @@ public:
|
|||||||
bool mipMap,
|
bool mipMap,
|
||||||
TextureIntent intent) const;
|
TextureIntent intent) const;
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<GPUImage> createImageFromData(
|
||||||
|
const ImageData& data,
|
||||||
|
VkImageUsageFlags usage,
|
||||||
|
bool mipMap,
|
||||||
|
const std::string& debugName) const;
|
||||||
|
|
||||||
void uploadImageData(
|
void uploadImageData(
|
||||||
const GPUImage& image,
|
const GPUImage& image,
|
||||||
const void* pixelData,
|
const void* pixelData,
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public:
|
|||||||
VkDeviceSize offset = 0,
|
VkDeviceSize offset = 0,
|
||||||
VkDeviceSize size = VK_WHOLE_SIZE) const;
|
VkDeviceSize size = VK_WHOLE_SIZE) const;
|
||||||
|
|
||||||
|
void invalidateAllocation(
|
||||||
|
const GPUBuffer& buffer,
|
||||||
|
VkDeviceSize offset = 0,
|
||||||
|
VkDeviceSize size = VK_WHOLE_SIZE) const;
|
||||||
|
|
||||||
[[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const;
|
[[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public:
|
|||||||
VkPhysicalDeviceFeatures device{
|
VkPhysicalDeviceFeatures device{
|
||||||
.imageCubeArray = VK_TRUE,
|
.imageCubeArray = VK_TRUE,
|
||||||
.geometryShader = VK_TRUE,
|
.geometryShader = VK_TRUE,
|
||||||
|
.multiDrawIndirect = VK_TRUE,
|
||||||
|
.drawIndirectFirstInstance = VK_TRUE,
|
||||||
.depthClamp = VK_TRUE,
|
.depthClamp = VK_TRUE,
|
||||||
.fillModeNonSolid = VK_TRUE,
|
.fillModeNonSolid = VK_TRUE,
|
||||||
.samplerAnisotropy = VK_TRUE,
|
.samplerAnisotropy = VK_TRUE,
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ enum class TextureFilteringMode : std::uint32_t {
|
|||||||
Anisotropic,
|
Anisotropic,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class BlendMode : std::uint32_t {
|
||||||
|
Opaque,
|
||||||
|
Transparent,
|
||||||
|
};
|
||||||
|
|
||||||
// Metallic / roughness workflow, matching glTF:
|
// Metallic / roughness workflow, matching glTF:
|
||||||
// metallic is read from the blue channel, roughness from green and occlusion from red,
|
// metallic is read from the blue channel, roughness from green and occlusion from red,
|
||||||
// so a packed (occlusion)metallicRoughness texture can be used for all three slots.
|
// so a packed (occlusion)metallicRoughness texture can be used for all three slots.
|
||||||
@@ -59,6 +64,9 @@ struct Material {
|
|||||||
float normalScale{1.f};
|
float normalScale{1.f};
|
||||||
float occlusionStrength{1.f};
|
float occlusionStrength{1.f};
|
||||||
|
|
||||||
|
BlendMode blendMode{BlendMode::Opaque};
|
||||||
|
float opacity{1.f};
|
||||||
|
|
||||||
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
||||||
|
|
||||||
ImageID diffuseTexture{NULL_IMAGE_ID};
|
ImageID diffuseTexture{NULL_IMAGE_ID};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ struct MeshDrawCommand {
|
|||||||
// skinned meshes only
|
// skinned meshes only
|
||||||
const SkinnedMesh* skinnedMesh{nullptr};
|
const SkinnedMesh* skinnedMesh{nullptr};
|
||||||
std::uint32_t jointMatricesStartIndex{0};
|
std::uint32_t jointMatricesStartIndex{0};
|
||||||
|
|
||||||
|
std::uint32_t objectId{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //MESHDRAWCOMMAND_H
|
#endif //MESHDRAWCOMMAND_H
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
#include <destrum/Graphics/Pipeline.h>
|
#include <destrum/Graphics/Pipeline.h>
|
||||||
#include <destrum/Graphics/Camera.h>
|
#include <destrum/Graphics/IndirectDrawList.h>
|
||||||
#include <destrum/Graphics/Resources/Buffer.h>
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
#include <destrum/Graphics/MeshDrawCommand.h>
|
|
||||||
|
|
||||||
class GfxDevice;
|
class GfxDevice;
|
||||||
class RenderResources;
|
class RenderResources;
|
||||||
@@ -25,25 +24,33 @@ public:
|
|||||||
GfxDevice& gfxDevice,
|
GfxDevice& gfxDevice,
|
||||||
RenderResources& resources,
|
RenderResources& resources,
|
||||||
VkFormat drawImageFormat,
|
VkFormat drawImageFormat,
|
||||||
VkFormat depthImageFormat);
|
VkFormat depthImageFormat,
|
||||||
|
VkFormat selectionImageFormat);
|
||||||
|
|
||||||
void drawDepthPrepass(
|
void drawDepthPrepass(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
VkExtent2D renderExtent,
|
VkExtent2D renderExtent,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const Camera& camera,
|
|
||||||
const GPUBuffer& sceneDataBuffer,
|
const GPUBuffer& sceneDataBuffer,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands);
|
const IndirectDrawList::View& view,
|
||||||
|
bool writeObjectIds);
|
||||||
|
|
||||||
void draw(
|
void draw(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
VkExtent2D renderExtent,
|
VkExtent2D renderExtent,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const Camera& camera,
|
|
||||||
const GPUBuffer& sceneDataBuffer,
|
const GPUBuffer& sceneDataBuffer,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands);
|
const IndirectDrawList::View& view);
|
||||||
|
|
||||||
|
void drawTransparent(
|
||||||
|
VkCommandBuffer cmd,
|
||||||
|
VkExtent2D renderExtent,
|
||||||
|
const RenderResources& resources,
|
||||||
|
const GPUBuffer& sceneDataBuffer,
|
||||||
|
const IndirectDrawList& drawList,
|
||||||
|
const IndirectDrawList::View& view);
|
||||||
|
|
||||||
void cleanup(VkDevice device);
|
void cleanup(VkDevice device);
|
||||||
|
|
||||||
@@ -57,11 +64,8 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
struct PushConstants {
|
struct PushConstants {
|
||||||
glm::mat4 transform;
|
|
||||||
VkDeviceAddress sceneDataBuffer;
|
VkDeviceAddress sceneDataBuffer;
|
||||||
VkDeviceAddress vertexBuffer;
|
VkDeviceAddress instanceBuffer;
|
||||||
std::uint32_t materialId;
|
|
||||||
std::uint32_t padding;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void drawMeshes(
|
void drawMeshes(
|
||||||
@@ -69,15 +73,16 @@ private:
|
|||||||
const Pipeline& pipeline,
|
const Pipeline& pipeline,
|
||||||
VkExtent2D renderExtent,
|
VkExtent2D renderExtent,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const Camera& camera,
|
|
||||||
const GPUBuffer& sceneDataBuffer,
|
const GPUBuffer& sceneDataBuffer,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands);
|
const IndirectDrawList::View& view);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
|
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
|
||||||
std::unique_ptr<Pipeline> m_pipeline;
|
std::unique_ptr<Pipeline> m_pipeline;
|
||||||
|
std::unique_ptr<Pipeline> m_transparentPipeline;
|
||||||
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
|
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
|
||||||
|
std::unique_ptr<Pipeline> m_selectionPrepassPipeline;
|
||||||
|
|
||||||
bool m_renderWireframe{false};
|
bool m_renderWireframe{false};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#ifndef DESTRUM_POSTPROCESSPIPELINE_H
|
||||||
|
#define DESTRUM_POSTPROCESSPIPELINE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <glm/vec2.hpp>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Pipeline.h>
|
||||||
|
#include <destrum/Graphics/Tonemapper.h>
|
||||||
|
#include <destrum/Graphics/ids.h>
|
||||||
|
|
||||||
|
class GfxDevice;
|
||||||
|
class RenderResources;
|
||||||
|
|
||||||
|
class PostProcessPipeline final {
|
||||||
|
public:
|
||||||
|
struct Settings {
|
||||||
|
ImageID sceneColor{NULL_IMAGE_ID};
|
||||||
|
std::optional<ImageID> selection;
|
||||||
|
std::optional<glm::ivec2> hoverPixel;
|
||||||
|
Tonemapper tonemapper{Tonemapper::PbrNeutral};
|
||||||
|
float exposure{0.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
void init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat outputFormat);
|
||||||
|
void cleanup(VkDevice device);
|
||||||
|
|
||||||
|
void draw(VkCommandBuffer cmd, const RenderResources& resources, VkExtent2D extent, const Settings& settings) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Must match PostPushConstants in post.frag
|
||||||
|
struct PushConstants {
|
||||||
|
std::uint32_t sceneColorTexture;
|
||||||
|
std::uint32_t selectionTexture;
|
||||||
|
std::uint32_t tonemapper;
|
||||||
|
float exposure;
|
||||||
|
glm::ivec2 hoverPixel;
|
||||||
|
};
|
||||||
|
|
||||||
|
VkPipelineLayout m_PipelineLayout{VK_NULL_HANDLE};
|
||||||
|
std::unique_ptr<Pipeline> m_Pipeline;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_POSTPROCESSPIPELINE_H
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <destrum/Graphics/Light.h>
|
#include <destrum/Graphics/Light.h>
|
||||||
#include <destrum/Graphics/Pipeline.h>
|
#include <destrum/Graphics/Pipeline.h>
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
#include <destrum/Graphics/MeshDrawCommand.h>
|
#include <destrum/Graphics/IndirectDrawList.h>
|
||||||
|
|
||||||
class GfxDevice;
|
class GfxDevice;
|
||||||
struct GPUImage;
|
struct GPUImage;
|
||||||
@@ -32,15 +32,15 @@ public:
|
|||||||
void drawDirectionalShadow(
|
void drawDirectionalShadow(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands,
|
const IndirectDrawList::View& view,
|
||||||
const glm::mat4& lightViewProj);
|
const glm::mat4& lightViewProj);
|
||||||
|
|
||||||
void drawSpotShadows(
|
void drawSpotShadows(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands,
|
const std::vector<IndirectDrawList::View>& views,
|
||||||
const std::vector<glm::mat4>& lightViewProjs);
|
const std::vector<glm::mat4>& lightViewProjs);
|
||||||
|
|
||||||
[[nodiscard]] glm::mat4 calculateDirectionalViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const;
|
[[nodiscard]] glm::mat4 calculateDirectionalViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const;
|
||||||
@@ -57,8 +57,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
struct PushConstants
|
struct PushConstants
|
||||||
{
|
{
|
||||||
glm::mat4 modelToLightClip;
|
glm::mat4 lightViewProj;
|
||||||
VkDeviceAddress vertexBuffer;
|
VkDeviceAddress instanceBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
void beginDepthRendering(VkCommandBuffer cmd, const RenderResources& resources, const GPUImage& depthImage);
|
void beginDepthRendering(VkCommandBuffer cmd, const RenderResources& resources, const GPUImage& depthImage);
|
||||||
@@ -67,8 +67,8 @@ private:
|
|||||||
void drawShadowCasters(
|
void drawShadowCasters(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const std::vector<MeshDrawCommand>& drawCommands,
|
const IndirectDrawList& drawList,
|
||||||
const std::vector<std::size_t>& sortedDrawCommands,
|
const IndirectDrawList::View& view,
|
||||||
const glm::mat4& lightViewProj);
|
const glm::mat4& lightViewProj);
|
||||||
|
|
||||||
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
|
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
#ifndef RENDERCONTEXT_H
|
#ifndef RENDERCONTEXT_H
|
||||||
#define RENDERCONTEXT_H
|
#define RENDERCONTEXT_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <destrum/Components/UI/CanvasFrame.h>
|
||||||
#include <destrum/Graphics/Renderer.h>
|
#include <destrum/Graphics/Renderer.h>
|
||||||
|
|
||||||
struct RenderContext {
|
struct RenderContext {
|
||||||
GameRenderer& renderer;
|
GameRenderer& renderer;
|
||||||
const Camera& camera;
|
const Camera& camera;
|
||||||
const GameRenderer::SceneData& sceneData;
|
const GameRenderer::SceneData& sceneData;
|
||||||
|
// Unset: canvases fill the draw image
|
||||||
|
std::optional<CanvasFrame> canvasFrame{};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //RENDERCONTEXT_H
|
#endif //RENDERCONTEXT_H
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace ModelDoc { struct MaterialInfo; }
|
namespace ModelDoc { struct MaterialInfo; }
|
||||||
|
|
||||||
@@ -45,7 +46,24 @@ public:
|
|||||||
bool mipMap = true,
|
bool mipMap = true,
|
||||||
TextureIntent intent = TextureIntent::ColorSrgb);
|
TextureIntent intent = TextureIntent::ColorSrgb);
|
||||||
|
|
||||||
|
ImageID loadMaterialTexture(GfxDevice& gfxDevice, const std::filesystem::path& path, TextureIntent intent);
|
||||||
|
|
||||||
|
// While a scope is open material textures decode on job workers, closing the last one uploads them
|
||||||
|
void beginDeferredTextureLoads();
|
||||||
|
void endDeferredTextureLoads();
|
||||||
|
|
||||||
|
struct ReleaseStats {
|
||||||
|
std::size_t meshes{0};
|
||||||
|
std::size_t materials{0};
|
||||||
|
std::size_t images{0};
|
||||||
|
};
|
||||||
|
ReleaseStats releaseUnused(
|
||||||
|
GfxDevice& gfxDevice,
|
||||||
|
const std::unordered_set<MeshID>& usedMeshes,
|
||||||
|
const std::unordered_set<MaterialID>& usedMaterials);
|
||||||
|
|
||||||
ImageID addImageToCache(GPUImage image);
|
ImageID addImageToCache(GPUImage image);
|
||||||
|
void destroyImage(ImageID id);
|
||||||
|
|
||||||
// Imports a file-backed model and uploads its mesh/material resources.
|
// Imports a file-backed model and uploads its mesh/material resources.
|
||||||
// References are keyed as "source#subresource" in the resource caches.
|
// References are keyed as "source#subresource" in the resource caches.
|
||||||
@@ -85,9 +103,26 @@ private:
|
|||||||
std::unique_ptr<MeshCache> meshCache;
|
std::unique_ptr<MeshCache> meshCache;
|
||||||
std::unique_ptr<MaterialCache> materialCache;
|
std::unique_ptr<MaterialCache> materialCache;
|
||||||
|
|
||||||
|
std::unordered_set<ImageID> materialTextures;
|
||||||
|
std::unordered_set<ImageID> pinnedImages;
|
||||||
|
|
||||||
ImageID whiteImageId{NULL_IMAGE_ID};
|
ImageID whiteImageId{NULL_IMAGE_ID};
|
||||||
ImageID errorImageId{NULL_IMAGE_ID};
|
ImageID errorImageId{NULL_IMAGE_ID};
|
||||||
ImageID defaultNormalImageId{NULL_IMAGE_ID};
|
ImageID defaultNormalImageId{NULL_IMAGE_ID};
|
||||||
|
|
||||||
|
int deferredTextureLoads{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeferredTextureLoadScope {
|
||||||
|
public:
|
||||||
|
explicit DeferredTextureLoadScope(RenderResources* resources);
|
||||||
|
~DeferredTextureLoadScope();
|
||||||
|
|
||||||
|
DeferredTextureLoadScope(const DeferredTextureLoadScope&) = delete;
|
||||||
|
DeferredTextureLoadScope& operator=(const DeferredTextureLoadScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RenderResources* resources;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2,16 +2,20 @@
|
|||||||
#define RENDERER_H
|
#define RENDERER_H
|
||||||
|
|
||||||
#include <glm/mat4x4.hpp>
|
#include <glm/mat4x4.hpp>
|
||||||
|
#include <glm/vec2.hpp>
|
||||||
#include <glm/vec3.hpp>
|
#include <glm/vec3.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Camera.h>
|
#include <destrum/Graphics/Camera.h>
|
||||||
|
#include <destrum/Graphics/IndirectDrawList.h>
|
||||||
#include <destrum/Graphics/Light.h>
|
#include <destrum/Graphics/Light.h>
|
||||||
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
|
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
|
||||||
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
|
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
|
||||||
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
||||||
|
#include <destrum/Graphics/Pipelines/PostProcessPipeline.h>
|
||||||
#include <destrum/Graphics/Pipelines/Renderer2DPass.h>
|
#include <destrum/Graphics/Pipelines/Renderer2DPass.h>
|
||||||
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
|
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
|
#include <destrum/Graphics/Tonemapper.h>
|
||||||
#include <destrum/Graphics/MeshDrawCommand.h>
|
#include <destrum/Graphics/MeshDrawCommand.h>
|
||||||
#include <destrum/Graphics/Resources/NBuffer.h>
|
#include <destrum/Graphics/Resources/NBuffer.h>
|
||||||
#include <destrum/Graphics/RenderResources.h>
|
#include <destrum/Graphics/RenderResources.h>
|
||||||
@@ -20,6 +24,9 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
||||||
@@ -29,6 +36,15 @@
|
|||||||
class GameRenderer
|
class GameRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct DrawStats
|
||||||
|
{
|
||||||
|
std::uint32_t meshDraws{0};
|
||||||
|
std::uint32_t indirectCommands{0};
|
||||||
|
std::uint32_t instances{0};
|
||||||
|
std::uint32_t mainViewCommands{0};
|
||||||
|
std::uint32_t mainViewInstances{0};
|
||||||
|
};
|
||||||
|
|
||||||
struct SceneData
|
struct SceneData
|
||||||
{
|
{
|
||||||
const Camera& camera;
|
const Camera& camera;
|
||||||
@@ -37,6 +53,8 @@ public:
|
|||||||
glm::vec3 fogColor;
|
glm::vec3 fogColor;
|
||||||
float fogDensity;
|
float fogDensity;
|
||||||
float skyboxRotation{0.0f}; // radians around the world up axis
|
float skyboxRotation{0.0f}; // radians around the world up axis
|
||||||
|
float exposure{0.0f}; // EV stops, the scene color is multiplied by 2^exposure
|
||||||
|
Tonemapper tonemapper{Tonemapper::PbrNeutral};
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit GameRenderer();
|
explicit GameRenderer();
|
||||||
@@ -48,24 +66,23 @@ public:
|
|||||||
void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData);
|
void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData);
|
||||||
void cleanup(GfxDevice& gfxDevice);
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
|
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, std::uint32_t objectId = 0);
|
||||||
void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh,
|
void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh,
|
||||||
std::size_t jointMatricesStartIndex);
|
std::size_t jointMatricesStartIndex, std::uint32_t objectId = 0);
|
||||||
void submitLight(const Light& light);
|
void submitLight(const Light& light);
|
||||||
const GPUImage& getDrawImage() const;
|
const GPUImage& getDrawImage() const;
|
||||||
|
|
||||||
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize)
|
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize)
|
||||||
{
|
{
|
||||||
createDrawImage(gfxDevice, newSize, false);
|
createDrawImage(gfxDevice, newSize);
|
||||||
lightCullingPipeline->resize(gfxDevice, newSize);
|
lightCullingPipeline->resize(gfxDevice, newSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
Material& getMaterialMutable(MaterialID id);
|
Material& getMaterialMutable(MaterialID id);
|
||||||
void updateMaterialGPU(MaterialID id);
|
void updateMaterialGPU(MaterialID id);
|
||||||
|
|
||||||
void setSkyboxTexture(ImageID skyboxImageId);
|
void setSkyboxTexture(ImageID imageId);
|
||||||
|
|
||||||
void flushMaterialUpdates(GfxDevice& gfxDevice);
|
|
||||||
|
|
||||||
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const
|
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const
|
||||||
{
|
{
|
||||||
@@ -92,6 +109,43 @@ public:
|
|||||||
return renderLightHeatmap;
|
return renderLightHeatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRenderSelectionView(bool selectionView)
|
||||||
|
{
|
||||||
|
renderSelectionView = selectionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getRenderSelectionView() const
|
||||||
|
{
|
||||||
|
return renderSelectionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectionPassEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
selectionPassEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestSelectionPick(const glm::ivec2& pixel)
|
||||||
|
{
|
||||||
|
requestedPick = pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHoverPixel(std::optional<glm::ivec2> pixel)
|
||||||
|
{
|
||||||
|
hoverPixel = pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHighlightedObjects(std::span<const std::uint32_t> objectIds)
|
||||||
|
{
|
||||||
|
highlightedObjects = {objectIds.begin(), objectIds.end()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reloadShaders(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::uint32_t> takeSelectionPick()
|
||||||
|
{
|
||||||
|
return std::exchange(pickResult, std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
void setShadowDistance(float distance)
|
void setShadowDistance(float distance)
|
||||||
{
|
{
|
||||||
shadowMapPipeline->setShadowDistance(distance);
|
shadowMapPipeline->setShadowDistance(distance);
|
||||||
@@ -107,29 +161,53 @@ public:
|
|||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const DrawStats& getDrawStats() const { return lastDrawStats; }
|
||||||
|
|
||||||
void draw2D(
|
void draw2D(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
GfxDevice& gfxDevice,
|
GfxDevice& gfxDevice,
|
||||||
const glm::mat4& projection);
|
const glm::mat4& projection);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize);
|
||||||
void uploadLights(const Camera& camera);
|
void uploadLights(const Camera& camera);
|
||||||
|
void recordSelectionPick(VkCommandBuffer cmd, std::uint32_t frameIndex);
|
||||||
[[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum);
|
[[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum);
|
||||||
|
|
||||||
RenderResources* resources = nullptr;
|
RenderResources* resources = nullptr;
|
||||||
|
|
||||||
|
void recordMaterialUpdates(VkCommandBuffer cmd);
|
||||||
|
|
||||||
std::vector<MaterialID> pendingMaterialUploads;
|
std::vector<MaterialID> pendingMaterialUploads;
|
||||||
|
|
||||||
|
|
||||||
std::vector<MeshDrawCommand> meshDrawCommands;
|
std::vector<MeshDrawCommand> meshDrawCommands;
|
||||||
std::vector<std::size_t> sortedMeshDrawCommands;
|
std::vector<std::size_t> sortedMeshDrawCommands;
|
||||||
|
std::vector<std::size_t> transparentMeshDrawCommands;
|
||||||
|
IndirectDrawList drawList;
|
||||||
|
DrawStats lastDrawStats;
|
||||||
|
|
||||||
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
static constexpr std::uint32_t HighlightedObjectFlag = 0x80000000u;
|
||||||
|
|
||||||
|
VkFormat sceneColorFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
||||||
|
VkFormat drawImageFormat{VK_FORMAT_R8G8B8A8_SRGB};
|
||||||
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
|
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
|
||||||
|
VkFormat selectionImageFormat{VK_FORMAT_R32_UINT};
|
||||||
|
|
||||||
|
ImageID sceneColorImageId{NULL_IMAGE_ID};
|
||||||
ImageID drawImageId{NULL_IMAGE_ID};
|
ImageID drawImageId{NULL_IMAGE_ID};
|
||||||
|
ImageID skyboxImageId{NULL_IMAGE_ID};
|
||||||
ImageID depthImageId{NULL_IMAGE_ID};
|
ImageID depthImageId{NULL_IMAGE_ID};
|
||||||
|
ImageID selectionImageId{NULL_IMAGE_ID};
|
||||||
|
|
||||||
|
std::array<GPUBuffer, FRAMES_IN_FLIGHT> pickBuffers{};
|
||||||
|
std::array<bool, FRAMES_IN_FLIGHT> pickPending{};
|
||||||
|
std::optional<glm::ivec2> requestedPick;
|
||||||
|
std::optional<std::uint32_t> pickResult;
|
||||||
|
std::optional<glm::ivec2> hoverPixel;
|
||||||
|
std::unordered_set<std::uint32_t> highlightedObjects;
|
||||||
|
bool selectionPassEnabled{false};
|
||||||
|
bool renderSelectionView{false};
|
||||||
|
|
||||||
VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT};
|
VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT};
|
||||||
|
|
||||||
@@ -158,7 +236,7 @@ private:
|
|||||||
|
|
||||||
std::uint32_t shadowMapId;
|
std::uint32_t shadowMapId;
|
||||||
float shadowNormalOffset;
|
float shadowNormalOffset;
|
||||||
std::uint32_t padding;
|
std::uint32_t debugSelectionView;
|
||||||
glm::mat4 lightViewProj;
|
glm::mat4 lightViewProj;
|
||||||
|
|
||||||
std::uint32_t spotShadowAtlasId;
|
std::uint32_t spotShadowAtlasId;
|
||||||
@@ -176,6 +254,7 @@ private:
|
|||||||
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
||||||
std::unique_ptr<LightCullingPipeline> lightCullingPipeline;
|
std::unique_ptr<LightCullingPipeline> lightCullingPipeline;
|
||||||
std::unique_ptr<ShadowMapPipeline> shadowMapPipeline;
|
std::unique_ptr<ShadowMapPipeline> shadowMapPipeline;
|
||||||
|
std::unique_ptr<PostProcessPipeline> postProcessPipeline;
|
||||||
|
|
||||||
std::optional<glm::vec3> shadowLightDirection;
|
std::optional<glm::vec3> shadowLightDirection;
|
||||||
std::vector<Light> submittedLights;
|
std::vector<Light> submittedLights;
|
||||||
|
|||||||
@@ -20,10 +20,11 @@
|
|||||||
|
|
||||||
struct GPUMesh {
|
struct GPUMesh {
|
||||||
GPUBuffer vertexBuffer;
|
GPUBuffer vertexBuffer;
|
||||||
GPUBuffer indexBuffer;
|
|
||||||
|
|
||||||
std::uint32_t numVertices{0};
|
std::uint32_t numVertices{0};
|
||||||
std::uint32_t numIndices{0};
|
std::uint32_t numIndices{0};
|
||||||
|
// offset into MeshCache::getIndexBuffer(), every mesh shares that one index buffer
|
||||||
|
std::uint32_t firstIndex{0};
|
||||||
|
|
||||||
// AABB
|
// AABB
|
||||||
glm::vec3 minPos;
|
glm::vec3 minPos;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef PRIMITIVEMESHES_H
|
#ifndef PRIMITIVEMESHES_H
|
||||||
#define PRIMITIVEMESHES_H
|
#define PRIMITIVEMESHES_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -10,9 +11,36 @@
|
|||||||
#include <destrum/Graphics/Resources/CPUMesh.h>
|
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||||
|
|
||||||
namespace PrimitiveMeshes {
|
namespace PrimitiveMeshes {
|
||||||
|
enum class Shape {
|
||||||
|
Box,
|
||||||
|
Plane,
|
||||||
|
Sphere,
|
||||||
|
Cylinder,
|
||||||
|
Capsule,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<Shape, 5> AllShapes{
|
||||||
|
Shape::Box, Shape::Plane, Shape::Sphere, Shape::Cylinder, Shape::Capsule,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Desc {
|
||||||
|
Shape shape{Shape::Box};
|
||||||
|
glm::vec3 size{1.0f};
|
||||||
|
float radius{0.5f};
|
||||||
|
float height{2.0f};
|
||||||
|
|
||||||
|
bool operator==(const Desc&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] const char* ShapeName(Shape shape);
|
||||||
|
[[nodiscard]] Desc DefaultDesc(Shape shape);
|
||||||
|
|
||||||
|
[[nodiscard]] CPUMesh Make(const Desc& desc);
|
||||||
[[nodiscard]] CPUMesh MakeBox(const glm::vec3& size);
|
[[nodiscard]] CPUMesh MakeBox(const glm::vec3& size);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string MakeKey(const Desc& desc);
|
||||||
[[nodiscard]] std::string BoxKey(const glm::vec3& size);
|
[[nodiscard]] std::string BoxKey(const glm::vec3& size);
|
||||||
|
[[nodiscard]] std::optional<Desc> ParseKey(std::string_view key);
|
||||||
[[nodiscard]] bool IsPrimitiveKey(std::string_view key);
|
[[nodiscard]] bool IsPrimitiveKey(std::string_view key);
|
||||||
[[nodiscard]] std::string_view StripDuplicateSuffix(std::string_view key);
|
[[nodiscard]] std::string_view StripDuplicateSuffix(std::string_view key);
|
||||||
[[nodiscard]] std::optional<CPUMesh> MakeFromKey(std::string_view key);
|
[[nodiscard]] std::optional<CPUMesh> MakeFromKey(std::string_view key);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#include <VkBootstrap.h>
|
#include <VkBootstrap.h>
|
||||||
|
|
||||||
static constexpr int FRAMES_IN_FLIGHT = 2;
|
#include <destrum/Graphics/FrameConfig.h>
|
||||||
|
|
||||||
class Swapchain {
|
class Swapchain {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef DESTRUM_TONEMAPPER_H
|
||||||
|
#define DESTRUM_TONEMAPPER_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
// Must match the TONEMAPPER_ defines in post.frag
|
||||||
|
enum class Tonemapper : std::uint32_t {
|
||||||
|
None,
|
||||||
|
Aces,
|
||||||
|
PbrNeutral,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<std::string_view, 3> TonemapperNames{"none", "aces", "pbrNeutral"};
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::string_view ToString(Tonemapper tonemapper)
|
||||||
|
{
|
||||||
|
return TonemapperNames.at(static_cast<std::size_t>(tonemapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::optional<Tonemapper> ParseTonemapper(std::string_view name)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < TonemapperNames.size(); ++i) {
|
||||||
|
if (TonemapperNames[i] == name) {
|
||||||
|
return static_cast<Tonemapper>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_TONEMAPPER_H
|
||||||
@@ -83,6 +83,18 @@ namespace vkutil {
|
|||||||
VkDeviceSize offset = 0,
|
VkDeviceSize offset = 0,
|
||||||
VkDeviceSize size = VK_WHOLE_SIZE);
|
VkDeviceSize size = VK_WHOLE_SIZE);
|
||||||
|
|
||||||
|
void bufferShaderReadToTransferWriteBarrier(
|
||||||
|
VkCommandBuffer cmd,
|
||||||
|
VkBuffer buffer,
|
||||||
|
VkDeviceSize offset = 0,
|
||||||
|
VkDeviceSize size = VK_WHOLE_SIZE);
|
||||||
|
|
||||||
|
void bufferHostAndTransferWriteToShaderReadBarrier(
|
||||||
|
VkCommandBuffer cmd,
|
||||||
|
VkBuffer buffer,
|
||||||
|
VkDeviceSize offset = 0,
|
||||||
|
VkDeviceSize size = VK_WHOLE_SIZE);
|
||||||
|
|
||||||
void bufferHostWriteToTransferReadBarrier(
|
void bufferHostWriteToTransferReadBarrier(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
VkBuffer buffer,
|
VkBuffer buffer,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <destrum/ObjectModel/Object.h>
|
#include <destrum/ObjectModel/Object.h>
|
||||||
#include <destrum/ObjectModel/ObjectId.h>
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
|
|
||||||
|
class FieldVisitor;
|
||||||
class GameObject;
|
class GameObject;
|
||||||
class Transform;
|
class Transform;
|
||||||
|
|
||||||
@@ -51,15 +52,16 @@ public:
|
|||||||
// Serialization.
|
// Serialization.
|
||||||
virtual std::string GetTypeName() const = 0;
|
virtual std::string GetTypeName() const = 0;
|
||||||
|
|
||||||
virtual nlohmann::json Serialize() const {
|
// The defaults save, load, resolve and draw the fields listed by Reflect
|
||||||
return nlohmann::json::object();
|
virtual void Reflect(FieldVisitor&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Deserialize(const nlohmann::json&) {
|
virtual void OnFieldsEdited() {
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ResolveReferences(const ObjectMap&) {
|
virtual nlohmann::json Serialize() const;
|
||||||
}
|
virtual void Deserialize(const nlohmann::json& data);
|
||||||
|
virtual void ResolveReferences(const ObjectMap& objects);
|
||||||
|
|
||||||
// Trigger callbacks called when this component's owner overlaps a sensor.
|
// Trigger callbacks called when this component's owner overlaps a sensor.
|
||||||
// Only called when the owner has a Collider set as a trigger (or overlaps one).
|
// Only called when the owner has a Collider set as a trigger (or overlaps one).
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <destrum/Physics/PhysicsTypes.h>
|
||||||
|
|
||||||
|
struct CPUMesh;
|
||||||
|
|
||||||
|
// Shares collision data between colliders using the same mesh, so physics backends can share their shapes too.
|
||||||
|
// Main thread only.
|
||||||
|
namespace PhysicsMeshCache {
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> FromCPUMesh(const std::string& key, const CPUMesh& mesh);
|
||||||
|
|
||||||
|
// Null when the points don't span a volume (e.g. a plane)
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> ConvexHull(const std::shared_ptr<const PhysicsMeshData>& source,
|
||||||
|
int maxVertices);
|
||||||
|
}
|
||||||
@@ -4,19 +4,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <destrum/Physics/PhysicsWorld.h>
|
#include <destrum/Physics/PhysicsWorld.h>
|
||||||
#include <destrum/Physics/SimplePhysicsWorld.h>
|
|
||||||
|
|
||||||
class GameObject;
|
class GameObject;
|
||||||
|
|
||||||
// Small helper you can store inside Scene.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
// class Scene {
|
|
||||||
// public:
|
|
||||||
// PhysicsSceneBridge& GetPhysics() { return m_Physics; }
|
|
||||||
// private:
|
|
||||||
// PhysicsSceneBridge m_Physics{std::make_unique<SimplePhysicsWorld>()};
|
|
||||||
// };
|
|
||||||
class PhysicsSceneBridge final {
|
class PhysicsSceneBridge final {
|
||||||
public:
|
public:
|
||||||
explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world);
|
explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
@@ -17,7 +19,10 @@ enum class PhysicsShapeType {
|
|||||||
None,
|
None,
|
||||||
Box,
|
Box,
|
||||||
Sphere,
|
Sphere,
|
||||||
Capsule
|
Capsule,
|
||||||
|
Cylinder,
|
||||||
|
ConvexHull,
|
||||||
|
Mesh
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PhysicsBodyHandle {
|
struct PhysicsBodyHandle {
|
||||||
@@ -50,6 +55,12 @@ struct PhysicsMaterialDesc {
|
|||||||
float restitution{0.0f};
|
float restitution{0.0f};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Triangles share vertices through indices. A convex hull only uses the points, the indices are for drawing it.
|
||||||
|
struct PhysicsMeshData {
|
||||||
|
std::vector<glm::vec3> points;
|
||||||
|
std::vector<std::uint32_t> indices;
|
||||||
|
};
|
||||||
|
|
||||||
struct PhysicsShapeDesc {
|
struct PhysicsShapeDesc {
|
||||||
PhysicsShapeType type{PhysicsShapeType::None};
|
PhysicsShapeType type{PhysicsShapeType::None};
|
||||||
|
|
||||||
@@ -59,9 +70,14 @@ struct PhysicsShapeDesc {
|
|||||||
// Sphere / capsule
|
// Sphere / capsule
|
||||||
float radius{0.5f};
|
float radius{0.5f};
|
||||||
|
|
||||||
// Capsule full height, including the two hemispheres.
|
// Capsule full height, including the two hemispheres. Cylinder full height.
|
||||||
float height{2.0f};
|
float height{2.0f};
|
||||||
|
|
||||||
|
// Convex hull / mesh, in the collider's local space before scale. Backends may cache
|
||||||
|
// shapes per mesh data, so it must not be modified once shared.
|
||||||
|
std::shared_ptr<const PhysicsMeshData> mesh;
|
||||||
|
glm::vec3 scale{1.0f};
|
||||||
|
|
||||||
// Local offset from the rigidbody transform.
|
// Local offset from the rigidbody transform.
|
||||||
glm::vec3 centerOffset{0.0f};
|
glm::vec3 centerOffset{0.0f};
|
||||||
|
|
||||||
@@ -101,6 +117,45 @@ struct PhysicsShapeDesc {
|
|||||||
desc.isTrigger = isTrigger;
|
desc.isTrigger = isTrigger;
|
||||||
return desc;
|
return desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc Cylinder(float radius,
|
||||||
|
float height,
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::Cylinder;
|
||||||
|
desc.radius = radius;
|
||||||
|
desc.height = height;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc ConvexHull(std::shared_ptr<const PhysicsMeshData> hull,
|
||||||
|
const glm::vec3& scale = glm::vec3{1.0f},
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::ConvexHull;
|
||||||
|
desc.mesh = std::move(hull);
|
||||||
|
desc.scale = scale;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc Mesh(std::shared_ptr<const PhysicsMeshData> mesh,
|
||||||
|
const glm::vec3& scale = glm::vec3{1.0f},
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::Mesh;
|
||||||
|
desc.mesh = std::move(mesh);
|
||||||
|
desc.scale = scale;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PhysicsBodyDesc {
|
struct PhysicsBodyDesc {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include <destrum/Physics/PhysicsWorld.h>
|
|
||||||
|
|
||||||
class Rigidbody;
|
|
||||||
|
|
||||||
// Simple CPU placeholder backend.
|
|
||||||
//
|
|
||||||
// This is NOT a replacement for Jolt/PhysX/Bullet.
|
|
||||||
// It exists so your component/scene/render sync can be implemented and tested
|
|
||||||
// before the real backend is added.
|
|
||||||
//
|
|
||||||
// It supports:
|
|
||||||
// - static, dynamic, kinematic bodies
|
|
||||||
// - gravity
|
|
||||||
// - force/impulse integration
|
|
||||||
// - very simple sphere/box/capsule raycasts using bounding spheres
|
|
||||||
//
|
|
||||||
// It does NOT support:
|
|
||||||
// - collision response
|
|
||||||
// - contacts
|
|
||||||
// - friction
|
|
||||||
// - constraints
|
|
||||||
// - real character controllers
|
|
||||||
class SimplePhysicsWorld final : public PhysicsWorld {
|
|
||||||
public:
|
|
||||||
explicit SimplePhysicsWorld(const glm::vec3& gravity = glm::vec3{0.0f, -9.81f, 0.0f});
|
|
||||||
|
|
||||||
void Step(float fixedDt) override;
|
|
||||||
|
|
||||||
PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) override;
|
|
||||||
void DestroyBody(PhysicsBodyHandle body) override;
|
|
||||||
|
|
||||||
void SetBodyTransform(PhysicsBodyHandle body, const PhysicsTransform& transform) override;
|
|
||||||
PhysicsTransform GetBodyTransform(PhysicsBodyHandle body) const override;
|
|
||||||
|
|
||||||
void SetLinearVelocity(PhysicsBodyHandle body, const glm::vec3& velocity) override;
|
|
||||||
glm::vec3 GetLinearVelocity(PhysicsBodyHandle body) const override;
|
|
||||||
|
|
||||||
void AddForce(PhysicsBodyHandle body, const glm::vec3& force) override;
|
|
||||||
void AddImpulse(PhysicsBodyHandle body, const glm::vec3& impulse) override;
|
|
||||||
|
|
||||||
bool Raycast(const glm::vec3& origin,
|
|
||||||
const glm::vec3& direction,
|
|
||||||
float maxDistance,
|
|
||||||
PhysicsRaycastHit& hit,
|
|
||||||
PhysicsBodyHandle ignoreBody = {}) const override;
|
|
||||||
|
|
||||||
void SyncKinematicBodiesToPhysics(float fixedDt) override;
|
|
||||||
void SyncDynamicBodiesToTransforms();
|
|
||||||
|
|
||||||
[[nodiscard]] const glm::vec3& GetGravity() const { return m_Gravity; }
|
|
||||||
void SetGravity(const glm::vec3& gravity) { m_Gravity = gravity; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct BodyRecord {
|
|
||||||
bool alive{false};
|
|
||||||
|
|
||||||
PhysicsBodyHandle handle{};
|
|
||||||
PhysicsBodyDesc desc{};
|
|
||||||
|
|
||||||
PhysicsTransform previousTransform{};
|
|
||||||
PhysicsTransform currentTransform{};
|
|
||||||
|
|
||||||
glm::vec3 linearVelocity{0.0f};
|
|
||||||
glm::vec3 accumulatedForce{0.0f};
|
|
||||||
|
|
||||||
Rigidbody* rigidbody{nullptr};
|
|
||||||
};
|
|
||||||
|
|
||||||
[[nodiscard]] BodyRecord* FindBody(PhysicsBodyHandle body);
|
|
||||||
[[nodiscard]] const BodyRecord* FindBody(PhysicsBodyHandle body) const;
|
|
||||||
|
|
||||||
[[nodiscard]] float GetApproxBoundingRadius(const BodyRecord& body) const;
|
|
||||||
[[nodiscard]] static bool RaySphere(const glm::vec3& origin,
|
|
||||||
const glm::vec3& dirNormalized,
|
|
||||||
const glm::vec3& center,
|
|
||||||
float radius,
|
|
||||||
float maxDistance,
|
|
||||||
float& outDistance);
|
|
||||||
|
|
||||||
glm::vec3 m_Gravity{0.0f, -9.81f, 0.0f};
|
|
||||||
|
|
||||||
std::vector<BodyRecord> m_Bodies;
|
|
||||||
std::uint32_t m_NextId{0};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef DESTRUM_RESOURCECOLLECTOR_H
|
||||||
|
#define DESTRUM_RESOURCECOLLECTOR_H
|
||||||
|
|
||||||
|
class GfxDevice;
|
||||||
|
class RenderResources;
|
||||||
|
class SceneManager;
|
||||||
|
|
||||||
|
void CollectUnusedResources(SceneManager& sceneManager, RenderResources& resources, GfxDevice& gfxDevice);
|
||||||
|
|
||||||
|
#endif // DESTRUM_RESOURCECOLLECTOR_H
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <destrum/Event.h>
|
#include <destrum/Event.h>
|
||||||
|
#include <destrum/Graphics/Tonemapper.h>
|
||||||
#include <destrum/ObjectModel/ObjectId.h>
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
#include <destrum/Util/SceneInspector.h>
|
|
||||||
|
|
||||||
#include "destrum/Physics/JoltPhysicsWorld.h"
|
#include "destrum/Physics/JoltPhysicsWorld.h"
|
||||||
#include "destrum/Physics/PhysicsSceneBridge.h"
|
#include "destrum/Physics/PhysicsSceneBridge.h"
|
||||||
@@ -26,6 +26,9 @@ struct SceneEnvironment {
|
|||||||
|
|
||||||
float skyboxRotationSpeed{0.01f};
|
float skyboxRotationSpeed{0.01f};
|
||||||
float skyboxRotation{0.0f};
|
float skyboxRotation{0.0f};
|
||||||
|
|
||||||
|
float exposure{0.0f};
|
||||||
|
Tonemapper tonemapper{Tonemapper::PbrNeutral};
|
||||||
};
|
};
|
||||||
class SceneSerializer;
|
class SceneSerializer;
|
||||||
class PrefabSerializer;
|
class PrefabSerializer;
|
||||||
@@ -49,7 +52,6 @@ public:
|
|||||||
void FixedUpdate(float dt);
|
void FixedUpdate(float dt);
|
||||||
void LateUpdate(float dt);
|
void LateUpdate(float dt);
|
||||||
void Render(const RenderContext& ctx);
|
void Render(const RenderContext& ctx);
|
||||||
void RenderImgui(bool* open = nullptr);
|
|
||||||
|
|
||||||
void CleanupDestroyedGameObjects();
|
void CleanupDestroyedGameObjects();
|
||||||
void Unload();
|
void Unload();
|
||||||
@@ -76,6 +78,9 @@ public:
|
|||||||
// nullptr. Also reorders the object list so the order survives a save. False if the move isn't allowed.
|
// nullptr. Also reorders the object list so the order survives a save. False if the move isn't allowed.
|
||||||
bool MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore = nullptr);
|
bool MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore = nullptr);
|
||||||
void SetSelectedObject(ObjectId id) { m_SelectedObjectId = id; }
|
void SetSelectedObject(ObjectId id) { m_SelectedObjectId = id; }
|
||||||
|
[[nodiscard]] ObjectId GetSelectedObjectId() const { return m_SelectedObjectId; }
|
||||||
|
|
||||||
|
void DrawSettingsImGui();
|
||||||
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
|
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
|
||||||
|
|
||||||
void SetRegisterBindings(std::function<void()> registerBindings) {
|
void SetRegisterBindings(std::function<void()> registerBindings) {
|
||||||
@@ -159,13 +164,11 @@ private:
|
|||||||
//Imgui vars
|
//Imgui vars
|
||||||
bool m_ShowDemoWindow{false};
|
bool m_ShowDemoWindow{false};
|
||||||
ObjectId m_SelectedObjectId{InvalidObjectId};
|
ObjectId m_SelectedObjectId{InvalidObjectId};
|
||||||
SceneInspector m_Inspector;
|
|
||||||
|
|
||||||
ObjectId m_CurrentCameraId{InvalidObjectId};
|
ObjectId m_CurrentCameraId{InvalidObjectId};
|
||||||
|
|
||||||
SceneEnvironment m_Environment{};
|
SceneEnvironment m_Environment{};
|
||||||
|
|
||||||
void DrawSettingsImGui();
|
|
||||||
|
|
||||||
std::function<void()> m_registerBindings;
|
std::function<void()> m_registerBindings;
|
||||||
std::function<void()> m_unregisterBindings;
|
std::function<void()> m_unregisterBindings;
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ public:
|
|||||||
Scene& GetCurrentScene() const;
|
Scene& GetCurrentScene() const;
|
||||||
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
|
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
|
||||||
|
|
||||||
|
void SetPlaying(bool playing) { m_playing = playing; }
|
||||||
|
[[nodiscard]] bool IsPlaying() const { return m_playing; }
|
||||||
|
void SetPaused(bool paused) { m_paused = paused; }
|
||||||
|
[[nodiscard]] bool IsPaused() const { return m_paused; }
|
||||||
|
|
||||||
void Update(float dt);
|
void Update(float dt);
|
||||||
void FixedUpdate(float dt);
|
void FixedUpdate(float dt);
|
||||||
void LateUpdate(float dt);
|
void LateUpdate(float dt);
|
||||||
|
|
||||||
void Render(const RenderContext& ctx);
|
void Render(const RenderContext& ctx);
|
||||||
void RenderImgui(bool* open = nullptr);
|
|
||||||
|
|
||||||
void HandleGameObjectDestroy();
|
void HandleGameObjectDestroy();
|
||||||
void DestroyGameObjects();
|
void DestroyGameObjects();
|
||||||
@@ -52,6 +56,11 @@ private:
|
|||||||
|
|
||||||
std::vector<std::shared_ptr<Scene>> m_activeScenes;
|
std::vector<std::shared_ptr<Scene>> m_activeScenes;
|
||||||
std::vector<std::shared_ptr<Scene>> m_scenes;
|
std::vector<std::shared_ptr<Scene>> m_scenes;
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsTicking() const { return m_playing && !m_paused; }
|
||||||
|
|
||||||
|
bool m_playing{true};
|
||||||
|
bool m_paused{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //SCENEMANAGER_H
|
#endif //SCENEMANAGER_H
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#define DESTRUM_ENGINECOMPONENTLIST_H
|
#define DESTRUM_ENGINECOMPONENTLIST_H
|
||||||
|
|
||||||
#include <destrum/Components/Animator.h>
|
#include <destrum/Components/Animator.h>
|
||||||
|
#include <destrum/Components/AudioListener.h>
|
||||||
|
#include <destrum/Components/AudioSource.h>
|
||||||
#include <destrum/Components/CameraComponent.h>
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/Components/LightComponent.h>
|
#include <destrum/Components/LightComponent.h>
|
||||||
#include <destrum/Components/MeshRendererComponent.h>
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
@@ -10,6 +12,9 @@
|
|||||||
#include <destrum/Components/Spinner.h>
|
#include <destrum/Components/Spinner.h>
|
||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||||
|
#include <destrum/Components/Physics/ConvexHullCollider.h>
|
||||||
|
#include <destrum/Components/Physics/CylinderCollider.h>
|
||||||
|
#include <destrum/Components/Physics/MeshCollider.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/Components/Physics/SphereCollider.h>
|
#include <destrum/Components/Physics/SphereCollider.h>
|
||||||
#include <destrum/Components/UI/Canvas.h>
|
#include <destrum/Components/UI/Canvas.h>
|
||||||
@@ -25,10 +30,12 @@
|
|||||||
X(Spinner) \
|
X(Spinner) \
|
||||||
X(OrbitAndSpin) \
|
X(OrbitAndSpin) \
|
||||||
X(Animator) \
|
X(Animator) \
|
||||||
|
X(AudioSource) \
|
||||||
|
X(AudioListener) \
|
||||||
X(Rigidbody) \
|
X(Rigidbody) \
|
||||||
X(BoxCollider) \
|
X(BoxCollider) \
|
||||||
X(SphereCollider) \
|
X(SphereCollider) \
|
||||||
X(CapsuleCollider) \
|
X(CapsuleCollider) X(CylinderCollider) X(ConvexHullCollider) X(MeshCollider) \
|
||||||
X(Canvas) \
|
X(Canvas) \
|
||||||
X(RectTransform) \
|
X(RectTransform) \
|
||||||
X(UIImage) \
|
X(UIImage) \
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef DESTRUM_MATERIALSERIALIZATION_H
|
||||||
|
#define DESTRUM_MATERIALSERIALIZATION_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Material.h>
|
||||||
|
|
||||||
|
class GfxDevice;
|
||||||
|
class RenderResources;
|
||||||
|
|
||||||
|
namespace MaterialSerialization {
|
||||||
|
inline constexpr int CurrentFileVersion = 1;
|
||||||
|
|
||||||
|
using TextureWriter = std::function<nlohmann::json(ImageID)>;
|
||||||
|
using TextureReader = std::function<ImageID(const nlohmann::json&)>;
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json ToJson(const Material& material, const TextureWriter& writeTexture);
|
||||||
|
[[nodiscard]] Material FromJson(const nlohmann::json& data, const TextureReader& readTexture);
|
||||||
|
[[nodiscard]] std::optional<std::string> Validate(const nlohmann::json& materials);
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsFileKey(std::string_view key);
|
||||||
|
[[nodiscard]] nlohmann::json ToFile(const Material& material, const RenderResources* resources = nullptr);
|
||||||
|
MaterialID LoadFile(RenderResources& resources, GfxDevice& gfxDevice, std::string_view path);
|
||||||
|
bool SaveFile(const RenderResources& resources, MaterialID id, std::string_view path);
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json EmbedLocalMaterials(nlohmann::json& objects);
|
||||||
|
void LoadLocalMaterials(const nlohmann::json& materials, nlohmann::json& objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //DESTRUM_MATERIALSERIALIZATION_H
|
||||||
@@ -18,7 +18,7 @@ namespace ObjectSerialization {
|
|||||||
using ObjectMap = std::unordered_map<ObjectId, GameObject*>;
|
using ObjectMap = std::unordered_map<ObjectId, GameObject*>;
|
||||||
using ValidationError = std::optional<std::string>;
|
using ValidationError = std::optional<std::string>;
|
||||||
|
|
||||||
constexpr int CurrentObjectVersion = 2;
|
constexpr int CurrentObjectVersion = 3;
|
||||||
|
|
||||||
[[nodiscard]] std::optional<std::string> FindNonFinite(const nlohmann::json& value, const std::string& path);
|
[[nodiscard]] std::optional<std::string> FindNonFinite(const nlohmann::json& value, const std::string& path);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Transform;
|
|||||||
|
|
||||||
class PrefabSerializer final {
|
class PrefabSerializer final {
|
||||||
public:
|
public:
|
||||||
static constexpr int CurrentPrefabVersion = 1;
|
static constexpr int CurrentPrefabVersion = 2;
|
||||||
|
|
||||||
[[nodiscard]] static std::optional<nlohmann::json> Capture(const GameObject& root);
|
[[nodiscard]] static std::optional<nlohmann::json> Capture(const GameObject& root);
|
||||||
static bool Save(const GameObject& root, const std::filesystem::path& path);
|
static bool Save(const GameObject& root, const std::filesystem::path& path);
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
#ifndef DESTRUM_REFLECT_H
|
||||||
|
#define DESTRUM_REFLECT_H
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/ObjectRef.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
|
class Component;
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
|
||||||
|
// Designated initializers have to follow this order
|
||||||
|
struct FieldOptions {
|
||||||
|
float speed = 0.1f;
|
||||||
|
std::optional<double> min;
|
||||||
|
std::optional<double> max;
|
||||||
|
const char* format = nullptr;
|
||||||
|
bool color = false;
|
||||||
|
bool angle = false; // radians, shown in degrees
|
||||||
|
bool multiline = false;
|
||||||
|
bool acceptAssets = false; // strings take a virtual path dropped from the asset browser
|
||||||
|
bool readOnly = false;
|
||||||
|
bool hidden = false; // saved, not shown
|
||||||
|
bool transient = false; // shown, not saved
|
||||||
|
const char* label = nullptr;
|
||||||
|
const char* tooltip = nullptr;
|
||||||
|
std::span<const char* const> enumNames;
|
||||||
|
std::function<bool(const GameObject&)> filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldOps {
|
||||||
|
void (*save)(const void* value, nlohmann::json& out, const FieldOptions& options);
|
||||||
|
void (*load)(void* value, const nlohmann::json& in, const FieldOptions& options);
|
||||||
|
bool (*draw)(const char* label, void* value, const FieldOptions& options, const Scene* scene);
|
||||||
|
void (*resolve)(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace reflect_detail {
|
||||||
|
enum class Scalar { S8, U8, S16, U16, S32, U32, S64, U64, Float, Double };
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
consteval Scalar ScalarOf()
|
||||||
|
{
|
||||||
|
if constexpr (std::same_as<T, float>) return Scalar::Float;
|
||||||
|
else if constexpr (std::same_as<T, double>) return Scalar::Double;
|
||||||
|
else if constexpr (std::is_signed_v<T>) {
|
||||||
|
if constexpr (sizeof(T) == 1) return Scalar::S8;
|
||||||
|
else if constexpr (sizeof(T) == 2) return Scalar::S16;
|
||||||
|
else if constexpr (sizeof(T) == 4) return Scalar::S32;
|
||||||
|
else return Scalar::S64;
|
||||||
|
} else {
|
||||||
|
if constexpr (sizeof(T) == 1) return Scalar::U8;
|
||||||
|
else if constexpr (sizeof(T) == 2) return Scalar::U16;
|
||||||
|
else if constexpr (sizeof(T) == 4) return Scalar::U32;
|
||||||
|
else return Scalar::U64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options);
|
||||||
|
bool DrawAngle(const char* label, float& radians, const FieldOptions& options);
|
||||||
|
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options);
|
||||||
|
|
||||||
|
void BeginList(const char* label);
|
||||||
|
[[nodiscard]] bool ListRemoveButton(std::size_t index);
|
||||||
|
void EndListRow();
|
||||||
|
[[nodiscard]] bool ListAddButton();
|
||||||
|
|
||||||
|
[[nodiscard]] std::string PrettifyFieldName(std::string_view key);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept Numeric = std::is_arithmetic_v<T> && !std::same_as<T, bool>;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept FloatVec = std::same_as<T, glm::vec2> || std::same_as<T, glm::vec3> || std::same_as<T, glm::vec4>;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, bool& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, std::string& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
|
||||||
|
template <reflect_detail::Numeric T>
|
||||||
|
bool DrawField(const char* label, T& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
if constexpr (std::same_as<T, float>) {
|
||||||
|
if (options.angle) {
|
||||||
|
return reflect_detail::DrawAngle(label, value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const T min = options.min ? static_cast<T>(*options.min) : T{};
|
||||||
|
const T max = options.max ? static_cast<T>(*options.max) : T{};
|
||||||
|
return reflect_detail::DrawScalar(label, reflect_detail::ScalarOf<T>(), &value, 1,
|
||||||
|
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class E>
|
||||||
|
requires std::is_enum_v<E>
|
||||||
|
bool DrawField(const char* label, E& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
auto raw = static_cast<std::int64_t>(value);
|
||||||
|
if (!reflect_detail::DrawEnum(label, raw, options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = static_cast<E>(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
bool DrawField(const char* label, std::vector<T>& values, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
reflect_detail::BeginList(label);
|
||||||
|
bool changed = false;
|
||||||
|
std::optional<std::size_t> removeIndex;
|
||||||
|
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||||
|
if (reflect_detail::ListRemoveButton(i)) {
|
||||||
|
removeIndex = i;
|
||||||
|
}
|
||||||
|
changed |= DrawField("##value", values[i], options, scene);
|
||||||
|
reflect_detail::EndListRow();
|
||||||
|
}
|
||||||
|
if (removeIndex) {
|
||||||
|
values.erase(values.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (reflect_detail::ListAddButton()) {
|
||||||
|
values.emplace_back();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept Reflectable = std::is_constructible_v<nlohmann::json, const T&> &&
|
||||||
|
requires(T& value, const FieldOptions& options, const Scene* scene) {
|
||||||
|
{ DrawField("", value, options, scene) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept HasReferences = requires(T& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped) {
|
||||||
|
ResolveField(value, objects, dropped);
|
||||||
|
};
|
||||||
|
|
||||||
|
template <Reflectable T>
|
||||||
|
struct FieldCodec {
|
||||||
|
static void Save(const void* value, nlohmann::json& out, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
const T& typed = *static_cast<const T*>(value);
|
||||||
|
if constexpr (std::is_enum_v<T>) {
|
||||||
|
const auto index = static_cast<std::size_t>(typed);
|
||||||
|
if (index < options.enumNames.size()) {
|
||||||
|
out = options.enumNames[index];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = typed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Load(void* value, const nlohmann::json& in, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
T& typed = *static_cast<T*>(value);
|
||||||
|
if constexpr (std::is_enum_v<T>) {
|
||||||
|
if (in.is_string()) {
|
||||||
|
const auto name = in.get<std::string>();
|
||||||
|
for (std::size_t i = 0; i < options.enumNames.size(); ++i) {
|
||||||
|
if (name == options.enumNames[i]) {
|
||||||
|
typed = static_cast<T>(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw std::runtime_error("unknown value \"" + name + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typed = in.get<T>();
|
||||||
|
if constexpr (reflect_detail::Numeric<T>) {
|
||||||
|
if (options.min && typed < static_cast<T>(*options.min)) typed = static_cast<T>(*options.min);
|
||||||
|
if (options.max && typed > static_cast<T>(*options.max)) typed = static_cast<T>(*options.max);
|
||||||
|
} else if constexpr (reflect_detail::FloatVec<T>) {
|
||||||
|
if (options.min) typed = glm::max(typed, T{static_cast<float>(*options.min)});
|
||||||
|
if (options.max) typed = glm::min(typed, T{static_cast<float>(*options.max)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Draw(const char* label, void* value, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
return DrawField(label, *static_cast<T*>(value), options, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Resolve(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||||
|
{
|
||||||
|
if constexpr (HasReferences<T>) {
|
||||||
|
ResolveField(*static_cast<T*>(value), objects, dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr FieldOps Ops{&Save, &Load, &Draw, HasReferences<T> ? &Resolve : nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
class FieldVisitor {
|
||||||
|
public:
|
||||||
|
virtual ~FieldVisitor() = default;
|
||||||
|
|
||||||
|
using Commit = std::function<void(void* value)>;
|
||||||
|
|
||||||
|
template <Reflectable T>
|
||||||
|
void Field(std::string_view key, T& value, const FieldOptions& options = {})
|
||||||
|
{
|
||||||
|
Visit(key, &value, options, FieldCodec<T>::Ops, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads and edits go through the setter, so it can validate and react
|
||||||
|
template <Reflectable T, std::invocable<const T&> Setter>
|
||||||
|
void Property(std::string_view key, const T& value, Setter&& setter, const FieldOptions& options = {})
|
||||||
|
{
|
||||||
|
T copy = value;
|
||||||
|
Visit(key, ©, options, FieldCodec<T>::Ops, [&setter](void* changed) {
|
||||||
|
setter(*static_cast<const T*>(changed));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonWriter final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] nlohmann::json& Result() { return m_Result; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
nlohmann::json m_Result = nlohmann::json::object();
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonReader final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
explicit JsonReader(const nlohmann::json& data) : m_Data(data) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const nlohmann::json& m_Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReferenceResolver final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
ReferenceResolver(const Component& owner, const ObjectRef::ObjectMap& objects) : m_Owner(owner), m_Objects(objects) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Component& m_Owner;
|
||||||
|
const ObjectRef::ObjectMap& m_Objects;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InspectorDrawer final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
explicit InspectorDrawer(const Scene* scene) : m_Scene(scene) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Changed() const { return m_Changed; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Scene* m_Scene;
|
||||||
|
bool m_Changed{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_REFLECT_H
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
@@ -17,8 +18,11 @@ public:
|
|||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] static std::optional<nlohmann::json> Capture(Scene& scene);
|
||||||
static bool Save(Scene& scene, const std::filesystem::path& path);
|
static bool Save(Scene& scene, const std::filesystem::path& path);
|
||||||
|
|
||||||
|
static bool LoadFromJson(Scene& scene, const nlohmann::json& root);
|
||||||
|
|
||||||
static bool Load(Scene& scene, const std::filesystem::path& path,
|
static bool Load(Scene& scene, const std::filesystem::path& path,
|
||||||
std::function<void(float)> progressCallback = nullptr);
|
std::function<void(float)> progressCallback = nullptr);
|
||||||
|
|
||||||
@@ -27,8 +31,6 @@ public:
|
|||||||
static bool ConstructScene(Scene& scene, LoadResult& result,
|
static bool ConstructScene(Scene& scene, LoadResult& result,
|
||||||
std::function<void(float)> progressCallback = nullptr);
|
std::function<void(float)> progressCallback = nullptr);
|
||||||
|
|
||||||
// Parse the scene file on a background thread. Call LoadFromJson on the
|
|
||||||
// main thread once the future is ready.
|
|
||||||
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
|
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#ifndef DESTRUM_JOBSYSTEM_H
|
||||||
|
#define DESTRUM_JOBSYSTEM_H
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <destrum/Singleton.h>
|
||||||
|
|
||||||
|
enum class JobPriority {
|
||||||
|
High,
|
||||||
|
Low,
|
||||||
|
};
|
||||||
|
|
||||||
|
class JobCounter {
|
||||||
|
public:
|
||||||
|
JobCounter() = default;
|
||||||
|
JobCounter(const JobCounter&) = delete;
|
||||||
|
JobCounter& operator=(const JobCounter&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsDone() const { return m_Pending.load(std::memory_order_acquire) == 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class JobSystem;
|
||||||
|
std::atomic<int> m_Pending{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JobDispatchArgs {
|
||||||
|
std::uint32_t index{0};
|
||||||
|
std::uint32_t groupIndex{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
class JobSystem final : public Singleton<JobSystem> {
|
||||||
|
public:
|
||||||
|
friend class Singleton<JobSystem>;
|
||||||
|
using Job = std::function<void()>;
|
||||||
|
|
||||||
|
~JobSystem() override;
|
||||||
|
|
||||||
|
// 0 workers picks hardware_concurrency - 1
|
||||||
|
void Init(std::uint32_t workerCount = 0);
|
||||||
|
void Shutdown();
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsRunning() const { return !m_Workers.empty(); }
|
||||||
|
[[nodiscard]] std::uint32_t GetWorkerCount() const { return static_cast<std::uint32_t>(m_Workers.size()); }
|
||||||
|
|
||||||
|
void Execute(JobCounter& counter, Job job, JobPriority priority = JobPriority::High);
|
||||||
|
void Dispatch(JobCounter& counter, std::uint32_t count, std::uint32_t groupSize,
|
||||||
|
std::function<void(JobDispatchArgs)> job);
|
||||||
|
void Wait(JobCounter& counter);
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
auto Async(F&& fn, JobPriority priority = JobPriority::Low) -> std::future<std::invoke_result_t<std::decay_t<F>>>
|
||||||
|
{
|
||||||
|
using Result = std::invoke_result_t<std::decay_t<F>>;
|
||||||
|
auto task = std::make_shared<std::packaged_task<Result()>>(std::forward<F>(fn));
|
||||||
|
auto future = task->get_future();
|
||||||
|
Submit([task] { (*task)(); }, priority);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunOnMainThread(Job job);
|
||||||
|
void DrainMainThreadQueue();
|
||||||
|
|
||||||
|
private:
|
||||||
|
JobSystem() = default;
|
||||||
|
|
||||||
|
void Submit(Job job, JobPriority priority);
|
||||||
|
bool TryPopLocked(Job& job, bool& isLow);
|
||||||
|
[[nodiscard]] bool CanTakeLowLocked() const;
|
||||||
|
bool TryRunHighJob();
|
||||||
|
void WorkerLoop(std::uint32_t index);
|
||||||
|
|
||||||
|
std::vector<std::thread> m_Workers;
|
||||||
|
std::mutex m_Mutex;
|
||||||
|
std::condition_variable m_WorkAvailable;
|
||||||
|
std::deque<Job> m_HighQueue;
|
||||||
|
std::deque<Job> m_LowQueue;
|
||||||
|
std::uint32_t m_ActiveLowJobs{0};
|
||||||
|
std::uint32_t m_MaxActiveLowJobs{1};
|
||||||
|
bool m_Stopping{false};
|
||||||
|
|
||||||
|
std::mutex m_MainThreadMutex;
|
||||||
|
std::vector<Job> m_MainThreadQueue;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_JOBSYSTEM_H
|
||||||
@@ -24,6 +24,7 @@ namespace ImGuiUtils {
|
|||||||
float speed = 0.1f);
|
float speed = 0.1f);
|
||||||
|
|
||||||
inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
|
inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
|
||||||
|
inline constexpr const char* AssetPayload = "DESTRUM_ASSET";
|
||||||
|
|
||||||
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
|
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
|
||||||
bool InputStringMultiline(const char* label, std::string& value);
|
bool InputStringMultiline(const char* label, std::string& value);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
//
|
//
|
||||||
// Link against: assimp
|
// Link against: assimp
|
||||||
|
|
||||||
|
#include <assimp/IOSystem.hpp>
|
||||||
#include <assimp/Importer.hpp>
|
#include <assimp/Importer.hpp>
|
||||||
#include <assimp/material.h>
|
#include <assimp/material.h>
|
||||||
#include <assimp/postprocess.h>
|
#include <assimp/postprocess.h>
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -998,8 +1000,12 @@ static void CollectMergedPerNodeMeshes(const aiScene* scene,
|
|||||||
|
|
||||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static ModelAsset LoadModel(const std::string& path, const LoadOptions& options = {}) {
|
// The importer owns io, without one it reads from disk
|
||||||
|
static ModelAsset LoadModel(const std::string& path, const LoadOptions& options = {},
|
||||||
|
std::unique_ptr<Assimp::IOSystem> io = nullptr) {
|
||||||
Assimp::Importer importer;
|
Assimp::Importer importer;
|
||||||
|
if (io)
|
||||||
|
importer.SetIOHandler(io.release());
|
||||||
const aiScene* scene = LoadScene(importer, path, options);
|
const aiScene* scene = LoadScene(importer, path, options);
|
||||||
|
|
||||||
ModelAsset model{};
|
ModelAsset model{};
|
||||||
@@ -1084,10 +1090,11 @@ static const aiScene* LoadAnimationScene(
|
|||||||
Assimp::Importer& importer,
|
Assimp::Importer& importer,
|
||||||
const std::string& path
|
const std::string& path
|
||||||
) {
|
) {
|
||||||
if (!std::filesystem::exists(path)) {
|
if (!importer.GetIOHandler()->Exists(path.c_str())) {
|
||||||
throw std::runtime_error("Animation file does not exist: " + path);
|
throw std::runtime_error("Animation file does not exist: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (importer.IsDefaultIOHandler()) {
|
||||||
if (!std::filesystem::is_regular_file(path)) {
|
if (!std::filesystem::is_regular_file(path)) {
|
||||||
throw std::runtime_error("Animation path is not a regular file: " + path);
|
throw std::runtime_error("Animation path is not a regular file: " + path);
|
||||||
}
|
}
|
||||||
@@ -1095,6 +1102,7 @@ static const aiScene* LoadAnimationScene(
|
|||||||
if (std::filesystem::file_size(path) == 0) {
|
if (std::filesystem::file_size(path) == 0) {
|
||||||
throw std::runtime_error("Animation file is empty: " + path);
|
throw std::runtime_error("Animation file is empty: " + path);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Important: do not use mesh post-process flags for animation-only files.
|
// Important: do not use mesh post-process flags for animation-only files.
|
||||||
const unsigned int flags = 0;
|
const unsigned int flags = 0;
|
||||||
@@ -1141,9 +1149,12 @@ static const aiScene* LoadAnimationScene(
|
|||||||
|
|
||||||
static std::vector<SkeletalAnimation> LoadAnimationClips(
|
static std::vector<SkeletalAnimation> LoadAnimationClips(
|
||||||
const std::string& path,
|
const std::string& path,
|
||||||
const Skeleton& targetSkeleton
|
const Skeleton& targetSkeleton,
|
||||||
|
std::unique_ptr<Assimp::IOSystem> io = nullptr
|
||||||
) {
|
) {
|
||||||
Assimp::Importer importer;
|
Assimp::Importer importer;
|
||||||
|
if (io)
|
||||||
|
importer.SetIOHandler(io.release());
|
||||||
const aiScene* scene = LoadAnimationScene(importer, path);
|
const aiScene* scene = LoadAnimationScene(importer, path);
|
||||||
|
|
||||||
auto clips = LoadAnimations(scene, targetSkeleton);
|
auto clips = LoadAnimations(scene, targetSkeleton);
|
||||||
|
|||||||
+306
-312
@@ -1,56 +1,67 @@
|
|||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <exception>
|
|
||||||
#include <thread>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <destrum/App.h>
|
#include <destrum/App.h>
|
||||||
|
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <algorithm>
|
||||||
#include <destrum/Graphics/Resources/FontCache.h>
|
#include <cmath>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <exception>
|
||||||
#include <destrum/Util/DeltaTime.h>
|
#include <thread>
|
||||||
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "glm/gtx/transform.hpp"
|
|
||||||
#include "spdlog/spdlog.h"
|
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
#include <tracy/Tracy.hpp>
|
#include <tracy/Tracy.hpp>
|
||||||
#include <common/TracySystem.hpp>
|
#include <common/TracySystem.hpp>
|
||||||
|
|
||||||
struct TracyFrameScope
|
#include <destrum/Audio/AudioSystem.h>
|
||||||
{
|
#include <destrum/FS/AssetFS.h>
|
||||||
~TracyFrameScope()
|
#include <destrum/Graphics/Resources/FontCache.h>
|
||||||
{
|
#include <destrum/Scene/ResourceCollector.h>
|
||||||
FrameMark;
|
#include <destrum/Scene/Scene.h>
|
||||||
}
|
#include <destrum/Scene/SceneManager.h>
|
||||||
};
|
#include <destrum/Threading/JobSystem.h>
|
||||||
|
#include <destrum/Util/DeltaTime.h>
|
||||||
|
|
||||||
App::App()
|
namespace {
|
||||||
{
|
struct TracyFrameScope {
|
||||||
|
~TracyFrameScope() { FrameMark; }
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr float MaxFrameDelta = 0.25f;
|
||||||
|
constexpr int MaxFixedSteps = 5;
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsMouseEvent(const SDL_Event& event)
|
||||||
|
{
|
||||||
|
return event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP ||
|
||||||
|
event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsKeyboardEvent(const SDL_Event& event)
|
||||||
|
{
|
||||||
|
return event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_TEXTINPUT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App::App() = default;
|
||||||
|
|
||||||
App::~App()
|
App::~App()
|
||||||
{
|
{
|
||||||
if (!cleanedUp) {
|
if (!cleanedUp) {
|
||||||
try {
|
try {
|
||||||
cleanup();
|
cleanup();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// Destructors must not throw. Initialization failures are already
|
|
||||||
// reported by the caller, while cleanup is best effort here.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::init(const AppParams& params)
|
void App::init(const AppParams& params)
|
||||||
{
|
{
|
||||||
|
ZoneScopedN("App::init");
|
||||||
cleanedUp = false;
|
cleanedUp = false;
|
||||||
customInitStarted = false;
|
customInitStarted = false;
|
||||||
try {
|
try {
|
||||||
m_params = params;
|
m_params = params;
|
||||||
ZoneScopedN("App::init");
|
|
||||||
tracy::SetThreadName("Main Thread");
|
tracy::SetThreadName("Main Thread");
|
||||||
TracySetProgramName(params.appName.c_str());
|
TracySetProgramName(params.appName.c_str());
|
||||||
AssetFS::GetInstance().Init(params.exeDir);
|
AssetFS::GetInstance().Init(params.exeDir);
|
||||||
|
JobSystem::GetInstance().Init();
|
||||||
|
|
||||||
window.Create({
|
window.Create({
|
||||||
.title = params.windowTitle,
|
.title = params.windowTitle,
|
||||||
@@ -59,15 +70,15 @@ void App::init(const AppParams& params)
|
|||||||
.maximized = params.startMaximized,
|
.maximized = params.startMaximized,
|
||||||
});
|
});
|
||||||
|
|
||||||
gfxDevice.init(window.GetHandle(), params.appName, false);
|
initDevices();
|
||||||
imguiPass.init(window.GetHandle(), gfxDevice);
|
imguiPass.init(window.GetHandle(), gfxDevice);
|
||||||
|
resources.init(gfxDevice);
|
||||||
|
renderer.init(gfxDevice, resources, params.renderSize);
|
||||||
|
|
||||||
InputManager::GetInstance().Init();
|
|
||||||
Time::GetInstance().Update();
|
Time::GetInstance().Update();
|
||||||
|
|
||||||
customInitStarted = true;
|
customInitStarted = true;
|
||||||
customInit();
|
customInit();
|
||||||
cleanedUp = false;
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
try {
|
try {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -77,262 +88,306 @@ void App::init(const AppParams& params)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::initDevices()
|
||||||
|
{
|
||||||
|
ZoneScopedN("App::initDevices");
|
||||||
|
auto& jobs = JobSystem::GetInstance();
|
||||||
|
auto gfxInit = jobs.Async([this] { gfxDevice.init(window.GetHandle(), m_params.appName, false); });
|
||||||
|
auto audioInit = jobs.Async([] { AudioSystem::GetInstance().Init(); });
|
||||||
|
|
||||||
|
// SDL wants its subsystems initialised on the main thread
|
||||||
|
std::exception_ptr inputError;
|
||||||
|
try {
|
||||||
|
InputManager::GetInstance().Init();
|
||||||
|
} catch (...) {
|
||||||
|
inputError = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for both before rethrowing anything, cleanup must not race a running init
|
||||||
|
gfxInit.wait();
|
||||||
|
audioInit.wait();
|
||||||
|
if (inputError) {
|
||||||
|
std::rethrow_exception(inputError);
|
||||||
|
}
|
||||||
|
gfxInit.get();
|
||||||
|
audioInit.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::attachLayer(std::unique_ptr<AppLayer> newLayer)
|
||||||
|
{
|
||||||
|
if (layer) {
|
||||||
|
layer->onDetach();
|
||||||
|
}
|
||||||
|
layer = std::move(newLayer);
|
||||||
|
if (layer) {
|
||||||
|
layer->onAttach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::isPlaying() const
|
||||||
|
{
|
||||||
|
return SceneManager::GetInstance().IsPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::isPaused() const
|
||||||
|
{
|
||||||
|
return SceneManager::GetInstance().IsPaused();
|
||||||
|
}
|
||||||
|
|
||||||
|
float App::getRenderAspectRatio() const
|
||||||
|
{
|
||||||
|
const VkExtent3D extent = renderer.getDrawImage().extent;
|
||||||
|
return extent.height > 0 ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera& App::getViewCamera()
|
||||||
|
{
|
||||||
|
if (layer) {
|
||||||
|
if (Camera* camera = layer->viewCamera()) {
|
||||||
|
return *camera;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Camera* camera = gameCamera()) {
|
||||||
|
return *camera;
|
||||||
|
}
|
||||||
|
return fallbackCamera;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::resizeRenderTarget(const glm::ivec2& size)
|
||||||
|
{
|
||||||
|
renderer.resize(gfxDevice, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::collectUnusedResources()
|
||||||
|
{
|
||||||
|
CollectUnusedResources(SceneManager::GetInstance(), resources, gfxDevice);
|
||||||
|
AudioSystem::GetInstance().ReleaseUnusedClips();
|
||||||
|
}
|
||||||
|
|
||||||
void App::run()
|
void App::run()
|
||||||
{
|
{
|
||||||
ZoneScopedN("App::run");
|
ZoneScopedN("App::run");
|
||||||
|
|
||||||
Time::GetInstance().Update();
|
Time::GetInstance().Update();
|
||||||
|
simulation.fixedDeltaTime = static_cast<float>(Time::GetInstance().FixedDeltaTime());
|
||||||
const float fixedDt = static_cast<float>(Time::GetInstance().FixedDeltaTime());
|
|
||||||
const int maxSteps = 5;
|
|
||||||
float accumulator = 0.0f;
|
|
||||||
|
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
|
||||||
while (isRunning)
|
while (isRunning) {
|
||||||
{
|
|
||||||
TracyFrameScope tracyFrame;
|
TracyFrameScope tracyFrame;
|
||||||
ZoneScopedN("App::Frame");
|
ZoneScopedN("App::Frame");
|
||||||
|
|
||||||
Time::GetInstance().Update();
|
Time::GetInstance().Update();
|
||||||
float dt = static_cast<float>(Time::GetInstance().DeltaTime());
|
const float dt = std::min(static_cast<float>(Time::GetInstance().DeltaTime()), MaxFrameDelta);
|
||||||
|
if (dt > 0.0f) {
|
||||||
if (dt > 0.25f) dt = 0.25f;
|
avgFPS = std::lerp(avgFPS, 1.0f / dt, 0.1f);
|
||||||
|
|
||||||
if (dt > 0.0f)
|
|
||||||
{
|
|
||||||
float newFPS = 1.0f / dt;
|
|
||||||
avgFPS = std::lerp(avgFPS, newFPS, 0.1f);
|
|
||||||
TracyPlot("FPS", avgFPS);
|
TracyPlot("FPS", avgFPS);
|
||||||
TracyPlot("Delta Time ms", dt * 1000.0f);
|
|
||||||
}
|
}
|
||||||
|
simulation.accumulator += dt;
|
||||||
|
|
||||||
accumulator += dt;
|
processEvents();
|
||||||
|
if (!isRunning) {
|
||||||
{
|
|
||||||
ZoneScopedN("Input BeginFrame");
|
|
||||||
InputManager::GetInstance().BeginFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("SDL Events");
|
|
||||||
|
|
||||||
SDL_Event event;
|
|
||||||
while (SDL_PollEvent(&event))
|
|
||||||
{
|
|
||||||
ZoneScopedN("SDL Event");
|
|
||||||
|
|
||||||
imguiPass.handleEvent(event);
|
|
||||||
|
|
||||||
const bool mouseEvent =
|
|
||||||
event.type == SDL_MOUSEBUTTONDOWN ||
|
|
||||||
event.type == SDL_MOUSEBUTTONUP ||
|
|
||||||
event.type == SDL_MOUSEMOTION ||
|
|
||||||
event.type == SDL_MOUSEWHEEL;
|
|
||||||
|
|
||||||
if (event.type == SDL_QUIT)
|
|
||||||
{
|
|
||||||
isRunning = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ProcessEvent(event);
|
if (window.IsMinimized()) {
|
||||||
if (window.ConsumeResized())
|
|
||||||
{
|
|
||||||
resizePending = true;
|
|
||||||
lastResizeTime = std::chrono::steady_clock::now();
|
|
||||||
}
|
|
||||||
if (window.ConsumeCloseRequested())
|
|
||||||
{
|
|
||||||
isRunning = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool keyboardEvent =
|
|
||||||
event.type == SDL_KEYDOWN ||
|
|
||||||
event.type == SDL_KEYUP ||
|
|
||||||
event.type == SDL_TEXTINPUT;
|
|
||||||
|
|
||||||
const bool capturedByImgui =
|
|
||||||
(mouseEvent && !InputManager::GetInstance().IsMouseCaptured() && imguiPass.wantsMouse()) ||
|
|
||||||
(keyboardEvent && imguiPass.wantsKeyboard());
|
|
||||||
|
|
||||||
if (!capturedByImgui)
|
|
||||||
{
|
|
||||||
ZoneScopedN("Input ProcessEvent");
|
|
||||||
|
|
||||||
if (InputManager::GetInstance().ProcessEvent(event))
|
|
||||||
{
|
|
||||||
isRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRunning) break;
|
|
||||||
|
|
||||||
if (window.IsMinimized())
|
|
||||||
{
|
|
||||||
// The surface is 0x0 while minimized, there is nothing to render or present to
|
// The surface is 0x0 while minimized, there is nothing to render or present to
|
||||||
accumulator = 0.0f;
|
simulation.accumulator = 0.0f;
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume SDL events before updating the base camera so held and
|
JobSystem::GetInstance().DrainMainThreadQueue();
|
||||||
// newly pressed inputs are applied in the same frame.
|
if (layer) {
|
||||||
camera.Update(dt);
|
layer->onBeginFrame();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("ImGui BeginFrame");
|
|
||||||
imguiPass.beginFrame();
|
imguiPass.beginFrame();
|
||||||
|
if (layer) {
|
||||||
|
layer->onImGuiBegin(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("customUpdate");
|
ZoneScopedN("Update");
|
||||||
customUpdate(dt);
|
customUpdate(dt);
|
||||||
|
SceneManager::GetInstance().Update(dt);
|
||||||
|
SceneManager::GetInstance().LateUpdate(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_params.showDebugUi)
|
if (layer) {
|
||||||
{
|
ZoneScopedN("Layer ImGui");
|
||||||
ZoneScopedN("Debug ImGui");
|
layer->onImGui(dt);
|
||||||
drawWindowsMenu();
|
|
||||||
|
|
||||||
if (m_DebugWindows.inspector)
|
|
||||||
{
|
|
||||||
SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_DebugWindows.stats)
|
|
||||||
{
|
|
||||||
if (ImGui::Begin("Debug", &m_DebugWindows.stats))
|
|
||||||
{
|
|
||||||
ImGui::Text("FPS: %.2f", avgFPS);
|
|
||||||
}
|
|
||||||
ImGui::End();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_DebugWindows.physics)
|
|
||||||
{
|
|
||||||
drawPhysicsPanel(dt, fixedDt, accumulator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_DebugWindows.imageCache)
|
|
||||||
{
|
|
||||||
m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass);
|
|
||||||
}
|
|
||||||
|
|
||||||
TracyPlot("FPS", avgFPS);
|
|
||||||
TracyPlot("Frame Time ms", dt * 1000.0f);
|
|
||||||
TracyPlot("Accumulator ms", accumulator * 1000.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("ImGui EndFrame");
|
|
||||||
imguiPass.endFrame();
|
imguiPass.endFrame();
|
||||||
|
|
||||||
|
if (layer) {
|
||||||
|
layer->onEndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
runFixedSteps();
|
||||||
ZoneScopedN("FixedUpdate");
|
|
||||||
|
|
||||||
int steps = 0;
|
AudioSystem::GetInstance().Update(dt, isPlaying() && (isPaused() || simulation.paused), &getViewCamera());
|
||||||
|
|
||||||
if (m_PhysicsPaused)
|
bool skipFrame = false;
|
||||||
{
|
handleSwapchainResize(skipFrame);
|
||||||
// Important: prevent physics from building up a huge backlog while paused.
|
if (skipFrame) {
|
||||||
accumulator = 0.0f;
|
|
||||||
|
|
||||||
if (m_PhysicsStepOnce)
|
|
||||||
{
|
|
||||||
ZoneScopedN("Single Physics Step");
|
|
||||||
|
|
||||||
customFixedUpdate(fixedDt * m_PhysicsTimeScale);
|
|
||||||
|
|
||||||
steps = 1;
|
|
||||||
m_PhysicsStepOnce = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
while (accumulator >= fixedDt && steps < maxSteps)
|
|
||||||
{
|
|
||||||
ZoneScopedN("Fixed Step");
|
|
||||||
|
|
||||||
customFixedUpdate(fixedDt * m_PhysicsTimeScale);
|
|
||||||
|
|
||||||
accumulator -= fixedDt;
|
|
||||||
steps++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (steps == maxSteps)
|
|
||||||
{
|
|
||||||
accumulator = 0.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_PhysicsStepsLastFrame = steps;
|
|
||||||
|
|
||||||
TracyPlot("Fixed Steps", static_cast<int64_t>(steps));
|
|
||||||
}
|
|
||||||
|
|
||||||
Time::GetInstance().SetFixedAlpha(std::clamp(accumulator / fixedDt, 0.0f, 1.0f));
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("Swapchain Resize Check");
|
|
||||||
|
|
||||||
if (gfxDevice.needsSwapchainRecreate() || resizePending)
|
|
||||||
{
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
if (resizePending &&
|
|
||||||
now - lastResizeTime < std::chrono::milliseconds(100))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const glm::ivec2 drawableSize = window.GetDrawableSize();
|
renderFrame();
|
||||||
const int w = drawableSize.x;
|
|
||||||
const int h = drawableSize.y;
|
|
||||||
|
|
||||||
if (w == 0 || h == 0)
|
if (frameLimit) {
|
||||||
{
|
if (const auto sleepTime = Time::GetInstance().SleepDuration(); sleepTime.count() > 0) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("Recreate Swapchain");
|
|
||||||
if (!gfxDevice.recreateSwapchain(w, h))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
spdlog::info("Recreated swapchain at {}x{}", w, h);
|
|
||||||
imguiPass.onSwapchainRecreated();
|
|
||||||
onWindowResize(w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
resizePending = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("customDraw");
|
|
||||||
customDraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frameLimit)
|
|
||||||
{
|
|
||||||
ZoneScopedN("Frame Limit Sleep");
|
|
||||||
|
|
||||||
auto sleepTime = Time::GetInstance().SleepDuration();
|
|
||||||
if (sleepTime.count() > 0)
|
|
||||||
{
|
|
||||||
std::this_thread::sleep_for(sleepTime);
|
std::this_thread::sleep_for(sleepTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gfxDevice.waitIdle();
|
void App::processEvents()
|
||||||
|
{
|
||||||
|
ZoneScopedN("SDL Events");
|
||||||
|
auto& input = InputManager::GetInstance();
|
||||||
|
input.BeginFrame();
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
imguiPass.handleEvent(event);
|
||||||
|
|
||||||
|
if (event.type == SDL_QUIT) {
|
||||||
|
isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ProcessEvent(event);
|
||||||
|
if (window.ConsumeResized()) {
|
||||||
|
resizePending = true;
|
||||||
|
lastResizeTime = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
if (window.ConsumeCloseRequested()) {
|
||||||
|
isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImGui wants every event while one of its windows is hovered or focused, a layer can claim
|
||||||
|
// them back for the game (the editor viewport is an ImGui window)
|
||||||
|
const AppLayer::InputFocus focus = layer ? layer->gameInputFocus() : AppLayer::InputFocus{};
|
||||||
|
const bool mouseCaptured = input.IsMouseCaptured();
|
||||||
|
const bool capturedByImgui =
|
||||||
|
(IsMouseEvent(event) && !mouseCaptured && !focus.mouse && imguiPass.wantsMouse()) ||
|
||||||
|
(IsKeyboardEvent(event) && !focus.keyboard && imguiPass.wantsKeyboard());
|
||||||
|
|
||||||
|
if (!capturedByImgui && input.ProcessEvent(event)) {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::runFixedSteps()
|
||||||
|
{
|
||||||
|
ZoneScopedN("FixedUpdate");
|
||||||
|
const float step = simulation.fixedDeltaTime * simulation.timeScale;
|
||||||
|
int steps = 0;
|
||||||
|
|
||||||
|
const auto fixedUpdate = [&] {
|
||||||
|
customFixedUpdate(step);
|
||||||
|
SceneManager::GetInstance().FixedUpdate(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (simulation.paused) {
|
||||||
|
simulation.accumulator = 0.0f;
|
||||||
|
if (simulation.stepOnce) {
|
||||||
|
fixedUpdate();
|
||||||
|
steps = 1;
|
||||||
|
simulation.stepOnce = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (simulation.accumulator >= simulation.fixedDeltaTime && steps < MaxFixedSteps) {
|
||||||
|
fixedUpdate();
|
||||||
|
simulation.accumulator -= simulation.fixedDeltaTime;
|
||||||
|
++steps;
|
||||||
|
}
|
||||||
|
if (steps == MaxFixedSteps) {
|
||||||
|
simulation.accumulator = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
simulation.stepsLastFrame = steps;
|
||||||
|
Time::GetInstance().SetFixedAlpha(std::clamp(simulation.accumulator / simulation.fixedDeltaTime, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handleSwapchainResize(bool& skipFrame)
|
||||||
|
{
|
||||||
|
if (!gfxDevice.needsSwapchainRecreate() && !resizePending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipFrame = true;
|
||||||
|
if (resizePending && std::chrono::steady_clock::now() - lastResizeTime < std::chrono::milliseconds(100)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glm::ivec2 size = window.GetDrawableSize();
|
||||||
|
if (size.x == 0 || size.y == 0 || !gfxDevice.recreateSwapchain(size.x, size.y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info("Recreated swapchain at {}x{}", size.x, size.y);
|
||||||
|
imguiPass.onSwapchainRecreated();
|
||||||
|
if (!layer || !layer->hostsViewport()) {
|
||||||
|
resizeRenderTarget(size);
|
||||||
|
}
|
||||||
|
onWindowResize(size.x, size.y);
|
||||||
|
resizePending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::renderFrame()
|
||||||
|
{
|
||||||
|
ZoneScopedN("Render");
|
||||||
|
const VkCommandBuffer cmd = gfxDevice.beginFrame();
|
||||||
|
if (cmd == VK_NULL_HANDLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.beginDrawing(gfxDevice);
|
||||||
|
customRender(renderer);
|
||||||
|
if (layer) {
|
||||||
|
layer->onRender(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera& camera = getViewCamera();
|
||||||
|
if (const float aspectRatio = getRenderAspectRatio(); camera.GetAspectRatio() != aspectRatio) {
|
||||||
|
camera.setAspectRatio(aspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& sceneManager = SceneManager::GetInstance();
|
||||||
|
const SceneEnvironment environment = sceneManager.GetActiveScenes().empty()
|
||||||
|
? SceneEnvironment{}
|
||||||
|
: sceneManager.GetCurrentScene().GetEnvironment();
|
||||||
|
const GameRenderer::SceneData sceneData{
|
||||||
|
.camera = camera,
|
||||||
|
.ambientColor = environment.ambientColor,
|
||||||
|
.ambientIntensity = environment.ambientIntensity,
|
||||||
|
.fogColor = environment.fogColor,
|
||||||
|
.fogDensity = environment.fogDensity,
|
||||||
|
.skyboxRotation = environment.skyboxRotation,
|
||||||
|
.exposure = environment.exposure,
|
||||||
|
.tonemapper = environment.tonemapper,
|
||||||
|
};
|
||||||
|
|
||||||
|
sceneManager.Render(RenderContext{
|
||||||
|
.renderer = renderer,
|
||||||
|
.camera = camera,
|
||||||
|
.sceneData = sceneData,
|
||||||
|
.canvasFrame = layer ? layer->canvasFrame() : std::nullopt,
|
||||||
|
});
|
||||||
|
renderer.endDrawing();
|
||||||
|
renderer.draw(cmd, gfxDevice, camera, sceneData);
|
||||||
|
|
||||||
|
gfxDevice.endFrame(cmd, renderer.getDrawImage(), {
|
||||||
|
.clearColor = {{0.0f, 0.0f, 0.0f, 1.0f}},
|
||||||
|
.imguiPass = &imguiPass,
|
||||||
|
.copyDrawImageToSwapchain = !layer || !layer->hostsViewport(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::cleanup()
|
void App::cleanup()
|
||||||
@@ -355,10 +410,22 @@ void App::cleanup()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The one place we wait for the GPU on shutdown: the cleanup calls below destroy
|
||||||
|
// pipelines and other objects directly, which is only valid once nothing is in flight.
|
||||||
|
attempt([this] { gfxDevice.waitIdle(); });
|
||||||
|
attempt([] { JobSystem::GetInstance().Shutdown(); });
|
||||||
|
|
||||||
|
attempt([this] {
|
||||||
|
if (layer) {
|
||||||
|
layer->onDetach();
|
||||||
|
layer.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
if (customInitStarted) {
|
if (customInitStarted) {
|
||||||
attempt([this] { customCleanup(); });
|
attempt([this] { customCleanup(); });
|
||||||
}
|
}
|
||||||
attempt([] { SceneManager::GetInstance().Destroy(); });
|
attempt([] { SceneManager::GetInstance().Destroy(); });
|
||||||
|
attempt([] { AudioSystem::GetInstance().Shutdown(); });
|
||||||
attempt([] { FontCache::GetInstance().Clear(); });
|
attempt([] { FontCache::GetInstance().Clear(); });
|
||||||
attempt([this] { renderer.cleanup(gfxDevice); });
|
attempt([this] { renderer.cleanup(gfxDevice); });
|
||||||
attempt([this] { imguiPass.cleanup(); });
|
attempt([this] { imguiPass.cleanup(); });
|
||||||
@@ -376,76 +443,3 @@ void App::cleanup()
|
|||||||
std::rethrow_exception(firstException);
|
std::rethrow_exception(firstException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::drawWindowsMenu()
|
|
||||||
{
|
|
||||||
// ImGui appends to the main menu bar, so this ends up next to whatever menus the game added
|
|
||||||
if (!ImGui::BeginMainMenuBar())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::BeginMenu("Windows"))
|
|
||||||
{
|
|
||||||
ImGui::MenuItem("Debug", nullptr, &m_DebugWindows.stats);
|
|
||||||
ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics);
|
|
||||||
ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector);
|
|
||||||
ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache);
|
|
||||||
|
|
||||||
customWindowsMenu();
|
|
||||||
|
|
||||||
ImGui::EndMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndMainMenuBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator)
|
|
||||||
{
|
|
||||||
if (!ImGui::Begin("Physics", &m_DebugWindows.physics))
|
|
||||||
{
|
|
||||||
ImGui::End();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f);
|
|
||||||
ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f);
|
|
||||||
ImGui::Text("Accumulator: %.3f ms", accumulator * 1000.0f);
|
|
||||||
ImGui::Text("Steps last frame: %d", m_PhysicsStepsLastFrame);
|
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
if (ImGui::Button(m_PhysicsPaused ? "Resume Physics" : "Pause Physics"))
|
|
||||||
{
|
|
||||||
m_PhysicsPaused = !m_PhysicsPaused;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
if (!m_PhysicsPaused)
|
|
||||||
{
|
|
||||||
ImGui::BeginDisabled();
|
|
||||||
ImGui::Button("Step Physics");
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (ImGui::Button("Step Physics"))
|
|
||||||
{
|
|
||||||
m_PhysicsStepOnce = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Checkbox("Paused", &m_PhysicsPaused);
|
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
ImGui::SliderFloat("Physics Time Scale", &m_PhysicsTimeScale, 0.0f, 2.0f, "%.2fx");
|
|
||||||
|
|
||||||
if (ImGui::Button("Reset Time Scale"))
|
|
||||||
{
|
|
||||||
m_PhysicsTimeScale = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::End();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#include <destrum/Assets/AssetDatabase.h>
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/Assets/AssetTypes.h>
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
[[nodiscard]] bool IsVirtualPath(std::string_view path)
|
||||||
|
{
|
||||||
|
return path.find("://") != std::string_view::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<AssetGuid> ReadMetaFile(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const std::vector<std::uint8_t> bytes = AssetFS::GetInstance().ReadFile(path);
|
||||||
|
const auto meta = nlohmann::json::parse(bytes.begin(), bytes.end());
|
||||||
|
return AssetGuid::Parse(meta.at("guid").get<std::string>());
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::warn("Ignoring broken asset meta file {}: {}", path.string(), exception.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetGuid AssetGuid::Generate()
|
||||||
|
{
|
||||||
|
static std::mt19937_64 random{std::random_device{}() ^ (static_cast<std::uint64_t>(std::random_device{}()) << 32)};
|
||||||
|
AssetGuid guid;
|
||||||
|
while (!guid.IsValid()) {
|
||||||
|
guid = {random(), random()};
|
||||||
|
}
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AssetGuid> AssetGuid::Parse(std::string_view text)
|
||||||
|
{
|
||||||
|
if (text.size() != 32) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
AssetGuid guid;
|
||||||
|
const auto parseHalf = [](std::string_view half, std::uint64_t& out) {
|
||||||
|
const auto [end, error] = std::from_chars(half.data(), half.data() + half.size(), out, 16);
|
||||||
|
return error == std::errc{} && end == half.data() + half.size();
|
||||||
|
};
|
||||||
|
if (!parseHalf(text.substr(0, 16), guid.high) || !parseHalf(text.substr(16), guid.low) || !guid.IsValid()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AssetGuid::ToString() const
|
||||||
|
{
|
||||||
|
return std::format("{:016x}{:016x}", high, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase::MetaFileScope::MetaFileScope()
|
||||||
|
{
|
||||||
|
++AssetDatabase::GetInstance().m_MetaFileScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase::MetaFileScope::~MetaFileScope()
|
||||||
|
{
|
||||||
|
--AssetDatabase::GetInstance().m_MetaFileScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetDatabase::Clear()
|
||||||
|
{
|
||||||
|
m_Guids.clear();
|
||||||
|
m_Paths.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetDatabase::Refresh()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
const auto& assetFs = AssetFS::GetInstance();
|
||||||
|
for (const FSMount& mount : assetFs.GetMounts()) {
|
||||||
|
for (const std::filesystem::path& file : assetFs.ListFiles(mount)) {
|
||||||
|
if (!AssetTypes::IsMeta(file.string())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (const auto guid = ReadMetaFile(file)) {
|
||||||
|
std::filesystem::path asset = file;
|
||||||
|
asset.replace_extension();
|
||||||
|
Register(mount.scheme + "://" + asset.lexically_relative(mount.root).generic_string(), *guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetDatabase::Register(std::string_view path, const AssetGuid& guid)
|
||||||
|
{
|
||||||
|
if (const auto existing = m_Paths.find(guid); existing != m_Paths.end() && existing->second != path) {
|
||||||
|
spdlog::warn("Assets {} and {} share the id {}, copy assets without their .meta file",
|
||||||
|
existing->second, path, guid.ToString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_Guids.insert_or_assign(std::string{path}, guid);
|
||||||
|
m_Paths.insert_or_assign(guid, std::string{path});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AssetGuid> AssetDatabase::FindGuid(std::string_view path) const
|
||||||
|
{
|
||||||
|
const auto it = m_Guids.find(std::string{path});
|
||||||
|
return it != m_Guids.end() ? std::optional{it->second} : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> AssetDatabase::FindPath(const AssetGuid& guid) const
|
||||||
|
{
|
||||||
|
const auto it = m_Paths.find(guid);
|
||||||
|
return it != m_Paths.end() ? std::optional{it->second} : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AssetGuid> AssetDatabase::GuidForSave(std::string_view path)
|
||||||
|
{
|
||||||
|
if (auto guid = FindGuid(path)) {
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
if (m_MetaFileScopes == 0 || !IsVirtualPath(path)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& assetFs = AssetFS::GetInstance();
|
||||||
|
if (!assetFs.Exists(assetFs.GetFullPath(path))) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetGuid guid = AssetGuid::Generate();
|
||||||
|
const nlohmann::json meta{{"guid", guid.ToString()}};
|
||||||
|
if (!assetFs.WriteFile(std::string{path} + std::string{AssetTypes::MetaExtension}, meta.dump(4))) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
Register(path, guid);
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json AssetDatabase::WriteReference(std::string_view path, std::string_view subresource)
|
||||||
|
{
|
||||||
|
nlohmann::json reference{{"path", path}};
|
||||||
|
if (!subresource.empty()) {
|
||||||
|
reference["sub"] = subresource;
|
||||||
|
}
|
||||||
|
if (const auto guid = GuidForSave(path)) {
|
||||||
|
reference["guid"] = guid->ToString();
|
||||||
|
}
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AssetReference> AssetDatabase::ReadReference(const nlohmann::json& reference) const
|
||||||
|
{
|
||||||
|
if (!reference.is_object() || !reference.contains("path") || !reference.at("path").is_string()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetReference result{
|
||||||
|
.path = reference.at("path").get<std::string>(),
|
||||||
|
.subresource = reference.value("sub", std::string{}),
|
||||||
|
};
|
||||||
|
if (const auto guid = AssetGuid::Parse(reference.value("guid", std::string{}))) {
|
||||||
|
if (auto current = FindPath(*guid); current && *current != result.path) {
|
||||||
|
spdlog::info("{} moved to {}, following its id", result.path, *current);
|
||||||
|
result.path = std::move(*current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#ifndef DESTRUM_ASSETFSIOSYSTEM_H
|
||||||
|
#define DESTRUM_ASSETFSIOSYSTEM_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <assimp/IOSystem.hpp>
|
||||||
|
#include <assimp/MemoryIOWrapper.h>
|
||||||
|
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
|
class AssetFSIOSystem final : public Assimp::IOSystem {
|
||||||
|
public:
|
||||||
|
bool Exists(const char* file) const override {
|
||||||
|
return AssetFS::GetInstance().Exists(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
char getOsSeparator() const override {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
Assimp::IOStream* Open(const char* file, const char* mode) override {
|
||||||
|
if (std::string_view{mode}.find_first_of("wa+") != std::string_view::npos) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> bytes;
|
||||||
|
try {
|
||||||
|
bytes = AssetFS::GetInstance().ReadFile(file);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto* buffer = new std::uint8_t[bytes.size()];
|
||||||
|
std::memcpy(buffer, bytes.data(), bytes.size());
|
||||||
|
return new Assimp::MemoryIOStream(buffer, bytes.size(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Close(Assimp::IOStream* stream) override {
|
||||||
|
delete stream;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_ASSETFSIOSYSTEM_H
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
#include <destrum/Assets/AssetManager.h>
|
#include <destrum/Assets/AssetManager.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <destrum/Assets/ModelFile.h>
|
#include <destrum/Assets/ModelFile.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
|
#include "AssetFSIOSystem.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::string ToReference(std::string_view reference) {
|
std::string ToReference(std::string_view reference) {
|
||||||
if (reference.empty()) {
|
if (reference.empty()) {
|
||||||
@@ -30,6 +33,14 @@ namespace {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assimp opens files itself, so with packed assets it has to go through AssetFS
|
||||||
|
std::unique_ptr<Assimp::IOSystem> MakeIOSystem() {
|
||||||
|
if (!AssetFS::GetInstance().IsPacked()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::make_unique<AssetFSIOSystem>();
|
||||||
|
}
|
||||||
|
|
||||||
// Cooked texture paths are relative to the model file, imported ones are absolute
|
// Cooked texture paths are relative to the model file, imported ones are absolute
|
||||||
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
|
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
|
||||||
for (ModelDoc::MaterialInfo& material : model.materials) {
|
for (ModelDoc::MaterialInfo& material : model.materials) {
|
||||||
@@ -95,7 +106,7 @@ ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
|
|||||||
const ModelDoc::LoadOptions& options) const
|
const ModelDoc::LoadOptions& options) const
|
||||||
{
|
{
|
||||||
if (const auto cookedPath = FindCookedModel(reference)) {
|
if (const auto cookedPath = FindCookedModel(reference)) {
|
||||||
ModelDoc::ModelAsset model = ModelFile::ReadFromFile(*cookedPath);
|
ModelDoc::ModelAsset model = ModelFile::Read(AssetFS::GetInstance().ReadFile(*cookedPath));
|
||||||
if (ModelFile::SupportsLoadOptions(model, options)) {
|
if (ModelFile::SupportsLoadOptions(model, options)) {
|
||||||
ModelFile::ApplyLoadOptions(model, options);
|
ModelFile::ApplyLoadOptions(model, options);
|
||||||
ResolveTexturePaths(model, cookedPath->parent_path());
|
ResolveTexturePaths(model, cookedPath->parent_path());
|
||||||
@@ -105,7 +116,7 @@ ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
|
|||||||
spdlog::debug("Importing {}, its cooked model doesn't match the requested load options", reference);
|
spdlog::debug("Importing {}, its cooked model doesn't match the requested load options", reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options);
|
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options, MakeIOSystem());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
||||||
@@ -115,7 +126,8 @@ std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
|||||||
const std::string sourceReference = ToReference(reference);
|
const std::string sourceReference = ToReference(reference);
|
||||||
auto clips = ModelDoc::LoadAnimationClips(
|
auto clips = ModelDoc::LoadAnimationClips(
|
||||||
Resolve(sourceReference).generic_string(),
|
Resolve(sourceReference).generic_string(),
|
||||||
targetSkeleton);
|
targetSkeleton,
|
||||||
|
MakeIOSystem());
|
||||||
for (std::size_t index = 0; index < clips.size(); ++index) {
|
for (std::size_t index = 0; index < clips.size(); ++index) {
|
||||||
auto& clip = clips[index];
|
auto& clip = clips[index];
|
||||||
clip.assetReference.path = sourceReference;
|
clip.assetReference.path = sourceReference;
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#include <destrum/Assets/PackFile.h>
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "BinaryStream.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using AssetBinary::BinaryReader;
|
||||||
|
using AssetBinary::BinaryWriter;
|
||||||
|
|
||||||
|
constexpr AssetBinary::Magic Magic{'D', 'P', 'A', 'K'};
|
||||||
|
constexpr std::string_view Kind = "pack";
|
||||||
|
constexpr std::size_t HeaderSize = sizeof(AssetBinary::Magic) + sizeof(std::uint32_t) + sizeof(std::uint64_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PackFile::Write(const std::vector<Source>& files, const std::filesystem::path& path) {
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!out) {
|
||||||
|
throw std::runtime_error("Failed to open file for writing: " + path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryWriter header;
|
||||||
|
header.WriteHeader(Magic, Version);
|
||||||
|
header.Write(std::uint64_t{0});
|
||||||
|
const std::vector<std::uint8_t> headerBytes = header.TakeBytes();
|
||||||
|
out.write(reinterpret_cast<const char*>(headerBytes.data()), static_cast<std::streamsize>(headerBytes.size()));
|
||||||
|
|
||||||
|
std::set<std::string> seen;
|
||||||
|
BinaryWriter index;
|
||||||
|
index.WriteCount(files.size());
|
||||||
|
for (const Source& file : files) {
|
||||||
|
if (!seen.insert(file.path).second) {
|
||||||
|
throw std::runtime_error("Pack has two files at " + file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream in(file.file, std::ios::binary);
|
||||||
|
if (!in) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + file.file.string());
|
||||||
|
}
|
||||||
|
const auto offset = static_cast<std::uint64_t>(out.tellp());
|
||||||
|
// Inserting an empty stream buffer sets failbit
|
||||||
|
if (std::filesystem::file_size(file.file) > 0) {
|
||||||
|
out << in.rdbuf();
|
||||||
|
}
|
||||||
|
const auto size = static_cast<std::uint64_t>(out.tellp()) - offset;
|
||||||
|
|
||||||
|
index.WriteString(file.path);
|
||||||
|
index.Write(offset);
|
||||||
|
index.Write(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto indexOffset = static_cast<std::uint64_t>(out.tellp());
|
||||||
|
const std::vector<std::uint8_t> indexBytes = index.TakeBytes();
|
||||||
|
out.write(reinterpret_cast<const char*>(indexBytes.data()), static_cast<std::streamsize>(indexBytes.size()));
|
||||||
|
out.seekp(static_cast<std::streamoff>(HeaderSize - sizeof(std::uint64_t)));
|
||||||
|
out.write(reinterpret_cast<const char*>(&indexOffset), sizeof(indexOffset));
|
||||||
|
if (!out) {
|
||||||
|
throw std::runtime_error("Failed to write file: " + path.string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PackFile::Reader::Reader(std::filesystem::path path)
|
||||||
|
: m_Path(std::move(path)) {
|
||||||
|
std::ifstream file(m_Path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + m_Path.string());
|
||||||
|
}
|
||||||
|
const auto fileSize = static_cast<std::uint64_t>(file.tellg());
|
||||||
|
|
||||||
|
std::array<std::uint8_t, HeaderSize> headerBytes{};
|
||||||
|
file.seekg(0);
|
||||||
|
file.read(reinterpret_cast<char*>(headerBytes.data()), static_cast<std::streamsize>(headerBytes.size()));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Not a " + std::string{Kind} + " file: " + m_Path.string());
|
||||||
|
}
|
||||||
|
BinaryReader header{headerBytes};
|
||||||
|
header.ReadHeader(Magic, Version, Kind);
|
||||||
|
const auto indexOffset = header.Read<std::uint64_t>();
|
||||||
|
if (indexOffset < HeaderSize || indexOffset > fileSize) {
|
||||||
|
throw std::runtime_error("Pack file is corrupt: index is out of range");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> indexBytes(static_cast<std::size_t>(fileSize - indexOffset));
|
||||||
|
file.seekg(static_cast<std::streamoff>(indexOffset));
|
||||||
|
file.read(reinterpret_cast<char*>(indexBytes.data()), static_cast<std::streamsize>(indexBytes.size()));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read file: " + m_Path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryReader index{indexBytes};
|
||||||
|
m_Entries.resize(index.ReadCount(3 * sizeof(std::uint64_t)));
|
||||||
|
for (std::size_t i = 0; i < m_Entries.size(); ++i) {
|
||||||
|
Entry& entry = m_Entries[i];
|
||||||
|
entry.path = index.ReadString();
|
||||||
|
entry.offset = index.Read<std::uint64_t>();
|
||||||
|
entry.size = index.Read<std::uint64_t>();
|
||||||
|
if (entry.offset < HeaderSize || entry.offset > indexOffset || entry.size > indexOffset - entry.offset) {
|
||||||
|
throw std::runtime_error("Pack file is corrupt: " + entry.path + " is out of range");
|
||||||
|
}
|
||||||
|
if (!m_ByPath.emplace(entry.path, i).second) {
|
||||||
|
throw std::runtime_error("Pack file is corrupt: " + entry.path + " is listed twice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.ExpectEnd(Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PackFile::Entry* PackFile::Reader::Find(std::string_view path) const {
|
||||||
|
const auto it = m_ByPath.find(std::string{path});
|
||||||
|
return it != m_ByPath.end() ? &m_Entries[it->second] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens its own stream so loads on worker threads don't share a file position
|
||||||
|
std::vector<std::uint8_t> PackFile::Reader::Read(const Entry& entry) const {
|
||||||
|
std::ifstream file(m_Path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + m_Path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(entry.size));
|
||||||
|
file.seekg(static_cast<std::streamoff>(entry.offset));
|
||||||
|
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read " + entry.path + " from " + m_Path.string());
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ std::size_t TextureFile::BytesPerPixel(PixelFormat format) {
|
|||||||
switch (format) {
|
switch (format) {
|
||||||
case PixelFormat::RGBA16Float:
|
case PixelFormat::RGBA16Float:
|
||||||
return 4 * sizeof(std::uint16_t);
|
return 4 * sizeof(std::uint16_t);
|
||||||
|
case PixelFormat::RGBA8:
|
||||||
|
return 4;
|
||||||
}
|
}
|
||||||
throw std::runtime_error("Unknown cooked texture pixel format");
|
throw std::runtime_error("Unknown cooked texture pixel format");
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,19 @@ TextureFile::Texture TextureFile::FromRGBA32Float(std::uint32_t width, std::uint
|
|||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextureFile::Texture TextureFile::FromRGBA8(std::uint32_t width, std::uint32_t height, std::span<const std::uint8_t> rgba) {
|
||||||
|
if (rgba.size() != std::size_t{width} * height * 4) {
|
||||||
|
throw std::invalid_argument("RGBA pixel data doesn't match the texture size");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Texture{
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.format = PixelFormat::RGBA8,
|
||||||
|
.pixels = {rgba.begin(), rgba.end()},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::uint8_t> TextureFile::Write(const Texture& texture) {
|
std::vector<std::uint8_t> TextureFile::Write(const Texture& texture) {
|
||||||
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||||
throw std::invalid_argument("Texture pixel data doesn't match its size and format");
|
throw std::invalid_argument("Texture pixel data doesn't match its size and format");
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
#include <destrum/Audio/AudioDecoder.h>
|
||||||
|
|
||||||
|
#include <destrum/Assets/AssetTypes.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
#include <stb_vorbis.c>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::uint16_t FormatPcm = 1;
|
||||||
|
constexpr std::uint16_t FormatFloat = 3;
|
||||||
|
constexpr std::uint16_t FormatExtensible = 0xFFFE;
|
||||||
|
|
||||||
|
// OpenAL only has mono and stereo formats in core
|
||||||
|
constexpr int MaxChannels = 2;
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint16_t ReadU16(std::span<const std::uint8_t> bytes, std::size_t offset)
|
||||||
|
{
|
||||||
|
return static_cast<std::uint16_t>(bytes[offset] | (bytes[offset + 1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t ReadU32(std::span<const std::uint8_t> bytes, std::size_t offset)
|
||||||
|
{
|
||||||
|
return static_cast<std::uint32_t>(bytes[offset]) |
|
||||||
|
(static_cast<std::uint32_t>(bytes[offset + 1]) << 8) |
|
||||||
|
(static_cast<std::uint32_t>(bytes[offset + 2]) << 16) |
|
||||||
|
(static_cast<std::uint32_t>(bytes[offset + 3]) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Tag(std::span<const std::uint8_t> bytes, std::size_t offset, const char (&tag)[5])
|
||||||
|
{
|
||||||
|
return std::memcmp(bytes.data() + offset, tag, 4) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::int16_t FloatToPcm(float value)
|
||||||
|
{
|
||||||
|
return static_cast<std::int16_t>(std::lround(std::clamp(value, -1.0f, 1.0f) * 32767.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::int16_t ReadSample(std::span<const std::uint8_t> data, std::size_t offset,
|
||||||
|
std::uint16_t format, std::uint16_t bits)
|
||||||
|
{
|
||||||
|
if (format == FormatFloat) {
|
||||||
|
float value = 0.0f;
|
||||||
|
const std::uint32_t raw = ReadU32(data, offset);
|
||||||
|
std::memcpy(&value, &raw, sizeof(value));
|
||||||
|
return std::isfinite(value) ? FloatToPcm(value) : std::int16_t{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (bits) {
|
||||||
|
case 8:
|
||||||
|
return static_cast<std::int16_t>((static_cast<int>(data[offset]) - 128) << 8);
|
||||||
|
case 16:
|
||||||
|
return static_cast<std::int16_t>(ReadU16(data, offset));
|
||||||
|
case 24:
|
||||||
|
return static_cast<std::int16_t>(data[offset + 1] | (data[offset + 2] << 8));
|
||||||
|
default:
|
||||||
|
return static_cast<std::int16_t>(ReadU32(data, offset) >> 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioData> AudioDecoder::DecodeWav(std::span<const std::uint8_t> bytes)
|
||||||
|
{
|
||||||
|
if (bytes.size() < 12 || !Tag(bytes, 0, "RIFF") || !Tag(bytes, 8, "WAVE")) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t format = 0;
|
||||||
|
std::uint16_t channels = 0;
|
||||||
|
std::uint32_t sampleRate = 0;
|
||||||
|
std::uint16_t blockAlign = 0;
|
||||||
|
std::uint16_t bits = 0;
|
||||||
|
std::span<const std::uint8_t> data;
|
||||||
|
bool hasFormat = false;
|
||||||
|
bool hasData = false;
|
||||||
|
|
||||||
|
std::size_t offset = 12;
|
||||||
|
while (offset + 8 <= bytes.size()) {
|
||||||
|
const std::size_t chunkSize = ReadU32(bytes, offset + 4);
|
||||||
|
const std::size_t chunkStart = offset + 8;
|
||||||
|
// Writers that stream sometimes leave the data size at 0xFFFFFFFF, take what is there
|
||||||
|
const std::size_t available = std::min(chunkSize, bytes.size() - chunkStart);
|
||||||
|
|
||||||
|
if (Tag(bytes, offset, "fmt ")) {
|
||||||
|
if (available < 16) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
format = ReadU16(bytes, chunkStart);
|
||||||
|
channels = ReadU16(bytes, chunkStart + 2);
|
||||||
|
sampleRate = ReadU32(bytes, chunkStart + 4);
|
||||||
|
blockAlign = ReadU16(bytes, chunkStart + 12);
|
||||||
|
bits = ReadU16(bytes, chunkStart + 14);
|
||||||
|
if (format == FormatExtensible) {
|
||||||
|
if (available < 26) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
format = ReadU16(bytes, chunkStart + 24);
|
||||||
|
}
|
||||||
|
hasFormat = true;
|
||||||
|
} else if (Tag(bytes, offset, "data")) {
|
||||||
|
data = bytes.subspan(chunkStart, available);
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkSize > bytes.size() - chunkStart) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Chunks are padded to an even size
|
||||||
|
offset = chunkStart + chunkSize + (chunkSize & 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFormat || !hasData) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool pcm = format == FormatPcm && (bits == 8 || bits == 16 || bits == 24 || bits == 32);
|
||||||
|
const bool floats = format == FormatFloat && bits == 32;
|
||||||
|
if ((!pcm && !floats) || channels == 0 || channels > MaxChannels || sampleRate == 0 ||
|
||||||
|
sampleRate > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
|
||||||
|
spdlog::warn("Unsupported WAV: format {}, {} bits, {} channels, {} Hz", format, bits, channels, sampleRate);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t bytesPerSample = bits / 8u;
|
||||||
|
if (blockAlign < bytesPerSample * channels) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioData result;
|
||||||
|
result.channels = channels;
|
||||||
|
result.sampleRate = static_cast<int>(sampleRate);
|
||||||
|
|
||||||
|
const std::size_t frames = data.size() / blockAlign;
|
||||||
|
result.samples.reserve(frames * channels);
|
||||||
|
for (std::size_t frame = 0; frame < frames; ++frame) {
|
||||||
|
for (std::size_t channel = 0; channel < channels; ++channel) {
|
||||||
|
result.samples.push_back(ReadSample(data, frame * blockAlign + channel * bytesPerSample, format, bits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioData> AudioDecoder::DecodeOgg(std::span<const std::uint8_t> bytes)
|
||||||
|
{
|
||||||
|
if (bytes.empty() || bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int channels = 0;
|
||||||
|
int sampleRate = 0;
|
||||||
|
short* decoded = nullptr;
|
||||||
|
const int frames = stb_vorbis_decode_memory(bytes.data(), static_cast<int>(bytes.size()), &channels, &sampleRate, &decoded);
|
||||||
|
if (frames < 0 || decoded == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioData result;
|
||||||
|
result.channels = channels;
|
||||||
|
result.sampleRate = sampleRate;
|
||||||
|
result.samples.assign(decoded, decoded + static_cast<std::size_t>(frames) * static_cast<std::size_t>(channels));
|
||||||
|
std::free(decoded);
|
||||||
|
|
||||||
|
if (channels <= 0 || channels > MaxChannels || sampleRate <= 0) {
|
||||||
|
spdlog::warn("Unsupported OGG: {} channels, {} Hz", channels, sampleRate);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioData> AudioDecoder::Decode(std::span<const std::uint8_t> bytes, std::string_view path)
|
||||||
|
{
|
||||||
|
const std::string extension = AssetTypes::Extension(path);
|
||||||
|
if (extension == ".wav") {
|
||||||
|
return DecodeWav(bytes);
|
||||||
|
}
|
||||||
|
if (extension == ".ogg") {
|
||||||
|
return DecodeOgg(bytes);
|
||||||
|
}
|
||||||
|
spdlog::warn("Unsupported audio format: {}", path);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
#include <destrum/Audio/AudioSystem.h>
|
||||||
|
|
||||||
|
#include <destrum/Audio/AudioDecoder.h>
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Graphics/Camera.h>
|
||||||
|
|
||||||
|
#include <AL/al.h>
|
||||||
|
#include <AL/alc.h>
|
||||||
|
#include <AL/alext.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <exception>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::size_t MaxOneShots = 32;
|
||||||
|
// Above this the object teleported (respawn, camera cut), a doppler shift would only sound wrong
|
||||||
|
constexpr float MaxSpeed = 100.0f;
|
||||||
|
constexpr float VelocitySmoothingTime = 0.1f;
|
||||||
|
|
||||||
|
[[nodiscard]] ALint SourceState(ALuint source)
|
||||||
|
{
|
||||||
|
ALint state = AL_STOPPED;
|
||||||
|
alGetSourcei(source, AL_SOURCE_STATE, &state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physics moves objects in fixed steps, so the raw per frame velocity jumps between 0 and a spike and doppler warbles
|
||||||
|
glm::vec3 SmoothAudioVelocity(const glm::vec3& current, const glm::vec3& previous, const glm::vec3& smoothed, float dt)
|
||||||
|
{
|
||||||
|
if (dt <= 0.0f) {
|
||||||
|
return smoothed;
|
||||||
|
}
|
||||||
|
const glm::vec3 velocity = (current - previous) / dt;
|
||||||
|
if (glm::length(velocity) > MaxSpeed) {
|
||||||
|
return glm::vec3{0.0f};
|
||||||
|
}
|
||||||
|
return glm::mix(smoothed, velocity, 1.0f - std::exp(-dt / VelocitySmoothingTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioClip::AudioClip(std::string path, std::uint32_t buffer, int channels, int sampleRate, float duration)
|
||||||
|
: m_Path(std::move(path))
|
||||||
|
, m_Buffer(buffer)
|
||||||
|
, m_Channels(channels)
|
||||||
|
, m_SampleRate(sampleRate)
|
||||||
|
, m_Duration(duration)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioClip::~AudioClip()
|
||||||
|
{
|
||||||
|
if (m_Buffer != 0 && AudioSystem::GetInstance().IsAvailable()) {
|
||||||
|
const ALuint buffer = m_Buffer;
|
||||||
|
alDeleteBuffers(1, &buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioSystem::~AudioSystem()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Shutdown();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioSystem::Init()
|
||||||
|
{
|
||||||
|
if (IsAvailable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Device = alcOpenDevice(nullptr);
|
||||||
|
if (m_Device == nullptr) {
|
||||||
|
spdlog::warn("No audio device, sound is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Context = alcCreateContext(m_Device, nullptr);
|
||||||
|
if (m_Context == nullptr || alcMakeContextCurrent(m_Context) == ALC_FALSE) {
|
||||||
|
spdlog::warn("Could not create an OpenAL context, sound is disabled");
|
||||||
|
if (m_Context != nullptr) {
|
||||||
|
alcDestroyContext(m_Context);
|
||||||
|
m_Context = nullptr;
|
||||||
|
}
|
||||||
|
alcCloseDevice(m_Device);
|
||||||
|
m_Device = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
|
||||||
|
if (alIsExtensionPresent("AL_SOFT_source_spatialize") == AL_FALSE) {
|
||||||
|
spdlog::warn("AL_SOFT_source_spatialize is missing, stereo clips will not be positioned");
|
||||||
|
}
|
||||||
|
alListenerf(AL_GAIN, m_MasterVolume);
|
||||||
|
|
||||||
|
const ALCchar* name = alcGetString(m_Device, ALC_DEVICE_SPECIFIER);
|
||||||
|
spdlog::info("Audio device: {}", name != nullptr ? name : "unknown");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::Shutdown()
|
||||||
|
{
|
||||||
|
if (!IsAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const OneShot& oneShot : m_OneShots) {
|
||||||
|
DestroySource(oneShot.source);
|
||||||
|
}
|
||||||
|
m_OneShots.clear();
|
||||||
|
|
||||||
|
while (!m_Sources.empty()) {
|
||||||
|
DestroySource(*m_Sources.begin());
|
||||||
|
}
|
||||||
|
m_PausedSources.clear();
|
||||||
|
m_Clips.clear();
|
||||||
|
|
||||||
|
alcMakeContextCurrent(nullptr);
|
||||||
|
alcDestroyContext(m_Context);
|
||||||
|
alcCloseDevice(m_Device);
|
||||||
|
m_Context = nullptr;
|
||||||
|
m_Device = nullptr;
|
||||||
|
m_Paused = false;
|
||||||
|
m_HasLastListenerPosition = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<const AudioClip> AudioSystem::LoadClip(std::string_view vpath)
|
||||||
|
{
|
||||||
|
if (!IsAvailable() || vpath.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string key{vpath};
|
||||||
|
if (const auto it = m_Clips.find(key); it != m_Clips.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoneScopedN("AudioSystem::LoadClip");
|
||||||
|
std::vector<std::uint8_t> bytes;
|
||||||
|
try {
|
||||||
|
bytes = AssetFS::GetInstance().ReadBytes(vpath);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
spdlog::error("Could not read audio clip {}: {}", key, e.what());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto data = AudioDecoder::Decode(bytes, vpath);
|
||||||
|
if (!data) {
|
||||||
|
spdlog::error("Could not decode audio clip {}", key);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return CreateClip(key, *data);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<const AudioClip> AudioSystem::CreateClip(std::string path, const AudioData& data)
|
||||||
|
{
|
||||||
|
if (!IsAvailable() || data.samples.empty() || (data.channels != 1 && data.channels != 2)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
alGetError();
|
||||||
|
ALuint buffer = 0;
|
||||||
|
alGenBuffers(1, &buffer);
|
||||||
|
alBufferData(buffer, data.channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data.samples.data(),
|
||||||
|
static_cast<ALsizei>(data.samples.size() * sizeof(std::int16_t)), data.sampleRate);
|
||||||
|
if (const ALenum error = alGetError(); error != AL_NO_ERROR) {
|
||||||
|
spdlog::error("Could not upload audio clip {}: OpenAL error {:#x}", path, error);
|
||||||
|
alDeleteBuffers(1, &buffer);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto clip = std::make_shared<const AudioClip>(path, buffer, data.channels, data.sampleRate, data.Duration());
|
||||||
|
m_Clips[path] = clip;
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::ReleaseUnusedClips()
|
||||||
|
{
|
||||||
|
std::erase_if(m_Clips, [](const auto& entry) { return entry.second.use_count() == 1; });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t AudioSystem::CreateSource()
|
||||||
|
{
|
||||||
|
if (!IsAvailable()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
alGetError();
|
||||||
|
ALuint source = 0;
|
||||||
|
alGenSources(1, &source);
|
||||||
|
if (alGetError() != AL_NO_ERROR) {
|
||||||
|
spdlog::warn("Out of OpenAL sources");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_Sources.insert(source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::DestroySource(std::uint32_t source)
|
||||||
|
{
|
||||||
|
if (source == 0 || !IsAvailable() || !m_Sources.erase(source)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::erase(m_PausedSources, source);
|
||||||
|
|
||||||
|
const ALuint alSource = source;
|
||||||
|
alSourceStop(alSource);
|
||||||
|
alSourcei(alSource, AL_BUFFER, 0);
|
||||||
|
alDeleteSources(1, &alSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::PlayOneShot(std::string_view vpath, const glm::vec3& position, const OneShotParams& params)
|
||||||
|
{
|
||||||
|
auto clip = LoadClip(vpath);
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_OneShots.size() >= MaxOneShots) {
|
||||||
|
DestroySource(m_OneShots.front().source);
|
||||||
|
m_OneShots.erase(m_OneShots.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint32_t source = CreateSource();
|
||||||
|
if (source == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alSourcei(source, AL_BUFFER, static_cast<ALint>(clip->GetBuffer()));
|
||||||
|
alSourcef(source, AL_GAIN, params.volume);
|
||||||
|
alSourcef(source, AL_PITCH, params.pitch);
|
||||||
|
alSourcei(source, AL_SOURCE_RELATIVE, params.spatial ? AL_FALSE : AL_TRUE);
|
||||||
|
alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, params.spatial ? AL_TRUE : AL_FALSE);
|
||||||
|
alSourcef(source, AL_ROLLOFF_FACTOR, params.spatial ? 1.0f : 0.0f);
|
||||||
|
alSourcef(source, AL_REFERENCE_DISTANCE, params.minDistance);
|
||||||
|
alSourcef(source, AL_MAX_DISTANCE, params.maxDistance);
|
||||||
|
const glm::vec3 sourcePosition = params.spatial ? position : glm::vec3{0.0f};
|
||||||
|
alSourcefv(source, AL_POSITION, glm::value_ptr(sourcePosition));
|
||||||
|
alSourcePlay(source);
|
||||||
|
|
||||||
|
m_OneShots.push_back({source, std::move(clip)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::SetListener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up)
|
||||||
|
{
|
||||||
|
m_ListenerPosition = position;
|
||||||
|
m_ListenerForward = forward;
|
||||||
|
m_ListenerUp = up;
|
||||||
|
m_ListenerSetThisFrame = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::Update(float dt, bool paused, const Camera* fallbackListener)
|
||||||
|
{
|
||||||
|
if (!IsAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ZoneScopedN("AudioSystem::Update");
|
||||||
|
|
||||||
|
if (!m_ListenerSetThisFrame && fallbackListener != nullptr) {
|
||||||
|
SetListener(fallbackListener->GetPosition(), fallbackListener->GetForward(), fallbackListener->GetUp());
|
||||||
|
}
|
||||||
|
m_ListenerSetThisFrame = false;
|
||||||
|
|
||||||
|
if (!m_HasLastListenerPosition || paused) {
|
||||||
|
m_ListenerVelocity = glm::vec3{0.0f};
|
||||||
|
} else {
|
||||||
|
m_ListenerVelocity = SmoothAudioVelocity(m_ListenerPosition, m_LastListenerPosition, m_ListenerVelocity, dt);
|
||||||
|
}
|
||||||
|
m_LastListenerPosition = m_ListenerPosition;
|
||||||
|
m_HasLastListenerPosition = true;
|
||||||
|
|
||||||
|
const std::array orientation{
|
||||||
|
m_ListenerForward.x, m_ListenerForward.y, m_ListenerForward.z,
|
||||||
|
m_ListenerUp.x, m_ListenerUp.y, m_ListenerUp.z,
|
||||||
|
};
|
||||||
|
alListenerfv(AL_POSITION, glm::value_ptr(m_ListenerPosition));
|
||||||
|
alListenerfv(AL_VELOCITY, glm::value_ptr(m_ListenerVelocity));
|
||||||
|
alListenerfv(AL_ORIENTATION, orientation.data());
|
||||||
|
|
||||||
|
SetPaused(paused);
|
||||||
|
|
||||||
|
std::erase_if(m_OneShots, [this](const OneShot& oneShot) {
|
||||||
|
const ALint state = SourceState(oneShot.source);
|
||||||
|
if (state == AL_PLAYING || state == AL_PAUSED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DestroySource(oneShot.source);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::SetMasterVolume(float volume)
|
||||||
|
{
|
||||||
|
m_MasterVolume = std::max(volume, 0.0f);
|
||||||
|
if (IsAvailable()) {
|
||||||
|
alListenerf(AL_GAIN, m_MasterVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSystem::SetPaused(bool paused)
|
||||||
|
{
|
||||||
|
if (paused == m_Paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_Paused = paused;
|
||||||
|
|
||||||
|
if (paused) {
|
||||||
|
for (const std::uint32_t source : m_Sources) {
|
||||||
|
if (SourceState(source) == AL_PLAYING) {
|
||||||
|
alSourcePause(source);
|
||||||
|
m_PausedSources.push_back(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sources paused on purpose before the simulation paused stay paused
|
||||||
|
for (const std::uint32_t source : m_PausedSources) {
|
||||||
|
if (SourceState(source) == AL_PAUSED) {
|
||||||
|
alSourcePlay(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_PausedSources.clear();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user