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; +};