Browse Source

Added clone methods to meshbuilder and scenebuilder.
Added a wrapper to load/save gltfs from/to zip files.

Vicente Penades 6 years ago
parent
commit
eaea92fab3

+ 3 - 3
src/SharpGLTF.Core/Schema2/gltf.Serialization.Write.cs

@@ -103,7 +103,7 @@ namespace SharpGLTF.Schema2
         /// <param name="settings">Optional settings.</param>
         /// <param name="settings">Optional settings.</param>
         public void Save(string filePath, WriteSettings settings = null)
         public void Save(string filePath, WriteSettings settings = null)
         {
         {
-            Guard.NotNullOrEmpty(filePath, nameof(filePath));
+            Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
 
             bool isGltfExtension = filePath
             bool isGltfExtension = filePath
                 .ToLower(System.Globalization.CultureInfo.InvariantCulture)
                 .ToLower(System.Globalization.CultureInfo.InvariantCulture)
@@ -126,7 +126,7 @@ namespace SharpGLTF.Schema2
                 .CreateFromFile(filePath)
                 .CreateFromFile(filePath)
                 .WithBinarySettings();
                 .WithBinarySettings();
 
 
-            if (settings != null) settings.CopyTo(context);
+            settings?.CopyTo(context);
 
 
             var name = Path.GetFileNameWithoutExtension(filePath);
             var name = Path.GetFileNameWithoutExtension(filePath);
 
 
@@ -148,7 +148,7 @@ namespace SharpGLTF.Schema2
             var context = IO.WriteContext
             var context = IO.WriteContext
                 .CreateFromFile(filePath);
                 .CreateFromFile(filePath);
 
 
-            if (settings != null) settings.CopyTo(context);
+            settings?.CopyTo(context);
 
 
             var name = Path.GetFileNameWithoutExtension(filePath);
             var name = Path.GetFileNameWithoutExtension(filePath);
 
 

+ 15 - 2
src/SharpGLTF.Toolkit/Collections/VertexList.cs

@@ -16,10 +16,11 @@ namespace SharpGLTF.Collections
 
 
         private readonly List<T> _Vertices = new List<T>();
         private readonly List<T> _Vertices = new List<T>();
 
 
-        private readonly T[] _VertexProbe = new T[1];
-
         private readonly Dictionary<VertexKey, int> _VertexCache = new Dictionary<VertexKey, int>();
         private readonly Dictionary<VertexKey, int> _VertexCache = new Dictionary<VertexKey, int>();
 
 
+        [ThreadStatic]
+        private readonly T[] _VertexProbe = new T[1];
+
         #endregion
         #endregion
 
 
         #region API
         #region API
@@ -67,6 +68,18 @@ namespace SharpGLTF.Collections
             }
             }
         }
         }
 
 
+        public void CopyTo(VertexList<T> dst)
+        {
+            for (int i = 0; i < this._Vertices.Count; ++i)
+            {
+                var v = this._Vertices[i];
+
+                var idx = dst._Vertices.Count;
+                dst._Vertices.Add(v);
+                dst._VertexCache[new VertexKey(dst._Vertices, idx)] = idx;
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region types
         #region types

+ 40 - 0
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -53,6 +53,46 @@ namespace SharpGLTF.Geometry
             _VertexPreprocessor.SetSanitizerPreprocessors();
             _VertexPreprocessor.SetSanitizerPreprocessors();
         }
         }
 
 
+        IMeshBuilder<TMaterial> IMeshBuilder<TMaterial>.Clone(Func<TMaterial, TMaterial> materialCloneCallback)
+        {
+            return new MeshBuilder<TMaterial, TvG, TvM, TvS>(this, materialCloneCallback);
+        }
+
+        public MeshBuilder<TMaterial, TvG, TvM, TvS> Clone(Func<TMaterial, TMaterial> materialCloneCallback = null)
+        {
+            return new MeshBuilder<TMaterial, TvG, TvM, TvS>(this, materialCloneCallback);
+        }
+
+        private MeshBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> other, Func<TMaterial, TMaterial> materialCloneCallback = null)
+        {
+            Guard.NotNull(other, nameof(other));
+
+            this.Name = other.Name;
+            this._VertexPreprocessor = other._VertexPreprocessor;
+
+            foreach (var kvp in other._Primitives)
+            {
+                var material = kvp.Key.Material;
+
+                if (materialCloneCallback != null)
+                {
+                    material = materialCloneCallback(material);
+                    if (material == null) continue;
+                }
+
+                var key = (material, kvp.Key.PrimType);
+
+                if (_Primitives.TryGetValue(key, out PrimitiveBuilder<TMaterial, TvG, TvM, TvS> existing))
+                {
+                    existing.AddPrimitive(kvp.Value, null);
+                }
+                else
+                {
+                    _Primitives[key] = kvp.Value.Clone(this, material);
+                }
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data

+ 2 - 0
src/SharpGLTF.Toolkit/Geometry/MeshBuilderToolkit.cs

@@ -18,6 +18,8 @@ namespace SharpGLTF.Geometry
 
 
         IPrimitiveBuilder UsePrimitive(TMaterial material, int primitiveVertexCount = 3);
         IPrimitiveBuilder UsePrimitive(TMaterial material, int primitiveVertexCount = 3);
 
 
+        IMeshBuilder<TMaterial> Clone(Func<TMaterial, TMaterial> materialCloneCallback = null);
+
         void Validate();
         void Validate();
     }
     }
 
 

+ 9 - 2
src/SharpGLTF.Toolkit/Geometry/MorphTargetBuilder.cs

@@ -31,7 +31,14 @@ namespace SharpGLTF.Geometry
 
 
         internal PrimitiveMorphTargetBuilder(Func<int, TvG> baseVertexFunc)
         internal PrimitiveMorphTargetBuilder(Func<int, TvG> baseVertexFunc)
         {
         {
-            _BaseVertexFunc = baseVertexFunc;
+            this._BaseVertexFunc = baseVertexFunc;
+            this._MorphVertices = new Dictionary<int, TvG>();
+        }
+
+        internal PrimitiveMorphTargetBuilder(Func<int, TvG> baseVertexFunc, PrimitiveMorphTargetBuilder<TvG> other)
+        {
+            this._BaseVertexFunc = baseVertexFunc;
+            this._MorphVertices = new Dictionary<int, TvG>(other._MorphVertices);
         }
         }
 
 
         #endregion
         #endregion
@@ -40,7 +47,7 @@ namespace SharpGLTF.Geometry
 
 
         private readonly Func<int, TvG> _BaseVertexFunc;
         private readonly Func<int, TvG> _BaseVertexFunc;
 
 
-        private readonly Dictionary<int, TvG> _MorphVertices = new Dictionary<int, TvG>();
+        private readonly Dictionary<int, TvG> _MorphVertices;
 
 
         #endregion
         #endregion
 
 

+ 78 - 25
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -123,6 +123,24 @@ namespace SharpGLTF.Geometry
             this._Material = material;
             this._Material = material;
         }
         }
 
 
+        protected PrimitiveBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, PrimitiveBuilder<TMaterial, TvG, TvM, TvS> other, TMaterial material)
+        {
+            Guard.NotNull(mesh, nameof(mesh));
+            Guard.NotNull(other, nameof(other));
+
+            this._Mesh = mesh;
+            this._Material = material ?? other.Material;
+            other._Vertices.CopyTo(this._Vertices);
+
+            foreach (var otherMT in other._MorphTargets)
+            {
+                var thisMT = new PrimitiveMorphTargetBuilder<TvG>(idx => this._Vertices[idx].Geometry, otherMT);
+                this._MorphTargets.Add(otherMT);
+            }
+        }
+
+        internal abstract PrimitiveBuilder<TMaterial, TvG, TvM, TvS> Clone(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, TMaterial material);
+
         #endregion
         #endregion
 
 
         #region data
         #region data
@@ -302,60 +320,62 @@ namespace SharpGLTF.Geometry
         {
         {
             if (primitive == null) return;
             if (primitive == null) return;
 
 
+            if (vertexTransformFunc == null) vertexTransformFunc = v => v;
+
             // vertex-vertex map so we can know where to set the morph targets.
             // vertex-vertex map so we can know where to set the morph targets.
             var vmap = new Dictionary<int, int>();
             var vmap = new Dictionary<int, int>();
 
 
             if (this.VerticesPerPrimitive == 1)
             if (this.VerticesPerPrimitive == 1)
             {
             {
-                foreach (var p in primitive.Points)
+                foreach (var src in primitive.Points)
                 {
                 {
-                    var a = vertexTransformFunc(primitive.Vertices[p]);
+                    var vrt = vertexTransformFunc(primitive.Vertices[src]);
 
 
-                    var idx = AddPoint(a);
+                    var idx = AddPoint(vrt);
 
 
-                    vmap[p] = idx;
+                    vmap[src] = idx;
                 }
                 }
             }
             }
 
 
             if (this.VerticesPerPrimitive == 2)
             if (this.VerticesPerPrimitive == 2)
             {
             {
-                foreach (var l in primitive.Lines)
+                foreach (var (srcA, srcB) in primitive.Lines)
                 {
                 {
-                    var a = vertexTransformFunc(primitive.Vertices[l.A]);
-                    var b = vertexTransformFunc(primitive.Vertices[l.B]);
+                    var vrtA = vertexTransformFunc(primitive.Vertices[srcA]);
+                    var vrtB = vertexTransformFunc(primitive.Vertices[srcB]);
 
 
-                    var indices = AddLine(a, b);
+                    var (dstA, dstB) = AddLine(vrtA, vrtB);
 
 
-                    vmap[l.A] = indices.A;
-                    vmap[l.B] = indices.B;
+                    vmap[srcA] = dstA;
+                    vmap[srcB] = dstB;
                 }
                 }
             }
             }
 
 
             if (this.VerticesPerPrimitive == 3)
             if (this.VerticesPerPrimitive == 3)
             {
             {
-                foreach (var s in primitive.Surfaces)
+                foreach (var (srcA, srcB, srcC, srcD) in primitive.Surfaces)
                 {
                 {
-                    var a = vertexTransformFunc(primitive.Vertices[s.A]);
-                    var b = vertexTransformFunc(primitive.Vertices[s.B]);
-                    var c = vertexTransformFunc(primitive.Vertices[s.C]);
+                    var vrtA = vertexTransformFunc(primitive.Vertices[srcA]);
+                    var vrtB = vertexTransformFunc(primitive.Vertices[srcB]);
+                    var vrtC = vertexTransformFunc(primitive.Vertices[srcC]);
 
 
-                    if (s.D.HasValue)
+                    if (srcD.HasValue)
                     {
                     {
-                        var d = vertexTransformFunc(primitive.Vertices[s.D.Value]);
-                        var indices = AddQuadrangle(a, b, c, d);
+                        var vrtD = vertexTransformFunc(primitive.Vertices[srcD.Value]);
+                        var (dstA, dstB, dstC, dstD) = AddQuadrangle(vrtA, vrtB, vrtC, vrtD);
 
 
-                        vmap[s.A] = indices.A;
-                        vmap[s.B] = indices.B;
-                        vmap[s.C] = indices.C;
-                        vmap[s.D.Value] = indices.D;
+                        vmap[srcA] = dstA;
+                        vmap[srcB] = dstB;
+                        vmap[srcC] = dstC;
+                        vmap[srcD.Value] = dstD;
                     }
                     }
                     else
                     else
                     {
                     {
-                        var indices = AddTriangle(a, b, c);
+                        var (dstA, dstB, dstC) = AddTriangle(vrtA, vrtB, vrtC);
 
 
-                        vmap[s.A] = indices.A;
-                        vmap[s.B] = indices.B;
-                        vmap[s.C] = indices.C;
+                        vmap[srcA] = dstA;
+                        vmap[srcB] = dstB;
+                        vmap[srcC] = dstC;
                     }
                     }
                 }
                 }
             }
             }
@@ -452,6 +472,16 @@ namespace SharpGLTF.Geometry
         {
         {
         }
         }
 
 
+        internal override PrimitiveBuilder<TMaterial, TvG, TvM, TvS> Clone(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, TMaterial material)
+        {
+            return new PointsPrimitiveBuilder<TMaterial, TvG, TvM, TvS>(mesh, this, material);
+        }
+
+        private PointsPrimitiveBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, PointsPrimitiveBuilder<TMaterial, TvG, TvM, TvS> other, TMaterial material)
+            : base(mesh, other, material)
+        {
+        }
+
         #endregion
         #endregion
 
 
         #region properties
         #region properties
@@ -514,6 +544,17 @@ namespace SharpGLTF.Geometry
         {
         {
         }
         }
 
 
+        internal override PrimitiveBuilder<TMaterial, TvG, TvM, TvS> Clone(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, TMaterial material)
+        {
+            return new LinesPrimitiveBuilder<TMaterial, TvG, TvM, TvS>(mesh, this, material);
+        }
+
+        private LinesPrimitiveBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, LinesPrimitiveBuilder<TMaterial, TvG, TvM, TvS> other, TMaterial material)
+            : base(mesh, other, material)
+        {
+            this._Indices.AddRange(other._Indices);
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data
@@ -576,6 +617,18 @@ namespace SharpGLTF.Geometry
         {
         {
         }
         }
 
 
+        internal override PrimitiveBuilder<TMaterial, TvG, TvM, TvS> Clone(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, TMaterial material)
+        {
+            return new TrianglesPrimitiveBuilder<TMaterial, TvG, TvM, TvS>(mesh, this, material);
+        }
+
+        private TrianglesPrimitiveBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, TrianglesPrimitiveBuilder<TMaterial, TvG, TvM, TvS> other, TMaterial material)
+            : base(mesh, other, material)
+        {
+            this._TriIndices.AddRange(other._TriIndices);
+            this._QuadIndices.AddRange(other._QuadIndices);
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data

+ 170 - 0
src/SharpGLTF.Toolkit/IO/Zip.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using SharpGLTF.Schema2;
+
+namespace SharpGLTF.IO
+{
+    public sealed class ZipReader : IDisposable
+    {
+        #region lifecycle
+
+        public ZipReader(string zipPath)
+        {
+            _Archive = System.IO.Compression.ZipFile.Open(zipPath, System.IO.Compression.ZipArchiveMode.Read);
+        }
+
+        public void Dispose()
+        {
+            _Archive?.Dispose();
+            _Archive = null;
+        }
+
+        public static ModelRoot LoadSchema2(string zipPath, ReadSettings settings = null)
+        {
+            using (var zip = new ZipReader(zipPath))
+            {
+                return zip.LoadSchema2(settings);
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        private System.IO.Compression.ZipArchive _Archive;
+
+        #endregion
+
+        #region properties
+
+        public IEnumerable<string> ModelFiles => _GetEntries().Select(item => item.FullName);
+
+        #endregion
+
+        #region API
+
+        private IEnumerable<System.IO.Compression.ZipArchiveEntry> _GetEntries()
+        {
+            return _Archive
+                .Entries
+                .Where(item => item.FullName.ToLower().EndsWith(".gltf") || item.FullName.ToLower().EndsWith(".glb"))
+                .OrderBy(item => item.FullName);
+        }
+
+        public ModelRoot LoadSchema2(ReadSettings settings = null)
+        {
+            var gltfFile = ModelFiles.First();
+            return this._LoadSchema2(gltfFile, settings);
+        }
+
+        private ModelRoot _LoadSchema2(string gltfFile, ReadSettings settings = null)
+        {
+            var context = ReadContext.Create(_ReadAsset);
+
+            settings?.CopyTo(context);
+
+            using (var m = new System.IO.MemoryStream())
+            {
+                using (var s = _Archive.GetEntry(gltfFile).Open())
+                {
+                    s.CopyTo(m);
+                }
+
+                m.Position = 0;
+
+                return context.ReadSchema2(m);
+            }
+        }
+
+        private ArraySegment<Byte> _ReadAsset(string filePath)
+        {
+            System.IO.Compression.ZipArchiveEntry entry = _FindEntry(filePath);
+
+            using (var s = entry.Open())
+            {
+                using (var m = new System.IO.MemoryStream())
+                {
+                    s.CopyTo(m);
+
+                    if (m.TryGetBuffer(out ArraySegment<Byte> data)) return data;
+                    else return new ArraySegment<byte>(m.ToArray());
+                }
+            }
+
+        }
+
+        private System.IO.Compression.ZipArchiveEntry _FindEntry(string filePath)
+        {
+            filePath = filePath.ToLower();
+
+            var entry = _Archive.Entries.FirstOrDefault(item => item.FullName.ToLower() == filePath);
+            if (entry == null) throw new System.IO.FileNotFoundException(filePath);
+            return entry;
+        }
+
+        #endregion
+    }
+
+    public sealed class ZipWriter : IDisposable
+    {
+        #region lifecycle
+
+        public ZipWriter(string zipPath)
+        {
+            _Archive = System.IO.Compression.ZipFile.Open(zipPath, System.IO.Compression.ZipArchiveMode.Create);
+        }
+
+        public void Dispose()
+        {
+            _Archive?.Dispose();
+            _Archive = null;
+        }
+
+        #endregion
+
+        #region data
+
+        private System.IO.Compression.ZipArchive _Archive;
+
+        #endregion
+
+        #region API
+
+        public void AddModel(string filePath, ModelRoot model, WriteSettings settings = null)
+        {
+            Guard.NotNullOrEmpty(filePath, nameof(filePath));
+
+            var baseName = System.IO.Path.GetFileNameWithoutExtension(filePath);
+
+            bool isGltfExtension = filePath
+                .ToLower(System.Globalization.CultureInfo.InvariantCulture)
+                .EndsWith(".gltf", StringComparison.OrdinalIgnoreCase);
+
+            var context = WriteContext.Create(_WriteAsset);
+
+            if (!isGltfExtension) context.WithBinarySettings();
+
+            settings?.CopyTo(context);
+
+            if (isGltfExtension) context.WriteTextSchema2(baseName, model);
+            else context.WriteBinarySchema2(baseName, model);
+        }
+
+        private void _WriteAsset(string filePath, ArraySegment<Byte> bytes)
+        {
+            // TODO: should check for already existing assets with the same name, and same content.
+
+            var entry = _Archive.CreateEntry(filePath);
+
+            using (var s = entry.Open())
+            {
+                s.Write(bytes.Array, bytes.Offset, bytes.Count);
+            }
+        }
+
+        #endregion
+    }
+}

+ 15 - 1
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -86,6 +86,20 @@ namespace SharpGLTF.Materials
 
 
         #region API
         #region API
 
 
+        internal void CopyTo(ChannelBuilder other)
+        {
+            other.Parameter = this.Parameter;
+
+            if (this.Texture == null)
+            {
+                RemoveTexture();
+            }
+            else
+            {
+                this.Texture.CopyTo(other.UseTexture());
+            }
+        }
+
         public void SetDefaultParameter()
         public void SetDefaultParameter()
         {
         {
             switch (_Key)
             switch (_Key)
@@ -116,7 +130,7 @@ namespace SharpGLTF.Materials
 
 
         #endregion
         #endregion
 
 
-        #region Support types
+        #region Nested types
 
 
         sealed class _ContentComparer : IEqualityComparer<ChannelBuilder>
         sealed class _ContentComparer : IEqualityComparer<ChannelBuilder>
         {
         {

+ 24 - 0
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -21,6 +21,30 @@ namespace SharpGLTF.Materials
             return new MaterialBuilder("Default");
             return new MaterialBuilder("Default");
         }
         }
 
 
+        public MaterialBuilder Clone() { return new MaterialBuilder(this); }
+
+        public MaterialBuilder(MaterialBuilder other)
+        {
+            Guard.NotNull(other, nameof(other));
+
+            this.Name = other.Name;
+            this.AlphaMode = other.AlphaMode;
+            this.AlphaCutoff = other.AlphaCutoff;
+            this.DoubleSided = other.DoubleSided;
+            this.ShaderStyle = other.ShaderStyle;
+
+            this._CompatibilityFallbackMaterial = other._CompatibilityFallbackMaterial == null
+                ? null
+                : new MaterialBuilder(other._CompatibilityFallbackMaterial);
+
+            foreach (var otherChannel in other._Channels)
+            {
+                var thisChannel = UseChannel(otherChannel.Key);
+
+                otherChannel.CopyTo(thisChannel);
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data

+ 24 - 0
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -125,6 +125,22 @@ namespace SharpGLTF.Materials
 
 
         #region API
         #region API
 
 
+        internal void CopyTo(TextureBuilder other)
+        {
+            other._PrimaryImageContent = this._PrimaryImageContent;
+            other._FallbackImageContent = this._FallbackImageContent;
+
+            other.CoordinateSet = this.CoordinateSet;
+
+            other.MinFilter = this.MinFilter;
+            other.MagFilter = this.MagFilter;
+            other.WrapS = this.WrapS;
+            other.WrapT = this.WrapT;
+
+            var xform = new TextureTransformBuilder(this._Transform);
+            other._Transform = xform.IsDefault ? null : xform;
+        }
+
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
 
 
         [Obsolete("Use WithPrimaryImage instead.")]
         [Obsolete("Use WithPrimaryImage instead.")]
@@ -261,6 +277,14 @@ namespace SharpGLTF.Materials
             this.CoordinateSetOverride = coordSetOverride;
             this.CoordinateSetOverride = coordSetOverride;
         }
         }
 
 
+        internal TextureTransformBuilder(TextureTransformBuilder other)
+        {
+            this.Offset = other?.Offset ?? Vector2.Zero;
+            this.Scale = other?.Scale ?? Vector2.One;
+            this.Rotation = other?.Rotation ?? 0;
+            this.CoordinateSetOverride = other?.CoordinateSetOverride;
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data

+ 33 - 1
src/SharpGLTF.Toolkit/Scenes/CameraBuilder.cs

@@ -7,6 +7,12 @@ namespace SharpGLTF.Scenes
 {
 {
     public abstract class CameraBuilder
     public abstract class CameraBuilder
     {
     {
+        #region lifecycle
+
+        public abstract CameraBuilder Clone();
+
+        #endregion
+
         #region properties
         #region properties
 
 
         public static Vector3 LocalDirection => -Vector3.UnitZ;
         public static Vector3 LocalDirection => -Vector3.UnitZ;
@@ -25,7 +31,7 @@ namespace SharpGLTF.Scenes
 
 
         #endregion
         #endregion
 
 
-        #region types
+        #region Nested types
 
 
         #pragma warning disable CA1034 // Nested types should not be visible
         #pragma warning disable CA1034 // Nested types should not be visible
 
 
@@ -50,6 +56,19 @@ namespace SharpGLTF.Scenes
                 this.ZFar = ortho.ZFar;
                 this.ZFar = ortho.ZFar;
             }
             }
 
 
+            public override CameraBuilder Clone()
+            {
+                return new Orthographic(this);
+            }
+
+            internal Orthographic(Orthographic ortho)
+            {
+                this.XMag = ortho.XMag;
+                this.YMag = ortho.YMag;
+                this.ZNear = ortho.ZNear;
+                this.ZFar = ortho.ZFar;
+            }
+
             #endregion
             #endregion
 
 
             #region properties
             #region properties
@@ -93,6 +112,19 @@ namespace SharpGLTF.Scenes
                 this.ZFar = persp.ZFar;
                 this.ZFar = persp.ZFar;
             }
             }
 
 
+            public override CameraBuilder Clone()
+            {
+                return new Perspective(this);
+            }
+
+            internal Perspective(Perspective persp)
+            {
+                this.AspectRatio = persp.AspectRatio;
+                this.VerticalFOV = persp.VerticalFOV;
+                this.ZNear = persp.ZNear;
+                this.ZFar = persp.ZFar;
+            }
+
             #endregion
             #endregion
 
 
             #region properties
             #region properties

+ 35 - 3
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -12,7 +12,9 @@ namespace SharpGLTF.Scenes
         MESHBUILDER GetGeometryAsset();
         MESHBUILDER GetGeometryAsset();
     }
     }
 
 
-    partial class MeshContent : IRenderableContent
+    partial class MeshContent
+        : IRenderableContent
+        , ICloneable
     {
     {
         #region lifecycle
         #region lifecycle
 
 
@@ -21,6 +23,16 @@ namespace SharpGLTF.Scenes
             _Mesh = mesh;
             _Mesh = mesh;
         }
         }
 
 
+        public Object Clone()
+        {
+            return new MeshContent(this);
+        }
+
+        private MeshContent(MeshContent other)
+        {
+            this._Mesh = other._Mesh?.Clone(m => new Materials.MaterialBuilder(m));
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data
@@ -53,7 +65,7 @@ namespace SharpGLTF.Scenes
         #endregion
         #endregion
     }
     }
 
 
-    partial class CameraContent
+    partial class CameraContent : ICloneable
     {
     {
         #region lifecycle
         #region lifecycle
 
 
@@ -62,6 +74,16 @@ namespace SharpGLTF.Scenes
             _Camera = camera;
             _Camera = camera;
         }
         }
 
 
+        public Object Clone()
+        {
+            return new CameraContent(this);
+        }
+
+        private CameraContent(CameraContent other)
+        {
+            this._Camera = other._Camera?.Clone();
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data
@@ -71,7 +93,7 @@ namespace SharpGLTF.Scenes
         #endregion
         #endregion
     }
     }
 
 
-    partial class LightContent
+    partial class LightContent : ICloneable
     {
     {
         #region lifecycle
         #region lifecycle
 
 
@@ -80,6 +102,16 @@ namespace SharpGLTF.Scenes
             _Light = light;
             _Light = light;
         }
         }
 
 
+        public Object Clone()
+        {
+            return new LightContent(this);
+        }
+
+        private LightContent(LightContent other)
+        {
+            this._Light = other._Light?.Clone();
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data

+ 18 - 10
src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs

@@ -15,15 +15,6 @@ namespace SharpGLTF.Scenes
             _Parent = parent;
             _Parent = parent;
         }
         }
 
 
-        internal InstanceBuilder DeepClone(SceneBuilder newParent)
-        {
-            var clone = new InstanceBuilder(newParent);
-            clone._Name = this.Name;
-            clone._ContentTransformer = this._ContentTransformer?.DeepClone();
-
-            return clone;
-        }
-
         #endregion
         #endregion
 
 
         #region data
         #region data
@@ -31,7 +22,7 @@ namespace SharpGLTF.Scenes
         private string _Name;
         private string _Name;
 
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private readonly SceneBuilder _Parent;
+        private SceneBuilder _Parent;
 
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private ContentTransformer _ContentTransformer;
         private ContentTransformer _ContentTransformer;
@@ -56,6 +47,23 @@ namespace SharpGLTF.Scenes
 
 
         #region API
         #region API
 
 
+        public void Remove()
+        {
+            if (_Parent == null) return;
+
+            _Parent._Instances.Remove(this);
+            _Parent = null;
+        }
+
+        internal InstanceBuilder _CopyTo(SceneBuilder other)
+        {
+            var clone = new InstanceBuilder(other);
+            clone._Name = this._Name;
+            clone._ContentTransformer = this._ContentTransformer?.DeepClone();
+
+            return clone;
+        }
+
         void SCHEMA2SCENE.Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
         void SCHEMA2SCENE.Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
         {
         {
             if (_ContentTransformer is SCHEMA2SCENE schema2scb) schema2scb.Setup(dstScene, context);
             if (_ContentTransformer is SCHEMA2SCENE schema2scb) schema2scb.Setup(dstScene, context);

+ 68 - 1
src/SharpGLTF.Toolkit/Scenes/LightBuilder.cs

@@ -7,6 +7,8 @@ namespace SharpGLTF.Scenes
 {
 {
     public abstract class LightBuilder
     public abstract class LightBuilder
     {
     {
+        #region lifecycle
+
         protected LightBuilder(Schema2.PunctualLight light)
         protected LightBuilder(Schema2.PunctualLight light)
         {
         {
             Guard.NotNull(light, nameof(light));
             Guard.NotNull(light, nameof(light));
@@ -15,6 +17,18 @@ namespace SharpGLTF.Scenes
             this.Intensity = light.Intensity;
             this.Intensity = light.Intensity;
         }
         }
 
 
+        public abstract LightBuilder Clone();
+
+        protected LightBuilder(LightBuilder other)
+        {
+            this.Color = other.Color;
+            this.Intensity = other.Intensity;
+        }
+
+        #endregion
+
+        #region data
+
         public static Vector3 LocalDirection => -Vector3.UnitZ;
         public static Vector3 LocalDirection => -Vector3.UnitZ;
 
 
         /// <summary>
         /// <summary>
@@ -29,38 +43,72 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// </summary>
         public Single Intensity { get; set; }
         public Single Intensity { get; set; }
 
 
-        #region types
+        #endregion
+
+        #region Nested types
 
 
         #pragma warning disable CA1034 // Nested types should not be visible
         #pragma warning disable CA1034 // Nested types should not be visible
 
 
         [System.Diagnostics.DebuggerDisplay("Directional")]
         [System.Diagnostics.DebuggerDisplay("Directional")]
         public sealed class Directional : LightBuilder
         public sealed class Directional : LightBuilder
         {
         {
+            #region lifecycle
             internal Directional(Schema2.PunctualLight light)
             internal Directional(Schema2.PunctualLight light)
                 : base(light) { }
                 : base(light) { }
+
+            public override LightBuilder Clone()
+            {
+                return new Directional(this);
+            }
+
+            private Directional(Directional other)
+                : base(other) { }
+
+            #endregion
         }
         }
 
 
         [System.Diagnostics.DebuggerDisplay("Point")]
         [System.Diagnostics.DebuggerDisplay("Point")]
         public sealed class Point : LightBuilder
         public sealed class Point : LightBuilder
         {
         {
+            #region lifecycle
+
             internal Point(Schema2.PunctualLight light)
             internal Point(Schema2.PunctualLight light)
                 : base(light)
                 : base(light)
             {
             {
                 this.Range = light.Range;
                 this.Range = light.Range;
             }
             }
 
 
+            public override LightBuilder Clone()
+            {
+                return new Point(this);
+            }
+
+            private Point(Point other)
+                : base(other)
+            {
+                this.Range = other.Range;
+            }
+
+            #endregion
+
+            #region data
+
             /// <summary>
             /// <summary>
             /// Gets or sets a Hint defining a distance cutoff at which the light's intensity may be considered
             /// Gets or sets a Hint defining a distance cutoff at which the light's intensity may be considered
             /// to have reached zero. Supported only for point and spot lights. Must be > 0.
             /// to have reached zero. Supported only for point and spot lights. Must be > 0.
             /// When undefined, range is assumed to be infinite.
             /// When undefined, range is assumed to be infinite.
             /// </summary>
             /// </summary>
             public Single Range { get; set; }
             public Single Range { get; set; }
+
+            #endregion
         }
         }
 
 
         [System.Diagnostics.DebuggerDisplay("Spot")]
         [System.Diagnostics.DebuggerDisplay("Spot")]
 
 
         public sealed class Spot : LightBuilder
         public sealed class Spot : LightBuilder
         {
         {
+            #region lifecycle
+
             internal Spot(Schema2.PunctualLight light)
             internal Spot(Schema2.PunctualLight light)
                 : base(light)
                 : base(light)
             {
             {
@@ -69,6 +117,23 @@ namespace SharpGLTF.Scenes
                 this.OuterConeAngle = light.OuterConeAngle;
                 this.OuterConeAngle = light.OuterConeAngle;
             }
             }
 
 
+            public override LightBuilder Clone()
+            {
+                return new Spot(this);
+            }
+
+            private Spot(Spot other)
+                : base(other)
+            {
+                this.Range = other.Range;
+                this.InnerConeAngle = other.InnerConeAngle;
+                this.OuterConeAngle = other.OuterConeAngle;
+            }
+
+            #endregion
+
+            #region data
+
             /// <summary>
             /// <summary>
             /// Gets or sets a Hint defining a distance cutoff at which the light's intensity may be considered
             /// Gets or sets a Hint defining a distance cutoff at which the light's intensity may be considered
             /// to have reached zero. Supported only for point and spot lights. Must be > 0.
             /// to have reached zero. Supported only for point and spot lights. Must be > 0.
@@ -87,6 +152,8 @@ namespace SharpGLTF.Scenes
             /// Must be greater than innerConeAngle and less than or equal to PI / 2.0.
             /// Must be greater than innerConeAngle and less than or equal to PI / 2.0.
             /// </summary>
             /// </summary>
             public Single OuterConeAngle { get; set; }
             public Single OuterConeAngle { get; set; }
+
+            #endregion
         }
         }
 
 
         #pragma warning restore CA1034 // Nested types should not be visible
         #pragma warning restore CA1034 // Nested types should not be visible

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

@@ -161,6 +161,12 @@ namespace SharpGLTF.Scenes
     {
     {
         #region from SceneBuilder to Schema2
         #region from SceneBuilder to Schema2
 
 
+        /// <summary>
+        /// Convertes a collection of <see cref="SceneBuilder"/> instances to a single <see cref="ModelRoot"/> instance.
+        /// </summary>
+        /// <param name="srcScenes">A collection of scenes</param>
+        /// <param name="useStridedBuffers">True to generate strided vertex buffers whenever possible.</param>
+        /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
         public static ModelRoot ToSchema2(IEnumerable<SceneBuilder> srcScenes, bool useStridedBuffers = true)
         public static ModelRoot ToSchema2(IEnumerable<SceneBuilder> srcScenes, bool useStridedBuffers = true)
         {
         {
             Guard.NotNull(srcScenes, nameof(srcScenes));
             Guard.NotNull(srcScenes, nameof(srcScenes));
@@ -200,6 +206,8 @@ namespace SharpGLTF.Scenes
 
 
             context.AddScene(dstScene, this);
             context.AddScene(dstScene, this);
 
 
+            dstModel.DefaultScene = dstScene;
+
             return dstModel;
             return dstModel;
         }
         }
 
 

+ 43 - 2
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -24,7 +24,11 @@ namespace SharpGLTF.Scenes
             var clone = new SceneBuilder();
             var clone = new SceneBuilder();
 
 
             clone._Name = this._Name;
             clone._Name = this._Name;
-            clone._Instances.AddRange(this._Instances.Select(item => item.DeepClone(this)));
+
+            foreach (var inst in this._Instances)
+            {
+                inst._CopyTo(clone);
+            }
 
 
             return clone;
             return clone;
         }
         }
@@ -36,7 +40,7 @@ namespace SharpGLTF.Scenes
         private String _Name;
         private String _Name;
 
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
-        private readonly List<InstanceBuilder> _Instances = new List<InstanceBuilder>();
+        internal readonly List<InstanceBuilder> _Instances = new List<InstanceBuilder>();
 
 
         #endregion
         #endregion
 
 
@@ -69,6 +73,8 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix)
         public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix)
         {
         {
+            Guard.NotNull(mesh, nameof(mesh));
+
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new FixedTransformer(mesh, meshWorldMatrix);
             instance.Content = new FixedTransformer(mesh, meshWorldMatrix);
 
 
@@ -79,6 +85,9 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, NodeBuilder node)
         public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, NodeBuilder node)
         {
         {
+            Guard.NotNull(mesh, nameof(mesh));
+            Guard.NotNull(node, nameof(node));
+
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new RigidTransformer(mesh, node);
             instance.Content = new RigidTransformer(mesh, node);
 
 
@@ -89,6 +98,9 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, params NodeBuilder[] joints)
         public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, params NodeBuilder[] joints)
         {
         {
+            Guard.NotNull(mesh, nameof(mesh));
+            GuardAll.NotNull(joints, nameof(joints));
+
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new SkinnedTransformer(mesh, meshWorldMatrix, joints);
             instance.Content = new SkinnedTransformer(mesh, meshWorldMatrix, joints);
 
 
@@ -99,6 +111,9 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, params (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints)
         public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, params (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints)
         {
         {
+            Guard.NotNull(mesh, nameof(mesh));
+            GuardAll.NotNull(joints.Select(item => item.Joint), nameof(joints));
+
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new SkinnedTransformer(mesh, joints);
             instance.Content = new SkinnedTransformer(mesh, joints);
 
 
@@ -109,6 +124,9 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddCamera(CameraBuilder camera, NodeBuilder node)
         public InstanceBuilder AddCamera(CameraBuilder camera, NodeBuilder node)
         {
         {
+            Guard.NotNull(camera, nameof(camera));
+            Guard.NotNull(node, nameof(node));
+
             var content = new CameraContent(camera);
             var content = new CameraContent(camera);
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new RigidTransformer(content, node);
             instance.Content = new RigidTransformer(content, node);
@@ -118,12 +136,18 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddCamera(CameraBuilder camera, Vector3 cameraPosition, Vector3 targetPosition)
         public InstanceBuilder AddCamera(CameraBuilder camera, Vector3 cameraPosition, Vector3 targetPosition)
         {
         {
+            Guard.NotNull(camera, nameof(camera));
+            Guard.IsTrue(cameraPosition._IsFinite(), nameof(cameraPosition));
+            Guard.IsTrue(targetPosition._IsFinite(), nameof(targetPosition));
+
             var xform = Matrix4x4.CreateWorld(cameraPosition, Vector3.Normalize(targetPosition - cameraPosition), Vector3.UnitY);
             var xform = Matrix4x4.CreateWorld(cameraPosition, Vector3.Normalize(targetPosition - cameraPosition), Vector3.UnitY);
             return AddCamera(camera, xform);
             return AddCamera(camera, xform);
         }
         }
 
 
         public InstanceBuilder AddCamera(CameraBuilder camera, Matrix4x4 cameraWorldMatrix)
         public InstanceBuilder AddCamera(CameraBuilder camera, Matrix4x4 cameraWorldMatrix)
         {
         {
+            Guard.NotNull(camera, nameof(camera));
+
             var content = new CameraContent(camera);
             var content = new CameraContent(camera);
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new FixedTransformer(content, cameraWorldMatrix);
             instance.Content = new FixedTransformer(content, cameraWorldMatrix);
@@ -133,6 +157,8 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddLight(LightBuilder light, Matrix4x4 lightWorldMatrix)
         public InstanceBuilder AddLight(LightBuilder light, Matrix4x4 lightWorldMatrix)
         {
         {
+            Guard.NotNull(light, nameof(light));
+
             var content = new LightContent(light);
             var content = new LightContent(light);
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new FixedTransformer(content, lightWorldMatrix);
             instance.Content = new FixedTransformer(content, lightWorldMatrix);
@@ -142,6 +168,9 @@ namespace SharpGLTF.Scenes
 
 
         public InstanceBuilder AddLight(LightBuilder light, NodeBuilder node)
         public InstanceBuilder AddLight(LightBuilder light, NodeBuilder node)
         {
         {
+            Guard.NotNull(light, nameof(light));
+            Guard.NotNull(node, nameof(node));
+
             var content = new LightContent(light);
             var content = new LightContent(light);
             var instance = new InstanceBuilder(this);
             var instance = new InstanceBuilder(this);
             instance.Content = new RigidTransformer(content, node);
             instance.Content = new RigidTransformer(content, node);
@@ -149,6 +178,18 @@ namespace SharpGLTF.Scenes
             return instance;
             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)
         public void RenameAllNodes(string namePrefix)
         {
         {
             var allNodes = Instances
             var allNodes = Instances

+ 9 - 1
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -29,7 +29,15 @@ namespace SharpGLTF.Scenes
         {
         {
             Guard.NotNull(other, nameof(other));
             Guard.NotNull(other, nameof(other));
 
 
-            this._Content = other._Content;
+            if (other._Content is ICloneable cloneable)
+            {
+                this._Content = cloneable.Clone();
+            }
+            else
+            {
+                this._Content = other._Content;
+            }
+
             this._Morphings = other._Morphings?.Clone();
             this._Morphings = other._Morphings?.Clone();
         }
         }
 
 

+ 34 - 0
tests/SharpGLTF.Tests/Geometry/MeshBuilderTests.cs

@@ -7,6 +7,7 @@ using NUnit.Framework;
 
 
 using SharpGLTF.Geometry.VertexTypes;
 using SharpGLTF.Geometry.VertexTypes;
 using SharpGLTF.Schema2;
 using SharpGLTF.Schema2;
+using SharpGLTF.Geometry.Parametric;
 
 
 namespace SharpGLTF.Geometry
 namespace SharpGLTF.Geometry
 {
 {
@@ -275,7 +276,40 @@ namespace SharpGLTF.Geometry
             var indices = new[] { (0,1,2) } as IEnumerable<(int,int,int)>;
             var indices = new[] { (0,1,2) } as IEnumerable<(int,int,int)>;
 
 
             VertexBufferColumns.CalculateTangents(new[] { (vertices, indices) });
             VertexBufferColumns.CalculateTangents(new[] { (vertices, indices) });
+        }
+
+        [Test]
+        public void CloneMeshBuilder()
+        {
+            var material1 = Materials.MaterialBuilder.CreateDefault();
+            var material2 = Materials.MaterialBuilder.CreateDefault();
 
 
+            var mesh = new MeshBuilder<VertexPosition>();
+            mesh.AddCube(material1, Matrix4x4.Identity);
+            mesh.AddSphere(material2, 5, Matrix4x4.CreateTranslation(0, 10, 0));
+
+            var cloned1 = mesh.Clone( m => m.Clone() );
+
+            var primitivePairs = mesh.Primitives.Zip(cloned1.Primitives, (src, dst) => (src, dst));
+
+            foreach(var (src, dst) in primitivePairs)
+            {
+                Assert.AreNotSame(src.Material, dst.Material);
+                Assert.AreEqual(src.Triangles.Count, dst.Triangles.Count);
+
+                CollectionAssert.AreEqual(src.Triangles, dst.Triangles);
+            }
+
+            var material3 = Materials.MaterialBuilder.CreateDefault();
+            
+            // force all geometries to use a single material,
+            // which should result in a mesh with with all the primitives
+            // of the source mesh merged into a single primitive.
+            var cloned2 = cloned1.Clone(m => material3);
+
+            Assert.AreEqual(1, cloned2.Primitives.Count);
+            Assert.AreSame(material3, cloned2.Primitives.First().Material);
+            Assert.AreEqual(cloned1.Primitives.Sum(item => item.Triangles.Count), cloned2.Primitives.Sum(item => item.Triangles.Count));
         }
         }
     }
     }
 }
 }