feat: move editor to project / add post processing etc

This commit is contained in:
2026-10-23 20:54:12 +02:00
parent f7e7c56392
commit a3d3da4a67
104 changed files with 11750 additions and 2675 deletions
+3 -1
View File
@@ -27,6 +27,7 @@ if (MSVC)
endif()
add_subdirectory(destrum)
add_subdirectory(editor)
add_subdirectory(lightkeeper)
add_subdirectory(TheChef)
@@ -37,6 +38,7 @@ endif()
destrum_enable_warnings(destrum_assets)
destrum_enable_warnings(destrum)
destrum_enable_warnings(destrum_editor)
destrum_enable_warnings(lightkeeper)
add_custom_target(CleanupAssets
@@ -56,7 +58,7 @@ add_dependencies(CookAssets
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
+9 -2
View File
@@ -12,6 +12,9 @@ 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)
* CMake-based build setup with third-party dependencies as submodules
## Repository layout
@@ -24,6 +27,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/
@@ -119,7 +124,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 +139,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:
+3 -5
View File
@@ -75,7 +75,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"
@@ -99,6 +98,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"
@@ -111,11 +111,13 @@ 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"
@@ -126,10 +128,6 @@ set(SRC_FILES
"src/Util/DeltaTime.cpp"
"src/Util/ImGuiUtils.cpp"
"src/Util/TransformGizmo.cpp"
"src/Util/RectGizmo.cpp"
"src/Util/SceneInspector.cpp"
"src/Util/AssetBrowser.cpp"
"src/Physics/PhysicsWorld.cpp"
"src/Physics/PhysicsSceneBridge.cpp"
@@ -0,0 +1,3 @@
{
"guid": "a09100c14a67b3840518b68b93fbadda"
}
@@ -0,0 +1,3 @@
{
"guid": "0407d177c8487bca092ffb7fd4d47aa2"
}
+3
View File
@@ -0,0 +1,3 @@
{
"guid": "78d91a725664c8b815531d55bc62683f"
}
+1 -1
View File
@@ -142,7 +142,7 @@ void main()
}
if (pcs.sceneData.debugSelectionView != 0) {
fragColor = selectionColor(inObjectID);
fragColor = selectionColor(inObjectID & 0x7FFFFFFFu);
}
#if 0
+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);
}
+3
View File
@@ -0,0 +1,3 @@
{
"guid": "c2f1f9dcb185460c6d685c17d7ec4f82"
}
+56 -97
View File
@@ -1,29 +1,22 @@
#ifndef APP_H
#define APP_H
#include <filesystem>
#include <chrono>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
#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/Util/AssetBrowser.h>
#include <destrum/Input/InputManager.h>
class Scene;
class App {
public:
struct AppParams {
@@ -32,47 +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};
bool startInEditMode{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) {};
virtual void onViewportResize(int, int) {}
virtual void drawViewportOverlay() {}
virtual void onViewportAssetDropped(std::string_view) {}
virtual void onPlayModeChanged(bool) {}
[[nodiscard]] bool isPlaying() const { return playing; }
[[nodiscard]] bool isPaused() const { return paused; }
void requestPlayModeToggle() { playModeToggleRequested = true; }
void setPaused(bool pause);
virtual void customWindowsMenu() {}
virtual void onWindowResize(int, int) {}
virtual void onPlayModeChanged(bool) {}
[[nodiscard]] virtual Camera* gameCamera() { return nullptr; }
struct ViewportRect {
glm::vec2 min{0.0f};
glm::vec2 size{0.0f};
};
[[nodiscard]] bool isPlaying() const;
[[nodiscard]] bool isPaused() const;
[[nodiscard]] bool usesEditorViewport() const { return m_params.showDebugUi; }
[[nodiscard]] const ViewportRect& getViewportRect() const { return viewportRect; }
void claimViewportMouse() { viewportMouseClaimed = true; }
[[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;
@@ -81,72 +84,28 @@ 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 isRunning{false};
bool cleanedUp{false};
bool cleaningUp{false};
bool customInitStarted{false};
bool frameLimit{false};
float frameTime{0.f};
float avgFPS{0.f};
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};
bool assets{true};
};
DebugWindows m_DebugWindows{};
ResourcesInspector m_ResourcesInspector;
AssetBrowser m_AssetBrowser;
void drawWindowsMenu();
void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
private:
void drawDockSpace();
void drawViewportWindow();
void applyViewportResize();
void requestSelectionPick();
void applySelectionPick();
[[nodiscard]] bool canPickInViewport() const { return usesEditorViewport() && (!playing || paused); }
void processEvents();
void runFixedSteps();
void handleSwapchainResize(bool& skipFrame);
void renderFrame();
ViewportRect viewportRect{};
glm::ivec2 viewportDesiredSize{0};
bool viewportHovered{false};
bool viewportFocused{false};
bool viewportMouseClaimed{false};
bool pickSelectsParent{false};
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{};
void drawPlayControls();
void applyPlayModeToggle();
bool enterPlayMode();
void exitPlayMode();
bool playing{true};
bool paused{false};
bool mouseCapturedBeforePause{false};
bool playModeToggleRequested{false};
std::optional<nlohmann::json> editSnapshot;
std::weak_ptr<Scene> editScene;
bool isRunning{false};
bool cleaningUp{false};
bool customInitStarted{false};
bool frameLimit{false};
float avgFPS{0.0f};
bool resizePending{false};
std::chrono::steady_clock::time_point lastResizeTime{};
};
#endif //APP_H
#endif // APP_H
+30
View File
@@ -0,0 +1,30 @@
#ifndef DESTRUM_APPLAYER_H
#define DESTRUM_APPLAYER_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 {}; }
};
#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,38 @@
#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 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
+6 -1
View File
@@ -17,7 +17,7 @@ struct FSMount {
std::string scheme; // "engine", "game"
std::filesystem::path root;
std::optional<AssetManifest> manifest;
std::optional<std::filesystem::path> sourceRoot;
};
class AssetFS final: public Singleton<AssetFS> {
@@ -36,6 +36,11 @@ public:
[[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);
@@ -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>
@@ -28,6 +29,8 @@ public:
void cleanup(GfxDevice& gfxDevice);
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 = {});
@@ -55,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
@@ -50,7 +50,6 @@ public:
struct EndFrameProps
{
VkClearColorValue clearColor{{0.f, 0.f, 0.f, 1.f}};
glm::ivec4 drawImageBlitRect{};
ImguiPass* imguiPass{nullptr};
bool copyDrawImageToSwapchain{true};
};
@@ -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
@@ -10,6 +10,7 @@
#include <filesystem>
#include <memory>
#include <string_view>
#include <unordered_set>
namespace ModelDoc { struct MaterialInfo; }
@@ -45,6 +46,18 @@ public:
bool mipMap = true,
TextureIntent intent = TextureIntent::ColorSrgb);
ImageID loadMaterialTexture(GfxDevice& gfxDevice, const std::filesystem::path& path, TextureIntent intent);
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,6 +99,9 @@ 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};
+30 -4
View File
@@ -11,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>
@@ -22,6 +24,8 @@
#include <array>
#include <memory>
#include <optional>
#include <span>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -49,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();
@@ -68,14 +74,14 @@ public:
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
@@ -123,6 +129,18 @@ public:
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);
@@ -151,7 +169,7 @@ 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);
@@ -168,11 +186,16 @@ private:
IndirectDrawList drawList;
DrawStats lastDrawStats;
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
static constexpr std::uint32_t HighlightedObjectFlag = 0x80000000u;
VkFormat sceneColorFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
VkFormat drawImageFormat{VK_FORMAT_R8G8B8A8_SRGB};
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
VkFormat 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};
@@ -180,6 +203,8 @@ private:
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};
@@ -228,6 +253,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;
@@ -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
@@ -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;
@@ -28,7 +28,6 @@ public:
void LateUpdate(float dt);
void Render(const RenderContext& ctx);
void RenderImgui(bool* open = nullptr);
void HandleGameObjectDestroy();
void DestroyGameObjects();
@@ -4,22 +4,32 @@
#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]] nlohmann::json CollectEmbedded(const nlohmann::json& objects);
void RegisterEmbedded(const nlohmann::json& materials, bool overwriteExisting = false);
[[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);
+275 -676
View File
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
#include <destrum/Assets/AssetDatabase.h>
#include <charconv>
#include <filesystem>
#include <format>
#include <fstream>
#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)
{
std::ifstream file(path);
if (!file) {
return std::nullopt;
}
try {
const auto meta = nlohmann::json::parse(file);
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();
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
std::error_code error;
for (const auto& entry : std::filesystem::recursive_directory_iterator(mount.root, error)) {
if (!entry.is_regular_file(error) || !AssetTypes::IsMeta(entry.path().string())) {
continue;
}
if (const auto guid = ReadMetaFile(entry.path())) {
std::filesystem::path asset = entry.path();
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();
std::error_code error;
if (!std::filesystem::is_regular_file(assetFs.GetFullPath(path), error)) {
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;
}
@@ -1,5 +1,7 @@
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/Graphics/Renderer.h>
@@ -15,6 +17,58 @@
#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") {
}
@@ -76,15 +130,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;
@@ -104,8 +166,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()) {
@@ -143,6 +204,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);
@@ -12,10 +12,11 @@
#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/Util/AssetBrowser.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Util/GameState.h>
#include <destrum/Util/ImGuiUtils.h>
@@ -47,13 +48,6 @@ namespace {
return filter.empty() || Lowercase(text).find(Lowercase(filter)) != std::string::npos;
}
[[nodiscard]] bool IsImage(std::string_view path)
{
const std::string extension = Lowercase(std::filesystem::path{path}.extension().string());
return extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" ||
extension == ".bmp";
}
[[nodiscard]] std::optional<std::string> AcceptAssetDrop()
{
std::optional<std::string> dropped;
@@ -143,11 +137,12 @@ void MeshRendererComponent::DrawMeshField(RenderResources& resources)
m_MeshSearch.clear();
ImGui::OpenPopup("MeshPicker");
}
if (const auto dropped = AcceptAssetDrop(); dropped && AssetBrowser::IsModel(*dropped)) {
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().getMeshKey(id).starts_with(*dropped + "#") && !resources.meshes().getMesh(id).hasSkeleton) {
if (resources.meshes().isValid(id) && resources.meshes().getMeshKey(id).starts_with(*dropped + "#") &&
!resources.meshes().getMesh(id).hasSkeleton) {
parts.push_back(id);
}
}
@@ -184,7 +179,7 @@ void MeshRendererComponent::DrawMeshField(RenderResources& resources)
const MeshCache& meshes = resources.meshes();
for (MeshID id = 0; id < meshes.getMeshes().size(); ++id) {
const std::string& key = meshes.getMeshKey(id);
if (meshes.getMesh(id).hasSkeleton || !Matches(key, m_MeshSearch)) {
if (!meshes.isValid(id) || meshes.getMesh(id).hasSkeleton || !Matches(key, m_MeshSearch)) {
continue;
}
ImGui::PushID(static_cast<int>(id));
@@ -283,10 +278,18 @@ void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRe
m_MaterialSearch.clear();
ImGui::OpenPopup("MaterialPicker");
}
if (const auto dropped = AcceptAssetDrop(); dropped && AssetBrowser::IsModel(*dropped)) {
LoadDroppedModel(resources, *dropped);
m_MaterialSearch = *dropped;
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")) {
@@ -305,7 +308,7 @@ void MeshRendererComponent::DrawMaterialField(RenderResources& resources, GameRe
PickerList("LoadedMaterials", [&] {
for (MaterialID id = 0; id < materials.getMaterialCount(); ++id) {
const std::string& key = materials.getMaterialKey(id);
if (id == materials.getPlaceholderMaterialId() || !Matches(key, m_MaterialSearch)) {
if (!materials.isValid(id) || id == materials.getPlaceholderMaterialId() || !Matches(key, m_MaterialSearch)) {
continue;
}
ImGui::PushID(static_cast<int>(id));
@@ -331,8 +334,10 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
}
MaterialCache& materials = resources.materials();
const std::string sourcePath = materials.getMaterial(materialID).assetReference.path;
const bool placeholder = materialID == materials.getPlaceholderMaterialId();
const bool fileBacked = !materials.getMaterial(materialID).assetReference.empty();
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
@@ -345,44 +350,68 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
SetMaterialID(materials.addMaterial(std::move(copy)));
};
bool copied = false;
bool replaced = false;
if (placeholder) {
ImGui::TextDisabled("The default material, pick New Material to edit one");
} else if (fileBacked) {
ImGui::TextDisabled("From %s, edits would not be saved", materials.getMaterial(materialID).assetReference.path.c_str());
if (ImGui::Button("Make Editable Copy")) {
} else if (fromModel) {
ImGui::TextDisabled("From %s, edits would not be saved", sourcePath.c_str());
replaced = ImGui::Button("Make Editable Copy");
if (replaced) {
copyForThisObject();
copied = true;
}
} else if (users > 1) {
ImGui::Text("Shared by %d objects, edits change all of them", users);
ImGui::SameLine();
} else if (materialFile) {
ImGui::TextDisabled("%s, used by %d objects here", sourcePath.c_str(), users);
if (ImGui::SmallButton("Make Unique")) {
copyForThisObject();
copied = true;
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 (copied) {
// addMaterial may have moved the material storage, draw the copy next frame
if (replaced) {
// addMaterial may have moved the material storage, draw the new one next frame
ImGui::TreePop();
return;
}
ImGui::BeginDisabled(placeholder || fileBacked);
ImGui::BeginDisabled(placeholder || fromModel);
Material& material = materials.getMaterialMutable(materialID);
bool changed = false;
changed |= ImGui::ColorEdit3("Base Color", &material.baseColor.x);
changed |= ImGui::SliderFloat("Metallic", &material.metallicFactor, 0.0f, 1.0f);
changed |= ImGui::SliderFloat("Roughness", &material.roughnessFactor, 0.0f, 1.0f);
changed |= ImGui::ColorEdit3("Emissive", &material.emissiveColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float);
changed |= ImGui::DragFloat("Normal Scale", &material.normalScale, 0.01f, 0.0f, 4.0f);
changed |= ImGui::SliderFloat("Occlusion", &material.occlusionStrength, 0.0f, 1.0f);
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 = true;
changed = finished = true;
}
ImGui::SeparatorText("Textures");
@@ -396,11 +425,10 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
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 && IsImage(*dropped)) {
if (const auto dropped = AcceptAssetDrop(); dropped && AssetTypes::IsImage(*dropped)) {
try {
texture = resources.loadImageFromFile(GameState::GetInstance().Gfx(), AssetFS::GetInstance().GetFullPath(*dropped),
VK_IMAGE_USAGE_SAMPLED_BIT, true, slot.intent);
changed = true;
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());
}
@@ -409,7 +437,7 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
ImGui::BeginDisabled(texture == NULL_IMAGE_ID);
if (ImGui::Button("x", ImVec2{clearWidth, 0.0f})) {
texture = NULL_IMAGE_ID;
changed = true;
changed = finished = true;
}
ImGui::EndDisabled();
ImGui::PopID();
@@ -419,5 +447,8 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
if (changed) {
renderer.updateMaterialGPU(materialID);
}
if (finished && materialFile) {
MaterialSerialization::SaveFile(resources, materialID, sourcePath);
}
ImGui::TreePop();
}
+47
View File
@@ -214,6 +214,53 @@ std::optional<std::string> AssetFS::ToVirtualPath(const std::filesystem::path& f
return std::nullopt;
}
void AssetFS::SetSourceRoot(std::string_view scheme, std::filesystem::path sourceRoot) {
for (FSMount& mount : mounts) {
if (mount.scheme == scheme) {
mount.sourceRoot = std::filesystem::absolute(std::move(sourceRoot)).lexically_normal();
return;
}
}
throw std::runtime_error("asset mount not found: " + std::string{scheme});
}
std::optional<std::filesystem::path> AssetFS::GetSourcePath(std::string_view vpath) const {
const ParsedVirtualPath parsed = ParseVirtualPath(vpath);
const FSMount* mount = FindMount(mounts, parsed.scheme);
if (mount == nullptr || !mount->sourceRoot) {
return std::nullopt;
}
return (*mount->sourceRoot / parsed.relativePath).lexically_normal();
}
bool AssetFS::WriteFile(std::string_view vpath, std::string_view contents) {
std::vector<std::filesystem::path> targets{GetFullPath(vpath)};
if (auto source = GetSourcePath(vpath)) {
targets.push_back(std::move(*source));
}
for (const auto& target : targets) {
std::error_code error;
std::filesystem::create_directories(target.parent_path(), error);
std::filesystem::path temporary = target;
temporary += ".tmp";
{
std::ofstream file(temporary, std::ios::binary | std::ios::trunc);
if (!file.write(contents.data(), static_cast<std::streamsize>(contents.size()))) {
spdlog::error("Failed to write {}", temporary.string());
return false;
}
}
std::filesystem::rename(temporary, target, error);
if (error) {
spdlog::error("Failed to replace {}: {}", target.string(), error.message());
std::filesystem::remove(temporary, error);
return false;
}
}
return true;
}
std::vector<uint8_t> AssetFS::ReadFile(const std::filesystem::path& fullPath) {
std::ifstream file(fullPath, std::ios::binary);
if (!file) {
+27 -6
View File
@@ -46,6 +46,7 @@ void MaterialCache::cleanup(GfxDevice& gfxDevice)
materialDataBuffer = {};
materials.clear();
materialKeys.clear();
freeIds = std::make_shared<std::vector<MaterialID>>();
placeholderMaterialId = NULL_MATERIAL_ID;
device = nullptr;
}
@@ -54,7 +55,7 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
{
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
const auto id = getFreeMaterialId();
const MaterialID id = getFreeMaterialId();
if (id >= MAX_MATERIALS) {
throw std::runtime_error("MaterialCache capacity exhausted");
}
@@ -70,7 +71,7 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
static_cast<VkDeviceSize>(id) * sizeof(MaterialData),
sizeof(MaterialData));
const auto materialId = static_cast<MaterialID>(materials.size());
const MaterialID materialId = id;
if (key.empty() && !material.assetReference.empty()) {
key = material.assetReference.cacheKey();
} else if (key.empty()) {
@@ -84,12 +85,29 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key)
key = baseKey + "#" + std::to_string(suffix++);
}
materials.push_back(std::move(material));
materialKeys.push_back(std::move(key));
if (materialId == materials.size()) {
materials.push_back(std::move(material));
materialKeys.push_back(std::move(key));
} else {
std::erase(*freeIds, materialId);
materials[materialId] = std::move(material);
materialKeys[materialId] = std::move(key);
}
return materialId;
}
void MaterialCache::removeMaterial(MaterialID id)
{
if (!isValid(id) || id == placeholderMaterialId) {
return;
}
materials[id] = {};
materialKeys[id].clear();
device->deferDestroy([ids = freeIds, id] { ids->push_back(id); });
}
MaterialID MaterialCache::addSimpleTextureMaterial(ImageID textureID)
{
Material material = Material::SimpleColor(
@@ -117,6 +135,9 @@ const std::string& MaterialCache::getMaterialKey(MaterialID id) const
std::optional<MaterialID> MaterialCache::findMaterialByKey(std::string_view key) const
{
if (key.empty()) {
return std::nullopt;
}
const auto it = std::find(materialKeys.begin(), materialKeys.end(), key);
if (it == materialKeys.end()) {
return std::nullopt;
@@ -131,7 +152,7 @@ std::optional<MaterialID> MaterialCache::findMaterialByName(
std::optional<MaterialID> fallback;
for (MaterialID id = 0; id < materials.size(); ++id) {
const auto& material = materials[id];
if (material.name != name) {
if (!isValid(id) || material.name != name) {
continue;
}
@@ -186,7 +207,7 @@ MaterialData MaterialCache::toMaterialData(const Material& material) const
MaterialID MaterialCache::getFreeMaterialId() const
{
return static_cast<MaterialID>(materials.size());
return freeIds->empty() ? static_cast<MaterialID>(materials.size()) : freeIds->back();
}
MaterialID MaterialCache::getPlaceholderMaterialId() const
+61 -9
View File
@@ -29,9 +29,17 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
const auto id = meshes.size();
meshes.push_back(std::move(gpuMesh));
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
MeshID id = meshes.size();
if (!recycleBin->ids.empty()) {
id = recycleBin->ids.back();
recycleBin->ids.pop_back();
meshes[id] = std::move(gpuMesh);
cpuMeshes[id] = cpuMesh;
} else {
meshes.push_back(std::move(gpuMesh));
cpuMeshes.push_back(cpuMesh);
meshKeys.emplace_back();
}
std::string key;
if (!cpuMesh.assetReference.empty()) {
key = cpuMesh.assetReference.cacheKey();
@@ -45,10 +53,52 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
while (std::find(meshKeys.begin(), meshKeys.end(), key) != meshKeys.end()) {
key = baseKey + "#" + std::to_string(suffix++);
}
meshKeys.push_back(std::move(key));
meshKeys[id] = std::move(key);
return id;
}
void MeshCache::removeMesh(GfxDevice& gfxDevice, MeshID id)
{
if (!isValid(id)) {
return;
}
GPUMesh& mesh = meshes[id];
gfxDevice.destroyBuffer(mesh.vertexBuffer);
gfxDevice.destroyBuffer(mesh.skinningDataBuffer);
const IndexRange range{mesh.firstIndex, mesh.numIndices};
mesh = {};
cpuMeshes[id] = {};
meshKeys[id].clear();
gfxDevice.deferDestroy([bin = recycleBin, id, range] {
bin->ids.push_back(id);
if (range.count > 0) {
bin->indexRanges.push_back(range);
}
});
}
std::uint32_t MeshCache::allocateIndices(GfxDevice& gfxDevice, std::uint32_t count)
{
auto& ranges = recycleBin->indexRanges;
const auto fit = std::ranges::find_if(ranges, [count](const IndexRange& range) { return range.count >= count; });
if (fit != ranges.end()) {
const std::uint32_t first = fit->first;
fit->first += count;
fit->count -= count;
if (fit->count == 0) {
ranges.erase(fit);
}
return first;
}
ensureIndexCapacity(gfxDevice, indexCount + count);
const std::uint32_t first = indexCount;
indexCount += count;
return first;
}
void MeshCache::ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices)
{
if (requiredIndices <= indexCapacity) {
@@ -111,8 +161,7 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
{
try {
const auto numIndices = static_cast<std::uint32_t>(cpuMesh.indices.size());
ensureIndexCapacity(gfxDevice, indexCount + numIndices);
gpuMesh.firstIndex = indexCount;
gpuMesh.firstIndex = allocateIndices(gfxDevice, numIndices);
const auto indexBufferSize = cpuMesh.indices.size() * sizeof(std::uint32_t);
const auto indexBufferOffset = static_cast<VkDeviceSize>(gpuMesh.firstIndex) * sizeof(std::uint32_t);
@@ -139,8 +188,8 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
memcpy(static_cast<char*>(vertexIndexData) + vertexBufferSize, cpuMesh.indices.data(), indexBufferSize);
gfxDevice.getMemoryManager().flushAllocation(vertexIndexStaging);
// Writing into the shared index buffer is safe while frames are in flight:
// this range is new, they only read the ranges of meshes added before it.
// Writing into the shared index buffer is safe while frames are in flight: the range is either
// new or was recycled only after every frame that used it finished.
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
vkutil::bufferHostWriteToTransferReadBarrier(
cmd,
@@ -201,7 +250,6 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
}
gfxDevice.destroyBufferNow(vertexIndexStaging);
indexCount += numIndices;
const auto vtxBufferName = cpuMesh.name + " (vtx)";
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
@@ -295,6 +343,9 @@ const std::string& MeshCache::getMeshKey(MeshID id) const
std::optional<MeshID> MeshCache::findMeshByKey(std::string_view key) const
{
if (key.empty()) {
return std::nullopt;
}
const auto it = std::find(meshKeys.begin(), meshKeys.end(), key);
if (it == meshKeys.end()) {
return std::nullopt;
@@ -315,4 +366,5 @@ void MeshCache::cleanup(GfxDevice& gfxDevice)
meshes.clear();
cpuMeshes.clear();
meshKeys.clear();
recycleBin = std::make_shared<RecycleBin>();
}
@@ -0,0 +1,91 @@
#include <destrum/Graphics/Pipelines/PostProcessPipeline.h>
#include <array>
#include <cmath>
#include <stdexcept>
#include <volk.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h>
namespace {
constexpr std::uint32_t NoTexture = 0xFFFFFFFFu;
}
void PostProcessPipeline::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat outputFormat)
{
try {
const auto vertexShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/fullscreen_triangle.vert");
const auto fragmentShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/post.frag");
const VkPushConstantRange pushConstants{
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0,
.size = sizeof(PushConstants),
};
const VkDescriptorSetLayout bindlessLayout = resources.getBindlessDescSetLayout();
const VkPipelineLayoutCreateInfo layoutInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = 1,
.pSetLayouts = &bindlessLayout,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &pushConstants,
};
if (vkCreatePipelineLayout(gfxDevice.getDevice(), &layoutInfo, nullptr, &m_PipelineLayout) != VK_SUCCESS) {
throw std::runtime_error("Could not create the post process pipeline layout");
}
PipelineConfigInfo config{};
Pipeline::DefaultPipelineConfigInfo(config);
config.name = "Post Process Pipeline";
config.pipelineLayout = m_PipelineLayout;
config.vertexAttributeDescriptions = {};
config.vertexBindingDescriptions = {};
config.colorAttachments = {outputFormat};
config.depthAttachment = VK_FORMAT_UNDEFINED;
config.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
config.depthStencilInfo.depthTestEnable = VK_FALSE;
config.depthStencilInfo.depthWriteEnable = VK_FALSE;
m_Pipeline = std::make_unique<Pipeline>(gfxDevice, vertexShader.string(), fragmentShader.string(), config);
} catch (...) {
cleanup(gfxDevice.getDevice());
throw;
}
}
void PostProcessPipeline::cleanup(VkDevice device)
{
m_Pipeline.reset();
if (m_PipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device, m_PipelineLayout, nullptr);
}
m_PipelineLayout = VK_NULL_HANDLE;
}
void PostProcessPipeline::draw(VkCommandBuffer cmd, const RenderResources& resources, VkExtent2D extent, const Settings& settings) const
{
m_Pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, m_PipelineLayout);
const VkViewport viewport{
.width = static_cast<float>(extent.width),
.height = static_cast<float>(extent.height),
.maxDepth = 1.0f,
};
const VkRect2D scissor{.extent = extent};
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &scissor);
const PushConstants constants{
.sceneColorTexture = settings.sceneColor,
.selectionTexture = settings.selection ? *settings.selection : NoTexture,
.tonemapper = static_cast<std::uint32_t>(settings.tonemapper),
.exposure = std::exp2(settings.exposure),
.hoverPixel = settings.hoverPixel.value_or(glm::ivec2{-1}),
};
vkCmdPushConstants(cmd, m_PipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstants), &constants);
vkCmdDraw(cmd, 3, 1, 0, 0);
}
+60 -7
View File
@@ -193,7 +193,63 @@ ImageID RenderResources::loadImageFromFile(
TextureIntent intent)
{
(void)gfxDevice;
return imageCache->loadImageFromFile(path, usage, mipMap, intent);
const ImageID id = imageCache->loadImageFromFile(path, usage, mipMap, intent);
pinnedImages.insert(id);
return id;
}
ImageID RenderResources::loadMaterialTexture(
GfxDevice& gfxDevice,
const std::filesystem::path& path,
TextureIntent intent)
{
(void)gfxDevice;
const ImageID id = imageCache->loadImageFromFile(path, VK_IMAGE_USAGE_SAMPLED_BIT, true, intent);
if (id != errorImageId) {
materialTextures.insert(id);
}
return id;
}
RenderResources::ReleaseStats RenderResources::releaseUnused(
GfxDevice& gfxDevice,
const std::unordered_set<MeshID>& usedMeshes,
const std::unordered_set<MaterialID>& usedMaterials)
{
ReleaseStats stats;
for (MeshID id = 0; id < meshCache->getSlotCount(); ++id) {
if (meshCache->isValid(id) && !usedMeshes.contains(id)) {
meshCache->removeMesh(gfxDevice, id);
++stats.meshes;
}
}
std::unordered_set<ImageID> usedImages;
for (MaterialID id = 0; id < materialCache->getMaterialCount(); ++id) {
if (!materialCache->isValid(id)) {
continue;
}
if (id != materialCache->getPlaceholderMaterialId() && !usedMaterials.contains(id)) {
materialCache->removeMaterial(id);
++stats.materials;
continue;
}
const Material& material = materialCache->getMaterial(id);
for (const ImageID texture : {material.diffuseTexture, material.normalTexture, material.metallicTexture,
material.roughnessTexture, material.occlusionTexture, material.emissiveTexture}) {
usedImages.insert(texture);
}
}
std::erase_if(materialTextures, [&](ImageID id) {
if (usedImages.contains(id) || pinnedImages.contains(id)) {
return false;
}
imageCache->removeImage(id);
++stats.images;
return true;
});
return stats;
}
const GPUImage& RenderResources::getImage(ImageID id) const {
@@ -253,12 +309,7 @@ MaterialID RenderResources::addModelMaterial(
if (texture == nullptr) {
return NULL_IMAGE_ID;
}
return loadImageFromFile(
gfxDevice,
std::filesystem::path{texture->path},
VK_IMAGE_USAGE_SAMPLED_BIT,
true,
intent);
return loadMaterialTexture(gfxDevice, std::filesystem::path{texture->path}, intent);
};
Material material = Material::SimpleColor(
@@ -326,6 +377,8 @@ void RenderResources::cleanup(GfxDevice& gfxDevice)
return;
}
materialTextures.clear();
pinnedImages.clear();
if (materialCache) {
materialCache->cleanup(gfxDevice);
}
+140 -34
View File
@@ -30,10 +30,10 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
sizeof(GPUSceneData),
"scene data");
createDrawImage(gfxDevice, drawImageSize, true);
createDrawImage(gfxDevice, drawImageSize);
meshPipeline = std::make_unique<MeshPipeline>();
meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat, selectionImageFormat);
meshPipeline->init(gfxDevice, _resources, sceneColorFormat, depthImageFormat, selectionImageFormat);
for (GPUBuffer& pickBuffer : pickBuffers)
{
@@ -44,7 +44,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
}
skyboxPipeline = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
skyboxPipeline->init(gfxDevice, _resources, sceneColorFormat, depthImageFormat);
skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice);
@@ -61,6 +61,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
renderer2DPass = std::make_unique<Renderer2DPass>();
renderer2DPass->init(gfxDevice, _resources, drawImageFormat);
postProcessPipeline = std::make_unique<PostProcessPipeline>();
postProcessPipeline->init(gfxDevice, _resources, drawImageFormat);
GameState::GetInstance().SetRenderer(this);
initialized = true;
}
@@ -79,7 +82,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
{
pickPending[frameIndex] = false;
gfxDevice.getMemoryManager().invalidateAllocation(pickBuffers[frameIndex]);
pickResult = *static_cast<const std::uint32_t*>(pickBuffers[frameIndex].info.pMappedData);
pickResult = *static_cast<const std::uint32_t*>(pickBuffers[frameIndex].info.pMappedData) & ~HighlightedObjectFlag;
}
meshDrawCommands.clear();
@@ -250,18 +253,19 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
);
}
const auto& sceneColor = resources->getImage(sceneColorImageId);
const auto& drawImage = resources->getImage(drawImageId);
const auto& depthImage = resources->getImage(depthImageId);
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Draw Image");
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Scene Color");
vkutil::transitionImage(
cmd,
drawImage.image,
drawImage.layout,
sceneColor.image,
sceneColor.layout,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
drawImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
sceneColor.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
}
{
@@ -376,8 +380,8 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
}
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(),
.colorImageView = drawImage.imageView,
.renderExtent = sceneColor.getExtent2D(),
.colorImageView = sceneColor.imageView,
.colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f},
.depthImageView = depthImage.imageView,
});
@@ -420,6 +424,38 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
vkutil::cmdEndLabel(cmd);
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Post Process");
vkutil::ScopedCmdLabel label(cmd, "Post Process", vkutil::labelColors::opaque);
vkutil::transitionImage(cmd, sceneColor.image, sceneColor.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
sceneColor.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
if (selectionPassEnabled)
{
const auto& selectionImage = resources->getImage(selectionImageId);
vkutil::transitionImage(cmd, selectionImage.image, selectionImage.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
selectionImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
vkutil::transitionImage(cmd, drawImage.image, drawImage.layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
drawImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
auto postInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(),
.colorImageView = drawImage.imageView,
});
postInfo.renderingInfo.pColorAttachments = &postInfo.colorAttachment;
vkCmdBeginRendering(cmd, &postInfo.renderingInfo);
postProcessPipeline->draw(cmd, *resources, drawImage.getExtent2D(), {
.sceneColor = sceneColorImageId,
.selection = selectionPassEnabled ? std::optional{selectionImageId} : std::nullopt,
.hoverPixel = hoverPixel,
.tonemapper = renderSelectionView ? Tonemapper::None : sceneData.tonemapper,
.exposure = renderSelectionView ? 0.0f : sceneData.exposure,
});
vkCmdEndRendering(cmd);
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "LineRenderingPass::draw");
vkutil::ScopedCmdLabel label(cmd, "LineRenderingPass::draw", vkutil::labelColors::debug);
@@ -524,6 +560,9 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
if (meshPipeline)
meshPipeline->cleanup(device);
if (postProcessPipeline)
postProcessPipeline->cleanup(device);
sceneDataBuffer.cleanup(gfxDevice);
drawList.cleanup(gfxDevice);
for (GPUBuffer& pickBuffer : pickBuffers)
@@ -540,9 +579,13 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
// if (depthImageId != NULL_IMAGE_ID)
// gfxDevice.destroyImage(depthImageId);
sceneColorImageId = NULL_IMAGE_ID;
drawImageId = NULL_IMAGE_ID;
depthImageId = NULL_IMAGE_ID;
selectionImageId = NULL_IMAGE_ID;
postProcessPipeline.reset();
highlightedObjects.clear();
hoverPixel.reset();
resources = nullptr;
meshPipeline.reset();
skyboxPipeline.reset();
@@ -572,7 +615,7 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma
.transformMatrix = transform,
.worldBoundingSphere = worldBoundingSphere,
.materialId = materialId,
.objectId = objectId,
.objectId = highlightedObjects.contains(objectId) ? objectId | HighlightedObjectFlag : objectId,
});
}
@@ -595,7 +638,7 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
.materialId = materialId,
.skinnedMesh = skinnedMesh,
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex),
.objectId = objectId,
.objectId = highlightedObjects.contains(objectId) ? objectId | HighlightedObjectFlag : objectId,
});
}
@@ -661,10 +704,73 @@ void GameRenderer::updateMaterialGPU(MaterialID id)
pendingMaterialUploads.push_back(id);
}
void GameRenderer::setSkyboxTexture(ImageID skyboxImageId)
void GameRenderer::setSkyboxTexture(ImageID imageId)
{
spdlog::debug("Set skybox texture to image id {}", skyboxImageId);
skyboxPipeline->setSkyboxImage(skyboxImageId);
spdlog::debug("Set skybox texture to image id {}", imageId);
skyboxImageId = imageId;
skyboxPipeline->setSkyboxImage(imageId);
}
bool GameRenderer::reloadShaders(GfxDevice& gfxDevice)
{
const VkDevice device = gfxDevice.getDevice();
const VkExtent3D extent = getDrawImage().extent;
auto mesh = std::make_unique<MeshPipeline>();
auto skybox = std::make_unique<SkyboxPipeline>();
auto skinning = std::make_unique<SkinningPipeline>();
auto lightCulling = std::make_unique<LightCullingPipeline>();
auto shadows = std::make_unique<ShadowMapPipeline>();
auto lines = std::make_unique<LineRenderingPass>();
auto ui = std::make_unique<Renderer2DPass>();
auto post = std::make_unique<PostProcessPipeline>();
try
{
mesh->init(gfxDevice, *resources, sceneColorFormat, depthImageFormat, selectionImageFormat);
skybox->init(gfxDevice, *resources, sceneColorFormat, depthImageFormat);
skinning->init(gfxDevice);
lightCulling->init(gfxDevice, *resources, glm::ivec2{extent.width, extent.height});
shadows->init(gfxDevice, *resources);
lines->init(gfxDevice, drawImageFormat);
ui->init(gfxDevice, *resources, drawImageFormat);
post->init(gfxDevice, *resources, drawImageFormat);
}
catch (const std::exception& exception)
{
spdlog::error("Shader reload failed, keeping the current pipelines: {}", exception.what());
mesh->cleanup(device);
skybox->cleanup(device);
skinning->cleanup(gfxDevice);
lightCulling->cleanup(gfxDevice);
shadows->cleanup(device);
lines->cleanup(gfxDevice);
ui->cleanup(gfxDevice);
post->cleanup(device);
return false;
}
mesh->setRenderWireframe(meshPipeline->getRenderWireframe());
skybox->setSkyboxImage(skyboxImageId);
shadows->setShadowDistance(shadowMapPipeline->getShadowDistance());
// Frames in flight still use the old pipelines, the deletion queue destroys them once those are done
const auto retire = [&gfxDevice](auto& current, auto& replacement, auto destroy) {
std::shared_ptr old{std::move(current)};
gfxDevice.deferDestroy([old, destroy]() mutable { destroy(*old); });
current = std::move(replacement);
};
retire(meshPipeline, mesh, [device](MeshPipeline& pipeline) { pipeline.cleanup(device); });
retire(skyboxPipeline, skybox, [device](SkyboxPipeline& pipeline) { pipeline.cleanup(device); });
retire(skinningPipeline, skinning, [&gfxDevice](SkinningPipeline& pipeline) { pipeline.cleanup(gfxDevice); });
retire(lightCullingPipeline, lightCulling, [&gfxDevice](LightCullingPipeline& pipeline) { pipeline.cleanup(gfxDevice); });
retire(shadowMapPipeline, shadows, [device](ShadowMapPipeline& pipeline) { pipeline.cleanup(device); });
retire(lineRenderingPass, lines, [&gfxDevice](LineRenderingPass& pass) { pass.cleanup(gfxDevice); });
retire(renderer2DPass, ui, [&gfxDevice](Renderer2DPass& pass) { pass.cleanup(gfxDevice); });
retire(postProcessPipeline, post, [device](PostProcessPipeline& pipeline) { pipeline.cleanup(device); });
spdlog::info("Reloaded shaders");
return true;
}
void GameRenderer::recordMaterialUpdates(VkCommandBuffer cmd)
@@ -696,8 +802,7 @@ void GameRenderer::recordMaterialUpdates(VkCommandBuffer cmd)
}
void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
const glm::ivec2& drawImageSize,
bool firstCreate)
const glm::ivec2& drawImageSize)
{
const VkExtent3D drawImageExtent{
.width = (std::uint32_t)drawImageSize.x,
@@ -708,29 +813,30 @@ void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
constexpr VkSampleCountFlagBits noMsaa = VK_SAMPLE_COUNT_1_BIT;
{
// setup draw image (single-sampled)
VkImageUsageFlags usages{};
usages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
usages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
usages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
usages |= VK_IMAGE_USAGE_SAMPLED_BIT;
auto createImageInfo = vkutil::CreateImageInfo{
.format = drawImageFormat,
.usage = usages,
// HDR target of the forward pass, the post pass tonemaps it into the draw image
const auto createInfo = vkutil::CreateImageInfo{
.format = sceneColorFormat,
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.extent = drawImageExtent,
.samples = noMsaa,
};
// a fresh bindless slot, the old one may still be read by frames in flight
resources->destroyImage(drawImageId);
drawImageId = resources->createImage(gfxDevice, createImageInfo, "draw image");
resources->destroyImage(sceneColorImageId);
sceneColorImageId = resources->createImage(gfxDevice, createInfo, "scene color");
}
if (firstCreate)
{
// Optional: a separate post-fx target (ping-pong)
// postFXDrawImageId = gfxDevice.createImage(createImageInfo, "post FX draw image");
}
{
const auto createInfo = vkutil::CreateImageInfo{
.format = drawImageFormat,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.extent = drawImageExtent,
.samples = noMsaa,
};
resources->destroyImage(drawImageId);
drawImageId = resources->createImage(gfxDevice, createInfo, "draw image");
}
+39
View File
@@ -0,0 +1,39 @@
#include <destrum/Scene/ResourceCollector.h>
#include <unordered_set>
#include <spdlog/spdlog.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
void CollectUnusedResources(SceneManager& sceneManager, RenderResources& resources, GfxDevice& gfxDevice)
{
std::unordered_set<MeshID> usedMeshes;
std::unordered_set<MaterialID> usedMaterials;
for (const auto& scene : sceneManager.GetScenes()) {
for (const auto* objects : {&scene->GetObjects(), &scene->GetPendingAdditions()}) {
for (const auto& object : *objects) {
if (object == nullptr || object->IsBeingDestroyed()) {
continue;
}
for (const auto& component : object->GetComponents()) {
if (const auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(component.get())) {
usedMeshes.insert(meshRenderer->GetMeshID());
usedMaterials.insert(meshRenderer->GetMaterialID());
}
}
}
}
}
const auto stats = resources.releaseUnused(gfxDevice, usedMeshes, usedMaterials);
if (stats.meshes + stats.materials + stats.images > 0) {
spdlog::info("Released {} meshes, {} materials and {} textures nothing uses any more",
stats.meshes, stats.materials, stats.images);
}
}
+9 -3
View File
@@ -3,6 +3,7 @@
#include <destrum/ObjectModel/GameObject.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <glm/gtc/constants.hpp>
@@ -150,6 +151,14 @@ void Scene::DrawSettingsImGui() {
ImGui::ColorEdit3("Fog color", &environment.fogColor.x, ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_Float);
ImGui::DragFloat("Fog density", &environment.fogDensity, 0.001f, 0.0f, 1.0f, "%.4f");
ImGui::SeparatorText("Camera");
ImGui::DragFloat("Exposure (EV)", &environment.exposure, 0.05f, -10.0f, 10.0f, "%.2f");
constexpr std::array<const char*, 3> tonemappers{"None", "ACES", "Khronos PBR Neutral"};
int tonemapper = static_cast<int>(environment.tonemapper);
if (ImGui::Combo("Tonemapper", &tonemapper, tonemappers.data(), static_cast<int>(tonemappers.size()))) {
environment.tonemapper = static_cast<Tonemapper>(tonemapper);
}
ImGui::SeparatorText("Skybox");
float speedDegrees = glm::degrees(environment.skyboxRotationSpeed);
if (ImGui::DragFloat("Spin (deg/s)", &speedDegrees, 0.05f)) {
@@ -236,9 +245,6 @@ void Scene::SetCurrentCamera(const CameraComponent* camera) {
m_CurrentCameraId = camera != nullptr ? camera->GetGameObject()->GetId() : InvalidObjectId;
}
void Scene::RenderImgui(bool* open) {
m_Inspector.Draw(*this, m_SelectedObjectId, [this] { DrawSettingsImGui(); }, open);
}
bool Scene::MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore) {
if (IsIterating() || object.IsBeingDestroyed() || object.GetScene() != this || parent == &object) {
-6
View File
@@ -48,12 +48,6 @@ void SceneManager::Render(const RenderContext& ctx) {
}
}
void SceneManager::RenderImgui(bool* open) {
for (const auto& scene : m_activeScenes) {
scene->RenderImgui(open);
}
}
void SceneManager::HandleGameObjectDestroy() {
for (const auto& scene : m_scenes) {
scene->CleanupDestroyedGameObjects();
@@ -1,11 +1,16 @@
#include <destrum/Serialization/MaterialSerialization.h>
#include <array>
#include <atomic>
#include <filesystem>
#include <functional>
#include <unordered_map>
#include <utility>
#include <spdlog/spdlog.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Renderer.h>
@@ -26,31 +31,76 @@ namespace {
{"emissive", &Material::emissiveTexture},
}};
[[nodiscard]] const char* FilteringName(TextureFilteringMode mode) {
switch (mode) {
case TextureFilteringMode::Nearest: return "nearest";
case TextureFilteringMode::Anisotropic: return "anisotropic";
case TextureFilteringMode::Linear: break;
}
return "linear";
}
constexpr std::array<std::string_view, 3> FilteringNames{"nearest", "linear", "anisotropic"};
[[nodiscard]] TextureFilteringMode ParseFiltering(const std::string& name) {
if (name == "nearest") return TextureFilteringMode::Nearest;
if (name == "anisotropic") return TextureFilteringMode::Anisotropic;
[[nodiscard]] TextureFilteringMode ParseFiltering(std::string_view name)
{
for (std::size_t i = 0; i < FilteringNames.size(); ++i) {
if (FilteringNames[i] == name) {
return static_cast<TextureFilteringMode>(i);
}
}
return TextureFilteringMode::Linear;
}
[[nodiscard]] RenderResources* GetResources() {
if (!GameState::GetInstance().HasRenderer() || !GameState::GetInstance().HasGfxDevice()) {
[[nodiscard]] RenderResources* GetResources()
{
auto& state = GameState::GetInstance();
return state.HasRenderer() && state.HasGfxDevice() ? state.Renderer().getResources() : nullptr;
}
[[nodiscard]] json WriteTexture(const RenderResources& resources, ImageID id)
{
const ImageCache::LoadedImageInfo* info = resources.images().findLoadedImageInfo(id);
if (info == nullptr) {
spdlog::warn("Texture {} was not loaded from a file, it is not saved", id);
return nullptr;
}
return GameState::GetInstance().Renderer().getResources();
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
json texture = virtualPath ? AssetDatabase::GetInstance().WriteReference(*virtualPath)
: json{{"path", info->path.generic_string()}};
texture["srgb"] = info->intent == TextureIntent::ColorSrgb;
return texture;
}
[[nodiscard]] ImageID ReadTexture(RenderResources& resources, GfxDevice& gfxDevice, const json& texture)
{
const auto reference = AssetDatabase::GetInstance().ReadReference(texture);
if (!reference) {
return NULL_IMAGE_ID;
}
const TextureIntent intent = texture.value("srgb", true) ? TextureIntent::ColorSrgb : TextureIntent::DataLinear;
try {
const std::filesystem::path fullPath = reference->path.find("://") != std::string::npos
? AssetFS::GetInstance().GetFullPath(reference->path)
: std::filesystem::path{reference->path};
return resources.loadMaterialTexture(gfxDevice, fullPath, intent);
} catch (const std::exception& exception) {
spdlog::warn("Material texture \"{}\" could not be loaded: {}", reference->path, exception.what());
return resources.getErrorTextureID();
}
}
void VisitObjects(json& node, const std::function<void(json&)>& visit)
{
if (node.is_object()) {
visit(node);
for (auto& [key, value] : node.items()) {
VisitObjects(value, visit);
}
} else if (node.is_array()) {
for (auto& element : node) {
VisitObjects(element, visit);
}
}
}
std::atomic<std::uint32_t> s_LoadCounter{0};
}
namespace MaterialSerialization {
json ToJson(const Material& material, const TextureWriter& writeTexture) {
json ToJson(const Material& material, const TextureWriter& writeTexture)
{
json data = {
{"name", material.name},
{"baseColor", material.baseColor},
@@ -59,7 +109,7 @@ namespace MaterialSerialization {
{"emissiveColor", material.emissiveColor},
{"normalScale", material.normalScale},
{"occlusionStrength", material.occlusionStrength},
{"filtering", FilteringName(material.textureFilteringMode)},
{"filtering", FilteringNames.at(static_cast<std::size_t>(material.textureFilteringMode))},
};
json textures = json::object();
@@ -67,8 +117,7 @@ namespace MaterialSerialization {
if (material.*member == NULL_IMAGE_ID) {
continue;
}
json texture = writeTexture(material.*member);
if (!texture.is_null()) {
if (json texture = writeTexture(material.*member); !texture.is_null()) {
textures[slot] = std::move(texture);
}
}
@@ -78,7 +127,8 @@ namespace MaterialSerialization {
return data;
}
Material FromJson(const json& data, const TextureReader& readTexture) {
Material FromJson(const json& data, const TextureReader& readTexture)
{
Material material{};
material.name = data.value("name", std::string{});
material.baseColor = data.value("baseColor", material.baseColor);
@@ -100,7 +150,8 @@ namespace MaterialSerialization {
return material;
}
std::optional<std::string> Validate(const json& materials) {
std::optional<std::string> Validate(const json& materials)
{
if (!materials.is_object()) {
return "\"materials\" must be an object";
}
@@ -126,7 +177,48 @@ namespace MaterialSerialization {
return std::nullopt;
}
json CollectEmbedded(const json& objects) {
bool IsFileKey(std::string_view key)
{
return AssetTypes::IsMaterial(key);
}
json ToFile(const Material& material, const RenderResources* resources)
{
json data = ToJson(material, [resources](ImageID id) -> json {
return resources != nullptr ? WriteTexture(*resources, id) : json{};
});
data["version"] = CurrentFileVersion;
return data;
}
MaterialID LoadFile(RenderResources& resources, GfxDevice& gfxDevice, std::string_view path)
{
if (const auto cached = resources.materials().findMaterialByKey(path)) {
return *cached;
}
const auto bytes = AssetFS::GetInstance().ReadBytes(path);
const json data = json::parse(bytes.begin(), bytes.end());
if (const int version = data.value("version", 0); version < 1 || version > CurrentFileVersion) {
throw std::runtime_error("unsupported material file version " + std::to_string(version));
}
Material material = FromJson(data, [&](const json& texture) { return ReadTexture(resources, gfxDevice, texture); });
material.assetReference = AssetReference{.path = std::string{path}};
if (material.name.empty()) {
material.name = std::filesystem::path{path}.stem().string();
}
return resources.materials().addMaterial(std::move(material), std::string{path});
}
bool SaveFile(const RenderResources& resources, MaterialID id, std::string_view path)
{
const AssetDatabase::MetaFileScope createMetaFiles;
return AssetFS::GetInstance().WriteFile(path, ToFile(resources.materials().getMaterial(id), &resources).dump(4));
}
json EmbedLocalMaterials(json& objects)
{
json materials = json::object();
const RenderResources* resources = GetResources();
if (resources == nullptr) {
@@ -134,77 +226,59 @@ namespace MaterialSerialization {
}
const MaterialCache& cache = resources->materials();
const auto writeTexture = [&](ImageID id) -> json {
const ImageCache::LoadedImageInfo* info = resources->images().findLoadedImageInfo(id);
if (info == nullptr) {
spdlog::warn("Texture {} was not loaded from a file, it is not saved", id);
return nullptr;
std::unordered_map<std::string, std::string> localIds;
VisitObjects(objects, [&](json& node) {
const auto it = node.find("runtimeMaterial");
if (it == node.end() || !it->is_string()) {
return;
}
const std::string key = it->get<std::string>();
const auto id = cache.findMaterialByKey(key);
if (!id) {
spdlog::warn("Material \"{}\" no longer exists, the object loses it", key);
node = nullptr;
return;
}
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
return {
{"path", virtualPath ? *virtualPath : info->path.generic_string()},
{"srgb", info->intent == TextureIntent::ColorSrgb},
{"mipMap", info->mipMap},
};
};
for (const json& objectJson : objects) {
if (!objectJson.contains("components")) {
continue;
auto [local, inserted] = localIds.try_emplace(key, std::to_string(localIds.size() + 1));
if (inserted) {
materials[local->second] = ToJson(cache.getMaterial(*id), [resources](ImageID texture) {
return WriteTexture(*resources, texture);
});
}
for (const json& componentJson : objectJson.at("components")) {
if (!componentJson.contains("data") || !componentJson.at("data").contains("materialKey")) {
continue;
}
const json& keyJson = componentJson.at("data").at("materialKey");
if (!keyJson.is_string() || materials.contains(keyJson.get<std::string>())) {
continue;
}
const std::string key = keyJson.get<std::string>();
const auto id = cache.findMaterialByKey(key);
if (!id || *id == cache.getPlaceholderMaterialId() ||
!cache.getMaterial(*id).assetReference.empty()) {
continue;
}
materials[key] = ToJson(cache.getMaterial(*id), writeTexture);
}
}
node = json{{"localMaterial", local->second}};
});
return materials;
}
void RegisterEmbedded(const json& materials, bool overwriteExisting) {
void LoadLocalMaterials(const json& materials, json& objects)
{
RenderResources* resources = GetResources();
if (resources == nullptr || !materials.is_object()) {
return;
}
GfxDevice& gfxDevice = GameState::GetInstance().Gfx();
const auto readTexture = [&](const json& texture) -> ImageID {
const std::string path = texture.at("path").get<std::string>();
const TextureIntent intent = texture.value("srgb", true)
? TextureIntent::ColorSrgb
: TextureIntent::DataLinear;
try {
const std::filesystem::path fullPath = path.find("://") != std::string::npos
? AssetFS::GetInstance().GetFullPath(path)
: std::filesystem::path{path};
return resources->loadImageFromFile(
gfxDevice, fullPath, VK_IMAGE_USAGE_SAMPLED_BIT, texture.value("mipMap", true), intent);
} catch (const std::exception& exception) {
spdlog::warn("Material texture \"{}\" could not be loaded: {}", path, exception.what());
return resources->getErrorTextureID();
}
};
for (auto it = materials.begin(); it != materials.end(); ++it) {
const auto existing = resources->materials().findMaterialByKey(it.key());
if (!existing) {
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
} else if (overwriteExisting) {
resources->materials().getMaterialMutable(*existing) = FromJson(it.value(), readTexture);
GameState::GetInstance().Renderer().updateMaterialGPU(*existing);
}
// A fresh key per load keeps two scenes, or two loads of one scene, from sharing a local material
const std::string prefix = "local:" + std::to_string(++s_LoadCounter) + ":";
std::unordered_map<std::string, std::string> keys;
for (const auto& [local, data] : materials.items()) {
const std::string key = prefix + local;
Material material = FromJson(data, [&](const json& texture) { return ReadTexture(*resources, gfxDevice, texture); });
resources->materials().addMaterial(std::move(material), key);
keys.emplace(local, key);
}
VisitObjects(objects, [&](json& node) {
if (const auto it = node.find("localMaterial"); it != node.end() && it->is_string()) {
const auto key = keys.find(it->get<std::string>());
node = key != keys.end() ? json{{"runtimeMaterial", key->second}} : json{};
} else if (const auto legacy = node.find("materialKey"); legacy != node.end() && legacy->is_string()) {
// Scenes saved before local ids keyed their embedded materials by the material's cache key
if (const auto key = keys.find(legacy->get<std::string>()); key != keys.end()) {
*legacy = key->second;
}
}
});
}
}
@@ -9,6 +9,7 @@
#include <spdlog/spdlog.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h>
@@ -309,10 +310,9 @@ namespace ObjectSerialization {
// Generic: any nested object with a "path" field is an AssetReference.
std::function<void(const json&)> walk = [&](const json& node) {
if (node.is_object()) {
if (node.contains("path") && node.at("path").is_string()) {
const std::string path = node.at("path").get<std::string>();
if (!path.empty() && !AssetFileExists(path)) {
report(objectName, path);
if (const auto reference = AssetDatabase::GetInstance().ReadReference(node)) {
if (!reference->path.empty() && !AssetFileExists(reference->path)) {
report(objectName, reference->path);
}
}
for (auto it = node.begin(); it != node.end(); ++it) {
@@ -34,10 +34,11 @@ namespace {
}
try {
json objects = prefab.at("objects");
if (prefab.contains("materials")) {
MaterialSerialization::RegisterEmbedded(prefab.at("materials"));
MaterialSerialization::LoadLocalMaterials(prefab.at("materials"), objects);
}
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
auto built = ObjectSerialization::BuildObjects(scene, objects, externalObjects);
GameObject* root = built.objects.front();
if (parent != nullptr) {
root->GetTransform().SetParent(parent, false);
@@ -62,7 +63,7 @@ std::optional<json> PrefabSerializer::Capture(const GameObject& root) {
prefab["name"] = root.GetName();
try {
prefab["objects"] = ObjectSerialization::SerializeObjects(ObjectSerialization::CollectHierarchy(root));
if (json materials = MaterialSerialization::CollectEmbedded(prefab["objects"]); !materials.empty()) {
if (json materials = MaterialSerialization::EmbedLocalMaterials(prefab["objects"]); !materials.empty()) {
prefab["materials"] = std::move(materials);
}
} catch (const std::exception& exception) {
@@ -93,6 +93,8 @@ std::optional<json> SceneSerializer::Capture(Scene& scene) {
{"fogDensity", environment.fogDensity},
{"skyboxRotationSpeed", environment.skyboxRotationSpeed},
{"skyboxRotation", environment.skyboxRotation},
{"exposure", environment.exposure},
{"tonemapper", ToString(environment.tonemapper)},
};
std::vector<const GameObject*> objects;
@@ -105,7 +107,7 @@ std::optional<json> SceneSerializer::Capture(Scene& scene) {
try {
root["objects"] = ObjectSerialization::SerializeObjects(objects);
if (json materials = MaterialSerialization::CollectEmbedded(root["objects"]); !materials.empty()) {
if (json materials = MaterialSerialization::EmbedLocalMaterials(root["objects"]); !materials.empty()) {
root["materials"] = std::move(materials);
}
} catch (const std::exception& exception) {
@@ -205,12 +207,13 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
std::string context = "scene";
try {
json objects = root.at("objects");
if (root.contains("materials")) {
context = "materials";
MaterialSerialization::RegisterEmbedded(root.at("materials"), true);
MaterialSerialization::LoadLocalMaterials(root.at("materials"), objects);
context = "scene";
}
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
idMap = ObjectSerialization::BuildObjects(stagingScene, objects).idMap;
// Move objects from pending to objects list without physics registration
// (staging scene has no physics world).
@@ -284,6 +287,8 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
environment.skyboxRotation = data.value("skyboxRotation", environment.skyboxRotation);
environment.exposure = data.value("exposure", environment.exposure);
environment.tonemapper = ParseTonemapper(data.value("tonemapper", std::string{})).value_or(environment.tonemapper);
} catch (const std::exception& exception) {
spdlog::warn("Scene \"{}\" has invalid environment settings, using defaults: {}",
scene.m_name, exception.what());
+31
View File
@@ -0,0 +1,31 @@
set(EDITOR_SRC
src/Editor.cpp
src/EditorMenus.cpp
src/EditorViewport.cpp
src/EditorCamera.cpp
src/PlayMode.cpp
src/UndoStack.cpp
src/ShaderHotReload.cpp
src/SceneInspector.cpp
src/AssetBrowser.cpp
src/TransformGizmo.cpp
src/RectGizmo.cpp
src/ResourcesInspector.cpp
)
add_library(destrum_editor ${EDITOR_SRC})
add_library(destrum::editor ALIAS destrum_editor)
set_target_properties(destrum_editor PROPERTIES
CXX_STANDARD 20
CXX_EXTENSIONS OFF
)
target_include_directories(destrum_editor PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_link_libraries(destrum_editor PUBLIC destrum::destrum)
target_compile_definitions(destrum_editor PRIVATE
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
DESTRUM_THECHEF_PATH="$<TARGET_FILE:TheChef>"
)
add_dependencies(destrum_editor TheChef)
@@ -1,16 +1,14 @@
#ifndef DESTRUM_ASSETBROWSER_H
#define DESTRUM_ASSETBROWSER_H
#ifndef EDITOR_ASSETBROWSER_H
#define EDITOR_ASSETBROWSER_H
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class AssetBrowser final {
public:
void Draw(bool* open = nullptr);
[[nodiscard]] static bool IsModel(std::string_view path);
std::optional<std::string> Draw(bool* open = nullptr);
private:
struct Entry {
@@ -20,7 +18,9 @@ private:
void Refresh();
void Navigate(const std::string& scheme, const std::filesystem::path& folder);
bool DrawEntry(const Entry& entry);
[[nodiscard]] std::string VirtualPath(const std::string& name) const;
void DrawEntry(const Entry& entry, std::optional<std::string>& opened, bool& navigated);
void DrawFolderMenu();
std::string m_Scheme;
std::filesystem::path m_Folder;
@@ -29,4 +29,4 @@ private:
bool m_Dirty{true};
};
#endif // DESTRUM_ASSETBROWSER_H
#endif // EDITOR_ASSETBROWSER_H
+141
View File
@@ -0,0 +1,141 @@
#ifndef EDITOR_EDITOR_H
#define EDITOR_EDITOR_H
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <glm/vec2.hpp>
#include <destrum/AppLayer.h>
#include <editor/AssetBrowser.h>
#include <editor/EditorCamera.h>
#include <editor/PlayMode.h>
#include <editor/RectGizmo.h>
#include <editor/ResourcesInspector.h>
#include <editor/SceneInspector.h>
#include <editor/ShaderHotReload.h>
#include <editor/TransformGizmo.h>
#include <editor/UndoStack.h>
class App;
class Scene;
struct EditorSettings {
std::filesystem::path gameAssetsSource;
std::string scenePath;
bool startInEditMode{true};
};
class Editor final : public AppLayer {
public:
Editor(App& app, EditorSettings settings);
void onAttach() override;
void onBeginFrame() override;
void onImGuiBegin(float dt) override;
void onImGui(float dt) override;
void onEndFrame() override;
void onRender(GameRenderer& renderer) override;
[[nodiscard]] Camera* viewCamera() override;
[[nodiscard]] bool hostsViewport() const override { return true; }
[[nodiscard]] InputFocus gameInputFocus() const override;
bool OpenScene(std::string_view path);
bool SaveScene();
bool SaveSceneAs(std::string_view path);
void NewScene();
[[nodiscard]] UndoStack& GetUndoStack() { return m_UndoStack; }
[[nodiscard]] PlayMode& GetPlayMode() { return m_PlayMode; }
private:
struct Panels {
bool inspector{true};
bool assets{true};
bool stats{true};
bool physics{false};
bool imageCache{false};
};
struct ViewOptions {
bool colliders{false};
bool gizmos{true};
};
struct Viewport {
glm::vec2 min{0.0f};
glm::vec2 size{1.0f};
glm::ivec2 desiredSize{0};
bool hovered{false};
bool focused{false};
bool mouseClaimed{false};
bool pickSelectsParent{false};
std::optional<glm::ivec2> hoverPixel;
};
// Editor.cpp
[[nodiscard]] Scene* CurrentScene() const;
[[nodiscard]] bool UsesEditorCamera();
void HandleShortcuts();
void Undo();
void Redo();
void RecordEdits();
void AfterSceneReplaced(bool resetHistory);
void DeleteSelection();
void DuplicateSelection();
void FocusSelection();
void SetStatus(std::string status);
bool WriteScene(Scene& scene, std::string_view path);
// EditorViewport.cpp
void ApplyViewportResize();
void DrawViewport();
void DrawGizmos(Scene& scene);
void RequestPick();
void ApplyPick();
void OnAssetDropped(std::string_view path);
void SubmitColliderLines(Scene& scene) const;
// EditorMenus.cpp
void DrawDockSpace();
void DrawMainMenuBar();
void DrawFileMenu();
void DrawEditMenu();
void DrawViewMenu();
void DrawDisplayMenu();
void DrawPlayControls();
void DrawSceneDialogs();
void DrawStatsPanel();
void DrawPhysicsPanel();
App& m_App;
EditorSettings m_Settings;
EditorCamera m_Camera;
PlayMode m_PlayMode;
UndoStack m_UndoStack;
SceneInspector m_Inspector;
AssetBrowser m_AssetBrowser;
ResourcesInspector m_ResourcesInspector;
TransformGizmo m_TransformGizmo;
RectGizmo m_RectGizmo;
ShaderHotReload m_ShaderHotReload;
Panels m_Panels;
ViewOptions m_View;
Viewport m_Viewport;
std::string m_ScenePath;
std::string m_Status;
std::string m_SaveAsPath;
bool m_OpenSceneDialog{false};
bool m_SaveAsDialog{false};
bool m_ItemWasActive{false};
bool m_EditPending{false};
};
#endif // EDITOR_EDITOR_H
+36
View File
@@ -0,0 +1,36 @@
#ifndef EDITOR_EDITORCAMERA_H
#define EDITOR_EDITORCAMERA_H
#include <optional>
#include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h>
class GameObject;
class RenderResources;
struct WorldBounds {
glm::vec3 center{0.0f};
float radius{1.0f};
};
[[nodiscard]] WorldBounds ComputeWorldBounds(GameObject& object, const RenderResources& resources);
class EditorCamera final {
public:
EditorCamera();
void Update(float dt, bool hasInput);
void Focus(const WorldBounds& bounds);
[[nodiscard]] Camera& GetCamera() { return m_Camera; }
[[nodiscard]] bool IsLooking() const { return m_Looking; }
private:
Camera m_Camera;
std::optional<glm::vec3> m_FocusTarget;
bool m_Looking{false};
};
#endif // EDITOR_EDITORCAMERA_H
+39
View File
@@ -0,0 +1,39 @@
#ifndef EDITOR_PLAYMODE_H
#define EDITOR_PLAYMODE_H
#include <memory>
#include <optional>
#include <nlohmann/json.hpp>
class App;
class Scene;
class PlayMode final {
public:
explicit PlayMode(App& app);
void EnterEditMode();
void RequestToggle() { m_ToggleRequested = true; }
void SetPaused(bool paused);
[[nodiscard]] bool ApplyPendingToggle();
[[nodiscard]] bool IsPlaying() const { return m_Playing; }
[[nodiscard]] bool IsPaused() const { return m_Paused; }
[[nodiscard]] bool IsEditing() const { return !m_Playing; }
private:
bool Enter();
void Exit();
App& m_App;
bool m_Playing{true};
bool m_Paused{false};
bool m_ToggleRequested{false};
bool m_MouseCapturedBeforePause{false};
std::optional<nlohmann::json> m_EditSnapshot;
std::weak_ptr<Scene> m_EditScene;
};
#endif // EDITOR_PLAYMODE_H
@@ -1,5 +1,5 @@
#ifndef DESTRUM_RECTGIZMO_H
#define DESTRUM_RECTGIZMO_H
#ifndef EDITOR_RECTGIZMO_H
#define EDITOR_RECTGIZMO_H
#include <glm/glm.hpp>
@@ -25,4 +25,4 @@ private:
glm::vec2 m_DragStartSize{0.0f};
};
#endif // DESTRUM_RECTGIZMO_H
#endif // EDITOR_RECTGIZMO_H
@@ -1,5 +1,5 @@
#ifndef RESOURCESINSPECTOR_H
#define RESOURCESINSPECTOR_H
#ifndef EDITOR_RESOURCESINSPECTOR_H
#define EDITOR_RESOURCESINSPECTOR_H
#include <destrum/Graphics/ids.h>
@@ -32,4 +32,4 @@ private:
ImageID selectedImageId{NULL_IMAGE_ID};
};
#endif //RESOURCESINSPECTOR_H
#endif //EDITOR_RESOURCESINSPECTOR_H
@@ -1,5 +1,5 @@
#ifndef DESTRUM_SCENEINSPECTOR_H
#define DESTRUM_SCENEINSPECTOR_H
#ifndef EDITOR_SCENEINSPECTOR_H
#define EDITOR_SCENEINSPECTOR_H
#include <functional>
#include <string>
@@ -57,4 +57,4 @@ private:
nlohmann::json m_Clipboard;
};
#endif // DESTRUM_SCENEINSPECTOR_H
#endif // EDITOR_SCENEINSPECTOR_H
+33
View File
@@ -0,0 +1,33 @@
#ifndef EDITOR_SHADERHOTRELOAD_H
#define EDITOR_SHADERHOTRELOAD_H
#include <filesystem>
#include <future>
#include <string>
#include <unordered_map>
class ShaderHotReload final {
public:
void Configure(std::filesystem::path cooker, std::filesystem::path sourceRoot, std::filesystem::path runtimeRoot);
[[nodiscard]] bool IsConfigured() const { return !m_Cooker.empty(); }
void RequestCook() { m_CookRequested = true; }
[[nodiscard]] bool Poll(float dt);
[[nodiscard]] bool IsCooking() const { return m_Cook.valid(); }
[[nodiscard]] const std::string& GetLastResult() const { return m_LastResult; }
private:
[[nodiscard]] bool SourcesChanged();
void StartCook();
std::filesystem::path m_Cooker;
std::filesystem::path m_SourceRoot;
std::filesystem::path m_RuntimeRoot;
std::unordered_map<std::string, std::filesystem::file_time_type> m_Stamps;
std::future<int> m_Cook;
std::string m_LastResult;
float m_PollTimer{0.0f};
bool m_CookRequested{false};
};
#endif // EDITOR_SHADERHOTRELOAD_H
@@ -1,5 +1,5 @@
#ifndef DESTRUM_TRANSFORMGIZMO_H
#define DESTRUM_TRANSFORMGIZMO_H
#ifndef EDITOR_TRANSFORMGIZMO_H
#define EDITOR_TRANSFORMGIZMO_H
#include <glm/glm.hpp>
@@ -34,4 +34,4 @@ public:
float scaleSnap{0.1f};
};
#endif // DESTRUM_TRANSFORMGIZMO_H
#endif // EDITOR_TRANSFORMGIZMO_H
+42
View File
@@ -0,0 +1,42 @@
#ifndef EDITOR_UNDOSTACK_H
#define EDITOR_UNDOSTACK_H
#include <cstddef>
#include <deque>
#include <optional>
#include <string>
#include <vector>
#include <destrum/ObjectModel/ObjectId.h>
class Scene;
class UndoStack final {
public:
static constexpr std::size_t MaxSteps = 64;
void Reset(Scene& scene);
void Rebase(Scene& scene);
bool Record(Scene& scene);
bool Undo(Scene& scene);
bool Redo(Scene& scene);
[[nodiscard]] bool CanUndo() const { return !m_Undo.empty(); }
[[nodiscard]] bool CanRedo() const { return !m_Redo.empty(); }
[[nodiscard]] std::size_t UndoCount() const { return m_Undo.size(); }
private:
struct Snapshot {
std::string scene;
ObjectId selected{InvalidObjectId};
};
[[nodiscard]] static std::optional<Snapshot> Capture(Scene& scene);
bool Restore(Scene& scene, const Snapshot& snapshot);
std::deque<Snapshot> m_Undo;
std::vector<Snapshot> m_Redo;
std::optional<Snapshot> m_Current;
};
#endif // EDITOR_UNDOSTACK_H
@@ -1,56 +1,64 @@
#include <destrum/Util/AssetBrowser.h>
#include <editor/AssetBrowser.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <imgui.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Material.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] std::string Lowercase(std::string_view value) {
[[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 HasExtension(std::string_view path, std::initializer_list<std::string_view> extensions) {
const std::string extension = Lowercase(std::filesystem::path{path}.extension().string());
return std::ranges::find(extensions, extension) != extensions.end();
}
// Cooker outputs and bookkeeping sit next to the sources in the runtime folders, nobody picks those
[[nodiscard]] bool IsHidden(const std::filesystem::path& path) {
return path.filename() == "manifest.json" || HasExtension(path.string(), {".dmodel", ".dtex", ".spv"});
[[nodiscard]] bool IsHidden(const std::filesystem::path& path)
{
const std::string name = path.filename().string();
return name == "manifest.json" || name.ends_with(".tmp") ||
AssetTypes::HasExtension(name, {".dmodel", ".dtex", ".spv", AssetTypes::MetaExtension});
}
[[nodiscard]] const FSMount* FindMount(const std::string& scheme) {
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
if (mount.scheme == scheme) {
return &mount;
}
}
return nullptr;
[[nodiscard]] const FSMount* FindMount(const std::string& scheme)
{
const auto& mounts = AssetFS::GetInstance().GetMounts();
const auto it = std::ranges::find(mounts, scheme, &FSMount::scheme);
return it != mounts.end() ? &*it : nullptr;
}
[[nodiscard]] const char* Tag(const std::string& name, bool directory)
{
if (directory) return "[dir] ";
if (AssetTypes::IsModel(name)) return "[model] ";
if (AssetTypes::IsScene(name)) return "[scene] ";
if (AssetTypes::IsPrefab(name)) return "[prefab] ";
if (AssetTypes::IsMaterial(name)) return "[material] ";
if (AssetTypes::IsImage(name)) return "[image] ";
return "";
}
}
bool AssetBrowser::IsModel(std::string_view path) {
return HasExtension(path, {".fbx", ".glb", ".gltf", ".obj"});
}
void AssetBrowser::Draw(bool* open) {
std::optional<std::string> AssetBrowser::Draw(bool* open)
{
std::optional<std::string> opened;
if (!ImGui::Begin("Assets", open)) {
ImGui::End();
return;
return opened;
}
const auto& mounts = AssetFS::GetInstance().GetMounts();
if (mounts.empty()) {
ImGui::TextDisabled("No asset folders are mounted");
ImGui::End();
return;
return opened;
}
if (FindMount(m_Scheme) == nullptr) {
Navigate(mounts.front().scheme, {});
@@ -84,36 +92,47 @@ void AssetBrowser::Draw(bool* open) {
if (ImGui::BeginChild("Entries")) {
const std::string filter = Lowercase(m_Filter);
bool navigated = false;
for (const Entry& entry : m_Entries) {
if ((filter.empty() || Lowercase(entry.name).find(filter) != std::string::npos) && DrawEntry(entry)) {
// Entered a folder, the list is rebuilt next frame
if (filter.empty() || Lowercase(entry.name).find(filter) != std::string::npos) {
DrawEntry(entry, opened, navigated);
}
if (navigated) {
break;
}
}
DrawFolderMenu();
}
ImGui::EndChild();
ImGui::End();
return opened;
}
bool AssetBrowser::DrawEntry(const Entry& entry) {
bool navigated = false;
const std::string virtualPath = m_Scheme + "://" + (m_Folder / entry.name).generic_string();
const std::string label = (entry.directory ? "[dir] " : IsModel(entry.name) ? "[model] " : "") + entry.name;
std::string AssetBrowser::VirtualPath(const std::string& name) const
{
return m_Scheme + "://" + (m_Folder / name).generic_string();
}
void AssetBrowser::DrawEntry(const Entry& entry, std::optional<std::string>& opened, bool& navigated)
{
const std::string virtualPath = VirtualPath(entry.name);
const std::string label = Tag(entry.name, entry.directory) + entry.name;
ImGui::PushID(entry.name.c_str());
if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick) &&
entry.directory && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
Navigate(m_Scheme, m_Folder / entry.name);
navigated = true;
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
if (entry.directory) {
Navigate(m_Scheme, m_Folder / entry.name);
navigated = true;
} else {
opened = virtualPath;
}
}
if (!entry.directory && ImGui::BeginDragDropSource()) {
ImGui::SetDragDropPayload(ImGuiUtils::AssetPayload, virtualPath.c_str(), virtualPath.size() + 1);
ImGui::TextUnformatted(virtualPath.c_str());
if (IsModel(entry.name)) {
ImGui::TextDisabled("Drop it in the viewport to place it");
}
ImGui::EndDragDropSource();
}
@@ -125,17 +144,36 @@ bool AssetBrowser::DrawEntry(const Entry& entry) {
}
ImGui::SetItemTooltip("%s", virtualPath.c_str());
ImGui::PopID();
return navigated;
}
void AssetBrowser::Navigate(const std::string& scheme, const std::filesystem::path& folder) {
void AssetBrowser::DrawFolderMenu()
{
if (!ImGui::BeginPopupContextWindow("FolderMenu", ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) {
return;
}
if (ImGui::MenuItem("New Material")) {
std::string path = VirtualPath("new_material" + std::string{AssetTypes::MaterialExtension});
for (int suffix = 1; std::filesystem::exists(AssetFS::GetInstance().GetFullPath(path)); ++suffix) {
path = VirtualPath("new_material_" + std::to_string(suffix) + std::string{AssetTypes::MaterialExtension});
}
const Material material = Material::SimpleColor(glm::vec3{1.0f}, std::filesystem::path{path}.stem().string());
AssetFS::GetInstance().WriteFile(path, MaterialSerialization::ToFile(material).dump(4));
m_Dirty = true;
}
ImGui::EndPopup();
}
void AssetBrowser::Navigate(const std::string& scheme, const std::filesystem::path& folder)
{
m_Scheme = scheme;
m_Folder = folder;
m_Filter.clear();
m_Dirty = true;
}
void AssetBrowser::Refresh() {
void AssetBrowser::Refresh()
{
m_Dirty = false;
m_Entries.clear();
@@ -146,13 +184,9 @@ void AssetBrowser::Refresh() {
std::error_code error;
for (const auto& item : std::filesystem::directory_iterator(mount->root / m_Folder, error)) {
if (IsHidden(item.path())) {
continue;
if (!IsHidden(item.path())) {
m_Entries.push_back({.name = item.path().filename().string(), .directory = item.is_directory(error)});
}
m_Entries.push_back({
.name = item.path().filename().string(),
.directory = item.is_directory(error),
});
}
std::ranges::sort(m_Entries, [](const Entry& a, const Entry& b) {
+387
View File
@@ -0,0 +1,387 @@
#include <editor/Editor.h>
#include <utility>
#include <imgui.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <ImGuizmo.h>
namespace {
[[nodiscard]] bool AnyKeyReleased()
{
for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
if (ImGui::IsKeyReleased(static_cast<ImGuiKey>(key))) {
return true;
}
}
return false;
}
}
Editor::Editor(App& app, EditorSettings settings)
: m_App(app),
m_Settings(std::move(settings)),
m_PlayMode(app),
m_ScenePath(m_Settings.scenePath)
{
}
void Editor::onAttach()
{
auto& assetFs = AssetFS::GetInstance();
#ifdef DESTRUM_ENGINE_ASSETS_SRC_DIR
assetFs.SetSourceRoot("engine", DESTRUM_ENGINE_ASSETS_SRC_DIR);
#endif
if (!m_Settings.gameAssetsSource.empty()) {
assetFs.SetSourceRoot("game", m_Settings.gameAssetsSource);
}
AssetDatabase::GetInstance().Refresh();
#if defined(DESTRUM_ENGINE_ASSETS_SRC_DIR) && defined(DESTRUM_THECHEF_PATH)
m_ShaderHotReload.Configure(DESTRUM_THECHEF_PATH, DESTRUM_ENGINE_ASSETS_SRC_DIR, assetFs.GetFullPath("engine://shaders").parent_path());
#endif
if (m_Settings.startInEditMode) {
m_PlayMode.EnterEditMode();
}
if (Scene* scene = CurrentScene()) {
m_UndoStack.Reset(*scene);
}
}
Scene* Editor::CurrentScene() const
{
auto& sceneManager = SceneManager::GetInstance();
return sceneManager.GetActiveScenes().empty() ? nullptr : &sceneManager.GetCurrentScene();
}
bool Editor::UsesEditorCamera()
{
return m_PlayMode.IsEditing() || m_PlayMode.IsPaused() || m_App.gameCamera() == nullptr;
}
Camera* Editor::viewCamera()
{
return UsesEditorCamera() ? &m_Camera.GetCamera() : nullptr;
}
AppLayer::InputFocus Editor::gameInputFocus() const
{
return {
.mouse = m_Viewport.hovered && !m_Viewport.mouseClaimed,
.keyboard = m_Viewport.focused || m_Camera.IsLooking(),
};
}
void Editor::onBeginFrame()
{
ApplyViewportResize();
}
void Editor::onImGuiBegin(float dt)
{
DrawDockSpace();
ApplyPick();
HandleShortcuts();
if (UsesEditorCamera()) {
m_Camera.Update(dt, m_Viewport.hovered || m_Viewport.focused || m_Camera.IsLooking());
}
if (m_ShaderHotReload.Poll(dt)) {
const bool reloaded = m_App.getRenderer().reloadShaders(m_App.getGfxDevice());
SetStatus(reloaded ? "Reloaded shaders" : "Shader reload failed, the old shaders stay active");
}
}
void Editor::onImGui(float)
{
DrawMainMenuBar();
DrawSceneDialogs();
DrawViewport();
Scene* scene = CurrentScene();
if (scene != nullptr && m_Panels.inspector) {
ObjectId selected = scene->GetSelectedObjectId();
m_Inspector.Draw(*scene, selected, [scene] { scene->DrawSettingsImGui(); }, &m_Panels.inspector);
scene->SetSelectedObject(selected);
}
if (m_Panels.assets) {
if (const auto opened = m_AssetBrowser.Draw(&m_Panels.assets)) {
OnAssetDropped(*opened);
}
}
if (m_Panels.stats) {
DrawStatsPanel();
}
if (m_Panels.physics) {
DrawPhysicsPanel();
}
if (m_Panels.imageCache) {
m_ResourcesInspector.drawImageCacheWindow(&m_Panels.imageCache, m_App.getResources(), m_App.getImguiPass());
}
if (scene != nullptr && m_View.gizmos && UsesEditorCamera()) {
GameObject* selected = scene->GetSelectedObject();
if (selected != nullptr && !RectGizmo::CanManipulate(*selected)) {
m_TransformGizmo.DrawToolbar();
}
}
}
void Editor::onEndFrame()
{
if (m_PlayMode.ApplyPendingToggle()) {
AfterSceneReplaced(false);
}
const bool itemActive = ImGui::IsAnyItemActive() || ImGuizmo::IsUsing();
const bool gestureEnded = (m_ItemWasActive && !itemActive) || ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
AnyKeyReleased() || std::exchange(m_EditPending, false);
m_ItemWasActive = itemActive;
if (gestureEnded && !itemActive && m_PlayMode.IsEditing()) {
RecordEdits();
}
}
void Editor::onRender(GameRenderer& renderer)
{
const bool picking = m_PlayMode.IsEditing() || m_PlayMode.IsPaused();
renderer.setSelectionPassEnabled(picking);
renderer.setHoverPixel(picking ? m_Viewport.hoverPixel : std::nullopt);
std::vector<std::uint32_t> highlighted;
Scene* scene = CurrentScene();
if (scene == nullptr) {
renderer.setHighlightedObjects(highlighted);
return;
}
if (GameObject* selected = scene->GetSelectedObject(); selected != nullptr && picking) {
std::vector<GameObject*> pending{selected};
while (!pending.empty()) {
GameObject* object = pending.back();
pending.pop_back();
highlighted.push_back(static_cast<std::uint32_t>(object->GetId()));
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
}
}
renderer.setHighlightedObjects(highlighted);
if (m_View.colliders) {
SubmitColliderLines(*scene);
}
}
void Editor::HandleShortcuts()
{
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) {
m_PlayMode.RequestToggle();
}
if (ImGui::IsKeyPressed(ImGuiKey_F7, false) && m_PlayMode.IsPlaying()) {
m_PlayMode.SetPaused(!m_PlayMode.IsPaused());
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S, false)) {
SaveScene();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Z)) {
io.KeyShift ? Redo() : Undo();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Y)) {
Redo();
}
if (!m_Viewport.focused && !m_Viewport.hovered) {
return;
}
if (UsesEditorCamera() && !m_Camera.IsLooking()) {
m_TransformGizmo.HandleShortcuts();
}
if (ImGui::IsKeyPressed(ImGuiKey_F, false) && !io.KeyCtrl) {
FocusSelection();
}
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
DeleteSelection();
}
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_D, false)) {
DuplicateSelection();
}
}
void Editor::Undo()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
if (m_UndoStack.Undo(*scene)) {
m_App.collectUnusedResources();
m_App.onPlayModeChanged(false);
}
}
void Editor::Redo()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
if (m_UndoStack.Redo(*scene)) {
m_App.collectUnusedResources();
m_App.onPlayModeChanged(false);
}
}
void Editor::RecordEdits()
{
if (Scene* scene = CurrentScene()) {
m_UndoStack.Record(*scene);
}
}
void Editor::AfterSceneReplaced(bool resetHistory)
{
m_App.collectUnusedResources();
Scene* scene = CurrentScene();
if (scene == nullptr) {
return;
}
if (resetHistory) {
m_UndoStack.Reset(*scene);
} else {
m_UndoStack.Rebase(*scene);
}
}
void Editor::DeleteSelection()
{
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (selected == nullptr) {
return;
}
selected->Destroy();
scene->SetSelectedObject(InvalidObjectId);
m_EditPending = true;
}
void Editor::DuplicateSelection()
{
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (selected == nullptr) {
return;
}
if (const GameObject* copy = PrefabSerializer::Duplicate(*selected)) {
scene->SetSelectedObject(copy->GetId());
m_EditPending = true;
}
}
void Editor::FocusSelection()
{
Scene* scene = CurrentScene();
if (GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr) {
m_Camera.Focus(ComputeWorldBounds(*selected, m_App.getResources()));
}
}
void Editor::SetStatus(std::string status)
{
spdlog::info("{}", status);
m_Status = std::move(status);
}
bool Editor::WriteScene(Scene& scene, std::string_view path)
{
const AssetDatabase::MetaFileScope createMetaFiles;
const auto root = SceneSerializer::Capture(scene);
if (!root || !AssetFS::GetInstance().WriteFile(path, root->dump(4))) {
SetStatus("Could not save the scene to " + std::string{path});
return false;
}
SetStatus("Saved " + std::string{path});
return true;
}
bool Editor::SaveScene()
{
if (!m_PlayMode.IsEditing()) {
SetStatus("Stop play mode before saving, the scene is the play copy right now");
return false;
}
if (m_ScenePath.empty()) {
m_SaveAsDialog = true;
return false;
}
Scene* scene = CurrentScene();
return scene != nullptr && WriteScene(*scene, m_ScenePath);
}
bool Editor::SaveSceneAs(std::string_view path)
{
std::string target{path};
if (!AssetTypes::IsScene(target)) {
target += AssetTypes::SceneExtension;
}
m_ScenePath = target;
return SaveScene();
}
bool Editor::OpenScene(std::string_view path)
{
Scene* scene = CurrentScene();
if (scene == nullptr) {
return false;
}
if (!m_PlayMode.IsEditing()) {
SetStatus("Stop play mode before opening another scene");
return false;
}
if (!SceneSerializer::Load(*scene, AssetFS::GetInstance().GetFullPath(path))) {
SetStatus("Could not open " + std::string{path});
return false;
}
m_ScenePath = path;
m_PlayMode.EnterEditMode();
AfterSceneReplaced(true);
SetStatus("Opened " + std::string{path});
return true;
}
void Editor::NewScene()
{
Scene* scene = CurrentScene();
if (scene == nullptr || !m_PlayMode.IsEditing()) {
return;
}
const nlohmann::json empty{{"name", "Untitled"}, {"objects", nlohmann::json::array()}};
if (SceneSerializer::LoadFromJson(*scene, empty)) {
m_ScenePath.clear();
m_PlayMode.EnterEditMode();
AfterSceneReplaced(true);
}
}
+99
View File
@@ -0,0 +1,99 @@
#include <editor/EditorCamera.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
WorldBounds ComputeWorldBounds(GameObject& object, const RenderResources& resources)
{
glm::vec3 boundsMin{std::numeric_limits<float>::max()};
glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
std::vector<GameObject*> pending{&object};
while (!pending.empty()) {
GameObject* current = pending.back();
pending.pop_back();
for (const Transform* child : current->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
const auto* meshRenderer = current->GetComponent<MeshRendererComponent>();
if (meshRenderer == nullptr || !resources.meshes().isValid(meshRenderer->GetMeshID())) {
continue;
}
const GPUMesh& mesh = resources.meshes().getMesh(meshRenderer->GetMeshID());
const glm::mat4 world = current->GetTransform().GetWorldMatrix();
for (int corner = 0; corner < 8; ++corner) {
const glm::vec3 local{
corner & 1 ? mesh.maxPos.x : mesh.minPos.x,
corner & 2 ? mesh.maxPos.y : mesh.minPos.y,
corner & 4 ? mesh.maxPos.z : mesh.minPos.z,
};
const glm::vec3 point{world * glm::vec4{local, 1.0f}};
boundsMin = glm::min(boundsMin, point);
boundsMax = glm::max(boundsMax, point);
}
}
if (boundsMin.x > boundsMax.x) {
return {.center = object.GetTransform().GetWorldPosition(), .radius = 1.0f};
}
return {
.center = (boundsMin + boundsMax) * 0.5f,
.radius = std::max(glm::length(boundsMax - boundsMin) * 0.5f, 0.1f),
};
}
EditorCamera::EditorCamera()
: m_Camera{glm::vec3{0.0f, 2.0f, -6.0f}, glm::vec3{0.0f, 1.0f, 0.0f}}
{
m_Camera.SetRotation(glm::radians(glm::vec2{90.0f, -10.0f}));
}
void EditorCamera::Update(float dt, bool hasInput)
{
auto& input = InputManager::GetInstance();
const bool looking = hasInput && input.IsMouseDown(SDL_BUTTON_RIGHT);
if (looking != m_Looking) {
m_Looking = looking;
input.SetMouseCaptured(looking);
}
if (m_FocusTarget) {
if (looking) {
m_FocusTarget.reset();
} else {
m_Camera.m_position = glm::mix(m_Camera.m_position, *m_FocusTarget, 1.0f - std::exp(-12.0f * dt));
if (glm::distance(m_Camera.m_position, *m_FocusTarget) < 0.01f) {
m_Camera.m_position = *m_FocusTarget;
m_FocusTarget.reset();
}
}
}
if (hasInput) {
m_Camera.Update(dt);
} else {
m_Camera.CalculateViewMatrix();
}
}
void EditorCamera::Focus(const WorldBounds& bounds)
{
// Fit the bounding sphere in the narrower of the two field of view angles
const float halfFovY = glm::radians(m_Camera.GetFOVY()) * 0.5f;
const float halfFovX = std::atan(std::tan(halfFovY) * m_Camera.GetAspectRatio());
const float distance = bounds.radius / std::sin(std::min(halfFovX, halfFovY)) * 1.1f;
m_FocusTarget = bounds.center - m_Camera.GetForward() * distance;
}
+374
View File
@@ -0,0 +1,374 @@
#include <editor/Editor.h>
#include <algorithm>
#include <array>
#include <filesystem>
#include <string>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] std::vector<std::string> FindScenes()
{
std::vector<std::string> scenes;
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
std::error_code error;
for (const auto& entry : std::filesystem::recursive_directory_iterator(mount.root, error)) {
if (entry.is_regular_file(error) && AssetTypes::IsScene(entry.path().string())) {
scenes.push_back(mount.scheme + "://" + entry.path().lexically_relative(mount.root).generic_string());
}
}
}
std::ranges::sort(scenes);
return scenes;
}
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
const bool hasSavedLayout = ImGui::DockBuilderGetNode(dockspaceId) != nullptr;
const ImGuiViewport* mainViewport = ImGui::GetMainViewport();
ImGui::DockSpaceOverViewport(dockspaceId, mainViewport);
if (hasSavedLayout) {
return;
}
// First run without an imgui.ini: scene in the middle, inspector right, tools left, assets below
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, mainViewport->WorkSize);
ImGuiID center = dockspaceId;
const ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.25f, nullptr, &center);
const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, &center);
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, &center);
ImGui::DockBuilderDockWindow("Viewport", center);
ImGui::DockBuilderDockWindow("Inspector", right);
ImGui::DockBuilderDockWindow("Stats", left);
ImGui::DockBuilderDockWindow("Physics", left);
ImGui::DockBuilderDockWindow("Assets", bottom);
ImGui::DockBuilderFinish(dockspaceId);
}
void Editor::DrawMainMenuBar()
{
if (!ImGui::BeginMainMenuBar()) {
return;
}
DrawFileMenu();
DrawEditMenu();
DrawViewMenu();
DrawDisplayMenu();
DrawPlayControls();
ImGui::EndMainMenuBar();
}
void Editor::DrawFileMenu()
{
if (!ImGui::BeginMenu("File")) {
return;
}
const bool editing = m_PlayMode.IsEditing();
if (ImGui::MenuItem("New Scene", nullptr, false, editing)) {
NewScene();
}
if (ImGui::MenuItem("Open Scene...", nullptr, false, editing)) {
m_OpenSceneDialog = true;
}
if (ImGui::MenuItem("Save Scene", "Ctrl+S", false, editing)) {
SaveScene();
}
if (ImGui::MenuItem("Save Scene As...", nullptr, false, editing)) {
m_SaveAsDialog = true;
}
ImGui::Separator();
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (ImGui::MenuItem("Save Selection as Prefab", nullptr, false, selected != nullptr)) {
const std::string path = "game://prefabs/" + selected->GetName() + std::string{AssetTypes::PrefabExtension};
const auto prefab = PrefabSerializer::Capture(*selected);
SetStatus(prefab && AssetFS::GetInstance().WriteFile(path, prefab->dump(4))
? "Saved " + path + ", drag it from Assets into the viewport to place it"
: "Could not save " + path);
}
ImGui::Separator();
if (ImGui::MenuItem("Quit")) {
m_App.requestQuit();
}
ImGui::Separator();
ImGui::TextDisabled("%s", m_ScenePath.empty() ? "Unsaved scene" : m_ScenePath.c_str());
if (!m_Status.empty()) {
ImGui::TextWrapped("%s", m_Status.c_str());
}
ImGui::EndMenu();
}
void Editor::DrawEditMenu()
{
if (!ImGui::BeginMenu("Edit")) {
return;
}
const bool editing = m_PlayMode.IsEditing();
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, editing && m_UndoStack.CanUndo())) {
Undo();
}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, editing && m_UndoStack.CanRedo())) {
Redo();
}
ImGui::Separator();
Scene* scene = CurrentScene();
const bool hasSelection = scene != nullptr && scene->GetSelectedObject() != nullptr;
if (ImGui::MenuItem("Duplicate", "Ctrl+D", false, hasSelection)) {
DuplicateSelection();
}
if (ImGui::MenuItem("Delete", "Del", false, hasSelection)) {
DeleteSelection();
}
if (ImGui::MenuItem("Focus Selection", "F", false, hasSelection)) {
FocusSelection();
}
ImGui::EndMenu();
}
void Editor::DrawViewMenu()
{
if (!ImGui::BeginMenu("View")) {
return;
}
ImGui::MenuItem("Inspector", nullptr, &m_Panels.inspector);
ImGui::MenuItem("Assets", nullptr, &m_Panels.assets);
ImGui::MenuItem("Stats", nullptr, &m_Panels.stats);
ImGui::MenuItem("Physics", nullptr, &m_Panels.physics);
ImGui::MenuItem("Image Cache", nullptr, &m_Panels.imageCache);
m_App.customWindowsMenu();
ImGui::SeparatorText("Scene View");
GameRenderer& renderer = m_App.getRenderer();
ImGui::MenuItem("Gizmos", nullptr, &m_View.gizmos);
ImGui::MenuItem("Colliders", nullptr, &m_View.colliders);
if (bool wireframe = renderer.getRenderWireframe(); ImGui::MenuItem("Wireframe", nullptr, &wireframe)) {
renderer.setRenderWireframe(wireframe);
}
if (bool heatmap = renderer.getRenderLightHeatmap(); ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &heatmap)) {
renderer.setRenderLightHeatmap(heatmap);
}
if (bool selectionView = renderer.getRenderSelectionView(); ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
renderer.setRenderSelectionView(selectionView);
}
if (Scene* scene = CurrentScene(); scene != nullptr && ImGui::BeginMenu("Play Camera")) {
const auto cameras = scene->GetCameras();
if (cameras.empty()) {
ImGui::TextDisabled("No cameras in the scene");
}
for (CameraComponent* camera : cameras) {
const GameObject* owner = camera->GetGameObject();
ImGui::PushID(owner);
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, camera->IsCurrent())) {
scene->SetCurrentCamera(camera);
}
ImGui::PopID();
}
ImGui::EndMenu();
}
ImGui::Separator();
const bool cooking = m_ShaderHotReload.IsCooking();
if (ImGui::MenuItem(cooking ? "Cooking Shaders..." : "Reload Shaders", nullptr, false, m_ShaderHotReload.IsConfigured() && !cooking)) {
m_ShaderHotReload.RequestCook();
}
if (ImGui::MenuItem("Free Unused Resources")) {
m_App.collectUnusedResources();
}
ImGui::EndMenu();
}
void Editor::DrawDisplayMenu()
{
if (!ImGui::BeginMenu("Display")) {
return;
}
Window& window = m_App.getWindow();
const WindowMode mode = window.GetMode();
const bool fullscreen = window.IsFullscreen();
const bool maximized = window.IsMaximized();
if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) {
window.ToggleFullscreen();
}
if (ImGui::BeginMenu("Mode")) {
if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) {
window.SetMode(WindowMode::Windowed);
}
if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) {
window.SetMode(WindowMode::BorderlessFullscreen);
}
if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) {
window.SetMode(WindowMode::Fullscreen);
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) {
window.ToggleMaximized();
}
if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) {
window.Center();
}
if (ImGui::BeginMenu("Size", !fullscreen)) {
constexpr std::array sizes{glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}};
for (const glm::ivec2& size : sizes) {
const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y);
if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) {
if (maximized) {
window.Restore();
}
window.SetSize(size);
window.Center();
}
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
void Editor::DrawPlayControls()
{
const bool playing = m_PlayMode.IsPlaying();
const bool paused = m_PlayMode.IsPaused();
const char* state = !playing ? "EDIT" : paused ? "PAUSED" : "PLAYING";
const char* action = playing ? "Stop (F5)" : "Play (F5)";
const char* pauseAction = paused ? "Resume (F7)" : "Pause (F7)";
const ImGuiStyle& style = ImGui::GetStyle();
float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x + style.ItemSpacing.x * 2.0f +
style.FramePadding.x * 2.0f;
if (playing) {
width += ImGui::CalcTextSize(pauseAction).x + style.ItemSpacing.x + style.FramePadding.x * 2.0f;
}
// Centered like Godot's run buttons, unless the menus already reach past the middle
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), (ImGui::GetWindowWidth() - width) * 0.5f));
const ImVec4 stateColor = !playing ? ImVec4{0.7f, 0.7f, 0.7f, 1.0f}
: paused ? ImVec4{0.95f, 0.75f, 0.3f, 1.0f}
: ImVec4{0.4f, 0.9f, 0.4f, 1.0f};
ImGui::TextColored(stateColor, "%s", state);
if (playing && ImGui::MenuItem(pauseAction)) {
m_PlayMode.SetPaused(!paused);
}
if (ImGui::MenuItem(action)) {
m_PlayMode.RequestToggle();
}
}
void Editor::DrawSceneDialogs()
{
if (std::exchange(m_OpenSceneDialog, false)) {
ImGui::OpenPopup("Open Scene");
}
if (std::exchange(m_SaveAsDialog, false)) {
m_SaveAsPath = m_ScenePath.empty() ? "game://scenes/untitled.scene" : m_ScenePath;
ImGui::OpenPopup("Save Scene As");
}
if (ImGui::BeginPopupModal("Open Scene", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
const std::vector<std::string> scenes = FindScenes();
if (scenes.empty()) {
ImGui::TextDisabled("No .scene files in the mounted asset folders");
}
for (const std::string& path : scenes) {
if (ImGui::Selectable(path.c_str(), path == m_ScenePath)) {
OpenScene(path);
ImGui::CloseCurrentPopup();
}
}
ImGui::Separator();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Save Scene As", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SetNextItemWidth(420.0f);
const bool submitted = ImGuiUtils::InputString("##Path", m_SaveAsPath, ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::TextDisabled("A virtual path like game://scenes/level.scene");
if (ImGui::Button("Save") || submitted) {
SaveSceneAs(m_SaveAsPath);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void Editor::DrawStatsPanel()
{
if (ImGui::Begin("Stats", &m_Panels.stats)) {
ImGui::Text("FPS: %.1f", m_App.getAverageFps());
const auto& stats = m_App.getRenderer().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::SeparatorText("Memory");
ImGui::Text("Pending GPU deletions: %zu", m_App.getGfxDevice().getPendingDeletionCount());
ImGui::Text("Undo steps: %zu", m_UndoStack.UndoCount());
}
ImGui::End();
}
void Editor::DrawPhysicsPanel()
{
if (!ImGui::Begin("Physics", &m_Panels.physics)) {
ImGui::End();
return;
}
App::SimulationControls& simulation = m_App.getSimulation();
ImGui::Text("Fixed dt: %.3f ms", simulation.fixedDeltaTime * 1000.0f);
ImGui::Text("Accumulator: %.3f ms", simulation.accumulator * 1000.0f);
ImGui::Text("Steps last frame: %d", simulation.stepsLastFrame);
ImGui::Separator();
ImGui::Checkbox("Paused", &simulation.paused);
ImGui::SameLine();
ImGui::BeginDisabled(!simulation.paused);
if (ImGui::Button("Step")) {
simulation.stepOnce = true;
}
ImGui::EndDisabled();
ImGui::SliderFloat("Time Scale", &simulation.timeScale, 0.0f, 2.0f, "%.2fx");
if (ImGui::Button("Reset Time Scale")) {
simulation.timeScale = 1.0f;
}
ImGui::End();
}
+239
View File
@@ -0,0 +1,239 @@
#include <editor/Editor.h>
#include <algorithm>
#include <filesystem>
#include <vector>
#include <imgui.h>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/ModelSpawner.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
#include <destrum/Util/ModelDoc.h>
void Editor::ApplyViewportResize()
{
const glm::ivec2 desired = m_Viewport.desiredSize;
const VkExtent3D current = m_App.getRenderer().getDrawImage().extent;
if (desired.x <= 0 || desired.y <= 0 ||
(static_cast<int>(current.width) == desired.x && static_cast<int>(current.height) == desired.y)) {
return;
}
// While a dock splitter is dragged the old image is stretched, recreating it every frame is wasteful
if (ImGui::GetIO().MouseDown[ImGuiMouseButton_Left]) {
return;
}
m_App.resizeRenderTarget(desired);
}
void Editor::DrawViewport()
{
m_Viewport.mouseClaimed = false;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0.0f, 0.0f});
const bool visible = ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
if (!visible) {
m_Viewport.hovered = false;
m_Viewport.focused = false;
m_Viewport.hoverPixel.reset();
ImGui::End();
return;
}
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 available = ImGui::GetContentRegionAvail();
const ImVec2 framebufferScale = ImGui::GetIO().DisplayFramebufferScale;
m_Viewport.min = {origin.x, origin.y};
m_Viewport.size = {std::max(available.x, 1.0f), std::max(available.y, 1.0f)};
m_Viewport.desiredSize = {
std::max(static_cast<int>(available.x * framebufferScale.x), 1),
std::max(static_cast<int>(available.y * framebufferScale.y), 1),
};
const GPUImage& drawImage = m_App.getRenderer().getDrawImage();
if (const ImTextureID texture = m_App.getImguiPass().getFrameTexture(drawImage.imageView); texture != ImTextureID{}) {
ImGui::Image(texture, ImVec2{m_Viewport.size.x, m_Viewport.size.y});
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
OnAssetDropped(static_cast<const char*>(payload->Data));
}
ImGui::EndDragDropTarget();
}
}
m_Viewport.hovered = ImGui::IsWindowHovered();
// ImGui only focuses windows on a left click, the camera looks around with the right button
if (m_Viewport.hovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Right) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle))) {
ImGui::SetWindowFocus();
}
m_Viewport.focused = ImGui::IsWindowFocused();
const ImVec2 mouse = ImGui::GetMousePos();
const glm::vec2 uv = (glm::vec2{mouse.x, mouse.y} - m_Viewport.min) / m_Viewport.size;
const bool insideImage = uv.x >= 0.0f && uv.y >= 0.0f && uv.x < 1.0f && uv.y < 1.0f;
m_Viewport.hoverPixel.reset();
if (m_Viewport.hovered && insideImage && !m_Camera.IsLooking()) {
const VkExtent3D extent = drawImage.extent;
m_Viewport.hoverPixel = glm::ivec2{uv * glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)}};
}
ImGui::SetCursorScreenPos(origin);
if (Scene* scene = CurrentScene(); scene != nullptr && m_View.gizmos && UsesEditorCamera()) {
DrawGizmos(*scene);
}
RequestPick();
ImGui::End();
}
void Editor::DrawGizmos(Scene& scene)
{
GameObject* selected = scene.GetSelectedObject();
if (selected == nullptr) {
return;
}
if (RectGizmo::CanManipulate(*selected)) {
const VkExtent3D extent = m_App.getRenderer().getDrawImage().extent;
m_RectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)},
m_Viewport.min, m_Viewport.size);
m_Viewport.mouseClaimed = m_Viewport.mouseClaimed || m_RectGizmo.IsInteracting();
return;
}
m_TransformGizmo.Manipulate(*selected, m_Camera.GetCamera(), m_Viewport.min, m_Viewport.size);
m_Viewport.mouseClaimed = m_Viewport.mouseClaimed || m_TransformGizmo.IsOver() || m_TransformGizmo.IsUsing();
}
void Editor::RequestPick()
{
const bool picking = m_PlayMode.IsEditing() || m_PlayMode.IsPaused();
if (!picking || !m_Viewport.hoverPixel || m_Viewport.mouseClaimed || !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
return;
}
m_App.getRenderer().requestSelectionPick(*m_Viewport.hoverPixel);
m_Viewport.pickSelectsParent = ImGui::GetIO().KeyAlt;
}
void Editor::ApplyPick()
{
const auto picked = m_App.getRenderer().takeSelectionPick();
Scene* scene = CurrentScene();
if (!picked || scene == nullptr) {
return;
}
GameObject* object = scene->FindGameObject(*picked);
if (object != nullptr && m_Viewport.pickSelectsParent) {
if (const Transform* parent = object->GetTransform().GetParent(); parent != nullptr && parent->GetOwner() != nullptr) {
object = parent->GetOwner();
}
}
scene->SetSelectedObject(object != nullptr ? object->GetId() : InvalidObjectId);
}
void Editor::OnAssetDropped(std::string_view path)
{
Scene* scene = CurrentScene();
if (scene == nullptr) {
return;
}
if (AssetTypes::IsScene(path)) {
OpenScene(path);
return;
}
const Camera& view = m_Camera.GetCamera();
const glm::vec3 spawnPosition = view.GetPosition() + view.GetForward() * 5.0f;
const std::string name = std::filesystem::path{path}.stem().string();
try {
if (AssetTypes::IsModel(path)) {
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
GameObject* model = ModelSpawner::SpawnStaticModel(
*scene, m_App.getGfxDevice(), m_App.getResources(), path, options, name);
model->GetTransform().SetWorldPosition(spawnPosition);
scene->SetSelectedObject(model->GetId());
} else if (AssetTypes::IsPrefab(path)) {
GameObject* instance = PrefabSerializer::Instantiate(*scene, AssetFS::GetInstance().GetFullPath(path).string());
if (instance == nullptr) {
SetStatus("Could not instantiate " + std::string{path});
return;
}
instance->GetTransform().SetWorldPosition(spawnPosition);
scene->SetSelectedObject(instance->GetId());
} else if (AssetTypes::IsMaterial(path)) {
GameObject* selected = scene->GetSelectedObject();
if (selected == nullptr) {
SetStatus("Select an object to give it " + std::string{path});
return;
}
const MaterialID material = MaterialSerialization::LoadFile(m_App.getResources(), m_App.getGfxDevice(), path);
std::vector<GameObject*> pending{selected};
while (!pending.empty()) {
GameObject* object = pending.back();
pending.pop_back();
if (auto* meshRenderer = object->GetComponent<MeshRendererComponent>()) {
meshRenderer->SetMaterialID(material);
}
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
}
} else {
return;
}
m_EditPending = true;
} catch (const std::exception& exception) {
spdlog::error("Could not use {}: {}", path, exception.what());
SetStatus("Could not use " + std::string{path});
}
}
void Editor::SubmitColliderLines(Scene& scene) const
{
// Triggers orange, then colored by body type: static blue, dynamic green, kinematic purple
for (const auto& object : scene.GetObjects()) {
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
continue;
}
const auto* rigidbody = object->GetComponent<Rigidbody>();
const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static;
for (const auto& component : object->GetComponents()) {
const auto* collider = dynamic_cast<const Collider*>(component.get());
if (collider == nullptr || collider->IsBeingDestroyed() || !collider->isEnabled()) {
continue;
}
glm::vec4 color{0.1f, 0.8f, 1.0f, 1.0f};
if (collider->IsTrigger()) {
color = {1.0f, 0.75f, 0.1f, 1.0f};
} else if (type == RigidbodyType::Dynamic) {
color = {0.3f, 1.0f, 0.3f, 1.0f};
} else if (type == RigidbodyType::Kinematic) {
color = {0.8f, 0.4f, 1.0f, 1.0f};
}
collider->DrawDebugLines(color);
}
}
}
+118
View File
@@ -0,0 +1,118 @@
#include <editor/PlayMode.h>
#include <algorithm>
#include <spdlog/spdlog.h>
#include <destrum/App.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h>
PlayMode::PlayMode(App& app)
: m_App(app)
{
}
void PlayMode::EnterEditMode()
{
m_Playing = false;
m_Paused = false;
SceneManager::GetInstance().SetPlaying(false);
SceneManager::GetInstance().SetPaused(false);
InputManager::GetInstance().SetMouseCaptured(false);
m_App.onPlayModeChanged(false);
}
void PlayMode::SetPaused(bool paused)
{
if (!m_Playing || paused == m_Paused) {
return;
}
m_Paused = paused;
SceneManager::GetInstance().SetPaused(paused);
// Hand the mouse to the editor while paused, give it back to the game on resume
auto& input = InputManager::GetInstance();
if (paused) {
m_MouseCapturedBeforePause = input.IsMouseCaptured();
input.SetMouseCaptured(false);
} else {
input.SetMouseCaptured(m_MouseCapturedBeforePause);
}
}
bool PlayMode::ApplyPendingToggle()
{
if (!m_ToggleRequested) {
return false;
}
m_ToggleRequested = false;
if (m_Playing) {
Exit();
return true;
}
return Enter();
}
bool PlayMode::Enter()
{
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
spdlog::error("Cannot enter play mode without an active scene");
return false;
}
const std::shared_ptr<Scene> scene = sceneManager.GetActiveScenes().front();
auto snapshot = SceneSerializer::Capture(*scene);
if (!snapshot) {
spdlog::error("Cannot enter play mode, scene \"{}\" could not be saved", scene->GetName());
return false;
}
if (const GameObject* selected = scene->GetSelectedObject()) {
(*snapshot)["selected"] = selected->GetId();
}
// Play runs the scene as it would load from disk, so nothing the editor did to the live objects
// (started components, unsaved runtime state) leaks into the game
if (!SceneSerializer::LoadFromJson(*scene, *snapshot)) {
spdlog::error("Cannot enter play mode, scene \"{}\" did not load back", scene->GetName());
return false;
}
m_EditSnapshot = std::move(snapshot);
m_EditScene = scene;
m_Playing = true;
sceneManager.SetPlaying(true);
m_App.collectUnusedResources();
m_App.onPlayModeChanged(true);
spdlog::info("Entered play mode");
return true;
}
void PlayMode::Exit()
{
auto& sceneManager = SceneManager::GetInstance();
if (const std::shared_ptr<Scene> scene = m_EditScene.lock(); scene && m_EditSnapshot) {
const auto& scenes = sceneManager.GetScenes();
if (const auto it = std::ranges::find(scenes, scene);
it != scenes.end() && sceneManager.GetActiveScenes().front() != scene) {
sceneManager.SwitchScene(static_cast<int>(std::distance(scenes.begin(), it)));
}
if (!SceneSerializer::LoadFromJson(*scene, *m_EditSnapshot)) {
spdlog::error("Could not restore scene \"{}\" to its state before play mode", scene->GetName());
}
} else {
spdlog::warn("The scene play mode started from no longer exists, nothing to restore");
}
m_EditSnapshot.reset();
m_EditScene.reset();
EnterEditMode();
m_App.collectUnusedResources();
spdlog::info("Exited play mode");
}
@@ -1,4 +1,4 @@
#include <destrum/Util/RectGizmo.h>
#include <editor/RectGizmo.h>
#include <array>
#include <cmath>
@@ -1,4 +1,4 @@
#include <destrum/Graphics/ResourcesInspector.h>
#include <editor/ResourcesInspector.h>
#include <algorithm>
#include <cctype>
@@ -1,4 +1,4 @@
#include <destrum/Util/SceneInspector.h>
#include <editor/SceneInspector.h>
#include <algorithm>
#include <cctype>
+83
View File
@@ -0,0 +1,83 @@
#include <editor/ShaderHotReload.h>
#include <chrono>
#include <cstdlib>
#include <utility>
#include <spdlog/spdlog.h>
namespace {
constexpr float PollInterval = 0.5f;
[[nodiscard]] bool IsShaderSource(const std::filesystem::path& path)
{
const std::string extension = path.extension().string();
return extension == ".vert" || extension == ".frag" || extension == ".comp" || extension == ".glsl";
}
}
void ShaderHotReload::Configure(std::filesystem::path cooker, std::filesystem::path sourceRoot, std::filesystem::path runtimeRoot)
{
m_Cooker = std::move(cooker);
m_SourceRoot = std::move(sourceRoot);
m_RuntimeRoot = std::move(runtimeRoot);
m_Stamps.clear();
(void)SourcesChanged();
}
bool ShaderHotReload::SourcesChanged()
{
bool changed = false;
std::error_code error;
for (const auto& entry : std::filesystem::recursive_directory_iterator(m_SourceRoot / "shaders", error)) {
if (!entry.is_regular_file(error) || !IsShaderSource(entry.path())) {
continue;
}
const auto stamp = entry.last_write_time(error);
auto [it, inserted] = m_Stamps.try_emplace(entry.path().generic_string(), stamp);
if (!inserted && it->second != stamp) {
it->second = stamp;
changed = true;
}
}
return changed;
}
void ShaderHotReload::StartCook()
{
// cmd.exe strips the outer quotes, the inner ones keep paths with spaces intact
const std::string command = "\"\"" + m_Cooker.string() + "\" --input \"" + m_SourceRoot.string() +
"\" --output \"" + m_RuntimeRoot.string() + "\"\"";
m_Cook = std::async(std::launch::async, [command] { return std::system(command.c_str()); });
spdlog::info("Shader sources changed, cooking engine assets");
}
bool ShaderHotReload::Poll(float dt)
{
if (!IsConfigured()) {
return false;
}
if (m_Cook.valid()) {
if (m_Cook.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
return false;
}
const int exitCode = m_Cook.get();
m_LastResult = exitCode == 0 ? "Shaders cooked" : "Shader cook failed, see the log";
if (exitCode != 0) {
spdlog::error("Cooking the engine assets failed with exit code {}", exitCode);
}
return exitCode == 0;
}
m_PollTimer -= dt;
if (m_PollTimer > 0.0f && !m_CookRequested) {
return false;
}
m_PollTimer = PollInterval;
if (SourcesChanged() || std::exchange(m_CookRequested, false)) {
StartCook();
}
return false;
}
@@ -1,4 +1,4 @@
#include <destrum/Util/TransformGizmo.h>
#include <editor/TransformGizmo.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/ObjectModel/GameObject.h>
+95
View File
@@ -0,0 +1,95 @@
#include <editor/UndoStack.h>
#include <utility>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/SceneSerializer.h>
std::optional<UndoStack::Snapshot> UndoStack::Capture(Scene& scene)
{
const auto root = SceneSerializer::Capture(scene);
if (!root) {
return std::nullopt;
}
return Snapshot{.scene = root->dump(), .selected = scene.GetSelectedObjectId()};
}
void UndoStack::Reset(Scene& scene)
{
m_Undo.clear();
m_Redo.clear();
m_Current = Capture(scene);
}
void UndoStack::Rebase(Scene& scene)
{
m_Current = Capture(scene);
}
bool UndoStack::Record(Scene& scene)
{
auto now = Capture(scene);
if (!now) {
return false;
}
if (!m_Current || now->scene == m_Current->scene) {
m_Current = std::move(now);
return false;
}
// Undo keeps what is selected now, the ids are still the same because nothing was reloaded since
m_Current->selected = now->selected;
m_Undo.push_back(std::move(*m_Current));
if (m_Undo.size() > MaxSteps) {
m_Undo.pop_front();
}
m_Redo.clear();
m_Current = std::move(now);
return true;
}
bool UndoStack::Undo(Scene& scene)
{
Record(scene);
if (m_Undo.empty() || !m_Current) {
return false;
}
Snapshot target = std::move(m_Undo.back());
m_Undo.pop_back();
m_Redo.push_back(std::move(*m_Current));
return Restore(scene, target);
}
bool UndoStack::Redo(Scene& scene)
{
Record(scene);
if (m_Redo.empty() || !m_Current) {
return false;
}
Snapshot target = std::move(m_Redo.back());
m_Redo.pop_back();
m_Undo.push_back(std::move(*m_Current));
return Restore(scene, target);
}
bool UndoStack::Restore(Scene& scene, const Snapshot& snapshot)
{
nlohmann::json root = nlohmann::json::parse(snapshot.scene);
if (snapshot.selected != InvalidObjectId) {
root["selected"] = snapshot.selected;
}
if (!SceneSerializer::LoadFromJson(scene, root)) {
spdlog::error("Could not restore the scene from the undo history");
m_Current = Capture(scene);
return false;
}
// Loading gives every object a new id, the next comparison has to start from the new ids
m_Current = Capture(scene);
return true;
}
+2 -3
View File
@@ -6,9 +6,7 @@ set(GAME_SRC
src/main.cpp
src/Lightkeeper.cpp
src/LightkeeperDebugUi.cpp
src/scenes/MainScene.cpp
src/scenes/Experiments.cpp
src/components/GameComponentRegistry.cpp
@@ -40,7 +38,8 @@ set_target_properties(lightkeeper PROPERTIES
target_include_directories(lightkeeper PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_link_libraries(lightkeeper PRIVATE destrum::destrum)
target_link_libraries(lightkeeper PRIVATE destrum::destrum destrum::editor)
target_compile_definitions(lightkeeper PRIVATE LIGHTKEEPER_ASSETS_SRC_DIR="${CMAKE_CURRENT_LIST_DIR}/assets_src")
#set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
#set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_dark",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.9,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Dark/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "7d9c3cd4034fc8c13448d08ced3e4432"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_green",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.6,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Green/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "af12f3099fa89356d6a2db0f7b064312"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_light",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.5,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Light/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "6cf69d2191896c70166cc3694a778f0c"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_light_matte",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.8,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Light/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "5baf128c1cac1369814fe1170257272d"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_orange",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.6,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Orange/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "889deecef8a1972835f601442a7d9c7b"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_orange_matte",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.7,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Orange/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "e9a0ccff690986f920184af94ef4825f"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_purple",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.5,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Purple/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "cbc24f8efba0e64df911802de71d03af"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_purple_glossy",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.35,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Purple/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "73a209afd28c56cb4c97b0ca5b99b45d"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_red",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.6,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Red/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "f8c2d0e8b72b4fa561b078cf9195116d"
}
@@ -0,0 +1,25 @@
{
"version": 1,
"name": "prototype_red_matte",
"baseColor": [
1.0,
1.0,
1.0
],
"metallic": 0.0,
"roughness": 0.8,
"emissiveColor": [
0.0,
0.0,
0.0
],
"normalScale": 1.0,
"occlusionStrength": 1.0,
"filtering": "anisotropic",
"textures": {
"diffuse": {
"path": "engine://kenny/Red/texture_01.png",
"srgb": true
}
}
}
@@ -0,0 +1,3 @@
{
"guid": "57a4d970d7d6af8c484d357a18e358ac"
}
+19
View File
@@ -0,0 +1,19 @@
{
"baseColor": [
0.9313725233078003,
0.09587656706571579,
0.09587656706571579
],
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "new_material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"roughness": 1.0,
"version": 1
}
@@ -0,0 +1,3 @@
{
"guid": "1316639e4dcaa6ab872ae26cdccee84c"
}
+3
View File
@@ -0,0 +1,3 @@
{
"guid": "74483472d496fc022f2c755c96bdbd8b"
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
{
"guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4"
}
+14 -61
View File
@@ -1,83 +1,36 @@
#ifndef LIGHTKEEPER_H
#define LIGHTKEEPER_H
#include <optional>
#include <string>
#include <memory>
#include <string_view>
#include <destrum/App.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Util/RectGizmo.h>
#include <destrum/Util/TransformGizmo.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/Resources/Cubemap.h>
#include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/ObjectModel/GameObject.h"
#include <scenes/MainScene.h>
class CameraComponent;
class FirstPersonController;
class LightKeeper final : public App {
public:
LightKeeper();
virtual ~LightKeeper();
static constexpr std::string_view MainScenePath = "game://scenes/main.scene";
void customInit() override;
void customUpdate(float dt) override;
void customDraw() override;
void customCleanup() override;
void customFixedUpdate(float dt) override;
void onViewportResize(int newWidth, int newHeight) override;
void drawViewportOverlay() override;
void onViewportAssetDropped(std::string_view path) override;
void customWindowsMenu() override;
void onPlayModeChanged(bool nowPlaying) override;
void onPlayModeChanged(bool playing) override;
[[nodiscard]] Camera* gameCamera() override;
private:
void setupSkybox();
void updateViewMode();
void focusSelection();
void updateFocus(float dt);
[[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] Camera& getViewCamera();
[[nodiscard]] bool isPlayerViewActive() const;
// LightkeeperDebugUi.cpp
void updateDebugUi();
void drawDebugMenuBar();
void drawWindowMenu();
void findPlayer();
void drawTestWindow();
void spawnBall();
void saveCurrentScene();
void loadScene();
void createEmptyScene();
void saveSelectedPrefab();
void instantiatePrefab();
void findPlayer();
void submitDebugDraws();
void submitColliderDebugLines();
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
std::optional<Camera> editorCamera;
std::optional<glm::vec3> focusTarget;
std::unique_ptr<CubeMap> skyboxCubemap;
MainSceneObjects mainScene{};
ImageID debug2DTexture{NULL_IMAGE_ID};
TransformGizmo transformGizmo;
RectGizmo rectGizmo;
char scenePath[512]{"scenes/debug_scene.json"};
char newSceneName[128]{"EmptyScene"};
char prefabPath[512]{"prefabs/selected.prefab"};
std::string sceneStatus;
bool renderColliders{false};
bool render2DShapes{true};
bool renderDebugLines{true};
bool showTransformGizmo{true};
bool showTestWindow{true};
bool useFreeCamera{false};
Camera playerCamera{glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}};
FirstPersonController* playerController{nullptr};
bool showTestWindow{false};
};
#endif //LIGHTKEEPER_H
#endif // LIGHTKEEPER_H
-25
View File
@@ -1,25 +0,0 @@
#ifndef LIGHTKEEPER_MAINSCENE_H
#define LIGHTKEEPER_MAINSCENE_H
#include <destrum/Graphics/ids.h>
class FirstPersonController;
class GameObject;
class GfxDevice;
class RenderResources;
class Scene;
// What the app keeps a hold of after building the scene
struct MainSceneObjects {
GameObject* player{nullptr};
FirstPersonController* playerController{nullptr};
// Used by the debug "Spawn ball" button
MeshID sphereMesh{};
MaterialID sphereMaterial{};
};
// Sun, ground, parkour course, models, trigger zone, the player and the HUD
MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources);
#endif // LIGHTKEEPER_MAINSCENE_H
+97 -165
View File
@@ -1,53 +1,44 @@
#include "Lightkeeper.h"
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <imgui.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <components/FirstPersonController.h>
#include <components/GameComponentRegistry.h>
LightKeeper::LightKeeper() : App()
{
}
LightKeeper::~LightKeeper()
{
if (!cleanedUp) {
try {
cleanup();
} catch (...) {
}
}
}
void LightKeeper::customInit()
{
RegisterGameComponents();
resources.init(gfxDevice);
renderer.init(gfxDevice, resources, m_params.renderSize);
const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y);
camera.setAspectRatio(aspectRatio);
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
window.Maximize();
setupSkybox();
auto& scene = SceneManager::GetInstance().CreateScene("Main");
mainScene = BuildMainScene(scene, gfxDevice, resources);
debug2DTexture = resources.loadImageFromFile(
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"));
Scene& scene = SceneManager::GetInstance().CreateScene("Main");
if (!SceneSerializer::Load(scene, AssetFS::GetInstance().GetFullPath(MainScenePath))) {
throw std::runtime_error("Could not load " + std::string{MainScenePath});
}
onPlayModeChanged(isPlaying());
}
void LightKeeper::setupSkybox()
{
const auto skyboxPath = AssetFS::GetInstance().GetCookedPathForFile("engine://DJI_0001_stitch.jpg");
const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert");
const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
auto& assetFs = AssetFS::GetInstance();
const auto skyboxPath = assetFs.GetCookedPathForFile("engine://DJI_0001_stitch.jpg");
const auto vertShaderPath = assetFs.GetCookedPathForFile("engine://shaders/cubemap.vert");
const auto fragShaderPath = assetFs.GetCookedPathForFile("engine://shaders/cubemap.frag");
skyboxCubemap = std::make_unique<CubeMap>();
skyboxCubemap->LoadCubeMap(gfxDevice, resources, skyboxPath.generic_string());
@@ -57,161 +48,102 @@ void LightKeeper::setupSkybox()
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
}
void LightKeeper::customUpdate(float dt)
void LightKeeper::customUpdate(float)
{
updateViewMode();
// LightKeeper owns a camera that hides App::camera; update this instance
// after SDL events have been consumed.
if (!isPlayerViewActive() && getSceneCamera() == nullptr)
{
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F))
{
focusSelection();
}
updateFocus(dt);
camera.Update(dt);
}
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11))
{
const auto& input = InputManager::GetInstance();
if (input.WasKeyPressed(SDL_SCANCODE_F11)) {
window.ToggleFullscreen();
}
SceneManager::GetInstance().Update(dt);
SceneManager::GetInstance().LateUpdate(dt);
updateDebugUi();
}
void LightKeeper::updateViewMode()
{
auto& input = InputManager::GetInstance();
FirstPersonController* player = mainScene.playerController;
if (isPlaying() && !isPaused() && player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F6))
{
player->SetPossessed(!player->IsPossessed());
if (playerController != nullptr && isPlaying() && !isPaused() && input.WasKeyPressed(SDL_SCANCODE_F6)) {
playerController->SetPossessed(!playerController->IsPossessed());
}
if (!isPlayerViewActive())
{
const bool looking = input.IsMouseDown(SDL_BUTTON_RIGHT);
if (looking != input.IsMouseCaptured())
{
input.SetMouseCaptured(looking);
}
if (showTestWindow) {
drawTestWindow();
}
}
bool LightKeeper::isPlayerViewActive() const
{
return isPlaying() && !isPaused() && mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
}
CameraComponent* LightKeeper::getSceneCamera() const
{
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
if (!isPlaying() || useFreeCamera || activeScenes.empty()) {
return nullptr;
}
return activeScenes.front()->GetCurrentCamera();
}
Camera& LightKeeper::getViewCamera()
{
CameraComponent* sceneCamera = isPlayerViewActive() ? nullptr : getSceneCamera();
return sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera;
}
void LightKeeper::customDraw()
{
const auto cmd = gfxDevice.beginFrame();
if (cmd == VK_NULL_HANDLE) {
return;
}
renderer.beginDrawing(gfxDevice);
submitDebugDraws();
if (isPlayerViewActive())
{
camera.SetRotation(mainScene.playerController->GetYaw(), mainScene.playerController->GetPitch());
camera.m_position = mainScene.playerController->GetEyePosition();
camera.CalculateViewMatrix();
camera.CalculateProjectionMatrix();
}
Camera& viewCamera = getViewCamera();
const SceneEnvironment& environment = SceneManager::GetInstance().GetCurrentScene().GetEnvironment();
const GameRenderer::SceneData sceneData{
.camera = viewCamera,
.ambientColor = environment.ambientColor,
.ambientIntensity = environment.ambientIntensity,
.fogColor = environment.fogColor,
.fogDensity = environment.fogDensity,
.skyboxRotation = environment.skyboxRotation,
};
const RenderContext ctx{
.renderer = renderer,
.camera = viewCamera,
.sceneData = sceneData,
};
SceneManager::GetInstance().Render(ctx);
renderer.endDrawing();
const auto& drawImage = renderer.getDrawImage();
renderer.draw(cmd, gfxDevice, viewCamera, sceneData);
gfxDevice.endFrame(
cmd, drawImage, {
.clearColor = {{0.f, 0.f, 0.5f, 1.f}},
.drawImageBlitRect = glm::ivec4{},
.imguiPass = &imguiPass,
.copyDrawImageToSwapchain = !usesEditorViewport(),
});
}
void LightKeeper::customCleanup()
{
SceneManager::GetInstance().Destroy();
if (skyboxCubemap)
{
if (skyboxCubemap) {
skyboxCubemap->cleanup(gfxDevice);
skyboxCubemap.reset();
}
renderer.cleanup(gfxDevice);
}
void LightKeeper::customFixedUpdate(float dt)
void LightKeeper::customWindowsMenu()
{
SceneManager::GetInstance().FixedUpdate(dt);
ImGui::MenuItem("Test", nullptr, &showTestWindow);
}
void LightKeeper::onPlayModeChanged(bool nowPlaying)
void LightKeeper::onPlayModeChanged(bool playing)
{
findPlayer();
if (nowPlaying) {
// The player view moves the free camera, put it back where the editor left it on Stop
editorCamera = camera;
if (mainScene.playerController != nullptr) {
mainScene.playerController->SetPossessed(true);
}
} else if (editorCamera) {
const float aspectRatio = camera.GetAspectRatio();
camera = *editorCamera;
camera.setAspectRatio(aspectRatio);
editorCamera.reset();
if (playing && playerController != nullptr) {
playerController->SetPossessed(true);
}
}
void LightKeeper::onViewportResize(int newWidth, int newHeight)
Camera* LightKeeper::gameCamera()
{
const float aspectRatio = static_cast<float>(newWidth) / static_cast<float>(newHeight);
camera.setAspectRatio(aspectRatio);
if (playerController != nullptr && playerController->IsPossessed() && isPlaying() && !isPaused()) {
playerCamera.SetRotation(playerController->GetYaw(), playerController->GetPitch());
playerCamera.m_position = playerController->GetEyePosition();
playerCamera.CalculateViewMatrix();
return &playerCamera;
}
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
return nullptr;
}
CameraComponent* sceneCamera = sceneManager.GetCurrentScene().GetCurrentCamera();
return sceneCamera != nullptr ? &sceneCamera->GetCamera(getRenderAspectRatio()) : nullptr;
}
// Loading or switching scenes replaces the player, so look it up again instead of keeping a dangling pointer
void LightKeeper::findPlayer()
{
playerController = nullptr;
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
return;
}
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
if (object != nullptr && !object->IsBeingDestroyed()) {
if (auto* controller = object->GetComponent<FirstPersonController>()) {
playerController = controller;
return;
}
}
}
}
void LightKeeper::drawTestWindow()
{
if (ImGui::Begin("Test", &showTestWindow) && ImGui::Button("Spawn ball")) {
spawnBall();
}
ImGui::End();
}
void LightKeeper::spawnBall()
{
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
constexpr float radius = 0.75f;
GameObject* ball = scene.CreateGameObject("Ball");
ball->GetTransform().SetWorldPosition({static_cast<float>(std::rand() % 5) - 2.0f, 20.0f, static_cast<float>(std::rand() % 5) - 2.0f});
ball->AddComponent<SphereCollider>(radius);
ball->AddComponent<Rigidbody>()->SetMass(50.0f);
const auto desc = PrimitiveMeshes::Desc{.shape = PrimitiveMeshes::Shape::Sphere, .radius = radius};
auto mesh = resources.meshes().findMeshByKey(PrimitiveMeshes::MakeKey(desc));
if (!mesh) {
mesh = resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::Make(desc));
}
auto* meshRenderer = ball->AddComponent<MeshRendererComponent>();
meshRenderer->SetMeshID(*mesh);
meshRenderer->SetMaterialID(resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Ball"));
scene.GetPhysics().RefreshGameObject(*ball);
}
-552
View File
@@ -1,552 +0,0 @@
#include "Lightkeeper.h"
#include <destrum/Components/CameraComponent.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/ModelSpawner.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/AssetBrowser.h>
#include <destrum/Util/ModelDoc.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <components/FirstPersonController.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <limits>
#include <vector>
#include <stdexcept>
#include <string>
#include <string_view>
#include "imgui.h"
#include "spdlog/spdlog.h"
namespace {
std::filesystem::path ResolveScenePath(
std::string_view value,
const std::filesystem::path& exeDir) {
if (value.empty()) {
throw std::invalid_argument("Scene path cannot be empty");
}
if (value.find("://") != std::string_view::npos) {
return AssetFS::GetInstance().GetFullPath(value);
}
const std::filesystem::path path{value};
return path.is_absolute() ? path : exeDir / path;
}
}
void LightKeeper::updateDebugUi()
{
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1))
{
renderer.setRenderWireframe(!renderer.getRenderWireframe());
}
if (!m_params.showDebugUi) {
return;
}
drawDebugMenuBar();
if (showTransformGizmo && !isPlayerViewActive()) {
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
if (selected != nullptr && !RectGizmo::CanManipulate(*selected)) {
transformGizmo.HandleShortcuts();
transformGizmo.DrawToolbar();
}
}
if (showTestWindow) {
drawTestWindow();
}
}
void LightKeeper::customWindowsMenu()
{
ImGui::Separator();
ImGui::MenuItem("Test", nullptr, &showTestWindow);
}
void LightKeeper::drawTestWindow()
{
ImGui::Begin("Test", &showTestWindow);
if (ImGui::Button("Spawn ball"))
{
spawnBall();
}
ImGui::End();
}
void LightKeeper::spawnBall()
{
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
GameObject* sphere = scene.CreateGameObject("Sphere");
sphere->AddComponent<SphereCollider>(1.5f);
sphere->AddComponent<Rigidbody>()->SetMass(1000);
auto* meshRenderComp = sphere->AddComponent<MeshRendererComponent>();
meshRenderComp->SetMaterialID(mainScene.sphereMaterial);
meshRenderComp->SetMeshID(mainScene.sphereMesh);
sphere->GetTransform().SetWorldPosition((std::rand() % 2) - 0.5f, 100, (std::rand() % 2) - 0.5f);
sphere->GetTransform().SetWorldScale(glm::vec3{0.015f});
scene.GetPhysics().RefreshGameObject(*sphere);
}
void LightKeeper::drawDebugMenuBar()
{
if (!ImGui::BeginMainMenuBar()) {
return;
}
if (ImGui::BeginMenu("File")) {
ImGui::TextDisabled("Scene");
ImGui::SetNextItemWidth(320.0f);
ImGui::InputText("Scene path", scenePath, sizeof(scenePath));
ImGui::SetNextItemWidth(320.0f);
ImGui::InputText("New scene name", newSceneName, sizeof(newSceneName));
ImGui::Separator();
if (ImGui::MenuItem("New Empty Scene")) {
createEmptyScene();
}
if (ImGui::MenuItem("Load")) {
loadScene();
}
if (ImGui::MenuItem("Save")) {
saveCurrentScene();
}
ImGui::Separator();
ImGui::TextDisabled("Prefab");
ImGui::SetNextItemWidth(320.0f);
ImGui::InputText("Prefab path", prefabPath, sizeof(prefabPath));
const bool hasSelection = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject() != nullptr;
if (ImGui::MenuItem("Save Selected as Prefab", nullptr, false, hasSelection)) {
saveSelectedPrefab();
}
if (ImGui::MenuItem("Instantiate Prefab")) {
instantiatePrefab();
}
if (!sceneStatus.empty()) {
ImGui::Separator();
ImGui::TextWrapped("%s", sceneStatus.c_str());
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Render")) {
ImGui::MenuItem("Colliders", nullptr, &renderColliders);
ImGui::MenuItem("2D Shapes", nullptr, &render2DShapes);
ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines);
ImGui::MenuItem("Transform Gizmo", nullptr, &showTransformGizmo);
bool lightHeatmap = renderer.getRenderLightHeatmap();
if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) {
renderer.setRenderLightHeatmap(lightHeatmap);
}
bool selectionView = renderer.getRenderSelectionView();
if (ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
renderer.setRenderSelectionView(selectionView);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Camera")) {
ImGui::MenuItem("Free Camera", nullptr, &useFreeCamera);
ImGui::Separator();
auto& scene = SceneManager::GetInstance().GetCurrentScene();
const auto cameras = scene.GetCameras();
if (cameras.empty()) {
ImGui::TextDisabled("No cameras in the scene");
}
for (CameraComponent* sceneCamera : cameras) {
const GameObject* owner = sceneCamera->GetGameObject();
ImGui::PushID(owner);
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, !useFreeCamera && sceneCamera->IsCurrent())) {
scene.SetCurrentCamera(sceneCamera);
useFreeCamera = false;
}
ImGui::PopID();
}
ImGui::EndMenu();
}
drawWindowMenu();
ImGui::EndMainMenuBar();
}
void LightKeeper::onViewportAssetDropped(std::string_view path)
{
if (!AssetBrowser::IsModel(path)) {
sceneStatus = "Only models can be dropped in the viewport: " + std::string{path};
return;
}
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
try {
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
const std::string name = std::filesystem::path{path}.stem().string();
GameObject* model = ModelSpawner::SpawnStaticModel(scene, gfxDevice, resources, path, options, name);
const Camera& view = getViewCamera();
model->GetTransform().SetWorldPosition(view.GetPosition() + view.GetForward() * 5.0f);
scene.SetSelectedObject(model->GetId());
sceneStatus = "Placed " + std::string{path};
} catch (const std::exception& exception) {
spdlog::error("Could not place {}: {}", path, exception.what());
sceneStatus = "Could not place " + std::string{path};
}
}
void LightKeeper::focusSelection()
{
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
if (selected == nullptr) {
return;
}
glm::vec3 boundsMin{std::numeric_limits<float>::max()};
glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
std::vector<GameObject*> pending{selected};
while (!pending.empty()) {
GameObject* object = pending.back();
pending.pop_back();
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
pending.push_back(child->GetOwner());
}
}
const auto* meshRenderer = object->GetComponent<MeshRendererComponent>();
if (meshRenderer == nullptr || meshRenderer->GetMeshID() == NULL_MESH_ID) {
continue;
}
const GPUMesh& mesh = resources.meshes().getMesh(meshRenderer->GetMeshID());
const glm::mat4 world = object->GetTransform().GetWorldMatrix();
for (int corner = 0; corner < 8; ++corner) {
const glm::vec3 local{
corner & 1 ? mesh.maxPos.x : mesh.minPos.x,
corner & 2 ? mesh.maxPos.y : mesh.minPos.y,
corner & 4 ? mesh.maxPos.z : mesh.minPos.z,
};
const glm::vec3 point{world * glm::vec4{local, 1.0f}};
boundsMin = glm::min(boundsMin, point);
boundsMax = glm::max(boundsMax, point);
}
}
const bool hasBounds = boundsMin.x <= boundsMax.x;
const glm::vec3 center = hasBounds ? (boundsMin + boundsMax) * 0.5f : selected->GetTransform().GetWorldPosition();
const float radius = hasBounds ? std::max(glm::length(boundsMax - boundsMin) * 0.5f, 0.1f) : 1.0f;
// Fit the bounding sphere in the narrower of the two field of view angles
const float halfFovY = glm::radians(camera.GetFOVY()) * 0.5f;
const float halfFovX = std::atan(std::tan(halfFovY) * camera.GetAspectRatio());
const float distance = radius / std::sin(std::min(halfFovX, halfFovY)) * 1.1f;
focusTarget = center - camera.GetForward() * distance;
}
void LightKeeper::updateFocus(float dt)
{
if (!focusTarget) {
return;
}
if (InputManager::GetInstance().IsMouseDown(SDL_BUTTON_RIGHT)) {
focusTarget.reset();
return;
}
camera.m_position = glm::mix(camera.m_position, *focusTarget, 1.0f - std::exp(-12.0f * dt));
if (glm::distance(camera.m_position, *focusTarget) < 0.01f) {
camera.m_position = *focusTarget;
focusTarget.reset();
}
}
void LightKeeper::drawViewportOverlay()
{
if (!showTransformGizmo || isPlayerViewActive()) {
return;
}
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
if (selected == nullptr) {
return;
}
const ViewportRect& view = getViewportRect();
if (RectGizmo::CanManipulate(*selected)) {
const VkExtent3D extent = renderer.getDrawImage().extent;
rectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)},
view.min, view.size);
if (rectGizmo.IsInteracting()) {
claimViewportMouse();
}
return;
}
transformGizmo.Manipulate(*selected, getViewCamera(), view.min, view.size);
if (transformGizmo.IsOver() || transformGizmo.IsUsing()) {
claimViewportMouse();
}
}
void LightKeeper::drawWindowMenu()
{
if (!ImGui::BeginMenu("Window")) {
return;
}
const WindowMode mode = window.GetMode();
const bool fullscreen = window.IsFullscreen();
const bool maximized = window.IsMaximized();
if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) {
window.ToggleFullscreen();
}
if (ImGui::BeginMenu("Mode")) {
if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) {
window.SetMode(WindowMode::Windowed);
}
if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) {
window.SetMode(WindowMode::BorderlessFullscreen);
}
if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) {
window.SetMode(WindowMode::Fullscreen);
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) {
window.ToggleMaximized();
}
if (ImGui::MenuItem("Minimize", nullptr, false, !fullscreen)) {
window.Minimize();
}
if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) {
window.Center();
}
if (ImGui::BeginMenu("Size", !fullscreen)) {
const glm::ivec2 sizes[] = {glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}};
for (const glm::ivec2& size : sizes) {
const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y);
if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) {
if (maximized) {
window.Restore();
}
window.SetSize(size);
window.Center();
}
}
ImGui::EndMenu();
}
ImGui::Separator();
if (bool resizable = window.IsResizable(); ImGui::MenuItem("Resizable", nullptr, &resizable, !fullscreen)) {
window.SetResizable(resizable);
}
if (bool bordered = window.IsBordered(); ImGui::MenuItem("Bordered", nullptr, &bordered, !fullscreen)) {
window.SetBordered(bordered);
}
ImGui::Separator();
const glm::ivec2 drawableSize = window.GetDrawableSize();
ImGui::TextDisabled("Drawable: %d x %d", drawableSize.x, drawableSize.y);
ImGui::EndMenu();
}
void LightKeeper::saveCurrentScene()
{
try {
const auto path = ResolveScenePath(scenePath, m_params.exeDir);
const auto parent = path.parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent);
}
if (SceneSerializer::Save(
SceneManager::GetInstance().GetCurrentScene(),
path)) {
sceneStatus = "Saved scene to " + path.string();
} else {
sceneStatus = "Failed to save scene to " + path.string();
}
} catch (const std::exception& exception) {
sceneStatus = std::string{"Save failed: "} + exception.what();
}
}
void LightKeeper::saveSelectedPrefab()
{
try {
const GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
if (selected == nullptr) {
sceneStatus = "Select an object to save as a prefab";
return;
}
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
const auto parent = path.parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent);
}
sceneStatus = PrefabSerializer::Save(*selected, path)
? "Saved prefab \"" + selected->GetName() + "\" to " + path.string()
: "Failed to save prefab to " + path.string();
} catch (const std::exception& exception) {
sceneStatus = std::string{"Prefab save failed: "} + exception.what();
}
}
void LightKeeper::instantiatePrefab()
{
try {
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
const GameObject* instance = PrefabSerializer::Instantiate(
SceneManager::GetInstance().GetCurrentScene(), path.string());
sceneStatus = instance != nullptr
? "Instantiated \"" + instance->GetName() + "\" from " + path.string()
: "Failed to instantiate prefab " + path.string();
} catch (const std::exception& exception) {
sceneStatus = std::string{"Prefab instantiate failed: "} + exception.what();
}
}
void LightKeeper::loadScene()
{
try {
const auto path = ResolveScenePath(scenePath, m_params.exeDir);
if (SceneSerializer::Load(
SceneManager::GetInstance().GetCurrentScene(),
path)) {
sceneStatus = "Loaded scene from " + path.string();
} else {
sceneStatus = "Failed to load scene from " + path.string();
}
} catch (const std::exception& exception) {
sceneStatus = std::string{"Load failed: "} + exception.what();
}
findPlayer();
}
void LightKeeper::createEmptyScene()
{
try {
const std::string name = newSceneName[0] != '\0'
? newSceneName
: "EmptyScene";
auto& sceneManager = SceneManager::GetInstance();
const int newSceneIndex = sceneManager.GetSceneCount();
sceneManager.CreateScene(name);
sceneManager.SwitchScene(newSceneIndex);
sceneStatus = "Created and switched to scene '" + name + "'";
} catch (const std::exception& exception) {
sceneStatus = std::string{"New scene failed: "} + exception.what();
}
findPlayer();
}
// Loading or switching scenes replaces the player, so look it up again instead of keeping a dangling pointer
void LightKeeper::findPlayer()
{
mainScene.player = nullptr;
mainScene.playerController = nullptr;
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
return;
}
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
if (object == nullptr || object->IsBeingDestroyed()) {
continue;
}
if (auto* controller = object->GetComponent<FirstPersonController>()) {
mainScene.player = object.get();
mainScene.playerController = controller;
return;
}
}
}
void LightKeeper::submitDebugDraws()
{
submitColliderDebugLines();
if (renderDebugLines) {
LineRenderingManager::GetInstance().SubmitLine({0, 0, 0}, {0, 10, 0}, {255, 0, 0, 255});
}
}
// Triggers orange, then colored by body type: static blue, dynamic green, kinematic purple
void LightKeeper::submitColliderDebugLines()
{
if (!renderColliders) {
return;
}
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetActiveScenes().empty()) {
return;
}
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
continue;
}
const auto* rigidbody = object->GetComponent<Rigidbody>();
const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static;
for (const auto& component : object->GetComponents()) {
const auto* collider = dynamic_cast<const Collider*>(component.get());
if (collider == nullptr || collider->IsBeingDestroyed() || !collider->isEnabled()) {
continue;
}
glm::vec4 color{0.1f, 0.8f, 1.0f, 1.0f};
if (collider->IsTrigger()) {
color = {1.0f, 0.75f, 0.1f, 1.0f};
} else if (type == RigidbodyType::Dynamic) {
color = {0.3f, 1.0f, 0.3f, 1.0f};
} else if (type == RigidbodyType::Kinematic) {
color = {0.8f, 0.4f, 1.0f, 1.0f};
}
collider->DrawDebugLines(color);
}
}
}
+18 -4
View File
@@ -1,16 +1,26 @@
#include <cstdio>
#include <algorithm>
#include <memory>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <SDL.h>
#include <spdlog/spdlog.h>
#include <editor/Editor.h>
#include "Lightkeeper.h"
#include "spdlog/spdlog.h"
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), nullptr);
return 1;
}
const std::span<char*> arguments{argv, static_cast<std::size_t>(argc)};
const bool runGameOnly = std::ranges::any_of(arguments, [](const char* argument) { return std::string_view{argument} == "--game"; });
try {
char* basePath = SDL_GetBasePath();
if (basePath == nullptr) {
@@ -28,8 +38,13 @@ int main(int argc, char* argv[]) {
.appName = "Destrum Engine",
.windowTitle = "Lightkeeper",
.exeDir = exeDir,
.startInEditMode = true,
});
if (!runGameOnly) {
app.attachLayer(std::make_unique<Editor>(app, EditorSettings{
.gameAssetsSource = LIGHTKEEPER_ASSETS_SRC_DIR,
.scenePath = std::string{LightKeeper::MainScenePath},
}));
}
app.run();
app.cleanup();
} catch (const std::exception& exception) {
@@ -41,4 +56,3 @@ int main(int argc, char* argv[]) {
SDL_Quit();
return 0;
}
-792
View File
@@ -1,792 +0,0 @@
#include <scenes/MainScene.h>
#include <destrum/Assets/AssetManager.h>
#include <destrum/Components/Animator.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Components/UI/UIImage.h>
#include <destrum/Components/UI/UIText.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/ModelSpawner.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h>
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/Door.h>
#include <components/InteractableButton.h>
#include <components/LightToggle.h>
#include <components/MovingPlatform.h>
#include <components/PlayerRespawn.h>
#include <components/TriggerLoggerComponent.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
#include <spdlog/spdlog.h>
#include <cmath>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace {
// The visible ground plane is at y = -1
constexpr float GroundHeight = -1.0f;
constexpr float GroundHalfExtent = 25.0f;
ModelDoc::LoadOptions StaticModelOptions()
{
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
options.loadSkeleton = false;
options.loadAnimations = false;
return options;
}
void CreateSun(Scene& scene)
{
// Directional lights shine down their local -Z: tilt it 50 degrees down, then turn it around the up axis
GameObject* sunObj = scene.CreateGameObject("Sun");
sunObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 10.f, 0.f));
sunObj->GetTransform().SetWorldRotation(
glm::angleAxis(glm::radians(35.f), glm::vec3{0.f, 1.f, 0.f}) *
glm::angleAxis(glm::radians(-50.f), glm::vec3{1.f, 0.f, 0.f}));
auto* sun = sunObj->AddComponent<LightComponent>(LightType::Directional);
sun->SetColor(glm::vec3{1.0f, 0.95f, 0.85f});
sun->SetIntensity(3.0f);
sun->SetCastShadows(true);
}
void CreateHelmetSpotlight(Scene& scene)
{
const glm::vec3 position{2.0f, 3.5f, -2.5f};
const glm::vec3 target{2.0f, GroundHeight, 0.0f};
GameObject* spotObj = scene.CreateGameObject("HelmetSpotlight");
spotObj->GetTransform().SetWorldPosition(position);
spotObj->GetTransform().SetWorldRotation(glm::quatLookAt(glm::normalize(target - position), glm::vec3{0.0f, 1.0f, 0.0f}));
auto* spot = spotObj->AddComponent<LightComponent>(LightType::Spot);
spot->SetColor(glm::vec3{1.0f, 0.85f, 0.7f});
spot->SetIntensity(25.0f);
spot->SetRange(12.0f);
spot->SetConeAngles(glm::radians(20.0f), glm::radians(30.0f));
spot->SetCastShadows(true);
}
void CreateGround(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const auto planeModel = AssetManager::GetInstance().LoadModel("game://plane.glb", StaticModelOptions());
ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
const auto planeTexturePath = ModelDocUtils::PickTexturePath(
planeModel,
planePrimitive,
AssetFS::GetInstance().GetCookedPathForFile("engine://grass.jpg"));
const MaterialID planeMaterialID = resources.materials().addMaterial({
.baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive),
.textureFilteringMode = TextureFilteringMode::Nearest,
.diffuseTexture = resources.loadImageFromFile(gfxDevice, planeTexturePath),
.name = ModelDocUtils::GetImportedMaterialName(planeModel, planePrimitive, "GroundPlaneMaterial"),
});
GameObject* planeObj = scene.CreateGameObject("GroundPlane");
auto* planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
planeMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, planePrimitive.mesh));
planeMeshComp->SetMaterialID(planeMaterialID);
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight, 0.f));
planeObj->GetTransform().SetWorldScale(glm::vec3(GroundHalfExtent, 1.f, GroundHalfExtent));
// The plane mesh is flat, so the floor collider sits right below it
GameObject* groundObj = scene.CreateGameObject("GroundCollider");
groundObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight - 0.5f, 0.f));
groundObj->AddComponent<BoxCollider>(glm::vec3{GroundHalfExtent, 0.5f, GroundHalfExtent});
groundObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
scene.GetPhysics().RegisterGameObject(*groundObj);
}
// Kenney prototype textures, texture_01 is a 1 x 1 meter grid
MaterialID AddPrototypeMaterial(GfxDevice& gfxDevice, RenderResources& resources,
const std::string& color, float roughness)
{
Material material = Material::SimpleColor(glm::vec3{1.0f}, "Prototype" + color);
material.roughnessFactor = roughness;
material.textureFilteringMode = TextureFilteringMode::Anisotropic;
material.diffuseTexture = resources.loadImageFromFile(
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://kenny/" + color + "/texture_01.png"));
return resources.materials().addMaterial(std::move(material));
}
GameObject* CreateBox(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
const std::string& name, const glm::vec3& position, const glm::vec3& size,
MaterialID materialID, RigidbodyType type,
const glm::quat& rotation = glm::quat{1.0f, 0.0f, 0.0f, 0.0f})
{
GameObject* box = scene.CreateGameObject(name);
box->GetTransform().SetWorldPosition(position);
box->GetTransform().SetWorldRotation(rotation);
auto* boxMeshComp = box->AddComponent<MeshRendererComponent>();
boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox(size)));
boxMeshComp->SetMaterialID(materialID);
box->AddComponent<BoxCollider>(size * 0.5f);
auto* boxRb = box->AddComponent<Rigidbody>();
boxRb->SetType(type);
boxRb->SetMass(10.0f);
scene.GetPhysics().RegisterGameObject(*box);
return box;
}
// Every platform is reachable with a jump from the previous one
void CreateParkourCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID platformMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID wallMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID crateMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.35f);
// Platforms stand on the ground and are given by their top height
const auto createPlatform = [&](const std::string& name, const glm::vec2& center, float top,
const glm::vec2& footprint, MaterialID materialID)
{
const float height = top - GroundHeight;
CreateBox(scene, gfxDevice, resources, name,
glm::vec3{center.x, top - height * 0.5f, center.y},
glm::vec3{footprint.x, height, footprint.y},
materialID, RigidbodyType::Static);
};
createPlatform("Parkour_Step", {0.0f, 4.0f}, -0.5f, {2.0f, 2.0f}, platformMaterial);
createPlatform("Parkour_Block", {0.0f, 7.0f}, 0.4f, {2.0f, 2.0f}, platformMaterial);
createPlatform("Parkour_Pillar1", {3.5f, 8.5f}, 1.2f, {1.5f, 1.5f}, platformMaterial);
createPlatform("Parkour_Pillar2", {6.5f, 7.5f}, 2.0f, {1.2f, 1.2f}, platformMaterial);
CreateBox(scene, gfxDevice, resources, "Parkour_Beam", {6.5f, 1.85f, 4.0f}, {0.6f, 0.3f, 5.0f},
platformMaterial, RigidbodyType::Static);
createPlatform("Parkour_Goal", {6.5f, -0.5f}, 2.8f, {2.5f, 2.5f}, goalMaterial);
CreateBox(scene, gfxDevice, resources, "Wall", {-5.0f, 0.5f, 0.0f}, {0.5f, 3.0f, 8.0f},
wallMaterial, RigidbodyType::Static);
CreateBox(scene, gfxDevice, resources, "LowWall", {-2.5f, -0.6f, -4.0f}, {4.0f, 0.8f, 0.4f},
wallMaterial, RigidbodyType::Static);
for (int i = 0; i < 4; ++i)
{
CreateBox(scene, gfxDevice, resources, "Crate" + std::to_string(i),
{-2.5f, -0.6f + 0.81f * static_cast<float>(i), 2.0f},
glm::vec3{0.8f}, crateMaterial, RigidbodyType::Dynamic);
}
}
GameObject* CreateDoorway(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID frameMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID doorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
constexpr float x = 4.0f;
constexpr float z = -4.5f;
constexpr float frameHeight = 2.8f;
constexpr float doorWidth = 1.6f;
constexpr float doorHeight = 2.4f;
for (const float side : {-1.0f, 1.0f}) {
CreateBox(scene, gfxDevice, resources, side < 0.0f ? "DoorFrame_Left" : "DoorFrame_Right",
{x, GroundHeight + frameHeight * 0.5f, z + side * (doorWidth * 0.5f + 0.2f)},
{0.4f, frameHeight, 0.4f}, frameMaterial, RigidbodyType::Static);
}
CreateBox(scene, gfxDevice, resources, "DoorFrame_Top", {x, GroundHeight + frameHeight + 0.2f, z},
{0.4f, 0.4f, doorWidth + 0.8f}, frameMaterial, RigidbodyType::Static);
GameObject* door = CreateBox(scene, gfxDevice, resources, "Door", {x, GroundHeight + doorHeight * 0.5f, z},
{0.3f, doorHeight, doorWidth}, doorMaterial, RigidbodyType::Kinematic);
door->AddComponent<Door>()->SetOpenOffset({0.0f, 0.0f, -doorWidth - 0.1f});
return door;
}
void CreateButton(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const GameObject& target)
{
const MaterialID pedestalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
CreateBox(scene, gfxDevice, resources, "ButtonPedestal", {1.5f, -0.45f, -4.5f}, {0.4f, 1.1f, 0.4f},
pedestalMaterial, RigidbodyType::Static);
// Every button needs its own material, pressing it recolors the material
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f}, "ButtonMaterial");
buttonMaterial.roughnessFactor = 0.4f;
const MaterialID buttonMaterialID = resources.materials().addMaterial(std::move(buttonMaterial));
GameObject* button = CreateBox(scene, gfxDevice, resources, "Button", {1.5f, 0.15f, -4.5f}, {0.25f, 0.1f, 0.25f},
buttonMaterialID, RigidbodyType::Static);
button->AddComponent<InteractableButton>()->AddTarget(target);
}
void CreateMovingPlatform(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID rampMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID platformMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
constexpr float laneZ = 2.0f;
constexpr float platformHeight = 1.0f;
constexpr float rampStartX = 9.0f;
constexpr float rampLength = 3.0f;
constexpr float rampThickness = 0.3f;
constexpr float travelDistance = 8.0f;
const glm::vec3 platformSize{3.0f, 0.3f, 3.0f};
const float platformTop = GroundHeight + platformHeight;
const glm::quat rampRotation =
glm::angleAxis(std::atan2(platformHeight, rampLength), glm::vec3{0.0f, 0.0f, 1.0f});
const glm::vec3 rampTopCenter{rampStartX + rampLength * 0.5f, GroundHeight + platformHeight * 0.5f, laneZ};
const glm::vec3 rampCenter = rampTopCenter - rampRotation * glm::vec3{0.0f, rampThickness * 0.5f, 0.0f};
const float rampSlopeLength = glm::length(glm::vec2{rampLength, platformHeight});
CreateBox(scene, gfxDevice, resources, "PlatformRamp", rampCenter,
{rampSlopeLength, rampThickness, platformSize.z}, rampMaterial, RigidbodyType::Static,
rampRotation);
const float platformStartX = rampStartX + rampLength + platformSize.x * 0.5f;
GameObject* platform = CreateBox(scene, gfxDevice, resources, "MovingPlatform",
{platformStartX, platformTop - platformSize.y * 0.5f, laneZ},
platformSize, platformMaterial, RigidbodyType::Kinematic);
platform->AddComponent<MovingPlatform>()->SetTravelOffset({travelDistance, 0.0f, 0.0f});
const float landingStartX = platformStartX + travelDistance + platformSize.x * 0.5f;
CreateBox(scene, gfxDevice, resources, "PlatformLanding",
{landingStartX + 1.0f, GroundHeight + platformHeight * 0.5f, laneZ},
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static);
}
// Floats over the void past the west edge of the ground. The controller jumps 1.3 m and sprints 8.5 m/s,
// which carries about 5 m on a level jump, the hardest gaps stay a little under that
WinGoal* CreateWinCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID checkpointMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID pillarMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
const MaterialID stoneMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
const MaterialID beamMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID movingMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.5f);
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.6f);
constexpr float slabThickness = 0.5f;
GameObject* course = scene.CreateGameObject("WinCourse");
course->GetTransform().SetWorldPosition({-GroundHalfExtent, GroundHeight, 0.0f});
const auto addToCourse = [&](GameObject* object)
{
object->GetTransform().SetParent(&course->GetTransform(), true);
return object;
};
// Top center and half size of the last placed platform, the course runs along -X
glm::vec3 top{-GroundHalfExtent - 2.5f, GroundHeight, 0.0f};
glm::vec2 half{2.5f, 2.5f};
int platformCount = 0;
int checkpointCount = 0;
const auto slab = [&](const std::string& name, const glm::vec3& topCenter, const glm::vec2& size,
MaterialID materialID, RigidbodyType type)
{
return addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_" + name,
topCenter - glm::vec3{0.0f, slabThickness * 0.5f, 0.0f},
{size.x, slabThickness, size.y}, materialID, type));
};
// gap is the edge to edge distance along X, rise and dz move the top center
const auto next = [&](float gap, float rise, float dz, const glm::vec2& size, MaterialID materialID,
RigidbodyType type = RigidbodyType::Static)
{
top = glm::vec3{top.x - half.x - gap - size.x * 0.5f, top.y + rise, top.z + dz};
half = size * 0.5f;
return slab("Platform" + std::to_string(++platformCount), top, size, materialID, type);
};
const auto mover = [&](float gap, float rise, float dz, const glm::vec2& size, const glm::vec3& travel,
float travelDuration, float pauseDuration)
{
auto* platform = next(gap, rise, dz, size, movingMaterial, RigidbodyType::Kinematic)
->AddComponent<MovingPlatform>();
platform->SetTravelOffset(travel);
platform->SetTravelDuration(travelDuration);
platform->SetPauseDuration(pauseDuration);
};
const auto addCheckpoint = [&]()
{
GameObject* trigger = addToCourse(
scene.CreateGameObject("WinCourse_Checkpoint" + std::to_string(++checkpointCount)));
trigger->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 1.0f, 0.0f});
trigger->AddComponent<BoxCollider>(glm::vec3{half.x, 1.0f, half.y})->SetTrigger(true);
trigger->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
trigger->AddComponent<Checkpoint>()->SetSpawnOffset({0.0f, 0.2f, 0.0f});
scene.GetPhysics().RegisterGameObject(*trigger);
};
// The start pad touches the ground edge so it can be walked onto
slab("Start", top, {5.0f, 5.0f}, checkpointMaterial, RigidbodyType::Static);
addCheckpoint();
// Warm-up hops
next(2.0f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
next(2.4f, 0.5f, 1.5f, {1.5f, 1.5f}, pillarMaterial);
next(2.6f, 0.5f, -3.0f, {1.5f, 1.5f}, pillarMaterial);
next(2.8f, 0.0f, 1.5f, {1.2f, 1.2f}, pillarMaterial);
next(2.2f, -0.5f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Zig-zag over shrinking stones
next(2.6f, 0.0f, 1.0f, {1.0f, 1.0f}, stoneMaterial);
next(2.8f, 0.4f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
next(2.8f, 0.4f, 2.0f, {0.7f, 0.7f}, stoneMaterial);
next(3.0f, 0.4f, -2.0f, {0.7f, 0.7f}, stoneMaterial);
next(3.2f, -0.8f, 1.0f, {0.7f, 0.7f}, stoneMaterial);
next(2.5f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Offset balance beams, then long sprint jumps
next(1.2f, 0.0f, 0.0f, {6.0f, 0.4f}, beamMaterial);
next(1.4f, 0.3f, 0.9f, {5.0f, 0.35f}, beamMaterial);
next(1.4f, 0.3f, -1.8f, {4.0f, 0.3f}, beamMaterial);
next(3.6f, 0.0f, 0.9f, {1.5f, 1.5f}, pillarMaterial);
next(4.0f, -0.6f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
next(3.8f, 0.0f, 0.0f, {1.2f, 1.2f}, pillarMaterial);
next(3.0f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// A ferry over a 9 m hole, then sideways movers that drift out of sync
constexpr float ferryTravel = 9.0f;
mover(0.6f, 0.0f, 0.0f, {2.5f, 2.5f}, {-ferryTravel, 0.0f, 0.0f}, 3.5f, 1.0f);
top.x -= ferryTravel;
next(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
constexpr float sway = 3.0f;
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 2.0f, 0.3f);
top.z += sway;
mover(2.2f, 0.0f, sway, {1.5f, 1.5f}, {0.0f, 0.0f, -sway * 2.0f}, 2.6f, 0.3f);
top.z -= sway;
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 1.7f, 0.3f);
top.z += sway;
next(2.2f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// An elevator up to the last stretch
constexpr float liftHeight = 5.0f;
mover(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, {0.0f, liftHeight, 0.0f}, 3.0f, 1.5f);
top.y += liftHeight;
next(1.2f, 0.0f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
next(2.8f, 0.5f, 1.2f, {0.8f, 0.8f}, stoneMaterial);
next(3.0f, 0.5f, -2.4f, {0.7f, 0.7f}, stoneMaterial);
next(3.2f, 0.0f, 2.4f, {0.6f, 0.6f}, stoneMaterial);
next(3.4f, -0.4f, -2.4f, {0.6f, 0.6f}, stoneMaterial);
next(2.5f, 0.0f, 1.2f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Final gauntlet: a fast sideways mover and three tiny stones
constexpr float fastSway = 2.5f;
mover(2.4f, 0.0f, -fastSway, {1.2f, 1.2f}, {0.0f, 0.0f, fastSway * 2.0f}, 1.6f, 0.2f);
top.z += fastSway;
next(2.6f, 0.8f, 0.0f, {1.0f, 1.0f}, stoneMaterial);
next(3.4f, 0.0f, 1.0f, {0.8f, 0.8f}, stoneMaterial);
next(3.6f, -0.3f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
GameObject* goal = next(2.5f, 0.5f, 1.0f, {4.0f, 4.0f}, goalMaterial);
goal->SetName("WinCourse_Goal");
auto* winGoal = goal->AddComponent<WinGoal>();
const glm::vec3 buttonBase = top + glm::vec3{-1.0f, 0.0f, 0.0f};
addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_ButtonPedestal",
buttonBase + glm::vec3{0.0f, 0.55f, 0.0f}, {0.4f, 1.1f, 0.4f},
beamMaterial, RigidbodyType::Static));
// Every button needs its own material, pressing it recolors the material
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.85f, 0.2f}, "WinButtonMaterial");
buttonMaterial.roughnessFactor = 0.4f;
GameObject* button = addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_Button",
buttonBase + glm::vec3{0.0f, 1.15f, 0.0f}, {0.25f, 0.1f, 0.25f},
resources.materials().addMaterial(std::move(buttonMaterial)),
RigidbodyType::Static));
button->AddComponent<InteractableButton>()->AddTarget(*goal);
GameObject* goalLight = addToCourse(scene.CreateGameObject("WinCourse_GoalLight"));
goalLight->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 3.0f, 0.0f});
auto* light = goalLight->AddComponent<LightComponent>(LightType::Point);
light->SetColor(glm::vec3{1.0f, 0.8f, 0.3f});
light->SetIntensity(6.0f);
light->SetRange(8.0f);
return winGoal;
}
struct WallOpening {
float offset;
float width;
float bottom;
float top;
};
std::vector<GameObject*> CreateWall(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const std::string& name,
const glm::vec3& floorCenter, float length, float height, bool alongX, MaterialID materialID,
const std::optional<WallOpening>& opening = std::nullopt)
{
constexpr float thickness = 0.3f;
std::vector<GameObject*> pieces;
const auto createPiece = [&](const std::string& suffix, float from, float to, float bottom, float top)
{
if (to <= from || top <= bottom) {
return;
}
const float along = (from + to) * 0.5f;
const glm::vec3 alongOffset = alongX ? glm::vec3{along, 0.0f, 0.0f} : glm::vec3{0.0f, 0.0f, along};
const glm::vec3 center = floorCenter + alongOffset + glm::vec3{0.0f, (bottom + top) * 0.5f, 0.0f};
const glm::vec3 size = alongX
? glm::vec3{to - from, top - bottom, thickness}
: glm::vec3{thickness, top - bottom, to - from};
pieces.push_back(
CreateBox(scene, gfxDevice, resources, name + suffix, center, size, materialID, RigidbodyType::Static));
};
const float halfLength = length * 0.5f;
if (!opening) {
createPiece("", -halfLength, halfLength, 0.0f, height);
return pieces;
}
const float openingStart = opening->offset - opening->width * 0.5f;
const float openingEnd = opening->offset + opening->width * 0.5f;
createPiece("_Left", -halfLength, openingStart, 0.0f, height);
createPiece("_Right", openingEnd, halfLength, 0.0f, height);
createPiece("_Below", openingStart, openingEnd, 0.0f, opening->bottom);
createPiece("_Above", openingStart, openingEnd, opening->top, height);
return pieces;
}
GameObject* CreateCeilingLight(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
const glm::vec3& position)
{
GameObject* lightObj = scene.CreateGameObject("CeilingLight");
lightObj->GetTransform().SetWorldPosition(position);
// Every bulb needs its own material, the toggle changes its glow
Material bulbMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.95f, 0.85f}, "BulbMaterial");
auto* bulbMesh = lightObj->AddComponent<MeshRendererComponent>();
bulbMesh->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox({0.35f, 0.12f, 0.35f})));
bulbMesh->SetMaterialID(resources.materials().addMaterial(std::move(bulbMaterial)));
auto* light = lightObj->AddComponent<LightComponent>(LightType::Point);
light->SetColor(glm::vec3{1.0f, 0.82f, 0.55f});
light->SetIntensity(4.0f);
light->SetRange(7.0f);
lightObj->AddComponent<LightToggle>();
return lightObj;
}
void CreateHouse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID wallMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.8f);
const MaterialID floorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.7f);
const MaterialID roofMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.8f);
const glm::vec3 center{0.0f, GroundHeight, -16.0f};
constexpr float width = 8.0f;
constexpr float depth = 6.0f;
constexpr float wallHeight = 3.0f;
constexpr float doorOffset = -1.5f;
constexpr float doorWidth = 1.2f;
GameObject* house = scene.CreateGameObject("House");
house->GetTransform().SetWorldPosition(center);
const auto addToHouse = [&](GameObject* object)
{
object->GetTransform().SetParent(&house->GetTransform(), true);
};
const auto addWallToHouse = [&](const std::vector<GameObject*>& pieces)
{
for (GameObject* piece : pieces) {
addToHouse(piece);
}
};
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Floor", center + glm::vec3{0.0f, 0.03f, 0.0f},
{width, 0.06f, depth}, floorMaterial, RigidbodyType::Static));
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_FrontWall", center + glm::vec3{0.0f, 0.0f, depth * 0.5f},
width + 0.3f, wallHeight, true, wallMaterial,
WallOpening{.offset = doorOffset, .width = doorWidth, .bottom = 0.0f, .top = 2.2f}));
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_BackWall", center - glm::vec3{0.0f, 0.0f, depth * 0.5f},
width + 0.3f, wallHeight, true, wallMaterial,
WallOpening{.offset = 1.5f, .width = 1.4f, .bottom = 1.0f, .top = 2.0f}));
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_LeftWall", center - glm::vec3{width * 0.5f, 0.0f, 0.0f},
depth - 0.3f, wallHeight, false, wallMaterial));
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_RightWall", center + glm::vec3{width * 0.5f, 0.0f, 0.0f},
depth - 0.3f, wallHeight, false, wallMaterial,
WallOpening{.offset = 0.0f, .width = 1.6f, .bottom = 1.0f, .top = 2.0f}));
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Roof", center + glm::vec3{0.0f, wallHeight + 0.15f, 0.0f},
{width + 0.8f, 0.3f, depth + 0.8f}, roofMaterial, RigidbodyType::Static));
GameObject* ceilingLight =
CreateCeilingLight(scene, gfxDevice, resources, center + glm::vec3{0.0f, wallHeight - 0.06f, 0.0f});
addToHouse(ceilingLight);
// Inside, next to the door
Material switchMaterial = Material::SimpleColor(glm::vec3{0.9f}, "LightSwitchMaterial");
switchMaterial.roughnessFactor = 0.5f;
const glm::vec3 switchPosition =
center + glm::vec3{doorOffset + doorWidth * 0.5f + 0.4f, 1.3f, depth * 0.5f - 0.15f - 0.03f};
GameObject* lightSwitch = CreateBox(scene, gfxDevice, resources, "LightSwitch", switchPosition,
{0.12f, 0.2f, 0.06f},
resources.materials().addMaterial(std::move(switchMaterial)),
RigidbodyType::Static);
auto* switchButton = lightSwitch->AddComponent<InteractableButton>();
switchButton->SetCycleColors(false);
switchButton->AddTarget(*ceilingLight);
addToHouse(lightSwitch);
GameObject* sofa = ModelSpawner::SpawnStaticModel(
scene, gfxDevice, resources, "game://sofa.glb", StaticModelOptions(),
"Sofa"
);
sofa->GetTransform().SetWorldPosition({-3.7, -1, -17.5});
sofa->GetTransform().SetWorldRotation({180, 0, 180});
sofa->GetTransform().SetWorldScale({3, 3, 3});
addToHouse(sofa);
}
void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
GameObject* helmetObj = ModelSpawner::SpawnStaticModel(
scene, gfxDevice, resources,
"engine://flight-helmet/FlightHelmet.gltf",
StaticModelOptions(),
"FlightHelmet");
helmetObj->GetTransform().SetWorldPosition(glm::vec3(2.f, GroundHeight, 0.f));
helmetObj->GetTransform().SetWorldScale(glm::vec3(2.f));
}
void CreateTree(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
constexpr float scale = 3.0f;
GameObject* treeObj = ModelSpawner::SpawnStaticModel(
scene, gfxDevice, resources,
"engine://tree/base.obj",
StaticModelOptions(),
"Tree");
treeObj->GetTransform().SetWorldPosition(glm::vec3(-7.f, GroundHeight + scale, 7.f));
treeObj->GetTransform().SetWorldScale(glm::vec3(scale));
// The trunk is about 0.14 units thick and runs up through the foliage, colliders are in world units
treeObj->AddComponent<CapsuleCollider>(0.15f * scale, 2.0f * scale);
treeObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
scene.GetPhysics().RegisterGameObject(*treeObj);
}
void CreateCapybara(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
constexpr auto modelReference = "engine://cotw-capybara-male/source/capybara.fbx";
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
options.loadSkeleton = true;
options.loadAnimations = true;
auto charModel = AssetManager::GetInstance().LoadModel(modelReference, options);
ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(charModel, modelReference);
// The model stores an absolute texture path from another machine, so fall back to the shipped texture
const auto charTexturePath = ModelDocUtils::PickTexturePath(
charModel,
charPrimitive,
AssetFS::GetInstance().GetFullPath(
"engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs"));
const MaterialID charMaterialID = resources.materials().addMaterial({
.baseColor = ModelDocUtils::GetImportedBaseColor(charModel, charPrimitive),
.diffuseTexture = resources.loadImageFromFile(gfxDevice, charTexturePath),
.name = ModelDocUtils::GetImportedMaterialName(charModel, charPrimitive, "CharacterMaterial"),
});
GameObject* charObj = scene.CreateGameObject("Character");
auto* charMeshComp = charObj->AddComponent<MeshRendererComponent>();
charMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, charPrimitive.mesh));
charMeshComp->SetMaterialID(charMaterialID);
auto* animator = charObj->AddComponent<Animator>();
animator->setSkeleton(std::move(charModel.skeleton));
for (auto& clip : charModel.animations)
{
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
}
animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run");
charObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
charObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
}
// Logs everything that enters or leaves it, spheres from the "Spawn ball" button fall through it
void CreateTriggerZone(Scene& scene)
{
GameObject* triggerObj = scene.CreateGameObject("TriggerBox");
triggerObj->AddComponent<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f})->SetTrigger(true);
triggerObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
triggerObj->AddComponent<TriggerLoggerComponent>();
triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f});
}
void CreatePlayer(Scene& scene, MainSceneObjects& objects)
{
GameObject* playerObj = scene.CreateGameObject("Player");
playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 0.0f, -6.0f));
playerObj->AddComponent<CapsuleCollider>(0.4f, 1.8f);
playerObj->AddComponent<Rigidbody>();
// Facing the parkour course
auto* controller = playerObj->AddComponent<FirstPersonController>();
controller->SetYaw(glm::half_pi<float>());
playerObj->AddComponent<PlayerRespawn>();
scene.GetPhysics().RegisterGameObject(*playerObj);
objects.player = playerObj;
objects.playerController = controller;
}
// Fills its parent until it is given anchors or a size
RectTransform* CreateUIObject(Scene& scene, const std::string& name, GameObject& parent)
{
GameObject* object = scene.CreateGameObject(name);
object->GetTransform().SetParent(&parent.GetTransform(), false);
auto* rect = object->AddComponent<RectTransform>();
rect->Stretch();
return rect;
}
GameObject* CreateCrosshair(Scene& scene, GameObject& hud)
{
auto* crosshair = CreateUIObject(scene, "Crosshair", hud);
crosshair->SetAnchor({0.5f, 0.5f});
crosshair->SetSize({10.0f, 10.0f});
constexpr float thickness = 2.0f;
auto* horizontal = CreateUIObject(scene, "Crosshair_Horizontal", *crosshair->GetGameObject());
horizontal->SetAnchors({0.0f, 0.5f}, {1.0f, 0.5f});
horizontal->SetSize({0.0f, thickness});
horizontal->GetGameObject()->AddComponent<UIImage>();
auto* vertical = CreateUIObject(scene, "Crosshair_Vertical", *crosshair->GetGameObject());
vertical->SetAnchors({0.5f, 0.0f}, {0.5f, 1.0f});
vertical->SetSize({thickness, 0.0f});
vertical->GetGameObject()->AddComponent<UIImage>();
return crosshair->GetGameObject();
}
GameObject* CreateFocusIndicator(Scene& scene, GameObject& crosshair)
{
GameObject* indicator = CreateUIObject(scene, "FocusIndicator", crosshair)->GetGameObject();
auto* ring = CreateUIObject(scene, "FocusRing", *indicator);
ring->SetAnchor({0.5f, 0.5f});
ring->SetSize({28.0f, 28.0f});
auto* ringImage = ring->GetGameObject()->AddComponent<UIImage>();
ringImage->SetShape(UIImageShape::Ellipse);
ringImage->SetOutline(2.0f);
auto* prompt = CreateUIObject(scene, "InteractPrompt", *indicator);
prompt->SetAnchor({0.5f, 0.5f});
prompt->SetPivot({0.5f, 0.0f});
prompt->SetPosition({0.0f, 28.0f});
prompt->SetSize({400.0f, 30.0f});
auto* promptText = prompt->GetGameObject()->AddComponent<UIText>("[E] Interact", 22.0f);
promptText->SetOutline(2.0f, {0.0f, 0.0f, 0.0f, 0.75f});
promptText->SetAlignment(UITextAlign::Center, UITextAlign::Start);
return indicator;
}
GameObject* CreateWinScreen(Scene& scene, GameObject& hud)
{
GameObject* screen = CreateUIObject(scene, "WinScreen", hud)->GetGameObject();
screen->AddComponent<UIImage>(glm::vec4{0.0f, 0.0f, 0.0f, 0.35f});
auto* titleText = CreateUIObject(scene, "WinScreen_Title", *screen)->GetGameObject()->AddComponent<UIText>("YOU WIN", 195.0f);
titleText->SetColor({1.0f, 0.85f, 0.2f, 1.0f});
titleText->SetOutline(4.0f, {0.0f, 0.0f, 0.0f, 0.9f});
screen->SetActive(false);
return screen;
}
// Sizes are in 1080p pixels, the canvas scales them with the screen height
void CreateHud(Scene& scene, const GameObject& player, const GameObject& winGoal)
{
GameObject* hud = scene.CreateGameObject("HUD");
hud->AddComponent<Canvas>();
GameObject* crosshair = CreateCrosshair(scene, *hud);
const GameObject* focusIndicator = CreateFocusIndicator(scene, *crosshair);
auto* crosshairUi = hud->AddComponent<CrosshairUI>();
crosshairUi->SetPlayer(player);
crosshairUi->SetCrosshair(*crosshair);
crosshairUi->SetFocusIndicator(*focusIndicator);
auto* winScreenUi = hud->AddComponent<WinScreenUI>();
winScreenUi->SetGoal(winGoal);
winScreenUi->SetScreen(*CreateWinScreen(scene, *hud));
}
void LoadSpawnBallResources(GfxDevice& gfxDevice, RenderResources& resources, MainSceneObjects& objects)
{
const auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", StaticModelOptions());
ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx");
const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "engine://sphere.fbx");
objects.sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh);
objects.sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue");
}
}
MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
MainSceneObjects objects{};
scene.GetEnvironment().fogDensity = 0.01f;
CreateSun(scene);
CreateGround(scene, gfxDevice, resources);
CreateParkourCourse(scene, gfxDevice, resources);
CreateFlightHelmet(scene, gfxDevice, resources);
CreateHelmetSpotlight(scene);
CreateTree(scene, gfxDevice, resources);
// CreateCapybara(scene, gfxDevice, resources);
CreateTriggerZone(scene);
const GameObject* door = CreateDoorway(scene, gfxDevice, resources);
CreateButton(scene, gfxDevice, resources, *door);
CreateHouse(scene, gfxDevice, resources);
CreateMovingPlatform(scene, gfxDevice, resources);
const WinGoal* winGoal = CreateWinCourse(scene, gfxDevice, resources);
CreatePlayer(scene, objects);
CreateHud(scene, *objects.player, *winGoal->GetGameObject());
LoadSpawnBallResources(gfxDevice, resources, objects);
return objects;
}
+4 -2
View File
@@ -8,6 +8,7 @@ add_executable(destrum_tests
graphics_tests.cpp
ui_tests.cpp
gameplay_tests.cpp
editor_tests.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/InteractableButton.cpp
@@ -28,12 +29,13 @@ set_target_properties(destrum_tests PROPERTIES
target_compile_definitions(destrum_tests PRIVATE
SDL_MAIN_HANDLED
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
LIGHTKEEPER_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/lightkeeper/assets_src"
)
target_include_directories(destrum_tests PRIVATE "${CMAKE_SOURCE_DIR}/lightkeeper/include")
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
target_link_libraries(destrum_tests PRIVATE destrum::destrum destrum::editor)
destrum_enable_warnings(destrum_tests)
foreach(suite core serialization physics assets graphics ui gameplay)
foreach(suite core serialization physics assets graphics ui gameplay editor)
add_test(NAME destrum_tests.${suite} COMMAND destrum_tests ${suite})
endforeach()

Some files were not shown because too many files have changed in this diff Show More