Переглянути джерело

Improved SceneBuilder API to manipulate scenes.

Vicente Penades 5 роки тому
батько
коміт
9f1a7620ca

+ 139 - 15
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -232,6 +232,25 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        #pragma warning disable CA1721 // Property names should not match get methods
+
+        /// <summary>
+        /// Gets or sets the world transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
+        /// </summary>
+        public Matrix4x4 WorldMatrix
+        {
+            get
+            {
+                var vs = VisualParent;
+                return vs == null ? LocalMatrix : Transforms.Matrix4x4Factory.LocalToWorld(vs.WorldMatrix, LocalMatrix);
+            }
+            set
+            {
+                var vs = VisualParent;
+                LocalMatrix = vs == null ? value : Transforms.Matrix4x4Factory.WorldToLocal(vs.WorldMatrix, value);
+            }
+        }
+
         internal Transforms.Matrix4x4Double LocalMatrixPrecise
         {
             get
@@ -251,36 +270,48 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        #pragma warning disable CA1721 // Property names should not match get methods
+        internal Transforms.Matrix4x4Double WorldMatrixPrecise
+        {
+            get
+            {
+                var vs = this.VisualParent;
+                return vs == null ? LocalMatrixPrecise : LocalMatrixPrecise * vs.WorldMatrixPrecise;
+            }
+        }
+
+        #pragma warning restore CA1721 // Property names should not match get methods
 
         /// <summary>
-        /// Gets or sets the world transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
+        /// Gets a value indicating whether this transform is affected by any animation.
         /// </summary>
-        public Matrix4x4 WorldMatrix
+        public bool IsTransformAnimated
         {
             get
             {
-                var vs = VisualParent;
-                return vs == null ? LocalMatrix : Transforms.Matrix4x4Factory.LocalToWorld(vs.WorldMatrix, LocalMatrix);
-            }
-            set
-            {
-                var vs = VisualParent;
-                LocalMatrix = vs == null ? value : Transforms.Matrix4x4Factory.WorldToLocal(vs.WorldMatrix, value);
+                var root = this.LogicalParent;
+
+                if (root.LogicalAnimations.Count == 0) return false;
+
+                // check if it's affected by animations.
+                if (root.LogicalAnimations.Any(anim => anim.FindScaleSampler(this) != null)) return true;
+                if (root.LogicalAnimations.Any(anim => anim.FindRotationSampler(this) != null)) return true;
+                if (root.LogicalAnimations.Any(anim => anim.FindTranslationSampler(this) != null)) return true;
+
+                return false;
             }
         }
 
-        internal Transforms.Matrix4x4Double WorldMatrixPrecise
+        internal bool IsTransformDecomposed
         {
             get
             {
-                var vs = this.VisualParent;
-                return vs == null ? LocalMatrixPrecise : LocalMatrixPrecise * vs.WorldMatrixPrecise;
+                if (_scale.HasValue) return true;
+                if (_rotation.HasValue) return true;
+                if (_translation.HasValue) return true;
+                return false;
             }
         }
 
-        #pragma warning restore CA1721 // Property names should not match get methods
-
         #endregion
 
         #region API - Transforms
@@ -406,6 +437,32 @@ namespace SharpGLTF.Schema2
             return allChildren;
         }
 
+        internal void _SetVisualParent(Node parentNode)
+        {
+            Guard.MustShareLogicalParent(this, parentNode, nameof(parentNode));
+            Guard.IsFalse(_ContainsVisualNode(parentNode, true), nameof(parentNode));
+
+            // remove from all the scenes
+            foreach (var s in LogicalParent.LogicalScenes)
+            {
+                s._RemoveVisualNode(this);
+            }
+
+            // remove from current parent
+            _RemoveFromVisualParent();
+
+            // add to parent node.
+            parentNode._children.Add(this.LogicalIndex);
+        }
+
+        internal void _RemoveFromVisualParent()
+        {
+            var oldParent = this.VisualParent;
+            if (oldParent == null) return;
+
+            oldParent._children.Remove(this.LogicalIndex);
+        }
+
         #endregion
 
         #region validation
@@ -543,5 +600,72 @@ namespace SharpGLTF.Schema2
             parentChildren.Add(n.LogicalIndex);
             return n;
         }
+
+        /// <summary>
+        /// Applies a world transform to all the scenes of the model.
+        /// </summary>
+        /// <param name="basisTransform">The transform to apply.</param>
+        /// <param name="basisNodeName">The name of the new root node, if it needs to be created.</param>
+        /// <remarks>
+        /// This method is appropiate to apply a general axis or scale change to the whole model.
+        /// Animations are preserved by encapsulating animated nodes inside a master basis transform node.
+        /// Meanwhile, unanimated nodes are transformed directly.
+        /// If the determinant of <paramref name="basisTransform"/> is negative, the face culling should be
+        /// flipped when rendering.
+        /// </remarks>
+        public void ApplyBasisTransform(Matrix4x4 basisTransform, string basisNodeName = "BasisTransform")
+        {
+            // TODO: nameless nodes with decomposed transform
+            // could be considered intrinsic.
+
+            Guard.IsTrue(basisTransform.IsValid(true, true), nameof(basisTransform));
+
+            // gather all root nodes:
+            var rootNodes = this.LogicalNodes
+                .Where(item => item.VisualRoot == item)
+                .ToList();
+
+            // find all the nodes that cannot be modified
+            bool isSensible(Node node)
+            {
+                if (node.IsTransformAnimated) return true;
+                if (node.IsTransformDecomposed) return true;
+                return false;
+            }
+
+            var sensibleNodes = rootNodes
+                .Where(item => isSensible(item))
+                .ToList();
+
+            // find all the nodes that we can change their transform matrix safely.
+            var intrinsicNodes = rootNodes
+                .Except(sensibleNodes)
+                .ToList();
+
+            // apply the transform to the nodes that are safe to change.
+            foreach (var n in intrinsicNodes)
+            {
+                n.LocalMatrix *= basisTransform;
+            }
+
+            if (sensibleNodes.Count == 0) return;
+
+            // create a proxy node to be used as the root for all sensible nodes.
+            var basisNode = CreateLogicalNode();
+            basisNode.Name = basisNodeName;
+            basisNode.LocalMatrix = basisTransform;
+
+            // add the basis node to all scenes
+            foreach (var scene in this.LogicalScenes)
+            {
+                scene._UseVisualNode(basisNode);
+            }
+
+            // assign all the sensible nodes to the basis node.
+            foreach (var n in sensibleNodes)
+            {
+                n._SetVisualParent(basisNode);
+            }
+        }
     }
 }

+ 23 - 0
src/SharpGLTF.Core/Schema2/gltf.Scene.cs

@@ -57,6 +57,29 @@ namespace SharpGLTF.Schema2
             return VisualChildren.Any(item => item._ContainsVisualNode(node, true));
         }
 
+        internal void _RemoveVisualNode(Node node)
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+
+            _nodes.Remove(node.LogicalIndex);
+        }
+
+        internal void _UseVisualNode(Node node)
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+
+            var lidx = node.LogicalIndex;
+
+            if (_nodes.Contains(lidx)) return; // already in.
+
+            // a root node cannot be a child of another node.
+            node._RemoveFromVisualParent();
+
+            _nodes.Add(lidx);
+        }
+
         #endregion
 
         #region Validation

+ 38 - 0
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -12,6 +12,7 @@ namespace SharpGLTF.Scenes
         MESHBUILDER GetGeometryAsset();
     }
 
+    [System.Diagnostics.DebuggerDisplay("Mesh")]
     partial class MeshContent
         : IRenderableContent
         , ICloneable
@@ -37,10 +38,21 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private MESHBUILDER _Mesh;
 
         #endregion
 
+        #region properties
+
+        public MESHBUILDER Mesh
+        {
+            get => _Mesh;
+            set => _Mesh = value;
+        }
+
+        #endregion
+
         #region API
 
         public MESHBUILDER GetGeometryAsset() => _Mesh;
@@ -52,8 +64,10 @@ namespace SharpGLTF.Scenes
     {
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private IRenderableContent _Target;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly List<Animations.AnimatableProperty<float>> _MorphWeights = new List<Animations.AnimatableProperty<float>>();
 
         #endregion
@@ -65,6 +79,7 @@ namespace SharpGLTF.Scenes
         #endregion
     }
 
+    [System.Diagnostics.DebuggerDisplay("Camera")]
     partial class CameraContent : ICloneable
     {
         #region lifecycle
@@ -88,11 +103,23 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private CameraBuilder _Camera;
 
         #endregion
+
+        #region properties
+
+        public CameraBuilder Camera
+        {
+            get => _Camera;
+            set => _Camera = value;
+        }
+
+        #endregion
     }
 
+    [System.Diagnostics.DebuggerDisplay("Light")]
     partial class LightContent : ICloneable
     {
         #region lifecycle
@@ -116,8 +143,19 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private LightBuilder _Light;
 
         #endregion
+
+        #region properties
+
+        public LightBuilder Light
+        {
+            get => _Light;
+            set => _Light = value;
+        }
+
+        #endregion
     }
 }

+ 14 - 2
src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs

@@ -6,6 +6,7 @@ using SCHEMA2SCENE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Sc
 
 namespace SharpGLTF.Scenes
 {
+    [System.Diagnostics.DebuggerDisplay("{Content}")]
     public sealed class InstanceBuilder : SCHEMA2SCENE
     {
         #region lifecycle
@@ -29,6 +30,10 @@ namespace SharpGLTF.Scenes
 
         #region properties
 
+        /// <summary>
+        /// Gets The name of this instance.
+        /// This name represents the name that will take the <see cref="Schema2.Node"/> containing this content.
+        /// </summary>
         public string Name => _ContentTransformer?.Name;
 
         public ContentTransformer Content
@@ -41,6 +46,9 @@ namespace SharpGLTF.Scenes
 
         #region API
 
+        /// <summary>
+        /// Removes this instance from its parent <see cref="SceneBuilder"/>.
+        /// </summary>
         public void Remove()
         {
             if (_Parent == null) return;
@@ -49,11 +57,15 @@ namespace SharpGLTF.Scenes
             _Parent = null;
         }
 
-        internal InstanceBuilder _CopyTo(SceneBuilder other)
+        #endregion
+
+        #region internals
+
+        internal InstanceBuilder _CopyTo(SceneBuilder other, ContentTransformer.DeepCloneContext args)
         {
             var clone = new InstanceBuilder(other);
 
-            clone._ContentTransformer = this._ContentTransformer?.DeepClone();
+            clone._ContentTransformer = this._ContentTransformer?.DeepClone(args);
 
             return clone;
         }

+ 44 - 5
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -4,6 +4,8 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 
+using SharpGLTF.Schema2;
+
 namespace SharpGLTF.Scenes
 {
     /// <summary>
@@ -53,16 +55,40 @@ namespace SharpGLTF.Scenes
 
         public NodeBuilder(string name) { Name = name; }
 
-        private NodeBuilder(NodeBuilder parent)
+        public Dictionary<NodeBuilder, NodeBuilder> DeepClone()
         {
-            _Parent = parent;
+            var dict = new Dictionary<NodeBuilder, NodeBuilder>();
+
+            DeepClone(dict);
+
+            return dict;
+        }
+
+        private NodeBuilder DeepClone(IDictionary<NodeBuilder, NodeBuilder> nodeMap)
+        {
+            var clone = new NodeBuilder();
+
+            nodeMap[this] = clone;
+
+            clone.Name = this.Name;
+            clone._Matrix = this._Matrix;
+            clone._Scale = this._Scale?.Clone();
+            clone._Rotation = this._Rotation?.Clone();
+            clone._Translation = this._Translation?.Clone();
+
+            foreach (var c in _Children)
+            {
+                clone.AddNode(c.DeepClone(nodeMap));
+            }
+
+            return clone;
         }
 
         #endregion
 
         #region data
 
-        private readonly NodeBuilder _Parent;
+        private NodeBuilder _Parent;
 
         private readonly List<NodeBuilder> _Children = new List<NodeBuilder>();
 
@@ -188,12 +214,25 @@ namespace SharpGLTF.Scenes
 
         public NodeBuilder CreateNode(string name = null)
         {
-            var c = new NodeBuilder(this);
-            _Children.Add(c);
+            var c = new NodeBuilder();
             c.Name = name;
+            AddNode(c);
             return c;
         }
 
+        public void AddNode(NodeBuilder node)
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.IsFalse(Object.ReferenceEquals(this, node), "cannot add to itself");
+
+            if (node._Parent == this) return; // already added to this node.
+
+            Guard.MustBeNull(node._Parent, nameof(node), "is child of another node.");
+
+            node._Parent = this;
+            _Children.Add(node);
+        }
+
         /// <summary>
         /// Checks if the collection of joints can be used for skinning a mesh.
         /// </summary>

+ 0 - 9
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -201,15 +201,6 @@ namespace SharpGLTF.Scenes
             return dstModel;
         }
 
-        [Obsolete("Use ToGltf2")]
-        public ModelRoot ToSchema2(bool useStridedBuffers = true)
-        {
-            var settings = SceneBuilderSchema2Settings.Default;
-            settings.UseStridedBuffers = useStridedBuffers;
-
-            return ToGltf2(settings);
-        }
-
         /// <summary>
         /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
         /// </summary>

+ 100 - 28
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -4,10 +4,13 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 
+using SharpGLTF.Schema2;
+
 using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.MaterialBuilder>;
 
 namespace SharpGLTF.Scenes
 {
+    [System.Diagnostics.DebuggerDisplay("Scene {_Name}")]
     public partial class SceneBuilder
     {
         #region lifecycle
@@ -19,27 +22,51 @@ namespace SharpGLTF.Scenes
             _Name = name;
         }
 
-        public SceneBuilder DeepClone()
+        public SceneBuilder DeepClone(bool cloneArmatures = true)
         {
             var clone = new SceneBuilder();
 
             clone._Name = this._Name;
 
+            var nodeMap = new Dictionary<NodeBuilder, NodeBuilder>();
+
+            if (cloneArmatures)
+            {
+                foreach (var root in FindArmatures())
+                {
+                    var dict = root.DeepClone();
+
+                    foreach (var pair in dict) nodeMap[pair.Key] = pair.Value;
+                }
+            }
+
+            var args = new ContentTransformer.DeepCloneContext(nodeMap);
+
             foreach (var inst in this._Instances)
             {
-                inst._CopyTo(clone);
+                var cloneInst = inst._CopyTo(clone, args);
+
+                clone._Instances.Add(cloneInst);
+
             }
 
             return clone;
         }
 
+        public static SceneBuilder Load(string filePath, ReadSettings settings = null)
+        {
+            var mdl = ModelRoot.Load(filePath, settings);
+            return mdl.DefaultScene.ToSceneBuilder();
+        }
+
         #endregion
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private String _Name;
 
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         internal readonly List<InstanceBuilder> _Instances = new List<InstanceBuilder>();
 
         #endregion
@@ -52,25 +79,13 @@ namespace SharpGLTF.Scenes
             set => _Name = value;
         }
 
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         public IReadOnlyList<InstanceBuilder> Instances => _Instances;
 
         #endregion
 
         #region API
 
-        [Obsolete("Use AddRigidMesh")]
-        public InstanceBuilder AddMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix)
-        {
-            return AddRigidMesh(mesh, meshWorldMatrix);
-        }
-
-        [Obsolete("Use AddRigidMesh")]
-        public InstanceBuilder AddMesh(MESHBUILDER mesh, NodeBuilder node)
-        {
-            return AddRigidMesh(mesh, node);
-        }
-
         public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix)
         {
             return AddRigidMesh(mesh, null, meshWorldMatrix);
@@ -208,18 +223,6 @@ namespace SharpGLTF.Scenes
             return instance;
         }
 
-        /// <summary>
-        /// Adds an instance from this or other <see cref="SceneBuilder"/>
-        /// by making a copy of <paramref name="other"/>.
-        /// </summary>
-        /// <param name="other">The source <see cref="InstanceBuilder"/>.</param>
-        /// <returns>The new <see cref="InstanceBuilder"/> added to this scene.</returns>
-        public InstanceBuilder AddInstance(InstanceBuilder other)
-        {
-            if (other == null) return null;
-            return other._CopyTo(this);
-        }
-
         public void RenameAllNodes(string namePrefix)
         {
             var allNodes = Instances
@@ -232,6 +235,75 @@ namespace SharpGLTF.Scenes
             NodeBuilder.Rename(allNodes, namePrefix);
         }
 
+        /// <summary>
+        /// Gets all the unique armatures used by this <see cref="SceneBuilder"/>.
+        /// </summary>
+        /// <returns>A collection of <see cref="NodeBuilder"/> objects representing the root of each armature.</returns>
+        public IReadOnlyList<NodeBuilder> FindArmatures()
+        {
+            return _Instances
+                .Select(item => item.Content.GetArmatureRoot())
+                .Distinct()
+                .ToList();
+        }
+
+        public void ApplyBasisTransform(Matrix4x4 basisTransform, string basisNodeName = "BasisTransform")
+        {
+            // gather all root nodes:
+            var rootNodes = this.FindArmatures();
+
+            // find all the nodes that cannot be modified
+            bool isSensible(NodeBuilder node)
+            {
+                if (node.Scale != null) return true;
+                if (node.Rotation != null) return true;
+                if (node.Translation != null) return true;
+                if (node.HasAnimations) return true;
+                return false;
+            }
+
+            var sensibleNodes = rootNodes
+                .Where(item => isSensible(item))
+                .ToList();
+
+            // find all the nodes that we can change their transform matrix safely.
+            var intrinsicNodes = rootNodes
+                .Except(sensibleNodes)
+                .ToList();
+
+            // apply the transform to the nodes that are safe to change.
+            foreach (var n in intrinsicNodes)
+            {
+                n.LocalMatrix *= basisTransform;
+            }
+
+            if (sensibleNodes.Count == 0) return;
+
+            // create a proxy node to be used as the root for all sensible nodes.
+            var basisNode = new NodeBuilder();
+            basisNode.Name = basisNodeName;
+            basisNode.LocalMatrix = basisTransform;
+
+            // assign all the sensible nodes to the basis node.
+            foreach (var n in sensibleNodes)
+            {
+                basisNode.AddNode(n);
+            }
+        }
+
+        public IReadOnlyList<InstanceBuilder> AddScene(SceneBuilder scene, Matrix4x4 sceneTransform)
+        {
+            Guard.NotNull(scene, nameof(scene));
+
+            scene = scene.DeepClone();
+
+            if (sceneTransform != Matrix4x4.Identity) scene.ApplyBasisTransform(sceneTransform);
+
+            this._Instances.AddRange(scene._Instances);
+
+            return scene._Instances;
+        }
+
         #endregion
     }
 }

+ 3 - 3
src/SharpGLTF.Toolkit/Scenes/Transformers.Schema2.cs

@@ -50,7 +50,7 @@ namespace SharpGLTF.Scenes
             var skinnedMeshNode = dstScene.CreateNode();
             skinnedMeshNode.Name = _NodeName;
 
-            if (_TargetBindMatrix.HasValue)
+            if (_MeshPoseWorldMatrix.HasValue)
             {
                 var dstNodes = new Node[_Joints.Count];
 
@@ -71,12 +71,12 @@ namespace SharpGLTF.Scenes
                 }
                 #endif
 
-                skinnedMeshNode.WithSkinBinding(_TargetBindMatrix.Value, dstNodes);
+                skinnedMeshNode.WithSkinBinding(_MeshPoseWorldMatrix.Value, dstNodes);
             }
             else
             {
                 var skinnedJoints = _Joints
-                .Select(j => (context.GetNode(j.Joints), j.InverseBindMatrix.Value))
+                .Select(j => (context.GetNode(j.Joint), j.InverseBindMatrix.Value))
                 .ToArray();
 
                 skinnedMeshNode.WithSkinBinding(skinnedJoints);

+ 76 - 19
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
+using System.Text;
 
 using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.MaterialBuilder>;
 
@@ -12,6 +13,13 @@ namespace SharpGLTF.Scenes
     /// </summary>
     public abstract class ContentTransformer
     {
+        #region debug
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private string _DebugName => string.IsNullOrWhiteSpace(Name) ? "*" : Name;
+
+        #endregion
+
         #region lifecycle
 
         protected ContentTransformer(Object content)
@@ -23,7 +31,7 @@ namespace SharpGLTF.Scenes
             _Content = content;
         }
 
-        public abstract ContentTransformer DeepClone();
+        public abstract ContentTransformer DeepClone(DeepCloneContext args);
 
         protected ContentTransformer(ContentTransformer other)
         {
@@ -45,8 +53,10 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private Object _Content;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private Animations.AnimatableProperty<Transforms.SparseWeight8> _Morphings;
 
         #endregion
@@ -91,12 +101,35 @@ namespace SharpGLTF.Scenes
             return UseMorphing().UseTrackBuilder(animationTrack);
         }
 
+        public abstract Matrix4x4 GetPoseWorldMatrix();
+
+        #endregion
+
+        #region nestedTypes
+
+        public readonly struct DeepCloneContext
+        {
+            public DeepCloneContext(IReadOnlyDictionary<NodeBuilder, NodeBuilder> nmap)
+            {
+                _NodeMap = nmap;
+            }
+
+            private readonly IReadOnlyDictionary<NodeBuilder, NodeBuilder> _NodeMap;
+
+            public NodeBuilder GetNode(NodeBuilder node)
+            {
+                if (_NodeMap == null) return node;
+                return _NodeMap.TryGetValue(node, out NodeBuilder clone) ? clone : node;
+            }
+        }
+
         #endregion
     }
 
     /// <summary>
     /// Applies a fixed <see cref="Matrix4x4"/> transform to the underlaying content.
     /// </summary>
+    [System.Diagnostics.DebuggerDisplay("Fixed Node[{_DebugName,nq}] = {Content}")]
     public partial class FixedTransformer : ContentTransformer
     {
         #region lifecycle
@@ -117,7 +150,7 @@ namespace SharpGLTF.Scenes
             this._WorldTransform = other._WorldTransform;
         }
 
-        public override ContentTransformer DeepClone()
+        public override ContentTransformer DeepClone(DeepCloneContext args)
         {
             return new FixedTransformer(this);
         }
@@ -126,8 +159,10 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private String _NodeName;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private Matrix4x4 _WorldTransform;
 
         #endregion
@@ -136,7 +171,7 @@ namespace SharpGLTF.Scenes
 
         public override String Name => _NodeName;
 
-        public Matrix4x4 WorldTransform
+        public Matrix4x4 WorldMatrix
         {
             get => _WorldTransform;
             set => _WorldTransform = value;
@@ -148,12 +183,16 @@ namespace SharpGLTF.Scenes
 
         public override NodeBuilder GetArmatureRoot() { return null; }
 
+        public override Matrix4x4 GetPoseWorldMatrix() => WorldMatrix;
+
         #endregion
+
     }
 
     /// <summary>
     /// Applies the transform of a single <see cref="NodeBuilder"/> to the underlaying content.
     /// </summary>
+    [System.Diagnostics.DebuggerDisplay("Rigid Node[{_DebugName,nq}] = {Content}")]
     public partial class RigidTransformer : ContentTransformer
     {
         #region lifecycle
@@ -164,23 +203,24 @@ namespace SharpGLTF.Scenes
             _Node = node;
         }
 
-        protected RigidTransformer(RigidTransformer other)
+        protected RigidTransformer(RigidTransformer other, DeepCloneContext args)
             : base(other)
         {
             Guard.NotNull(other, nameof(other));
 
-            this._Node = other._Node;
+            this._Node = args.GetNode(other._Node);
         }
 
-        public override ContentTransformer DeepClone()
+        public override ContentTransformer DeepClone(DeepCloneContext args)
         {
-            return new RigidTransformer(this);
+            return new RigidTransformer(this, args);
         }
 
         #endregion
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private NodeBuilder _Node;
 
         #endregion
@@ -201,12 +241,15 @@ namespace SharpGLTF.Scenes
 
         public override NodeBuilder GetArmatureRoot() { return _Node.Root; }
 
+        public override Matrix4x4 GetPoseWorldMatrix() => Transform.WorldMatrix;
+
         #endregion
     }
 
     /// <summary>
     /// Applies the transforms of many <see cref="NodeBuilder"/> to the underlaying content.
     /// </summary>
+    [System.Diagnostics.DebuggerDisplay("Skinned Node[{_DebugName,nq}] = {Content}")]
     public partial class SkinnedTransformer : ContentTransformer
     {
         #region lifecycle
@@ -225,31 +268,43 @@ namespace SharpGLTF.Scenes
             SetJoints(joints);
         }
 
-        protected SkinnedTransformer(SkinnedTransformer other)
+        protected SkinnedTransformer(SkinnedTransformer other, DeepCloneContext args)
             : base(other)
         {
             Guard.NotNull(other, nameof(other));
 
             this._NodeName = other._NodeName;
-            this._TargetBindMatrix = other._TargetBindMatrix;
-            this._Joints.AddRange(other._Joints);
+            this._MeshPoseWorldMatrix = other._MeshPoseWorldMatrix;
+
+            foreach (var (joint, inverseBindMatrix) in other._Joints)
+            {
+                var jj = (args.GetNode(joint), inverseBindMatrix);
+
+                this._Joints.Add(jj);
+            }
         }
 
-        public override ContentTransformer DeepClone()
+        public override ContentTransformer DeepClone(DeepCloneContext args)
         {
-            return new SkinnedTransformer(this);
+            return new SkinnedTransformer(this, args);
         }
 
         #endregion
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private String _NodeName;
 
-        private Matrix4x4? _TargetBindMatrix;
+        /// <summary>
+        /// Defines the world matrix of the mesh at the time of binding.
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private Matrix4x4? _MeshPoseWorldMatrix;
 
         // condition: all NodeBuilder objects must have the same root.
-        private readonly List<(NodeBuilder Joints, Matrix4x4? InverseBindMatrix)> _Joints = new List<(NodeBuilder, Matrix4x4?)>();
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly List<(NodeBuilder Joint, Matrix4x4? InverseBindMatrix)> _Joints = new List<(NodeBuilder, Matrix4x4?)>();
 
         #endregion
 
@@ -266,7 +321,7 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(joints, nameof(joints));
             Guard.IsTrue(NodeBuilder.IsValidArmature(joints), nameof(joints));
 
-            _TargetBindMatrix = meshWorldMatrix;
+            _MeshPoseWorldMatrix = meshWorldMatrix;
             _Joints.Clear();
             _Joints.AddRange(joints.Select(item => (item, (Matrix4x4?)null)));
         }
@@ -276,7 +331,7 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(joints, nameof(joints));
             Guard.IsTrue(NodeBuilder.IsValidArmature(joints.Select(item => item.Joint)), nameof(joints));
 
-            _TargetBindMatrix = null;
+            _MeshPoseWorldMatrix = null;
             _Joints.Clear();
             _Joints.AddRange(joints.Select(item => (item.Joint, (Matrix4x4?)item.InverseBindMatrix)));
         }
@@ -287,8 +342,8 @@ namespace SharpGLTF.Scenes
 
             for (int i = 0; i < jb.Length; ++i)
             {
-                var j = _Joints[i].Joints;
-                var m = _Joints[i].InverseBindMatrix ?? Transforms.SkinnedTransform.CalculateInverseBinding(_TargetBindMatrix ?? Matrix4x4.Identity, j.WorldMatrix);
+                var j = _Joints[i].Joint;
+                var m = _Joints[i].InverseBindMatrix ?? Transforms.SkinnedTransform.CalculateInverseBinding(_MeshPoseWorldMatrix ?? Matrix4x4.Identity, j.WorldMatrix);
 
                 jb[i] = (j, m);
             }
@@ -299,7 +354,7 @@ namespace SharpGLTF.Scenes
         public override NodeBuilder GetArmatureRoot()
         {
             return _Joints
-                .Select(item => item.Joints.Root)
+                .Select(item => item.Joint.Root)
                 .Distinct()
                 .FirstOrDefault();
         }
@@ -317,6 +372,8 @@ namespace SharpGLTF.Scenes
                 );
         }
 
+        public override Matrix4x4 GetPoseWorldMatrix() => _MeshPoseWorldMatrix ?? Matrix4x4.Identity;
+
         #endregion
     }
 }

+ 14 - 0
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Numerics;
 using System.Text;
 
 using NUnit.Framework;
@@ -91,6 +92,19 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
                 TestContext.WriteLine($"Triangle {ap} {an} {bp} {bn} {cp} {cn}");
             }
+
+            // create a clone and apply a global axis transform.
+
+            var clonedModel = model.DeepClone();
+
+            var basisTransform
+                = Matrix4x4.CreateScale(1, 2, 1)
+                * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3)                
+                * Matrix4x4.CreateTranslation(10,5,2);
+
+            clonedModel.ApplyBasisTransform(basisTransform);
+
+            clonedModel.AttachToCurrentTest("polly_out_transformed.glb");
         }
 
         [Test]

+ 53 - 3
tests/SharpGLTF.Toolkit.Tests/Scenes/SceneBuilderTests.cs

@@ -11,7 +11,6 @@ using SharpGLTF.Geometry.VertexTypes;
 using SharpGLTF.Geometry.Parametric;
 using SharpGLTF.Materials;
 
-
 namespace SharpGLTF.Scenes
 {
     using VPOSNRM = VertexBuilder<VertexPositionNormal, VertexEmpty, VertexEmpty>;
@@ -464,8 +463,7 @@ namespace SharpGLTF.Scenes
         [TestCase("BoxAnimated.glb")]
         [TestCase("BrainStem.glb")]
         [TestCase("CesiumMan.glb")]
-        [TestCase("GearboxAssy.glb")]
-        // [TestCase("Monster.glb")]
+        [TestCase("GearboxAssy.glb")]        
         [TestCase("OrientationTest.glb")]
         [TestCase("RiggedFigure.glb")]
         [TestCase("RiggedSimple.glb")]
@@ -535,6 +533,40 @@ namespace SharpGLTF.Scenes
             }
         }
 
+        
+        [TestCase("GearboxAssy.glb")]        
+        public void ExportMeshes(string path)
+        {
+            TestContext.CurrentContext.AttachShowDirLink();
+            TestContext.CurrentContext.AttachGltfValidatorLinks();
+
+            path = TestFiles
+                .GetSampleModelsPaths()
+                .FirstOrDefault(item => item.Contains(path));
+
+            // load the glTF model
+            var srcModel = ModelRoot.Load(path, Validation.ValidationMode.TryFix);
+            Assert.NotNull(srcModel);
+
+            // convert it to a SceneBuilder so we can manipulate it:
+            var srcScene = srcModel.DefaultScene.ToSceneBuilder();
+
+            // export all the individual meshes to OBJ:            
+            for(int i=0; i < srcScene.Instances.Count; ++i)
+            {
+                var inst = srcScene.Instances[i].Content;
+
+                // scan for meshes:
+                if (inst.Content is MeshContent mesh)
+                {
+                    var newScene = new SceneBuilder();
+
+                    newScene.AddRigidMesh(mesh.Mesh, inst.GetPoseWorldMatrix());
+
+                    newScene.AttachToCurrentTest($"result_{i}.obj");
+                }                
+            }
+        }
 
         [Test]
         public void TestCreateEmptyMesh()
@@ -625,5 +657,23 @@ namespace SharpGLTF.Scenes
             nb.UseScale().UseTrackBuilder("Default");
         }
 
+        [Test(Description = "Creates a new scene by merging multiple scenes")]
+        public void CreateSceneComposition()
+        {
+            // load Polly model
+            var polly = SceneBuilder.Load(TestFiles.GetPollyFileModelPath(), Validation.ValidationMode.TryFix);
+            
+            var xform0 = Matrix4x4.CreateFromYawPitchRoll(1, 0, 0) * Matrix4x4.CreateTranslation(1.5f, 0, 0);
+            var xform1 = Matrix4x4.CreateFromYawPitchRoll(0, 1, 0) * Matrix4x4.CreateTranslation(-1.5f, 1, 0);
+
+            var scene = new SceneBuilder();
+
+            scene.AddScene(polly, xform0);
+            scene.AddScene(polly, xform1);
+
+            scene.AttachToCurrentTest("construction.glb");
+
+        }
+
     }
 }