Compare commits
19
Commits
3a04c1884a
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d26f61a16f | ||
|
|
6a85d17899 | ||
|
|
08acd84404 | ||
|
|
0fbc023018 | ||
|
|
de7e489cc0 | ||
|
|
9cf9242c2a | ||
|
|
118016bcc7 | ||
|
|
355dfa7851 | ||
|
|
aa3eef259c | ||
|
|
9c5a97cfbd | ||
|
|
b798ed0378 | ||
|
|
0b7cc38543 | ||
|
|
a3d3da4a67 | ||
|
|
f7e7c56392 | ||
|
|
f055bdcbf1 | ||
|
|
f99147b1a2 | ||
|
|
dde2c7daf9 | ||
|
|
e5d37965a1 | ||
|
|
3df0f89343 |
@@ -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
+80
-4
@@ -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"
|
||||||
@@ -74,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"
|
||||||
@@ -98,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"
|
||||||
|
|
||||||
@@ -110,25 +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/PhysicsMeshCache.cpp"
|
||||||
"src/Physics/PhysicsSceneBridge.cpp"
|
"src/Physics/PhysicsSceneBridge.cpp"
|
||||||
"src/Physics/JoltPhysicsWorld.cpp"
|
"src/Physics/JoltPhysicsWorld.cpp"
|
||||||
)
|
)
|
||||||
@@ -171,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
|
||||||
@@ -217,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
|
||||||
@@ -271,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"
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ struct InstanceData {
|
|||||||
mat4 transform;
|
mat4 transform;
|
||||||
VertexBuffer vertexBuffer;
|
VertexBuffer vertexBuffer;
|
||||||
uint materialID;
|
uint materialID;
|
||||||
uint padding;
|
uint objectID;
|
||||||
};
|
};
|
||||||
|
|
||||||
layout (buffer_reference, scalar) readonly buffer InstanceBuffer {
|
layout (buffer_reference, scalar) readonly buffer InstanceBuffer {
|
||||||
|
|||||||
@@ -13,9 +13,23 @@ 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 = 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);
|
||||||
@@ -127,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)) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 = 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
|
||||||
@@ -34,4 +35,5 @@ void main()
|
|||||||
// 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(instance.transform) * v.tangent.xyz, v.tangent.w);
|
outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w);
|
||||||
outMaterialID = instance.materialID;
|
outMaterialID = instance.materialID;
|
||||||
|
outObjectID = instance.objectID;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,7 +4,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <future>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -12,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;
|
||||||
|
|
||||||
@@ -19,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(
|
||||||
@@ -26,9 +36,18 @@ 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);
|
void removeImage(ImageID id);
|
||||||
|
|
||||||
[[nodiscard]] ImageID getFreeImageId() const;
|
[[nodiscard]] ImageID getFreeImageId() const;
|
||||||
@@ -53,16 +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>>();
|
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,
|
||||||
@@ -54,6 +58,7 @@ private:
|
|||||||
|
|
||||||
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,6 +17,9 @@ 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; }
|
[[nodiscard]] const std::vector<GPUMesh>& getMeshes() const { return meshes; }
|
||||||
const CPUMesh& getCPUMesh(MeshID id) const;
|
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||||
@@ -26,8 +31,19 @@ public:
|
|||||||
[[nodiscard]] std::uint32_t getIndexCapacity() const { return indexCapacity; }
|
[[nodiscard]] std::uint32_t getIndexCapacity() const { return indexCapacity; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
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 uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh);
|
||||||
void ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices);
|
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;
|
static constexpr std::uint32_t InitialIndexCapacity = 1u << 20;
|
||||||
|
|
||||||
@@ -38,6 +54,7 @@ private:
|
|||||||
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
|
||||||
|
|||||||
@@ -50,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:
|
||||||
@@ -141,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,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct GPUInstanceData {
|
|||||||
glm::mat4 transform;
|
glm::mat4 transform;
|
||||||
VkDeviceAddress vertexBuffer;
|
VkDeviceAddress vertexBuffer;
|
||||||
std::uint32_t materialId;
|
std::uint32_t materialId;
|
||||||
std::uint32_t padding;
|
std::uint32_t objectId;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(GPUInstanceData) == 80);
|
static_assert(sizeof(GPUInstanceData) == 80);
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ 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,
|
||||||
@@ -32,7 +33,8 @@ public:
|
|||||||
const RenderResources& resources,
|
const RenderResources& resources,
|
||||||
const GPUBuffer& sceneDataBuffer,
|
const GPUBuffer& sceneDataBuffer,
|
||||||
const IndirectDrawList& drawList,
|
const IndirectDrawList& drawList,
|
||||||
const IndirectDrawList::View& view);
|
const IndirectDrawList::View& view,
|
||||||
|
bool writeObjectIds);
|
||||||
|
|
||||||
void draw(
|
void draw(
|
||||||
VkCommandBuffer cmd,
|
VkCommandBuffer cmd,
|
||||||
@@ -42,6 +44,14 @@ public:
|
|||||||
const IndirectDrawList& drawList,
|
const IndirectDrawList& drawList,
|
||||||
const IndirectDrawList::View& view);
|
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);
|
||||||
|
|
||||||
void setRenderWireframe(bool wireframe) {
|
void setRenderWireframe(bool wireframe) {
|
||||||
@@ -70,7 +80,9 @@ private:
|
|||||||
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
|
||||||
@@ -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,6 +46,22 @@ 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);
|
void destroyImage(ImageID id);
|
||||||
|
|
||||||
@@ -86,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,6 +2,7 @@
|
|||||||
#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>
|
||||||
@@ -10,9 +11,11 @@
|
|||||||
#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>
|
||||||
@@ -21,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>
|
||||||
@@ -47,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();
|
||||||
@@ -58,22 +66,22 @@ 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);
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const
|
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const
|
||||||
@@ -101,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);
|
||||||
@@ -124,8 +169,9 @@ public:
|
|||||||
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;
|
||||||
@@ -137,14 +183,31 @@ private:
|
|||||||
|
|
||||||
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;
|
IndirectDrawList drawList;
|
||||||
DrawStats lastDrawStats;
|
DrawStats lastDrawStats;
|
||||||
|
|
||||||
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
static constexpr std::uint32_t HighlightedObjectFlag = 0x80000000u;
|
||||||
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
|
|
||||||
|
|
||||||
|
VkFormat sceneColorFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
||||||
|
VkFormat drawImageFormat{VK_FORMAT_R8G8B8A8_SRGB};
|
||||||
|
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};
|
||||||
|
|
||||||
@@ -173,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;
|
||||||
@@ -191,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,16 +1090,18 @@ 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 (!std::filesystem::is_regular_file(path)) {
|
if (importer.IsDefaultIOHandler()) {
|
||||||
throw std::runtime_error("Animation path is not a regular file: " + path);
|
if (!std::filesystem::is_regular_file(path)) {
|
||||||
}
|
throw std::runtime_error("Animation path is not a regular file: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -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);
|
||||||
|
|||||||
+311
-326
@@ -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,269 +88,308 @@ 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;
|
||||||
|
|
||||||
|
processEvents();
|
||||||
|
if (!isRunning) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulator += dt;
|
if (window.IsMinimized()) {
|
||||||
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.ProcessEvent(event);
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
imguiPass.beginFrame();
|
||||||
ZoneScopedN("ImGui BeginFrame");
|
if (layer) {
|
||||||
imguiPass.beginFrame();
|
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();
|
}
|
||||||
|
imguiPass.endFrame();
|
||||||
|
|
||||||
if (m_DebugWindows.inspector)
|
if (layer) {
|
||||||
{
|
layer->onEndFrame();
|
||||||
SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_DebugWindows.stats)
|
|
||||||
{
|
|
||||||
if (ImGui::Begin("Debug", &m_DebugWindows.stats))
|
|
||||||
{
|
|
||||||
ImGui::Text("FPS: %.2f", avgFPS);
|
|
||||||
|
|
||||||
const auto& stats = renderer.getDrawStats();
|
|
||||||
ImGui::SeparatorText("Meshes");
|
|
||||||
ImGui::Text("Submitted meshes: %u", stats.meshDraws);
|
|
||||||
ImGui::Text("Camera: %u indirect draws, %u instances", stats.mainViewCommands, stats.mainViewInstances);
|
|
||||||
ImGui::Text("All views: %u indirect draws, %u instances", stats.indirectCommands, stats.instances);
|
|
||||||
ImGui::Text("Pending GPU deletions: %zu", gfxDevice.getPendingDeletionCount());
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
runFixedSteps();
|
||||||
ZoneScopedN("ImGui EndFrame");
|
|
||||||
imguiPass.endFrame();
|
AudioSystem::GetInstance().Update(dt, isPlaying() && (isPaused() || simulation.paused), &getViewCamera());
|
||||||
|
|
||||||
|
bool skipFrame = false;
|
||||||
|
handleSwapchainResize(skipFrame);
|
||||||
|
if (skipFrame) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
renderFrame();
|
||||||
ZoneScopedN("FixedUpdate");
|
|
||||||
|
|
||||||
int steps = 0;
|
if (frameLimit) {
|
||||||
|
if (const auto sleepTime = Time::GetInstance().SleepDuration(); sleepTime.count() > 0) {
|
||||||
if (m_PhysicsPaused)
|
|
||||||
{
|
|
||||||
// Important: prevent physics from building up a huge backlog while paused.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const glm::ivec2 drawableSize = window.GetDrawableSize();
|
|
||||||
const int w = drawableSize.x;
|
|
||||||
const int h = drawableSize.y;
|
|
||||||
|
|
||||||
if (w == 0 || h == 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
if (cleanedUp || cleaningUp) {
|
if (cleanedUp || cleaningUp) {
|
||||||
@@ -363,11 +413,19 @@ void App::cleanup()
|
|||||||
// The one place we wait for the GPU on shutdown: the cleanup calls below destroy
|
// 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.
|
// pipelines and other objects directly, which is only valid once nothing is in flight.
|
||||||
attempt([this] { gfxDevice.waitIdle(); });
|
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(); });
|
||||||
@@ -385,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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include <destrum/Components/AudioListener.h>
|
||||||
|
|
||||||
|
#include <destrum/Audio/AudioSystem.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
AudioListener::AudioListener(GameObject& parent)
|
||||||
|
: Component(parent, "AudioListener")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioListener::LateUpdate(float)
|
||||||
|
{
|
||||||
|
auto& transform = GetTransform();
|
||||||
|
const glm::quat rotation = transform.GetWorldRotation();
|
||||||
|
AudioSystem::GetInstance().SetListener(
|
||||||
|
transform.GetWorldPosition(),
|
||||||
|
rotation * glm::vec3{0.0f, 0.0f, -1.0f},
|
||||||
|
rotation * glm::vec3{0.0f, 1.0f, 0.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioListener::ImGuiInspector()
|
||||||
|
{
|
||||||
|
auto& audio = AudioSystem::GetInstance();
|
||||||
|
float volume = audio.GetMasterVolume();
|
||||||
|
if (ImGui::SliderFloat("Master Volume", &volume, 0.0f, 1.0f)) {
|
||||||
|
audio.SetMasterVolume(volume);
|
||||||
|
}
|
||||||
|
ImGui::TextDisabled("Hears the scene while playing, the view camera does otherwise");
|
||||||
|
ImGui::TextWrapped("Uses this object's position and rotation. If the transform does not turn with the view "
|
||||||
|
"(a first person body), remove this and let the camera listen.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#include <destrum/Components/AudioSource.h>
|
||||||
|
|
||||||
|
#include <destrum/Assets/AssetDatabase.h>
|
||||||
|
#include <destrum/Assets/AssetTypes.h>
|
||||||
|
#include <destrum/Audio/AudioSystem.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
#include <AL/al.h>
|
||||||
|
#include <AL/alext.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
[[nodiscard]] ALint SourceState(std::uint32_t source)
|
||||||
|
{
|
||||||
|
ALint state = AL_STOPPED;
|
||||||
|
if (source != 0) {
|
||||||
|
alGetSourcei(source, AL_SOURCE_STATE, &state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioSource::AudioSource(GameObject& parent, std::string clipPath)
|
||||||
|
: Component(parent, "AudioSource")
|
||||||
|
, m_ClipPath(std::move(clipPath))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioSource::~AudioSource()
|
||||||
|
{
|
||||||
|
ReleaseSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Update(float)
|
||||||
|
{
|
||||||
|
if (m_AutoPlayed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_AutoPlayed = true;
|
||||||
|
if (m_PlayOnStart) {
|
||||||
|
Play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::LateUpdate(float dt)
|
||||||
|
{
|
||||||
|
SyncPosition(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled) {
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
Component::SetEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Play()
|
||||||
|
{
|
||||||
|
if (!EnsureSource()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Clip || m_Clip->GetPath() != m_ClipPath) {
|
||||||
|
alSourceStop(m_Source);
|
||||||
|
alSourcei(m_Source, AL_BUFFER, 0);
|
||||||
|
m_Clip = AudioSystem::GetInstance().LoadClip(m_ClipPath);
|
||||||
|
if (!m_Clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alSourcei(m_Source, AL_BUFFER, static_cast<ALint>(m_Clip->GetBuffer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyProperties();
|
||||||
|
m_HasLastPosition = false;
|
||||||
|
SyncPosition(0.0f);
|
||||||
|
alSourcePlay(m_Source);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Pause()
|
||||||
|
{
|
||||||
|
if (SourceState(m_Source) == AL_PLAYING) {
|
||||||
|
alSourcePause(m_Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Stop()
|
||||||
|
{
|
||||||
|
if (m_Source != 0) {
|
||||||
|
alSourceStop(m_Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioSource::IsPlaying() const
|
||||||
|
{
|
||||||
|
return SourceState(m_Source) == AL_PLAYING;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::PlayOneShot(const std::string& clipPath, float volumeScale) const
|
||||||
|
{
|
||||||
|
AudioSystem::GetInstance().PlayOneShot(clipPath, GetTransform().GetWorldPosition(), {
|
||||||
|
.volume = m_Volume * volumeScale,
|
||||||
|
.pitch = m_Pitch,
|
||||||
|
.spatial = m_Spatial,
|
||||||
|
.minDistance = m_MinDistance,
|
||||||
|
.maxDistance = m_MaxDistance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetClip(const std::string& clipPath)
|
||||||
|
{
|
||||||
|
if (clipPath == m_ClipPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_ClipPath = clipPath;
|
||||||
|
if (m_Source != 0) {
|
||||||
|
alSourceStop(m_Source);
|
||||||
|
alSourcei(m_Source, AL_BUFFER, 0);
|
||||||
|
}
|
||||||
|
m_Clip.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetVolume(float volume)
|
||||||
|
{
|
||||||
|
m_Volume = std::max(volume, 0.0f);
|
||||||
|
ApplyProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetPitch(float pitch)
|
||||||
|
{
|
||||||
|
m_Pitch = std::max(pitch, 0.01f);
|
||||||
|
ApplyProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetLoop(bool loop)
|
||||||
|
{
|
||||||
|
m_Loop = loop;
|
||||||
|
ApplyProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetSpatial(bool spatial)
|
||||||
|
{
|
||||||
|
m_Spatial = spatial;
|
||||||
|
ApplyProperties();
|
||||||
|
SyncPosition(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SetDistances(float minDistance, float maxDistance)
|
||||||
|
{
|
||||||
|
m_MinDistance = std::max(minDistance, 0.01f);
|
||||||
|
m_MaxDistance = std::max(maxDistance, m_MinDistance);
|
||||||
|
ApplyProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Reflect(FieldVisitor& v)
|
||||||
|
{
|
||||||
|
v.Property("volume", m_Volume, [this](float volume) { SetVolume(volume); }, {.speed = 0.01f, .min = 0.0, .max = 2.0});
|
||||||
|
v.Property("pitch", m_Pitch, [this](float pitch) { SetPitch(pitch); }, {.speed = 0.01f, .min = 0.1, .max = 3.0});
|
||||||
|
v.Property("loop", m_Loop, [this](bool loop) { SetLoop(loop); });
|
||||||
|
v.Field("playOnStart", m_PlayOnStart);
|
||||||
|
v.Property("spatial", m_Spatial, [this](bool spatial) { SetSpatial(spatial); });
|
||||||
|
v.Property("minDistance", m_MinDistance, [this](float minDistance) { SetDistances(minDistance, m_MaxDistance); },
|
||||||
|
{.speed = 0.05f, .min = 0.01, .format = "%.2f m", .hidden = !m_Spatial, .tooltip = "Full volume up to this distance"});
|
||||||
|
v.Property("maxDistance", m_MaxDistance, [this](float maxDistance) { SetDistances(m_MinDistance, maxDistance); },
|
||||||
|
{.speed = 0.1f, .min = 0.01, .format = "%.2f m", .hidden = !m_Spatial,
|
||||||
|
.tooltip = "The sound stops getting quieter past this distance"});
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json AudioSource::Serialize() const
|
||||||
|
{
|
||||||
|
nlohmann::json data = Component::Serialize();
|
||||||
|
data["clip"] = m_ClipPath.empty() ? nlohmann::json(nullptr) : AssetDatabase::GetInstance().WriteReference(m_ClipPath);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::Deserialize(const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (const auto clip = data.find("clip"); clip != data.end()) {
|
||||||
|
if (clip->is_string()) {
|
||||||
|
SetClip(clip->get<std::string>());
|
||||||
|
} else if (const auto reference = AssetDatabase::GetInstance().ReadReference(*clip)) {
|
||||||
|
SetClip(reference->path);
|
||||||
|
} else {
|
||||||
|
SetClip({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::Deserialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::ImGuiInspector()
|
||||||
|
{
|
||||||
|
if (!AudioSystem::GetInstance().IsAvailable()) {
|
||||||
|
ImGui::TextDisabled("No audio device, sound is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string clipPath = m_ClipPath;
|
||||||
|
if (ImGuiUtils::InputString("Clip", clipPath, 0, "game://sounds/clip.ogg")) {
|
||||||
|
SetClip(clipPath);
|
||||||
|
}
|
||||||
|
if (ImGui::BeginDragDropTarget()) {
|
||||||
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
|
||||||
|
const std::string dropped = static_cast<const char*>(payload->Data);
|
||||||
|
if (AssetTypes::IsAudio(dropped)) {
|
||||||
|
SetClip(dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
ImGui::SetItemTooltip("Drop a .wav or .ogg from the Assets window");
|
||||||
|
|
||||||
|
if (m_Clip && m_Clip->GetPath() == m_ClipPath) {
|
||||||
|
ImGui::TextDisabled("%s, %d Hz, %.2f s", m_Clip->GetChannels() == 1 ? "Mono" : "Stereo",
|
||||||
|
m_Clip->GetSampleRate(), m_Clip->GetDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
Component::ImGuiInspector();
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(m_ClipPath.empty());
|
||||||
|
if (ImGui::Button(IsPlaying() ? "Restart" : "Play")) {
|
||||||
|
Stop();
|
||||||
|
Play();
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Stop")) {
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioSource::EnsureSource()
|
||||||
|
{
|
||||||
|
if (m_Source == 0) {
|
||||||
|
m_Source = AudioSystem::GetInstance().CreateSource();
|
||||||
|
}
|
||||||
|
return m_Source != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::ApplyProperties()
|
||||||
|
{
|
||||||
|
if (m_Source == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alSourcef(m_Source, AL_GAIN, m_Volume);
|
||||||
|
alSourcef(m_Source, AL_PITCH, m_Pitch);
|
||||||
|
alSourcei(m_Source, AL_LOOPING, m_Loop ? AL_TRUE : AL_FALSE);
|
||||||
|
alSourcei(m_Source, AL_SOURCE_RELATIVE, m_Spatial ? AL_FALSE : AL_TRUE);
|
||||||
|
alSourcei(m_Source, AL_SOURCE_SPATIALIZE_SOFT, m_Spatial ? AL_TRUE : AL_FALSE);
|
||||||
|
alSourcef(m_Source, AL_ROLLOFF_FACTOR, m_Spatial ? 1.0f : 0.0f);
|
||||||
|
alSourcef(m_Source, AL_REFERENCE_DISTANCE, m_MinDistance);
|
||||||
|
alSourcef(m_Source, AL_MAX_DISTANCE, m_MaxDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::SyncPosition(float dt)
|
||||||
|
{
|
||||||
|
if (m_Source == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Spatial) {
|
||||||
|
const glm::vec3 zero{0.0f};
|
||||||
|
alSourcefv(m_Source, AL_POSITION, glm::value_ptr(zero));
|
||||||
|
alSourcefv(m_Source, AL_VELOCITY, glm::value_ptr(zero));
|
||||||
|
m_HasLastPosition = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glm::vec3 position = GetTransform().GetWorldPosition();
|
||||||
|
m_Velocity = m_HasLastPosition ? SmoothAudioVelocity(position, m_LastPosition, m_Velocity, dt) : glm::vec3{0.0f};
|
||||||
|
m_LastPosition = position;
|
||||||
|
m_HasLastPosition = true;
|
||||||
|
|
||||||
|
alSourcefv(m_Source, AL_POSITION, glm::value_ptr(position));
|
||||||
|
alSourcefv(m_Source, AL_VELOCITY, glm::value_ptr(m_Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSource::ReleaseSource()
|
||||||
|
{
|
||||||
|
AudioSystem::GetInstance().DestroySource(m_Source);
|
||||||
|
m_Source = 0;
|
||||||
|
m_Clip.reset();
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
@@ -54,19 +55,10 @@ bool CameraComponent::IsCurrent() const
|
|||||||
return scene != nullptr && scene->GetCurrentCamera() == this;
|
return scene != nullptr && scene->GetCurrentCamera() == this;
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json CameraComponent::Serialize() const {
|
void CameraComponent::Reflect(FieldVisitor& v) {
|
||||||
return {
|
v.Property("fieldOfView", m_FieldOfView, [this](float fov) { SetFieldOfView(fov); }, {.speed = 0.5f, .min = 1.0, .max = 179.0});
|
||||||
{"fieldOfView", m_FieldOfView},
|
v.Property("zNear", m_ZNear, [this](float zNear) { SetClipPlanes(zNear, m_ZFar); }, {.speed = 0.01f, .min = 0.0001, .label = "Near"});
|
||||||
{"zNear", m_ZNear},
|
v.Property("zFar", m_ZFar, [this](float zFar) { SetClipPlanes(m_ZNear, zFar); }, {.speed = 1.0f, .min = 0.1, .label = "Far"});
|
||||||
{"zFar", m_ZFar}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraComponent::Deserialize(const nlohmann::json& data) {
|
|
||||||
if (data.contains("fieldOfView")) {
|
|
||||||
SetFieldOfView(data.at("fieldOfView").get<float>());
|
|
||||||
}
|
|
||||||
SetClipPlanes(data.value("zNear", m_ZNear), data.value("zFar", m_ZFar));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CameraComponent::ImGuiInspector() {
|
void CameraComponent::ImGuiInspector() {
|
||||||
@@ -74,17 +66,5 @@ void CameraComponent::ImGuiInspector() {
|
|||||||
if (ImGui::Checkbox("Current Camera", ¤t) && current) {
|
if (ImGui::Checkbox("Current Camera", ¤t) && current) {
|
||||||
MakeCurrent();
|
MakeCurrent();
|
||||||
}
|
}
|
||||||
|
Component::ImGuiInspector();
|
||||||
float fieldOfView = m_FieldOfView;
|
|
||||||
if (ImGui::DragFloat("Field Of View", &fieldOfView, 0.5f, 1.0f, 179.0f)) {
|
|
||||||
SetFieldOfView(fieldOfView);
|
|
||||||
}
|
|
||||||
|
|
||||||
float zNear = m_ZNear;
|
|
||||||
float zFar = m_ZFar;
|
|
||||||
const bool nearChanged = ImGui::DragFloat("Near", &zNear, 0.01f, 0.0001f, 100.0f);
|
|
||||||
const bool farChanged = ImGui::DragFloat("Far", &zFar, 1.0f, 0.1f, 100000.0f);
|
|
||||||
if (nearChanged || farChanged) {
|
|
||||||
SetClipPlanes(zNear, zFar);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/RenderContext.h>
|
#include <destrum/Graphics/RenderContext.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
#include <glm/gtc/constants.hpp>
|
#include <glm/gtc/constants.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
@@ -11,8 +11,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
LightComponent::LightComponent(GameObject& parent, LightType type)
|
LightComponent::LightComponent(GameObject& parent, LightType type)
|
||||||
: Component(parent, "LightComponent")
|
: Component(parent, "LightComponent")
|
||||||
, m_Type(type)
|
, m_Type(type)
|
||||||
@@ -52,86 +50,19 @@ void LightComponent::Render(const RenderContext& ctx)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json LightComponent::Serialize() const {
|
void LightComponent::Reflect(FieldVisitor& v) {
|
||||||
return {
|
static constexpr std::array<const char*, 3> TypeNames{"Directional", "Point", "Spot"};
|
||||||
{"type", static_cast<std::uint32_t>(m_Type)},
|
|
||||||
{"color", m_Color},
|
const bool directional = m_Type == LightType::Directional;
|
||||||
{"intensity", m_Intensity},
|
const bool spot = m_Type == LightType::Spot;
|
||||||
{"range", m_Range},
|
v.Field("type", m_Type, {.enumNames = TypeNames});
|
||||||
{"innerConeAngle", m_InnerConeAngle},
|
v.Field("color", m_Color, {.color = true});
|
||||||
{"outerConeAngle", m_OuterConeAngle},
|
v.Property("intensity", m_Intensity, [this](float intensity) { SetIntensity(intensity); }, {.speed = 0.05f, .min = 0.0});
|
||||||
{"castShadows", m_CastShadows}
|
v.Field("castShadows", m_CastShadows, {.hidden = !directional && !spot,
|
||||||
};
|
.tooltip = directional ? "Only the first directional light casts shadows" : nullptr});
|
||||||
}
|
v.Property("range", m_Range, [this](float range) { SetRange(range); }, {.speed = 0.05f, .min = 0.01, .hidden = directional});
|
||||||
|
v.Property("innerConeAngle", m_InnerConeAngle, [this](float angle) { SetConeAngles(angle, m_OuterConeAngle); },
|
||||||
void LightComponent::Deserialize(const nlohmann::json& data) {
|
{.min = 0.0, .max = glm::half_pi<double>(), .angle = true, .hidden = !spot, .label = "Inner Cone"});
|
||||||
if (data.contains("type")) {
|
v.Property("outerConeAngle", m_OuterConeAngle, [this](float angle) { SetConeAngles(m_InnerConeAngle, angle); },
|
||||||
const auto type = data.at("type").get<std::uint32_t>();
|
{.min = 0.0, .max = glm::half_pi<double>(), .angle = true, .hidden = !spot, .label = "Outer Cone"});
|
||||||
if (type <= static_cast<std::uint32_t>(LightType::Spot)) {
|
|
||||||
m_Type = static_cast<LightType>(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_Color = data.value("color", m_Color);
|
|
||||||
if (data.contains("intensity")) {
|
|
||||||
SetIntensity(data.at("intensity").get<float>());
|
|
||||||
}
|
|
||||||
if (data.contains("range")) {
|
|
||||||
SetRange(data.at("range").get<float>());
|
|
||||||
}
|
|
||||||
|
|
||||||
float innerConeAngle = m_InnerConeAngle;
|
|
||||||
float outerConeAngle = m_OuterConeAngle;
|
|
||||||
if (data.contains("innerConeAngle")) {
|
|
||||||
innerConeAngle = data.at("innerConeAngle").get<float>();
|
|
||||||
}
|
|
||||||
if (data.contains("outerConeAngle")) {
|
|
||||||
outerConeAngle = data.at("outerConeAngle").get<float>();
|
|
||||||
}
|
|
||||||
SetConeAngles(innerConeAngle, outerConeAngle);
|
|
||||||
|
|
||||||
if (data.contains("castShadows")) {
|
|
||||||
m_CastShadows = data.at("castShadows").get<bool>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LightComponent::ImGuiInspector() {
|
|
||||||
constexpr std::array typeNames{"Directional", "Point", "Spot"};
|
|
||||||
|
|
||||||
int type = static_cast<int>(m_Type);
|
|
||||||
if (ImGui::Combo("Type", &type, typeNames.data(), static_cast<int>(typeNames.size()))) {
|
|
||||||
SetType(static_cast<LightType>(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::ColorEdit3("Color", &m_Color.x);
|
|
||||||
|
|
||||||
float intensity = m_Intensity;
|
|
||||||
if (ImGui::DragFloat("Intensity", &intensity, 0.05f, 0.0f)) {
|
|
||||||
SetIntensity(intensity);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_Type == LightType::Directional) {
|
|
||||||
ImGui::Checkbox("Cast Shadows", &m_CastShadows);
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextDisabled("(first directional light only)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_Type == LightType::Spot) {
|
|
||||||
ImGui::Checkbox("Cast Shadows", &m_CastShadows);
|
|
||||||
}
|
|
||||||
|
|
||||||
float range = m_Range;
|
|
||||||
if (ImGui::DragFloat("Range", &range, 0.05f, 0.01f)) {
|
|
||||||
SetRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_Type == LightType::Spot) {
|
|
||||||
float innerConeAngle = m_InnerConeAngle;
|
|
||||||
float outerConeAngle = m_OuterConeAngle;
|
|
||||||
bool changed = ImGui::SliderAngle("Inner Cone", &innerConeAngle, 0.0f, 90.0f);
|
|
||||||
changed |= ImGui::SliderAngle("Outer Cone", &outerConeAngle, 0.0f, 90.0f);
|
|
||||||
if (changed) {
|
|
||||||
SetConeAngles(innerConeAngle, outerConeAngle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include <destrum/Components/MeshRendererComponent.h>
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
|
#include <destrum/Assets/AssetDatabase.h>
|
||||||
#include <destrum/Assets/AssetReference.h>
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
#include <destrum/Graphics/Renderer.h>
|
#include <destrum/Graphics/Renderer.h>
|
||||||
@@ -9,13 +12,64 @@
|
|||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/Util/GameState.h"
|
#include "destrum/Util/GameState.h"
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
[[nodiscard]] bool IsAssetPath(std::string_view path)
|
||||||
|
{
|
||||||
|
return path.find("://") != std::string_view::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json MeshReference(const std::string& key)
|
||||||
|
{
|
||||||
|
if (key.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (PrimitiveMeshes::IsPrimitiveKey(key)) {
|
||||||
|
return std::string{PrimitiveMeshes::StripDuplicateSuffix(key)};
|
||||||
|
}
|
||||||
|
if (const auto asset = AssetReference::fromCacheKey(key); asset && IsAssetPath(asset->path)) {
|
||||||
|
return AssetDatabase::GetInstance().WriteReference(asset->path, asset->subresource);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json MaterialReference(const std::string& key)
|
||||||
|
{
|
||||||
|
if (key.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (MaterialSerialization::IsFileKey(key)) {
|
||||||
|
return AssetDatabase::GetInstance().WriteReference(key);
|
||||||
|
}
|
||||||
|
if (const auto asset = AssetReference::fromCacheKey(key); asset && IsAssetPath(asset->path)) {
|
||||||
|
return AssetDatabase::GetInstance().WriteReference(asset->path, asset->subresource);
|
||||||
|
}
|
||||||
|
return {{"runtimeMaterial", key}};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string ReadKey(const nlohmann::json& reference)
|
||||||
|
{
|
||||||
|
if (reference.is_string()) {
|
||||||
|
return reference.get<std::string>();
|
||||||
|
}
|
||||||
|
if (!reference.is_object()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (const auto runtime = reference.find("runtimeMaterial"); runtime != reference.end() && runtime->is_string()) {
|
||||||
|
return runtime->get<std::string>();
|
||||||
|
}
|
||||||
|
if (const auto asset = AssetDatabase::GetInstance().ReadReference(reference)) {
|
||||||
|
return asset->subresource.empty() ? asset->path : asset->cacheKey();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -77,15 +131,23 @@ nlohmann::json MeshRendererComponent::Serialize() const {
|
|||||||
throw std::runtime_error("MeshRendererComponent has a material without a key, it cannot be saved");
|
throw std::runtime_error("MeshRendererComponent has a material without a key, it cannot be saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GameState::GetInstance().HasRenderer()) {
|
||||||
|
if (const auto* resources = GameState::GetInstance().Renderer().getResources();
|
||||||
|
resources != nullptr && materialID == resources->materials().getPlaceholderMaterialId() &&
|
||||||
|
savedMaterialKey == resources->materials().getMaterialKey(materialID)) {
|
||||||
|
savedMaterialKey.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"meshKey", savedMeshKey},
|
{"mesh", MeshReference(savedMeshKey)},
|
||||||
{"materialKey", savedMaterialKey}
|
{"material", MaterialReference(savedMaterialKey)},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
||||||
meshKey = data.value("meshKey", std::string{});
|
meshKey = data.contains("mesh") ? ReadKey(data.at("mesh")) : data.value("meshKey", std::string{});
|
||||||
materialKey = data.value("materialKey", std::string{});
|
materialKey = data.contains("material") ? ReadKey(data.at("material")) : data.value("materialKey", std::string{});
|
||||||
|
|
||||||
// Older scenes also stored raw ids, only trust them when there is no key
|
// Older scenes also stored raw ids, only trust them when there is no key
|
||||||
meshID = meshKey.empty() ? data.value("meshId", NULL_MESH_ID) : NULL_MESH_ID;
|
meshID = meshKey.empty() ? data.value("meshId", NULL_MESH_ID) : NULL_MESH_ID;
|
||||||
@@ -105,8 +167,7 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
const auto loadAssetForKey = [&](const std::string& key) {
|
const auto loadAssetForKey = [&](const std::string& key) {
|
||||||
const auto asset = AssetReference::fromCacheKey(key);
|
const auto asset = AssetReference::fromCacheKey(key);
|
||||||
// Runtime-only keys such as "BulbMaterial#2" also contain a '#', they are not files
|
// Runtime-only keys such as "BulbMaterial#2" also contain a '#', they are not files
|
||||||
if (!asset || (asset->path.find("://") == std::string::npos &&
|
if (!asset || (!IsAssetPath(asset->path) && !std::filesystem::exists(asset->path))) {
|
||||||
!std::filesystem::exists(asset->path))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!GameState::GetInstance().HasGfxDevice()) {
|
if (!GameState::GetInstance().HasGfxDevice()) {
|
||||||
@@ -144,6 +205,13 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
}
|
}
|
||||||
if (!materialKey.empty()) {
|
if (!materialKey.empty()) {
|
||||||
auto resolved = resources->materials().findMaterialByKey(materialKey);
|
auto resolved = resources->materials().findMaterialByKey(materialKey);
|
||||||
|
if (!resolved && MaterialSerialization::IsFileKey(materialKey) && GameState::GetInstance().HasGfxDevice()) {
|
||||||
|
try {
|
||||||
|
resolved = MaterialSerialization::LoadFile(*resources, GameState::GetInstance().Gfx(), materialKey);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::warn("Material file {} could not be loaded: {}", materialKey, exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
loadAssetForKey(materialKey);
|
loadAssetForKey(materialKey);
|
||||||
resolved = resources->materials().findMaterialByKey(materialKey);
|
resolved = resources->materials().findMaterialByKey(materialKey);
|
||||||
@@ -179,30 +247,6 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::ImGuiInspector() {
|
|
||||||
if (meshID == NULL_MESH_ID) {
|
|
||||||
ImGui::TextDisabled("Mesh: <none>");
|
|
||||||
} else {
|
|
||||||
ImGui::Text("Mesh ID: %zu", meshID);
|
|
||||||
ImGui::TextWrapped(
|
|
||||||
"Mesh key: %s",
|
|
||||||
meshKey.empty() ? "<not resolved>" : meshKey.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materialID == NULL_MATERIAL_ID) {
|
|
||||||
ImGui::TextDisabled("Material: <none>");
|
|
||||||
} else {
|
|
||||||
ImGui::Text("Material ID: %u", materialID);
|
|
||||||
ImGui::TextWrapped(
|
|
||||||
"Material key: %s",
|
|
||||||
materialKey.empty() ? "<not resolved>" : materialKey.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Text(
|
|
||||||
"Skinning: %s",
|
|
||||||
m_skinnedMesh != nullptr ? "active" : "static");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MeshRendererComponent::SetMeshID(MeshID id) {
|
void MeshRendererComponent::SetMeshID(MeshID id) {
|
||||||
meshID = id;
|
meshID = id;
|
||||||
meshKey.clear();
|
meshKey.clear();
|
||||||
@@ -211,6 +255,9 @@ void MeshRendererComponent::SetMeshID(MeshID id) {
|
|||||||
meshKey = resources->meshes().getMeshKey(id);
|
meshKey = resources->meshes().getMeshKey(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (GetGameObject()->GetComponent<MeshShapeCollider>() != nullptr) {
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::SetMaterialID(MaterialID id) {
|
void MeshRendererComponent::SetMaterialID(MaterialID id) {
|
||||||
@@ -241,9 +288,11 @@ void MeshRendererComponent::Render(const RenderContext& ctx) {
|
|||||||
GetTransform().GetWorldMatrix(),
|
GetTransform().GetWorldMatrix(),
|
||||||
materialID,
|
materialID,
|
||||||
m_skinnedMesh.get(),
|
m_skinnedMesh.get(),
|
||||||
jointMatricesStartIndex
|
jointMatricesStartIndex,
|
||||||
|
static_cast<std::uint32_t>(GetGameObject()->GetId())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
|
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID,
|
||||||
|
static_cast<std::uint32_t>(GetGameObject()->GetId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/Components/Animator.h>
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
|
#include <destrum/Graphics/Renderer.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Assets/AssetTypes.h>
|
||||||
|
#include <destrum/Util/GameState.h>
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct TextureSlot {
|
||||||
|
const char* label;
|
||||||
|
ImageID Material::* member;
|
||||||
|
TextureIntent intent;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array<TextureSlot, 6> TextureSlots{{
|
||||||
|
{"Diffuse", &Material::diffuseTexture, TextureIntent::ColorSrgb},
|
||||||
|
{"Normal", &Material::normalTexture, TextureIntent::DataLinear},
|
||||||
|
{"Metallic", &Material::metallicTexture, TextureIntent::DataLinear},
|
||||||
|
{"Roughness", &Material::roughnessTexture, TextureIntent::DataLinear},
|
||||||
|
{"Occlusion", &Material::occlusionTexture, TextureIntent::DataLinear},
|
||||||
|
{"Emissive", &Material::emissiveTexture, TextureIntent::ColorSrgb},
|
||||||
|
}};
|
||||||
|
|
||||||
|
[[nodiscard]] std::string Lowercase(std::string_view value)
|
||||||
|
{
|
||||||
|
std::string result{value};
|
||||||
|
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Matches(std::string_view text, const std::string& filter)
|
||||||
|
{
|
||||||
|
return filter.empty() || Lowercase(text).find(Lowercase(filter)) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> AcceptAssetDrop()
|
||||||
|
{
|
||||||
|
std::optional<std::string> dropped;
|
||||||
|
if (ImGui::BeginDragDropTarget()) {
|
||||||
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
|
||||||
|
dropped = static_cast<const char*>(payload->Data);
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
return dropped;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool PickerButton(const char* label, const std::string& value)
|
||||||
|
{
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(label);
|
||||||
|
ImGui::SameLine(90.0f);
|
||||||
|
const bool clicked = ImGui::Button((value + "##" + label).c_str(), ImVec2{-FLT_MIN, 0.0f});
|
||||||
|
ImGui::SetItemTooltip("%s\nClick to pick, or drop a file from the Assets window", value.c_str());
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string TextureLabel(const RenderResources& resources, ImageID id)
|
||||||
|
{
|
||||||
|
if (id == NULL_IMAGE_ID) {
|
||||||
|
return "<none>";
|
||||||
|
}
|
||||||
|
if (const ImageCache::LoadedImageInfo* info = resources.images().findLoadedImageInfo(id)) {
|
||||||
|
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
|
||||||
|
return virtualPath ? *virtualPath : info->path.filename().string();
|
||||||
|
}
|
||||||
|
return "texture " + std::to_string(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int CountMaterialUsers(const GameObject* self, MaterialID id)
|
||||||
|
{
|
||||||
|
const Scene* scene = self != nullptr ? self->GetScene() : nullptr;
|
||||||
|
if (scene == nullptr) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int users = 0;
|
||||||
|
for (const auto& object : scene->GetObjects()) {
|
||||||
|
for (const auto& component : object->GetComponents()) {
|
||||||
|
const auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(component.get());
|
||||||
|
if (meshRenderer != nullptr && meshRenderer->GetMaterialID() == id) {
|
||||||
|
++users;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Fn>
|
||||||
|
void PickerList(const char* id, Fn&& drawItems)
|
||||||
|
{
|
||||||
|
const float height = std::min(ImGui::GetTextLineHeightWithSpacing() * 14.0f, 320.0f);
|
||||||
|
if (ImGui::BeginChild(id, ImVec2{420.0f, height}, ImGuiChildFlags_None)) {
|
||||||
|
drawItems();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::ImGuiInspector()
|
||||||
|
{
|
||||||
|
if (!GameState::GetInstance().HasRenderer() || !GameState::GetInstance().HasGfxDevice()) {
|
||||||
|
ImGui::TextDisabled("Meshes and materials can only be picked with a renderer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GameRenderer& renderer = GameState::GetInstance().Renderer();
|
||||||
|
RenderResources* resources = renderer.getResources();
|
||||||
|
if (resources == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawMeshField(*resources);
|
||||||
|
ImGui::Spacing();
|
||||||
|
DrawMaterialField(*resources, renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::DrawMeshField(RenderResources& resources)
|
||||||
|
{
|
||||||
|
// The skinning buffer is sized for the mesh in Start, a different mesh would overrun it
|
||||||
|
const bool skinned = GetGameObject()->GetComponent<Animator>() != nullptr;
|
||||||
|
ImGui::BeginDisabled(skinned);
|
||||||
|
if (PickerButton("Mesh", meshID == NULL_MESH_ID ? "<none>" : meshKey)) {
|
||||||
|
m_MeshSearch.clear();
|
||||||
|
ImGui::OpenPopup("MeshPicker");
|
||||||
|
}
|
||||||
|
if (const auto dropped = AcceptAssetDrop(); dropped && AssetTypes::IsModel(*dropped)) {
|
||||||
|
LoadDroppedModel(resources, *dropped);
|
||||||
|
std::vector<MeshID> parts;
|
||||||
|
for (MeshID id = 0; id < resources.meshes().getMeshes().size(); ++id) {
|
||||||
|
if (resources.meshes().isValid(id) && resources.meshes().getMeshKey(id).starts_with(*dropped + "#") &&
|
||||||
|
!resources.meshes().getMesh(id).hasSkeleton) {
|
||||||
|
parts.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parts.size() == 1) {
|
||||||
|
SetMeshID(parts.front());
|
||||||
|
} else if (!parts.empty()) {
|
||||||
|
m_MeshSearch = *dropped;
|
||||||
|
ImGui::OpenPopup("MeshPicker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (skinned) {
|
||||||
|
ImGui::TextDisabled("Animated meshes can't be swapped here");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopup("MeshPicker")) {
|
||||||
|
if (ImGui::IsWindowAppearing()) {
|
||||||
|
ImGui::SetKeyboardFocusHere();
|
||||||
|
}
|
||||||
|
ImGui::SetNextItemWidth(420.0f);
|
||||||
|
ImGuiUtils::InputString("##Search", m_MeshSearch, 0, "Search");
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Primitives");
|
||||||
|
for (const PrimitiveMeshes::Shape shape : PrimitiveMeshes::AllShapes) {
|
||||||
|
const char* name = PrimitiveMeshes::ShapeName(shape);
|
||||||
|
if (Matches(name, m_MeshSearch) && ImGui::Selectable(name)) {
|
||||||
|
AssignPrimitive(resources, PrimitiveMeshes::DefaultDesc(shape));
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Loaded");
|
||||||
|
PickerList("LoadedMeshes", [&] {
|
||||||
|
const MeshCache& meshes = resources.meshes();
|
||||||
|
for (MeshID id = 0; id < meshes.getMeshes().size(); ++id) {
|
||||||
|
const std::string& key = meshes.getMeshKey(id);
|
||||||
|
if (!meshes.isValid(id) || meshes.getMesh(id).hasSkeleton || !Matches(key, m_MeshSearch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ImGui::PushID(static_cast<int>(id));
|
||||||
|
if (ImGui::Selectable(key.c_str(), id == meshID)) {
|
||||||
|
SetMeshID(id);
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawPrimitiveSettings(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::DrawPrimitiveSettings(RenderResources& resources)
|
||||||
|
{
|
||||||
|
if (meshID == NULL_MESH_ID || !PrimitiveMeshes::IsPrimitiveKey(meshKey)) {
|
||||||
|
m_PrimitiveDraft.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Edits go into a draft and only build a mesh once the drag ends, every step would add one to the cache
|
||||||
|
if (!m_PrimitiveDraft || m_PrimitiveDraftKey != meshKey) {
|
||||||
|
m_PrimitiveDraft = PrimitiveMeshes::ParseKey(meshKey);
|
||||||
|
m_PrimitiveDraftKey = meshKey;
|
||||||
|
}
|
||||||
|
if (!m_PrimitiveDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveMeshes::Desc& draft = *m_PrimitiveDraft;
|
||||||
|
bool apply = false;
|
||||||
|
const auto edited = [&apply] { apply = apply || ImGui::IsItemDeactivatedAfterEdit(); };
|
||||||
|
constexpr float Min = 0.01f;
|
||||||
|
constexpr float Max = 1000.0f;
|
||||||
|
|
||||||
|
switch (draft.shape) {
|
||||||
|
case PrimitiveMeshes::Shape::Box:
|
||||||
|
ImGui::DragFloat3("Size", &draft.size.x, 0.05f, Min, Max, "%.2f m");
|
||||||
|
edited();
|
||||||
|
break;
|
||||||
|
case PrimitiveMeshes::Shape::Plane: {
|
||||||
|
float size[2]{draft.size.x, draft.size.z};
|
||||||
|
if (ImGui::DragFloat2("Size", size, 0.05f, Min, Max, "%.2f m")) {
|
||||||
|
draft.size.x = size[0];
|
||||||
|
draft.size.z = size[1];
|
||||||
|
}
|
||||||
|
edited();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PrimitiveMeshes::Shape::Sphere:
|
||||||
|
ImGui::DragFloat("Radius", &draft.radius, 0.01f, Min, Max, "%.2f m");
|
||||||
|
edited();
|
||||||
|
break;
|
||||||
|
case PrimitiveMeshes::Shape::Cylinder:
|
||||||
|
case PrimitiveMeshes::Shape::Capsule:
|
||||||
|
ImGui::DragFloat("Radius", &draft.radius, 0.01f, Min, Max, "%.2f m");
|
||||||
|
edited();
|
||||||
|
ImGui::DragFloat("Height", &draft.height, 0.05f, Min, Max, "%.2f m");
|
||||||
|
edited();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apply) {
|
||||||
|
draft.size = glm::max(draft.size, glm::vec3{Min});
|
||||||
|
draft.radius = std::max(draft.radius, Min);
|
||||||
|
draft.height = std::max(draft.height, Min);
|
||||||
|
AssignPrimitive(resources, draft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::AssignPrimitive(RenderResources& resources, const PrimitiveMeshes::Desc& desc)
|
||||||
|
{
|
||||||
|
const std::string key = PrimitiveMeshes::MakeKey(desc);
|
||||||
|
auto id = resources.meshes().findMeshByKey(key);
|
||||||
|
if (!id) {
|
||||||
|
id = resources.meshes().addMesh(GameState::GetInstance().Gfx(), PrimitiveMeshes::Make(desc));
|
||||||
|
}
|
||||||
|
SetMeshID(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::LoadDroppedModel(RenderResources& resources, const std::string& path)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
resources.loadModelAsset(GameState::GetInstance().Gfx(), path);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Could not load {}: {}", path, exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRenderer& renderer)
|
||||||
|
{
|
||||||
|
MaterialCache& materials = resources.materials();
|
||||||
|
if (PickerButton("Material", materialID == NULL_MATERIAL_ID ? "<none>" : materialKey)) {
|
||||||
|
m_MaterialSearch.clear();
|
||||||
|
ImGui::OpenPopup("MaterialPicker");
|
||||||
|
}
|
||||||
|
if (const auto dropped = AcceptAssetDrop()) {
|
||||||
|
if (AssetTypes::IsMaterial(*dropped)) {
|
||||||
|
try {
|
||||||
|
SetMaterialID(MaterialSerialization::LoadFile(resources, GameState::GetInstance().Gfx(), *dropped));
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Could not load {}: {}", *dropped, exception.what());
|
||||||
|
}
|
||||||
|
} else if (AssetTypes::IsModel(*dropped)) {
|
||||||
|
LoadDroppedModel(resources, *dropped);
|
||||||
|
m_MaterialSearch = *dropped;
|
||||||
|
ImGui::OpenPopup("MaterialPicker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopup("MaterialPicker")) {
|
||||||
|
if (ImGui::IsWindowAppearing()) {
|
||||||
|
ImGui::SetKeyboardFocusHere();
|
||||||
|
}
|
||||||
|
ImGui::SetNextItemWidth(420.0f);
|
||||||
|
ImGuiUtils::InputString("##Search", m_MaterialSearch, 0, "Search");
|
||||||
|
|
||||||
|
if (ImGui::Selectable("New Material")) {
|
||||||
|
SetMaterialID(materials.addMaterial(Material::SimpleColor(glm::vec3{1.0f}, "Material")));
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Loaded");
|
||||||
|
PickerList("LoadedMaterials", [&] {
|
||||||
|
for (MaterialID id = 0; id < materials.getMaterialCount(); ++id) {
|
||||||
|
const std::string& key = materials.getMaterialKey(id);
|
||||||
|
if (!materials.isValid(id) || id == materials.getPlaceholderMaterialId() || !Matches(key, m_MaterialSearch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ImGui::PushID(static_cast<int>(id));
|
||||||
|
if (ImGui::Selectable(key.c_str(), id == materialID)) {
|
||||||
|
SetMaterialID(id);
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialID != NULL_MATERIAL_ID) {
|
||||||
|
DrawMaterialEditor(resources, renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameRenderer& renderer)
|
||||||
|
{
|
||||||
|
if (!ImGui::TreeNodeEx("Material Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialCache& materials = resources.materials();
|
||||||
|
const std::string sourcePath = materials.getMaterial(materialID).assetReference.path;
|
||||||
|
const bool placeholder = materialID == materials.getPlaceholderMaterialId();
|
||||||
|
const bool materialFile = MaterialSerialization::IsFileKey(sourcePath);
|
||||||
|
const bool fromModel = !sourcePath.empty() && !materialFile;
|
||||||
|
const int users = CountMaterialUsers(GetGameObject(), materialID);
|
||||||
|
|
||||||
|
// A copy without a source file is saved inside the scene, so it can be edited freely
|
||||||
|
const auto copyForThisObject = [&] {
|
||||||
|
Material copy = materials.getMaterial(materialID);
|
||||||
|
copy.assetReference = {};
|
||||||
|
if (copy.name.empty() || placeholder) {
|
||||||
|
copy.name = "Material";
|
||||||
|
}
|
||||||
|
SetMaterialID(materials.addMaterial(std::move(copy)));
|
||||||
|
};
|
||||||
|
|
||||||
|
bool replaced = false;
|
||||||
|
if (placeholder) {
|
||||||
|
ImGui::TextDisabled("The default material, pick New Material to edit one");
|
||||||
|
} else if (fromModel) {
|
||||||
|
ImGui::TextDisabled("From %s, edits would not be saved", sourcePath.c_str());
|
||||||
|
replaced = ImGui::Button("Make Editable Copy");
|
||||||
|
if (replaced) {
|
||||||
|
copyForThisObject();
|
||||||
|
}
|
||||||
|
} else if (materialFile) {
|
||||||
|
ImGui::TextDisabled("%s, used by %d objects here", sourcePath.c_str(), users);
|
||||||
|
if (ImGui::SmallButton("Make Unique")) {
|
||||||
|
copyForThisObject();
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (users > 1) {
|
||||||
|
ImGui::Text("Shared by %d objects, edits change all of them", users);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("Make Unique")) {
|
||||||
|
copyForThisObject();
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced && ImGui::SmallButton("Save as Material File")) {
|
||||||
|
const std::string name = materials.getMaterial(materialID).name.empty() ? "material" : materials.getMaterial(materialID).name;
|
||||||
|
std::string path = "game://materials/" + name + std::string{AssetTypes::MaterialExtension};
|
||||||
|
for (int suffix = 1; std::filesystem::exists(AssetFS::GetInstance().GetFullPath(path)); ++suffix) {
|
||||||
|
path = "game://materials/" + name + "_" + std::to_string(suffix) + std::string{AssetTypes::MaterialExtension};
|
||||||
|
}
|
||||||
|
if (MaterialSerialization::SaveFile(resources, materialID, path)) {
|
||||||
|
SetMaterialID(MaterialSerialization::LoadFile(resources, GameState::GetInstance().Gfx(), path));
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replaced) {
|
||||||
|
// addMaterial may have moved the material storage, draw the new one next frame
|
||||||
|
ImGui::TreePop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(placeholder || fromModel);
|
||||||
|
Material& material = materials.getMaterialMutable(materialID);
|
||||||
|
bool changed = false;
|
||||||
|
bool finished = false;
|
||||||
|
const auto track = [&](bool valueChanged) {
|
||||||
|
changed |= valueChanged;
|
||||||
|
finished |= ImGui::IsItemDeactivatedAfterEdit();
|
||||||
|
};
|
||||||
|
track(ImGui::ColorEdit3("Base Color", &material.baseColor.x));
|
||||||
|
track(ImGui::SliderFloat("Metallic", &material.metallicFactor, 0.0f, 1.0f));
|
||||||
|
track(ImGui::SliderFloat("Roughness", &material.roughnessFactor, 0.0f, 1.0f));
|
||||||
|
track(ImGui::ColorEdit3("Emissive", &material.emissiveColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float));
|
||||||
|
track(ImGui::DragFloat("Normal Scale", &material.normalScale, 0.01f, 0.0f, 4.0f));
|
||||||
|
track(ImGui::SliderFloat("Occlusion", &material.occlusionStrength, 0.0f, 1.0f));
|
||||||
|
|
||||||
|
constexpr std::array<const char*, 3> filteringNames{"Nearest", "Linear", "Anisotropic"};
|
||||||
|
int filtering = static_cast<int>(material.textureFilteringMode);
|
||||||
|
if (ImGui::Combo("Filtering", &filtering, filteringNames.data(), static_cast<int>(filteringNames.size()))) {
|
||||||
|
material.textureFilteringMode = static_cast<TextureFilteringMode>(filtering);
|
||||||
|
changed = finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::array<const char*, 2> blendModeNames{"Opaque", "Transparent"};
|
||||||
|
int blendMode = static_cast<int>(material.blendMode);
|
||||||
|
if (ImGui::Combo("Blend", &blendMode, blendModeNames.data(), static_cast<int>(blendModeNames.size()))) {
|
||||||
|
material.blendMode = static_cast<BlendMode>(blendMode);
|
||||||
|
changed = finished = true;
|
||||||
|
}
|
||||||
|
if (material.blendMode == BlendMode::Transparent) {
|
||||||
|
track(ImGui::SliderFloat("Opacity", &material.opacity, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SeparatorText("Textures");
|
||||||
|
for (const TextureSlot& slot : TextureSlots) {
|
||||||
|
ImGui::PushID(slot.label);
|
||||||
|
ImageID& texture = material.*slot.member;
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(slot.label);
|
||||||
|
ImGui::SameLine(90.0f);
|
||||||
|
const float clearWidth = ImGui::GetFrameHeight();
|
||||||
|
ImGui::Button(TextureLabel(resources, texture).c_str(),
|
||||||
|
ImVec2{ImGui::GetContentRegionAvail().x - clearWidth - ImGui::GetStyle().ItemSpacing.x, 0.0f});
|
||||||
|
ImGui::SetItemTooltip("Drop an image from the Assets window");
|
||||||
|
if (const auto dropped = AcceptAssetDrop(); dropped && AssetTypes::IsImage(*dropped)) {
|
||||||
|
try {
|
||||||
|
texture = resources.loadMaterialTexture(GameState::GetInstance().Gfx(), AssetFS::GetInstance().GetFullPath(*dropped), slot.intent);
|
||||||
|
changed = finished = true;
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Could not load {}: {}", *dropped, exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginDisabled(texture == NULL_IMAGE_ID);
|
||||||
|
if (ImGui::Button("x", ImVec2{clearWidth, 0.0f})) {
|
||||||
|
texture = NULL_IMAGE_ID;
|
||||||
|
changed = finished = true;
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
renderer.updateMaterialGPU(materialID);
|
||||||
|
}
|
||||||
|
if (finished && materialFile) {
|
||||||
|
MaterialSerialization::SaveFile(resources, materialID, sourcePath);
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
@@ -7,10 +7,9 @@
|
|||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/ObjectModel/Transform.h"
|
#include "destrum/ObjectModel/Transform.h"
|
||||||
#include "destrum/Components/MeshRendererComponent.h"
|
#include "destrum/Components/MeshRendererComponent.h"
|
||||||
#include "destrum/Serialization/GlmJson.h"
|
#include "destrum/Serialization/Reflect.h"
|
||||||
#include "destrum/Util/GameState.h"
|
#include "destrum/Util/GameState.h"
|
||||||
#include "destrum/Util/DeltaTime.h"
|
#include "destrum/Util/DeltaTime.h"
|
||||||
#include "destrum/Util/ImGuiUtils.h"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -126,40 +125,33 @@ void OrbitAndSpin::Start() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json OrbitAndSpin::Serialize() const {
|
void OrbitAndSpin::Reflect(FieldVisitor& v) {
|
||||||
return {
|
v.Field("radius", m_Radius, {.speed = 0.05f, .min = 0.0});
|
||||||
{"radius", m_Radius},
|
v.Field("center", m_Center);
|
||||||
{"center", m_Center},
|
v.Field("orbitAxis", m_OrbitAxis);
|
||||||
{"orbitAxis", m_OrbitAxis},
|
v.Field("orbitSpeed", m_OrbitSpeed, {.speed = 0.05f});
|
||||||
{"orbitSpeed", m_OrbitSpeed},
|
v.Field("orbitPhase", m_OrbitPhase, {.speed = 0.05f});
|
||||||
{"orbitAngle", m_OrbitAngle},
|
v.Field("orbitAngle", m_OrbitAngle, {.format = "%.2f rad", .readOnly = true});
|
||||||
{"orbitPhase", m_OrbitPhase},
|
v.Field("growSpeed", m_GrowSpeed, {.speed = 0.05f, .min = 0.0});
|
||||||
{"growPhase", m_GrowPhase},
|
v.Field("growMin", m_GrowMin, {.speed = 0.01f, .min = 0.0});
|
||||||
{"growSpeed", m_GrowSpeed},
|
v.Field("growMax", m_GrowMax, {.speed = 0.01f, .min = 0.0});
|
||||||
{"growMin", m_GrowMin},
|
v.Field("baseScale", m_BaseScale, {.speed = 0.05f});
|
||||||
{"growMax", m_GrowMax},
|
v.Field("spinAxis", m_SpinAxis);
|
||||||
{"spinAxis", m_SpinAxis},
|
v.Field("spinSpeed", m_SpinSpeed, {.speed = 0.05f});
|
||||||
{"spinSpeed", m_SpinSpeed},
|
v.Field("growPhase", m_GrowPhase, {.format = "%.2f rad", .readOnly = true});
|
||||||
{"baseScale", m_BaseScale}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
||||||
m_Radius = data.value("radius", m_Radius);
|
Component::Deserialize(data);
|
||||||
m_Center = data.value("center", m_Center);
|
|
||||||
m_OrbitAxis = data.value("orbitAxis", m_OrbitAxis);
|
|
||||||
m_OrbitSpeed = data.value("orbitSpeed", m_OrbitSpeed);
|
|
||||||
m_OrbitAngle = data.value("orbitAngle", m_OrbitAngle);
|
|
||||||
m_OrbitPhase = data.value("orbitPhase", m_OrbitPhase);
|
|
||||||
m_GrowPhase = data.value("growPhase", m_GrowPhase);
|
|
||||||
m_GrowSpeed = data.value("growSpeed", m_GrowSpeed);
|
|
||||||
m_GrowMin = data.value("growMin", m_GrowMin);
|
|
||||||
m_GrowMax = data.value("growMax", m_GrowMax);
|
|
||||||
m_SpinAxis = data.value("spinAxis", m_SpinAxis);
|
|
||||||
m_BaseScale = data.value("baseScale", m_BaseScale);
|
|
||||||
m_BaseScaleLoaded = data.contains("baseScale");
|
m_BaseScaleLoaded = data.contains("baseScale");
|
||||||
m_SpinSpeed = data.value("spinSpeed", m_SpinSpeed);
|
Sanitize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrbitAndSpin::OnFieldsEdited() {
|
||||||
|
Sanitize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrbitAndSpin::Sanitize() {
|
||||||
if (m_GrowMin > m_GrowMax) {
|
if (m_GrowMin > m_GrowMax) {
|
||||||
std::swap(m_GrowMin, m_GrowMax);
|
std::swap(m_GrowMin, m_GrowMax);
|
||||||
}
|
}
|
||||||
@@ -172,62 +164,6 @@ void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OrbitAndSpin::ImGuiInspector() {
|
void OrbitAndSpin::ImGuiInspector() {
|
||||||
float radius = m_Radius;
|
Component::ImGuiInspector();
|
||||||
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0f)) {
|
|
||||||
SetRadius(radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 center = m_Center;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Center", center)) {
|
|
||||||
SetCenter(center);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 orbitAxis = m_OrbitAxis;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Orbit axis", orbitAxis)) {
|
|
||||||
if (glm::dot(orbitAxis, orbitAxis) >= 1e-8f) {
|
|
||||||
m_OrbitAxis = glm::normalize(orbitAxis);
|
|
||||||
BuildOrbitBasis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float orbitSpeed = m_OrbitSpeed;
|
|
||||||
if (ImGui::DragFloat("Orbit speed", &orbitSpeed, 0.05f)) {
|
|
||||||
m_OrbitSpeed = orbitSpeed;
|
|
||||||
}
|
|
||||||
ImGui::DragFloat("Orbit phase", &m_OrbitPhase, 0.05f);
|
|
||||||
ImGui::Text("Orbit angle: %.2f rad", m_OrbitAngle);
|
|
||||||
|
|
||||||
float growSpeed = m_GrowSpeed;
|
|
||||||
if (ImGui::DragFloat("Grow speed", &growSpeed, 0.05f, 0.0f)) {
|
|
||||||
m_GrowSpeed = growSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
float growMin = m_GrowMin;
|
|
||||||
if (ImGui::DragFloat("Grow min", &growMin, 0.01f, 0.0f)) {
|
|
||||||
m_GrowMin = std::min(growMin, m_GrowMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
float growMax = m_GrowMax;
|
|
||||||
if (ImGui::DragFloat("Grow max", &growMax, 0.01f, 0.0f)) {
|
|
||||||
m_GrowMax = std::max(growMax, m_GrowMin);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 baseScale = m_BaseScale;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Base scale", baseScale, 0.05f)) {
|
|
||||||
m_BaseScale = baseScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 spinAxis = m_SpinAxis;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Spin axis", spinAxis)) {
|
|
||||||
if (glm::dot(spinAxis, spinAxis) >= 1e-8f) {
|
|
||||||
m_SpinAxis = glm::normalize(spinAxis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float spinSpeed = m_SpinSpeed;
|
|
||||||
if (ImGui::DragFloat("Spin speed", &spinSpeed, 0.05f)) {
|
|
||||||
m_SpinSpeed = spinSpeed;
|
|
||||||
}
|
|
||||||
ImGui::Text("Grow phase: %.2f rad", m_GrowPhase);
|
|
||||||
ImGui::Text("Material ID: %u", m_MaterialID);
|
ImGui::Text("Material ID: %u", m_MaterialID);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,13 @@
|
|||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
|
|
||||||
#include <destrum/Graphics/DebugDraw.h>
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
#include <destrum/Util/ImGuiUtils.h>
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
void BoxCollider::DrawDebugLines(const glm::vec4& color) const {
|
void BoxCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||||
DebugDraw::Box(GetWorldCenter(), GetWorldRotation(), m_HalfExtents, color);
|
DebugDraw::Box(GetWorldCenter(), GetWorldRotation(), m_HalfExtents, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json BoxCollider::Serialize() const {
|
void BoxCollider::Reflect(FieldVisitor& v) {
|
||||||
auto data = SerializeCollider();
|
Collider::Reflect(v);
|
||||||
data["halfExtents"] = m_HalfExtents;
|
v.Property("halfExtents", m_HalfExtents, [this](const glm::vec3& halfExtents) { SetHalfExtents(halfExtents); }, {.min = 0.0001});
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BoxCollider::Deserialize(const nlohmann::json& data) {
|
|
||||||
DeserializeCollider(data);
|
|
||||||
if (data.contains("halfExtents")) {
|
|
||||||
SetHalfExtents(data.at("halfExtents").get<glm::vec3>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BoxCollider::ImGuiInspector() {
|
|
||||||
Collider::ImGuiInspector();
|
|
||||||
|
|
||||||
glm::vec3 halfExtents = m_HalfExtents;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Half extents", halfExtents)) {
|
|
||||||
SetHalfExtents(halfExtents);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,14 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/DebugDraw.h>
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
void CapsuleCollider::DrawDebugLines(const glm::vec4& color) const {
|
void CapsuleCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||||
DebugDraw::Capsule(GetWorldCenter(), GetWorldRotation(), m_Radius, m_Height, color);
|
DebugDraw::Capsule(GetWorldCenter(), GetWorldRotation(), m_Radius, m_Height, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CapsuleCollider::ImGuiInspector() {
|
void CapsuleCollider::Reflect(FieldVisitor& v) {
|
||||||
Collider::ImGuiInspector();
|
Collider::Reflect(v);
|
||||||
|
v.Property("radius", m_Radius, [this](float radius) { SetRadius(radius); }, {.speed = 0.05f, .min = 0.0001});
|
||||||
float radius = m_Radius;
|
v.Property("height", m_Height, [this](float height) { SetHeight(height); }, {.speed = 0.05f, .min = 0.0001});
|
||||||
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) {
|
|
||||||
SetRadius(radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
float height = m_Height;
|
|
||||||
if (ImGui::DragFloat("Height", &height, 0.05f, 0.0001f)) {
|
|
||||||
SetHeight(height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#include <destrum/Components/Physics/Collider.h>
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
#include <destrum/Util/ImGuiUtils.h>
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
glm::vec3 Collider::GetWorldCenter() const {
|
glm::vec3 Collider::GetWorldCenter() const {
|
||||||
Transform& transform = GetTransform();
|
Transform& transform = GetTransform();
|
||||||
@@ -14,14 +12,7 @@ glm::quat Collider::GetWorldRotation() const {
|
|||||||
return GetTransform().GetWorldRotation();
|
return GetTransform().GetWorldRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Collider::ImGuiInspector() {
|
void Collider::Reflect(FieldVisitor& v) {
|
||||||
bool isTrigger = m_IsTrigger;
|
v.Property("isTrigger", m_IsTrigger, [this](bool trigger) { SetTrigger(trigger); });
|
||||||
if (ImGui::Checkbox("Is trigger", &isTrigger)) {
|
v.Property("centerOffset", m_CenterOffset, [this](const glm::vec3& offset) { SetCenterOffset(offset); });
|
||||||
SetTrigger(isTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 centerOffset = m_CenterOffset;
|
|
||||||
if (ImGuiUtils::DrawVec3Control("Center offset", centerOffset)) {
|
|
||||||
SetCenterOffset(centerOffset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include <destrum/Components/Physics/ConvexHullCollider.h>
|
||||||
|
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
|
void ConvexHullCollider::Reflect(FieldVisitor& v) {
|
||||||
|
Collider::Reflect(v);
|
||||||
|
v.Property("maxVertices", m_MaxVertices, [this](int maxVertices) { SetMaxVertices(maxVertices); },
|
||||||
|
{.speed = 1.0f, .min = MinVertices, .max = MaxVertices});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConvexHullCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||||
|
DrawConvexHull(m_MaxVertices, color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#include <destrum/Components/Physics/CylinderCollider.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
|
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
|
void CylinderCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||||
|
DebugDraw::Cylinder(GetWorldCenter(), GetWorldRotation(), m_Radius, m_Height, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CylinderCollider::Reflect(FieldVisitor& v) {
|
||||||
|
Collider::Reflect(v);
|
||||||
|
v.Property("radius", m_Radius, [this](float radius) { SetRadius(radius); }, {.speed = 0.05f, .min = 0.0001});
|
||||||
|
v.Property("height", m_Height, [this](float height) { SetHeight(height); }, {.speed = 0.05f, .min = 0.0001});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user