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"]
|
||||
path = destrum/third_party/imguizmo
|
||||
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)
|
||||
|
||||
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)
|
||||
if (NOT DESTRUM_ENABLE_WARNINGS)
|
||||
@@ -27,6 +33,7 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
add_subdirectory(destrum)
|
||||
add_subdirectory(editor)
|
||||
add_subdirectory(lightkeeper)
|
||||
add_subdirectory(TheChef)
|
||||
|
||||
@@ -37,6 +44,7 @@ endif()
|
||||
|
||||
destrum_enable_warnings(destrum_assets)
|
||||
destrum_enable_warnings(destrum)
|
||||
destrum_enable_warnings(destrum_editor)
|
||||
destrum_enable_warnings(lightkeeper)
|
||||
|
||||
add_custom_target(CleanupAssets
|
||||
@@ -53,10 +61,19 @@ add_dependencies(CookAssets
|
||||
_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)
|
||||
|
||||
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
|
||||
-fsanitize=address,undefined
|
||||
-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
|
||||
* Skybox/cubemap rendering
|
||||
* 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
|
||||
|
||||
## Repository layout
|
||||
@@ -24,6 +28,8 @@ Destrum is a C++20 Vulkan rendering/game-engine playground. It contains the core
|
||||
│ ├── src/ # Engine implementation
|
||||
│ ├── assets_src/ # Source engine assets
|
||||
│ └── 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
|
||||
│ ├── include/
|
||||
│ ├── src/
|
||||
@@ -52,7 +58,8 @@ Most dependencies are included as Git submodules:
|
||||
* GLM
|
||||
* nlohmann/json
|
||||
* spdlog
|
||||
* stb
|
||||
* stb (image, vorbis)
|
||||
* OpenAL Soft
|
||||
* tinygltf
|
||||
* tinyexr
|
||||
* Assimp
|
||||
@@ -111,6 +118,16 @@ cmake --build build --target ValidateShaders
|
||||
`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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
@@ -119,7 +136,7 @@ cmake --install build --config Release
|
||||
|
||||
## 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
|
||||
|
||||
@@ -134,9 +151,11 @@ destrum::destrum
|
||||
Game/demo targets can link against it:
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
"src/Assets/ModelFile.cpp"
|
||||
"src/Assets/TextureFile.cpp"
|
||||
"src/Assets/PackFile.cpp"
|
||||
)
|
||||
|
||||
add_library(destrum::assets ALIAS destrum_assets)
|
||||
@@ -36,10 +37,16 @@ set(SRC_FILES
|
||||
"src/Event.cpp"
|
||||
"src/Window.cpp"
|
||||
|
||||
"src/Audio/AudioDecoder.cpp"
|
||||
"src/Audio/AudioSystem.cpp"
|
||||
|
||||
"src/Components/Animator.cpp"
|
||||
"src/Components/AudioListener.cpp"
|
||||
"src/Components/AudioSource.cpp"
|
||||
"src/Components/CameraComponent.cpp"
|
||||
"src/Components/LightComponent.cpp"
|
||||
"src/Components/MeshRendererComponent.cpp"
|
||||
"src/Components/MeshRendererInspector.cpp"
|
||||
"src/Components/Rotator.cpp"
|
||||
"src/Components/Spinner.cpp"
|
||||
"src/Components/OrbitAndSpin.cpp"
|
||||
@@ -48,6 +55,10 @@ set(SRC_FILES
|
||||
"src/Components/Physics/BoxCollider.cpp"
|
||||
"src/Components/Physics/CapsuleCollider.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/UI/Canvas.cpp"
|
||||
@@ -74,7 +85,6 @@ set(SRC_FILES
|
||||
"src/Graphics/Swapchain.cpp"
|
||||
"src/Graphics/Util.cpp"
|
||||
"src/Graphics/RenderResources.cpp"
|
||||
"src/Graphics/ResourcesInspector.cpp"
|
||||
|
||||
"src/Graphics/Managers/VulkanInstanceManager.cpp"
|
||||
"src/Graphics/Managers/MemoryManager.cpp"
|
||||
@@ -98,6 +108,7 @@ set(SRC_FILES
|
||||
"src/Graphics/Pipelines/ShadowMapPipeline.cpp"
|
||||
"src/Graphics/Pipelines/SkinningPipeline.cpp"
|
||||
"src/Graphics/Pipelines/ImguiPass.cpp"
|
||||
"src/Graphics/Pipelines/PostProcessPipeline.cpp"
|
||||
|
||||
"src/Input/InputManager.cpp"
|
||||
|
||||
@@ -110,25 +121,29 @@ set(SRC_FILES
|
||||
"src/Scene/Scene.cpp"
|
||||
"src/Scene/SceneManager.cpp"
|
||||
"src/Scene/ModelSpawner.cpp"
|
||||
"src/Scene/ResourceCollector.cpp"
|
||||
|
||||
"src/FS/AssetFS.cpp"
|
||||
"src/FS/Manifest.cpp"
|
||||
|
||||
"src/Assets/AssetManager.cpp"
|
||||
"src/Assets/AssetDatabase.cpp"
|
||||
|
||||
"src/Serialization/SceneSerializer.cpp"
|
||||
"src/Serialization/ObjectSerialization.cpp"
|
||||
"src/Serialization/PrefabSerializer.cpp"
|
||||
"src/Serialization/MaterialSerialization.cpp"
|
||||
"src/Serialization/PrefabCache.cpp"
|
||||
"src/Serialization/ComponentRegistry.cpp"
|
||||
"src/Serialization/Reflect.cpp"
|
||||
|
||||
"src/Util/DeltaTime.cpp"
|
||||
"src/Util/ImGuiUtils.cpp"
|
||||
"src/Util/TransformGizmo.cpp"
|
||||
"src/Util/RectGizmo.cpp"
|
||||
"src/Util/SceneInspector.cpp"
|
||||
|
||||
"src/Threading/JobSystem.cpp"
|
||||
|
||||
"src/Physics/PhysicsWorld.cpp"
|
||||
"src/Physics/PhysicsMeshCache.cpp"
|
||||
"src/Physics/PhysicsSceneBridge.cpp"
|
||||
"src/Physics/JoltPhysicsWorld.cpp"
|
||||
)
|
||||
@@ -171,6 +186,8 @@ target_link_libraries(destrum
|
||||
PRIVATE
|
||||
freetype::freetype
|
||||
tinyexr
|
||||
OpenAL::OpenAL
|
||||
stb::vorbis
|
||||
)
|
||||
|
||||
target_compile_definitions(destrum
|
||||
@@ -217,10 +234,21 @@ add_custom_target(_internal_cook_engine_assets
|
||||
COMMAND TheChef
|
||||
--input "${ASSETS_SRC_DIR}"
|
||||
--output "${ASSETS_RUNTIME_DIR}"
|
||||
${DESTRUM_CHEF_FLAGS}
|
||||
DEPENDS TheChef ${ENGINE_ASSET_SOURCES}
|
||||
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)
|
||||
if (SPIRV_VAL_EXECUTABLE)
|
||||
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")
|
||||
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
|
||||
ARCHIVE 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;
|
||||
VertexBuffer vertexBuffer;
|
||||
uint materialID;
|
||||
uint padding;
|
||||
uint objectID;
|
||||
};
|
||||
|
||||
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 = 3) in vec4 inTangent;
|
||||
layout (location = 4) flat in uint inMaterialID;
|
||||
layout (location = 5) flat in uint inObjectID;
|
||||
|
||||
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 n = normalize(inNormal);
|
||||
@@ -127,6 +141,10 @@ void main()
|
||||
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
|
||||
}
|
||||
|
||||
if (pcs.sceneData.debugSelectionView != 0) {
|
||||
fragColor = selectionColor(inObjectID & 0x7FFFFFFFu);
|
||||
}
|
||||
|
||||
#if 0
|
||||
// TANGENT DEBUG
|
||||
if (inTangent == vec4(0.0)) {
|
||||
|
||||
@@ -9,6 +9,7 @@ layout (location = 1) out vec2 outUV;
|
||||
layout (location = 2) out vec3 outNormal;
|
||||
layout (location = 3) out vec4 outTangent;
|
||||
layout (location = 4) flat out uint outMaterialID;
|
||||
layout (location = 5) flat out uint outObjectID;
|
||||
|
||||
// The depth pre-pass reuses this shader, positions have to match bit for bit
|
||||
// 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
|
||||
outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w);
|
||||
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
|
||||
uint shadowMapId;
|
||||
float shadowNormalOffset;
|
||||
uint padding;
|
||||
uint debugSelectionView;
|
||||
mat4 lightViewProj;
|
||||
|
||||
// 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
|
||||
#define APP_H
|
||||
#include <filesystem>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "glm/vec2.hpp"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <glm/vec2.hpp>
|
||||
|
||||
#include <destrum/AppLayer.h>
|
||||
#include <destrum/Window.h>
|
||||
#include <destrum/Graphics/Camera.h>
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
#include <destrum/Graphics/Pipelines/ImguiPass.h>
|
||||
#include <destrum/Graphics/ResourcesInspector.h>
|
||||
|
||||
#include <destrum/Input/InputManager.h>
|
||||
|
||||
|
||||
class App {
|
||||
public:
|
||||
struct AppParams {
|
||||
@@ -25,28 +25,57 @@ public:
|
||||
std::string appName{"Destrum App"};
|
||||
std::string windowTitle;
|
||||
std::filesystem::path exeDir;
|
||||
bool showDebugUi{true};
|
||||
WindowMode windowMode{WindowMode::Windowed};
|
||||
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();
|
||||
virtual ~App();
|
||||
|
||||
App(const App&) = delete;
|
||||
App& operator=(const App&) = delete;
|
||||
|
||||
void init(const AppParams& params);
|
||||
void attachLayer(std::unique_ptr<AppLayer> layer);
|
||||
void run();
|
||||
void cleanup();
|
||||
|
||||
virtual void customInit() = 0;
|
||||
virtual void customUpdate(float dt) = 0;
|
||||
virtual void customDraw() = 0;
|
||||
virtual void customUpdate(float) {}
|
||||
virtual void customFixedUpdate(float) {}
|
||||
virtual void customRender(GameRenderer&) {}
|
||||
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 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:
|
||||
Window window;
|
||||
@@ -55,44 +84,29 @@ protected:
|
||||
GfxDevice gfxDevice;
|
||||
GameRenderer renderer;
|
||||
RenderResources resources;
|
||||
|
||||
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 cleanedUp{false};
|
||||
bool cleaningUp{false};
|
||||
bool customInitStarted{false};
|
||||
bool gamePaused{false};
|
||||
|
||||
bool frameLimit{false};
|
||||
float frameTime{0.f};
|
||||
float avgFPS{0.f};
|
||||
float avgFPS{0.0f};
|
||||
|
||||
bool resizePending = false;
|
||||
bool resizePending{false};
|
||||
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 {
|
||||
RGBA16Float,
|
||||
RGBA8,
|
||||
};
|
||||
|
||||
struct Texture {
|
||||
@@ -26,6 +27,7 @@ namespace TextureFile {
|
||||
[[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 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]] 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"; }
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
// 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"; }
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
void SetType(LightType type) { m_Type = type; }
|
||||
[[nodiscard]] LightType GetType() const { return m_Type; }
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
#include <destrum/ObjectModel/Component.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 {
|
||||
public:
|
||||
@@ -30,6 +37,18 @@ public:
|
||||
}
|
||||
|
||||
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};
|
||||
MaterialID materialID{NULL_MATERIAL_ID};
|
||||
std::string meshKey;
|
||||
|
||||
@@ -26,9 +26,10 @@ public:
|
||||
|
||||
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 ImGuiInspector() override;
|
||||
void OnFieldsEdited() override;
|
||||
|
||||
// optional setters
|
||||
void SetRadius(float r) { m_Radius = r; }
|
||||
@@ -36,6 +37,7 @@ public:
|
||||
|
||||
private:
|
||||
void BuildOrbitBasis(); // builds m_U/m_V from m_OrbitAxis
|
||||
void Sanitize();
|
||||
|
||||
float m_Radius = 5.0f;
|
||||
glm::vec3 m_Center{0.0f};
|
||||
|
||||
@@ -11,16 +11,13 @@ public:
|
||||
|
||||
void Update(float) override {}
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
std::string GetTypeName() const override
|
||||
{
|
||||
return "BoxCollider";
|
||||
}
|
||||
|
||||
|
||||
void DrawDebugLines(const glm::vec4& color) const override;
|
||||
|
||||
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
||||
|
||||
@@ -17,24 +17,7 @@ public:
|
||||
return "CapsuleCollider";
|
||||
}
|
||||
|
||||
void ImGuiInspector() 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 Reflect(FieldVisitor& v) override;
|
||||
|
||||
void DrawDebugLines(const glm::vec4& color) const override;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
|
||||
[[nodiscard]] bool AffectsPhysics() const override { return true; }
|
||||
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() 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(); }
|
||||
|
||||
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};
|
||||
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 Destroy() override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
std::string GetTypeName() const override
|
||||
|
||||
@@ -12,9 +12,7 @@ public:
|
||||
|
||||
void Update(float) override {}
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
std::string GetTypeName() const override
|
||||
{
|
||||
return "SphereCollider";
|
||||
|
||||
@@ -20,9 +20,7 @@ public:
|
||||
|
||||
std::string GetTypeName() const override { return "Rotator"; }
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
void SetDistance(float distance);
|
||||
void SetSpeed(float speed) { m_Speed = speed; }
|
||||
|
||||
@@ -18,9 +18,7 @@ public:
|
||||
|
||||
std::string GetTypeName() const override { return "Spinner"; }
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
void SetAxis(const glm::vec3& axis) {
|
||||
m_Axis = glm::dot(axis, axis) > 0.000001f
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef DESTRUM_CANVAS_H
|
||||
#define DESTRUM_CANVAS_H
|
||||
|
||||
#include <destrum/Components/UI/CanvasFrame.h>
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
|
||||
@@ -22,10 +23,9 @@ public:
|
||||
void Start() override;
|
||||
void Render(const RenderContext& ctx) override;
|
||||
void Draw(const glm::vec2& screenSize);
|
||||
void Draw(const CanvasFrame& frame);
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
[[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"; }
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
[[nodiscard]] UIRect ComputeRect(const UIRect& parentRect, float scale) const;
|
||||
@@ -44,6 +43,8 @@ public:
|
||||
// Where the canvas last drew this object
|
||||
[[nodiscard]] const UIRect& GetScreenRect() const { return m_ScreenRect; }
|
||||
[[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)
|
||||
{
|
||||
m_ScreenRect = rect;
|
||||
|
||||
@@ -20,9 +20,7 @@ public:
|
||||
|
||||
void DrawUI(const UIRect& rect, float scale) override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
void SetShape(UIImageShape shape) { m_Shape = shape; }
|
||||
void SetOutline(float thickness) { m_Outline = thickness; }
|
||||
|
||||
@@ -21,9 +21,7 @@ public:
|
||||
|
||||
void DrawUI(const UIRect& rect, float scale) override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
void Reflect(FieldVisitor& v) override;
|
||||
|
||||
void SetText(std::string text) { m_Text = std::move(text); }
|
||||
void SetFont(std::string assetPath) { m_FontPath = std::move(assetPath); }
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <destrum/Singleton.h>
|
||||
#include <destrum/Assets/PackFile.h>
|
||||
|
||||
#include <destrum/FS/Manifest.h>
|
||||
|
||||
@@ -17,23 +19,42 @@ struct FSMount {
|
||||
std::string scheme; // "engine", "game"
|
||||
std::filesystem::path root;
|
||||
std::optional<AssetManifest> manifest;
|
||||
|
||||
std::optional<std::filesystem::path> sourceRoot;
|
||||
std::shared_ptr<const PackFile::Reader> pack;
|
||||
};
|
||||
|
||||
class AssetFS final: public Singleton<AssetFS> {
|
||||
public:
|
||||
static constexpr std::string_view PackName = "assets.dpak";
|
||||
|
||||
void Init(std::filesystem::path exeDir);
|
||||
void Reset();
|
||||
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);
|
||||
|
||||
[[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 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:
|
||||
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;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
@@ -29,7 +30,7 @@ struct AssetManifest {
|
||||
};
|
||||
|
||||
namespace FS {
|
||||
AssetManifest LoadAssetManifest(const std::filesystem::path& manifestPath);
|
||||
AssetManifest ParseAssetManifest(std::span<const std::uint8_t> bytes);
|
||||
}
|
||||
|
||||
#endif //MANIFEST_H
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -12,6 +14,7 @@
|
||||
#include <destrum/Graphics/GPUImage.h>
|
||||
#include <destrum/Graphics/ids.h>
|
||||
#include <destrum/Graphics/TextureIntent.h>
|
||||
#include <destrum/Graphics/imageLoader.h>
|
||||
|
||||
class GfxDevice;
|
||||
|
||||
@@ -19,6 +22,13 @@ class ImageCache {
|
||||
friend class ResourcesInspector;
|
||||
|
||||
public:
|
||||
struct LoadedImageInfo {
|
||||
std::filesystem::path path;
|
||||
TextureIntent intent;
|
||||
VkImageUsageFlags usage;
|
||||
bool mipMap;
|
||||
};
|
||||
|
||||
explicit ImageCache(GfxDevice& gfxDevice);
|
||||
|
||||
ImageID loadImageFromFile(
|
||||
@@ -26,9 +36,18 @@ public:
|
||||
VkImageUsageFlags usage,
|
||||
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(ImageID id, GPUImage image);
|
||||
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
||||
[[nodiscard]] const LoadedImageInfo* findLoadedImageInfo(ImageID id) const;
|
||||
void removeImage(ImageID id);
|
||||
|
||||
[[nodiscard]] ImageID getFreeImageId() const;
|
||||
@@ -53,16 +72,25 @@ public:
|
||||
}
|
||||
|
||||
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<PendingImage> pendingImages;
|
||||
std::shared_ptr<std::vector<ImageID>> freeIds = std::make_shared<std::vector<ImageID>>();
|
||||
GfxDevice& gfxDevice;
|
||||
|
||||
struct LoadedImageInfo {
|
||||
std::filesystem::path path;
|
||||
TextureIntent intent;
|
||||
VkImageUsageFlags usage;
|
||||
bool mipMap;
|
||||
};
|
||||
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
||||
ImageID errorImageId{NULL_IMAGE_ID};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#ifndef MATERIALCACHE_H
|
||||
#define MATERIALCACHE_H
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <destrum/Graphics/ids.h>
|
||||
#include <destrum/Graphics/Material.h>
|
||||
@@ -27,13 +28,16 @@ public:
|
||||
|
||||
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(
|
||||
glm::vec3 color,
|
||||
std::string name = {});
|
||||
MaterialID addSimpleTextureMaterial(ImageID textureID);
|
||||
[[nodiscard]] const Material& getMaterial(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> findMaterialByName(
|
||||
std::string_view name,
|
||||
@@ -54,6 +58,7 @@ private:
|
||||
|
||||
std::vector<Material> materials;
|
||||
std::vector<std::string> materialKeys;
|
||||
std::shared_ptr<std::vector<MaterialID>> freeIds = std::make_shared<std::vector<MaterialID>>();
|
||||
|
||||
static constexpr auto MAX_MATERIALS = 1000;
|
||||
GPUBuffer materialDataBuffer;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
#include <destrum/Graphics/Resources/Mesh.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "../ids.h"
|
||||
|
||||
@@ -15,6 +17,9 @@ public:
|
||||
void cleanup(GfxDevice& gfxDevice);
|
||||
|
||||
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;
|
||||
[[nodiscard]] const std::vector<GPUMesh>& getMeshes() const { return meshes; }
|
||||
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||
@@ -26,8 +31,19 @@ public:
|
||||
[[nodiscard]] std::uint32_t getIndexCapacity() const { return indexCapacity; }
|
||||
|
||||
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 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;
|
||||
|
||||
@@ -38,6 +54,7 @@ private:
|
||||
std::vector<GPUMesh> meshes;
|
||||
std::vector<CPUMesh> cpuMeshes;
|
||||
std::vector<std::string> meshKeys;
|
||||
std::shared_ptr<RecycleBin> recycleBin = std::make_shared<RecycleBin>();
|
||||
};
|
||||
|
||||
#endif //MESHCACHE_H
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace DebugDraw {
|
||||
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);
|
||||
@@ -14,6 +17,12 @@ namespace DebugDraw {
|
||||
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 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
|
||||
|
||||
@@ -50,8 +50,8 @@ public:
|
||||
struct EndFrameProps
|
||||
{
|
||||
VkClearColorValue clearColor{{0.f, 0.f, 0.f, 1.f}};
|
||||
glm::ivec4 drawImageBlitRect{};
|
||||
ImguiPass* imguiPass{nullptr};
|
||||
bool copyDrawImageToSwapchain{true};
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -141,6 +141,15 @@ public:
|
||||
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(
|
||||
const GPUImage& image,
|
||||
const void* pixelData,
|
||||
|
||||
@@ -22,7 +22,7 @@ struct GPUInstanceData {
|
||||
glm::mat4 transform;
|
||||
VkDeviceAddress vertexBuffer;
|
||||
std::uint32_t materialId;
|
||||
std::uint32_t padding;
|
||||
std::uint32_t objectId;
|
||||
};
|
||||
static_assert(sizeof(GPUInstanceData) == 80);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
class VulkanImmediateExecutor;
|
||||
class MemoryManager;
|
||||
struct ImageData;
|
||||
|
||||
class ImageManager {
|
||||
public:
|
||||
@@ -41,6 +42,12 @@ public:
|
||||
bool mipMap,
|
||||
TextureIntent intent) const;
|
||||
|
||||
[[nodiscard]] std::optional<GPUImage> createImageFromData(
|
||||
const ImageData& data,
|
||||
VkImageUsageFlags usage,
|
||||
bool mipMap,
|
||||
const std::string& debugName) const;
|
||||
|
||||
void uploadImageData(
|
||||
const GPUImage& image,
|
||||
const void* pixelData,
|
||||
|
||||
@@ -39,6 +39,11 @@ public:
|
||||
VkDeviceSize offset = 0,
|
||||
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;
|
||||
|
||||
private:
|
||||
|
||||
@@ -29,6 +29,11 @@ enum class TextureFilteringMode : std::uint32_t {
|
||||
Anisotropic,
|
||||
};
|
||||
|
||||
enum class BlendMode : std::uint32_t {
|
||||
Opaque,
|
||||
Transparent,
|
||||
};
|
||||
|
||||
// Metallic / roughness workflow, matching glTF:
|
||||
// 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.
|
||||
@@ -59,6 +64,9 @@ struct Material {
|
||||
float normalScale{1.f};
|
||||
float occlusionStrength{1.f};
|
||||
|
||||
BlendMode blendMode{BlendMode::Opaque};
|
||||
float opacity{1.f};
|
||||
|
||||
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
||||
|
||||
ImageID diffuseTexture{NULL_IMAGE_ID};
|
||||
|
||||
@@ -26,6 +26,8 @@ struct MeshDrawCommand {
|
||||
// skinned meshes only
|
||||
const SkinnedMesh* skinnedMesh{nullptr};
|
||||
std::uint32_t jointMatricesStartIndex{0};
|
||||
|
||||
std::uint32_t objectId{0};
|
||||
};
|
||||
|
||||
#endif //MESHDRAWCOMMAND_H
|
||||
|
||||
@@ -24,7 +24,8 @@ public:
|
||||
GfxDevice& gfxDevice,
|
||||
RenderResources& resources,
|
||||
VkFormat drawImageFormat,
|
||||
VkFormat depthImageFormat);
|
||||
VkFormat depthImageFormat,
|
||||
VkFormat selectionImageFormat);
|
||||
|
||||
void drawDepthPrepass(
|
||||
VkCommandBuffer cmd,
|
||||
@@ -32,7 +33,8 @@ public:
|
||||
const RenderResources& resources,
|
||||
const GPUBuffer& sceneDataBuffer,
|
||||
const IndirectDrawList& drawList,
|
||||
const IndirectDrawList::View& view);
|
||||
const IndirectDrawList::View& view,
|
||||
bool writeObjectIds);
|
||||
|
||||
void draw(
|
||||
VkCommandBuffer cmd,
|
||||
@@ -42,6 +44,14 @@ public:
|
||||
const IndirectDrawList& drawList,
|
||||
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 setRenderWireframe(bool wireframe) {
|
||||
@@ -70,7 +80,9 @@ private:
|
||||
private:
|
||||
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
|
||||
std::unique_ptr<Pipeline> m_pipeline;
|
||||
std::unique_ptr<Pipeline> m_transparentPipeline;
|
||||
std::unique_ptr<Pipeline> m_depthPrepassPipeline;
|
||||
std::unique_ptr<Pipeline> m_selectionPrepassPipeline;
|
||||
|
||||
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
|
||||
#define RENDERCONTEXT_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <destrum/Components/UI/CanvasFrame.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
|
||||
struct RenderContext {
|
||||
GameRenderer& renderer;
|
||||
const Camera& camera;
|
||||
const GameRenderer::SceneData& sceneData;
|
||||
// Unset: canvases fill the draw image
|
||||
std::optional<CanvasFrame> canvasFrame{};
|
||||
};
|
||||
|
||||
#endif //RENDERCONTEXT_H
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ModelDoc { struct MaterialInfo; }
|
||||
|
||||
@@ -45,6 +46,22 @@ public:
|
||||
bool mipMap = true,
|
||||
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);
|
||||
void destroyImage(ImageID id);
|
||||
|
||||
@@ -86,9 +103,26 @@ private:
|
||||
std::unique_ptr<MeshCache> meshCache;
|
||||
std::unique_ptr<MaterialCache> materialCache;
|
||||
|
||||
std::unordered_set<ImageID> materialTextures;
|
||||
std::unordered_set<ImageID> pinnedImages;
|
||||
|
||||
ImageID whiteImageId{NULL_IMAGE_ID};
|
||||
ImageID errorImageId{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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define RENDERER_H
|
||||
|
||||
#include <glm/mat4x4.hpp>
|
||||
#include <glm/vec2.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
#include <destrum/Graphics/Camera.h>
|
||||
@@ -10,9 +11,11 @@
|
||||
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
|
||||
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
|
||||
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
||||
#include <destrum/Graphics/Pipelines/PostProcessPipeline.h>
|
||||
#include <destrum/Graphics/Pipelines/Renderer2DPass.h>
|
||||
#include <destrum/Graphics/Pipelines/ShadowMapPipeline.h>
|
||||
#include <destrum/Graphics/ids.h>
|
||||
#include <destrum/Graphics/Tonemapper.h>
|
||||
#include <destrum/Graphics/MeshDrawCommand.h>
|
||||
#include <destrum/Graphics/Resources/NBuffer.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
@@ -21,6 +24,9 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
||||
@@ -47,6 +53,8 @@ public:
|
||||
glm::vec3 fogColor;
|
||||
float fogDensity;
|
||||
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();
|
||||
@@ -58,22 +66,22 @@ public:
|
||||
void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData);
|
||||
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,
|
||||
std::size_t jointMatricesStartIndex);
|
||||
std::size_t jointMatricesStartIndex, std::uint32_t objectId = 0);
|
||||
void submitLight(const Light& light);
|
||||
const GPUImage& getDrawImage() const;
|
||||
|
||||
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize)
|
||||
{
|
||||
createDrawImage(gfxDevice, newSize, false);
|
||||
createDrawImage(gfxDevice, newSize);
|
||||
lightCullingPipeline->resize(gfxDevice, newSize);
|
||||
}
|
||||
|
||||
Material& getMaterialMutable(MaterialID id);
|
||||
void updateMaterialGPU(MaterialID id);
|
||||
|
||||
void setSkyboxTexture(ImageID skyboxImageId);
|
||||
void setSkyboxTexture(ImageID imageId);
|
||||
|
||||
|
||||
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const
|
||||
@@ -101,6 +109,43 @@ public:
|
||||
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)
|
||||
{
|
||||
shadowMapPipeline->setShadowDistance(distance);
|
||||
@@ -124,8 +169,9 @@ public:
|
||||
const glm::mat4& projection);
|
||||
|
||||
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 recordSelectionPick(VkCommandBuffer cmd, std::uint32_t frameIndex);
|
||||
[[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum);
|
||||
|
||||
RenderResources* resources = nullptr;
|
||||
@@ -137,14 +183,31 @@ private:
|
||||
|
||||
std::vector<MeshDrawCommand> meshDrawCommands;
|
||||
std::vector<std::size_t> sortedMeshDrawCommands;
|
||||
std::vector<std::size_t> transparentMeshDrawCommands;
|
||||
IndirectDrawList drawList;
|
||||
DrawStats lastDrawStats;
|
||||
|
||||
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
||||
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
|
||||
static constexpr std::uint32_t HighlightedObjectFlag = 0x80000000u;
|
||||
|
||||
VkFormat sceneColorFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
|
||||
VkFormat drawImageFormat{VK_FORMAT_R8G8B8A8_SRGB};
|
||||
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
|
||||
VkFormat selectionImageFormat{VK_FORMAT_R32_UINT};
|
||||
|
||||
ImageID sceneColorImageId{NULL_IMAGE_ID};
|
||||
ImageID drawImageId{NULL_IMAGE_ID};
|
||||
ImageID skyboxImageId{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};
|
||||
|
||||
@@ -173,7 +236,7 @@ private:
|
||||
|
||||
std::uint32_t shadowMapId;
|
||||
float shadowNormalOffset;
|
||||
std::uint32_t padding;
|
||||
std::uint32_t debugSelectionView;
|
||||
glm::mat4 lightViewProj;
|
||||
|
||||
std::uint32_t spotShadowAtlasId;
|
||||
@@ -191,6 +254,7 @@ private:
|
||||
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
||||
std::unique_ptr<LightCullingPipeline> lightCullingPipeline;
|
||||
std::unique_ptr<ShadowMapPipeline> shadowMapPipeline;
|
||||
std::unique_ptr<PostProcessPipeline> postProcessPipeline;
|
||||
|
||||
std::optional<glm::vec3> shadowLightDirection;
|
||||
std::vector<Light> submittedLights;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef PRIMITIVEMESHES_H
|
||||
#define PRIMITIVEMESHES_H
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -10,9 +11,36 @@
|
||||
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||
|
||||
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]] std::string MakeKey(const Desc& desc);
|
||||
[[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]] std::string_view StripDuplicateSuffix(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/ObjectId.h>
|
||||
|
||||
class FieldVisitor;
|
||||
class GameObject;
|
||||
class Transform;
|
||||
|
||||
@@ -51,15 +52,16 @@ public:
|
||||
// Serialization.
|
||||
virtual std::string GetTypeName() const = 0;
|
||||
|
||||
virtual nlohmann::json Serialize() const {
|
||||
return nlohmann::json::object();
|
||||
// The defaults save, load, resolve and draw the fields listed by Reflect
|
||||
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.
|
||||
// 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 <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
@@ -17,7 +19,10 @@ enum class PhysicsShapeType {
|
||||
None,
|
||||
Box,
|
||||
Sphere,
|
||||
Capsule
|
||||
Capsule,
|
||||
Cylinder,
|
||||
ConvexHull,
|
||||
Mesh
|
||||
};
|
||||
|
||||
struct PhysicsBodyHandle {
|
||||
@@ -50,6 +55,12 @@ struct PhysicsMaterialDesc {
|
||||
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 {
|
||||
PhysicsShapeType type{PhysicsShapeType::None};
|
||||
|
||||
@@ -59,9 +70,14 @@ struct PhysicsShapeDesc {
|
||||
// Sphere / capsule
|
||||
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};
|
||||
|
||||
// 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.
|
||||
glm::vec3 centerOffset{0.0f};
|
||||
|
||||
@@ -101,6 +117,45 @@ struct PhysicsShapeDesc {
|
||||
desc.isTrigger = isTrigger;
|
||||
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 {
|
||||
|
||||
@@ -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 <destrum/Event.h>
|
||||
#include <destrum/Graphics/Tonemapper.h>
|
||||
#include <destrum/ObjectModel/ObjectId.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Util/SceneInspector.h>
|
||||
|
||||
#include "destrum/Physics/JoltPhysicsWorld.h"
|
||||
#include "destrum/Physics/PhysicsSceneBridge.h"
|
||||
@@ -26,6 +26,9 @@ struct SceneEnvironment {
|
||||
|
||||
float skyboxRotationSpeed{0.01f};
|
||||
float skyboxRotation{0.0f};
|
||||
|
||||
float exposure{0.0f};
|
||||
Tonemapper tonemapper{Tonemapper::PbrNeutral};
|
||||
};
|
||||
class SceneSerializer;
|
||||
class PrefabSerializer;
|
||||
@@ -49,7 +52,6 @@ public:
|
||||
void FixedUpdate(float dt);
|
||||
void LateUpdate(float dt);
|
||||
void Render(const RenderContext& ctx);
|
||||
void RenderImgui(bool* open = nullptr);
|
||||
|
||||
void CleanupDestroyedGameObjects();
|
||||
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.
|
||||
bool MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore = nullptr);
|
||||
void SetSelectedObject(ObjectId id) { m_SelectedObjectId = id; }
|
||||
[[nodiscard]] ObjectId GetSelectedObjectId() const { return m_SelectedObjectId; }
|
||||
|
||||
void DrawSettingsImGui();
|
||||
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
|
||||
|
||||
void SetRegisterBindings(std::function<void()> registerBindings) {
|
||||
@@ -159,13 +164,11 @@ private:
|
||||
//Imgui vars
|
||||
bool m_ShowDemoWindow{false};
|
||||
ObjectId m_SelectedObjectId{InvalidObjectId};
|
||||
SceneInspector m_Inspector;
|
||||
|
||||
ObjectId m_CurrentCameraId{InvalidObjectId};
|
||||
|
||||
SceneEnvironment m_Environment{};
|
||||
|
||||
void DrawSettingsImGui();
|
||||
|
||||
std::function<void()> m_registerBindings;
|
||||
std::function<void()> m_unregisterBindings;
|
||||
|
||||
@@ -18,12 +18,16 @@ public:
|
||||
Scene& GetCurrentScene() const;
|
||||
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 FixedUpdate(float dt);
|
||||
void LateUpdate(float dt);
|
||||
|
||||
void Render(const RenderContext& ctx);
|
||||
void RenderImgui(bool* open = nullptr);
|
||||
|
||||
void HandleGameObjectDestroy();
|
||||
void DestroyGameObjects();
|
||||
@@ -52,6 +56,11 @@ private:
|
||||
|
||||
std::vector<std::shared_ptr<Scene>> m_activeScenes;
|
||||
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
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define DESTRUM_ENGINECOMPONENTLIST_H
|
||||
|
||||
#include <destrum/Components/Animator.h>
|
||||
#include <destrum/Components/AudioListener.h>
|
||||
#include <destrum/Components/AudioSource.h>
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/LightComponent.h>
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
@@ -10,6 +12,9 @@
|
||||
#include <destrum/Components/Spinner.h>
|
||||
#include <destrum/Components/Physics/BoxCollider.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/SphereCollider.h>
|
||||
#include <destrum/Components/UI/Canvas.h>
|
||||
@@ -25,10 +30,12 @@
|
||||
X(Spinner) \
|
||||
X(OrbitAndSpin) \
|
||||
X(Animator) \
|
||||
X(AudioSource) \
|
||||
X(AudioListener) \
|
||||
X(Rigidbody) \
|
||||
X(BoxCollider) \
|
||||
X(SphereCollider) \
|
||||
X(CapsuleCollider) \
|
||||
X(CapsuleCollider) X(CylinderCollider) X(ConvexHullCollider) X(MeshCollider) \
|
||||
X(Canvas) \
|
||||
X(RectTransform) \
|
||||
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 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);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class Transform;
|
||||
|
||||
class PrefabSerializer final {
|
||||
public:
|
||||
static constexpr int CurrentPrefabVersion = 1;
|
||||
static constexpr int CurrentPrefabVersion = 2;
|
||||
|
||||
[[nodiscard]] static std::optional<nlohmann::json> Capture(const GameObject& root);
|
||||
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 <functional>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
@@ -17,8 +18,11 @@ public:
|
||||
nlohmann::json root;
|
||||
};
|
||||
|
||||
[[nodiscard]] static std::optional<nlohmann::json> Capture(Scene& scene);
|
||||
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,
|
||||
std::function<void(float)> progressCallback = nullptr);
|
||||
|
||||
@@ -27,8 +31,6 @@ public:
|
||||
static bool ConstructScene(Scene& scene, LoadResult& result,
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 InputStringMultiline(const char* label, std::string& value);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
//
|
||||
// Link against: assimp
|
||||
|
||||
#include <assimp/IOSystem.hpp>
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/material.h>
|
||||
#include <assimp/postprocess.h>
|
||||
@@ -43,6 +44,7 @@
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -998,8 +1000,12 @@ static void CollectMergedPerNodeMeshes(const aiScene* scene,
|
||||
|
||||
// ─── 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;
|
||||
if (io)
|
||||
importer.SetIOHandler(io.release());
|
||||
const aiScene* scene = LoadScene(importer, path, options);
|
||||
|
||||
ModelAsset model{};
|
||||
@@ -1084,10 +1090,11 @@ static const aiScene* LoadAnimationScene(
|
||||
Assimp::Importer& importer,
|
||||
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);
|
||||
}
|
||||
|
||||
if (importer.IsDefaultIOHandler()) {
|
||||
if (!std::filesystem::is_regular_file(path)) {
|
||||
throw std::runtime_error("Animation path is not a regular file: " + path);
|
||||
}
|
||||
@@ -1095,6 +1102,7 @@ static const aiScene* LoadAnimationScene(
|
||||
if (std::filesystem::file_size(path) == 0) {
|
||||
throw std::runtime_error("Animation file is empty: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
// Important: do not use mesh post-process flags for animation-only files.
|
||||
const unsigned int flags = 0;
|
||||
@@ -1141,9 +1149,12 @@ static const aiScene* LoadAnimationScene(
|
||||
|
||||
static std::vector<SkeletalAnimation> LoadAnimationClips(
|
||||
const std::string& path,
|
||||
const Skeleton& targetSkeleton
|
||||
const Skeleton& targetSkeleton,
|
||||
std::unique_ptr<Assimp::IOSystem> io = nullptr
|
||||
) {
|
||||
Assimp::Importer importer;
|
||||
if (io)
|
||||
importer.SetIOHandler(io.release());
|
||||
const aiScene* scene = LoadAnimationScene(importer, path);
|
||||
|
||||
auto clips = LoadAnimations(scene, targetSkeleton);
|
||||
|
||||
+303
-318
@@ -1,56 +1,67 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <thread>
|
||||
#include <stdexcept>
|
||||
#include <destrum/App.h>
|
||||
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/FontCache.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "glm/gtx/transform.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <thread>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <common/TracySystem.hpp>
|
||||
|
||||
struct TracyFrameScope
|
||||
{
|
||||
~TracyFrameScope()
|
||||
{
|
||||
FrameMark;
|
||||
}
|
||||
};
|
||||
#include <destrum/Audio/AudioSystem.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/FontCache.h>
|
||||
#include <destrum/Scene/ResourceCollector.h>
|
||||
#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()
|
||||
{
|
||||
if (!cleanedUp) {
|
||||
try {
|
||||
cleanup();
|
||||
} 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)
|
||||
{
|
||||
ZoneScopedN("App::init");
|
||||
cleanedUp = false;
|
||||
customInitStarted = false;
|
||||
try {
|
||||
m_params = params;
|
||||
ZoneScopedN("App::init");
|
||||
tracy::SetThreadName("Main Thread");
|
||||
TracySetProgramName(params.appName.c_str());
|
||||
AssetFS::GetInstance().Init(params.exeDir);
|
||||
JobSystem::GetInstance().Init();
|
||||
|
||||
window.Create({
|
||||
.title = params.windowTitle,
|
||||
@@ -59,15 +70,15 @@ void App::init(const AppParams& params)
|
||||
.maximized = params.startMaximized,
|
||||
});
|
||||
|
||||
gfxDevice.init(window.GetHandle(), params.appName, false);
|
||||
initDevices();
|
||||
imguiPass.init(window.GetHandle(), gfxDevice);
|
||||
resources.init(gfxDevice);
|
||||
renderer.init(gfxDevice, resources, params.renderSize);
|
||||
|
||||
InputManager::GetInstance().Init();
|
||||
Time::GetInstance().Update();
|
||||
|
||||
customInitStarted = true;
|
||||
customInit();
|
||||
cleanedUp = false;
|
||||
} catch (...) {
|
||||
try {
|
||||
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()
|
||||
{
|
||||
ZoneScopedN("App::run");
|
||||
|
||||
Time::GetInstance().Update();
|
||||
|
||||
const float fixedDt = static_cast<float>(Time::GetInstance().FixedDeltaTime());
|
||||
const int maxSteps = 5;
|
||||
float accumulator = 0.0f;
|
||||
|
||||
simulation.fixedDeltaTime = static_cast<float>(Time::GetInstance().FixedDeltaTime());
|
||||
isRunning = true;
|
||||
|
||||
while (isRunning)
|
||||
{
|
||||
while (isRunning) {
|
||||
TracyFrameScope tracyFrame;
|
||||
ZoneScopedN("App::Frame");
|
||||
|
||||
Time::GetInstance().Update();
|
||||
float dt = static_cast<float>(Time::GetInstance().DeltaTime());
|
||||
|
||||
if (dt > 0.25f) dt = 0.25f;
|
||||
|
||||
if (dt > 0.0f)
|
||||
{
|
||||
float newFPS = 1.0f / dt;
|
||||
avgFPS = std::lerp(avgFPS, newFPS, 0.1f);
|
||||
const float dt = std::min(static_cast<float>(Time::GetInstance().DeltaTime()), MaxFrameDelta);
|
||||
if (dt > 0.0f) {
|
||||
avgFPS = std::lerp(avgFPS, 1.0f / dt, 0.1f);
|
||||
TracyPlot("FPS", avgFPS);
|
||||
TracyPlot("Delta Time ms", dt * 1000.0f);
|
||||
}
|
||||
simulation.accumulator += dt;
|
||||
|
||||
accumulator += dt;
|
||||
|
||||
{
|
||||
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;
|
||||
processEvents();
|
||||
if (!isRunning) {
|
||||
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())
|
||||
{
|
||||
if (window.IsMinimized()) {
|
||||
// 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));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Consume SDL events before updating the base camera so held and
|
||||
// newly pressed inputs are applied in the same frame.
|
||||
camera.Update(dt);
|
||||
JobSystem::GetInstance().DrainMainThreadQueue();
|
||||
if (layer) {
|
||||
layer->onBeginFrame();
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("ImGui BeginFrame");
|
||||
imguiPass.beginFrame();
|
||||
if (layer) {
|
||||
layer->onImGuiBegin(dt);
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("customUpdate");
|
||||
ZoneScopedN("Update");
|
||||
customUpdate(dt);
|
||||
SceneManager::GetInstance().Update(dt);
|
||||
SceneManager::GetInstance().LateUpdate(dt);
|
||||
}
|
||||
|
||||
if (m_params.showDebugUi)
|
||||
{
|
||||
ZoneScopedN("Debug ImGui");
|
||||
drawWindowsMenu();
|
||||
|
||||
if (m_DebugWindows.inspector)
|
||||
{
|
||||
SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector);
|
||||
if (layer) {
|
||||
ZoneScopedN("Layer ImGui");
|
||||
layer->onImGui(dt);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("ImGui EndFrame");
|
||||
imguiPass.endFrame();
|
||||
|
||||
if (layer) {
|
||||
layer->onEndFrame();
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("FixedUpdate");
|
||||
runFixedSteps();
|
||||
|
||||
int steps = 0;
|
||||
AudioSystem::GetInstance().Update(dt, isPlaying() && (isPaused() || simulation.paused), &getViewCamera());
|
||||
|
||||
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))
|
||||
{
|
||||
bool skipFrame = false;
|
||||
handleSwapchainResize(skipFrame);
|
||||
if (skipFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const glm::ivec2 drawableSize = window.GetDrawableSize();
|
||||
const int w = drawableSize.x;
|
||||
const int h = drawableSize.y;
|
||||
renderFrame();
|
||||
|
||||
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)
|
||||
{
|
||||
if (frameLimit) {
|
||||
if (const auto sleepTime = Time::GetInstance().SleepDuration(); sleepTime.count() > 0) {
|
||||
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()
|
||||
{
|
||||
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
|
||||
// pipelines and other objects directly, which is only valid once nothing is in flight.
|
||||
attempt([this] { gfxDevice.waitIdle(); });
|
||||
attempt([] { JobSystem::GetInstance().Shutdown(); });
|
||||
|
||||
attempt([this] {
|
||||
if (layer) {
|
||||
layer->onDetach();
|
||||
layer.reset();
|
||||
}
|
||||
});
|
||||
if (customInitStarted) {
|
||||
attempt([this] { customCleanup(); });
|
||||
}
|
||||
attempt([] { SceneManager::GetInstance().Destroy(); });
|
||||
attempt([] { AudioSystem::GetInstance().Shutdown(); });
|
||||
attempt([] { FontCache::GetInstance().Clear(); });
|
||||
attempt([this] { renderer.cleanup(gfxDevice); });
|
||||
attempt([this] { imguiPass.cleanup(); });
|
||||
@@ -385,76 +443,3 @@ void App::cleanup()
|
||||
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 <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <destrum/Assets/ModelFile.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
|
||||
#include "AssetFSIOSystem.h"
|
||||
|
||||
namespace {
|
||||
std::string ToReference(std::string_view reference) {
|
||||
if (reference.empty()) {
|
||||
@@ -30,6 +33,14 @@ namespace {
|
||||
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
|
||||
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
|
||||
for (ModelDoc::MaterialInfo& material : model.materials) {
|
||||
@@ -95,7 +106,7 @@ ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
|
||||
const ModelDoc::LoadOptions& options) const
|
||||
{
|
||||
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)) {
|
||||
ModelFile::ApplyLoadOptions(model, options);
|
||||
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);
|
||||
}
|
||||
|
||||
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options);
|
||||
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options, MakeIOSystem());
|
||||
}
|
||||
|
||||
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
||||
@@ -115,7 +126,8 @@ std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
||||
const std::string sourceReference = ToReference(reference);
|
||||
auto clips = ModelDoc::LoadAnimationClips(
|
||||
Resolve(sourceReference).generic_string(),
|
||||
targetSkeleton);
|
||||
targetSkeleton,
|
||||
MakeIOSystem());
|
||||
for (std::size_t index = 0; index < clips.size(); ++index) {
|
||||
auto& clip = clips[index];
|
||||
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) {
|
||||
case PixelFormat::RGBA16Float:
|
||||
return 4 * sizeof(std::uint16_t);
|
||||
case PixelFormat::RGBA8:
|
||||
return 4;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||
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/Transform.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
@@ -54,19 +55,10 @@ bool CameraComponent::IsCurrent() const
|
||||
return scene != nullptr && scene->GetCurrentCamera() == this;
|
||||
}
|
||||
|
||||
nlohmann::json CameraComponent::Serialize() const {
|
||||
return {
|
||||
{"fieldOfView", m_FieldOfView},
|
||||
{"zNear", m_ZNear},
|
||||
{"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::Reflect(FieldVisitor& v) {
|
||||
v.Property("fieldOfView", m_FieldOfView, [this](float fov) { SetFieldOfView(fov); }, {.speed = 0.5f, .min = 1.0, .max = 179.0});
|
||||
v.Property("zNear", m_ZNear, [this](float zNear) { SetClipPlanes(zNear, m_ZFar); }, {.speed = 0.01f, .min = 0.0001, .label = "Near"});
|
||||
v.Property("zFar", m_ZFar, [this](float zFar) { SetClipPlanes(m_ZNear, zFar); }, {.speed = 1.0f, .min = 0.1, .label = "Far"});
|
||||
}
|
||||
|
||||
void CameraComponent::ImGuiInspector() {
|
||||
@@ -74,17 +66,5 @@ void CameraComponent::ImGuiInspector() {
|
||||
if (ImGui::Checkbox("Current Camera", ¤t) && current) {
|
||||
MakeCurrent();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Component::ImGuiInspector();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <destrum/Graphics/RenderContext.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
@@ -11,8 +11,6 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
LightComponent::LightComponent(GameObject& parent, LightType type)
|
||||
: Component(parent, "LightComponent")
|
||||
, m_Type(type)
|
||||
@@ -52,86 +50,19 @@ void LightComponent::Render(const RenderContext& ctx)
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json LightComponent::Serialize() const {
|
||||
return {
|
||||
{"type", static_cast<std::uint32_t>(m_Type)},
|
||||
{"color", m_Color},
|
||||
{"intensity", m_Intensity},
|
||||
{"range", m_Range},
|
||||
{"innerConeAngle", m_InnerConeAngle},
|
||||
{"outerConeAngle", m_OuterConeAngle},
|
||||
{"castShadows", m_CastShadows}
|
||||
};
|
||||
}
|
||||
|
||||
void LightComponent::Deserialize(const nlohmann::json& data) {
|
||||
if (data.contains("type")) {
|
||||
const auto type = data.at("type").get<std::uint32_t>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
void LightComponent::Reflect(FieldVisitor& v) {
|
||||
static constexpr std::array<const char*, 3> TypeNames{"Directional", "Point", "Spot"};
|
||||
|
||||
const bool directional = m_Type == LightType::Directional;
|
||||
const bool spot = m_Type == LightType::Spot;
|
||||
v.Field("type", m_Type, {.enumNames = TypeNames});
|
||||
v.Field("color", m_Color, {.color = true});
|
||||
v.Property("intensity", m_Intensity, [this](float intensity) { SetIntensity(intensity); }, {.speed = 0.05f, .min = 0.0});
|
||||
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); },
|
||||
{.min = 0.0, .max = glm::half_pi<double>(), .angle = true, .hidden = !spot, .label = "Inner Cone"});
|
||||
v.Property("outerConeAngle", m_OuterConeAngle, [this](float angle) { SetConeAngles(m_InnerConeAngle, angle); },
|
||||
{.min = 0.0, .max = glm::half_pi<double>(), .angle = true, .hidden = !spot, .label = "Outer Cone"});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/Assets/AssetDatabase.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/Resources/PrimitiveMeshes.h>
|
||||
#include <destrum/Graphics/Renderer.h>
|
||||
@@ -9,13 +12,64 @@
|
||||
#include "destrum/ObjectModel/GameObject.h"
|
||||
#include "destrum/Util/GameState.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
|
||||
#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") {
|
||||
|
||||
}
|
||||
@@ -77,15 +131,23 @@ nlohmann::json MeshRendererComponent::Serialize() const {
|
||||
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 {
|
||||
{"meshKey", savedMeshKey},
|
||||
{"materialKey", savedMaterialKey}
|
||||
{"mesh", MeshReference(savedMeshKey)},
|
||||
{"material", MaterialReference(savedMaterialKey)},
|
||||
};
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
||||
meshKey = data.value("meshKey", std::string{});
|
||||
materialKey = data.value("materialKey", std::string{});
|
||||
meshKey = data.contains("mesh") ? ReadKey(data.at("mesh")) : data.value("meshKey", 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
|
||||
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 asset = AssetReference::fromCacheKey(key);
|
||||
// Runtime-only keys such as "BulbMaterial#2" also contain a '#', they are not files
|
||||
if (!asset || (asset->path.find("://") == std::string::npos &&
|
||||
!std::filesystem::exists(asset->path))) {
|
||||
if (!asset || (!IsAssetPath(asset->path) && !std::filesystem::exists(asset->path))) {
|
||||
return;
|
||||
}
|
||||
if (!GameState::GetInstance().HasGfxDevice()) {
|
||||
@@ -144,6 +205,13 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
||||
}
|
||||
if (!materialKey.empty()) {
|
||||
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) {
|
||||
loadAssetForKey(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) {
|
||||
meshID = id;
|
||||
meshKey.clear();
|
||||
@@ -211,6 +255,9 @@ void MeshRendererComponent::SetMeshID(MeshID id) {
|
||||
meshKey = resources->meshes().getMeshKey(id);
|
||||
}
|
||||
}
|
||||
if (GetGameObject()->GetComponent<MeshShapeCollider>() != nullptr) {
|
||||
NotifyPhysicsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::SetMaterialID(MaterialID id) {
|
||||
@@ -241,9 +288,11 @@ void MeshRendererComponent::Render(const RenderContext& ctx) {
|
||||
GetTransform().GetWorldMatrix(),
|
||||
materialID,
|
||||
m_skinnedMesh.get(),
|
||||
jointMatricesStartIndex
|
||||
jointMatricesStartIndex,
|
||||
static_cast<std::uint32_t>(GetGameObject()->GetId())
|
||||
);
|
||||
} 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/Transform.h"
|
||||
#include "destrum/Components/MeshRendererComponent.h"
|
||||
#include "destrum/Serialization/GlmJson.h"
|
||||
#include "destrum/Serialization/Reflect.h"
|
||||
#include "destrum/Util/GameState.h"
|
||||
#include "destrum/Util/DeltaTime.h"
|
||||
#include "destrum/Util/ImGuiUtils.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -126,40 +125,33 @@ void OrbitAndSpin::Start() {
|
||||
|
||||
}
|
||||
|
||||
nlohmann::json OrbitAndSpin::Serialize() const {
|
||||
return {
|
||||
{"radius", m_Radius},
|
||||
{"center", m_Center},
|
||||
{"orbitAxis", m_OrbitAxis},
|
||||
{"orbitSpeed", m_OrbitSpeed},
|
||||
{"orbitAngle", m_OrbitAngle},
|
||||
{"orbitPhase", m_OrbitPhase},
|
||||
{"growPhase", m_GrowPhase},
|
||||
{"growSpeed", m_GrowSpeed},
|
||||
{"growMin", m_GrowMin},
|
||||
{"growMax", m_GrowMax},
|
||||
{"spinAxis", m_SpinAxis},
|
||||
{"spinSpeed", m_SpinSpeed},
|
||||
{"baseScale", m_BaseScale}
|
||||
};
|
||||
void OrbitAndSpin::Reflect(FieldVisitor& v) {
|
||||
v.Field("radius", m_Radius, {.speed = 0.05f, .min = 0.0});
|
||||
v.Field("center", m_Center);
|
||||
v.Field("orbitAxis", m_OrbitAxis);
|
||||
v.Field("orbitSpeed", m_OrbitSpeed, {.speed = 0.05f});
|
||||
v.Field("orbitPhase", m_OrbitPhase, {.speed = 0.05f});
|
||||
v.Field("orbitAngle", m_OrbitAngle, {.format = "%.2f rad", .readOnly = true});
|
||||
v.Field("growSpeed", m_GrowSpeed, {.speed = 0.05f, .min = 0.0});
|
||||
v.Field("growMin", m_GrowMin, {.speed = 0.01f, .min = 0.0});
|
||||
v.Field("growMax", m_GrowMax, {.speed = 0.01f, .min = 0.0});
|
||||
v.Field("baseScale", m_BaseScale, {.speed = 0.05f});
|
||||
v.Field("spinAxis", m_SpinAxis);
|
||||
v.Field("spinSpeed", m_SpinSpeed, {.speed = 0.05f});
|
||||
v.Field("growPhase", m_GrowPhase, {.format = "%.2f rad", .readOnly = true});
|
||||
}
|
||||
|
||||
void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
||||
m_Radius = data.value("radius", m_Radius);
|
||||
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);
|
||||
Component::Deserialize(data);
|
||||
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) {
|
||||
std::swap(m_GrowMin, m_GrowMax);
|
||||
}
|
||||
@@ -172,62 +164,6 @@ void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
||||
}
|
||||
|
||||
void OrbitAndSpin::ImGuiInspector() {
|
||||
float radius = m_Radius;
|
||||
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);
|
||||
Component::ImGuiInspector();
|
||||
ImGui::Text("Material ID: %u", m_MaterialID);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
#include <destrum/Components/Physics/BoxCollider.h>
|
||||
|
||||
#include <destrum/Graphics/DebugDraw.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
void BoxCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||
DebugDraw::Box(GetWorldCenter(), GetWorldRotation(), m_HalfExtents, color);
|
||||
}
|
||||
|
||||
nlohmann::json BoxCollider::Serialize() const {
|
||||
auto data = SerializeCollider();
|
||||
data["halfExtents"] = m_HalfExtents;
|
||||
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);
|
||||
}
|
||||
void BoxCollider::Reflect(FieldVisitor& v) {
|
||||
Collider::Reflect(v);
|
||||
v.Property("halfExtents", m_HalfExtents, [this](const glm::vec3& halfExtents) { SetHalfExtents(halfExtents); }, {.min = 0.0001});
|
||||
}
|
||||
|
||||
@@ -2,22 +2,14 @@
|
||||
|
||||
#include <destrum/Graphics/DebugDraw.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
void CapsuleCollider::DrawDebugLines(const glm::vec4& color) const {
|
||||
DebugDraw::Capsule(GetWorldCenter(), GetWorldRotation(), m_Radius, m_Height, color);
|
||||
}
|
||||
|
||||
void CapsuleCollider::ImGuiInspector() {
|
||||
Collider::ImGuiInspector();
|
||||
|
||||
float radius = m_Radius;
|
||||
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);
|
||||
}
|
||||
void CapsuleCollider::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});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
glm::vec3 Collider::GetWorldCenter() const {
|
||||
Transform& transform = GetTransform();
|
||||
@@ -14,14 +12,7 @@ glm::quat Collider::GetWorldRotation() const {
|
||||
return GetTransform().GetWorldRotation();
|
||||
}
|
||||
|
||||
void Collider::ImGuiInspector() {
|
||||
bool isTrigger = m_IsTrigger;
|
||||
if (ImGui::Checkbox("Is trigger", &isTrigger)) {
|
||||
SetTrigger(isTrigger);
|
||||
}
|
||||
|
||||
glm::vec3 centerOffset = m_CenterOffset;
|
||||
if (ImGuiUtils::DrawVec3Control("Center offset", centerOffset)) {
|
||||
SetCenterOffset(centerOffset);
|
||||
}
|
||||
void Collider::Reflect(FieldVisitor& v) {
|
||||
v.Property("isTrigger", m_IsTrigger, [this](bool trigger) { SetTrigger(trigger); });
|
||||
v.Property("centerOffset", m_CenterOffset, [this](const glm::vec3& offset) { SetCenterOffset(offset); });
|
||||
}
|
||||
|
||||
@@ -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