diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f862292..a0165bc78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - `AdaptiveGraphRouting.ErrorMessages` +- `Elements.Animation` +- `Element.Animation` ### Changed diff --git a/Elements/src/Animation.cs b/Elements/src/Animation.cs new file mode 100644 index 000000000..cf410ce50 --- /dev/null +++ b/Elements/src/Animation.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Elements +{ + /// + /// The animation of a transform expressed using glTF convention. + /// + public class Animation + { + readonly List _scaleTimes = new List(); + readonly List _scales = new List(); + readonly List _translationTimes = new List(); + readonly List _translations = new List(); + readonly List _rotationTimes = new List(); + readonly List _rotations = new List(); + readonly float[] _scaleMin; + readonly float[] _scaleMax; + float _scaleTimeMin; + float _scaleTimeMax; + readonly float[] _translationMin; + readonly float[] _translationMax; + float _translationTimeMin; + float _translationTimeMax; + readonly float[] _rotationMin; + readonly float[] _rotationMax; + float _rotationTimeMin; + float _rotationTimeMax; + + /// + /// An array of bytes representing the scale keys. + /// + public byte[] Scales => _scales.ToArray(); + + /// + /// An array of bytes representing the scale time keys. + /// + public byte[] ScaleTimes => _scaleTimes.ToArray(); + + /// + /// The minimum scale time. + /// + public float ScaleTimeMin => _scaleTimeMin; + + /// + /// The maximum scale time. + /// + public float ScaleTimeMax => _scaleTimeMax; + + /// + /// The minimum scale as [x,y,z]. + /// + public float[] ScaleMin => _scaleMin; + + /// + /// The maximum scale as [x,y,z]; + /// + public float[] ScaleMax => _scaleMax; + + /// + /// An array of bytes representing translation keys. + /// + public byte[] Translations => _translations.ToArray(); + + /// + /// An array of bytes representing translation time keys. + /// + public byte[] TranslationTimes => _translationTimes.ToArray(); + + /// + /// The minimum translation time. + /// + public float TranslationTimeMin => _translationTimeMin; + + /// + /// The maximum translation time. + /// + public float TranslationTimeMax => _translationTimeMax; + + /// + /// The minimum translation as [x,y,z]. + /// + public float[] TranslationMin => _translationMin; + + /// + /// The maximum translation as [x,y,z]. + /// + public float[] TranslationMax => _translationMax; + + /// + /// An array of bytes representing rotation keys. + /// + public byte[] Rotations => _rotations.ToArray(); + + /// + /// An array of bytes representing rotation time keys. + /// + public byte[] RotationTimes => _rotationTimes.ToArray(); + + /// + /// The minimum rotation time. + /// + public float RotationTimeMin => _rotationTimeMin; + + /// + /// The maximum rotation time. + /// + public float RotationTimeMax => _rotationTimeMax; + + /// + /// The minimum rotation as [x,y,z,w]. + /// + public float[] RotationMin => _rotationMin; + + /// + /// The maximum rotation as [x,y,z,w]. + /// + public float[] RotationMax => _rotationMax; + + /// + /// Create an animation for an element. + /// + public Animation() + { + _scaleMin = new float[3] { float.MaxValue, float.MaxValue, float.MaxValue }; + _scaleMax = new float[3] { float.MinValue, float.MinValue, float.MinValue }; + _scaleTimeMin = float.MaxValue; + _scaleTimeMax = float.MinValue; + + _translationMin = new float[3] { float.MaxValue, float.MaxValue, float.MaxValue }; + _translationMax = new float[3] { float.MinValue, float.MinValue, float.MinValue }; + _translationTimeMin = float.MaxValue; + _translationTimeMax = float.MinValue; + + _rotationMin = new float[4] { float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue }; + _rotationMax = new float[4] { float.MinValue, float.MinValue, float.MinValue, float.MinValue }; + _rotationTimeMin = float.MaxValue; + _rotationTimeMax = float.MinValue; + } + + /// + /// Add a scale keyframe. + /// + /// The scale value. + /// The time at which to apply the scale value. + public void AddScaleKeyframe(Geometry.Vector3 scale, double timeSeconds) + { + _scales.AddRange(BitConverter.GetBytes((float)scale.X)); + _scales.AddRange(BitConverter.GetBytes((float)scale.Y)); + _scales.AddRange(BitConverter.GetBytes((float)scale.Z)); + + _scaleTimes.AddRange(BitConverter.GetBytes((float)timeSeconds)); + + _scaleMin[0] = Math.Min(_scaleMin[0], (float)scale.X); + _scaleMin[1] = Math.Min(_scaleMin[1], (float)scale.Y); + _scaleMin[2] = Math.Min(_scaleMin[2], (float)scale.Z); + + _scaleMax[0] = Math.Max(_scaleMax[0], (float)scale.X); + _scaleMax[1] = Math.Max(_scaleMax[1], (float)scale.Y); + _scaleMax[2] = Math.Max(_scaleMax[2], (float)scale.Z); + + _scaleTimeMin = Math.Min(_scaleTimeMin, (float)timeSeconds); + _scaleTimeMax = Math.Max(_scaleTimeMax, (float)timeSeconds); + } + + /// + /// Add a rotation keyframe. + /// + /// The axis of rotation. + /// The angle of rotation in degrees. + /// The keyframe time. + public void AddRotationKeyframe(Geometry.Vector3 axis, double angleDegrees, double timeSeconds) + { + var rotation = new Quaternion(new Vector3((float)axis.X, (float)axis.Y, (float)axis.Z), (float)Units.DegreesToRadians(angleDegrees)); + rotation = Quaternion.Normalize(rotation); + + _rotations.AddRange(BitConverter.GetBytes(rotation.X)); + _rotations.AddRange(BitConverter.GetBytes(rotation.Y)); + _rotations.AddRange(BitConverter.GetBytes(rotation.Z)); + _rotations.AddRange(BitConverter.GetBytes(rotation.W)); + + _rotationTimes.AddRange(BitConverter.GetBytes((float)timeSeconds)); + + _rotationMin[0] = Math.Min(_rotationMin[0], rotation.X); + _rotationMin[1] = Math.Min(_rotationMin[1], rotation.Y); + _rotationMin[2] = Math.Min(_rotationMin[2], rotation.Z); + _rotationMin[3] = Math.Min(_rotationMin[3], rotation.W); + + _rotationMax[0] = Math.Max(_rotationMax[0], rotation.X); + _rotationMax[1] = Math.Max(_rotationMax[1], rotation.Y); + _rotationMax[2] = Math.Max(_rotationMax[2], rotation.Z); + _rotationMax[3] = Math.Max(_rotationMax[3], rotation.W); + + _rotationTimeMin = Math.Min(_rotationTimeMin, (float)timeSeconds); + _rotationTimeMax = Math.Max(_rotationTimeMax, (float)timeSeconds); + } + + /// + /// Add a translation keyframe. + /// + /// The translation value. + /// The time at which to apply the translation. + public void AddTranslationKeyframe(Geometry.Vector3 translation, double timeSeconds) + { + _translations.AddRange(BitConverter.GetBytes((float)translation.X)); + _translations.AddRange(BitConverter.GetBytes((float)translation.Y)); + _translations.AddRange(BitConverter.GetBytes((float)translation.Z)); + + _translationTimes.AddRange(BitConverter.GetBytes((float)timeSeconds)); + + _translationMin[0] = Math.Min(_translationMin[0], (float)translation.X); + _translationMin[1] = Math.Min(_translationMin[1], (float)translation.Y); + _translationMin[2] = Math.Min(_translationMin[2], (float)translation.Z); + + _translationMax[0] = Math.Max(_translationMax[0], (float)translation.X); + _translationMax[1] = Math.Max(_translationMax[1], (float)translation.Y); + _translationMax[2] = Math.Max(_translationMax[2], (float)translation.Z); + + _translationTimeMin = Math.Min(_translationTimeMin, (float)timeSeconds); + _translationTimeMax = Math.Max(_translationTimeMax, (float)timeSeconds); + } + + /// + /// Is the scale of the element animated? + /// + public bool HasAnimatedScale() + { + return _scales.Count > 0; + } + + /// + /// Is the translation of the element animated? + /// + public bool HasAnimatedTranslation() + { + return _translations.Count > 0; + } + + /// + /// Is the rotation of the element animated? + /// + public bool HasAnimatedRotation() + { + return _rotations.Count > 0; + } + } +} \ No newline at end of file diff --git a/Elements/src/GeometricElement.cs b/Elements/src/GeometricElement.cs index 06c799ce5..79374ca73 100644 --- a/Elements/src/GeometricElement.cs +++ b/Elements/src/GeometricElement.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using Elements.Geometry; using Elements.Geometry.Solids; @@ -25,10 +24,16 @@ public class GeometricElement : Element internal BBox3 _bounds; internal Csg.Solid _csg; + /// + /// The element's animation. + /// + [JsonIgnore] + public Animation Animation { get; set; } + /// /// The element's bounds. /// The bounds are only available when the geometry has been - /// updated using UpdateBoundsAndComputeSolid(), + /// updated using UpdateBoundsAndComputeSolid(). /// [JsonIgnore] public BBox3 Bounds => _bounds; diff --git a/Elements/src/Serialization/glTF/GltfExtensions.cs b/Elements/src/Serialization/glTF/GltfExtensions.cs index dea2ff7d7..a93bfd6ff 100644 --- a/Elements/src/Serialization/glTF/GltfExtensions.cs +++ b/Elements/src/Serialization/glTF/GltfExtensions.cs @@ -16,6 +16,8 @@ using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp; using Image = glTFLoader.Schema.Image; +using Vector3 = Elements.Geometry.Vector3; +using Quaternion = Elements.Geometry.Quaternion; using System.Reflection; [assembly: InternalsVisibleTo("Hypar.Elements.Tests")] @@ -498,7 +500,14 @@ internal static void AddLights(this Gltf gltf, List lights, List no } } - private static int AddAccessor(List accessors, int bufferView, int byteOffset, Accessor.ComponentTypeEnum componentType, int count, float[] min, float[] max, Accessor.TypeEnum accessorType) + private static int AddAccessor(List accessors, + int bufferView, + int byteOffset, + Accessor.ComponentTypeEnum componentType, + int count, + float[] min, + float[] max, + Accessor.TypeEnum accessorType) { var a = new Accessor { @@ -991,6 +1000,7 @@ internal static Gltf InitializeGlTF(Model model, var textures = new List(); var images = new List(); var samplers = new List(); + var animations = new List(); var materials = gltf.Materials != null ? gltf.Materials.ToList() : new List(); var meshElementMap = new Dictionary>(); @@ -1028,6 +1038,7 @@ internal static Gltf InitializeGlTF(Model model, meshTransformMap, currLines, drawEdges, + animations, mergeVertices); } catch (Exception ex) @@ -1090,6 +1101,10 @@ internal static Gltf InitializeGlTF(Model model, { gltf.Samplers = samplers.ToArray(samplers.Count); } + if (animations.Count > 0) + { + gltf.Animations = animations.ToArray(animations.Count); + } gltf.Nodes = nodes.ToArray(nodes.Count); if (meshes.Count > 0) { @@ -1128,10 +1143,12 @@ private static void GetRenderDataForElement(Element e, Dictionary meshTransformMap, List lines, bool drawEdges, + List animations, bool mergeVertices = false) { var materialId = BuiltInMaterials.Default.Id.ToString(); int meshId = -1; + int nodeId = -1; if (e is GeometricElement element) { @@ -1189,7 +1206,9 @@ private static void GetRenderDataForElement(Element e, nodes, materialId, ref meshId, - content); + content, + out nodeId, + mergeVertices); if (!meshElementMap.ContainsKey(e.Id)) { meshElementMap.Add(e.Id, new List { meshId }); @@ -1210,7 +1229,9 @@ private static void GetRenderDataForElement(Element e, nodes, materialId, ref meshId, - geometricElement); + geometricElement, + out nodeId, + mergeVertices); if (meshId > -1 && !meshElementMap.ContainsKey(e.Id)) { meshElementMap.Add(e.Id, new List { meshId }); @@ -1284,6 +1305,161 @@ private static void GetRenderDataForElement(Element e, nodes, ge.Transform); } + + if (ge.Animation != null && nodeId != -1) + { + // https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_007_Animations.md + + var channels = new List(); + var animationSamplers = new List(); + + if (ge.Animation.HasAnimatedScale()) + { + var scaleTimeBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.ScaleTimes.Length, null, null); + buffer.AddRange(ge.Animation.ScaleTimes); + var scaleTimeAccessor = AddAccessor(accessors, + scaleTimeBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.ScaleTimes.Length / sizeof(float), + new float[] { ge.Animation.ScaleTimeMin }, + new float[] { ge.Animation.ScaleTimeMax }, + Accessor.TypeEnum.SCALAR); + + var scaleBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.Scales.Length, null, null); + buffer.AddRange(ge.Animation.Scales); + var scaleAccessor = AddAccessor(accessors, + scaleBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.Scales.Length / sizeof(float) / 3, + ge.Animation.ScaleMin, + ge.Animation.ScaleMax, + Accessor.TypeEnum.VEC3); + + var scaleSampler = new AnimationSampler + { + Input = scaleTimeAccessor, // time + Output = scaleAccessor, // scale + Interpolation = AnimationSampler.InterpolationEnum.LINEAR + }; + + var scaleTarget = new AnimationChannelTarget + { + Node = nodeId, + Path = AnimationChannelTarget.PathEnum.scale + }; + + var scaleChannel = new AnimationChannel() + { + Target = scaleTarget, + Sampler = animationSamplers.Count + }; + + animationSamplers.Add(scaleSampler); + channels.Add(scaleChannel); + } + if (ge.Animation.HasAnimatedTranslation()) + { + var translationTimeBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.TranslationTimes.Length, null, null); + buffer.AddRange(ge.Animation.TranslationTimes); + var translationTimeAccessor = AddAccessor(accessors, + translationTimeBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.TranslationTimes.Length / sizeof(float), + new float[] { ge.Animation.TranslationTimeMin }, + new float[] { ge.Animation.TranslationTimeMax }, + Accessor.TypeEnum.SCALAR); + + var translationBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.Translations.Length, null, null); + buffer.AddRange(ge.Animation.Translations); + var translationAccessor = AddAccessor(accessors, + translationBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.Translations.Length / sizeof(float) / 3, + ge.Animation.TranslationMin, + ge.Animation.TranslationMax, + Accessor.TypeEnum.VEC3); + + var translationSampler = new AnimationSampler + { + Input = translationTimeAccessor, // time + Output = translationAccessor, // scale + Interpolation = AnimationSampler.InterpolationEnum.LINEAR + }; + + var translationTarget = new AnimationChannelTarget + { + Node = nodeId, + Path = AnimationChannelTarget.PathEnum.translation + }; + + var translationChannel = new AnimationChannel() + { + Target = translationTarget, + Sampler = animationSamplers.Count + }; + + animationSamplers.Add(translationSampler); + channels.Add(translationChannel); + } + if (ge.Animation.HasAnimatedRotation()) + { + var rotationTimeBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.RotationTimes.Length, null, null); + buffer.AddRange(ge.Animation.RotationTimes); + var rotationTimeAccessor = AddAccessor(accessors, + rotationTimeBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.RotationTimes.Length / sizeof(float), + new float[] { ge.Animation.RotationTimeMin }, + new float[] { ge.Animation.RotationTimeMax }, + Accessor.TypeEnum.SCALAR); + + var rotationBufferView = AddBufferView(bufferViews, 0, buffer.Count, ge.Animation.Rotations.Length, null, null); + buffer.AddRange(ge.Animation.Rotations); + var rotationAccessor = AddAccessor(accessors, + rotationBufferView, + 0, + Accessor.ComponentTypeEnum.FLOAT, + ge.Animation.Rotations.Length / sizeof(float) / 4, + ge.Animation.RotationMin, + ge.Animation.RotationMax, + Accessor.TypeEnum.VEC4); + + var rotationSampler = new AnimationSampler + { + Input = rotationTimeAccessor, // time + Output = rotationAccessor, // scale + Interpolation = AnimationSampler.InterpolationEnum.LINEAR + }; + + var rotationTarget = new AnimationChannelTarget + { + Node = nodeId, + Path = AnimationChannelTarget.PathEnum.rotation + }; + + var rotationChannel = new AnimationChannel() + { + Target = rotationTarget, + Sampler = animationSamplers.Count + }; + + animationSamplers.Add(rotationSampler); + channels.Add(rotationChannel); + } + + var anim = new glTFLoader.Schema.Animation() + { + Channels = channels.ToArray(), + Samplers = animationSamplers.ToArray() + }; + + animations.Add(anim); + } } if (e is ITessellate geo) @@ -1316,7 +1492,7 @@ private static void GetRenderDataForElement(Element e, var geom = (GeometricElement)e; if (!geom.IsElementDefinition) { - NodeUtilities.CreateNodeForMesh(meshId, nodes, geom.Transform); + nodeId = NodeUtilities.CreateNodeForMesh(meshId, nodes, geom.Transform); } } } @@ -1428,11 +1604,15 @@ private static int ProcessGeometricRepresentation(Element e, List nodes, string materialId, ref int meshId, - GeometricElement geometricElement) + GeometricElement geometricElement, + out int nodeId, + bool mergeVertices = false) { geometricElement.UpdateRepresentations(); geometricElement.UpdateBoundsAndComputeSolid(); + nodeId = -1; + // TODO: Remove this when we get rid of UpdateRepresentation. // The only reason we don't fully exclude openings from processing // is to ensure that openings have some geometry that will be used @@ -1462,7 +1642,7 @@ private static int ProcessGeometricRepresentation(Element e, if (!geometricElement.IsElementDefinition) { - NodeUtilities.CreateNodeForMesh(meshId, nodes, geometricElement.Transform); + nodeId = NodeUtilities.CreateNodeForMesh(meshId, nodes, geometricElement.Transform); } return meshId; } diff --git a/Elements/src/Serialization/glTF/NodeUtilities.cs b/Elements/src/Serialization/glTF/NodeUtilities.cs index 09e5894dd..13e875652 100644 --- a/Elements/src/Serialization/glTF/NodeUtilities.cs +++ b/Elements/src/Serialization/glTF/NodeUtilities.cs @@ -4,6 +4,8 @@ using Elements.Collections.Generics; using Elements.Geometry; using glTFLoader.Schema; +using System.Numerics; +using Vector3 = Elements.Geometry.Vector3; namespace Elements.Serialization.glTF { @@ -52,13 +54,14 @@ internal static int CreateAndAddTransformNode(List nodes, Transform transf var b = transform.YAxis; var c = transform.ZAxis; - var transNode = new Node(); - - transNode.Matrix = new[]{ + var transNode = new Node + { + Matrix = new[]{ (float)a.X, (float)a.Y, (float)a.Z, 0.0f, (float)b.X, (float)b.Y, (float)b.Z, 0.0f, (float)c.X, (float)c.Y, (float)c.Z, 0.0f, (float)transform.Origin.X,(float)transform.Origin.Y,(float)transform.Origin.Z, 1.0f + } }; parentId = AddNode(nodes, transNode, 0); @@ -67,6 +70,43 @@ internal static int CreateAndAddTransformNode(List nodes, Transform transf return parentId; } + internal static int CreateAndAddRTSNode(List nodes, Transform transform, int parentId) + { + if (transform != null) + { + var transNode = new Node(); + + // HACK: Using the matrix class from System.Numerics + // because it has support for matrix decomposition + // out of the box. + var m = new Matrix4x4((float)transform.Matrix.m11, + (float)transform.Matrix.m12, + (float)transform.Matrix.m13, + 0.0f, + (float)transform.Matrix.m21, + (float)transform.Matrix.m22, + (float)transform.Matrix.m23, + 0.0f, + (float)transform.Matrix.m31, + (float)transform.Matrix.m32, + (float)transform.Matrix.m33, + 0.0f, + (float)transform.Origin.X, + (float)transform.Origin.Y, + (float)transform.Origin.Z, + 1.0f); + + Matrix4x4.Decompose(m, out var scale, out System.Numerics.Quaternion rotation, out var translation); + transNode.Scale = new float[] { scale.X, scale.Y, scale.Z }; + transNode.Translation = new float[] { translation.X, translation.Y, translation.Z }; + transNode.Rotation = new float[] { rotation.X, rotation.Y, rotation.Z, rotation.W }; + + parentId = AddNode(nodes, transNode, 0); + } + + return parentId; + } + internal static void AddInstanceAsCopyOfNode( List nodes, ProtoNode nodeToCopy, @@ -83,9 +123,11 @@ internal static void AddInstanceAsCopyOfNode( // transform, so that the transform can be modified in explore at // runtime (e.g. by a transform override) and have the expected effect. float[] elementTransform = TransformToMatrix(transform); - var newNode = new glTFLoader.Schema.Node(); - newNode.Name = $"{instanceElementId}"; - newNode.Matrix = elementTransform; + var newNode = new glTFLoader.Schema.Node + { + Name = $"{instanceElementId}", + Matrix = elementTransform + }; nodes.Add(newNode); newNode.Children = new[] { nodes.Count }; @@ -95,8 +137,10 @@ internal static void AddInstanceAsCopyOfNode( // back to Y up further up in the node hierarchy. rootTransform.Rotate(new Vector3(1, 0, 0), 90.0); float[] glbOrientationTransform = TransformToMatrix(rootTransform); - var elementOrientationNode = new glTFLoader.Schema.Node(); - elementOrientationNode.Matrix = glbOrientationTransform; + var elementOrientationNode = new glTFLoader.Schema.Node + { + Matrix = glbOrientationTransform + }; nodes.Add(elementOrientationNode); elementOrientationNode.Children = new[] { nodes.Count }; @@ -107,8 +151,10 @@ internal static void AddInstanceAsCopyOfNode( private static int RecursivelyCopyNode(List nodes, ProtoNode nodeToCopy) { - var newNode = new Node(); - newNode.Matrix = nodeToCopy.Matrix; + var newNode = new Node + { + Matrix = nodeToCopy.Matrix + }; if (nodeToCopy.Mesh != null) { newNode.Mesh = nodeToCopy.Mesh; @@ -159,18 +205,21 @@ internal static int CreateNodeForMesh(int meshId, List n { var parentId = 0; - parentId = NodeUtilities.CreateAndAddTransformNode(nodes, transform, parentId); + parentId = CreateAndAddRTSNode(nodes, transform, parentId); // Add mesh node to gltf nodes - var node = new Node(); - node.Mesh = meshId; + var node = new Node + { + Mesh = meshId, + }; + var nodeId = AddNode(nodes, node, parentId); return nodeId; } - internal static void CreateNodeFromNode(List nodes, Node parentNode, Transform transform) + internal static void CreateNodeFromNode(List nodes, Transform transform) { - var parentId = NodeUtilities.CreateAndAddTransformNode(nodes, transform, 0); + CreateAndAddTransformNode(nodes, transform, 0); } } } \ No newline at end of file diff --git a/Elements/test/TransformTests.cs b/Elements/test/TransformTests.cs index 23d9f5d62..caa2ef72e 100644 --- a/Elements/test/TransformTests.cs +++ b/Elements/test/TransformTests.cs @@ -18,9 +18,9 @@ public void Example() var j = 1.0; var count = 10; - for (var i = 0.0; i < 360.0; i += 360.0 / (double)count) + for (var i = 0.0; i < 360.0; i += 360.0 / count) { - var m2 = new Mass(prof, 1.0, new Material($"color_{j}", new Color((float)j - 1.0f, 0.0f, 0.0f, 1.0f)), new Transform()); + var m2 = new Mass(prof, 1.0, new Material($"color_{j}", new Color((float)j - 1.0f, 0.0f, 0.0f, 1.0f))); // Scale the mass. m2.Transform.Scale(new Vector3(j, j, j)); @@ -30,12 +30,49 @@ public void Example() // Rotate the mass. m2.Transform.Rotate(Vector3.ZAxis, i); + this.Model.AddElement(m2); + j += 1.0 / (double)count; } // } + [Fact] + public void Animation() + { + this.Name = "Animation"; + + var m1 = new Mass(Polygon.Rectangle(1.0, 1.0), 1.0, new Material("yellow", Colors.Yellow)); + this.Model.AddElement(m1); + + Profile prof = Polygon.Rectangle(1.0, 1.0); + + var j = 1.0; + var count = 10; + for (var i = 0.0; i < 360.0; i += 360.0 / count) + { + var m2 = new Mass(prof, 1.0, new Material($"color_{j}", new Color((float)j - 1.0f, 0.0f, 0.0f, 1.0f))); + + var t = new Transform(); + t.Move(new Vector3(3, 0, 0)); + t.Rotate(Vector3.ZAxis, i); + + this.Model.AddElement(m2); + + m2.Animation = new Animation(); + m2.Animation.AddRotationKeyframe(Vector3.ZAxis, 0.0, 0.0); + m2.Animation.AddTranslationKeyframe(Vector3.Origin, 0.0); + m2.Animation.AddScaleKeyframe(Vector3.Origin, 0.0); + + m2.Animation.AddTranslationKeyframe(t.Origin, 3.0); + m2.Animation.AddRotationKeyframe(Vector3.ZAxis, i, 3.1); + m2.Animation.AddScaleKeyframe(new Vector3(j, j, j), 2.9); + + j += 1.0 / count; + } + } + [Fact] public void Transform_OfPoint() {