Compare commits

..
19 Commits
Author SHA1 Message Date
brammie15 d26f61a16f feat: add canvas rework 2026-11-01 23:16:58 +01:00
brammie15 6a85d17899 feat: add new fancy reflection 2026-11-01 23:12:00 +01:00
brammie15 08acd84404 feat: add reflection 2026-10-30 17:09:53 +01:00
brammie15 0fbc023018 feat: add export / file packing 2026-10-30 15:42:08 +01:00
brammie15 de7e489cc0 feat: update asset browser 2026-10-30 10:16:17 +01:00
brammie15 9cf9242c2a feat: add more image types to load from cooked files 2026-10-24 13:23:30 +02:00
brammie15 118016bcc7 feat: add more collider types + camera snapping fix 2026-10-24 02:34:09 +02:00
brammie15 355dfa7851 feat: make image loading async 2026-10-24 02:03:10 +02:00
brammie15 aa3eef259c feat: start loading some stuff async 2026-10-24 01:29:00 +02:00
brammie15 9c5a97cfbd feat: add JobSystem 2026-10-24 00:59:22 +02:00
brammie15 b798ed0378 feat: add simple transparency 2026-10-24 00:36:58 +02:00
brammie15 0b7cc38543 feat: add audio system 2026-10-23 22:07:52 +02:00
brammie15 a3d3da4a67 feat: move editor to project / add post processing etc 2026-10-23 20:54:12 +02:00
brammie15 f7e7c56392 feat: add click to select 2026-10-23 20:53:56 +02:00
brammie15 f055bdcbf1 feat: add selection to meshrendercomponent 2026-10-22 22:24:59 +02:00
brammie15 f99147b1a2 feat: move rendering to an imgui window 2026-10-22 22:02:57 +02:00
brammie15 dde2c7daf9 feat: add pauzing of the running game 2026-10-22 18:10:23 +02:00
brammie15 e5d37965a1 feat: add play / editor mode 2026-10-22 01:25:53 +02:00
brammie15 3df0f89343 fix: material serialization 2026-10-22 00:30:23 +02:00
246 changed files with 27216 additions and 3577 deletions
+3
View File
@@ -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
View File
@@ -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
+22 -3
View File
@@ -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
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
{
"allowFallback": [
"run.fbx"
]
}
@@ -0,0 +1,3 @@
{
"guid": "a09100c14a67b3840518b68b93fbadda"
}
@@ -0,0 +1,3 @@
{
"guid": "0407d177c8487bca092ffb7fd4d47aa2"
}
+3
View File
@@ -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 {
+18
View File
@@ -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)) {
+2
View File
@@ -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;
}
+115
View File
@@ -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);
}
+1 -1
View File
@@ -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
+16
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
{
"guid": "c2f1f9dcb185460c6d685c17d7ec4f82"
}
+59 -45
View File
@@ -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
+35
View File
@@ -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
+46
View File
@@ -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
+111
View File
@@ -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";
+1 -3
View File
@@ -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; }
+1 -3
View File
@@ -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); }
+23 -2
View File
@@ -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;
+2 -1
View File
@@ -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
+10 -1
View File
@@ -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
+72 -8
View File
@@ -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);
}
+57 -2
View File
@@ -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
+7 -4
View File
@@ -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;
+10 -1
View File
@@ -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, &copy, 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);
+14 -3
View File
@@ -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
View File
@@ -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();
}
+172
View File
@@ -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;
}
+45
View File
@@ -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
+15 -3
View File
@@ -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;
+126
View File
@@ -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;
}
+15
View File
@@ -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");
+189
View File
@@ -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;
}
+331
View File
@@ -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();
}
+33
View File
@@ -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.");
}
+288
View File
@@ -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();
}
+6 -26
View File
@@ -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", &current) && 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();
}
+16 -85
View File
@@ -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();
}
+24 -88
View File
@@ -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);
}
+4 -23
View File
@@ -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});
}
+4 -13
View File
@@ -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