From 118016bcc7d429a12b530b1b09191933729ce9a8 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Sat, 24 Oct 2026 02:34:09 +0200 Subject: [PATCH] feat: add more collider types + camera snapping fix --- destrum/CMakeLists.txt | 5 + .../Components/Physics/ConvexHullCollider.h | 41 + .../Components/Physics/CylinderCollider.h | 66 ++ .../destrum/Components/Physics/MeshCollider.h | 32 + .../Components/Physics/MeshShapeCollider.h | 36 + destrum/include/destrum/Graphics/DebugDraw.h | 9 + .../destrum/Physics/PhysicsMeshCache.h | 18 + .../include/destrum/Physics/PhysicsTypes.h | 59 +- .../Serialization/EngineComponentList.h | 5 +- .../src/Components/MeshRendererComponent.cpp | 4 + .../Components/Physics/ConvexHullCollider.cpp | 29 + .../Components/Physics/CylinderCollider.cpp | 23 + .../src/Components/Physics/MeshCollider.cpp | 43 + .../Components/Physics/MeshShapeCollider.cpp | 118 +++ destrum/src/Graphics/DebugDraw.cpp | 46 + destrum/src/Physics/JoltPhysicsWorld.cpp | 117 ++- destrum/src/Physics/PhysicsMeshCache.cpp | 151 +++ destrum/src/Physics/PhysicsWorld.cpp | 4 + editor/include/editor/Editor.h | 1 + editor/include/editor/UndoStack.h | 11 + editor/src/Editor.cpp | 15 +- editor/src/UndoStack.cpp | 10 + lightkeeper/assets_src/scenes/main.scene | 895 +++++++++++------- tests/core_tests.cpp | 3 + tests/editor_tests.cpp | 14 + tests/physics_tests.cpp | 124 +++ 26 files changed, 1525 insertions(+), 354 deletions(-) create mode 100644 destrum/include/destrum/Components/Physics/ConvexHullCollider.h create mode 100644 destrum/include/destrum/Components/Physics/CylinderCollider.h create mode 100644 destrum/include/destrum/Components/Physics/MeshCollider.h create mode 100644 destrum/include/destrum/Components/Physics/MeshShapeCollider.h create mode 100644 destrum/include/destrum/Physics/PhysicsMeshCache.h create mode 100644 destrum/src/Components/Physics/ConvexHullCollider.cpp create mode 100644 destrum/src/Components/Physics/CylinderCollider.cpp create mode 100644 destrum/src/Components/Physics/MeshCollider.cpp create mode 100644 destrum/src/Components/Physics/MeshShapeCollider.cpp create mode 100644 destrum/src/Physics/PhysicsMeshCache.cpp diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 87d77a2..cf550d3 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -54,6 +54,10 @@ set(SRC_FILES "src/Components/Physics/BoxCollider.cpp" "src/Components/Physics/CapsuleCollider.cpp" "src/Components/Physics/Collider.cpp" + "src/Components/Physics/ConvexHullCollider.cpp" + "src/Components/Physics/CylinderCollider.cpp" + "src/Components/Physics/MeshCollider.cpp" + "src/Components/Physics/MeshShapeCollider.cpp" "src/Components/Physics/SphereCollider.cpp" "src/Components/UI/Canvas.cpp" @@ -137,6 +141,7 @@ set(SRC_FILES "src/Threading/JobSystem.cpp" "src/Physics/PhysicsWorld.cpp" + "src/Physics/PhysicsMeshCache.cpp" "src/Physics/PhysicsSceneBridge.cpp" "src/Physics/JoltPhysicsWorld.cpp" ) diff --git a/destrum/include/destrum/Components/Physics/ConvexHullCollider.h b/destrum/include/destrum/Components/Physics/ConvexHullCollider.h new file mode 100644 index 0000000..e3f0377 --- /dev/null +++ b/destrum/include/destrum/Components/Physics/ConvexHullCollider.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +class ConvexHullCollider final : public MeshShapeCollider { +public: + static constexpr int MinVertices = 4; + static constexpr int MaxVertices = 256; + + explicit ConvexHullCollider(GameObject& owner, int maxVertices = 64) + : MeshShapeCollider(owner) + , m_MaxVertices(std::clamp(maxVertices, MinVertices, MaxVertices)) {} + + std::string GetTypeName() const override { + return "ConvexHullCollider"; + } + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; + + void DrawDebugLines(const glm::vec4& color) const override; + + [[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override { + return BuildConvexHullShape(m_MaxVertices); + } + + [[nodiscard]] int GetMaxVertices() const { + return m_MaxVertices; + } + + void SetMaxVertices(int maxVertices) { + m_MaxVertices = std::clamp(maxVertices, MinVertices, MaxVertices); + NotifyPhysicsChanged(); + } + +private: + int m_MaxVertices{64}; +}; diff --git a/destrum/include/destrum/Components/Physics/CylinderCollider.h b/destrum/include/destrum/Components/Physics/CylinderCollider.h new file mode 100644 index 0000000..2c942af --- /dev/null +++ b/destrum/include/destrum/Components/Physics/CylinderCollider.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include + +class CylinderCollider final : public Collider { +public: + explicit CylinderCollider(GameObject& owner, float radius = 0.5f, float height = 1.0f) + : Collider(owner) + , m_Radius(std::max(radius, 0.0001f)) + , m_Height(std::max(height, 0.0001f)) {} + + void Update(float) override {} + + std::string GetTypeName() const override { + return "CylinderCollider"; + } + + void ImGuiInspector() override; + + nlohmann::json Serialize() const override { + auto data = SerializeCollider(); + data["radius"] = m_Radius; + data["height"] = m_Height; + return data; + } + + void Deserialize(const nlohmann::json& data) override { + DeserializeCollider(data); + if (data.contains("radius")) { + SetRadius(data.at("radius").get()); + } + if (data.contains("height")) { + SetHeight(data.at("height").get()); + } + } + + void DrawDebugLines(const glm::vec4& color) const override; + + [[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override { + return PhysicsShapeDesc::Cylinder(m_Radius, m_Height, m_CenterOffset, m_IsTrigger); + } + + [[nodiscard]] float GetRadius() const { + return m_Radius; + } + + void SetRadius(float radius) { + m_Radius = std::max(radius, 0.0001f); + NotifyPhysicsChanged(); + } + + [[nodiscard]] float GetHeight() const { + return m_Height; + } + + void SetHeight(float height) { + m_Height = std::max(height, 0.0001f); + NotifyPhysicsChanged(); + } + +private: + float m_Radius{0.5f}; + float m_Height{1.0f}; +}; diff --git a/destrum/include/destrum/Components/Physics/MeshCollider.h b/destrum/include/destrum/Components/Physics/MeshCollider.h new file mode 100644 index 0000000..e0147fd --- /dev/null +++ b/destrum/include/destrum/Components/Physics/MeshCollider.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +// Exact triangle mesh for static and kinematic bodies. Dynamic bodies can't use one, they get a convex hull instead. +class MeshCollider final : public MeshShapeCollider { +public: + static constexpr int DynamicHullVertices = 64; + + explicit MeshCollider(GameObject& owner) + : MeshShapeCollider(owner) {} + + std::string GetTypeName() const override { + return "MeshCollider"; + } + + nlohmann::json Serialize() const override { + return SerializeCollider(); + } + + void Deserialize(const nlohmann::json& data) override { + DeserializeCollider(data); + } + + void ImGuiInspector() override; + + void DrawDebugLines(const glm::vec4& color) const override; + + [[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override; + + [[nodiscard]] bool UsesConvexFallback() const; +}; diff --git a/destrum/include/destrum/Components/Physics/MeshShapeCollider.h b/destrum/include/destrum/Components/Physics/MeshShapeCollider.h new file mode 100644 index 0000000..583cfb2 --- /dev/null +++ b/destrum/include/destrum/Components/Physics/MeshShapeCollider.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +// Base for colliders shaped by the MeshRendererComponent on the same object. Unlike the primitive colliders, +// the mesh is in local units, so the transform's world scale is applied. +class MeshShapeCollider : public Collider { +public: + explicit MeshShapeCollider(GameObject& owner) + : Collider(owner) {} + + void Update(float) override {} + + void ImGuiInspector() override; + + // Used instead of the MeshRenderer mesh, not serialized + void SetCustomMesh(std::shared_ptr mesh) { + m_CustomMesh = std::move(mesh); + NotifyPhysicsChanged(); + } + + [[nodiscard]] std::shared_ptr GetSourceMesh() const; + +protected: + [[nodiscard]] glm::vec3 GetShapeScale() const; + [[nodiscard]] glm::mat4 GetShapeMatrix() const; + + // Falls back to a box around the mesh when it has no volume + [[nodiscard]] PhysicsShapeDesc BuildConvexHullShape(int maxVertices) const; + void DrawConvexHull(int maxVertices, const glm::vec4& color) const; + +private: + std::shared_ptr m_CustomMesh; +}; diff --git a/destrum/include/destrum/Graphics/DebugDraw.h b/destrum/include/destrum/Graphics/DebugDraw.h index 4362eaf..19e3ef1 100644 --- a/destrum/include/destrum/Graphics/DebugDraw.h +++ b/destrum/include/destrum/Graphics/DebugDraw.h @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace DebugDraw { void Arc(const glm::vec3& center, const glm::vec3& axisA, const glm::vec3& axisB, float radius, float fromAngle, float toAngle, const glm::vec4& color, int segments = 16); @@ -14,6 +17,12 @@ namespace DebugDraw { void Sphere(const glm::vec3& center, float radius, const glm::vec4& color); void Capsule(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color); + void Cylinder(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color); + + // Wireframe of an indexed triangle list, stops after maxTriangles + void Triangles(const glm::mat4& transform, const std::vector& points, + const std::vector& indices, const glm::vec4& color, + std::size_t maxTriangles = 20000); } #endif // DEBUGDRAW_H diff --git a/destrum/include/destrum/Physics/PhysicsMeshCache.h b/destrum/include/destrum/Physics/PhysicsMeshCache.h new file mode 100644 index 0000000..f9aee4d --- /dev/null +++ b/destrum/include/destrum/Physics/PhysicsMeshCache.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +struct CPUMesh; + +// Shares collision data between colliders using the same mesh, so physics backends can share their shapes too. +// Main thread only. +namespace PhysicsMeshCache { + [[nodiscard]] std::shared_ptr FromCPUMesh(const std::string& key, const CPUMesh& mesh); + + // Null when the points don't span a volume (e.g. a plane) + [[nodiscard]] std::shared_ptr ConvexHull(const std::shared_ptr& source, + int maxVertices); +} diff --git a/destrum/include/destrum/Physics/PhysicsTypes.h b/destrum/include/destrum/Physics/PhysicsTypes.h index 72c1c5a..562aaef 100644 --- a/destrum/include/destrum/Physics/PhysicsTypes.h +++ b/destrum/include/destrum/Physics/PhysicsTypes.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -17,7 +19,10 @@ enum class PhysicsShapeType { None, Box, Sphere, - Capsule + Capsule, + Cylinder, + ConvexHull, + Mesh }; struct PhysicsBodyHandle { @@ -50,6 +55,12 @@ struct PhysicsMaterialDesc { float restitution{0.0f}; }; +// Triangles share vertices through indices. A convex hull only uses the points, the indices are for drawing it. +struct PhysicsMeshData { + std::vector points; + std::vector indices; +}; + struct PhysicsShapeDesc { PhysicsShapeType type{PhysicsShapeType::None}; @@ -59,9 +70,14 @@ struct PhysicsShapeDesc { // Sphere / capsule float radius{0.5f}; - // Capsule full height, including the two hemispheres. + // Capsule full height, including the two hemispheres. Cylinder full height. float height{2.0f}; + // Convex hull / mesh, in the collider's local space before scale. Backends may cache + // shapes per mesh data, so it must not be modified once shared. + std::shared_ptr mesh; + glm::vec3 scale{1.0f}; + // Local offset from the rigidbody transform. glm::vec3 centerOffset{0.0f}; @@ -101,6 +117,45 @@ struct PhysicsShapeDesc { desc.isTrigger = isTrigger; return desc; } + + [[nodiscard]] static PhysicsShapeDesc Cylinder(float radius, + float height, + const glm::vec3& centerOffset = glm::vec3{0.0f}, + bool isTrigger = false) { + PhysicsShapeDesc desc{}; + desc.type = PhysicsShapeType::Cylinder; + desc.radius = radius; + desc.height = height; + desc.centerOffset = centerOffset; + desc.isTrigger = isTrigger; + return desc; + } + + [[nodiscard]] static PhysicsShapeDesc ConvexHull(std::shared_ptr hull, + const glm::vec3& scale = glm::vec3{1.0f}, + const glm::vec3& centerOffset = glm::vec3{0.0f}, + bool isTrigger = false) { + PhysicsShapeDesc desc{}; + desc.type = PhysicsShapeType::ConvexHull; + desc.mesh = std::move(hull); + desc.scale = scale; + desc.centerOffset = centerOffset; + desc.isTrigger = isTrigger; + return desc; + } + + [[nodiscard]] static PhysicsShapeDesc Mesh(std::shared_ptr mesh, + const glm::vec3& scale = glm::vec3{1.0f}, + const glm::vec3& centerOffset = glm::vec3{0.0f}, + bool isTrigger = false) { + PhysicsShapeDesc desc{}; + desc.type = PhysicsShapeType::Mesh; + desc.mesh = std::move(mesh); + desc.scale = scale; + desc.centerOffset = centerOffset; + desc.isTrigger = isTrigger; + return desc; + } }; struct PhysicsBodyDesc { diff --git a/destrum/include/destrum/Serialization/EngineComponentList.h b/destrum/include/destrum/Serialization/EngineComponentList.h index 00a9873..6493f6e 100644 --- a/destrum/include/destrum/Serialization/EngineComponentList.h +++ b/destrum/include/destrum/Serialization/EngineComponentList.h @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -32,7 +35,7 @@ X(Rigidbody) \ X(BoxCollider) \ X(SphereCollider) \ - X(CapsuleCollider) \ + X(CapsuleCollider) X(CylinderCollider) X(ConvexHullCollider) X(MeshCollider) \ X(Canvas) \ X(RectTransform) \ X(UIImage) \ diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index 5b462e6..3a528a4 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -254,6 +255,9 @@ void MeshRendererComponent::SetMeshID(MeshID id) { meshKey = resources->meshes().getMeshKey(id); } } + if (GetGameObject()->GetComponent() != nullptr) { + NotifyPhysicsChanged(); + } } void MeshRendererComponent::SetMaterialID(MaterialID id) { diff --git a/destrum/src/Components/Physics/ConvexHullCollider.cpp b/destrum/src/Components/Physics/ConvexHullCollider.cpp new file mode 100644 index 0000000..eba2b29 --- /dev/null +++ b/destrum/src/Components/Physics/ConvexHullCollider.cpp @@ -0,0 +1,29 @@ +#include + +#include + +nlohmann::json ConvexHullCollider::Serialize() const { + auto data = SerializeCollider(); + data["maxVertices"] = m_MaxVertices; + return data; +} + +void ConvexHullCollider::Deserialize(const nlohmann::json& data) { + DeserializeCollider(data); + if (data.contains("maxVertices")) { + SetMaxVertices(data.at("maxVertices").get()); + } +} + +void ConvexHullCollider::DrawDebugLines(const glm::vec4& color) const { + DrawConvexHull(m_MaxVertices, color); +} + +void ConvexHullCollider::ImGuiInspector() { + MeshShapeCollider::ImGuiInspector(); + + int maxVertices = m_MaxVertices; + if (ImGui::SliderInt("Max vertices", &maxVertices, MinVertices, MaxVertices)) { + SetMaxVertices(maxVertices); + } +} diff --git a/destrum/src/Components/Physics/CylinderCollider.cpp b/destrum/src/Components/Physics/CylinderCollider.cpp new file mode 100644 index 0000000..e76294f --- /dev/null +++ b/destrum/src/Components/Physics/CylinderCollider.cpp @@ -0,0 +1,23 @@ +#include + +#include + +#include + +void CylinderCollider::DrawDebugLines(const glm::vec4& color) const { + DebugDraw::Cylinder(GetWorldCenter(), GetWorldRotation(), m_Radius, m_Height, color); +} + +void CylinderCollider::ImGuiInspector() { + Collider::ImGuiInspector(); + + float radius = m_Radius; + if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) { + SetRadius(radius); + } + + float height = m_Height; + if (ImGui::DragFloat("Height", &height, 0.05f, 0.0001f)) { + SetHeight(height); + } +} diff --git a/destrum/src/Components/Physics/MeshCollider.cpp b/destrum/src/Components/Physics/MeshCollider.cpp new file mode 100644 index 0000000..de401de --- /dev/null +++ b/destrum/src/Components/Physics/MeshCollider.cpp @@ -0,0 +1,43 @@ +#include + +#include + +#include +#include +#include + +bool MeshCollider::UsesConvexFallback() const { + const auto* rigidbody = GetGameObject()->GetComponent(); + return rigidbody != nullptr && rigidbody->GetType() == RigidbodyType::Dynamic; +} + +PhysicsShapeDesc MeshCollider::BuildPhysicsShape() const { + if (UsesConvexFallback()) { + return BuildConvexHullShape(DynamicHullVertices); + } + + auto source = GetSourceMesh(); + if (!source || source->indices.empty()) { + return {}; + } + return PhysicsShapeDesc::Mesh(std::move(source), GetShapeScale(), m_CenterOffset, m_IsTrigger); +} + +void MeshCollider::DrawDebugLines(const glm::vec4& color) const { + if (UsesConvexFallback()) { + DrawConvexHull(DynamicHullVertices, color); + return; + } + + if (const auto source = GetSourceMesh()) { + DebugDraw::Triangles(GetShapeMatrix(), source->points, source->indices, color); + } +} + +void MeshCollider::ImGuiInspector() { + MeshShapeCollider::ImGuiInspector(); + + if (UsesConvexFallback()) { + ImGui::TextColored(ImVec4{1.0f, 0.6f, 0.2f, 1.0f}, "Dynamic rigidbody: using a convex hull"); + } +} diff --git a/destrum/src/Components/Physics/MeshShapeCollider.cpp b/destrum/src/Components/Physics/MeshShapeCollider.cpp new file mode 100644 index 0000000..13cb1c8 --- /dev/null +++ b/destrum/src/Components/Physics/MeshShapeCollider.cpp @@ -0,0 +1,118 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + struct Bounds { + glm::vec3 center{0.0f}; + glm::vec3 halfExtents{0.0f}; + }; + + Bounds ComputeBounds(const PhysicsMeshData& mesh) { + glm::vec3 min{std::numeric_limits::max()}; + glm::vec3 max{std::numeric_limits::lowest()}; + for (const glm::vec3& point : mesh.points) { + min = glm::min(min, point); + max = glm::max(max, point); + } + return {(min + max) * 0.5f, (max - min) * 0.5f}; + } +} + +std::shared_ptr MeshShapeCollider::GetSourceMesh() const { + if (m_CustomMesh) { + return m_CustomMesh; + } + + const auto* meshRenderer = GetGameObject()->GetComponent(); + if (meshRenderer == nullptr || meshRenderer->GetMeshID() == NULL_MESH_ID || !GameState::GetInstance().HasRenderer()) { + return nullptr; + } + + const RenderResources* resources = GameState::GetInstance().Renderer().getResources(); + if (resources == nullptr || !resources->meshes().isValid(meshRenderer->GetMeshID())) { + return nullptr; + } + + const MeshCache& meshes = resources->meshes(); + return PhysicsMeshCache::FromCPUMesh(meshes.getMeshKey(meshRenderer->GetMeshID()), + meshes.getCPUMesh(meshRenderer->GetMeshID())); +} + +glm::vec3 MeshShapeCollider::GetShapeScale() const { + glm::vec3 scale = GetTransform().GetWorldScale(); + for (int axis = 0; axis < 3; ++axis) { + if (std::abs(scale[axis]) < 0.0001f) { + scale[axis] = scale[axis] < 0.0f ? -0.0001f : 0.0001f; + } + } + return scale; +} + +glm::mat4 MeshShapeCollider::GetShapeMatrix() const { + return glm::translate(glm::mat4{1.0f}, GetWorldCenter()) * + glm::mat4_cast(GetWorldRotation()) * + glm::scale(glm::mat4{1.0f}, GetShapeScale()); +} + +PhysicsShapeDesc MeshShapeCollider::BuildConvexHullShape(int maxVertices) const { + const auto source = GetSourceMesh(); + if (!source || source->points.empty()) { + return {}; + } + + const glm::vec3 scale = GetShapeScale(); + if (auto hull = PhysicsMeshCache::ConvexHull(source, maxVertices)) { + return PhysicsShapeDesc::ConvexHull(std::move(hull), scale, m_CenterOffset, m_IsTrigger); + } + + const Bounds bounds = ComputeBounds(*source); + return PhysicsShapeDesc::Box(glm::max(glm::abs(bounds.halfExtents * scale), glm::vec3{0.01f}), + m_CenterOffset + bounds.center * scale, + m_IsTrigger); +} + +void MeshShapeCollider::DrawConvexHull(int maxVertices, const glm::vec4& color) const { + const auto source = GetSourceMesh(); + if (!source || source->points.empty()) { + return; + } + + if (const auto hull = PhysicsMeshCache::ConvexHull(source, maxVertices)) { + DebugDraw::Triangles(GetShapeMatrix(), hull->points, hull->indices, color); + return; + } + + const Bounds bounds = ComputeBounds(*source); + const glm::vec3 scale = GetShapeScale(); + DebugDraw::Box(GetWorldCenter() + GetWorldRotation() * (bounds.center * scale), + GetWorldRotation(), + glm::max(glm::abs(bounds.halfExtents * scale), glm::vec3{0.01f}), + color); +} + +void MeshShapeCollider::ImGuiInspector() { + Collider::ImGuiInspector(); + + if (m_CustomMesh) { + ImGui::TextDisabled("Custom mesh: %zu points", m_CustomMesh->points.size()); + } else if (const auto source = GetSourceMesh()) { + ImGui::TextDisabled("Mesh: %zu vertices, %zu triangles", source->points.size(), source->indices.size() / 3); + } else { + ImGui::TextColored(ImVec4{1.0f, 0.6f, 0.2f, 1.0f}, "Needs a MeshRendererComponent with a mesh"); + } +} diff --git a/destrum/src/Graphics/DebugDraw.cpp b/destrum/src/Graphics/DebugDraw.cpp index 90a9d47..8a7df7e 100644 --- a/destrum/src/Graphics/DebugDraw.cpp +++ b/destrum/src/Graphics/DebugDraw.cpp @@ -94,3 +94,49 @@ void DebugDraw::Capsule(const glm::vec3& center, const glm::quat& rotation, floa Arc(bottom, right, up, radius, pi, glm::two_pi(), color); Arc(bottom, forward, up, radius, pi, glm::two_pi(), color); } + +void DebugDraw::Cylinder(const glm::vec3& center, const glm::quat& rotation, float radius, float height, + const glm::vec4& color) +{ + const glm::vec3 right = rotation * glm::vec3{1.0f, 0.0f, 0.0f}; + const glm::vec3 up = rotation * glm::vec3{0.0f, 1.0f, 0.0f}; + const glm::vec3 forward = rotation * glm::vec3{0.0f, 0.0f, 1.0f}; + + const glm::vec3 top = center + up * (height * 0.5f); + const glm::vec3 bottom = center - up * (height * 0.5f); + + Circle(top, right, forward, radius, color); + Circle(bottom, right, forward, radius, color); + + auto& lines = LineRenderingManager::GetInstance(); + for (const glm::vec3& side : {right, -right, forward, -forward}) { + lines.SubmitLine(top + side * radius, bottom + side * radius, color); + } +} + +void DebugDraw::Triangles(const glm::mat4& transform, const std::vector& points, + const std::vector& indices, const glm::vec4& color, + std::size_t maxTriangles) +{ + auto& lines = LineRenderingManager::GetInstance(); + const auto at = [&](std::uint32_t index) { + return glm::vec3(transform * glm::vec4(points[index], 1.0f)); + }; + + const std::size_t count = std::min(indices.size() / 3, maxTriangles); + for (std::size_t triangle = 0; triangle < count; ++triangle) { + const std::uint32_t ia = indices[triangle * 3]; + const std::uint32_t ib = indices[triangle * 3 + 1]; + const std::uint32_t ic = indices[triangle * 3 + 2]; + if (std::max({ia, ib, ic}) >= points.size()) { + continue; + } + + const glm::vec3 a = at(ia); + const glm::vec3 b = at(ib); + const glm::vec3 c = at(ic); + lines.SubmitLine(a, b, color); + lines.SubmitLine(b, c, color); + lines.SubmitLine(c, a, color); + } +} diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp index 966fea1..5cdfcd1 100644 --- a/destrum/src/Physics/JoltPhysicsWorld.cpp +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -16,7 +16,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -34,7 +38,9 @@ #include #include +#include #include +#include #include #include @@ -270,6 +276,46 @@ namespace return type == RigidbodyType::Static ? Layers::NON_MOVING : Layers::MOVING; } + [[nodiscard]] ShapeRefC CheckedCreate(const ShapeSettings& settings, const char* what) + { + ShapeSettings::ShapeResult result = settings.Create(); + if (!result.IsValid()) + { + throw std::runtime_error( + std::string("Failed to create Jolt ") + what + " shape: " + std::string(result.GetError())); + } + return result.Get(); + } + + [[nodiscard]] ShapeRefC CreateConvexHullShape(const PhysicsMeshData& hull) + { + Array points; + points.reserve(hull.points.size()); + for (const glm::vec3& point : hull.points) + { + points.push_back(ToJoltVec3(point)); + } + return CheckedCreate(ConvexHullShapeSettings(points), "convex hull"); + } + + [[nodiscard]] ShapeRefC CreateMeshShape(const PhysicsMeshData& mesh) + { + VertexList vertices; + vertices.reserve(mesh.points.size()); + for (const glm::vec3& point : mesh.points) + { + vertices.push_back(Float3(point.x, point.y, point.z)); + } + + IndexedTriangleList triangles; + triangles.reserve(mesh.indices.size() / 3); + for (std::size_t i = 0; i + 2 < mesh.indices.size(); i += 3) + { + triangles.push_back(IndexedTriangle(mesh.indices[i], mesh.indices[i + 1], mesh.indices[i + 2])); + } + return CheckedCreate(MeshShapeSettings(std::move(vertices), std::move(triangles)), "mesh"); + } + [[nodiscard]] ShapeRefC CreateJoltShape(const PhysicsShapeDesc& desc) { switch (desc.type) @@ -298,6 +344,17 @@ namespace return new CapsuleShape(halfCylinderHeight, desc.radius); } + case PhysicsShapeType::Cylinder: + { + const float halfHeight = std::max(0.0001f, desc.height * 0.5f); + const float convexRadius = std::min({cDefaultConvexRadius, halfHeight, desc.radius}); + return new CylinderShape(halfHeight, desc.radius, convexRadius); + } + + case PhysicsShapeType::ConvexHull: + case PhysicsShapeType::Mesh: + throw std::runtime_error("Mesh based shapes are created through the shape cache."); + case PhysicsShapeType::None: default: throw std::runtime_error("Cannot create Jolt body without a valid shape."); @@ -433,13 +490,14 @@ public: } m_Bodies.clear(); + m_MeshShapeCache.clear(); ShutdownJoltGlobals(); } PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) { - ShapeRefC shape = CreateJoltShape(desc.shape); + ShapeRefC shape = desc.shape.mesh ? GetMeshBasedShape(desc.shape) : CreateJoltShape(desc.shape); // Move the shape itself relative to the body origin, so the body transform stays aligned with // the owning GameObject (an offset center of mass shape would only move the center of mass) @@ -478,7 +536,16 @@ public: } settings.mUserData = reinterpret_cast(desc.owner); - if (desc.type == RigidbodyType::Dynamic) + if (desc.shape.type == PhysicsShapeType::Mesh && desc.type != RigidbodyType::Static) + { + // Mesh shapes have no volume to derive mass from, approximate with their bounds + settings.mOverrideMassProperties = EOverrideMassProperties::MassAndInertiaProvided; + settings.mMassPropertiesOverride.SetMassAndInertiaOfSolidBox( + Vec3::sMax(shape->GetLocalBounds().GetSize(), Vec3::sReplicate(0.01f)), + 1.0f); + settings.mMassPropertiesOverride.ScaleToMass(std::max(0.0001f, desc.mass)); + } + else if (desc.type == RigidbodyType::Dynamic) { settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia; settings.mMassPropertiesOverride.mMass = std::max(0.0001f, desc.mass); @@ -709,6 +776,50 @@ public: } private: + struct CachedMeshShape + { + std::weak_ptr data; + PhysicsShapeType type{PhysicsShapeType::None}; + ShapeRefC shape; + }; + + // Building a hull or mesh BVH is expensive and bodies get recreated on every edit, so the + // unscaled shape is shared by every body using the same mesh data + ShapeRefC GetMeshBasedShape(const PhysicsShapeDesc& desc) + { + const PhysicsMeshData* key = desc.mesh.get(); + ShapeRefC shape; + + auto it = m_MeshShapeCache.find(key); + if (it != m_MeshShapeCache.end() && !it->second.data.expired() && it->second.type == desc.type) + { + shape = it->second.shape; + } + else + { + std::erase_if(m_MeshShapeCache, [](const auto& entry) { return entry.second.data.expired(); }); + + shape = desc.type == PhysicsShapeType::ConvexHull + ? CreateConvexHullShape(*desc.mesh) + : CreateMeshShape(*desc.mesh); + m_MeshShapeCache.insert_or_assign(key, CachedMeshShape{desc.mesh, desc.type, shape}); + } + + if (glm::all(glm::epsilonEqual(desc.scale, glm::vec3{1.0f}, 0.0001f))) + { + return shape; + } + + const Vec3 scale = ToJoltVec3(desc.scale); + if (!shape->IsValidScale(scale)) + { + spdlog::warn("Physics: unsupported scale ({}, {}, {}) for mesh based shape, ignoring it", + desc.scale.x, desc.scale.y, desc.scale.z); + return shape; + } + return new ScaledShape(shape, scale); + } + struct BodyRecord { bool alive{false}; @@ -802,6 +913,8 @@ private: std::unordered_map m_Bodies; std::uint32_t m_NextHandle{0}; + std::unordered_map m_MeshShapeCache; + SensorContactListener m_SensorListener; std::unordered_map m_PreviousSensorOverlaps; std::vector m_TriggerEvents; diff --git a/destrum/src/Physics/PhysicsMeshCache.cpp b/destrum/src/Physics/PhysicsMeshCache.cpp new file mode 100644 index 0000000..6de7a2c --- /dev/null +++ b/destrum/src/Physics/PhysicsMeshCache.cpp @@ -0,0 +1,151 @@ +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +namespace { + struct HullEntry { + std::weak_ptr source; + std::weak_ptr hull; + bool failed{false}; + }; + + std::unordered_map> g_Meshes; + std::map, HullEntry> g_Hulls; + + std::shared_ptr BuildHull(const PhysicsMeshData& source, int maxVertices) { + // The builder allocates through Jolt, which may run before any physics world registered the allocator + if (JPH::Allocate == nullptr) { + JPH::RegisterDefaultAllocator(); + } + + JPH::ConvexHullBuilder::Positions positions; + positions.reserve(source.points.size()); + for (const glm::vec3& point : source.points) { + positions.push_back(JPH::Vec3(point.x, point.y, point.z)); + } + + JPH::ConvexHullBuilder builder(positions); + const char* error = nullptr; + const auto result = builder.Initialize(maxVertices, 1.0e-3f, error); + if (result != JPH::ConvexHullBuilder::EResult::Success && + result != JPH::ConvexHullBuilder::EResult::MaxVerticesReached) { + return nullptr; + } + + // Coplanar points still build a flat hull, which can't be a convex shape + JPH::Vec3 centerOfMass; + float volume = 0.0f; + builder.GetCenterOfMassAndVolume(centerOfMass, volume); + glm::vec3 min{std::numeric_limits::max()}; + glm::vec3 max{std::numeric_limits::lowest()}; + for (const glm::vec3& point : source.points) { + min = glm::min(min, point); + max = glm::max(max, point); + } + const float extent = glm::compMax(max - min); + if (volume <= 1.0e-6f * extent * extent * extent) { + return nullptr; + } + + auto hull = std::make_shared(); + std::unordered_map remap; + const auto hullIndex = [&](int sourceIndex) { + auto [it, inserted] = remap.try_emplace(sourceIndex, static_cast(hull->points.size())); + if (inserted) { + hull->points.push_back(source.points[static_cast(sourceIndex)]); + } + return it->second; + }; + + std::vector polygon; + for (const JPH::ConvexHullBuilder::Face* face : builder.GetFaces()) { + if (face->mRemoved) { + continue; + } + + polygon.clear(); + const JPH::ConvexHullBuilder::Edge* edge = face->mFirstEdge; + do { + polygon.push_back(hullIndex(edge->mStartIdx)); + edge = edge->mNextEdge; + } while (edge != face->mFirstEdge); + + for (std::size_t i = 1; i + 1 < polygon.size(); ++i) { + hull->indices.insert(hull->indices.end(), {polygon[0], polygon[i], polygon[i + 1]}); + } + } + + if (hull->points.size() < 4) { + return nullptr; + } + return hull; + } +} + +std::shared_ptr PhysicsMeshCache::FromCPUMesh(const std::string& key, const CPUMesh& mesh) { + if (auto it = g_Meshes.find(key); it != g_Meshes.end()) { + // Same key with a different size means the mesh was reimported + if (auto cached = it->second.lock(); + cached && cached->points.size() == mesh.vertices.size() && cached->indices.size() == mesh.indices.size()) { + return cached; + } + } + + auto data = std::make_shared(); + data->points.reserve(mesh.vertices.size()); + for (const CPUMesh::Vertex& vertex : mesh.vertices) { + data->points.push_back(vertex.position); + } + + data->indices.reserve(mesh.indices.size() - mesh.indices.size() % 3); + for (std::size_t i = 0; i + 2 < mesh.indices.size(); i += 3) { + const std::uint32_t a = mesh.indices[i]; + const std::uint32_t b = mesh.indices[i + 1]; + const std::uint32_t c = mesh.indices[i + 2]; + if (std::max({a, b, c}) < data->points.size()) { + data->indices.insert(data->indices.end(), {a, b, c}); + } + } + + std::erase_if(g_Meshes, [](const auto& entry) { return entry.second.expired(); }); + g_Meshes.insert_or_assign(key, data); + return data; +} + +std::shared_ptr PhysicsMeshCache::ConvexHull(const std::shared_ptr& source, + int maxVertices) { + if (!source || source->points.size() < 4) { + return nullptr; + } + + const auto key = std::make_pair(source.get(), maxVertices); + if (auto it = g_Hulls.find(key); it != g_Hulls.end() && it->second.source.lock() == source) { + if (it->second.failed) { + return nullptr; + } + if (auto hull = it->second.hull.lock()) { + return hull; + } + } + + auto hull = BuildHull(*source, maxVertices); + + std::erase_if(g_Hulls, [](const auto& entry) { + return entry.second.source.expired() || (!entry.second.failed && entry.second.hull.expired()); + }); + g_Hulls.insert_or_assign(key, HullEntry{source, hull, hull == nullptr}); + return hull; +} diff --git a/destrum/src/Physics/PhysicsWorld.cpp b/destrum/src/Physics/PhysicsWorld.cpp index 5121f9c..36d220e 100644 --- a/destrum/src/Physics/PhysicsWorld.cpp +++ b/destrum/src/Physics/PhysicsWorld.cpp @@ -61,6 +61,10 @@ void PhysicsWorld::RegisterRigidbody(Rigidbody& rigidbody) { desc.transform.position = transform.GetWorldPosition(); desc.transform.rotation = transform.GetWorldRotation(); desc.shape = collider->BuildPhysicsShape(); + if (desc.shape.type == PhysicsShapeType::None) { + // e.g. a mesh collider whose mesh isn't loaded, the next refresh retries + return; + } // Collider dimensions are expressed in world units. Render meshes may // use a transform scale to normalize imported asset units, so applying diff --git a/editor/include/editor/Editor.h b/editor/include/editor/Editor.h index a1a4009..da4ed3c 100644 --- a/editor/include/editor/Editor.h +++ b/editor/include/editor/Editor.h @@ -136,6 +136,7 @@ private: bool m_SaveAsDialog{false}; bool m_ItemWasActive{false}; bool m_EditPending{false}; + bool m_CameraHadInput{false}; }; #endif // EDITOR_EDITOR_H diff --git a/editor/include/editor/UndoStack.h b/editor/include/editor/UndoStack.h index 454977a..0362578 100644 --- a/editor/include/editor/UndoStack.h +++ b/editor/include/editor/UndoStack.h @@ -11,6 +11,17 @@ class Scene; +struct EditGestureState { + bool itemWasActive{false}; + bool itemActive{false}; + bool mouseReleased{false}; + bool keyReleased{false}; + bool keyboardDrivesCamera{false}; + bool editPending{false}; +}; + +[[nodiscard]] bool EndsEditGesture(const EditGestureState& state); + class UndoStack final { public: static constexpr std::size_t MaxSteps = 64; diff --git a/editor/src/Editor.cpp b/editor/src/Editor.cpp index 6dcaa14..36de501 100644 --- a/editor/src/Editor.cpp +++ b/editor/src/Editor.cpp @@ -101,8 +101,9 @@ void Editor::onImGuiBegin(float dt) ApplyPick(); HandleShortcuts(); + m_CameraHadInput = UsesEditorCamera() && (m_Viewport.hovered || m_Viewport.focused || m_Camera.IsLooking()); if (UsesEditorCamera()) { - m_Camera.Update(dt, m_Viewport.hovered || m_Viewport.focused || m_Camera.IsLooking()); + m_Camera.Update(dt, m_CameraHadInput); } if (m_ShaderHotReload.Poll(dt)) { @@ -153,10 +154,16 @@ void Editor::onEndFrame() } const bool itemActive = ImGui::IsAnyItemActive() || ImGuizmo::IsUsing(); - const bool gestureEnded = (m_ItemWasActive && !itemActive) || ImGui::IsMouseReleased(ImGuiMouseButton_Left) || - AnyKeyReleased() || std::exchange(m_EditPending, false); + const bool gestureEnded = EndsEditGesture({ + .itemWasActive = m_ItemWasActive, + .itemActive = itemActive, + .mouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left), + .keyReleased = AnyKeyReleased(), + .keyboardDrivesCamera = m_CameraHadInput, + .editPending = std::exchange(m_EditPending, false), + }); m_ItemWasActive = itemActive; - if (gestureEnded && !itemActive && m_PlayMode.IsEditing()) { + if (gestureEnded && m_PlayMode.IsEditing()) { RecordEdits(); } } diff --git a/editor/src/UndoStack.cpp b/editor/src/UndoStack.cpp index d136fa1..6e95b4d 100644 --- a/editor/src/UndoStack.cpp +++ b/editor/src/UndoStack.cpp @@ -8,6 +8,16 @@ #include #include +bool EndsEditGesture(const EditGestureState& state) +{ + if (state.itemActive) { + return false; + } + // Capturing a snapshot serializes the whole scene, doing that on every WASD release while flying hitches the camera + const bool keyGesture = state.keyReleased && !state.keyboardDrivesCamera; + return state.itemWasActive || state.mouseReleased || keyGesture || state.editPending; +} + std::optional UndoStack::Capture(Scene& scene) { const auto root = SceneSerializer::Capture(scene); diff --git a/lightkeeper/assets_src/scenes/main.scene b/lightkeeper/assets_src/scenes/main.scene index 651e943..e3fdd30 100644 --- a/lightkeeper/assets_src/scenes/main.scene +++ b/lightkeeper/assets_src/scenes/main.scene @@ -99,6 +99,53 @@ } } }, + "12": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "transparent", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0, + "textures": { + "diffuse": { + "guid": "a6c64648ac15e99110de13faea6b80e1", + "path": "game://kobe.png", + "srgb": true + } + } + }, + "13": { + "baseColor": [ + 1.0, + 1.0, + 1.0 + ], + "blend": "opaque", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "Material", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0 + }, "2": { "baseColor": [ 1.0, @@ -127,86 +174,6 @@ } }, "3": { - "baseColor": [ - 0.8999999761581421, - 0.10000000149011612, - 0.10000000149011612 - ], - "blend": "opaque", - "emissiveColor": [ - 0.35999998450279236, - 0.04000000283122063, - 0.04000000283122063 - ], - "filtering": "linear", - "metallic": 0.0, - "name": "ButtonMaterial", - "normalScale": 1.0, - "occlusionStrength": 1.0, - "opacity": 1.0, - "roughness": 0.4000000059604645 - }, - "4": { - "baseColor": [ - 1.0, - 0.949999988079071, - 0.8500000238418579 - ], - "blend": "opaque", - "emissiveColor": [ - 1.0, - 0.8199999928474426, - 0.550000011920929 - ], - "filtering": "linear", - "metallic": 0.0, - "name": "BulbMaterial", - "normalScale": 1.0, - "occlusionStrength": 1.0, - "opacity": 1.0, - "roughness": 1.0 - }, - "5": { - "baseColor": [ - 0.8999999761581421, - 0.8999999761581421, - 0.8999999761581421 - ], - "blend": "opaque", - "emissiveColor": [ - 0.0, - 0.0, - 0.0 - ], - "filtering": "linear", - "metallic": 0.0, - "name": "LightSwitchMaterial", - "normalScale": 1.0, - "occlusionStrength": 1.0, - "opacity": 1.0, - "roughness": 0.5 - }, - "6": { - "baseColor": [ - 0.8999999761581421, - 0.10000000149011612, - 0.10000000149011612 - ], - "blend": "opaque", - "emissiveColor": [ - 0.35999998450279236, - 0.04000000283122063, - 0.04000000283122063 - ], - "filtering": "linear", - "metallic": 0.0, - "name": "WinButtonMaterial", - "normalScale": 1.0, - "occlusionStrength": 1.0, - "opacity": 1.0, - "roughness": 0.4000000059604645 - }, - "7": { "baseColor": [ 1.0, 1.0, @@ -233,6 +200,86 @@ } } }, + "4": { + "baseColor": [ + 0.8999999761581421, + 0.10000000149011612, + 0.10000000149011612 + ], + "blend": "opaque", + "emissiveColor": [ + 0.35999998450279236, + 0.04000000283122063, + 0.04000000283122063 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "ButtonMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 0.4000000059604645 + }, + "5": { + "baseColor": [ + 1.0, + 0.949999988079071, + 0.8500000238418579 + ], + "blend": "opaque", + "emissiveColor": [ + 1.0, + 0.8199999928474426, + 0.550000011920929 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "BulbMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 1.0 + }, + "6": { + "baseColor": [ + 0.8999999761581421, + 0.8999999761581421, + 0.8999999761581421 + ], + "blend": "opaque", + "emissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "LightSwitchMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 0.5 + }, + "7": { + "baseColor": [ + 0.8999999761581421, + 0.10000000149011612, + 0.10000000149011612 + ], + "blend": "opaque", + "emissiveColor": [ + 0.35999998450279236, + 0.04000000283122063, + 0.04000000283122063 + ], + "filtering": "linear", + "metallic": 0.0, + "name": "WinButtonMaterial", + "normalScale": 1.0, + "occlusionStrength": 1.0, + "opacity": 1.0, + "roughness": 0.4000000059604645 + }, "8": { "baseColor": [ 1.0, @@ -311,7 +358,7 @@ "type": "LightComponent" } ], - "id": 880, + "id": 2423, "name": "Sun", "parent": 0, "transform": { @@ -351,7 +398,7 @@ "type": "MeshRendererComponent" } ], - "id": 881, + "id": 2424, "name": "GroundPlane", "parent": 0, "transform": { @@ -407,7 +454,7 @@ "type": "Rigidbody" } ], - "id": 882, + "id": 2425, "name": "GroundCollider", "parent": 0, "transform": { @@ -474,7 +521,7 @@ "type": "Rigidbody" } ], - "id": 883, + "id": 2426, "name": "Parkour_Step", "parent": 0, "transform": { @@ -541,7 +588,7 @@ "type": "Rigidbody" } ], - "id": 884, + "id": 2427, "name": "Parkour_Block", "parent": 0, "transform": { @@ -608,7 +655,7 @@ "type": "Rigidbody" } ], - "id": 885, + "id": 2428, "name": "Parkour_Pillar1", "parent": 0, "transform": { @@ -675,7 +722,7 @@ "type": "Rigidbody" } ], - "id": 886, + "id": 2429, "name": "Parkour_Pillar2", "parent": 0, "transform": { @@ -742,7 +789,7 @@ "type": "Rigidbody" } ], - "id": 887, + "id": 2430, "name": "Parkour_Beam", "parent": 0, "transform": { @@ -809,7 +856,7 @@ "type": "Rigidbody" } ], - "id": 888, + "id": 2431, "name": "Parkour_Goal", "parent": 0, "transform": { @@ -876,7 +923,7 @@ "type": "Rigidbody" } ], - "id": 889, + "id": 2432, "name": "Wall", "parent": 0, "transform": { @@ -943,7 +990,7 @@ "type": "Rigidbody" } ], - "id": 890, + "id": 2433, "name": "LowWall", "parent": 0, "transform": { @@ -1010,7 +1057,7 @@ "type": "Rigidbody" } ], - "id": 891, + "id": 2434, "name": "Crate0", "parent": 0, "transform": { @@ -1077,7 +1124,7 @@ "type": "Rigidbody" } ], - "id": 892, + "id": 2435, "name": "Crate1", "parent": 0, "transform": { @@ -1113,9 +1160,9 @@ "type": "MeshRendererComponent" } ], - "id": 893, + "id": 2436, "name": "Decal", - "parent": 892, + "parent": 2435, "transform": { "position": [ -0.00057220458984375, @@ -1180,7 +1227,7 @@ "type": "Rigidbody" } ], - "id": 894, + "id": 2437, "name": "Crate2", "parent": 0, "transform": { @@ -1247,7 +1294,7 @@ "type": "Rigidbody" } ], - "id": 895, + "id": 2438, "name": "Crate3", "parent": 0, "transform": { @@ -1272,7 +1319,7 @@ { "active": true, "components": [], - "id": 896, + "id": 2439, "name": "FlightHelmet", "parent": 0, "transform": { @@ -1314,9 +1361,9 @@ "type": "MeshRendererComponent" } ], - "id": 897, + "id": 2440, "name": "Lenses_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1356,9 +1403,9 @@ "type": "MeshRendererComponent" } ], - "id": 898, + "id": 2441, "name": "LeatherParts_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1398,9 +1445,9 @@ "type": "MeshRendererComponent" } ], - "id": 899, + "id": 2442, "name": "MetalParts_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1440,9 +1487,9 @@ "type": "MeshRendererComponent" } ], - "id": 900, + "id": 2443, "name": "GlassPlastic_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1482,9 +1529,9 @@ "type": "MeshRendererComponent" } ], - "id": 901, + "id": 2444, "name": "RubberWood_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1524,9 +1571,9 @@ "type": "MeshRendererComponent" } ], - "id": 902, + "id": 2445, "name": "Hose_low", - "parent": 896, + "parent": 2439, "transform": { "position": [ 0.0, @@ -1567,7 +1614,7 @@ "type": "LightComponent" } ], - "id": 903, + "id": 2446, "name": "HelmetSpotlight", "parent": 0, "transform": { @@ -1620,7 +1667,7 @@ "type": "Rigidbody" } ], - "id": 904, + "id": 2447, "name": "Tree", "parent": 0, "transform": { @@ -1662,9 +1709,9 @@ "type": "MeshRendererComponent" } ], - "id": 905, + "id": 2448, "name": "model", - "parent": 904, + "parent": 2447, "transform": { "position": [ 0.0, @@ -1726,7 +1773,7 @@ "type": "TriggerLoggerComponent" } ], - "id": 906, + "id": 2449, "name": "TriggerBox", "parent": 0, "transform": { @@ -1793,7 +1840,7 @@ "type": "Rigidbody" } ], - "id": 907, + "id": 2450, "name": "DoorFrame_Left", "parent": 0, "transform": { @@ -1860,7 +1907,7 @@ "type": "Rigidbody" } ], - "id": 908, + "id": 2451, "name": "DoorFrame_Right", "parent": 0, "transform": { @@ -1927,7 +1974,7 @@ "type": "Rigidbody" } ], - "id": 909, + "id": 2452, "name": "DoorFrame_Top", "parent": 0, "transform": { @@ -1958,7 +2005,7 @@ "guid": "cbc24f8efba0e64df911802de71d03af", "path": "game://materials/prototype_purple.dmat" }, - "mesh": "prim://box?size=0.3,2.4,1.6" + "mesh": "prim://box?size=0.25,2.4,1.6" }, "enabled": true, "type": "MeshRendererComponent" @@ -2009,7 +2056,7 @@ "type": "Door" } ], - "id": 910, + "id": 2453, "name": "Door", "parent": 0, "transform": { @@ -2031,6 +2078,42 @@ ] } }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "3" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 2454, + "name": "Decal", + "parent": 2453, + "transform": { + "position": [ + -0.13358354568481445, + 0.5996032953262329, + 0.01394510269165039 + ], + "rotation": [ + 0.5, + -0.5, + 0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, { "active": true, "components": [ @@ -2076,7 +2159,7 @@ "type": "Rigidbody" } ], - "id": 911, + "id": 2455, "name": "ButtonPedestal", "parent": 0, "transform": { @@ -2104,7 +2187,7 @@ { "data": { "material": { - "localMaterial": "3" + "localMaterial": "4" }, "mesh": "prim://box?size=0.25,0.1,0.25" }, @@ -2147,14 +2230,14 @@ "cycleColors": true, "pressCount": 0, "targets": [ - 910 + 2453 ] }, "enabled": true, "type": "InteractableButton" } ], - "id": 912, + "id": 2456, "name": "Button", "parent": 0, "transform": { @@ -2179,7 +2262,7 @@ { "active": true, "components": [], - "id": 913, + "id": 2457, "name": "House", "parent": 0, "transform": { @@ -2246,9 +2329,9 @@ "type": "Rigidbody" } ], - "id": 914, + "id": 2458, "name": "House_Floor", - "parent": 913, + "parent": 2457, "transform": { "position": [ 0.0, @@ -2313,9 +2396,9 @@ "type": "Rigidbody" } ], - "id": 915, + "id": 2459, "name": "House_FrontWall_Left", - "parent": 913, + "parent": 2457, "transform": { "position": [ -3.125, @@ -2380,9 +2463,9 @@ "type": "Rigidbody" } ], - "id": 916, + "id": 2460, "name": "House_FrontWall_Right", - "parent": 913, + "parent": 2457, "transform": { "position": [ 1.625, @@ -2447,9 +2530,9 @@ "type": "Rigidbody" } ], - "id": 917, + "id": 2461, "name": "House_FrontWall_Above", - "parent": 913, + "parent": 2457, "transform": { "position": [ -1.5, @@ -2514,9 +2597,9 @@ "type": "Rigidbody" } ], - "id": 918, + "id": 2462, "name": "House_BackWall_Left", - "parent": 913, + "parent": 2457, "transform": { "position": [ -1.6750000715255737, @@ -2581,9 +2664,9 @@ "type": "Rigidbody" } ], - "id": 919, + "id": 2463, "name": "House_BackWall_Right", - "parent": 913, + "parent": 2457, "transform": { "position": [ 3.1750001907348633, @@ -2648,9 +2731,9 @@ "type": "Rigidbody" } ], - "id": 920, + "id": 2464, "name": "House_BackWall_Below", - "parent": 913, + "parent": 2457, "transform": { "position": [ 1.5, @@ -2715,9 +2798,9 @@ "type": "Rigidbody" } ], - "id": 921, + "id": 2465, "name": "House_BackWall_Above", - "parent": 913, + "parent": 2457, "transform": { "position": [ 1.5, @@ -2782,9 +2865,9 @@ "type": "Rigidbody" } ], - "id": 922, + "id": 2466, "name": "House_LeftWall", - "parent": 913, + "parent": 2457, "transform": { "position": [ -4.0, @@ -2849,9 +2932,9 @@ "type": "Rigidbody" } ], - "id": 923, + "id": 2467, "name": "House_RightWall_Left", - "parent": 913, + "parent": 2457, "transform": { "position": [ 4.0, @@ -2916,9 +2999,9 @@ "type": "Rigidbody" } ], - "id": 924, + "id": 2468, "name": "House_RightWall_Right", - "parent": 913, + "parent": 2457, "transform": { "position": [ 4.0, @@ -2983,9 +3066,9 @@ "type": "Rigidbody" } ], - "id": 925, + "id": 2469, "name": "House_RightWall_Below", - "parent": 913, + "parent": 2457, "transform": { "position": [ 4.0, @@ -3050,9 +3133,9 @@ "type": "Rigidbody" } ], - "id": 926, + "id": 2470, "name": "House_RightWall_Above", - "parent": 913, + "parent": 2457, "transform": { "position": [ 4.0, @@ -3117,9 +3200,9 @@ "type": "Rigidbody" } ], - "id": 927, + "id": 2471, "name": "House_Roof", - "parent": 913, + "parent": 2457, "transform": { "position": [ 0.0, @@ -3145,7 +3228,7 @@ { "data": { "material": { - "localMaterial": "4" + "localMaterial": "5" }, "mesh": "prim://box?size=0.35,0.12,0.35" }, @@ -3177,9 +3260,9 @@ "type": "LightToggle" } ], - "id": 928, + "id": 2472, "name": "CeilingLight", - "parent": 913, + "parent": 2457, "transform": { "position": [ 0.0, @@ -3205,7 +3288,7 @@ { "data": { "material": { - "localMaterial": "5" + "localMaterial": "6" }, "mesh": "prim://box?size=0.12,0.2,0.06" }, @@ -3248,16 +3331,16 @@ "cycleColors": false, "pressCount": 0, "targets": [ - 928 + 2472 ] }, "enabled": true, "type": "InteractableButton" } ], - "id": 929, + "id": 2473, "name": "LightSwitch", - "parent": 913, + "parent": 2457, "transform": { "position": [ -0.4999999701976776, @@ -3280,9 +3363,9 @@ { "active": true, "components": [], - "id": 930, + "id": 2474, "name": "Sofa", - "parent": 913, + "parent": 2457, "transform": { "position": [ -3.700000047683716, @@ -3322,9 +3405,9 @@ "type": "MeshRendererComponent" } ], - "id": 931, + "id": 2475, "name": "loungeSofa_1", - "parent": 930, + "parent": 2474, "transform": { "position": [ 0.0, @@ -3364,9 +3447,9 @@ "type": "MeshRendererComponent" } ], - "id": 932, + "id": 2476, "name": "loungeSofa_1", - "parent": 930, + "parent": 2474, "transform": { "position": [ 0.0, @@ -3406,9 +3489,9 @@ "type": "MeshRendererComponent" } ], - "id": 933, + "id": 2477, "name": "loungeSofa", - "parent": 930, + "parent": 2474, "transform": { "position": [ 0.0, @@ -3448,9 +3531,9 @@ "type": "MeshRendererComponent" } ], - "id": 934, + "id": 2478, "name": "loungeSofa", - "parent": 930, + "parent": 2474, "transform": { "position": [ 0.0, @@ -3515,7 +3598,7 @@ "type": "Rigidbody" } ], - "id": 935, + "id": 2479, "name": "PlatformRamp", "parent": 0, "transform": { @@ -3596,7 +3679,7 @@ "type": "MovingPlatform" } ], - "id": 936, + "id": 2480, "name": "MovingPlatform", "parent": 0, "transform": { @@ -3663,7 +3746,7 @@ "type": "Rigidbody" } ], - "id": 937, + "id": 2481, "name": "PlatformLanding", "parent": 0, "transform": { @@ -3688,7 +3771,7 @@ { "active": true, "components": [], - "id": 938, + "id": 2482, "name": "WinCourse", "parent": 0, "transform": { @@ -3755,9 +3838,9 @@ "type": "Rigidbody" } ], - "id": 939, + "id": 2483, "name": "WinCourse_Start", - "parent": 938, + "parent": 2482, "transform": { "position": [ -2.5, @@ -3822,9 +3905,9 @@ "type": "Checkpoint" } ], - "id": 940, + "id": 2484, "name": "WinCourse_Checkpoint1", - "parent": 938, + "parent": 2482, "transform": { "position": [ -2.5, @@ -3889,9 +3972,9 @@ "type": "Rigidbody" } ], - "id": 941, + "id": 2485, "name": "WinCourse_Platform1", - "parent": 938, + "parent": 2482, "transform": { "position": [ -8.0, @@ -3956,9 +4039,9 @@ "type": "Rigidbody" } ], - "id": 942, + "id": 2486, "name": "WinCourse_Platform2", - "parent": 938, + "parent": 2482, "transform": { "position": [ -12.150001525878906, @@ -4023,9 +4106,9 @@ "type": "Rigidbody" } ], - "id": 943, + "id": 2487, "name": "WinCourse_Platform3", - "parent": 938, + "parent": 2482, "transform": { "position": [ -16.25, @@ -4090,9 +4173,9 @@ "type": "Rigidbody" } ], - "id": 944, + "id": 2488, "name": "WinCourse_Platform4", - "parent": 938, + "parent": 2482, "transform": { "position": [ -20.39999771118164, @@ -4157,9 +4240,9 @@ "type": "Rigidbody" } ], - "id": 945, + "id": 2489, "name": "WinCourse_Platform5", - "parent": 938, + "parent": 2482, "transform": { "position": [ -24.699996948242188, @@ -4224,9 +4307,9 @@ "type": "Checkpoint" } ], - "id": 946, + "id": 2490, "name": "WinCourse_Checkpoint2", - "parent": 938, + "parent": 2482, "transform": { "position": [ -24.699996948242188, @@ -4291,9 +4374,9 @@ "type": "Rigidbody" } ], - "id": 947, + "id": 2491, "name": "WinCourse_Platform6", - "parent": 938, + "parent": 2482, "transform": { "position": [ -29.29999542236328, @@ -4358,9 +4441,9 @@ "type": "Rigidbody" } ], - "id": 948, + "id": 2492, "name": "WinCourse_Platform7", - "parent": 938, + "parent": 2482, "transform": { "position": [ -32.999996185302734, @@ -4425,9 +4508,9 @@ "type": "Rigidbody" } ], - "id": 949, + "id": 2493, "name": "WinCourse_Platform8", - "parent": 938, + "parent": 2482, "transform": { "position": [ -36.54999542236328, @@ -4492,9 +4575,9 @@ "type": "Rigidbody" } ], - "id": 950, + "id": 2494, "name": "WinCourse_Platform9", - "parent": 938, + "parent": 2482, "transform": { "position": [ -40.24999237060547, @@ -4559,9 +4642,9 @@ "type": "Rigidbody" } ], - "id": 951, + "id": 2495, "name": "WinCourse_Platform10", - "parent": 938, + "parent": 2482, "transform": { "position": [ -44.149986267089844, @@ -4626,9 +4709,9 @@ "type": "Rigidbody" } ], - "id": 952, + "id": 2496, "name": "WinCourse_Platform11", - "parent": 938, + "parent": 2482, "transform": { "position": [ -48.49998474121094, @@ -4693,9 +4776,9 @@ "type": "Checkpoint" } ], - "id": 953, + "id": 2497, "name": "WinCourse_Checkpoint3", - "parent": 938, + "parent": 2482, "transform": { "position": [ -48.49998474121094, @@ -4760,9 +4843,9 @@ "type": "Rigidbody" } ], - "id": 954, + "id": 2498, "name": "WinCourse_Platform12", - "parent": 938, + "parent": 2482, "transform": { "position": [ -54.199981689453125, @@ -4827,9 +4910,9 @@ "type": "Rigidbody" } ], - "id": 955, + "id": 2499, "name": "WinCourse_Platform13", - "parent": 938, + "parent": 2482, "transform": { "position": [ -61.09998321533203, @@ -4894,9 +4977,9 @@ "type": "Rigidbody" } ], - "id": 956, + "id": 2500, "name": "WinCourse_Platform14", - "parent": 938, + "parent": 2482, "transform": { "position": [ -66.99998474121094, @@ -4961,9 +5044,9 @@ "type": "Rigidbody" } ], - "id": 957, + "id": 2501, "name": "WinCourse_Platform15", - "parent": 938, + "parent": 2482, "transform": { "position": [ -73.34998321533203, @@ -5028,9 +5111,9 @@ "type": "Rigidbody" } ], - "id": 958, + "id": 2502, "name": "WinCourse_Platform16", - "parent": 938, + "parent": 2482, "transform": { "position": [ -78.84998321533203, @@ -5095,9 +5178,9 @@ "type": "Rigidbody" } ], - "id": 959, + "id": 2503, "name": "WinCourse_Platform17", - "parent": 938, + "parent": 2482, "transform": { "position": [ -83.99998474121094, @@ -5162,9 +5245,9 @@ "type": "Rigidbody" } ], - "id": 960, + "id": 2504, "name": "WinCourse_Platform18", - "parent": 938, + "parent": 2482, "transform": { "position": [ -89.09998321533203, @@ -5229,9 +5312,9 @@ "type": "Checkpoint" } ], - "id": 961, + "id": 2505, "name": "WinCourse_Checkpoint4", - "parent": 938, + "parent": 2482, "transform": { "position": [ -89.09998321533203, @@ -5310,9 +5393,9 @@ "type": "MovingPlatform" } ], - "id": 962, + "id": 2506, "name": "WinCourse_Platform19", - "parent": 938, + "parent": 2482, "transform": { "position": [ -92.44998168945313, @@ -5377,9 +5460,9 @@ "type": "Rigidbody" } ], - "id": 963, + "id": 2507, "name": "WinCourse_Platform20", - "parent": 938, + "parent": 2482, "transform": { "position": [ -104.29998779296875, @@ -5458,9 +5541,9 @@ "type": "MovingPlatform" } ], - "id": 964, + "id": 2508, "name": "WinCourse_Platform21", - "parent": 938, + "parent": 2482, "transform": { "position": [ -108.24998474121094, @@ -5539,9 +5622,9 @@ "type": "MovingPlatform" } ], - "id": 965, + "id": 2509, "name": "WinCourse_Platform22", - "parent": 938, + "parent": 2482, "transform": { "position": [ -111.94998168945313, @@ -5620,9 +5703,9 @@ "type": "MovingPlatform" } ], - "id": 966, + "id": 2510, "name": "WinCourse_Platform23", - "parent": 938, + "parent": 2482, "transform": { "position": [ -115.64997863769531, @@ -5687,9 +5770,9 @@ "type": "Rigidbody" } ], - "id": 967, + "id": 2511, "name": "WinCourse_Platform24", - "parent": 938, + "parent": 2482, "transform": { "position": [ -120.0999755859375, @@ -5754,9 +5837,9 @@ "type": "Checkpoint" } ], - "id": 968, + "id": 2512, "name": "WinCourse_Checkpoint5", - "parent": 938, + "parent": 2482, "transform": { "position": [ -120.0999755859375, @@ -5835,9 +5918,9 @@ "type": "MovingPlatform" } ], - "id": 969, + "id": 2513, "name": "WinCourse_Platform25", - "parent": 938, + "parent": 2482, "transform": { "position": [ -123.19998168945313, @@ -5902,9 +5985,9 @@ "type": "Rigidbody" } ], - "id": 970, + "id": 2514, "name": "WinCourse_Platform26", - "parent": 938, + "parent": 2482, "transform": { "position": [ -126.14997863769531, @@ -5969,9 +6052,9 @@ "type": "Rigidbody" } ], - "id": 971, + "id": 2515, "name": "WinCourse_Platform27", - "parent": 938, + "parent": 2482, "transform": { "position": [ -130.0999755859375, @@ -6036,9 +6119,9 @@ "type": "Rigidbody" } ], - "id": 972, + "id": 2516, "name": "WinCourse_Platform28", - "parent": 938, + "parent": 2482, "transform": { "position": [ -133.8499755859375, @@ -6103,9 +6186,9 @@ "type": "Rigidbody" } ], - "id": 973, + "id": 2517, "name": "WinCourse_Platform29", - "parent": 938, + "parent": 2482, "transform": { "position": [ -137.69998168945313, @@ -6170,9 +6253,9 @@ "type": "Rigidbody" } ], - "id": 974, + "id": 2518, "name": "WinCourse_Platform30", - "parent": 938, + "parent": 2482, "transform": { "position": [ -141.69998168945313, @@ -6237,9 +6320,9 @@ "type": "Rigidbody" } ], - "id": 975, + "id": 2519, "name": "WinCourse_Platform31", - "parent": 938, + "parent": 2482, "transform": { "position": [ -145.99998474121094, @@ -6304,9 +6387,9 @@ "type": "Checkpoint" } ], - "id": 976, + "id": 2520, "name": "WinCourse_Checkpoint6", - "parent": 938, + "parent": 2482, "transform": { "position": [ -145.99998474121094, @@ -6385,9 +6468,9 @@ "type": "MovingPlatform" } ], - "id": 977, + "id": 2521, "name": "WinCourse_Platform32", - "parent": 938, + "parent": 2482, "transform": { "position": [ -150.49998474121094, @@ -6452,9 +6535,9 @@ "type": "Rigidbody" } ], - "id": 978, + "id": 2522, "name": "WinCourse_Platform33", - "parent": 938, + "parent": 2482, "transform": { "position": [ -154.1999969482422, @@ -6519,9 +6602,9 @@ "type": "Rigidbody" } ], - "id": 979, + "id": 2523, "name": "WinCourse_Platform34", - "parent": 938, + "parent": 2482, "transform": { "position": [ -158.49998474121094, @@ -6586,9 +6669,9 @@ "type": "Rigidbody" } ], - "id": 980, + "id": 2524, "name": "WinCourse_Platform35", - "parent": 938, + "parent": 2482, "transform": { "position": [ -162.8999786376953, @@ -6660,9 +6743,9 @@ "type": "WinGoal" } ], - "id": 981, + "id": 2525, "name": "WinCourse_Goal", - "parent": 938, + "parent": 2482, "transform": { "position": [ -167.7999725341797, @@ -6727,9 +6810,9 @@ "type": "Rigidbody" } ], - "id": 982, + "id": 2526, "name": "WinCourse_ButtonPedestal", - "parent": 938, + "parent": 2482, "transform": { "position": [ -168.7999725341797, @@ -6755,7 +6838,7 @@ { "data": { "material": { - "localMaterial": "6" + "localMaterial": "7" }, "mesh": "prim://box?size=0.25,0.1,0.25" }, @@ -6798,16 +6881,16 @@ "cycleColors": true, "pressCount": 0, "targets": [ - 981 + 2525 ] }, "enabled": true, "type": "InteractableButton" } ], - "id": 983, + "id": 2527, "name": "WinCourse_Button", - "parent": 938, + "parent": 2482, "transform": { "position": [ -168.7999725341797, @@ -6848,9 +6931,9 @@ "type": "LightComponent" } ], - "id": 984, + "id": 2528, "name": "WinCourse_GoalLight", - "parent": 938, + "parent": 2482, "transform": { "position": [ -167.7999725341797, @@ -6935,7 +7018,7 @@ "type": "PlayerRespawn" } ], - "id": 985, + "id": 2529, "name": "Player", "parent": 0, "transform": { @@ -6978,8 +7061,8 @@ 1.0, 0.800000011920929 ], - "crosshair": 987, - "focusIndicator": 990, + "crosshair": 2531, + "focusIndicator": 2534, "focusedColor": [ 1.0, 0.800000011920929, @@ -6987,7 +7070,7 @@ 1.0 ], "focusedSize": 16.0, - "player": 985, + "player": 2529, "size": 10.0 }, "enabled": true, @@ -6995,14 +7078,14 @@ }, { "data": { - "goal": 981, - "screen": 993 + "goal": 2525, + "screen": 2537 }, "enabled": true, "type": "WinScreenUI" } ], - "id": 986, + "id": 2530, "name": "HUD", "parent": 0, "transform": { @@ -7054,9 +7137,9 @@ "type": "RectTransform" } ], - "id": 987, + "id": 2531, "name": "Crosshair", - "parent": 986, + "parent": 2530, "transform": { "position": [ 0.0, @@ -7123,9 +7206,9 @@ "type": "UIImage" } ], - "id": 988, + "id": 2532, "name": "Crosshair_Horizontal", - "parent": 987, + "parent": 2531, "transform": { "position": [ 0.0, @@ -7192,9 +7275,9 @@ "type": "UIImage" } ], - "id": 989, + "id": 2533, "name": "Crosshair_Vertical", - "parent": 987, + "parent": 2531, "transform": { "position": [ 0.0, @@ -7244,9 +7327,9 @@ "type": "RectTransform" } ], - "id": 990, + "id": 2534, "name": "FocusIndicator", - "parent": 987, + "parent": 2531, "transform": { "position": [ 0.0, @@ -7313,9 +7396,9 @@ "type": "UIImage" } ], - "id": 991, + "id": 2535, "name": "FocusRing", - "parent": 990, + "parent": 2534, "transform": { "position": [ 0.0, @@ -7389,9 +7472,9 @@ "type": "UIText" } ], - "id": 992, + "id": 2536, "name": "InteractPrompt", - "parent": 990, + "parent": 2534, "transform": { "position": [ 0.0, @@ -7458,9 +7541,9 @@ "type": "UIImage" } ], - "id": 993, + "id": 2537, "name": "WinScreen", - "parent": 986, + "parent": 2530, "transform": { "position": [ 0.0, @@ -7534,9 +7617,9 @@ "type": "UIText" } ], - "id": 994, + "id": 2538, "name": "WinScreen_Title", - "parent": 993, + "parent": 2537, "transform": { "position": [ 0.0, @@ -7575,7 +7658,7 @@ "type": "MeshRendererComponent" } ], - "id": 995, + "id": 2539, "name": "GameObject", "parent": 0, "transform": { @@ -7642,7 +7725,7 @@ "type": "Rigidbody" } ], - "id": 996, + "id": 2540, "name": "Parkour_Goal", "parent": 0, "transform": { @@ -7667,7 +7750,7 @@ { "active": true, "components": [], - "id": 997, + "id": 2541, "name": "capybara", "parent": 0, "transform": { @@ -7709,9 +7792,9 @@ "type": "MeshRendererComponent" } ], - "id": 998, + "id": 2542, "name": "Capybara", - "parent": 997, + "parent": 2541, "transform": { "position": [ 0.0, @@ -7751,9 +7834,9 @@ "type": "MeshRendererComponent" } ], - "id": 999, + "id": 2543, "name": "Capybara", - "parent": 997, + "parent": 2541, "transform": { "position": [ 0.0, @@ -7793,7 +7876,7 @@ "type": "AudioSource" } ], - "id": 1000, + "id": 2544, "name": "Radio", "parent": 0, "transform": { @@ -7815,42 +7898,6 @@ ] } }, - { - "active": true, - "components": [ - { - "data": { - "material": { - "localMaterial": "7" - }, - "mesh": "prim://plane?size=1,1" - }, - "enabled": true, - "type": "MeshRendererComponent" - } - ], - "id": 1001, - "name": "Decal", - "parent": 0, - "transform": { - "position": [ - -4.743542194366455, - 0.3649294674396515, - 0.07782143354415894 - ], - "rotation": [ - 0.5, - 0.5, - -0.5, - 0.5 - ], - "scale": [ - 1.0, - 0.9999995231628418, - 0.9999995827674866 - ] - } - }, { "active": true, "components": [ @@ -7865,14 +7912,14 @@ "type": "MeshRendererComponent" } ], - "id": 1002, + "id": 2545, "name": "Decal", "parent": 0, "transform": { "position": [ -4.743542194366455, - 1.1351869106292725, - 2.188525676727295 + 0.3649294674396515, + 0.07782143354415894 ], "rotation": [ 0.5, @@ -7901,14 +7948,14 @@ "type": "MeshRendererComponent" } ], - "id": 1003, + "id": 2546, "name": "Decal", "parent": 0, "transform": { "position": [ -4.743542194366455, - 1.5165855884552002, - 0.601291835308075 + 1.1351869106292725, + 2.188525676727295 ], "rotation": [ 0.5, @@ -7937,14 +7984,14 @@ "type": "MeshRendererComponent" } ], - "id": 1004, + "id": 2547, "name": "Decal", "parent": 0, "transform": { "position": [ -4.743542194366455, - 0.5876348614692688, - -1.545495867729187 + 1.5165855884552002, + 0.601291835308075 ], "rotation": [ 0.5, @@ -7973,7 +8020,43 @@ "type": "MeshRendererComponent" } ], - "id": 1005, + "id": 2548, + "name": "Decal", + "parent": 0, + "transform": { + "position": [ + -4.743542194366455, + 0.5876348614692688, + -1.545495867729187 + ], + "rotation": [ + 0.5, + 0.5, + -0.5, + 0.5 + ], + "scale": [ + 1.0, + 0.9999995231628418, + 0.9999995827674866 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "12" + }, + "mesh": "prim://plane?size=1,1" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 2549, "name": "Decal", "parent": 0, "transform": { @@ -7994,6 +8077,128 @@ 0.9999995827674866 ] } + }, + { + "active": true, + "components": [ + { + "data": { + "material": { + "localMaterial": "13" + }, + "mesh": "prim://box?size=0.4,3.8,3" + }, + "enabled": true, + "type": "MeshRendererComponent" + } + ], + "id": 2550, + "name": "DoorContainer", + "parent": 0, + "transform": { + "position": [ + 3.980215072631836, + 0.4207366108894348, + -7.099858283996582 + ], + "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, + 1.0, + 1.0 + ], + "innerConeAngle": 0.3499999940395355, + "intensity": 5.0, + "outerConeAngle": 0.5, + "range": 10.0, + "type": 2 + }, + "enabled": true, + "type": "LightComponent" + } + ], + "id": 2551, + "name": "Spot", + "parent": 0, + "transform": { + "position": [ + 3.151332139968872, + 1.707247018814087, + -2.4504735469818115 + ], + "rotation": [ + -0.19591297209262848, + -0.2843289077281952, + -0.39191678166389465, + 0.8527464270591736 + ], + "scale": [ + 0.9999979138374329, + 0.9999996423721313, + 0.9999963045120239 + ] + } + }, + { + "active": true, + "components": [ + { + "data": { + "castShadows": false, + "color": [ + 1.0, + 1.0, + 1.0 + ], + "innerConeAngle": 0.3499999940395355, + "intensity": 5.0, + "outerConeAngle": 0.5, + "range": 10.0, + "type": 2 + }, + "enabled": true, + "type": "LightComponent" + } + ], + "id": 2552, + "name": "Spot", + "parent": 0, + "transform": { + "position": [ + 2.525489091873169, + 1.7072468996047974, + -5.727573394775391 + ], + "rotation": [ + -0.16937240958213806, + 0.831908106803894, + 0.40409985184669495, + -0.34050723910331726 + ], + "scale": [ + 1.0000030994415283, + 0.9999993443489075, + 0.999999463558197 + ] + } } ], "version": 3 diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp index 760082f..c45ca97 100644 --- a/tests/core_tests.cpp +++ b/tests/core_tests.cpp @@ -178,6 +178,9 @@ namespace { "BoxCollider", "SphereCollider", "CapsuleCollider", + "CylinderCollider", + "ConvexHullCollider", + "MeshCollider", "Canvas", "RectTransform", "UIImage", diff --git a/tests/editor_tests.cpp b/tests/editor_tests.cpp index b836b9e..db489c0 100644 --- a/tests/editor_tests.cpp +++ b/tests/editor_tests.cpp @@ -59,10 +59,24 @@ namespace { SceneManager::GetInstance().Destroy(); } + + void testCameraKeysDoNotEndEditGestures() + { + check(!EndsEditGesture({.keyReleased = true, .keyboardDrivesCamera = true}), + "releasing a key while flying the editor camera must not capture an undo snapshot"); + check(EndsEditGesture({.keyReleased = true}), "releasing a key outside the viewport still ends an edit"); + check(EndsEditGesture({.keyboardDrivesCamera = true, .editPending = true}), + "viewport shortcuts that edit (delete, duplicate) still end an edit"); + check(EndsEditGesture({.mouseReleased = true, .keyboardDrivesCamera = true}), + "releasing the mouse after a viewport drag still ends an edit"); + check(!EndsEditGesture({.itemWasActive = true, .itemActive = true, .mouseReleased = true}), + "nothing ends while an item is still being edited"); + } } void RunEditorTests() { testUndoRedoRestoresEdits(); testUndoHistoryIsCapped(); + testCameraKeysDoNotEndEditGestures(); } diff --git a/tests/physics_tests.cpp b/tests/physics_tests.cpp index 5062a8e..7f0f73b 100644 --- a/tests/physics_tests.cpp +++ b/tests/physics_tests.cpp @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #include #include @@ -357,6 +360,124 @@ namespace { SceneManager::GetInstance().Destroy(); } + + std::shared_ptr makeCubeMesh(float halfExtent) + { + auto mesh = std::make_shared(); + for (int corner = 0; corner < 8; ++corner) { + mesh->points.push_back({ + corner & 1 ? halfExtent : -halfExtent, + corner & 2 ? halfExtent : -halfExtent, + corner & 4 ? halfExtent : -halfExtent}); + } + mesh->points.push_back(glm::vec3{0.0f}); + mesh->indices = { + 0, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, + 0, 1, 4, 1, 5, 4, 2, 6, 3, 3, 6, 7, + 0, 4, 2, 2, 4, 6, 1, 3, 5, 3, 7, 5}; + return mesh; + } + + std::shared_ptr makeQuadMesh(float halfExtent) + { + auto mesh = std::make_shared(); + mesh->points = { + {-halfExtent, 0.0f, -halfExtent}, + {halfExtent, 0.0f, -halfExtent}, + {-halfExtent, 0.0f, halfExtent}, + {halfExtent, 0.0f, halfExtent}}; + mesh->indices = {0, 2, 1, 1, 2, 3}; + return mesh; + } + + void testCylinderCollider() + { + JoltPhysicsWorld world{{.workerThreadCount = 1, .gravity = glm::vec3{0.0f}}}; + GameObject object{"cylinder"}; + object.AddComponent(1.0f, 4.0f); + Rigidbody* rigidbody = object.AddComponent(); + rigidbody->SetType(RigidbodyType::Static); + world.RegisterRigidbody(*rigidbody); + + PhysicsRaycastHit hit; + check(world.Raycast({-3.0f, 1.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f, hit) && close(hit.distance, 2.0f), + "cylinder sides must be at its radius along the whole height"); + check(world.Raycast({0.0f, 5.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, 5.0f, hit) && close(hit.distance, 3.0f), + "cylinder caps must be flat at half its height"); + } + + void testConvexHullCollider() + { + JoltPhysicsWorld world{{.workerThreadCount = 1, .gravity = glm::vec3{0.0f}}}; + GameObject object{"hull"}; + object.GetTransform().SetLocalScale(glm::vec3{2.0f}); + auto* collider = object.AddComponent(); + collider->SetCustomMesh(makeCubeMesh(1.0f)); + check(collider->BuildPhysicsShape().type == PhysicsShapeType::ConvexHull, + "a cube mesh must build a convex hull"); + check(collider->BuildPhysicsShape().mesh->points.size() == 8, + "interior points must not end up in the hull"); + + Rigidbody* rigidbody = object.AddComponent(); + rigidbody->SetType(RigidbodyType::Static); + world.RegisterRigidbody(*rigidbody); + + PhysicsRaycastHit hit; + check(world.Raycast({-5.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 10.0f, hit) && close(hit.distance, 3.0f), + "mesh based colliders must apply the transform scale"); + + GameObject flat{"flat-hull"}; + auto* flatCollider = flat.AddComponent(); + flatCollider->SetCustomMesh(makeQuadMesh(1.0f)); + check(flatCollider->BuildPhysicsShape().type == PhysicsShapeType::Box, + "a mesh without volume must fall back to a box"); + + GameObject empty{"empty-hull"}; + empty.AddComponent(); + Rigidbody* emptyBody = empty.AddComponent(); + world.RegisterRigidbody(*emptyBody); + check(!emptyBody->HasPhysicsBody(), "a mesh collider without a mesh must not create a body"); + } + + void testMeshCollider() + { + JoltPhysicsWorld world{{.workerThreadCount = 1, .gravity = {0.0f, -9.81f, 0.0f}}}; + + GameObject ground{"mesh-ground"}; + ground.AddComponent()->SetCustomMesh(makeQuadMesh(10.0f)); + Rigidbody* groundBody = ground.AddComponent(); + groundBody->SetType(RigidbodyType::Static); + world.RegisterRigidbody(*groundBody); + check(groundBody->HasPhysicsBody(), "a static mesh collider must create a body"); + + GameObject ball{"ball"}; + ball.GetTransform().SetLocalPosition({0.0f, 2.0f, 0.0f}); + ball.AddComponent(0.5f); + Rigidbody* ballBody = ball.AddComponent(); + world.RegisterRigidbody(*ballBody); + + for (int step = 0; step < 180; ++step) { + world.Step(1.0f / 60.0f); + world.SyncDynamicBodiesToTransforms(); + } + const float restHeight = ball.GetTransform().GetWorldPosition().y; + check(restHeight > 0.4f && restHeight < 0.6f, "a sphere must come to rest on a mesh collider"); + + GameObject kinematic{"kinematic-mesh"}; + kinematic.GetTransform().SetLocalPosition({50.0f, 0.0f, 0.0f}); + kinematic.AddComponent()->SetCustomMesh(makeCubeMesh(1.0f)); + Rigidbody* kinematicBody = kinematic.AddComponent(); + kinematicBody->SetType(RigidbodyType::Kinematic); + world.RegisterRigidbody(*kinematicBody); + check(kinematicBody->HasPhysicsBody(), "a kinematic mesh collider must create a body"); + + GameObject dynamic{"dynamic-mesh"}; + auto* dynamicCollider = dynamic.AddComponent(); + dynamicCollider->SetCustomMesh(makeCubeMesh(1.0f)); + dynamic.AddComponent(); + check(dynamicCollider->BuildPhysicsShape().type == PhysicsShapeType::ConvexHull, + "dynamic mesh colliders must fall back to a convex hull"); + } } void RunPhysicsTests() @@ -368,5 +489,8 @@ void RunPhysicsTests() testKinematicBodiesMoveWithVelocity(); testColliderCenterOffset(); testFrozenRotationCapsule(); + testCylinderCollider(); + testConvexHullCollider(); + testMeshCollider(); testTriggerZone(); }