diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs
index 1eeb55511..9ce64efa9 100644
--- a/Source/Editor/SceneGraph/Actors/SplineNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs
@@ -308,10 +308,11 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.OnContextMenu(contextMenu);
- contextMenu.AddButton("Add spline model", OnAddSplineMode);
+ contextMenu.AddButton("Add spline model", OnAddSplineModel);
+ contextMenu.AddButton("Add spline collider", OnAddSplineCollider);
}
- private void OnAddSplineMode()
+ private void OnAddSplineModel()
{
var actor = new SplineModel
{
@@ -321,6 +322,17 @@ namespace FlaxEditor.SceneGraph.Actors
Editor.Instance.SceneEditing.Spawn(actor, Actor);
}
+ private void OnAddSplineCollider()
+ {
+ var actor = new SplineCollider
+ {
+ StaticFlags = Actor.StaticFlags,
+ Transform = Actor.Transform,
+ };
+ // TODO: auto pick the collision data if already using spline model
+ Editor.Instance.SceneEditing.Spawn(actor, Actor);
+ }
+
///
public override void OnDispose()
{
diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp
index 1a5e580bb..76ecb9653 100644
--- a/Source/Engine/Level/Actors/Spline.cpp
+++ b/Source/Engine/Level/Actors/Spline.cpp
@@ -10,6 +10,11 @@ Spline::Spline(const SpawnParams& params)
{
}
+bool Spline::GetIsLoop() const
+{
+ return _loop;
+}
+
void Spline::SetIsLoop(bool value)
{
if (_loop != value)
diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h
index 747530703..f19f9a443 100644
--- a/Source/Engine/Level/Actors/Spline.h
+++ b/Source/Engine/Level/Actors/Spline.h
@@ -28,10 +28,7 @@ public:
/// Whether to use spline as closed loop. In that case, ensure to place start and end at the same location.
///
API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Spline\")")
- FORCE_INLINE bool GetIsLoop() const
- {
- return _loop;
- }
+ bool GetIsLoop() const;
///
/// Whether to use spline as closed loop. In that case, ensure to place start and end at the same location.
diff --git a/Source/Engine/Physics/Colliders/SplineCollider.cpp b/Source/Engine/Physics/Colliders/SplineCollider.cpp
new file mode 100644
index 000000000..a529baf7d
--- /dev/null
+++ b/Source/Engine/Physics/Colliders/SplineCollider.cpp
@@ -0,0 +1,329 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#include "SplineCollider.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Core/Math/Matrix.h"
+#include "Engine/Level/Actors/Spline.h"
+#include "Engine/Serialization/Serialization.h"
+#include "Engine/Physics/Utilities.h"
+#include "Engine/Physics/Physics.h"
+#include "Engine/Profiler/ProfilerCPU.h"
+#if COMPILE_WITH_PHYSICS_COOKING
+#include "Engine/Physics/CollisionCooking.h"
+#include
+#include
+#endif
+#include
+
+SplineCollider::SplineCollider(const SpawnParams& params)
+ : Collider(params)
+{
+ CollisionData.Changed.Bind(this);
+ CollisionData.Loaded.Bind(this);
+}
+
+void SplineCollider::OnCollisionDataChanged()
+{
+ // This should not be called during physics simulation, if it happened use write lock on physx scene
+ ASSERT(!Physics::IsDuringSimulation());
+
+ if (CollisionData)
+ {
+ // Ensure that collision asset is loaded (otherwise objects might fall though collider that is not yet loaded on play begin)
+ CollisionData->WaitForLoaded();
+ }
+
+ UpdateGeometry();
+}
+
+void SplineCollider::OnCollisionDataLoaded()
+{
+ UpdateGeometry();
+}
+
+void SplineCollider::OnSplineUpdated()
+{
+ if (!_spline || !IsActiveInHierarchy() || _spline->GetSplinePointsCount() < 2 || !CollisionData || !CollisionData->IsLoaded())
+ {
+ _box = BoundingBox(_transform.Translation, _transform.Translation);
+ BoundingSphere::FromBox(_box, _sphere);
+ return;
+ }
+
+ UpdateGeometry();
+}
+
+bool SplineCollider::CanAttach(RigidBody* rigidBody) const
+{
+ return false;
+}
+
+bool SplineCollider::CanBeTrigger() const
+{
+ return false;
+}
+
+#if USE_EDITOR
+
+#include "Engine/Debug/DebugDraw.h"
+
+void SplineCollider::DrawPhysicsDebug(RenderView& view)
+{
+ DEBUG_DRAW_TRIANGLES_EX(_vertexBuffer, _indexBuffer, Color::GreenYellow * 0.8f, 0, true);
+}
+
+void SplineCollider::OnDebugDrawSelected()
+{
+ DEBUG_DRAW_TRIANGLES_EX(_vertexBuffer, _indexBuffer, Color::GreenYellow, 0, false);
+
+ // Base
+ Collider::OnDebugDrawSelected();
+}
+
+#endif
+
+bool SplineCollider::IntersectsItself(const Ray& ray, float& distance, Vector3& normal)
+{
+ // Use detailed hit
+ if (_shape)
+ {
+ RayCastHit hitInfo;
+ if (!RayCast(ray.Position, ray.Direction, hitInfo))
+ return false;
+ distance = hitInfo.Distance;
+ normal = hitInfo.Normal;
+ return true;
+ }
+
+ // Fallback to AABB
+ return _box.Intersects(ray, distance, normal);
+}
+
+void SplineCollider::Serialize(SerializeStream& stream, const void* otherObj)
+{
+ // Base
+ Collider::Serialize(stream, otherObj);
+
+ SERIALIZE_GET_OTHER_OBJ(SplineCollider);
+
+ SERIALIZE(CollisionData);
+}
+
+void SplineCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
+{
+ // Base
+ Collider::Deserialize(stream, modifier);
+
+ DESERIALIZE(CollisionData);
+}
+
+void SplineCollider::OnParentChanged()
+{
+ if (_spline)
+ {
+ _spline->SplineUpdated.Unbind(this);
+ }
+
+ // Base
+ Collider::OnParentChanged();
+
+ _spline = Cast(_parent);
+ if (_spline)
+ {
+ _spline->SplineUpdated.Bind(this);
+ }
+
+ OnSplineUpdated();
+}
+
+void SplineCollider::EndPlay()
+{
+ // Base
+ Collider::EndPlay();
+
+ // Cleanup
+ if (_triangleMesh)
+ {
+ Physics::RemoveObject(_triangleMesh);
+ _triangleMesh = nullptr;
+ }
+}
+
+void SplineCollider::UpdateBounds()
+{
+ // Unused as bounds are updated during collision building
+}
+
+void SplineCollider::GetGeometry(PxGeometryHolder& geometry)
+{
+ // Reset bounds
+ _box = BoundingBox(_transform.Translation, _transform.Translation);
+ BoundingSphere::FromBox(_box, _sphere);
+
+ // Skip if sth is missing
+ if (!_spline || !IsActiveInHierarchy() || _spline->GetSplinePointsCount() < 2 || !CollisionData || !CollisionData->IsLoaded())
+ {
+ geometry.storeAny(PxSphereGeometry(0.001f));
+ return;
+ }
+ PROFILE_CPU();
+
+ // Extract collision geometry
+ // TODO: cache memory allocation for dynamic colliders
+ Array collisionVertices;
+ Array collisionIndices;
+ CollisionData->ExtractGeometry(collisionVertices, collisionIndices);
+ if (collisionIndices.IsEmpty())
+ {
+ geometry.storeAny(PxSphereGeometry(0.001f));
+ return;
+ }
+
+ // Apply local mesh transformation
+ const Transform localTransform = _localTransform;
+ if (!localTransform.IsIdentity())
+ {
+ for (int32 i = 0; i < collisionVertices.Count(); i++)
+ collisionVertices[i] = localTransform.LocalToWorld(collisionVertices[i]);
+ }
+
+ // Find collision geometry local bounds
+ BoundingBox localModelBounds;
+ localModelBounds.Minimum = localModelBounds.Maximum = collisionVertices[0];
+ for (int32 i = 1; i < collisionVertices.Count(); i++)
+ {
+ Vector3 v = collisionVertices[i];
+ localModelBounds.Minimum = Vector3::Min(localModelBounds.Minimum, v);
+ localModelBounds.Maximum = Vector3::Max(localModelBounds.Maximum, v);
+ }
+ auto localModelBoundsSize = localModelBounds.GetSize();
+
+ // Deform geometry over the spline
+ const auto& keyframes = _spline->Curve.GetKeyframes();
+ const int32 segments = keyframes.Count() - 1;
+ _vertexBuffer.Resize(collisionVertices.Count() * segments);
+ _indexBuffer.Resize(collisionIndices.Count() * segments);
+ const Transform splineTransform = _spline->GetTransform();
+ const Transform colliderTransform = GetTransform();
+ Transform curveTransform, leftTangent, rightTangent;
+ for (int32 segment = 0; segment < segments; segment++)
+ {
+ // Setup for the spline segment
+ auto offsetVertices = segment * collisionVertices.Count();
+ auto offsetIndices = segment * collisionIndices.Count();
+ const auto& start = keyframes[segment];
+ const auto& end = keyframes[segment + 1];
+ const float length = end.Time - start.Time;
+ AnimationUtils::GetTangent(start.Value, start.TangentOut, length, leftTangent);
+ AnimationUtils::GetTangent(end.Value, end.TangentIn, length, rightTangent);
+
+ // Vertex buffer is deformed along the spline
+ auto srcVertices = collisionVertices.Get();
+ auto dstVertices = _vertexBuffer.Get() + offsetVertices;
+ for (int32 i = 0; i < collisionVertices.Count(); i++)
+ {
+ Vector3 v = srcVertices[i];
+ const float alpha = Math::Saturate((v.Z - localModelBounds.Minimum.Z) / localModelBoundsSize.Z);
+ v.Z = alpha;
+
+ // Evaluate transformation at the curve
+ AnimationUtils::Bezier(start.Value, leftTangent, rightTangent, end.Value, alpha, curveTransform);
+
+ // Apply spline direction (from position 1st derivative)
+ Vector3 direction;
+ AnimationUtils::BezierFirstDerivative(start.Value.Translation, leftTangent.Translation, rightTangent.Translation, end.Value.Translation, alpha, direction);
+ direction.Normalize();
+ Quaternion orientation;
+ if (direction.IsZero())
+ orientation = Quaternion::Identity;
+ else if (Vector3::Dot(direction, Vector3::Up) >= 0.999f)
+ Quaternion::RotationAxis(Vector3::Left, PI_HALF, orientation);
+ else
+ Quaternion::LookRotation(direction, Vector3::Cross(Vector3::Cross(direction, Vector3::Up), direction), orientation);
+ curveTransform.Orientation = orientation * curveTransform.Orientation;
+
+ // Transform vertex
+ v = curveTransform.LocalToWorld(v);
+ v = splineTransform.LocalToWorld(v);
+ v = colliderTransform.WorldToLocal(v);
+
+ dstVertices[i] = v;
+ }
+
+ // Index buffer is the same for every segment except it's shifted
+ auto srcIndices = collisionIndices.Get();
+ auto dstIndices = _indexBuffer.Get() + offsetIndices;
+ for (int32 i = 0; i < collisionIndices.Count(); i++)
+ dstIndices[i] = srcIndices[i] + offsetVertices;
+ }
+
+ // Prepare scale
+ Vector3 scale = _cachedScale;
+ scale.Absolute();
+ const float minSize = 0.001f;
+ scale = Vector3::Max(scale, minSize);
+
+ // TODO: add support for cooking collision for static splines in editor and reusing it in game
+
+#if COMPILE_WITH_PHYSICS_COOKING
+ // Cook triangle mesh collision
+ CollisionCooking::CookingInput cookingInput;
+ cookingInput.VertexCount = _vertexBuffer.Count();
+ cookingInput.VertexData = _vertexBuffer.Get();
+ cookingInput.IndexCount = _indexBuffer.Count();
+ cookingInput.IndexData = _indexBuffer.Get();
+ cookingInput.Is16bitIndexData = false;
+ BytesContainer collisionData;
+ if (!CollisionCooking::CookTriangleMesh(cookingInput, collisionData))
+ {
+ // Create triangle mesh
+ if (_triangleMesh)
+ {
+ Physics::RemoveObject(_triangleMesh);
+ _triangleMesh = nullptr;
+ }
+ PxDefaultMemoryInputData input(collisionData.Get(), collisionData.Length());
+ // TODO: try using getVerticesForModification for dynamic triangle mesh vertices updating when changing curve in the editor
+ _triangleMesh = Physics::GetPhysics()->createTriangleMesh(input);
+ if (!_triangleMesh)
+ {
+ LOG(Error, "Failed to create triangle mesh from collision data of {0}.", ToString());
+ geometry.storeAny(PxSphereGeometry(0.001f));
+ return;
+ }
+
+#if USE_EDITOR
+ // Transform vertices back to world space for debug shapes drawing
+ for (int32 i = 0; i < _vertexBuffer.Count(); i++)
+ _vertexBuffer[i] = colliderTransform.LocalToWorld(_vertexBuffer[i]);
+#endif
+
+ // Update bounds
+ _box = P2C(_triangleMesh->getLocalBounds());
+ Matrix splineWorld;
+ colliderTransform.GetWorld(splineWorld);
+ BoundingBox::Transform(_box, splineWorld, _box);
+ BoundingSphere::FromBox(_box, _sphere);
+
+ // Setup geometry
+ PxTriangleMeshGeometry triangleMesh;
+ triangleMesh.scale.scale = C2P(scale);
+ triangleMesh.triangleMesh = _triangleMesh;
+ geometry.storeAny(triangleMesh);
+
+#if !USE_EDITOR
+ // Free memory for static splines (if editor collision preview is not needed)
+ if (IsStatic())
+ {
+ _vertexBuffer.Resize(0);
+ _indexBuffer.Resize(0);
+ }
+#endif
+
+ return;
+ }
+#endif
+
+ LOG(Error, "Cannot build collision data for {0} due to runtime collision cooking diabled.", ToString());
+ geometry.storeAny(PxSphereGeometry(0.001f));
+}
diff --git a/Source/Engine/Physics/Colliders/SplineCollider.h b/Source/Engine/Physics/Colliders/SplineCollider.h
new file mode 100644
index 000000000..b5c0cf318
--- /dev/null
+++ b/Source/Engine/Physics/Colliders/SplineCollider.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Collider.h"
+#include "Engine/Content/AssetReference.h"
+#include "Engine/Physics/CollisionData.h"
+
+class Spline;
+
+///
+/// A collider represented by an arbitrary mesh that goes over the spline.
+///
+///
+///
+API_CLASS() class FLAXENGINE_API SplineCollider : public Collider
+{
+DECLARE_SCENE_OBJECT(SplineCollider);
+private:
+ Spline* _spline = nullptr;
+ PxTriangleMesh* _triangleMesh = nullptr;
+ Array _vertexBuffer;
+ Array _indexBuffer;
+
+public:
+
+ ///
+ /// Linked collision data asset that contains convex mesh or triangle mesh used to represent a spline collider shape.
+ ///
+ API_FIELD(Attributes="EditorOrder(100), DefaultValue(null), EditorDisplay(\"Collider\")")
+ AssetReference CollisionData;
+
+private:
+
+ void OnCollisionDataChanged();
+ void OnCollisionDataLoaded();
+ void OnSplineUpdated();
+
+public:
+
+ // [Collider]
+ bool CanAttach(RigidBody* rigidBody) const override;
+ bool CanBeTrigger() const override;
+#if USE_EDITOR
+ void OnDebugDrawSelected() override;
+#endif
+ bool IntersectsItself(const Ray& ray, float& distance, Vector3& normal) override;
+ void Serialize(SerializeStream& stream, const void* otherObj) override;
+ void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
+ void OnParentChanged() override;
+ void EndPlay() override;
+
+protected:
+
+ // [Collider]
+#if USE_EDITOR
+ void DrawPhysicsDebug(RenderView& view) override;
+#endif
+ void UpdateBounds() override;
+ void GetGeometry(PxGeometryHolder& geometry) override;
+};