diff --git a/CMakeLists.txt b/CMakeLists.txt index cbf2adf..a49659e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/Readme.md b/Readme.md index 6f27f75..6ffcc4b 100644 --- a/Readme.md +++ b/Readme.md @@ -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: diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 4161cad..ff48c3a 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -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" diff --git a/destrum/assets_src/cotw-capybara-male/source/capybara.fbx.meta b/destrum/assets_src/cotw-capybara-male/source/capybara.fbx.meta new file mode 100644 index 0000000..0ea6534 --- /dev/null +++ b/destrum/assets_src/cotw-capybara-male/source/capybara.fbx.meta @@ -0,0 +1,3 @@ +{ + "guid": "a09100c14a67b3840518b68b93fbadda" +} \ No newline at end of file diff --git a/destrum/assets_src/flight-helmet/FlightHelmet.gltf.meta b/destrum/assets_src/flight-helmet/FlightHelmet.gltf.meta new file mode 100644 index 0000000..b2eb2af --- /dev/null +++ b/destrum/assets_src/flight-helmet/FlightHelmet.gltf.meta @@ -0,0 +1,3 @@ +{ + "guid": "0407d177c8487bca092ffb7fd4d47aa2" +} \ No newline at end of file diff --git a/destrum/assets_src/grass.jpg.meta b/destrum/assets_src/grass.jpg.meta new file mode 100644 index 0000000..d7e08df --- /dev/null +++ b/destrum/assets_src/grass.jpg.meta @@ -0,0 +1,3 @@ +{ + "guid": "78d91a725664c8b815531d55bc62683f" +} \ No newline at end of file diff --git a/destrum/assets_src/shaders/mesh.frag b/destrum/assets_src/shaders/mesh.frag index 0d863e8..fc0ca88 100644 --- a/destrum/assets_src/shaders/mesh.frag +++ b/destrum/assets_src/shaders/mesh.frag @@ -142,7 +142,7 @@ void main() } if (pcs.sceneData.debugSelectionView != 0) { - fragColor = selectionColor(inObjectID); + fragColor = selectionColor(inObjectID & 0x7FFFFFFFu); } #if 0 diff --git a/destrum/assets_src/shaders/post.frag b/destrum/assets_src/shaders/post.frag new file mode 100644 index 0000000..4812ac5 --- /dev/null +++ b/destrum/assets_src/shaders/post.frag @@ -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); +} diff --git a/destrum/assets_src/tree/base.obj.meta b/destrum/assets_src/tree/base.obj.meta new file mode 100644 index 0000000..e98a5af --- /dev/null +++ b/destrum/assets_src/tree/base.obj.meta @@ -0,0 +1,3 @@ +{ + "guid": "c2f1f9dcb185460c6d685c17d7ec4f82" +} \ No newline at end of file diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index a28062b..32f2c0f 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -1,29 +1,22 @@ #ifndef APP_H #define APP_H -#include + #include +#include #include -#include #include -#include - -#include "glm/vec2.hpp" - -#include +#include +#include #include +#include #include #include +#include #include -#include -#include - #include - -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 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 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 editSnapshot; - std::weak_ptr 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 diff --git a/destrum/include/destrum/AppLayer.h b/destrum/include/destrum/AppLayer.h new file mode 100644 index 0000000..aa064d1 --- /dev/null +++ b/destrum/include/destrum/AppLayer.h @@ -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 diff --git a/destrum/include/destrum/Assets/AssetDatabase.h b/destrum/include/destrum/Assets/AssetDatabase.h new file mode 100644 index 0000000..cac3109 --- /dev/null +++ b/destrum/include/destrum/Assets/AssetDatabase.h @@ -0,0 +1,64 @@ +#ifndef DESTRUM_ASSETDATABASE_H +#define DESTRUM_ASSETDATABASE_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +struct AssetGuid { + std::uint64_t high{0}; + std::uint64_t low{0}; + + [[nodiscard]] static AssetGuid Generate(); + [[nodiscard]] static std::optional 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 { + std::size_t operator()(const AssetGuid& guid) const noexcept + { + return std::hash{}(guid.high ^ (guid.low * 0x9E3779B97F4A7C15ull)); + } +}; + +class AssetDatabase final : public Singleton { +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 FindGuid(std::string_view path) const; + [[nodiscard]] std::optional FindPath(const AssetGuid& guid) const; + std::optional GuidForSave(std::string_view path); + + [[nodiscard]] nlohmann::json WriteReference(std::string_view path, std::string_view subresource = {}); + [[nodiscard]] std::optional ReadReference(const nlohmann::json& reference) const; + +private: + std::unordered_map m_Guids; + std::unordered_map m_Paths; + int m_MetaFileScopes{0}; +}; + +#endif // DESTRUM_ASSETDATABASE_H diff --git a/destrum/include/destrum/Assets/AssetTypes.h b/destrum/include/destrum/Assets/AssetTypes.h new file mode 100644 index 0000000..cc6d4f4 --- /dev/null +++ b/destrum/include/destrum/Assets/AssetTypes.h @@ -0,0 +1,38 @@ +#ifndef DESTRUM_ASSETTYPES_H +#define DESTRUM_ASSETTYPES_H + +#include +#include +#include +#include +#include +#include + +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(std::tolower(c)); }); + return extension; + } + + [[nodiscard]] inline bool HasExtension(std::string_view path, std::initializer_list 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 diff --git a/destrum/include/destrum/FS/AssetFS.h b/destrum/include/destrum/FS/AssetFS.h index 68d5bff..2dcc31e 100644 --- a/destrum/include/destrum/FS/AssetFS.h +++ b/destrum/include/destrum/FS/AssetFS.h @@ -17,7 +17,7 @@ struct FSMount { std::string scheme; // "engine", "game" std::filesystem::path root; std::optional manifest; - + std::optional sourceRoot; }; class AssetFS final: public Singleton { @@ -36,6 +36,11 @@ public: [[nodiscard]] const std::vector& GetMounts() const { return mounts; } + void SetSourceRoot(std::string_view scheme, std::filesystem::path sourceRoot); + [[nodiscard]] std::optional GetSourcePath(std::string_view vpath) const; + + bool WriteFile(std::string_view vpath, std::string_view contents); + private: static std::vector ReadFile(const std::filesystem::path& fullPath); diff --git a/destrum/include/destrum/Graphics/Caches/MaterialCache.h b/destrum/include/destrum/Graphics/Caches/MaterialCache.h index 0abae75..bc6de2d 100644 --- a/destrum/include/destrum/Graphics/Caches/MaterialCache.h +++ b/destrum/include/destrum/Graphics/Caches/MaterialCache.h @@ -1,9 +1,10 @@ #ifndef MATERIALCACHE_H #define MATERIALCACHE_H -#include +#include #include #include +#include #include #include @@ -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 materials; std::vector materialKeys; + std::shared_ptr> freeIds = std::make_shared>(); static constexpr auto MAX_MATERIALS = 1000; GPUBuffer materialDataBuffer; diff --git a/destrum/include/destrum/Graphics/Caches/MeshCache.h b/destrum/include/destrum/Graphics/Caches/MeshCache.h index b6cdfe8..07bc04e 100644 --- a/destrum/include/destrum/Graphics/Caches/MeshCache.h +++ b/destrum/include/destrum/Graphics/Caches/MeshCache.h @@ -3,8 +3,10 @@ #include +#include #include #include +#include #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& 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 ids; + std::vector 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 meshes; std::vector cpuMeshes; std::vector meshKeys; + std::shared_ptr recycleBin = std::make_shared(); }; #endif //MESHCACHE_H diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index 330bbfb..cef6ec8 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.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}; }; diff --git a/destrum/include/destrum/Graphics/Pipelines/PostProcessPipeline.h b/destrum/include/destrum/Graphics/Pipelines/PostProcessPipeline.h new file mode 100644 index 0000000..823be86 --- /dev/null +++ b/destrum/include/destrum/Graphics/Pipelines/PostProcessPipeline.h @@ -0,0 +1,47 @@ +#ifndef DESTRUM_POSTPROCESSPIPELINE_H +#define DESTRUM_POSTPROCESSPIPELINE_H + +#include +#include +#include + +#include +#include + +#include +#include +#include + +class GfxDevice; +class RenderResources; + +class PostProcessPipeline final { +public: + struct Settings { + ImageID sceneColor{NULL_IMAGE_ID}; + std::optional selection; + std::optional 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 m_Pipeline; +}; + +#endif // DESTRUM_POSTPROCESSPIPELINE_H diff --git a/destrum/include/destrum/Graphics/RenderResources.h b/destrum/include/destrum/Graphics/RenderResources.h index b858850..7d312bb 100644 --- a/destrum/include/destrum/Graphics/RenderResources.h +++ b/destrum/include/destrum/Graphics/RenderResources.h @@ -10,6 +10,7 @@ #include #include #include +#include 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& usedMeshes, + const std::unordered_set& usedMaterials); + ImageID addImageToCache(GPUImage image); void destroyImage(ImageID id); @@ -86,6 +99,9 @@ private: std::unique_ptr meshCache; std::unique_ptr materialCache; + std::unordered_set materialTextures; + std::unordered_set pinnedImages; + ImageID whiteImageId{NULL_IMAGE_ID}; ImageID errorImageId{NULL_IMAGE_ID}; ImageID defaultNormalImageId{NULL_IMAGE_ID}; diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 1957b67..6674d41 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -22,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -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 pixel) + { + hoverPixel = pixel; + } + + void setHighlightedObjects(std::span objectIds) + { + highlightedObjects = {objectIds.begin(), objectIds.end()}; + } + + bool reloadShaders(GfxDevice& gfxDevice); + [[nodiscard]] std::optional 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 pickPending{}; std::optional requestedPick; std::optional pickResult; + std::optional hoverPixel; + std::unordered_set highlightedObjects; bool selectionPassEnabled{false}; bool renderSelectionView{false}; @@ -228,6 +253,7 @@ private: std::unique_ptr skinningPipeline; std::unique_ptr lightCullingPipeline; std::unique_ptr shadowMapPipeline; + std::unique_ptr postProcessPipeline; std::optional shadowLightDirection; std::vector submittedLights; diff --git a/destrum/include/destrum/Graphics/Tonemapper.h b/destrum/include/destrum/Graphics/Tonemapper.h new file mode 100644 index 0000000..99cbe64 --- /dev/null +++ b/destrum/include/destrum/Graphics/Tonemapper.h @@ -0,0 +1,33 @@ +#ifndef DESTRUM_TONEMAPPER_H +#define DESTRUM_TONEMAPPER_H + +#include +#include +#include +#include + +// Must match the TONEMAPPER_ defines in post.frag +enum class Tonemapper : std::uint32_t { + None, + Aces, + PbrNeutral, +}; + +inline constexpr std::array TonemapperNames{"none", "aces", "pbrNeutral"}; + +[[nodiscard]] inline std::string_view ToString(Tonemapper tonemapper) +{ + return TonemapperNames.at(static_cast(tonemapper)); +} + +[[nodiscard]] inline std::optional ParseTonemapper(std::string_view name) +{ + for (std::size_t i = 0; i < TonemapperNames.size(); ++i) { + if (TonemapperNames[i] == name) { + return static_cast(i); + } + } + return std::nullopt; +} + +#endif // DESTRUM_TONEMAPPER_H diff --git a/destrum/include/destrum/Scene/ResourceCollector.h b/destrum/include/destrum/Scene/ResourceCollector.h new file mode 100644 index 0000000..ead6b6a --- /dev/null +++ b/destrum/include/destrum/Scene/ResourceCollector.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 diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index df23ce1..5f9fa48 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -6,9 +6,9 @@ #include #include +#include #include #include -#include #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 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 m_registerBindings; std::function m_unregisterBindings; diff --git a/destrum/include/destrum/Scene/SceneManager.h b/destrum/include/destrum/Scene/SceneManager.h index 2123f13..baf41d5 100644 --- a/destrum/include/destrum/Scene/SceneManager.h +++ b/destrum/include/destrum/Scene/SceneManager.h @@ -28,7 +28,6 @@ public: void LateUpdate(float dt); void Render(const RenderContext& ctx); - void RenderImgui(bool* open = nullptr); void HandleGameObjectDestroy(); void DestroyGameObjects(); diff --git a/destrum/include/destrum/Serialization/MaterialSerialization.h b/destrum/include/destrum/Serialization/MaterialSerialization.h index 60be578..6e58a55 100644 --- a/destrum/include/destrum/Serialization/MaterialSerialization.h +++ b/destrum/include/destrum/Serialization/MaterialSerialization.h @@ -4,22 +4,32 @@ #include #include #include +#include #include #include +class GfxDevice; +class RenderResources; + namespace MaterialSerialization { + inline constexpr int CurrentFileVersion = 1; + using TextureWriter = std::function; using TextureReader = std::function; [[nodiscard]] nlohmann::json ToJson(const Material& material, const TextureWriter& writeTexture); [[nodiscard]] Material FromJson(const nlohmann::json& data, const TextureReader& readTexture); - [[nodiscard]] std::optional 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 diff --git a/destrum/include/destrum/Serialization/ObjectSerialization.h b/destrum/include/destrum/Serialization/ObjectSerialization.h index 9ee8e5a..07749e2 100644 --- a/destrum/include/destrum/Serialization/ObjectSerialization.h +++ b/destrum/include/destrum/Serialization/ObjectSerialization.h @@ -18,7 +18,7 @@ namespace ObjectSerialization { using ObjectMap = std::unordered_map; using ValidationError = std::optional; - constexpr int CurrentObjectVersion = 2; + constexpr int CurrentObjectVersion = 3; [[nodiscard]] std::optional FindNonFinite(const nlohmann::json& value, const std::string& path); diff --git a/destrum/include/destrum/Serialization/PrefabSerializer.h b/destrum/include/destrum/Serialization/PrefabSerializer.h index 7a18ddb..9d1daad 100644 --- a/destrum/include/destrum/Serialization/PrefabSerializer.h +++ b/destrum/include/destrum/Serialization/PrefabSerializer.h @@ -14,7 +14,7 @@ class Transform; class PrefabSerializer final { public: - static constexpr int CurrentPrefabVersion = 1; + static constexpr int CurrentPrefabVersion = 2; [[nodiscard]] static std::optional Capture(const GameObject& root); static bool Save(const GameObject& root, const std::filesystem::path& path); diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 7ed7c63..6dd8b46 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -1,59 +1,61 @@ -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "imgui.h" -#include "imgui_internal.h" -#include "glm/gtx/transform.hpp" -#include "spdlog/spdlog.h" +#include +#include +#include +#include +#include +#include #include #include -struct TracyFrameScope -{ - ~TracyFrameScope() - { - FrameMark; - } -}; +#include +#include +#include +#include +#include +#include -App::App() -{ +namespace { + struct TracyFrameScope { + ~TracyFrameScope() { FrameMark; } + }; + + constexpr float MaxFrameDelta = 0.25f; + constexpr int MaxFixedSteps = 5; + + [[nodiscard]] bool IsMouseEvent(const SDL_Event& event) + { + return event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || + event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL; + } + + [[nodiscard]] bool IsKeyboardEvent(const SDL_Event& event) + { + return event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_TEXTINPUT; + } } +App::App() = default; + App::~App() { if (!cleanedUp) { try { cleanup(); } catch (...) { - // Destructors must not throw. Initialization failures are already - // reported by the caller, while cleanup is best effort here. } } } void App::init(const AppParams& params) { + ZoneScopedN("App::init"); cleanedUp = false; customInitStarted = false; try { m_params = params; - ZoneScopedN("App::init"); tracy::SetThreadName("Main Thread"); TracySetProgramName(params.appName.c_str()); AssetFS::GetInstance().Init(params.exeDir); @@ -67,15 +69,14 @@ void App::init(const AppParams& params) gfxDevice.init(window.GetHandle(), params.appName, false); imguiPass.init(window.GetHandle(), gfxDevice); + resources.init(gfxDevice); + renderer.init(gfxDevice, resources, params.renderSize); InputManager::GetInstance().Init(); Time::GetInstance().Update(); customInitStarted = true; customInit(); - playing = !params.startInEditMode; - SceneManager::GetInstance().SetPlaying(playing); - cleanedUp = false; } catch (...) { try { cleanup(); @@ -85,320 +86,278 @@ void App::init(const AppParams& params) } } +void App::attachLayer(std::unique_ptr newLayer) +{ + if (layer) { + layer->onDetach(); + } + layer = std::move(newLayer); + if (layer) { + layer->onAttach(); + } +} + +bool App::isPlaying() const +{ + return SceneManager::GetInstance().IsPlaying(); +} + +bool App::isPaused() const +{ + return SceneManager::GetInstance().IsPaused(); +} + +float App::getRenderAspectRatio() const +{ + const VkExtent3D extent = renderer.getDrawImage().extent; + return extent.height > 0 ? static_cast(extent.width) / static_cast(extent.height) : 1.0f; +} + +Camera& App::getViewCamera() +{ + if (layer) { + if (Camera* camera = layer->viewCamera()) { + return *camera; + } + } + if (Camera* camera = gameCamera()) { + return *camera; + } + return fallbackCamera; +} + +void App::resizeRenderTarget(const glm::ivec2& size) +{ + renderer.resize(gfxDevice, size); +} + +void App::collectUnusedResources() +{ + CollectUnusedResources(SceneManager::GetInstance(), resources, gfxDevice); +} + void App::run() { ZoneScopedN("App::run"); Time::GetInstance().Update(); - - const float fixedDt = static_cast(Time::GetInstance().FixedDeltaTime()); - const int maxSteps = 5; - float accumulator = 0.0f; - + simulation.fixedDeltaTime = static_cast(Time::GetInstance().FixedDeltaTime()); isRunning = true; - while (isRunning) - { + while (isRunning) { TracyFrameScope tracyFrame; ZoneScopedN("App::Frame"); Time::GetInstance().Update(); - float dt = static_cast(Time::GetInstance().DeltaTime()); - - if (dt > 0.25f) dt = 0.25f; - - if (dt > 0.0f) - { - float newFPS = 1.0f / dt; - avgFPS = std::lerp(avgFPS, newFPS, 0.1f); + const float dt = std::min(static_cast(Time::GetInstance().DeltaTime()), MaxFrameDelta); + if (dt > 0.0f) { + avgFPS = std::lerp(avgFPS, 1.0f / dt, 0.1f); TracyPlot("FPS", avgFPS); - TracyPlot("Delta Time ms", dt * 1000.0f); + } + simulation.accumulator += dt; + + processEvents(); + if (!isRunning) { + break; } - accumulator += dt; - - { - ZoneScopedN("Input BeginFrame"); - InputManager::GetInstance().BeginFrame(); - } - - { - ZoneScopedN("SDL Events"); - - SDL_Event event; - while (SDL_PollEvent(&event)) - { - ZoneScopedN("SDL Event"); - - imguiPass.handleEvent(event); - - const bool mouseEvent = - event.type == SDL_MOUSEBUTTONDOWN || - event.type == SDL_MOUSEBUTTONUP || - event.type == SDL_MOUSEMOTION || - event.type == SDL_MOUSEWHEEL; - - if (event.type == SDL_QUIT) - { - isRunning = false; - break; - } - - window.ProcessEvent(event); - if (window.ConsumeResized()) - { - resizePending = true; - lastResizeTime = std::chrono::steady_clock::now(); - } - if (window.ConsumeCloseRequested()) - { - isRunning = false; - break; - } - - const bool keyboardEvent = - event.type == SDL_KEYDOWN || - event.type == SDL_KEYUP || - event.type == SDL_TEXTINPUT; - - // ImGui wants every event while a window is hovered or focused, the editor viewport - // is one, so the game gets them there instead - const bool mouseCaptured = InputManager::GetInstance().IsMouseCaptured(); - const bool viewportGetsMouse = usesEditorViewport() && viewportHovered && !viewportMouseClaimed; - const bool viewportGetsKeyboard = usesEditorViewport() && (viewportFocused || mouseCaptured) && - !ImGui::GetIO().WantTextInput; - const bool capturedByImgui = - (mouseEvent && !mouseCaptured && !viewportGetsMouse && imguiPass.wantsMouse()) || - (keyboardEvent && !viewportGetsKeyboard && imguiPass.wantsKeyboard()); - - if (!capturedByImgui) - { - ZoneScopedN("Input ProcessEvent"); - - if (InputManager::GetInstance().ProcessEvent(event)) - { - isRunning = false; - } - } - } - } - -if (!isRunning) break; - - if (window.IsMinimized()) - { + if (window.IsMinimized()) { // The surface is 0x0 while minimized, there is nothing to render or present to - accumulator = 0.0f; + simulation.accumulator = 0.0f; std::this_thread::sleep_for(std::chrono::milliseconds(16)); continue; } - // Consume SDL events before updating the base camera so held and - // newly pressed inputs are applied in the same frame. - camera.Update(dt); + if (layer) { + layer->onBeginFrame(); + } - if (usesEditorViewport()) - { - applyViewportResize(); + imguiPass.beginFrame(); + if (layer) { + layer->onImGuiBegin(dt); } { - ZoneScopedN("ImGui BeginFrame"); - imguiPass.beginFrame(); - } - - { - // Global shortcuts, also when an editor window has keyboard focus - const auto& input = InputManager::GetInstance(); - const bool typing = ImGui::GetIO().WantTextInput; - if (input.WasKeyPressed(SDL_SCANCODE_F5) || (!typing && ImGui::IsKeyPressed(ImGuiKey_F5, false))) - { - playModeToggleRequested = true; - } - if (playing && (input.WasKeyPressed(SDL_SCANCODE_F7) || (!typing && ImGui::IsKeyPressed(ImGuiKey_F7, false)))) - { - setPaused(!paused); - } - } - - if (usesEditorViewport()) - { - drawDockSpace(); - } - - { - ZoneScopedN("customUpdate"); + ZoneScopedN("Update"); customUpdate(dt); + SceneManager::GetInstance().Update(dt); + SceneManager::GetInstance().LateUpdate(dt); } - if (m_params.showDebugUi) - { - ZoneScopedN("Debug ImGui"); - drawWindowsMenu(); - applySelectionPick(); - drawViewportWindow(); + if (layer) { + ZoneScopedN("Layer ImGui"); + layer->onImGui(dt); + } + imguiPass.endFrame(); - if (m_DebugWindows.inspector) - { - SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector); - } - - if (m_DebugWindows.stats) - { - if (ImGui::Begin("Debug", &m_DebugWindows.stats)) - { - ImGui::Text("FPS: %.2f", avgFPS); - - const auto& stats = renderer.getDrawStats(); - ImGui::SeparatorText("Meshes"); - ImGui::Text("Submitted meshes: %u", stats.meshDraws); - ImGui::Text("Camera: %u indirect draws, %u instances", stats.mainViewCommands, stats.mainViewInstances); - ImGui::Text("All views: %u indirect draws, %u instances", stats.indirectCommands, stats.instances); - ImGui::Text("Pending GPU deletions: %zu", gfxDevice.getPendingDeletionCount()); - } - ImGui::End(); - } - - if (m_DebugWindows.physics) - { - drawPhysicsPanel(dt, fixedDt, accumulator); - } - - if (m_DebugWindows.assets) - { - m_AssetBrowser.Draw(&m_DebugWindows.assets); - } - - if (m_DebugWindows.imageCache) - { - m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass); - } - - TracyPlot("FPS", avgFPS); - TracyPlot("Frame Time ms", dt * 1000.0f); - TracyPlot("Accumulator ms", accumulator * 1000.0f); + if (layer) { + layer->onEndFrame(); } - { - ZoneScopedN("ImGui EndFrame"); - imguiPass.endFrame(); + runFixedSteps(); + + bool skipFrame = false; + handleSwapchainResize(skipFrame); + if (skipFrame) { + continue; } - if (playModeToggleRequested) - { - ZoneScopedN("Play Mode Toggle"); - playModeToggleRequested = false; - applyPlayModeToggle(); - accumulator = 0.0f; - } + renderFrame(); - { - ZoneScopedN("FixedUpdate"); - - int steps = 0; - - if (m_PhysicsPaused) - { - // Important: prevent physics from building up a huge backlog while paused. - accumulator = 0.0f; - - if (m_PhysicsStepOnce) - { - ZoneScopedN("Single Physics Step"); - - customFixedUpdate(fixedDt * m_PhysicsTimeScale); - - steps = 1; - m_PhysicsStepOnce = false; - } - } - else - { - while (accumulator >= fixedDt && steps < maxSteps) - { - ZoneScopedN("Fixed Step"); - - customFixedUpdate(fixedDt * m_PhysicsTimeScale); - - accumulator -= fixedDt; - steps++; - } - - if (steps == maxSteps) - { - accumulator = 0.0f; - } - } - - m_PhysicsStepsLastFrame = steps; - - TracyPlot("Fixed Steps", static_cast(steps)); - } - - Time::GetInstance().SetFixedAlpha(std::clamp(accumulator / fixedDt, 0.0f, 1.0f)); - - { - ZoneScopedN("Swapchain Resize Check"); - - if (gfxDevice.needsSwapchainRecreate() || resizePending) - { - auto now = std::chrono::steady_clock::now(); - - if (resizePending && - now - lastResizeTime < std::chrono::milliseconds(100)) - { - continue; - } - - const glm::ivec2 drawableSize = window.GetDrawableSize(); - const int w = drawableSize.x; - const int h = drawableSize.y; - - if (w == 0 || h == 0) - { - continue; - } - - { - ZoneScopedN("Recreate Swapchain"); - if (!gfxDevice.recreateSwapchain(w, h)) - { - continue; - } - spdlog::info("Recreated swapchain at {}x{}", w, h); - imguiPass.onSwapchainRecreated(); - if (!usesEditorViewport()) - { - renderer.resize(gfxDevice, drawableSize); - onViewportResize(w, h); - } - onWindowResize(w, h); - } - - resizePending = false; - continue; - } - } - - { - ZoneScopedN("customDraw"); - renderer.setSelectionPassEnabled(canPickInViewport()); - customDraw(); - } - - if (frameLimit) - { - ZoneScopedN("Frame Limit Sleep"); - - auto sleepTime = Time::GetInstance().SleepDuration(); - if (sleepTime.count() > 0) - { + if (frameLimit) { + if (const auto sleepTime = Time::GetInstance().SleepDuration(); sleepTime.count() > 0) { std::this_thread::sleep_for(sleepTime); } } } } +void App::processEvents() +{ + ZoneScopedN("SDL Events"); + auto& input = InputManager::GetInstance(); + input.BeginFrame(); + + SDL_Event event; + while (SDL_PollEvent(&event)) { + imguiPass.handleEvent(event); + + if (event.type == SDL_QUIT) { + isRunning = false; + return; + } + + window.ProcessEvent(event); + if (window.ConsumeResized()) { + resizePending = true; + lastResizeTime = std::chrono::steady_clock::now(); + } + if (window.ConsumeCloseRequested()) { + isRunning = false; + return; + } + + // ImGui wants every event while one of its windows is hovered or focused, a layer can claim + // them back for the game (the editor viewport is an ImGui window) + const AppLayer::InputFocus focus = layer ? layer->gameInputFocus() : AppLayer::InputFocus{}; + const bool mouseCaptured = input.IsMouseCaptured(); + const bool capturedByImgui = + (IsMouseEvent(event) && !mouseCaptured && !focus.mouse && imguiPass.wantsMouse()) || + (IsKeyboardEvent(event) && !focus.keyboard && imguiPass.wantsKeyboard()); + + if (!capturedByImgui && input.ProcessEvent(event)) { + isRunning = false; + } + } +} + +void App::runFixedSteps() +{ + ZoneScopedN("FixedUpdate"); + const float step = simulation.fixedDeltaTime * simulation.timeScale; + int steps = 0; + + const auto fixedUpdate = [&] { + customFixedUpdate(step); + SceneManager::GetInstance().FixedUpdate(step); + }; + + if (simulation.paused) { + simulation.accumulator = 0.0f; + if (simulation.stepOnce) { + fixedUpdate(); + steps = 1; + simulation.stepOnce = false; + } + } else { + while (simulation.accumulator >= simulation.fixedDeltaTime && steps < MaxFixedSteps) { + fixedUpdate(); + simulation.accumulator -= simulation.fixedDeltaTime; + ++steps; + } + if (steps == MaxFixedSteps) { + simulation.accumulator = 0.0f; + } + } + + simulation.stepsLastFrame = steps; + Time::GetInstance().SetFixedAlpha(std::clamp(simulation.accumulator / simulation.fixedDeltaTime, 0.0f, 1.0f)); +} + +void App::handleSwapchainResize(bool& skipFrame) +{ + if (!gfxDevice.needsSwapchainRecreate() && !resizePending) { + return; + } + + skipFrame = true; + if (resizePending && std::chrono::steady_clock::now() - lastResizeTime < std::chrono::milliseconds(100)) { + return; + } + + const glm::ivec2 size = window.GetDrawableSize(); + if (size.x == 0 || size.y == 0 || !gfxDevice.recreateSwapchain(size.x, size.y)) { + return; + } + + spdlog::info("Recreated swapchain at {}x{}", size.x, size.y); + imguiPass.onSwapchainRecreated(); + if (!layer || !layer->hostsViewport()) { + resizeRenderTarget(size); + } + onWindowResize(size.x, size.y); + resizePending = false; +} + +void App::renderFrame() +{ + ZoneScopedN("Render"); + const VkCommandBuffer cmd = gfxDevice.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + return; + } + + renderer.beginDrawing(gfxDevice); + customRender(renderer); + if (layer) { + layer->onRender(renderer); + } + + Camera& camera = getViewCamera(); + if (const float aspectRatio = getRenderAspectRatio(); camera.GetAspectRatio() != aspectRatio) { + camera.setAspectRatio(aspectRatio); + } + + auto& sceneManager = SceneManager::GetInstance(); + const SceneEnvironment environment = sceneManager.GetActiveScenes().empty() + ? SceneEnvironment{} + : sceneManager.GetCurrentScene().GetEnvironment(); + const GameRenderer::SceneData sceneData{ + .camera = camera, + .ambientColor = environment.ambientColor, + .ambientIntensity = environment.ambientIntensity, + .fogColor = environment.fogColor, + .fogDensity = environment.fogDensity, + .skyboxRotation = environment.skyboxRotation, + .exposure = environment.exposure, + .tonemapper = environment.tonemapper, + }; + + sceneManager.Render(RenderContext{ + .renderer = renderer, + .camera = camera, + .sceneData = sceneData, + }); + renderer.endDrawing(); + renderer.draw(cmd, gfxDevice, camera, sceneData); + + gfxDevice.endFrame(cmd, renderer.getDrawImage(), { + .clearColor = {{0.0f, 0.0f, 0.0f, 1.0f}}, + .imguiPass = &imguiPass, + .copyDrawImageToSwapchain = !layer || !layer->hostsViewport(), + }); +} + void App::cleanup() { if (cleanedUp || cleaningUp) { @@ -423,6 +382,12 @@ void App::cleanup() // pipelines and other objects directly, which is only valid once nothing is in flight. attempt([this] { gfxDevice.waitIdle(); }); + attempt([this] { + if (layer) { + layer->onDetach(); + layer.reset(); + } + }); if (customInitStarted) { attempt([this] { customCleanup(); }); } @@ -444,369 +409,3 @@ void App::cleanup() std::rethrow_exception(firstException); } } - -void App::drawWindowsMenu() -{ - // ImGui appends to the main menu bar, so this ends up next to whatever menus the game added - if (!ImGui::BeginMainMenuBar()) - { - return; - } - - if (ImGui::BeginMenu("Windows")) - { - ImGui::MenuItem("Debug", nullptr, &m_DebugWindows.stats); - ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics); - ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector); - ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache); - ImGui::MenuItem("Assets", nullptr, &m_DebugWindows.assets); - - customWindowsMenu(); - - ImGui::EndMenu(); - } - - drawPlayControls(); - - ImGui::EndMainMenuBar(); -} - -void App::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 - 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, ¢er); - const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.22f, nullptr, ¢er); - const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, ¢er); - - ImGui::DockBuilderDockWindow("Viewport", center); - ImGui::DockBuilderDockWindow("Inspector", right); - ImGui::DockBuilderDockWindow("Debug", left); - ImGui::DockBuilderDockWindow("Physics", left); - ImGui::DockBuilderDockWindow("Assets", bottom); - ImGui::DockBuilderFinish(dockspaceId); -} - -void App::drawViewportWindow() -{ - viewportMouseClaimed = 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) - { - viewportHovered = false; - viewportFocused = false; - ImGui::End(); - return; - } - - const ImVec2 origin = ImGui::GetCursorScreenPos(); - const ImVec2 available = ImGui::GetContentRegionAvail(); - const ImVec2 framebufferScale = ImGui::GetIO().DisplayFramebufferScale; - viewportRect = { - .min = {origin.x, origin.y}, - .size = {std::max(available.x, 1.0f), std::max(available.y, 1.0f)}, - }; - viewportDesiredSize = { - std::max(static_cast(available.x * framebufferScale.x), 1), - std::max(static_cast(available.y * framebufferScale.y), 1), - }; - - const GPUImage& drawImage = renderer.getDrawImage(); - if (const ImTextureID texture = imguiPass.getFrameTexture(drawImage.imageView); texture != ImTextureID{}) - { - ImGui::Image(texture, ImVec2{viewportRect.size.x, viewportRect.size.y}); - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) - { - onViewportAssetDropped(static_cast(payload->Data)); - } - ImGui::EndDragDropTarget(); - } - } - - viewportHovered = ImGui::IsWindowHovered(); - // ImGui only focuses windows on a left click, the free camera looks around with the right button - if (viewportHovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Right) || ImGui::IsMouseClicked(ImGuiMouseButton_Middle))) - { - ImGui::SetWindowFocus(); - } - viewportFocused = ImGui::IsWindowFocused(); - - ImGui::SetCursorScreenPos(origin); - drawViewportOverlay(); - requestSelectionPick(); - - ImGui::End(); -} - -void App::requestSelectionPick() -{ - if (!canPickInViewport() || !viewportHovered || viewportMouseClaimed || - !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - { - return; - } - - const ImVec2 mouse = ImGui::GetMousePos(); - const glm::vec2 uv = (glm::vec2{mouse.x, mouse.y} - viewportRect.min) / viewportRect.size; - if (uv.x < 0.0f || uv.y < 0.0f || uv.x >= 1.0f || uv.y >= 1.0f) - { - return; - } - - const VkExtent3D extent = renderer.getDrawImage().extent; - renderer.requestSelectionPick(glm::ivec2{uv * glm::vec2{static_cast(extent.width), static_cast(extent.height)}}); - pickSelectsParent = ImGui::GetIO().KeyAlt; -} - -void App::applySelectionPick() -{ - const auto picked = renderer.takeSelectionPick(); - auto& sceneManager = SceneManager::GetInstance(); - if (!picked || sceneManager.GetActiveScenes().empty()) - { - return; - } - - Scene& scene = sceneManager.GetCurrentScene(); - GameObject* object = scene.FindGameObject(*picked); - if (object != nullptr && pickSelectsParent) - { - if (const Transform* parent = object->GetTransform().GetParent(); parent != nullptr && parent->GetOwner() != nullptr) - { - object = parent->GetOwner(); - } - } - scene.SetSelectedObject(object != nullptr ? object->GetId() : InvalidObjectId); -} - -void App::applyViewportResize() -{ - if (viewportDesiredSize.x <= 0 || viewportDesiredSize.y <= 0) - { - return; - } - - const VkExtent3D current = renderer.getDrawImage().extent; - if (static_cast(current.width) == viewportDesiredSize.x && - static_cast(current.height) == viewportDesiredSize.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; - } - - ZoneScopedN("Resize Viewport"); - renderer.resize(gfxDevice, viewportDesiredSize); - onViewportResize(viewportDesiredSize.x, viewportDesiredSize.y); -} - -void App::drawPlayControls() -{ - 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)) - { - setPaused(!paused); - } - if (ImGui::MenuItem(action)) - { - playModeToggleRequested = true; - } -} - -void App::setPaused(bool pause) -{ - if (!playing || pause == paused) - { - return; - } - - paused = pause; - 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) - { - mouseCapturedBeforePause = input.IsMouseCaptured(); - input.SetMouseCaptured(false); - } - else - { - input.SetMouseCaptured(mouseCapturedBeforePause); - } - spdlog::info(paused ? "Paused play mode" : "Resumed play mode"); -} - -void App::applyPlayModeToggle() -{ - if (playing) - { - exitPlayMode(); - } - else - { - enterPlayMode(); - } -} - -bool App::enterPlayMode() -{ - 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 = 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; - } - - editSnapshot = std::move(snapshot); - editScene = scene; - playing = true; - sceneManager.SetPlaying(true); - spdlog::info("Entered play mode"); - onPlayModeChanged(true); - return true; -} - -void App::exitPlayMode() -{ - auto& sceneManager = SceneManager::GetInstance(); - playing = false; - paused = false; - sceneManager.SetPlaying(false); - sceneManager.SetPaused(false); - InputManager::GetInstance().SetMouseCaptured(false); - - const std::shared_ptr scene = editScene.lock(); - if (scene && editSnapshot) - { - const auto& scenes = sceneManager.GetScenes(); - const auto it = std::find(scenes.begin(), scenes.end(), scene); - if (it != scenes.end() && (sceneManager.GetActiveScenes().empty() || - sceneManager.GetActiveScenes().front() != scene)) - { - sceneManager.SwitchScene(static_cast(std::distance(scenes.begin(), it))); - } - if (!SceneSerializer::LoadFromJson(*scene, *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"); - } - - editSnapshot.reset(); - editScene.reset(); - spdlog::info("Exited play mode"); - onPlayModeChanged(false); -} - -void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) -{ - if (!ImGui::Begin("Physics", &m_DebugWindows.physics)) - { - ImGui::End(); - return; - } - - ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f); - ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f); - ImGui::Text("Accumulator: %.3f ms", accumulator * 1000.0f); - ImGui::Text("Steps last frame: %d", m_PhysicsStepsLastFrame); - - ImGui::Separator(); - - if (ImGui::Button(m_PhysicsPaused ? "Resume Physics" : "Pause Physics")) - { - m_PhysicsPaused = !m_PhysicsPaused; - } - - ImGui::SameLine(); - - if (!m_PhysicsPaused) - { - ImGui::BeginDisabled(); - ImGui::Button("Step Physics"); - ImGui::EndDisabled(); - } - else - { - if (ImGui::Button("Step Physics")) - { - m_PhysicsStepOnce = true; - } - } - - ImGui::Checkbox("Paused", &m_PhysicsPaused); - - ImGui::Separator(); - - ImGui::SliderFloat("Physics Time Scale", &m_PhysicsTimeScale, 0.0f, 2.0f, "%.2fx"); - - if (ImGui::Button("Reset Time Scale")) - { - m_PhysicsTimeScale = 1.0f; - } - - ImGui::End(); -} diff --git a/destrum/src/Assets/AssetDatabase.cpp b/destrum/src/Assets/AssetDatabase.cpp new file mode 100644 index 0000000..bd59c3a --- /dev/null +++ b/destrum/src/Assets/AssetDatabase.cpp @@ -0,0 +1,177 @@ +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace { + [[nodiscard]] bool IsVirtualPath(std::string_view path) + { + return path.find("://") != std::string_view::npos; + } + + [[nodiscard]] std::optional 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()); + } 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::random_device{}()) << 32)}; + AssetGuid guid; + while (!guid.IsValid()) { + guid = {random(), random()}; + } + return guid; +} + +std::optional 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 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 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 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 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(), + .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; +} diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index 124fdd2..5b462e6 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include #include @@ -15,6 +17,58 @@ #include +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(); + } + if (!reference.is_object()) { + return {}; + } + if (const auto runtime = reference.find("runtimeMaterial"); runtime != reference.end() && runtime->is_string()) { + return runtime->get(); + } + 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); diff --git a/destrum/src/Components/MeshRendererInspector.cpp b/destrum/src/Components/MeshRendererInspector.cpp index 0e926d4..45c9875 100644 --- a/destrum/src/Components/MeshRendererInspector.cpp +++ b/destrum/src/Components/MeshRendererInspector.cpp @@ -12,10 +12,11 @@ #include #include +#include #include #include #include -#include +#include #include #include @@ -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 AcceptAssetDrop() { std::optional 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 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(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(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 filteringNames{"Nearest", "Linear", "Anisotropic"}; int filtering = static_cast(material.textureFilteringMode); if (ImGui::Combo("Filtering", &filtering, filteringNames.data(), static_cast(filteringNames.size()))) { material.textureFilteringMode = static_cast(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(); } diff --git a/destrum/src/FS/AssetFS.cpp b/destrum/src/FS/AssetFS.cpp index 7afd552..d1ffe1a 100644 --- a/destrum/src/FS/AssetFS.cpp +++ b/destrum/src/FS/AssetFS.cpp @@ -214,6 +214,53 @@ std::optional 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 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 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(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 AssetFS::ReadFile(const std::filesystem::path& fullPath) { std::ifstream file(fullPath, std::ios::binary); if (!file) { diff --git a/destrum/src/Graphics/Caches/MaterialCache.cpp b/destrum/src/Graphics/Caches/MaterialCache.cpp index 61bd585..ad2b578 100644 --- a/destrum/src/Graphics/Caches/MaterialCache.cpp +++ b/destrum/src/Graphics/Caches/MaterialCache.cpp @@ -46,6 +46,7 @@ void MaterialCache::cleanup(GfxDevice& gfxDevice) materialDataBuffer = {}; materials.clear(); materialKeys.clear(); + freeIds = std::make_shared>(); placeholderMaterialId = NULL_MATERIAL_ID; device = nullptr; } @@ -54,7 +55,7 @@ MaterialID MaterialCache::addMaterial(Material material, std::string key) { MaterialData* data = static_cast(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(id) * sizeof(MaterialData), sizeof(MaterialData)); - const auto materialId = static_cast(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 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 MaterialCache::findMaterialByName( std::optional 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(materials.size()); + return freeIds->empty() ? static_cast(materials.size()) : freeIds->back(); } MaterialID MaterialCache::getPlaceholderMaterialId() const diff --git a/destrum/src/Graphics/MeshCache.cpp b/destrum/src/Graphics/MeshCache.cpp index 6bfa8fb..ef7cd79 100644 --- a/destrum/src/Graphics/MeshCache.cpp +++ b/destrum/src/Graphics/MeshCache.cpp @@ -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(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(gpuMesh.firstIndex) * sizeof(std::uint32_t); @@ -139,8 +188,8 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh memcpy(static_cast(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 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(); } diff --git a/destrum/src/Graphics/Pipelines/PostProcessPipeline.cpp b/destrum/src/Graphics/Pipelines/PostProcessPipeline.cpp new file mode 100644 index 0000000..c1d1e46 --- /dev/null +++ b/destrum/src/Graphics/Pipelines/PostProcessPipeline.cpp @@ -0,0 +1,91 @@ +#include + +#include +#include +#include + +#include + +#include +#include +#include + +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(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(extent.width), + .height = static_cast(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(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); +} diff --git a/destrum/src/Graphics/RenderResources.cpp b/destrum/src/Graphics/RenderResources.cpp index f55b05f..6c85adb 100644 --- a/destrum/src/Graphics/RenderResources.cpp +++ b/destrum/src/Graphics/RenderResources.cpp @@ -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& usedMeshes, + const std::unordered_set& 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 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); } diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 43fb18a..03bdaf7 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -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->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->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); + skyboxPipeline->init(gfxDevice, _resources, sceneColorFormat, depthImageFormat); skinningPipeline = std::make_unique(); skinningPipeline->init(gfxDevice); @@ -61,6 +61,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: renderer2DPass = std::make_unique(); renderer2DPass->init(gfxDevice, _resources, drawImageFormat); + postProcessPipeline = std::make_unique(); + 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(pickBuffers[frameIndex].info.pMappedData); + pickResult = *static_cast(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(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(); + auto skybox = std::make_unique(); + auto skinning = std::make_unique(); + auto lightCulling = std::make_unique(); + auto shadows = std::make_unique(); + auto lines = std::make_unique(); + auto ui = std::make_unique(); + auto post = std::make_unique(); + + 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"); } diff --git a/destrum/src/Scene/ResourceCollector.cpp b/destrum/src/Scene/ResourceCollector.cpp new file mode 100644 index 0000000..787618c --- /dev/null +++ b/destrum/src/Scene/ResourceCollector.cpp @@ -0,0 +1,39 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include + +void CollectUnusedResources(SceneManager& sceneManager, RenderResources& resources, GfxDevice& gfxDevice) +{ + std::unordered_set usedMeshes; + std::unordered_set 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(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); + } +} diff --git a/destrum/src/Scene/Scene.cpp b/destrum/src/Scene/Scene.cpp index e6cd576..d982f3a 100644 --- a/destrum/src/Scene/Scene.cpp +++ b/destrum/src/Scene/Scene.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -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 tonemappers{"None", "ACES", "Khronos PBR Neutral"}; + int tonemapper = static_cast(environment.tonemapper); + if (ImGui::Combo("Tonemapper", &tonemapper, tonemappers.data(), static_cast(tonemappers.size()))) { + environment.tonemapper = static_cast(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) { diff --git a/destrum/src/Scene/SceneManager.cpp b/destrum/src/Scene/SceneManager.cpp index cb81ef2..37f0ae7 100644 --- a/destrum/src/Scene/SceneManager.cpp +++ b/destrum/src/Scene/SceneManager.cpp @@ -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(); diff --git a/destrum/src/Serialization/MaterialSerialization.cpp b/destrum/src/Serialization/MaterialSerialization.cpp index 399e699..c7c2eae 100644 --- a/destrum/src/Serialization/MaterialSerialization.cpp +++ b/destrum/src/Serialization/MaterialSerialization.cpp @@ -1,11 +1,16 @@ #include #include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -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 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(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& 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 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(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 Validate(const json& materials) { + std::optional 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 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(); + 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())) { - continue; - } - - const std::string key = keyJson.get(); - 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(); - 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 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()); + 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()); key != keys.end()) { + *legacy = key->second; + } + } + }); } } diff --git a/destrum/src/Serialization/ObjectSerialization.cpp b/destrum/src/Serialization/ObjectSerialization.cpp index 3e1768a..81227ad 100644 --- a/destrum/src/Serialization/ObjectSerialization.cpp +++ b/destrum/src/Serialization/ObjectSerialization.cpp @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -309,10 +310,9 @@ namespace ObjectSerialization { // Generic: any nested object with a "path" field is an AssetReference. std::function 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(); - 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) { diff --git a/destrum/src/Serialization/PrefabSerializer.cpp b/destrum/src/Serialization/PrefabSerializer.cpp index 930632f..4ef013a 100644 --- a/destrum/src/Serialization/PrefabSerializer.cpp +++ b/destrum/src/Serialization/PrefabSerializer.cpp @@ -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 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) { diff --git a/destrum/src/Serialization/SceneSerializer.cpp b/destrum/src/Serialization/SceneSerializer.cpp index c1cdb10..f0cbc45 100644 --- a/destrum/src/Serialization/SceneSerializer.cpp +++ b/destrum/src/Serialization/SceneSerializer.cpp @@ -93,6 +93,8 @@ std::optional SceneSerializer::Capture(Scene& scene) { {"fogDensity", environment.fogDensity}, {"skyboxRotationSpeed", environment.skyboxRotationSpeed}, {"skyboxRotation", environment.skyboxRotation}, + {"exposure", environment.exposure}, + {"tonemapper", ToString(environment.tonemapper)}, }; std::vector objects; @@ -105,7 +107,7 @@ std::optional 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()); diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt new file mode 100644 index 0000000..dbdda3c --- /dev/null +++ b/editor/CMakeLists.txt @@ -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="$" +) +add_dependencies(destrum_editor TheChef) diff --git a/destrum/include/destrum/Util/AssetBrowser.h b/editor/include/editor/AssetBrowser.h similarity index 54% rename from destrum/include/destrum/Util/AssetBrowser.h rename to editor/include/editor/AssetBrowser.h index e758a17..2bd2e12 100644 --- a/destrum/include/destrum/Util/AssetBrowser.h +++ b/editor/include/editor/AssetBrowser.h @@ -1,16 +1,14 @@ -#ifndef DESTRUM_ASSETBROWSER_H -#define DESTRUM_ASSETBROWSER_H +#ifndef EDITOR_ASSETBROWSER_H +#define EDITOR_ASSETBROWSER_H #include +#include #include -#include #include class AssetBrowser final { public: - void Draw(bool* open = nullptr); - - [[nodiscard]] static bool IsModel(std::string_view path); + std::optional 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& 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 diff --git a/editor/include/editor/Editor.h b/editor/include/editor/Editor.h new file mode 100644 index 0000000..a1a4009 --- /dev/null +++ b/editor/include/editor/Editor.h @@ -0,0 +1,141 @@ +#ifndef EDITOR_EDITOR_H +#define EDITOR_EDITOR_H + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 diff --git a/editor/include/editor/EditorCamera.h b/editor/include/editor/EditorCamera.h new file mode 100644 index 0000000..1171f7e --- /dev/null +++ b/editor/include/editor/EditorCamera.h @@ -0,0 +1,36 @@ +#ifndef EDITOR_EDITORCAMERA_H +#define EDITOR_EDITORCAMERA_H + +#include + +#include + +#include + +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 m_FocusTarget; + bool m_Looking{false}; +}; + +#endif // EDITOR_EDITORCAMERA_H diff --git a/editor/include/editor/PlayMode.h b/editor/include/editor/PlayMode.h new file mode 100644 index 0000000..911193a --- /dev/null +++ b/editor/include/editor/PlayMode.h @@ -0,0 +1,39 @@ +#ifndef EDITOR_PLAYMODE_H +#define EDITOR_PLAYMODE_H + +#include +#include + +#include + +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 m_EditSnapshot; + std::weak_ptr m_EditScene; +}; + +#endif // EDITOR_PLAYMODE_H diff --git a/destrum/include/destrum/Util/RectGizmo.h b/editor/include/editor/RectGizmo.h similarity index 88% rename from destrum/include/destrum/Util/RectGizmo.h rename to editor/include/editor/RectGizmo.h index 916f616..ed0979f 100644 --- a/destrum/include/destrum/Util/RectGizmo.h +++ b/editor/include/editor/RectGizmo.h @@ -1,5 +1,5 @@ -#ifndef DESTRUM_RECTGIZMO_H -#define DESTRUM_RECTGIZMO_H +#ifndef EDITOR_RECTGIZMO_H +#define EDITOR_RECTGIZMO_H #include @@ -25,4 +25,4 @@ private: glm::vec2 m_DragStartSize{0.0f}; }; -#endif // DESTRUM_RECTGIZMO_H +#endif // EDITOR_RECTGIZMO_H diff --git a/destrum/include/destrum/Graphics/ResourcesInspector.h b/editor/include/editor/ResourcesInspector.h similarity index 89% rename from destrum/include/destrum/Graphics/ResourcesInspector.h rename to editor/include/editor/ResourcesInspector.h index 08efad6..c25a085 100644 --- a/destrum/include/destrum/Graphics/ResourcesInspector.h +++ b/editor/include/editor/ResourcesInspector.h @@ -1,5 +1,5 @@ -#ifndef RESOURCESINSPECTOR_H -#define RESOURCESINSPECTOR_H +#ifndef EDITOR_RESOURCESINSPECTOR_H +#define EDITOR_RESOURCESINSPECTOR_H #include @@ -32,4 +32,4 @@ private: ImageID selectedImageId{NULL_IMAGE_ID}; }; -#endif //RESOURCESINSPECTOR_H +#endif //EDITOR_RESOURCESINSPECTOR_H diff --git a/destrum/include/destrum/Util/SceneInspector.h b/editor/include/editor/SceneInspector.h similarity index 94% rename from destrum/include/destrum/Util/SceneInspector.h rename to editor/include/editor/SceneInspector.h index 6072514..a2adff0 100644 --- a/destrum/include/destrum/Util/SceneInspector.h +++ b/editor/include/editor/SceneInspector.h @@ -1,5 +1,5 @@ -#ifndef DESTRUM_SCENEINSPECTOR_H -#define DESTRUM_SCENEINSPECTOR_H +#ifndef EDITOR_SCENEINSPECTOR_H +#define EDITOR_SCENEINSPECTOR_H #include #include @@ -57,4 +57,4 @@ private: nlohmann::json m_Clipboard; }; -#endif // DESTRUM_SCENEINSPECTOR_H +#endif // EDITOR_SCENEINSPECTOR_H diff --git a/editor/include/editor/ShaderHotReload.h b/editor/include/editor/ShaderHotReload.h new file mode 100644 index 0000000..5fbe151 --- /dev/null +++ b/editor/include/editor/ShaderHotReload.h @@ -0,0 +1,33 @@ +#ifndef EDITOR_SHADERHOTRELOAD_H +#define EDITOR_SHADERHOTRELOAD_H + +#include +#include +#include +#include + +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 m_Stamps; + std::future m_Cook; + std::string m_LastResult; + float m_PollTimer{0.0f}; + bool m_CookRequested{false}; +}; + +#endif // EDITOR_SHADERHOTRELOAD_H diff --git a/destrum/include/destrum/Util/TransformGizmo.h b/editor/include/editor/TransformGizmo.h similarity index 87% rename from destrum/include/destrum/Util/TransformGizmo.h rename to editor/include/editor/TransformGizmo.h index 0a79d3d..2994cea 100644 --- a/destrum/include/destrum/Util/TransformGizmo.h +++ b/editor/include/editor/TransformGizmo.h @@ -1,5 +1,5 @@ -#ifndef DESTRUM_TRANSFORMGIZMO_H -#define DESTRUM_TRANSFORMGIZMO_H +#ifndef EDITOR_TRANSFORMGIZMO_H +#define EDITOR_TRANSFORMGIZMO_H #include @@ -34,4 +34,4 @@ public: float scaleSnap{0.1f}; }; -#endif // DESTRUM_TRANSFORMGIZMO_H +#endif // EDITOR_TRANSFORMGIZMO_H diff --git a/editor/include/editor/UndoStack.h b/editor/include/editor/UndoStack.h new file mode 100644 index 0000000..454977a --- /dev/null +++ b/editor/include/editor/UndoStack.h @@ -0,0 +1,42 @@ +#ifndef EDITOR_UNDOSTACK_H +#define EDITOR_UNDOSTACK_H + +#include +#include +#include +#include +#include + +#include + +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 Capture(Scene& scene); + bool Restore(Scene& scene, const Snapshot& snapshot); + + std::deque m_Undo; + std::vector m_Redo; + std::optional m_Current; +}; + +#endif // EDITOR_UNDOSTACK_H diff --git a/destrum/src/Util/AssetBrowser.cpp b/editor/src/AssetBrowser.cpp similarity index 51% rename from destrum/src/Util/AssetBrowser.cpp rename to editor/src/AssetBrowser.cpp index f1d53ac..714b6a3 100644 --- a/destrum/src/Util/AssetBrowser.cpp +++ b/editor/src/AssetBrowser.cpp @@ -1,56 +1,64 @@ -#include +#include #include -#include #include #include +#include #include +#include +#include #include 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(std::tolower(c)); }); return result; } - [[nodiscard]] bool HasExtension(std::string_view path, std::initializer_list 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 AssetBrowser::Draw(bool* open) +{ + std::optional 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& 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) { diff --git a/editor/src/Editor.cpp b/editor/src/Editor.cpp new file mode 100644 index 0000000..5ac1a98 --- /dev/null +++ b/editor/src/Editor.cpp @@ -0,0 +1,387 @@ +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + [[nodiscard]] bool AnyKeyReleased() + { + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) { + if (ImGui::IsKeyReleased(static_cast(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 highlighted; + Scene* scene = CurrentScene(); + if (scene == nullptr) { + renderer.setHighlightedObjects(highlighted); + return; + } + + if (GameObject* selected = scene->GetSelectedObject(); selected != nullptr && picking) { + std::vector pending{selected}; + while (!pending.empty()) { + GameObject* object = pending.back(); + pending.pop_back(); + highlighted.push_back(static_cast(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); + } +} diff --git a/editor/src/EditorCamera.cpp b/editor/src/EditorCamera.cpp new file mode 100644 index 0000000..d573eec --- /dev/null +++ b/editor/src/EditorCamera.cpp @@ -0,0 +1,99 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +WorldBounds ComputeWorldBounds(GameObject& object, const RenderResources& resources) +{ + glm::vec3 boundsMin{std::numeric_limits::max()}; + glm::vec3 boundsMax{std::numeric_limits::lowest()}; + + std::vector 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(); + 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; +} diff --git a/editor/src/EditorMenus.cpp b/editor/src/EditorMenus.cpp new file mode 100644 index 0000000..28bb8f8 --- /dev/null +++ b/editor/src/EditorMenus.cpp @@ -0,0 +1,374 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + [[nodiscard]] std::vector FindScenes() + { + std::vector 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, ¢er); + const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, ¢er); + const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, ¢er); + + 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 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(); +} diff --git a/editor/src/EditorViewport.cpp b/editor/src/EditorViewport.cpp new file mode 100644 index 0000000..7c2cbee --- /dev/null +++ b/editor/src/EditorViewport.cpp @@ -0,0 +1,239 @@ +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(current.width) == desired.x && static_cast(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(available.x * framebufferScale.x), 1), + std::max(static_cast(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(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(extent.width), static_cast(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(extent.width), static_cast(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 pending{selected}; + while (!pending.empty()) { + GameObject* object = pending.back(); + pending.pop_back(); + if (auto* meshRenderer = object->GetComponent()) { + 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(); + const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static; + for (const auto& component : object->GetComponents()) { + const auto* collider = dynamic_cast(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); + } + } +} diff --git a/editor/src/PlayMode.cpp b/editor/src/PlayMode.cpp new file mode 100644 index 0000000..a01fc0b --- /dev/null +++ b/editor/src/PlayMode.cpp @@ -0,0 +1,118 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +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 = 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 = 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(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"); +} diff --git a/destrum/src/Util/RectGizmo.cpp b/editor/src/RectGizmo.cpp similarity index 99% rename from destrum/src/Util/RectGizmo.cpp rename to editor/src/RectGizmo.cpp index 4df9654..278f0f7 100644 --- a/destrum/src/Util/RectGizmo.cpp +++ b/editor/src/RectGizmo.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/destrum/src/Graphics/ResourcesInspector.cpp b/editor/src/ResourcesInspector.cpp similarity index 99% rename from destrum/src/Graphics/ResourcesInspector.cpp rename to editor/src/ResourcesInspector.cpp index a50f14c..ecb60c5 100644 --- a/destrum/src/Graphics/ResourcesInspector.cpp +++ b/editor/src/ResourcesInspector.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/destrum/src/Util/SceneInspector.cpp b/editor/src/SceneInspector.cpp similarity index 99% rename from destrum/src/Util/SceneInspector.cpp rename to editor/src/SceneInspector.cpp index 84df153..13de46e 100644 --- a/destrum/src/Util/SceneInspector.cpp +++ b/editor/src/SceneInspector.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/editor/src/ShaderHotReload.cpp b/editor/src/ShaderHotReload.cpp new file mode 100644 index 0000000..82988e8 --- /dev/null +++ b/editor/src/ShaderHotReload.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include +#include + +#include + +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; +} diff --git a/destrum/src/Util/TransformGizmo.cpp b/editor/src/TransformGizmo.cpp similarity index 99% rename from destrum/src/Util/TransformGizmo.cpp rename to editor/src/TransformGizmo.cpp index 4e93f9f..97ed49e 100644 --- a/destrum/src/Util/TransformGizmo.cpp +++ b/editor/src/TransformGizmo.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/editor/src/UndoStack.cpp b/editor/src/UndoStack.cpp new file mode 100644 index 0000000..d136fa1 --- /dev/null +++ b/editor/src/UndoStack.cpp @@ -0,0 +1,95 @@ +#include + +#include + +#include +#include + +#include +#include + +std::optional 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; +} diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 0558c85..973f47d 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -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") diff --git a/lightkeeper/assets_src/materials/prototype_dark.dmat b/lightkeeper/assets_src/materials/prototype_dark.dmat new file mode 100644 index 0000000..552d9f7 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_dark.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_dark.dmat.meta b/lightkeeper/assets_src/materials/prototype_dark.dmat.meta new file mode 100644 index 0000000..57af7fb --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_dark.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "7d9c3cd4034fc8c13448d08ced3e4432" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_green.dmat b/lightkeeper/assets_src/materials/prototype_green.dmat new file mode 100644 index 0000000..abb4049 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_green.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_green.dmat.meta b/lightkeeper/assets_src/materials/prototype_green.dmat.meta new file mode 100644 index 0000000..c50faf2 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_green.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "af12f3099fa89356d6a2db0f7b064312" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_light.dmat b/lightkeeper/assets_src/materials/prototype_light.dmat new file mode 100644 index 0000000..0453019 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_light.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_light.dmat.meta b/lightkeeper/assets_src/materials/prototype_light.dmat.meta new file mode 100644 index 0000000..a9dc6db --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_light.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "6cf69d2191896c70166cc3694a778f0c" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_light_matte.dmat b/lightkeeper/assets_src/materials/prototype_light_matte.dmat new file mode 100644 index 0000000..b6bcfc0 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_light_matte.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_light_matte.dmat.meta b/lightkeeper/assets_src/materials/prototype_light_matte.dmat.meta new file mode 100644 index 0000000..2effbb1 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_light_matte.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "5baf128c1cac1369814fe1170257272d" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_orange.dmat b/lightkeeper/assets_src/materials/prototype_orange.dmat new file mode 100644 index 0000000..9e99ce7 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_orange.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_orange.dmat.meta b/lightkeeper/assets_src/materials/prototype_orange.dmat.meta new file mode 100644 index 0000000..d887a03 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_orange.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "889deecef8a1972835f601442a7d9c7b" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_orange_matte.dmat b/lightkeeper/assets_src/materials/prototype_orange_matte.dmat new file mode 100644 index 0000000..4f67b58 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_orange_matte.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_orange_matte.dmat.meta b/lightkeeper/assets_src/materials/prototype_orange_matte.dmat.meta new file mode 100644 index 0000000..83dbe37 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_orange_matte.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "e9a0ccff690986f920184af94ef4825f" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_purple.dmat b/lightkeeper/assets_src/materials/prototype_purple.dmat new file mode 100644 index 0000000..b453a82 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_purple.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_purple.dmat.meta b/lightkeeper/assets_src/materials/prototype_purple.dmat.meta new file mode 100644 index 0000000..29895f3 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_purple.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "cbc24f8efba0e64df911802de71d03af" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat b/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat new file mode 100644 index 0000000..6aa505b --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat.meta b/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat.meta new file mode 100644 index 0000000..0f59192 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_purple_glossy.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "73a209afd28c56cb4c97b0ca5b99b45d" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_red.dmat b/lightkeeper/assets_src/materials/prototype_red.dmat new file mode 100644 index 0000000..106f669 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_red.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_red.dmat.meta b/lightkeeper/assets_src/materials/prototype_red.dmat.meta new file mode 100644 index 0000000..a81ee80 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_red.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "f8c2d0e8b72b4fa561b078cf9195116d" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/materials/prototype_red_matte.dmat b/lightkeeper/assets_src/materials/prototype_red_matte.dmat new file mode 100644 index 0000000..4cdf9b6 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_red_matte.dmat @@ -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 + } + } +} diff --git a/lightkeeper/assets_src/materials/prototype_red_matte.dmat.meta b/lightkeeper/assets_src/materials/prototype_red_matte.dmat.meta new file mode 100644 index 0000000..90bdbe3 --- /dev/null +++ b/lightkeeper/assets_src/materials/prototype_red_matte.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "57a4d970d7d6af8c484d357a18e358ac" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/new_material.dmat b/lightkeeper/assets_src/new_material.dmat new file mode 100644 index 0000000..83593a8 --- /dev/null +++ b/lightkeeper/assets_src/new_material.dmat @@ -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 +} \ No newline at end of file diff --git a/lightkeeper/assets_src/new_material.dmat.meta b/lightkeeper/assets_src/new_material.dmat.meta new file mode 100644 index 0000000..cf52e42 --- /dev/null +++ b/lightkeeper/assets_src/new_material.dmat.meta @@ -0,0 +1,3 @@ +{ + "guid": "1316639e4dcaa6ab872ae26cdccee84c" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/plane.glb.meta b/lightkeeper/assets_src/plane.glb.meta new file mode 100644 index 0000000..9f622ff --- /dev/null +++ b/lightkeeper/assets_src/plane.glb.meta @@ -0,0 +1,3 @@ +{ + "guid": "74483472d496fc022f2c755c96bdbd8b" +} \ No newline at end of file diff --git a/lightkeeper/assets_src/scenes/main.scene b/lightkeeper/assets_src/scenes/main.scene new file mode 100644 index 0000000..7b685d1 --- /dev/null +++ b/lightkeeper/assets_src/scenes/main.scene @@ -0,0 +1,7570 @@ +{ + "environment": { + "ambientColor": [ + 0.10000000149011612, + 0.10000000149011612, + 0.10000000149011612 + ], + "ambientIntensity": 0.5, + "exposure": 0.0, + "fogColor": [ + 0.5, + 0.5, + 0.5 + ], + "fogDensity": 0.009999999776482582, + "skyboxRotation": 0.0, + "skyboxRotationSpeed": 0.009999999776482582, + "tonemapper": "pbrNeutral" + }, + "materials": { + "1": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "nearest", + "metallic": 0.0, + "name": "material_0", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "roughness": 0.699999988079071, + "textures": { + "diffuse": { + "guid": "78d91a725664c8b815531d55bc62683f", + "path": "engine://grass.jpg", + "srgb": true + } + } + }, + "2": { + "baseColor": [ + 0.8999999761581421, + 0.10000000149011612, + 0.10000000149011612 + ], + "emissiveColor": [ + 0.35999998450279236, + 0.04000000283122063, + 0.04000000283122063 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "ButtonMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "roughness": 0.4000000059604645 + }, + "3": { + "baseColor": [ + 1.0, + 0.949999988079071, + 0.8500000238418579 + ], + "emissiveColor": [ + 1.0, + 0.8199999928474426, + 0.550000011920929 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "BulbMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "roughness": 1.0 + }, + "4": { + "baseColor": [ + 0.8999999761581421, + 0.8999999761581421, + 0.8999999761581421 + ], + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "LightSwitchMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "roughness": 0.5 + }, + "5": { + "baseColor": [ + 0.8999999761581421, + 0.10000000149011612, + 0.10000000149011612 + ], + "emissiveColor": [ + 0.35999998450279236, + 0.04000000283122063, + 0.04000000283122063 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "WinButtonMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "roughness": 0.4000000059604645 + } + }, + "name": "Main", + "objects": [ + { + "active": true, + "components": [ + { + "data": { + "castShadows": true, + "color": [ + 1.0, + 0.949999988079071, + 0.8500000238418579 + ], + "innerConeAngle": 0.3499999940395355, + "intensity": 3.0, + "outerConeAngle": 0.5, + "range": 10.0, + "type": 0 + }, + "enabled": true, + "type": "LightComponent" + } + ], + "id": 1154, + "name": "Sun", + "parent": 0, + "transform": { + "position": [ + 0.0, + 10.0, + 0.0 + ], + "rotation": [ + -0.4030582010746002, + 0.27253201603889465, + 0.12708376348018646, + 0.8643611073493958 + ], + "scale": [ + 0.9999999403953552, + 0.9999999403953552, + 0.9999999403953552 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "1" + }, + "mesh": { + "guid": "74483472d496fc022f2c755c96bdbd8b", + "path": "game://plane.glb", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1155, + "name": "GroundPlane", + "parent": 0, + "transform": { + "position": [ + 0.0, + -1.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 25.0, + 1.0, + 25.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 25.0, + 0.5, + 25.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1156, + "name": "GroundCollider", + "parent": 0, + "transform": { + "position": [ + 0.0, + -1.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=2,0.5,2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.25, + 1.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1157, + "name": "Parkour_Step", + "parent": 0, + "transform": { + "position": [ + 0.0, + -0.75, + 4.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=2,1.4,2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.699999988079071, + 1.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1158, + "name": "Parkour_Block", + "parent": 0, + "transform": { + "position": [ + 0.0, + -0.29999998211860657, + 7.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,2.2,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 1.100000023841858, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1159, + "name": "Parkour_Pillar1", + "parent": 0, + "transform": { + "position": [ + 3.5, + 0.10000002384185791, + 8.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.2,3,1.2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.6000000238418579, + 1.5, + 0.6000000238418579 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1160, + "name": "Parkour_Pillar2", + "parent": 0, + "transform": { + "position": [ + 6.5, + 0.5, + 7.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=0.6,0.3,5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.30000001192092896, + 0.15000000596046448, + 2.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1161, + "name": "Parkour_Beam", + "parent": 0, + "transform": { + "position": [ + 6.5, + 1.850000023841858, + 4.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=2.5,3.8,2.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.25, + 1.899999976158142, + 1.25 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1162, + "name": "Parkour_Goal", + "parent": 0, + "transform": { + "position": [ + 6.5, + 0.8999999761581421, + -0.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.5,3,8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.25, + 1.5, + 4.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1163, + "name": "Wall", + "parent": 0, + "transform": { + "position": [ + -5.0, + 0.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=4,0.8,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.0, + 0.4000000059604645, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1164, + "name": "LowWall", + "parent": 0, + "transform": { + "position": [ + -2.5, + -0.6000000238418579, + -4.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "73a209afd28c56cb4c97b0ca5b99b45d", + "path": "game://materials/prototype_purple_glossy.dmat" + }, + "mesh": "prim://box?size=0.8,0.8,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.4000000059604645, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Dynamic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1165, + "name": "Crate0", + "parent": 0, + "transform": { + "position": [ + -2.5, + -0.6000000238418579, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "73a209afd28c56cb4c97b0ca5b99b45d", + "path": "game://materials/prototype_purple_glossy.dmat" + }, + "mesh": "prim://box?size=0.8,0.8,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.4000000059604645, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Dynamic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1166, + "name": "Crate1", + "parent": 0, + "transform": { + "position": [ + -2.5, + 0.20999997854232788, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "73a209afd28c56cb4c97b0ca5b99b45d", + "path": "game://materials/prototype_purple_glossy.dmat" + }, + "mesh": "prim://box?size=0.8,0.8,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.4000000059604645, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Dynamic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1167, + "name": "Crate2", + "parent": 0, + "transform": { + "position": [ + -2.5, + 1.0199999809265137, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "73a209afd28c56cb4c97b0ca5b99b45d", + "path": "game://materials/prototype_purple_glossy.dmat" + }, + "mesh": "prim://box?size=0.8,0.8,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.4000000059604645, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Dynamic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1168, + "name": "Crate3", + "parent": 0, + "transform": { + "position": [ + -2.5, + 1.8300000429153442, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [], + "id": 1169, + "name": "FlightHelmet", + "parent": 0, + "transform": { + "position": [ + 2.0, + -1.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 2.0, + 2.0, + 2.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:5" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:5" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1170, + "name": "Lenses_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:4" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:4" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1171, + "name": "LeatherParts_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:3" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:3" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1172, + "name": "MetalParts_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:2" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:2" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1173, + "name": "GlassPlastic_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:1" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:1" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1174, + "name": "RubberWood_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "material:0" + }, + "mesh": { + "guid": "0407d177c8487bca092ffb7fd4d47aa2", + "path": "engine://flight-helmet/FlightHelmet.gltf", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1175, + "name": "Hose_low", + "parent": 1169, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "castShadows": true, + "color": [ + 1.0, + 0.8500000238418579, + 0.699999988079071 + ], + "innerConeAngle": 0.3490658402442932, + "intensity": 25.0, + "outerConeAngle": 0.5235987901687622, + "range": 12.0, + "type": 2 + }, + "enabled": true, + "type": "LightComponent" + } + ], + "id": 1176, + "name": "HelmetSpotlight", + "parent": 0, + "transform": { + "position": [ + 2.0, + 3.5, + -2.5 + ], + "rotation": [ + 0.0, + 0.8618708848953247, + 0.5071278214454651, + 0.0 + ], + "scale": [ + 1.0, + 0.9999999403953552, + 0.9999999403953552 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "height": 6.0, + "isTrigger": false, + "radius": 0.45000001788139343 + }, + "enabled": true, + "type": "CapsuleCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1177, + "name": "Tree", + "parent": 0, + "transform": { + "position": [ + -7.0, + 2.0, + 7.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 3.0, + 3.0, + 3.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "c2f1f9dcb185460c6d685c17d7ec4f82", + "path": "engine://tree/base.obj", + "sub": "material:1" + }, + "mesh": { + "guid": "c2f1f9dcb185460c6d685c17d7ec4f82", + "path": "engine://tree/base.obj", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1178, + "name": "model", + "parent": 1177, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 3.0, + 3.0, + 3.0 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "triggerEnterCount": 0, + "triggerExitCount": 0 + }, + "enabled": true, + "type": "TriggerLoggerComponent" + } + ], + "id": 1179, + "name": "TriggerBox", + "parent": 0, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.4,2.8,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.20000000298023224, + 1.399999976158142, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1180, + "name": "DoorFrame_Left", + "parent": 0, + "transform": { + "position": [ + 4.0, + 0.3999999761581421, + -5.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.4,2.8,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.20000000298023224, + 1.399999976158142, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1181, + "name": "DoorFrame_Right", + "parent": 0, + "transform": { + "position": [ + 4.0, + 0.3999999761581421, + -3.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.4,0.4,2.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.20000000298023224, + 0.20000000298023224, + 1.2000000476837158 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1182, + "name": "DoorFrame_Top", + "parent": 0, + "transform": { + "position": [ + 4.0, + 2.0, + -4.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.3,2.4,1.6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 1.2000000476837158, + 0.800000011920929 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "open": false, + "openAmount": 0.0, + "openDuration": 1.0, + "openOffset": [ + 0.0, + 0.0, + -1.7000000476837158 + ], + "toggleable": true + }, + "enabled": true, + "type": "Door" + } + ], + "id": 1183, + "name": "Door", + "parent": 0, + "transform": { + "position": [ + 4.0, + 0.20000004768371582, + -4.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.4,1.1,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.20000000298023224, + 0.550000011920929, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1184, + "name": "ButtonPedestal", + "parent": 0, + "transform": { + "position": [ + 1.5, + -0.44999998807907104, + -4.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "2" + }, + "mesh": "prim://box?size=0.25,0.1,0.25" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.125, + 0.05000000074505806, + 0.125 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "colorIndex": 0, + "cycleColors": true, + "pressCount": 0, + "targets": [ + 1183 + ] + }, + "enabled": true, + "type": "InteractableButton" + } + ], + "id": 1185, + "name": "Button", + "parent": 0, + "transform": { + "position": [ + 1.5, + 0.15000000596046448, + -4.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [], + "id": 1186, + "name": "House", + "parent": 0, + "transform": { + "position": [ + 0.0, + -1.0, + -16.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "e9a0ccff690986f920184af94ef4825f", + "path": "game://materials/prototype_orange_matte.dmat" + }, + "mesh": "prim://box?size=8,0.06,6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 4.0, + 0.029999999329447746, + 3.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1187, + "name": "House_Floor", + "parent": 1186, + "transform": { + "position": [ + 0.0, + 0.029999971389770508, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=2.0500002,3,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0250000953674316, + 1.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1188, + "name": "House_FrontWall_Left", + "parent": 1186, + "transform": { + "position": [ + -3.125, + 1.5, + 3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=5.05,3,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.5250000953674316, + 1.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1189, + "name": "House_FrontWall_Right", + "parent": 1186, + "transform": { + "position": [ + 1.625, + 1.5, + 3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=1.1999999,0.79999995,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.5999999642372131, + 0.3999999761581421, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1190, + "name": "House_FrontWall_Above", + "parent": 1186, + "transform": { + "position": [ + -1.5, + 2.5999999046325684, + 3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=4.9500003,3,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.4750001430511475, + 1.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1191, + "name": "House_BackWall_Left", + "parent": 1186, + "transform": { + "position": [ + -1.6750000715255737, + 1.5, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=1.95,3,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.9750000238418579, + 1.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1192, + "name": "House_BackWall_Right", + "parent": 1186, + "transform": { + "position": [ + 3.1750001907348633, + 1.5, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=1.4000001,1,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.7000000476837158, + 0.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1193, + "name": "House_BackWall_Below", + "parent": 1186, + "transform": { + "position": [ + 1.5, + 0.5, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=1.4000001,1,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.7000000476837158, + 0.5, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1194, + "name": "House_BackWall_Above", + "parent": 1186, + "transform": { + "position": [ + 1.5, + 2.5, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=0.3,3,5.7" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 1.5, + 2.8499999046325684 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1195, + "name": "House_LeftWall", + "parent": 1186, + "transform": { + "position": [ + -4.0, + 1.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=0.3,3,2.05" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 1.5, + 1.024999976158142 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1196, + "name": "House_RightWall_Left", + "parent": 1186, + "transform": { + "position": [ + 4.0, + 1.5, + -1.8250007629394531 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=0.3,3,2.05" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 1.5, + 1.024999976158142 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1197, + "name": "House_RightWall_Right", + "parent": 1186, + "transform": { + "position": [ + 4.0, + 1.5, + 1.8249998092651367 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=0.3,1,1.6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 0.5, + 0.800000011920929 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1198, + "name": "House_RightWall_Below", + "parent": 1186, + "transform": { + "position": [ + 4.0, + 0.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "5baf128c1cac1369814fe1170257272d", + "path": "game://materials/prototype_light_matte.dmat" + }, + "mesh": "prim://box?size=0.3,1,1.6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.15000000596046448, + 0.5, + 0.800000011920929 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1199, + "name": "House_RightWall_Above", + "parent": 1186, + "transform": { + "position": [ + 4.0, + 2.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "57a4d970d7d6af8c484d357a18e358ac", + "path": "game://materials/prototype_red_matte.dmat" + }, + "mesh": "prim://box?size=8.8,0.3,6.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 4.400000095367432, + 0.15000000596046448, + 3.4000000953674316 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1200, + "name": "House_Roof", + "parent": 1186, + "transform": { + "position": [ + 0.0, + 3.1500000953674316, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "3" + }, + "mesh": "prim://box?size=0.35,0.12,0.35" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "castShadows": false, + "color": [ + 1.0, + 0.8199999928474426, + 0.550000011920929 + ], + "innerConeAngle": 0.3499999940395355, + "intensity": 4.0, + "outerConeAngle": 0.5, + "range": 7.0, + "type": 1 + }, + "enabled": true, + "type": "LightComponent" + }, + { + "data": { + "on": true + }, + "enabled": true, + "type": "LightToggle" + } + ], + "id": 1201, + "name": "CeilingLight", + "parent": 1186, + "transform": { + "position": [ + 0.0, + 2.940000057220459, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "4" + }, + "mesh": "prim://box?size=0.12,0.2,0.06" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.05999999865889549, + 0.10000000149011612, + 0.029999999329447746 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "colorIndex": 0, + "cycleColors": false, + "pressCount": 0, + "targets": [ + 1201 + ] + }, + "enabled": true, + "type": "InteractableButton" + } + ], + "id": 1202, + "name": "LightSwitch", + "parent": 1186, + "transform": { + "position": [ + -0.4999999701976776, + 1.2999999523162842, + 2.8199996948242188 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [], + "id": 1203, + "name": "Sofa", + "parent": 1186, + "transform": { + "position": [ + -3.700000047683716, + 0.0, + -1.5 + ], + "rotation": [ + -4.371138473402425e-08, + 1.0, + -4.371138473402425e-08, + 1.910685253406468e-15 + ], + "scale": [ + 3.0, + 3.0, + 3.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "material:1" + }, + "mesh": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "mesh:2" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1204, + "name": "loungeSofa_1", + "parent": 1203, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "material:0" + }, + "mesh": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "mesh:3" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1205, + "name": "loungeSofa_1", + "parent": 1203, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "material:0" + }, + "mesh": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1206, + "name": "loungeSofa", + "parent": 1203, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "material:1" + }, + "mesh": { + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4", + "path": "game://sofa.glb", + "sub": "mesh:1" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1207, + "name": "loungeSofa", + "parent": 1203, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3.1622777,0.3,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5811388492584229, + 0.15000000596046448, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1208, + "name": "PlatformRamp", + "parent": 0, + "transform": { + "position": [ + 10.547433853149414, + -0.6423025131225586, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.16018223762512207, + 0.9870875477790833 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=3,0.3,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.15000000596046448, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 1.0, + "travelDuration": 4.0, + "travelOffset": [ + 8.0, + 0.0, + 0.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1209, + "name": "MovingPlatform", + "parent": 0, + "transform": { + "position": [ + 13.5, + -0.15000000596046448, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=2,1,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.5, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1210, + "name": "PlatformLanding", + "parent": 0, + "transform": { + "position": [ + 24.0, + -0.5, + 2.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [], + "id": 1211, + "name": "WinCourse", + "parent": 0, + "transform": { + "position": [ + -25.0, + -1.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=5,0.5,5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.5, + 0.25, + 2.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1212, + "name": "WinCourse_Start", + "parent": 1211, + "transform": { + "position": [ + -2.5, + -0.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.5, + 1.0, + 2.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1213, + "name": "WinCourse_Checkpoint1", + "parent": 1211, + "transform": { + "position": [ + -2.5, + 1.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=2,0.5,2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.25, + 1.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1214, + "name": "WinCourse_Platform1", + "parent": 1211, + "transform": { + "position": [ + -8.0, + -0.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1215, + "name": "WinCourse_Platform2", + "parent": 1211, + "transform": { + "position": [ + -12.150001525878906, + 0.25, + 1.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1216, + "name": "WinCourse_Platform3", + "parent": 1211, + "transform": { + "position": [ + -16.25, + 0.75, + -1.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.2,0.5,1.2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.6000000238418579, + 0.25, + 0.6000000238418579 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1217, + "name": "WinCourse_Platform4", + "parent": 1211, + "transform": { + "position": [ + -20.39999771118164, + 0.75, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3,0.5,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.25, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1218, + "name": "WinCourse_Platform5", + "parent": 1211, + "transform": { + "position": [ + -24.699996948242188, + 0.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 1.0, + 1.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1219, + "name": "WinCourse_Checkpoint2", + "parent": 1211, + "transform": { + "position": [ + -24.699996948242188, + 1.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=1,0.5,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.5, + 0.25, + 0.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1220, + "name": "WinCourse_Platform6", + "parent": 1211, + "transform": { + "position": [ + -29.29999542236328, + 0.25, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.8,0.5,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.25, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1221, + "name": "WinCourse_Platform7", + "parent": 1211, + "transform": { + "position": [ + -32.999996185302734, + 0.6499999761581421, + -1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.7,0.5,0.7" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.3499999940395355, + 0.25, + 0.3499999940395355 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1222, + "name": "WinCourse_Platform8", + "parent": 1211, + "transform": { + "position": [ + -36.54999542236328, + 1.0499999523162842, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.7,0.5,0.7" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.3499999940395355, + 0.25, + 0.3499999940395355 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1223, + "name": "WinCourse_Platform9", + "parent": 1211, + "transform": { + "position": [ + -40.24999237060547, + 1.4500000476837158, + -1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.7,0.5,0.7" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.3499999940395355, + 0.25, + 0.3499999940395355 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1224, + "name": "WinCourse_Platform10", + "parent": 1211, + "transform": { + "position": [ + -44.149986267089844, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3,0.5,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.25, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1225, + "name": "WinCourse_Platform11", + "parent": 1211, + "transform": { + "position": [ + -48.49998474121094, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 1.0, + 1.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1226, + "name": "WinCourse_Checkpoint3", + "parent": 1211, + "transform": { + "position": [ + -48.49998474121094, + 1.9000000953674316, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=6,0.5,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 3.0, + 0.25, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1227, + "name": "WinCourse_Platform12", + "parent": 1211, + "transform": { + "position": [ + -54.199981689453125, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=5,0.5,0.35" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.5, + 0.25, + 0.17499999701976776 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1228, + "name": "WinCourse_Platform13", + "parent": 1211, + "transform": { + "position": [ + -61.09998321533203, + 0.9500000476837158, + 0.8999999761581421 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=4,0.5,0.3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.0, + 0.25, + 0.15000000596046448 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1229, + "name": "WinCourse_Platform14", + "parent": 1211, + "transform": { + "position": [ + -66.99998474121094, + 1.25, + -0.8999999761581421 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1230, + "name": "WinCourse_Platform15", + "parent": 1211, + "transform": { + "position": [ + -73.34998321533203, + 1.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1231, + "name": "WinCourse_Platform16", + "parent": 1211, + "transform": { + "position": [ + -78.84998321533203, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.2,0.5,1.2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.6000000238418579, + 0.25, + 0.6000000238418579 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1232, + "name": "WinCourse_Platform17", + "parent": 1211, + "transform": { + "position": [ + -83.99998474121094, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3,0.5,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.25, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1233, + "name": "WinCourse_Platform18", + "parent": 1211, + "transform": { + "position": [ + -89.09998321533203, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 1.0, + 1.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1234, + "name": "WinCourse_Checkpoint4", + "parent": 1211, + "transform": { + "position": [ + -89.09998321533203, + 1.9000000953674316, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=2.5,0.5,2.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.25, + 0.25, + 1.25 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 1.0, + "travelDuration": 3.5, + "travelOffset": [ + -9.0, + 0.0, + 0.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1235, + "name": "WinCourse_Platform19", + "parent": 1211, + "transform": { + "position": [ + -92.44998168945313, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=2,0.5,2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.25, + 1.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1236, + "name": "WinCourse_Platform20", + "parent": 1211, + "transform": { + "position": [ + -104.29998779296875, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 0.30000001192092896, + "travelDuration": 2.0, + "travelOffset": [ + 0.0, + 0.0, + 6.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1237, + "name": "WinCourse_Platform21", + "parent": 1211, + "transform": { + "position": [ + -108.24998474121094, + 0.6500000357627869, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 0.30000001192092896, + "travelDuration": 2.5999999046325684, + "travelOffset": [ + 0.0, + 0.0, + -6.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1238, + "name": "WinCourse_Platform22", + "parent": 1211, + "transform": { + "position": [ + -111.94998168945313, + 0.6500000357627869, + 3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 0.30000001192092896, + "travelDuration": 1.7000000476837158, + "travelOffset": [ + 0.0, + 0.0, + 6.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1239, + "name": "WinCourse_Platform23", + "parent": 1211, + "transform": { + "position": [ + -115.64997863769531, + 0.6500000357627869, + -3.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3,0.5,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.25, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1240, + "name": "WinCourse_Platform24", + "parent": 1211, + "transform": { + "position": [ + -120.0999755859375, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 1.0, + 1.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1241, + "name": "WinCourse_Checkpoint5", + "parent": 1211, + "transform": { + "position": [ + -120.0999755859375, + 1.9000000953674316, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=2,0.5,2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.0, + 0.25, + 1.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 1.5, + "travelDuration": 3.0, + "travelOffset": [ + 0.0, + 5.0, + 0.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1242, + "name": "WinCourse_Platform25", + "parent": 1211, + "transform": { + "position": [ + -123.19998168945313, + 0.6500000357627869, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "889deecef8a1972835f601442a7d9c7b", + "path": "game://materials/prototype_orange.dmat" + }, + "mesh": "prim://box?size=1.5,0.5,1.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.75, + 0.25, + 0.75 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1243, + "name": "WinCourse_Platform26", + "parent": 1211, + "transform": { + "position": [ + -126.14997863769531, + 5.650000095367432, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.8,0.5,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.25, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1244, + "name": "WinCourse_Platform27", + "parent": 1211, + "transform": { + "position": [ + -130.0999755859375, + 6.150000095367432, + 1.2000000476837158 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.7,0.5,0.7" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.3499999940395355, + 0.25, + 0.3499999940395355 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1245, + "name": "WinCourse_Platform28", + "parent": 1211, + "transform": { + "position": [ + -133.8499755859375, + 6.650000095367432, + -1.2000000476837158 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.6,0.5,0.6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.30000001192092896, + 0.25, + 0.30000001192092896 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1246, + "name": "WinCourse_Platform29", + "parent": 1211, + "transform": { + "position": [ + -137.69998168945313, + 6.650000095367432, + 1.2000000476837158 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.6,0.5,0.6" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.30000001192092896, + 0.25, + 0.30000001192092896 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1247, + "name": "WinCourse_Platform30", + "parent": 1211, + "transform": { + "position": [ + -141.69998168945313, + 6.25, + -1.2000000476837158 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=3,0.5,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 0.25, + 1.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1248, + "name": "WinCourse_Platform31", + "parent": 1211, + "transform": { + "position": [ + -145.99998474121094, + 6.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.5, + 1.0, + 1.5 + ], + "isTrigger": true + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 1.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "spawnOffset": [ + 0.0, + 0.20000000298023224, + 0.0 + ] + }, + "enabled": true, + "type": "Checkpoint" + } + ], + "id": 1249, + "name": "WinCourse_Checkpoint6", + "parent": 1211, + "transform": { + "position": [ + -145.99998474121094, + 7.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "6cf69d2191896c70166cc3694a778f0c", + "path": "game://materials/prototype_light.dmat" + }, + "mesh": "prim://box?size=1.2,0.5,1.2" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.6000000238418579, + 0.25, + 0.6000000238418579 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Kinematic", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "elapsedTime": 0.0, + "pauseDuration": 0.20000000298023224, + "travelDuration": 1.600000023841858, + "travelOffset": [ + 0.0, + 0.0, + 5.0 + ] + }, + "enabled": true, + "type": "MovingPlatform" + } + ], + "id": 1250, + "name": "WinCourse_Platform32", + "parent": 1211, + "transform": { + "position": [ + -150.49998474121094, + 6.25, + -2.5 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=1,0.5,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.5, + 0.25, + 0.5 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1251, + "name": "WinCourse_Platform33", + "parent": 1211, + "transform": { + "position": [ + -154.1999969482422, + 7.050000190734863, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.8,0.5,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.25, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1252, + "name": "WinCourse_Platform34", + "parent": 1211, + "transform": { + "position": [ + -158.49998474121094, + 7.050000190734863, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "cbc24f8efba0e64df911802de71d03af", + "path": "game://materials/prototype_purple.dmat" + }, + "mesh": "prim://box?size=0.8,0.5,0.8" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.4000000059604645, + 0.25, + 0.4000000059604645 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1253, + "name": "WinCourse_Platform35", + "parent": 1211, + "transform": { + "position": [ + -162.8999786376953, + 6.75, + -1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "f8c2d0e8b72b4fa561b078cf9195116d", + "path": "game://materials/prototype_red.dmat" + }, + "mesh": "prim://box?size=4,0.5,4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 2.0, + 0.25, + 2.0 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "reached": false + }, + "enabled": true, + "type": "WinGoal" + } + ], + "id": 1254, + "name": "WinCourse_Goal", + "parent": 1211, + "transform": { + "position": [ + -167.7999725341797, + 7.25, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "7d9c3cd4034fc8c13448d08ced3e4432", + "path": "game://materials/prototype_dark.dmat" + }, + "mesh": "prim://box?size=0.4,1.1,0.4" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.20000000298023224, + 0.550000011920929, + 0.20000000298023224 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1255, + "name": "WinCourse_ButtonPedestal", + "parent": 1211, + "transform": { + "position": [ + -168.7999725341797, + 8.050000190734863, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "5" + }, + "mesh": "prim://box?size=0.25,0.1,0.25" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 0.125, + 0.05000000074505806, + 0.125 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "colorIndex": 0, + "cycleColors": true, + "pressCount": 0, + "targets": [ + 1254 + ] + }, + "enabled": true, + "type": "InteractableButton" + } + ], + "id": 1256, + "name": "WinCourse_Button", + "parent": 1211, + "transform": { + "position": [ + -168.7999725341797, + 8.649999618530273, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "castShadows": false, + "color": [ + 1.0, + 0.800000011920929, + 0.30000001192092896 + ], + "innerConeAngle": 0.3499999940395355, + "intensity": 6.0, + "outerConeAngle": 0.5, + "range": 8.0, + "type": 1 + }, + "enabled": true, + "type": "LightComponent" + } + ], + "id": 1257, + "name": "WinCourse_GoalLight", + "parent": 1211, + "transform": { + "position": [ + -167.7999725341797, + 10.5, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "height": 1.7999999523162842, + "isTrigger": false, + "radius": 0.4000000059604645 + }, + "enabled": true, + "type": "CapsuleCollider" + }, + { + "data": { + "allowSleep": false, + "freezeRotation": true, + "friction": 0.0, + "mass": 80.0, + "restitution": 0.0, + "type": "Dynamic", + "useGravity": false + }, + "enabled": true, + "type": "Rigidbody" + }, + { + "data": { + "airAcceleration": 15.0, + "coyoteTime": 0.10000000149011612, + "eyeHeight": 0.699999988079071, + "fallGravityMultiplier": 1.600000023841858, + "gravity": 22.0, + "groundAcceleration": 60.0, + "groundStickSpeed": 2.0, + "interactDistance": 3.0, + "jumpBufferTime": 0.11999999731779099, + "jumpHeight": 1.2999999523162842, + "maxSlopeAngle": 0.8726646304130554, + "mouseSensitivity": 0.0020000000949949026, + "pitch": 0.0, + "sprintSpeed": 8.5, + "walkSpeed": 5.0, + "yaw": 1.5707963705062866 + }, + "enabled": true, + "type": "FirstPersonController" + }, + { + "data": { + "killHeight": -15.0, + "respawnPoint": [ + 0.0, + 0.0, + -6.0 + ] + }, + "enabled": true, + "type": "PlayerRespawn" + } + ], + "id": 1258, + "name": "Player", + "parent": 0, + "transform": { + "position": [ + 0.0, + 0.0, + -6.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "drawDebugRects": false, + "referenceHeight": 1080.0, + "scaleFactor": 1.0, + "scaleMode": "scaleWithScreenHeight" + }, + "enabled": true, + "type": "Canvas" + }, + { + "data": { + "color": [ + 1.0, + 1.0, + 1.0, + 0.800000011920929 + ], + "crosshair": 1260, + "focusIndicator": 1263, + "focusedColor": [ + 1.0, + 0.800000011920929, + 0.25, + 1.0 + ], + "focusedSize": 16.0, + "player": 1258, + "size": 10.0 + }, + "enabled": true, + "type": "CrosshairUI" + }, + { + "data": { + "goal": 1254, + "screen": 1266 + }, + "enabled": true, + "type": "WinScreenUI" + } + ], + "id": 1259, + "name": "HUD", + "parent": 0, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 0.5, + 0.5 + ], + "anchorMin": [ + 0.5, + 0.5 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 10.0, + 10.0 + ] + }, + "enabled": true, + "type": "RectTransform" + } + ], + "id": 1260, + "name": "Crosshair", + "parent": 1259, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 1.0, + 0.5 + ], + "anchorMin": [ + 0.0, + 0.5 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "cornerRadius": 0.0, + "filter": "linear", + "outline": 0.0, + "shape": "rect", + "texture": "" + }, + "enabled": true, + "type": "UIImage" + } + ], + "id": 1261, + "name": "Crosshair_Horizontal", + "parent": 1260, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 0.5, + 1.0 + ], + "anchorMin": [ + 0.5, + 0.0 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 2.0, + 0.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "cornerRadius": 0.0, + "filter": "linear", + "outline": 0.0, + "shape": "rect", + "texture": "" + }, + "enabled": true, + "type": "UIImage" + } + ], + "id": 1262, + "name": "Crosshair_Vertical", + "parent": 1260, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 1.0, + 1.0 + ], + "anchorMin": [ + 0.0, + 0.0 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0 + ] + }, + "enabled": true, + "type": "RectTransform" + } + ], + "id": 1263, + "name": "FocusIndicator", + "parent": 1260, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 0.5, + 0.5 + ], + "anchorMin": [ + 0.5, + 0.5 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 28.0, + 28.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "cornerRadius": 0.0, + "filter": "linear", + "outline": 2.0, + "shape": "ellipse", + "texture": "" + }, + "enabled": true, + "type": "UIImage" + } + ], + "id": 1264, + "name": "FocusRing", + "parent": 1263, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 0.5, + 0.5 + ], + "anchorMin": [ + 0.5, + 0.5 + ], + "pivot": [ + 0.5, + 0.0 + ], + "position": [ + 0.0, + 28.0 + ], + "size": [ + 400.0, + 30.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "font": "engine://fonts/Roboto-Medium.ttf", + "fontSize": 22.0, + "horizontalAlign": "center", + "outline": 2.0, + "outlineColor": [ + 0.0, + 0.0, + 0.0, + 0.75 + ], + "text": "[E] Interact", + "verticalAlign": "start" + }, + "enabled": true, + "type": "UIText" + } + ], + "id": 1265, + "name": "InteractPrompt", + "parent": 1263, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": false, + "components": [ + { + "data": { + "anchorMax": [ + 1.0, + 1.0 + ], + "anchorMin": [ + 0.0, + 0.0 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 0.0, + 0.0, + 0.0, + 0.3499999940395355 + ], + "cornerRadius": 0.0, + "filter": "linear", + "outline": 0.0, + "shape": "rect", + "texture": "" + }, + "enabled": true, + "type": "UIImage" + } + ], + "id": 1266, + "name": "WinScreen", + "parent": 1259, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "anchorMax": [ + 1.0, + 1.0 + ], + "anchorMin": [ + 0.0, + 0.0 + ], + "pivot": [ + 0.5, + 0.5 + ], + "position": [ + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0 + ] + }, + "enabled": true, + "type": "RectTransform" + }, + { + "data": { + "color": [ + 1.0, + 0.8500000238418579, + 0.20000000298023224, + 1.0 + ], + "font": "engine://fonts/Roboto-Medium.ttf", + "fontSize": 195.0, + "horizontalAlign": "center", + "outline": 4.0, + "outlineColor": [ + 0.0, + 0.0, + 0.0, + 0.8999999761581421 + ], + "text": "YOU WIN", + "verticalAlign": "center" + }, + "enabled": true, + "type": "UIText" + } + ], + "id": 1267, + "name": "WinScreen_Title", + "parent": 1266, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "1316639e4dcaa6ab872ae26cdccee84c", + "path": "game://new_material.dmat" + }, + "mesh": { + "guid": "c2f1f9dcb185460c6d685c17d7ec4f82", + "path": "engine://tree/base.obj", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1268, + "name": "GameObject", + "parent": 0, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "af12f3099fa89356d6a2db0f7b064312", + "path": "game://materials/prototype_green.dmat" + }, + "mesh": "prim://box?size=2.5,3.8,2.5" + }, + "enabled": true, + "type": "MeshRendererComponent" + }, + { + "data": { + "centerOffset": [ + 0.0, + 0.0, + 0.0 + ], + "halfExtents": [ + 1.25, + 1.899999976158142, + 1.25 + ], + "isTrigger": false + }, + "enabled": true, + "type": "BoxCollider" + }, + { + "data": { + "allowSleep": true, + "freezeRotation": false, + "friction": 0.5, + "mass": 10.0, + "restitution": 0.0, + "type": "Static", + "useGravity": true + }, + "enabled": true, + "type": "Rigidbody" + } + ], + "id": 1269, + "name": "Parkour_Goal", + "parent": 0, + "transform": { + "position": [ + 10.006229400634766, + 0.9000000357627869, + -3.5605835914611816 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [], + "id": 1270, + "name": "capybara", + "parent": 0, + "transform": { + "position": [ + 10.355945587158203, + 3.321378469467163, + 4.4160590171813965 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "a09100c14a67b3840518b68b93fbadda", + "path": "engine://cotw-capybara-male/source/capybara.fbx", + "sub": "material:0" + }, + "mesh": { + "guid": "a09100c14a67b3840518b68b93fbadda", + "path": "engine://cotw-capybara-male/source/capybara.fbx", + "sub": "mesh:0" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1271, + "name": "Capybara", + "parent": 1270, + "transform": { + "position": [ + 0.0, + -4.122679710388184, + 1.4179201126098633 + ], + "rotation": [ + -0.7073400020599365, + 0.0, + 0.0, + 0.7068734765052795 + ], + "scale": [ + 1.0, + 1.0000001192092896, + 1.0000001192092896 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "guid": "a09100c14a67b3840518b68b93fbadda", + "path": "engine://cotw-capybara-male/source/capybara.fbx", + "sub": "material:1" + }, + "mesh": { + "guid": "a09100c14a67b3840518b68b93fbadda", + "path": "engine://cotw-capybara-male/source/capybara.fbx", + "sub": "mesh:1" + } + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 1272, + "name": "Capybara", + "parent": 1270, + "transform": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + } + ], + "version": 3 +} \ No newline at end of file diff --git a/lightkeeper/assets_src/sofa.glb.meta b/lightkeeper/assets_src/sofa.glb.meta new file mode 100644 index 0000000..3af7def --- /dev/null +++ b/lightkeeper/assets_src/sofa.glb.meta @@ -0,0 +1,3 @@ +{ + "guid": "fb9ad9c1679d25eb8c30ffc3e96b3ac4" +} \ No newline at end of file diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index 3b6ddf3..87602fd 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -1,83 +1,36 @@ #ifndef LIGHTKEEPER_H #define LIGHTKEEPER_H -#include -#include +#include +#include #include -#include -#include -#include -#include +#include +#include -#include "destrum/Graphics/Resources/Cubemap.h" -#include "destrum/ObjectModel/GameObject.h" - -#include - -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 editorCamera; - std::optional focusTarget; std::unique_ptr 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 diff --git a/lightkeeper/include/scenes/MainScene.h b/lightkeeper/include/scenes/MainScene.h deleted file mode 100644 index 0c76b2b..0000000 --- a/lightkeeper/include/scenes/MainScene.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef LIGHTKEEPER_MAINSCENE_H -#define LIGHTKEEPER_MAINSCENE_H - -#include - -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 diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index e2cf5ef..063f4ff 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -1,53 +1,44 @@ #include "Lightkeeper.h" +#include +#include +#include + +#include + #include +#include +#include +#include #include +#include +#include #include +#include +#include #include #include -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(m_params.renderSize.x) / static_cast(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(); 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(newWidth) / static_cast(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()) { + 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(std::rand() % 5) - 2.0f, 20.0f, static_cast(std::rand() % 5) - 2.0f}); + ball->AddComponent(radius); + ball->AddComponent()->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(); + meshRenderer->SetMeshID(*mesh); + meshRenderer->SetMaterialID(resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Ball")); + + scene.GetPhysics().RefreshGameObject(*ball); } diff --git a/lightkeeper/src/LightkeeperDebugUi.cpp b/lightkeeper/src/LightkeeperDebugUi.cpp deleted file mode 100644 index fc5234f..0000000 --- a/lightkeeper/src/LightkeeperDebugUi.cpp +++ /dev/null @@ -1,552 +0,0 @@ -#include "Lightkeeper.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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(1.5f); - sphere->AddComponent()->SetMass(1000); - - auto* meshRenderComp = sphere->AddComponent(); - 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::max()}; - glm::vec3 boundsMax{std::numeric_limits::lowest()}; - std::vector 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(); - 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(extent.width), static_cast(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()) { - 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(); - const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static; - - for (const auto& component : object->GetComponents()) { - const auto* collider = dynamic_cast(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); - } - } -} diff --git a/lightkeeper/src/main.cpp b/lightkeeper/src/main.cpp index 23c55e7..950fe18 100644 --- a/lightkeeper/src/main.cpp +++ b/lightkeeper/src/main.cpp @@ -1,16 +1,26 @@ -#include +#include +#include +#include #include #include +#include + #include +#include + +#include #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 arguments{argv, static_cast(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(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; } - diff --git a/lightkeeper/src/scenes/MainScene.cpp b/lightkeeper/src/scenes/MainScene.cpp deleted file mode 100644 index 98d7bd9..0000000 --- a/lightkeeper/src/scenes/MainScene.cpp +++ /dev/null @@ -1,792 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include -#include -#include - -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(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(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(); - 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(glm::vec3{GroundHalfExtent, 0.5f, GroundHalfExtent}); - groundObj->AddComponent()->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(); - boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox(size))); - boxMeshComp->SetMaterialID(materialID); - - box->AddComponent(size * 0.5f); - auto* boxRb = box->AddComponent(); - 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(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()->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()->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()->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(); - 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(glm::vec3{half.x, 1.0f, half.y})->SetTrigger(true); - trigger->AddComponent()->SetType(RigidbodyType::Static); - trigger->AddComponent()->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(); - - 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()->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(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 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& opening = std::nullopt) - { - constexpr float thickness = 0.3f; - std::vector 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(); - 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(LightType::Point); - light->SetColor(glm::vec3{1.0f, 0.82f, 0.55f}); - light->SetIntensity(4.0f); - light->SetRange(7.0f); - - lightObj->AddComponent(); - 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& 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(); - 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(0.15f * scale, 2.0f * scale); - treeObj->AddComponent()->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(); - charMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, charPrimitive.mesh)); - charMeshComp->SetMaterialID(charMaterialID); - - auto* animator = charObj->AddComponent(); - 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(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(glm::vec3{3.0f, 3.0f, 3.0f})->SetTrigger(true); - triggerObj->AddComponent()->SetType(RigidbodyType::Static); - triggerObj->AddComponent(); - 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(0.4f, 1.8f); - playerObj->AddComponent(); - - // Facing the parkour course - auto* controller = playerObj->AddComponent(); - controller->SetYaw(glm::half_pi()); - playerObj->AddComponent(); - 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(); - 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(); - - 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(); - - 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(); - 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("[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(glm::vec4{0.0f, 0.0f, 0.0f, 0.35f}); - - auto* titleText = CreateUIObject(scene, "WinScreen_Title", *screen)->GetGameObject()->AddComponent("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(); - - GameObject* crosshair = CreateCrosshair(scene, *hud); - const GameObject* focusIndicator = CreateFocusIndicator(scene, *crosshair); - auto* crosshairUi = hud->AddComponent(); - crosshairUi->SetPlayer(player); - crosshairUi->SetCrosshair(*crosshair); - crosshairUi->SetFocusIndicator(*focusIndicator); - - auto* winScreenUi = hud->AddComponent(); - 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; -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1806873..df3878c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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() diff --git a/tests/assets_tests.cpp b/tests/assets_tests.cpp index 6f6e198..db2accc 100644 --- a/tests/assets_tests.cpp +++ b/tests/assets_tests.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include #include @@ -16,6 +18,8 @@ #include #include #include +#include +#include #include "TestUtils.h" @@ -447,6 +451,71 @@ namespace { check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(), "plain cache names must not be treated as file-backed assets"); } + + void testAssetGuids() + { + const AssetGuid first = AssetGuid::Generate(); + const AssetGuid second = AssetGuid::Generate(); + check(first.IsValid() && second.IsValid() && first != second, "generated asset ids must be valid and unique"); + + const std::string text = first.ToString(); + check(text.size() == 32 && AssetGuid::Parse(text) == first, "asset ids must round-trip through their text form"); + check(!AssetGuid::Parse("1234").has_value() && !AssetGuid::Parse(std::string(32, 'z')).has_value() && + !AssetGuid::Parse(std::string(32, '0')).has_value(), + "malformed or empty asset ids must be rejected"); + } + + void testAssetDatabaseFollowsMovedAssets() + { + auto& database = AssetDatabase::GetInstance(); + database.Clear(); + + const AssetGuid guid = AssetGuid::Generate(); + database.Register("game://models/crate.glb", guid); + check(database.FindGuid("game://models/crate.glb") == guid && database.FindPath(guid) == "game://models/crate.glb", + "registered assets must be found by path and by id"); + + const nlohmann::json reference = database.WriteReference("game://models/crate.glb", "mesh:0"); + check(reference.at("guid") == guid.ToString() && reference.at("sub") == "mesh:0", + "references to known assets must carry the id and subresource"); + check(!database.WriteReference("game://unknown.png").contains("guid"), + "assets without a meta file must not get an id outside a save"); + + database.Clear(); + database.Register("game://props/crate.glb", guid); + const auto moved = database.ReadReference(reference); + check(moved.has_value() && moved->path == "game://props/crate.glb" && moved->subresource == "mesh:0", + "references must follow their id when the asset moved"); + + const auto byPath = database.ReadReference(nlohmann::json{{"path", "game://other.png"}}); + check(byPath.has_value() && byPath->path == "game://other.png", "references without a known id must keep their path"); + check(!database.ReadReference(nlohmann::json{{"guid", guid.ToString()}}).has_value(), + "a reference needs a path to fall back on"); + database.Clear(); + } + + void testAssetTypes() + { + check(AssetTypes::IsModel("game://a/B.GLB") && AssetTypes::IsImage("x.Png") && AssetTypes::IsMaterial("m.dmat") && + AssetTypes::IsScene("game://scenes/main.scene") && AssetTypes::IsPrefab("p.prefab") && AssetTypes::IsMeta("a.glb.meta"), + "asset types must be recognised by extension, ignoring case"); + check(!AssetTypes::IsModel("model.glb.meta") && !AssetTypes::IsScene("scene.json"), + "only the last extension decides the asset type"); + check(MaterialSerialization::IsFileKey("game://materials/a.dmat") && !MaterialSerialization::IsFileKey("local:3:1") && + !MaterialSerialization::IsFileKey("game://a.glb#material:0"), + "only .dmat paths are material files"); + } + + void testMainSceneFileIsValid() + { + std::ifstream file{std::filesystem::path{LIGHTKEEPER_ASSETS_SRC_DIR} / "scenes" / "main.scene"}; + const auto root = nlohmann::json::parse(file, nullptr, false); + check(!root.is_discarded() && root.at("version") == ObjectSerialization::CurrentObjectVersion && + root.at("objects").size() > 100, + "the main scene must be saved in the current format with the whole level"); + check(!MaterialSerialization::Validate(root.at("materials")).has_value(), + "the main scene's local materials must be valid"); + } } void RunAssetsTests() @@ -460,4 +529,8 @@ void RunAssetsTests() testTextureFileRoundTrip(); testImageLoaderReadsCookedTextures(); testAssetReferenceCacheKeys(); + testAssetGuids(); + testAssetDatabaseFollowsMovedAssets(); + testAssetTypes(); + testMainSceneFileIsValid(); } diff --git a/tests/editor_tests.cpp b/tests/editor_tests.cpp new file mode 100644 index 0000000..b836b9e --- /dev/null +++ b/tests/editor_tests.cpp @@ -0,0 +1,68 @@ +#include +#include +#include + +#include + +#include "TestUtils.h" + +using namespace test; + +namespace { + void testUndoRedoRestoresEdits() + { + Scene& scene = SceneManager::GetInstance().CreateScene("undo"); + GameObject* box = scene.CreateGameObject("box"); + box->GetTransform().SetLocalPosition({1.0f, 0.0f, 0.0f}); + scene.CommitPendingAdditions(); + + UndoStack history; + history.Reset(scene); + check(!history.CanUndo() && !history.Record(scene), "an unchanged scene must not create an undo step"); + + findByName(scene, "box")->GetTransform().SetLocalPosition({5.0f, 0.0f, 0.0f}); + scene.SetSelectedObject(findByName(scene, "box")->GetId()); + check(history.Record(scene) && history.CanUndo() && !history.CanRedo(), "an edit must create an undo step"); + + check(history.Undo(scene), "undo must succeed"); + scene.CommitPendingAdditions(); + const GameObject* undone = findByName(scene, "box"); + check(undone != nullptr && close(undone->GetTransform().GetLocalPosition().x, 1.0f), "undo must restore the earlier state"); + check(scene.GetSelectedObject() == undone, "undo must keep the edited object selected"); + check(history.CanRedo(), "undo must allow redo"); + + check(history.Redo(scene), "redo must succeed"); + scene.CommitPendingAdditions(); + check(close(findByName(scene, "box")->GetTransform().GetLocalPosition().x, 5.0f), "redo must bring the edit back"); + check(!history.Record(scene), "reloading the scene for undo must not count as an edit"); + + scene.CreateGameObject("spawned"); + scene.CommitPendingAdditions(); + check(history.Record(scene) && !history.CanRedo(), "a new edit must drop the redo steps"); + + SceneManager::GetInstance().Destroy(); + } + + void testUndoHistoryIsCapped() + { + Scene& scene = SceneManager::GetInstance().CreateScene("undo cap"); + GameObject* box = scene.CreateGameObject("box"); + scene.CommitPendingAdditions(); + + UndoStack history; + history.Reset(scene); + for (std::size_t i = 0; i < UndoStack::MaxSteps + 10; ++i) { + box->GetTransform().SetLocalPosition({static_cast(i + 1), 0.0f, 0.0f}); + history.Record(scene); + } + check(history.UndoCount() == UndoStack::MaxSteps, "the undo history must keep only the newest steps"); + + SceneManager::GetInstance().Destroy(); + } +} + +void RunEditorTests() +{ + testUndoRedoRestoresEdits(); + testUndoHistoryIsCapped(); +} diff --git a/tests/main.cpp b/tests/main.cpp index ca7d102..5706f89 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -9,6 +9,7 @@ void RunAssetsTests(); void RunGraphicsTests(); void RunUiTests(); void RunGameplayTests(); +void RunEditorTests(); namespace { struct Suite { @@ -24,6 +25,7 @@ namespace { {"graphics", RunGraphicsTests}, {"ui", RunUiTests}, {"gameplay", RunGameplayTests}, + {"editor", RunEditorTests}, }; } diff --git a/tests/serialization_tests.cpp b/tests/serialization_tests.cpp index 7dc07b7..26a92a4 100644 --- a/tests/serialization_tests.cpp +++ b/tests/serialization_tests.cpp @@ -36,6 +36,8 @@ namespace { environment.fogDensity = 0.05f; environment.skyboxRotationSpeed = 0.5f; environment.skyboxRotation = 1.0f; + environment.exposure = -1.5f; + environment.tonemapper = Tonemapper::Aces; scene.Update(1.0f); check(close(environment.skyboxRotation, 1.5f), "scene updates must spin the skybox"); @@ -56,7 +58,8 @@ namespace { loadedEnvironment.fogColor == glm::vec3{0.6f, 0.7f, 0.8f} && close(loadedEnvironment.fogDensity, 0.05f) && close(loadedEnvironment.skyboxRotationSpeed, 0.5f) && - close(loadedEnvironment.skyboxRotation, 1.5f), + close(loadedEnvironment.skyboxRotation, 1.5f) && + close(loadedEnvironment.exposure, -1.5f) && loadedEnvironment.tonemapper == Tonemapper::Aces, "scene round-trip must preserve the environment settings"); SceneManager::GetInstance().Destroy(); @@ -370,8 +373,19 @@ namespace { } } +namespace { + void testTonemapperNames() + { + for (const Tonemapper tonemapper : {Tonemapper::None, Tonemapper::Aces, Tonemapper::PbrNeutral}) { + check(ParseTonemapper(ToString(tonemapper)) == tonemapper, "tonemapper names must round-trip"); + } + check(!ParseTonemapper("filmic").has_value(), "unknown tonemapper names must be rejected"); + } +} + void RunSerializationTests() { + testTonemapperNames(); testSceneRoundTrip(); testLegacySceneLoad(); testSceneLoadRollback();