diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs
new file mode 100644
index 000000000..ea826c29e
--- /dev/null
+++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+using FlaxEngine;
+
+namespace FlaxEditor.SceneGraph.Actors
+{
+ ///
+ /// Scene tree node for actor type.
+ ///
+ [HideInEditor]
+ public sealed class SplineNode : ActorNode
+ {
+ ///
+ public SplineNode(Actor actor)
+ : base(actor)
+ {
+ }
+ }
+}
diff --git a/Source/Editor/SceneGraph/SceneGraphFactory.cs b/Source/Editor/SceneGraph/SceneGraphFactory.cs
index 0076742b3..5f3dd8b0f 100644
--- a/Source/Editor/SceneGraph/SceneGraphFactory.cs
+++ b/Source/Editor/SceneGraph/SceneGraphFactory.cs
@@ -65,6 +65,7 @@ namespace FlaxEditor.SceneGraph
CustomNodesTypes.Add(typeof(NavModifierVolume), typeof(NavModifierVolumeNode));
CustomNodesTypes.Add(typeof(ParticleEffect), typeof(ParticleEffectNode));
CustomNodesTypes.Add(typeof(SceneAnimationPlayer), typeof(SceneAnimationPlayerNode));
+ CustomNodesTypes.Add(typeof(Spline), typeof(SplineNode));
}
///
diff --git a/Source/Editor/Windows/SceneTreeWindow.Actors.cs b/Source/Editor/Windows/SceneTreeWindow.Actors.cs
index 6fafe444f..8970b6a48 100644
--- a/Source/Editor/Windows/SceneTreeWindow.Actors.cs
+++ b/Source/Editor/Windows/SceneTreeWindow.Actors.cs
@@ -99,6 +99,7 @@ namespace FlaxEditor.Windows
new KeyValuePair("Nav Mesh Bounds Volume", typeof(NavMeshBoundsVolume)),
new KeyValuePair("Nav Link", typeof(NavLink)),
new KeyValuePair("Nav Modifier Volume", typeof(NavModifierVolume)),
+ new KeyValuePair("Spline", typeof(Spline)),
}
},
new ActorsGroup
diff --git a/Source/Editor/Windows/ToolboxWindow.cs b/Source/Editor/Windows/ToolboxWindow.cs
index 8c30621b0..63e1f1f05 100644
--- a/Source/Editor/Windows/ToolboxWindow.cs
+++ b/Source/Editor/Windows/ToolboxWindow.cs
@@ -167,6 +167,7 @@ namespace FlaxEditor.Windows
groupOther.AddChild(CreateActorItem("Nav Mesh Bounds Volume", typeof(NavMeshBoundsVolume)));
groupOther.AddChild(CreateActorItem("Nav Link", typeof(NavLink)));
groupOther.AddChild(CreateActorItem("Nav Modifier Volume", typeof(NavModifierVolume)));
+ groupOther.AddChild(CreateActorItem("Spline", typeof(Spline)));
var groupGui = CreateGroupWithList(actorGroups, "GUI");
groupGui.AddChild(CreateActorItem("UI Control", typeof(UIControl)));
diff --git a/Source/Engine/Animations/AnimationUtils.h b/Source/Engine/Animations/AnimationUtils.h
index 248efea0c..a7f85f124 100644
--- a/Source/Engine/Animations/AnimationUtils.h
+++ b/Source/Engine/Animations/AnimationUtils.h
@@ -7,6 +7,7 @@
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Quaternion.h"
+#include "Engine/Core/Math/Transform.h"
namespace AnimationUtils
{
@@ -40,6 +41,12 @@ namespace AnimationUtils
return Quaternion::Identity;
}
+ template<>
+ FORCE_INLINE Transform GetZero()
+ {
+ return Transform::Identity;
+ }
+
template<>
FORCE_INLINE Color GetZero()
{
@@ -66,6 +73,16 @@ namespace AnimationUtils
Quaternion::Slerp(a, b, oneThird, result);
}
+ template<>
+ FORCE_INLINE void GetTangent(const Transform& a, const Transform& b, float length, Transform& result)
+ {
+ const float oneThird = 1.0f / 3.0f;
+ const float oneThirdLength = length * oneThird;
+ result.Translation = a.Translation + b.Translation * oneThirdLength;
+ Quaternion::Slerp(a.Orientation, b.Orientation, oneThird, result.Orientation);
+ result.Scale = a.Scale + b.Scale * oneThirdLength;
+ }
+
template
FORCE_INLINE static void Interpolate(const T& a, const T& b, float alpha, T& result)
{
@@ -203,4 +220,12 @@ namespace AnimationUtils
Quaternion::Slerp(p12, p23, alpha, p123);
Quaternion::Slerp(p012, p123, alpha, result);
}
+
+ template<>
+ void Bezier(const Transform& p0, const Transform& p1, const Transform& p2, const Transform& p3, float alpha, Transform& result)
+ {
+ Bezier(p0.Translation, p1.Translation, p2.Translation, p3.Translation, alpha, result.Translation);
+ Bezier(p0.Orientation, p1.Orientation, p2.Orientation, p3.Orientation, alpha, result.Orientation);
+ Bezier(p0.Scale, p1.Scale, p2.Scale, p3.Scale, alpha, result.Scale);
+ }
}
diff --git a/Source/Engine/Animations/Curve.cs b/Source/Engine/Animations/Curve.cs
index d9762d05f..3a8ddc100 100644
--- a/Source/Engine/Animations/Curve.cs
+++ b/Source/Engine/Animations/Curve.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.InteropServices;
namespace FlaxEngine
{
@@ -338,6 +339,7 @@ namespace FlaxEngine
///
/// A single keyframe that can be injected into linear curve.
///
+ [StructLayout(LayoutKind.Sequential)]
public struct Keyframe : IComparable, IComparable
{
///
@@ -586,6 +588,7 @@ namespace FlaxEngine
///
/// A single keyframe that can be injected into Bezier curve.
///
+ [StructLayout(LayoutKind.Sequential)]
public struct Keyframe : IComparable, IComparable
{
///
diff --git a/Source/Engine/Animations/Curve.h b/Source/Engine/Animations/Curve.h
index 8261dea5b..618e0ee0c 100644
--- a/Source/Engine/Animations/Curve.h
+++ b/Source/Engine/Animations/Curve.h
@@ -52,6 +52,11 @@ public:
{
result = a;
}
+
+ bool operator==(const StepCurveKeyframe& other) const
+ {
+ return Math::NearEqual(Time, other.Time) && Math::NearEqual(Value, other.Value);
+ }
} PACK_END();
///
@@ -97,6 +102,11 @@ public:
result.Time = a.Time + (b.Time - a.Time) * alpha;
AnimationUtils::Interpolate(a.Value, b.Value, alpha, result.Value);
}
+
+ bool operator==(const LinearCurveKeyframe& other) const
+ {
+ return Math::NearEqual(Time, other.Time) && Math::NearEqual(Value, other.Value);
+ }
} PACK_END();
///
@@ -164,6 +174,11 @@ public:
result.TangentIn /= length;
result.TangentOut = result.TangentIn;
}
+
+ bool operator==(const HermiteCurveKeyframe& other) const
+ {
+ return Math::NearEqual(Time, other.Time) && Math::NearEqual(Value, other.Value) && Math::NearEqual(TangentIn, other.TangentIn) && Math::NearEqual(TangentOut, other.TangentOut);
+ }
} PACK_END();
///
@@ -240,6 +255,11 @@ public:
result.TangentIn = a.TangentOut;
result.TangentOut = b.TangentIn;
}
+
+ bool operator==(const BezierCurveKeyframe& other) const
+ {
+ return Math::NearEqual(Time, other.Time) && Math::NearEqual(Value, other.Value) && Math::NearEqual(TangentIn, other.TangentIn) && Math::NearEqual(TangentOut, other.TangentOut);
+ }
} PACK_END();
// @formatter:on
@@ -708,6 +728,30 @@ public:
return false;
}
+
+public:
+
+ FORCE_INLINE KeyFrame& operator[](int32 index)
+ {
+ return _keyframes[index];
+ }
+
+ FORCE_INLINE const KeyFrame& operator[](int32 index) const
+ {
+ return _keyframes[index];
+ }
+
+ bool operator==(const Curve& other) const
+ {
+ if (_keyframes.Count() != other._keyframes.Count())
+ return false;
+ for (int32 i = 0; i < _keyframes.Count(); i++)
+ {
+ if (!(_keyframes[i] == other._keyframes[i]))
+ return false;
+ }
+ return true;
+ }
};
///
diff --git a/Source/Engine/Animations/CurveSerialization.h b/Source/Engine/Animations/CurveSerialization.h
new file mode 100644
index 000000000..06f7788ea
--- /dev/null
+++ b/Source/Engine/Animations/CurveSerialization.h
@@ -0,0 +1,171 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Curve.h"
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Serialization/Serialization.h"
+
+// @formatter:off
+
+namespace Serialization
+{
+ // StepCurveKeyframe
+
+ template
+ inline bool ShouldSerialize(const StepCurveKeyframe& v, const void* otherObj)
+ {
+ if (!otherObj)
+ return true;
+ const auto other = (const StepCurveKeyframe*)otherObj;
+ return !(v == *other);
+ }
+ template
+ inline void Serialize(ISerializable::SerializeStream& stream, const StepCurveKeyframe& v, const void* otherObj)
+ {
+ stream.StartObject();
+ stream.JKEY("Time");
+ Serialize(stream, v.Time, nullptr);
+ stream.JKEY("Value");
+ Serialize(stream, v.Value, nullptr);
+ stream.EndObject();
+ }
+ template
+ inline void Deserialize(ISerializable::DeserializeStream& stream, StepCurveKeyframe& v, ISerializeModifier* modifier)
+ {
+ DESERIALIZE_MEMBER(Time, v.Time);
+ DESERIALIZE_MEMBER(Value, v.Value);
+ }
+
+ // LinearCurveKeyframe
+
+ template
+ inline bool ShouldSerialize(const LinearCurveKeyframe& v, const void* otherObj)
+ {
+ if (!otherObj)
+ return true;
+ const auto other = (const LinearCurveKeyframe*)otherObj;
+ return !(v == *other);
+ }
+ template
+ inline void Serialize(ISerializable::SerializeStream& stream, const LinearCurveKeyframe& v, const void* otherObj)
+ {
+ stream.StartObject();
+ stream.JKEY("Time");
+ Serialize(stream, v.Time, nullptr);
+ stream.JKEY("Value");
+ Serialize(stream, v.Value, nullptr);
+ stream.EndObject();
+ }
+ template
+ inline void Deserialize(ISerializable::DeserializeStream& stream, LinearCurveKeyframe& v, ISerializeModifier* modifier)
+ {
+ DESERIALIZE_MEMBER(Time, v.Time);
+ DESERIALIZE_MEMBER(Value, v.Value);
+ }
+
+ // HermiteCurveKeyframe
+
+ template
+ inline bool ShouldSerialize(const HermiteCurveKeyframe& v, const void* otherObj)
+ {
+ if (!otherObj)
+ return true;
+ const auto other = (const HermiteCurveKeyframe*)otherObj;
+ return !(v == *other);
+ }
+ template
+ inline void Serialize(ISerializable::SerializeStream& stream, const HermiteCurveKeyframe& v, const void* otherObj)
+ {
+ stream.StartObject();
+ stream.JKEY("Time");
+ Serialize(stream, v.Time, nullptr);
+ stream.JKEY("Value");
+ Serialize(stream, v.Value, nullptr);
+ stream.JKEY("TangentIn");
+ Serialize(stream, v.TangentIn, nullptr);
+ stream.JKEY("TangentOut");
+ Serialize(stream, v.TangentOut, nullptr);
+ stream.EndObject();
+ }
+ template
+ inline void Deserialize(ISerializable::DeserializeStream& stream, HermiteCurveKeyframe& v, ISerializeModifier* modifier)
+ {
+ DESERIALIZE_MEMBER(Time, v.Time);
+ DESERIALIZE_MEMBER(Value, v.Value);
+ DESERIALIZE_MEMBER(TangentIn, v.TangentIn);
+ DESERIALIZE_MEMBER(TangentOut, v.TangentOut);
+ }
+
+ // BezierCurveKeyframe
+
+ template
+ inline bool ShouldSerialize(const BezierCurveKeyframe& v, const void* otherObj)
+ {
+ if (!otherObj)
+ return true;
+ const auto other = (const BezierCurveKeyframe*)otherObj;
+ return !(v == *other);
+ }
+ template
+ inline void Serialize(ISerializable::SerializeStream& stream, const BezierCurveKeyframe& v, const void* otherObj)
+ {
+ stream.StartObject();
+ stream.JKEY("Time");
+ Serialize(stream, v.Time, nullptr);
+ stream.JKEY("Value");
+ Serialize(stream, v.Value, nullptr);
+ stream.JKEY("TangentIn");
+ Serialize(stream, v.TangentIn, nullptr);
+ stream.JKEY("TangentOut");
+ Serialize(stream, v.TangentOut, nullptr);
+ stream.EndObject();
+ }
+ template
+ inline void Deserialize(ISerializable::DeserializeStream& stream, BezierCurveKeyframe& v, ISerializeModifier* modifier)
+ {
+ DESERIALIZE_MEMBER(Time, v.Time);
+ DESERIALIZE_MEMBER(Value, v.Value);
+ DESERIALIZE_MEMBER(TangentIn, v.TangentIn);
+ DESERIALIZE_MEMBER(TangentOut, v.TangentOut);
+ }
+
+ // Curve
+
+ template
+ inline bool ShouldSerialize(const Curve& v, const void* otherObj)
+ {
+ if (!otherObj)
+ return true;
+ const auto other = (const Curve*)otherObj;
+ return !(v == *other);
+ }
+ template
+ inline void Serialize(ISerializable::SerializeStream& stream, const Curve& v, const void* otherObj)
+ {
+ stream.StartObject();
+ stream.JKEY("Keyframes");
+ stream.StartArray();
+ for (auto& k : v.GetKeyframes())
+ Serialize(stream, k, nullptr);
+ stream.EndArray();
+ stream.EndObject();
+ }
+ template
+ inline void Deserialize(ISerializable::DeserializeStream& stream, Curve& v, ISerializeModifier* modifier)
+ {
+ if (!stream.IsObject())
+ return;
+ const auto mKeyframes = SERIALIZE_FIND_MEMBER(stream, "Keyframes");
+ if (mKeyframes != stream.MemberEnd())
+ {
+ const auto& keyframesArray = mKeyframes->value.GetArray();
+ auto& keyframes = v.GetKeyframes();
+ keyframes.Resize(keyframesArray.Size());
+ for (rapidjson::SizeType i = 0; i < keyframesArray.Size(); i++)
+ Deserialize(keyframesArray[i], keyframes[i], modifier);
+ }
+ }
+}
+
+// @formatter:on
diff --git a/Source/Engine/Core/Math/Color.h b/Source/Engine/Core/Math/Color.h
index 3eee6d9f8..5a7986517 100644
--- a/Source/Engine/Core/Math/Color.h
+++ b/Source/Engine/Core/Math/Color.h
@@ -506,6 +506,14 @@ inline Color operator*(float a, const Color& b)
return b * a;
}
+namespace Math
+{
+ FORCE_INLINE static bool NearEqual(const Color& a, const Color& b)
+ {
+ return Color::NearEqual(a, b);
+ }
+}
+
template<>
struct TIsPODType
{
diff --git a/Source/Engine/Core/Math/Color32.h b/Source/Engine/Core/Math/Color32.h
index 89f64cc35..f848f6f73 100644
--- a/Source/Engine/Core/Math/Color32.h
+++ b/Source/Engine/Core/Math/Color32.h
@@ -231,6 +231,14 @@ inline Color32 operator*(float a, const Color32& b)
return b * a;
}
+namespace Math
+{
+ FORCE_INLINE static bool NearEqual(const Color32& a, const Color32& b)
+ {
+ return a == b;
+ }
+}
+
template<>
struct TIsPODType
{
diff --git a/Source/Engine/Core/Math/Transform.h b/Source/Engine/Core/Math/Transform.h
index ed19efeae..c5e67c986 100644
--- a/Source/Engine/Core/Math/Transform.h
+++ b/Source/Engine/Core/Math/Transform.h
@@ -293,6 +293,14 @@ public:
static void Lerp(const Transform& t1, const Transform& t2, float amount, Transform& result);
};
+namespace Math
+{
+ FORCE_INLINE static bool NearEqual(const Transform& a, const Transform& b)
+ {
+ return Transform::NearEqual(a, b);
+ }
+}
+
template<>
struct TIsPODType
{
diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h
index adca955fc..c7771aa67 100644
--- a/Source/Engine/Core/Math/Vector4.h
+++ b/Source/Engine/Core/Math/Vector4.h
@@ -379,7 +379,7 @@ public:
static bool NearEqual(const Vector4& a, const Vector4& b)
{
- return Math::NearEqual(a.X, b.X) && Math::NearEqual(a.Y, b.Y) & Math::NearEqual(a.Z, b.Z) && Math::NearEqual(a.W, b.W);
+ return Math::NearEqual(a.X, b.X) && Math::NearEqual(a.Y, b.Y) && Math::NearEqual(a.Z, b.Z) && Math::NearEqual(a.W, b.W);
}
static bool NearEqual(const Vector4& a, const Vector4& b, float epsilon)
diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h
index 63c6c4807..c431429fd 100644
--- a/Source/Engine/Level/Actors/ModelInstanceActor.h
+++ b/Source/Engine/Level/Actors/ModelInstanceActor.h
@@ -42,7 +42,7 @@ public:
/// Sets the material to the entry slot. Can be used to override the material of the meshes using this slot.
///
/// The material slot entry index.
- /// The material to set..
+ /// The material to set.
API_FUNCTION() void SetMaterial(int32 entryIndex, MaterialBase* material);
///
diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp
new file mode 100644
index 000000000..509c2f389
--- /dev/null
+++ b/Source/Engine/Level/Actors/Spline.cpp
@@ -0,0 +1,335 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#include "Spline.h"
+#include "Engine/Serialization/Serialization.h"
+#include "Engine/Animations/CurveSerialization.h"
+#include
+
+Spline::Spline(const SpawnParams& params)
+ : Actor(params)
+{
+}
+
+void Spline::SetIsLoop(bool value)
+{
+ if (_loop != value)
+ {
+ _loop = value;
+ UpdateSpline();
+ }
+}
+
+Vector3 Spline::GetSplinePoint(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return _transform.LocalToWorld(t.Translation);
+}
+
+Vector3 Spline::GetSplineLocalPoint(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return t.Translation;
+}
+
+Quaternion Spline::GetSplineOrientation(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ Quaternion::Multiply(_transform.Orientation, t.Orientation, t.Orientation);
+ t.Orientation.Normalize();
+ return t.Orientation;
+}
+
+Quaternion Spline::GetSplineLocalOrientation(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return t.Orientation;
+}
+
+Vector3 Spline::GetSplineScale(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return _transform.Scale * t.Scale;
+}
+
+Vector3 Spline::GetSplineLocalScale(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return t.Scale;
+}
+
+Transform Spline::GetSplineTransform(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return _transform.LocalToWorld(t);
+}
+
+Transform Spline::GetSplineLocalTransform(float time) const
+{
+ Transform t;
+ Curve.Evaluate(t, time, _loop);
+ return t;
+}
+
+Vector3 Spline::GetSplinePoint(int32 index) const
+{
+ CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), Vector3::Zero)
+ return _transform.LocalToWorld(Curve[index].Value.Translation);
+}
+
+Vector3 Spline::GetSplineLocalPoint(int32 index) const
+{
+ CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), Vector3::Zero)
+ return Curve[index].Value.Translation;
+}
+
+Transform Spline::GetSplineTransform(int32 index) const
+{
+ CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), Vector3::Zero)
+ return _transform.LocalToWorld(Curve[index].Value);
+}
+
+Transform Spline::GetSplineLocalTransform(int32 index) const
+{
+ CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), Vector3::Zero)
+ return Curve[index].Value;
+}
+
+int32 Spline::GetSplinePointsCount() const
+{
+ return Curve.GetKeyframes().Count();
+}
+
+float Spline::GetSplineDuration() const
+{
+ return Curve.GetLength();
+}
+
+float Spline::GetSplineLength() const
+{
+ float sum = 0.0f;
+ for (int32 i = 1; i < Curve.GetKeyframes().Count(); i++)
+ sum += Vector3::DistanceSquared(Curve[i - 1].Value.Translation * _transform.Scale, Curve[i].Value.Translation * _transform.Scale);
+ return Math::Sqrt(sum);
+}
+
+namespace
+{
+ void FindTimeClosestToPoint(const Vector3& point, const Spline::Keyframe& start, const Spline::Keyframe& end, float& bestDistanceSquared, float& bestTime)
+ {
+ // TODO: implement sth more analytical than brute-force solution
+ const int32 slices = 100;
+ const float step = 1.0f / (float)slices;
+ const float length = Math::Abs(end.Time - start.Time);
+ for (int32 i = 0; i <= slices; i++)
+ {
+ const float t = (float)i * step;
+ Transform result;
+ Spline::Keyframe::Interpolate(start, end, t, length, result);
+ const float distanceSquared = Vector3::DistanceSquared(point, result.Translation);
+ if (distanceSquared < bestDistanceSquared)
+ {
+ bestDistanceSquared = distanceSquared;
+ bestTime = start.Time + t * length;
+ }
+ }
+ }
+}
+
+float Spline::GetSplineTimeClosestToPoint(const Vector3& point) const
+{
+ const int32 pointsCount = Curve.GetKeyframes().Count();
+ if (pointsCount == 0)
+ return 0.0f;
+ if (pointsCount == 1)
+ return Curve[0].Time;
+ const Vector3 localPoint = _transform.WorldToLocal(point);
+ float bestDistanceSquared = MAX_float;
+ float bestTime = 0.0f;
+ for (int32 i = 1; i < pointsCount; i++)
+ FindTimeClosestToPoint(localPoint, Curve[i - 1], Curve[i], bestDistanceSquared, bestTime);
+ if (_loop)
+ FindTimeClosestToPoint(localPoint, Curve[pointsCount - 1], Curve[0], bestDistanceSquared, bestTime);
+ return bestTime;
+}
+
+Vector3 Spline::GetSplinePointClosestToPoint(const Vector3& point) const
+{
+ return GetSplinePoint(GetSplineTimeClosestToPoint(point));
+}
+
+void Spline::GetSplinePoints(Array& points) const
+{
+ for (auto& e : Curve.GetKeyframes())
+ points.Add(_transform.LocalToWorld(e.Value.Translation));
+}
+
+void Spline::GetSplineLocalPoints(Array& points) const
+{
+ for (auto& e : Curve.GetKeyframes())
+ points.Add(e.Value.Translation);
+}
+
+void Spline::GetSplinePoints(Array& points) const
+{
+ for (auto& e : Curve.GetKeyframes())
+ points.Add(_transform.LocalToWorld(e.Value));
+}
+
+void Spline::GetSplineLocalPoints(Array& points) const
+{
+ for (auto& e : Curve.GetKeyframes())
+ points.Add(e.Value);
+}
+
+void Spline::ClearSpline()
+{
+ if (Curve.IsEmpty())
+ return;
+ Curve.Clear();
+ UpdateSpline();
+}
+
+void Spline::AddSplinePoint(const Vector3& point, bool updateSpline)
+{
+ const Keyframe k(Curve.IsEmpty() ? 0.0f : Curve.GetKeyframes().Last().Time + 1.0f, Transform(point));
+ Curve.GetKeyframes().Add(k);
+ if (updateSpline)
+ UpdateSpline();
+}
+
+void Spline::AddSplineLocalPoint(const Vector3& point, bool updateSpline)
+{
+ const Keyframe k(Curve.IsEmpty() ? 0.0f : Curve.GetKeyframes().Last().Time + 1.0f, Transform(_transform.WorldToLocal(point)));
+ Curve.GetKeyframes().Add(k);
+ if (updateSpline)
+ UpdateSpline();
+}
+
+void Spline::AddSplinePoint(const Transform& point, bool updateSpline)
+{
+ const Keyframe k(Curve.IsEmpty() ? 0.0f : Curve.GetKeyframes().Last().Time + 1.0f, _transform.WorldToLocal(point));
+ Curve.GetKeyframes().Add(k);
+ if (updateSpline)
+ UpdateSpline();
+}
+
+void Spline::AddSplineLocalPoint(const Transform& point, bool updateSpline)
+{
+ const Keyframe k(Curve.IsEmpty() ? 0.0f : Curve.GetKeyframes().Last().Time + 1.0f, point);
+ Curve.GetKeyframes().Add(k);
+ if (updateSpline)
+ UpdateSpline();
+}
+
+void Spline::UpdateSpline()
+{
+}
+
+void Spline::GetKeyframes(MonoArray* data)
+{
+ Platform::MemoryCopy(mono_array_addr_with_size(data, sizeof(Keyframe), 0), Curve.GetKeyframes().Get(), sizeof(Keyframe) * Curve.GetKeyframes().Count());
+}
+
+void Spline::SetKeyframes(MonoArray* data)
+{
+ const auto count = (int32)mono_array_length(data);
+ Curve.GetKeyframes().Resize(count, false);
+ Platform::MemoryCopy(Curve.GetKeyframes().Get(), mono_array_addr_with_size(data, sizeof(Keyframe), 0), sizeof(Keyframe) * count);
+ UpdateSpline();
+}
+
+#if USE_EDITOR
+
+#include "Engine/Debug/DebugDraw.h"
+
+namespace
+{
+ void DrawSegment(Spline* spline, int32 start, int32 end, const Color& color, const Transform& transform, bool depthTest)
+ {
+ const auto& startKey = spline->Curve[start];
+ const auto& endKey = spline->Curve[end];
+ const Vector3 startPos = transform.LocalToWorld(startKey.Value.Translation);
+ const Vector3 startTangent = transform.LocalToWorld(startKey.TangentOut.Translation);
+ const Vector3 endPos = transform.LocalToWorld(endKey.Value.Translation);
+ const Vector3 endTangent = transform.LocalToWorld(endKey.TangentIn.Translation);
+ const float d = (endKey.Time - startKey.Time) / 3.0f;
+ DEBUG_DRAW_BEZIER(startPos, startPos + startTangent * d, endPos + endTangent * d, endPos, color, 0.0f, depthTest);
+ }
+
+ void DrawSpline(Spline* spline, const Color& color, const Transform& transform, bool depthTest)
+ {
+ const int32 count = spline->Curve.GetKeyframes().Count();
+ for (int32 i = 0; i < count; i++)
+ {
+ Vector3 p = transform.LocalToWorld(spline->Curve[i].Value.Translation);
+ DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(p, 5.0f), color, 0.0f, true);
+ if (i != 0)
+ DrawSegment(spline, i - 1, i, color, transform, depthTest);
+ }
+ if (spline->GetIsLoop() && count > 1)
+ DrawSegment(spline, count - 1, 0, color, transform, depthTest);
+ }
+}
+
+void Spline::OnDebugDraw()
+{
+ const Color color = GetSplineColor();
+ DrawSpline(this, color.AlphaMultiplied(0.7f), _transform, true);
+
+ // Base
+ Actor::OnDebugDraw();
+}
+
+void Spline::OnDebugDrawSelected()
+{
+ const Color color = GetSplineColor();
+ DrawSpline(this, color.AlphaMultiplied(0.3f), _transform, false);
+ DrawSpline(this, color, _transform, true);
+
+ // Base
+ Actor::OnDebugDrawSelected();
+}
+
+#endif
+
+void Spline::Serialize(SerializeStream& stream, const void* otherObj)
+{
+ // Base
+ Actor::Serialize(stream, otherObj);
+
+ SERIALIZE_GET_OTHER_OBJ(Spline);
+
+ SERIALIZE_MEMBER(IsLoop, _loop);
+ SERIALIZE(Curve);
+}
+
+void Spline::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
+{
+ // Base
+ Actor::Deserialize(stream, modifier);
+
+ DESERIALIZE_MEMBER(IsLoop, _loop);
+ DESERIALIZE(Curve);
+
+ // Initialize spline when loading data during gameplay
+ if (IsDuringPlay())
+ {
+ UpdateSpline();
+ }
+}
+
+void Spline::OnEnable()
+{
+ // Base
+ Actor::OnEnable();
+
+ // Initialize spline
+ UpdateSpline();
+}
diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h
new file mode 100644
index 000000000..a94804ea7
--- /dev/null
+++ b/Source/Engine/Level/Actors/Spline.h
@@ -0,0 +1,247 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "../Actor.h"
+#include "Engine/Animations/Curve.h"
+
+///
+/// Spline shape actor that defines spatial curve with utility functions for general purpose usage.
+///
+API_CLASS() class FLAXENGINE_API Spline : public Actor
+{
+DECLARE_SCENE_OBJECT(Spline);
+ typedef BezierCurveKeyframe Keyframe;
+private:
+
+ bool _loop = false;
+
+public:
+
+ ///
+ /// The spline bezier curve points represented as series of transformations in 3D space (with tangents). Points are stored in local-space of the actor.
+ ///
+ /// Ensure to call UpdateSpline() after editing curve to reflect the changes.
+ BezierCurve Curve;
+
+ ///
+ /// Whether to use spline as closed loop.
+ ///
+ API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Spline\")")
+ FORCE_INLINE bool GetIsLoop() const
+ {
+ return _loop;
+ }
+
+ ///
+ /// Whether to use spline as closed loop.
+ ///
+ API_PROPERTY() void SetIsLoop(bool value);
+
+public:
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point location in 3D (world-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point location (world-space).
+ API_FUNCTION() Vector3 GetSplinePoint(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point location in 3D (local-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point location (local-space).
+ API_FUNCTION() Vector3 GetSplineLocalPoint(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point rotation in 3D (world-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point rotation (world-space).
+ API_FUNCTION() Quaternion GetSplineOrientation(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point rotation in 3D (local-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point rotation (local-space).
+ API_FUNCTION() Quaternion GetSplineLocalOrientation(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point scale in 3D (world-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point scale (world-space).
+ API_FUNCTION() Vector3 GetSplineScale(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the point scale in 3D (local-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point scale (local-space).
+ API_FUNCTION() Vector3 GetSplineLocalScale(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the transformation in 3D (world-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point transformation (world-space).
+ API_FUNCTION() Transform GetSplineTransform(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given time and calculates the transformation in 3D (local-space).
+ ///
+ /// The time value. Can be negative or larger than curve length (curve will loop or clamp).
+ /// The calculated curve point transformation (local-space).
+ API_FUNCTION() Transform GetSplineLocalTransform(float time) const;
+
+ ///
+ /// Evaluates the spline curve at the given index (world-space).
+ ///
+ /// The curve keyframe index. Zero-based, smaller than GetSplinePointsCount().
+ /// The curve point location (world-space).
+ API_FUNCTION() Vector3 GetSplinePoint(int32 index) const;
+
+ ///
+ /// Evaluates the spline curve at the given index (local-space).
+ ///
+ /// The curve keyframe index. Zero-based, smaller than GetSplinePointsCount().
+ /// The curve point location (local-space).
+ API_FUNCTION() Vector3 GetSplineLocalPoint(int32 index) const;
+
+ ///
+ /// Evaluates the spline curve at the given index (world-space).
+ ///
+ /// The curve keyframe index. Zero-based, smaller than GetSplinePointsCount().
+ /// The curve point transformation (world-space).
+ API_FUNCTION() Transform GetSplineTransform(int32 index) const;
+
+ ///
+ /// Evaluates the spline curve at the given index (local-space).
+ ///
+ /// The curve keyframe index. Zero-based, smaller than GetSplinePointsCount().
+ /// The curve point transformation (local-space).
+ API_FUNCTION() Transform GetSplineLocalTransform(int32 index) const;
+
+ ///
+ /// Gets the amount of points in the spline.
+ ///
+ API_PROPERTY() int32 GetSplinePointsCount() const;
+
+ ///
+ /// Gets the total duration of the spline curve (time of the last point).
+ ///
+ API_PROPERTY() float GetSplineDuration() const;
+
+ ///
+ /// Gets the total length of the spline curve (distance between all the points).
+ ///
+ API_PROPERTY() float GetSplineLength() const;
+
+ ///
+ /// Calculates the closest point to the given location and returns the spline time at that point.
+ ///
+ /// The point in world-space to find the spline point that is closest to it.
+ /// The spline time.
+ API_FUNCTION() float GetSplineTimeClosestToPoint(const Vector3& point) const;
+
+ ///
+ /// Calculates the closest point to the given location.
+ ///
+ /// The point in world-space to find the spline point that is closest to it.
+ /// The spline position.
+ API_FUNCTION() Vector3 GetSplinePointClosestToPoint(const Vector3& point) const;
+
+ ///
+ /// Gets the spline curve points list (world-space).
+ ///
+ /// The result points collection.
+ API_FUNCTION() void GetSplinePoints(API_PARAM(Out) Array& points) const;
+
+ ///
+ /// Gets the spline curve points list (local-space).
+ ///
+ /// The result points collection.
+ API_FUNCTION() void GetSplineLocalPoints(API_PARAM(Out) Array& points) const;
+
+ ///
+ /// Gets the spline curve points list (world-space).
+ ///
+ /// The result points collection.
+ API_FUNCTION() void GetSplinePoints(API_PARAM(Out) Array& points) const;
+
+ ///
+ /// Gets the spline curve points list (local-space).
+ ///
+ /// The result points collection.
+ API_FUNCTION() void GetSplineLocalPoints(API_PARAM(Out) Array& points) const;
+
+public:
+
+ ///
+ /// Clears the spline to be empty.
+ ///
+ API_FUNCTION() void ClearSpline();
+
+ ///
+ /// Adds the point to the spline curve (at the end).
+ ///
+ /// The location of the point to add to the curve (world-space).
+ /// True if update spline after adding the point, otherwise false.
+ API_FUNCTION() void AddSplinePoint(const Vector3& point, bool updateSpline = true);
+
+ ///
+ /// Adds the point to the spline curve (at the end).
+ ///
+ /// The location of the point to add to the curve (local-space).
+ /// True if update spline after adding the point, otherwise false.
+ API_FUNCTION() void AddSplineLocalPoint(const Vector3& point, bool updateSpline = true);
+
+ ///
+ /// Adds the point to the spline curve (at the end).
+ ///
+ /// The transformation of the point to add to the curve (world-space).
+ /// True if update spline after adding the point, otherwise false.
+ API_FUNCTION() void AddSplinePoint(const Transform& point, bool updateSpline = true);
+
+ ///
+ /// Adds the point to the spline curve (at the end).
+ ///
+ /// The transformation of the point to add to the curve (local-space).
+ /// True if update spline after adding the point, otherwise false.
+ API_FUNCTION() void AddSplineLocalPoint(const Transform& point, bool updateSpline = true);
+
+public:
+
+ ///
+ /// Updates the spline after it was modified. Recreates the collision and/or any cached state that depends on the spline type.
+ ///
+ API_FUNCTION() virtual void UpdateSpline();
+
+protected:
+
+#if USE_EDITOR
+ virtual Color GetSplineColor()
+ {
+ return Color::White;
+ }
+#endif
+
+private:
+
+ // Internal bindings
+ API_FUNCTION(NoProxy) void GetKeyframes(MonoArray* data);
+ API_FUNCTION(NoProxy) void SetKeyframes(MonoArray* data);
+
+public:
+
+ // [Actor]
+#if USE_EDITOR
+ void OnDebugDraw() override;
+ void OnDebugDrawSelected() override;
+#endif
+ void Serialize(SerializeStream& stream, const void* otherObj) override;
+ void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
+ void OnEnable() override;
+};
diff --git a/Source/Engine/Level/Spline.cs b/Source/Engine/Level/Spline.cs
new file mode 100644
index 000000000..46a05cd5d
--- /dev/null
+++ b/Source/Engine/Level/Spline.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace FlaxEngine
+{
+ partial class Spline
+ {
+ private BezierCurve.Keyframe[] _keyframes;
+
+ ///
+ /// Gets or sets the spline keyframes collection.
+ ///
+ [Unmanaged]
+ [Tooltip("Spline keyframes collection.")]
+ [EditorOrder(10), EditorDisplay("Spline"), Collection(CanReorderItems = false)]
+ public BezierCurve.Keyframe[] SplineKeyframes
+ {
+ get
+ {
+ var count = SplinePointsCount;
+ if (_keyframes == null || _keyframes.Length != count)
+ _keyframes = new BezierCurve.Keyframe[count];
+#if !BUILD_RELEASE
+ if (Marshal.SizeOf(typeof(BezierCurve.Keyframe)) != Transform.SizeInBytes * 3 + sizeof(float))
+ throw new Exception("Invalid size of BezierCurve keyframe " + Marshal.SizeOf(typeof(BezierCurve.Keyframe)) + " bytes.");
+#endif
+ Internal_GetKeyframes(__unmanagedPtr, _keyframes);
+ return _keyframes;
+ }
+ set
+ {
+ if (value == null)
+ value = Utils.GetEmptyArray.Keyframe>();
+ _keyframes = null;
+ Internal_SetKeyframes(__unmanagedPtr, value);
+ }
+ }
+ }
+}