feat: add more collider types + camera snapping fix
This commit is contained in:
@@ -54,6 +54,10 @@ set(SRC_FILES
|
|||||||
"src/Components/Physics/BoxCollider.cpp"
|
"src/Components/Physics/BoxCollider.cpp"
|
||||||
"src/Components/Physics/CapsuleCollider.cpp"
|
"src/Components/Physics/CapsuleCollider.cpp"
|
||||||
"src/Components/Physics/Collider.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/Physics/SphereCollider.cpp"
|
||||||
|
|
||||||
"src/Components/UI/Canvas.cpp"
|
"src/Components/UI/Canvas.cpp"
|
||||||
@@ -137,6 +141,7 @@ set(SRC_FILES
|
|||||||
"src/Threading/JobSystem.cpp"
|
"src/Threading/JobSystem.cpp"
|
||||||
|
|
||||||
"src/Physics/PhysicsWorld.cpp"
|
"src/Physics/PhysicsWorld.cpp"
|
||||||
|
"src/Physics/PhysicsMeshCache.cpp"
|
||||||
"src/Physics/PhysicsSceneBridge.cpp"
|
"src/Physics/PhysicsSceneBridge.cpp"
|
||||||
"src/Physics/JoltPhysicsWorld.cpp"
|
"src/Physics/JoltPhysicsWorld.cpp"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
|
||||||
|
class ConvexHullCollider final : public MeshShapeCollider {
|
||||||
|
public:
|
||||||
|
static constexpr int MinVertices = 4;
|
||||||
|
static constexpr int MaxVertices = 256;
|
||||||
|
|
||||||
|
explicit ConvexHullCollider(GameObject& owner, int maxVertices = 64)
|
||||||
|
: MeshShapeCollider(owner)
|
||||||
|
, m_MaxVertices(std::clamp(maxVertices, MinVertices, MaxVertices)) {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "ConvexHullCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
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};
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
|
class CylinderCollider final : public Collider {
|
||||||
|
public:
|
||||||
|
explicit CylinderCollider(GameObject& owner, float radius = 0.5f, float height = 1.0f)
|
||||||
|
: Collider(owner)
|
||||||
|
, m_Radius(std::max(radius, 0.0001f))
|
||||||
|
, m_Height(std::max(height, 0.0001f)) {}
|
||||||
|
|
||||||
|
void Update(float) override {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "CylinderCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
nlohmann::json Serialize() const override {
|
||||||
|
auto data = SerializeCollider();
|
||||||
|
data["radius"] = m_Radius;
|
||||||
|
data["height"] = m_Height;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Deserialize(const nlohmann::json& data) override {
|
||||||
|
DeserializeCollider(data);
|
||||||
|
if (data.contains("radius")) {
|
||||||
|
SetRadius(data.at("radius").get<float>());
|
||||||
|
}
|
||||||
|
if (data.contains("height")) {
|
||||||
|
SetHeight(data.at("height").get<float>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawDebugLines(const glm::vec4& color) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override {
|
||||||
|
return PhysicsShapeDesc::Cylinder(m_Radius, m_Height, m_CenterOffset, m_IsTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] float GetRadius() const {
|
||||||
|
return m_Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRadius(float radius) {
|
||||||
|
m_Radius = std::max(radius, 0.0001f);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] float GetHeight() const {
|
||||||
|
return m_Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetHeight(float height) {
|
||||||
|
m_Height = std::max(height, 0.0001f);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_Radius{0.5f};
|
||||||
|
float m_Height{1.0f};
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
|
||||||
|
// Exact triangle mesh for static and kinematic bodies. Dynamic bodies can't use one, they get a convex hull instead.
|
||||||
|
class MeshCollider final : public MeshShapeCollider {
|
||||||
|
public:
|
||||||
|
static constexpr int DynamicHullVertices = 64;
|
||||||
|
|
||||||
|
explicit MeshCollider(GameObject& owner)
|
||||||
|
: MeshShapeCollider(owner) {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override {
|
||||||
|
return "MeshCollider";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
|
// Base for colliders shaped by the MeshRendererComponent on the same object. Unlike the primitive colliders,
|
||||||
|
// the mesh is in local units, so the transform's world scale is applied.
|
||||||
|
class MeshShapeCollider : public Collider {
|
||||||
|
public:
|
||||||
|
explicit MeshShapeCollider(GameObject& owner)
|
||||||
|
: Collider(owner) {}
|
||||||
|
|
||||||
|
void Update(float) override {}
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
// Used instead of the MeshRenderer mesh, not serialized
|
||||||
|
void SetCustomMesh(std::shared_ptr<const PhysicsMeshData> mesh) {
|
||||||
|
m_CustomMesh = std::move(mesh);
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> GetSourceMesh() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] glm::vec3 GetShapeScale() const;
|
||||||
|
[[nodiscard]] glm::mat4 GetShapeMatrix() const;
|
||||||
|
|
||||||
|
// Falls back to a box around the mesh when it has no volume
|
||||||
|
[[nodiscard]] PhysicsShapeDesc BuildConvexHullShape(int maxVertices) const;
|
||||||
|
void DrawConvexHull(int maxVertices, const glm::vec4& color) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<const PhysicsMeshData> m_CustomMesh;
|
||||||
|
};
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace DebugDraw {
|
namespace DebugDraw {
|
||||||
void Arc(const glm::vec3& center, const glm::vec3& axisA, const glm::vec3& axisB, float radius,
|
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);
|
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 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 Capsule(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color);
|
||||||
|
void Cylinder(const glm::vec3& center, const glm::quat& rotation, float radius, float height, const glm::vec4& color);
|
||||||
|
|
||||||
|
// Wireframe of an indexed triangle list, stops after maxTriangles
|
||||||
|
void Triangles(const glm::mat4& transform, const std::vector<glm::vec3>& points,
|
||||||
|
const std::vector<std::uint32_t>& indices, const glm::vec4& color,
|
||||||
|
std::size_t maxTriangles = 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // DEBUGDRAW_H
|
#endif // DEBUGDRAW_H
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <destrum/Physics/PhysicsTypes.h>
|
||||||
|
|
||||||
|
struct CPUMesh;
|
||||||
|
|
||||||
|
// Shares collision data between colliders using the same mesh, so physics backends can share their shapes too.
|
||||||
|
// Main thread only.
|
||||||
|
namespace PhysicsMeshCache {
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> FromCPUMesh(const std::string& key, const CPUMesh& mesh);
|
||||||
|
|
||||||
|
// Null when the points don't span a volume (e.g. a plane)
|
||||||
|
[[nodiscard]] std::shared_ptr<const PhysicsMeshData> ConvexHull(const std::shared_ptr<const PhysicsMeshData>& source,
|
||||||
|
int maxVertices);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
@@ -17,7 +19,10 @@ enum class PhysicsShapeType {
|
|||||||
None,
|
None,
|
||||||
Box,
|
Box,
|
||||||
Sphere,
|
Sphere,
|
||||||
Capsule
|
Capsule,
|
||||||
|
Cylinder,
|
||||||
|
ConvexHull,
|
||||||
|
Mesh
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PhysicsBodyHandle {
|
struct PhysicsBodyHandle {
|
||||||
@@ -50,6 +55,12 @@ struct PhysicsMaterialDesc {
|
|||||||
float restitution{0.0f};
|
float restitution{0.0f};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Triangles share vertices through indices. A convex hull only uses the points, the indices are for drawing it.
|
||||||
|
struct PhysicsMeshData {
|
||||||
|
std::vector<glm::vec3> points;
|
||||||
|
std::vector<std::uint32_t> indices;
|
||||||
|
};
|
||||||
|
|
||||||
struct PhysicsShapeDesc {
|
struct PhysicsShapeDesc {
|
||||||
PhysicsShapeType type{PhysicsShapeType::None};
|
PhysicsShapeType type{PhysicsShapeType::None};
|
||||||
|
|
||||||
@@ -59,9 +70,14 @@ struct PhysicsShapeDesc {
|
|||||||
// Sphere / capsule
|
// Sphere / capsule
|
||||||
float radius{0.5f};
|
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};
|
float height{2.0f};
|
||||||
|
|
||||||
|
// Convex hull / mesh, in the collider's local space before scale. Backends may cache
|
||||||
|
// shapes per mesh data, so it must not be modified once shared.
|
||||||
|
std::shared_ptr<const PhysicsMeshData> mesh;
|
||||||
|
glm::vec3 scale{1.0f};
|
||||||
|
|
||||||
// Local offset from the rigidbody transform.
|
// Local offset from the rigidbody transform.
|
||||||
glm::vec3 centerOffset{0.0f};
|
glm::vec3 centerOffset{0.0f};
|
||||||
|
|
||||||
@@ -101,6 +117,45 @@ struct PhysicsShapeDesc {
|
|||||||
desc.isTrigger = isTrigger;
|
desc.isTrigger = isTrigger;
|
||||||
return desc;
|
return desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc Cylinder(float radius,
|
||||||
|
float height,
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::Cylinder;
|
||||||
|
desc.radius = radius;
|
||||||
|
desc.height = height;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc ConvexHull(std::shared_ptr<const PhysicsMeshData> hull,
|
||||||
|
const glm::vec3& scale = glm::vec3{1.0f},
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::ConvexHull;
|
||||||
|
desc.mesh = std::move(hull);
|
||||||
|
desc.scale = scale;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static PhysicsShapeDesc Mesh(std::shared_ptr<const PhysicsMeshData> mesh,
|
||||||
|
const glm::vec3& scale = glm::vec3{1.0f},
|
||||||
|
const glm::vec3& centerOffset = glm::vec3{0.0f},
|
||||||
|
bool isTrigger = false) {
|
||||||
|
PhysicsShapeDesc desc{};
|
||||||
|
desc.type = PhysicsShapeType::Mesh;
|
||||||
|
desc.mesh = std::move(mesh);
|
||||||
|
desc.scale = scale;
|
||||||
|
desc.centerOffset = centerOffset;
|
||||||
|
desc.isTrigger = isTrigger;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PhysicsBodyDesc {
|
struct PhysicsBodyDesc {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
#include <destrum/Components/Spinner.h>
|
#include <destrum/Components/Spinner.h>
|
||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||||
|
#include <destrum/Components/Physics/ConvexHullCollider.h>
|
||||||
|
#include <destrum/Components/Physics/CylinderCollider.h>
|
||||||
|
#include <destrum/Components/Physics/MeshCollider.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/Components/Physics/SphereCollider.h>
|
#include <destrum/Components/Physics/SphereCollider.h>
|
||||||
#include <destrum/Components/UI/Canvas.h>
|
#include <destrum/Components/UI/Canvas.h>
|
||||||
@@ -32,7 +35,7 @@
|
|||||||
X(Rigidbody) \
|
X(Rigidbody) \
|
||||||
X(BoxCollider) \
|
X(BoxCollider) \
|
||||||
X(SphereCollider) \
|
X(SphereCollider) \
|
||||||
X(CapsuleCollider) \
|
X(CapsuleCollider) X(CylinderCollider) X(ConvexHullCollider) X(MeshCollider) \
|
||||||
X(Canvas) \
|
X(Canvas) \
|
||||||
X(RectTransform) \
|
X(RectTransform) \
|
||||||
X(UIImage) \
|
X(UIImage) \
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <destrum/Components/MeshRendererComponent.h>
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
#include <destrum/Assets/AssetDatabase.h>
|
#include <destrum/Assets/AssetDatabase.h>
|
||||||
#include <destrum/Assets/AssetReference.h>
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
#include <destrum/Serialization/MaterialSerialization.h>
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
@@ -254,6 +255,9 @@ void MeshRendererComponent::SetMeshID(MeshID id) {
|
|||||||
meshKey = resources->meshes().getMeshKey(id);
|
meshKey = resources->meshes().getMeshKey(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (GetGameObject()->GetComponent<MeshShapeCollider>() != nullptr) {
|
||||||
|
NotifyPhysicsChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::SetMaterialID(MaterialID id) {
|
void MeshRendererComponent::SetMaterialID(MaterialID id) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#include <destrum/Components/Physics/ConvexHullCollider.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
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<int>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#include <destrum/Components/Physics/CylinderCollider.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include <destrum/Components/Physics/MeshCollider.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
|
||||||
|
bool MeshCollider::UsesConvexFallback() const {
|
||||||
|
const auto* rigidbody = GetGameObject()->GetComponent<Rigidbody>();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include <destrum/Components/Physics/MeshShapeCollider.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
|
#include <destrum/Graphics/DebugDraw.h>
|
||||||
|
#include <destrum/Graphics/RenderResources.h>
|
||||||
|
#include <destrum/Graphics/Renderer.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Physics/PhysicsMeshCache.h>
|
||||||
|
#include <destrum/Util/GameState.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct Bounds {
|
||||||
|
glm::vec3 center{0.0f};
|
||||||
|
glm::vec3 halfExtents{0.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds ComputeBounds(const PhysicsMeshData& mesh) {
|
||||||
|
glm::vec3 min{std::numeric_limits<float>::max()};
|
||||||
|
glm::vec3 max{std::numeric_limits<float>::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<const PhysicsMeshData> MeshShapeCollider::GetSourceMesh() const {
|
||||||
|
if (m_CustomMesh) {
|
||||||
|
return m_CustomMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* meshRenderer = GetGameObject()->GetComponent<MeshRendererComponent>();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<float>(), color);
|
Arc(bottom, right, up, radius, pi, glm::two_pi<float>(), color);
|
||||||
Arc(bottom, forward, up, radius, pi, glm::two_pi<float>(), color);
|
Arc(bottom, forward, up, radius, pi, glm::two_pi<float>(), 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<glm::vec3>& points,
|
||||||
|
const std::vector<std::uint32_t>& 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
#include <Jolt/Physics/Collision/RayCast.h>
|
#include <Jolt/Physics/Collision/RayCast.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||||
#include <Jolt/Physics/PhysicsSystem.h>
|
#include <Jolt/Physics/PhysicsSystem.h>
|
||||||
#include <Jolt/RegisterTypes.h>
|
#include <Jolt/RegisterTypes.h>
|
||||||
@@ -34,7 +38,9 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/gtc/epsilon.hpp>
|
||||||
#include <glm/gtx/norm.hpp>
|
#include <glm/gtx/norm.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
@@ -270,6 +276,46 @@ namespace
|
|||||||
return type == RigidbodyType::Static ? Layers::NON_MOVING : Layers::MOVING;
|
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<Vec3> 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)
|
[[nodiscard]] ShapeRefC CreateJoltShape(const PhysicsShapeDesc& desc)
|
||||||
{
|
{
|
||||||
switch (desc.type)
|
switch (desc.type)
|
||||||
@@ -298,6 +344,17 @@ namespace
|
|||||||
return new CapsuleShape(halfCylinderHeight, desc.radius);
|
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:
|
case PhysicsShapeType::None:
|
||||||
default:
|
default:
|
||||||
throw std::runtime_error("Cannot create Jolt body without a valid shape.");
|
throw std::runtime_error("Cannot create Jolt body without a valid shape.");
|
||||||
@@ -433,13 +490,14 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_Bodies.clear();
|
m_Bodies.clear();
|
||||||
|
m_MeshShapeCache.clear();
|
||||||
|
|
||||||
ShutdownJoltGlobals();
|
ShutdownJoltGlobals();
|
||||||
}
|
}
|
||||||
|
|
||||||
PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc)
|
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
|
// 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)
|
// 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<JPH::uint64>(desc.owner);
|
settings.mUserData = reinterpret_cast<JPH::uint64>(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.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
|
||||||
settings.mMassPropertiesOverride.mMass = std::max(0.0001f, desc.mass);
|
settings.mMassPropertiesOverride.mMass = std::max(0.0001f, desc.mass);
|
||||||
@@ -709,6 +776,50 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct CachedMeshShape
|
||||||
|
{
|
||||||
|
std::weak_ptr<const PhysicsMeshData> 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
|
struct BodyRecord
|
||||||
{
|
{
|
||||||
bool alive{false};
|
bool alive{false};
|
||||||
@@ -802,6 +913,8 @@ private:
|
|||||||
std::unordered_map<std::uint32_t, BodyRecord> m_Bodies;
|
std::unordered_map<std::uint32_t, BodyRecord> m_Bodies;
|
||||||
std::uint32_t m_NextHandle{0};
|
std::uint32_t m_NextHandle{0};
|
||||||
|
|
||||||
|
std::unordered_map<const PhysicsMeshData*, CachedMeshShape> m_MeshShapeCache;
|
||||||
|
|
||||||
SensorContactListener m_SensorListener;
|
SensorContactListener m_SensorListener;
|
||||||
std::unordered_map<SensorContactListener::BodyPair, SensorContactListener::PairData, SensorContactListener::PairHash> m_PreviousSensorOverlaps;
|
std::unordered_map<SensorContactListener::BodyPair, SensorContactListener::PairData, SensorContactListener::PairHash> m_PreviousSensorOverlaps;
|
||||||
std::vector<TriggerEvent> m_TriggerEvents;
|
std::vector<TriggerEvent> m_TriggerEvents;
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#include <destrum/Physics/PhysicsMeshCache.h>
|
||||||
|
|
||||||
|
#include <Jolt/Jolt.h>
|
||||||
|
|
||||||
|
#include <Jolt/Core/Memory.h>
|
||||||
|
#include <Jolt/Geometry/ConvexHullBuilder.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <glm/gtx/component_wise.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct HullEntry {
|
||||||
|
std::weak_ptr<const PhysicsMeshData> source;
|
||||||
|
std::weak_ptr<const PhysicsMeshData> hull;
|
||||||
|
bool failed{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::weak_ptr<const PhysicsMeshData>> g_Meshes;
|
||||||
|
std::map<std::pair<const PhysicsMeshData*, int>, HullEntry> g_Hulls;
|
||||||
|
|
||||||
|
std::shared_ptr<const PhysicsMeshData> 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<float>::max()};
|
||||||
|
glm::vec3 max{std::numeric_limits<float>::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<PhysicsMeshData>();
|
||||||
|
std::unordered_map<int, std::uint32_t> remap;
|
||||||
|
const auto hullIndex = [&](int sourceIndex) {
|
||||||
|
auto [it, inserted] = remap.try_emplace(sourceIndex, static_cast<std::uint32_t>(hull->points.size()));
|
||||||
|
if (inserted) {
|
||||||
|
hull->points.push_back(source.points[static_cast<std::size_t>(sourceIndex)]);
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::uint32_t> 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<const PhysicsMeshData> 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<PhysicsMeshData>();
|
||||||
|
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<const PhysicsMeshData> PhysicsMeshCache::ConvexHull(const std::shared_ptr<const PhysicsMeshData>& 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;
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@ void PhysicsWorld::RegisterRigidbody(Rigidbody& rigidbody) {
|
|||||||
desc.transform.position = transform.GetWorldPosition();
|
desc.transform.position = transform.GetWorldPosition();
|
||||||
desc.transform.rotation = transform.GetWorldRotation();
|
desc.transform.rotation = transform.GetWorldRotation();
|
||||||
desc.shape = collider->BuildPhysicsShape();
|
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
|
// Collider dimensions are expressed in world units. Render meshes may
|
||||||
// use a transform scale to normalize imported asset units, so applying
|
// use a transform scale to normalize imported asset units, so applying
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ private:
|
|||||||
bool m_SaveAsDialog{false};
|
bool m_SaveAsDialog{false};
|
||||||
bool m_ItemWasActive{false};
|
bool m_ItemWasActive{false};
|
||||||
bool m_EditPending{false};
|
bool m_EditPending{false};
|
||||||
|
bool m_CameraHadInput{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // EDITOR_EDITOR_H
|
#endif // EDITOR_EDITOR_H
|
||||||
|
|||||||
@@ -11,6 +11,17 @@
|
|||||||
|
|
||||||
class Scene;
|
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 {
|
class UndoStack final {
|
||||||
public:
|
public:
|
||||||
static constexpr std::size_t MaxSteps = 64;
|
static constexpr std::size_t MaxSteps = 64;
|
||||||
|
|||||||
+11
-4
@@ -101,8 +101,9 @@ void Editor::onImGuiBegin(float dt)
|
|||||||
ApplyPick();
|
ApplyPick();
|
||||||
HandleShortcuts();
|
HandleShortcuts();
|
||||||
|
|
||||||
|
m_CameraHadInput = UsesEditorCamera() && (m_Viewport.hovered || m_Viewport.focused || m_Camera.IsLooking());
|
||||||
if (UsesEditorCamera()) {
|
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)) {
|
if (m_ShaderHotReload.Poll(dt)) {
|
||||||
@@ -153,10 +154,16 @@ void Editor::onEndFrame()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bool itemActive = ImGui::IsAnyItemActive() || ImGuizmo::IsUsing();
|
const bool itemActive = ImGui::IsAnyItemActive() || ImGuizmo::IsUsing();
|
||||||
const bool gestureEnded = (m_ItemWasActive && !itemActive) || ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
|
const bool gestureEnded = EndsEditGesture({
|
||||||
AnyKeyReleased() || std::exchange(m_EditPending, false);
|
.itemWasActive = m_ItemWasActive,
|
||||||
|
.itemActive = itemActive,
|
||||||
|
.mouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left),
|
||||||
|
.keyReleased = AnyKeyReleased(),
|
||||||
|
.keyboardDrivesCamera = m_CameraHadInput,
|
||||||
|
.editPending = std::exchange(m_EditPending, false),
|
||||||
|
});
|
||||||
m_ItemWasActive = itemActive;
|
m_ItemWasActive = itemActive;
|
||||||
if (gestureEnded && !itemActive && m_PlayMode.IsEditing()) {
|
if (gestureEnded && m_PlayMode.IsEditing()) {
|
||||||
RecordEdits();
|
RecordEdits();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,16 @@
|
|||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
|
|
||||||
|
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::Snapshot> UndoStack::Capture(Scene& scene)
|
std::optional<UndoStack::Snapshot> UndoStack::Capture(Scene& scene)
|
||||||
{
|
{
|
||||||
const auto root = SceneSerializer::Capture(scene);
|
const auto root = SceneSerializer::Capture(scene);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -178,6 +178,9 @@ namespace {
|
|||||||
"BoxCollider",
|
"BoxCollider",
|
||||||
"SphereCollider",
|
"SphereCollider",
|
||||||
"CapsuleCollider",
|
"CapsuleCollider",
|
||||||
|
"CylinderCollider",
|
||||||
|
"ConvexHullCollider",
|
||||||
|
"MeshCollider",
|
||||||
"Canvas",
|
"Canvas",
|
||||||
"RectTransform",
|
"RectTransform",
|
||||||
"UIImage",
|
"UIImage",
|
||||||
|
|||||||
@@ -59,10 +59,24 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
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()
|
void RunEditorTests()
|
||||||
{
|
{
|
||||||
testUndoRedoRestoresEdits();
|
testUndoRedoRestoresEdits();
|
||||||
testUndoHistoryIsCapped();
|
testUndoHistoryIsCapped();
|
||||||
|
testCameraKeysDoNotEndEditGestures();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||||
#include <destrum/Components/Physics/Collider.h>
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
#include <destrum/Components/Physics/ConvexHullCollider.h>
|
||||||
|
#include <destrum/Components/Physics/CylinderCollider.h>
|
||||||
|
#include <destrum/Components/Physics/MeshCollider.h>
|
||||||
#include <destrum/Components/LightComponent.h>
|
#include <destrum/Components/LightComponent.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
|
|
||||||
@@ -357,6 +360,124 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<const PhysicsMeshData> makeCubeMesh(float halfExtent)
|
||||||
|
{
|
||||||
|
auto mesh = std::make_shared<PhysicsMeshData>();
|
||||||
|
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<const PhysicsMeshData> makeQuadMesh(float halfExtent)
|
||||||
|
{
|
||||||
|
auto mesh = std::make_shared<PhysicsMeshData>();
|
||||||
|
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<CylinderCollider>(1.0f, 4.0f);
|
||||||
|
Rigidbody* rigidbody = object.AddComponent<Rigidbody>();
|
||||||
|
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<ConvexHullCollider>();
|
||||||
|
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>();
|
||||||
|
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<ConvexHullCollider>();
|
||||||
|
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<ConvexHullCollider>();
|
||||||
|
Rigidbody* emptyBody = empty.AddComponent<Rigidbody>();
|
||||||
|
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<MeshCollider>()->SetCustomMesh(makeQuadMesh(10.0f));
|
||||||
|
Rigidbody* groundBody = ground.AddComponent<Rigidbody>();
|
||||||
|
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<SphereCollider>(0.5f);
|
||||||
|
Rigidbody* ballBody = ball.AddComponent<Rigidbody>();
|
||||||
|
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<MeshCollider>()->SetCustomMesh(makeCubeMesh(1.0f));
|
||||||
|
Rigidbody* kinematicBody = kinematic.AddComponent<Rigidbody>();
|
||||||
|
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<MeshCollider>();
|
||||||
|
dynamicCollider->SetCustomMesh(makeCubeMesh(1.0f));
|
||||||
|
dynamic.AddComponent<Rigidbody>();
|
||||||
|
check(dynamicCollider->BuildPhysicsShape().type == PhysicsShapeType::ConvexHull,
|
||||||
|
"dynamic mesh colliders must fall back to a convex hull");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunPhysicsTests()
|
void RunPhysicsTests()
|
||||||
@@ -368,5 +489,8 @@ void RunPhysicsTests()
|
|||||||
testKinematicBodiesMoveWithVelocity();
|
testKinematicBodiesMoveWithVelocity();
|
||||||
testColliderCenterOffset();
|
testColliderCenterOffset();
|
||||||
testFrozenRotationCapsule();
|
testFrozenRotationCapsule();
|
||||||
|
testCylinderCollider();
|
||||||
|
testConvexHullCollider();
|
||||||
|
testMeshCollider();
|
||||||
testTriggerZone();
|
testTriggerZone();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user