static int ExportIndices(Vrm10Storage storage, BufferAccessor x, int offset, int count, ExportArgs option) { if (x.Count <= ushort.MaxValue) { if (x.ComponentType == AccessorValueType.UNSIGNED_INT) { // ensure ushort var src = x.GetSpan <UInt32>().Slice(offset, count); var bytes = new byte[src.Length * 2]; var dst = SpanLike.Wrap <UInt16>(new ArraySegment <byte>(bytes)); for (int i = 0; i < src.Length; ++i) { dst[i] = (ushort)src[i]; } var accessor = new BufferAccessor(new ArraySegment <byte>(bytes), AccessorValueType.UNSIGNED_SHORT, AccessorVectorType.SCALAR, count); return(accessor.AddAccessorTo(storage, 0, option.sparse, null, 0, count)); } else { return(x.AddAccessorTo(storage, 0, option.sparse, null, offset, count)); } } else { return(x.AddAccessorTo(storage, 0, option.sparse, null, offset, count)); } }
public static Model Read(UniGLTF.GltfParser parser) { var storage = new Vrm10Storage(parser); var model = Load(storage, Path.GetFileName(parser.TargetPath)); model.ConvertCoordinate(Coordinates.Unity); return(model); }
public static Model Read(UniGLTF.GltfData data, Coordinates?coords = default) { var storage = new Vrm10Storage(data); var model = Load(storage, Path.GetFileName(data.TargetPath), coords.GetValueOrDefault(Coordinates.Vrm1)); model.ConvertCoordinate(Coordinates.Unity); return(model); }
public static Model CreateVrmModel(GltfParser parser) { var storage = new Vrm10Storage(parser); var model = ModelLoader.Load(storage, Path.GetFileName(parser.TargetPath)); model.ConvertCoordinate(Coordinates.Unity); return(model); }
public static VertexBuffer FromGltf(this gltfMorphTarget target, Vrm10Storage storage) { var b = new VertexBuffer(); storage.CreateBufferAccessorAndAdd(target.POSITION, b, VertexBuffer.PositionKey); storage.CreateBufferAccessorAndAdd(target.NORMAL, b, VertexBuffer.NormalKey); storage.CreateBufferAccessorAndAdd(target.TANGENT, b, VertexBuffer.TangentKey); return(b); }
public static glTFImage ToGltf(this VrmLib.Image src, Vrm10Storage storage) { var viewIndex = storage.AppendToBuffer(0, src.Bytes, 1); var gltf = storage.Gltf; return(new glTFImage { name = src.Name, mimeType = src.MimeType, bufferView = viewIndex, }); }
static glTFImage GetNormalImage(Vrm10Storage storage, glTFMaterial m) { if (m.normalTexture == null) { return(null); } if (!m.normalTexture.index.TryGetValidIndex(storage.TextureCount, out int index)) { return(null); } return(GetTexture(storage, index)); }
public static MeshGroup FromGltf(this glTFMesh x, Vrm10Storage storage) { var group = new MeshGroup(x.name); if (x.primitives.Count == 1) { var primitive = x.primitives[0]; var mesh = primitive.FromGltf(storage, x); var materialIndex = primitive.material; mesh.Submeshes.Add( new Submesh(0, mesh.IndexBuffer.Count, materialIndex)); group.Meshes.Add(mesh); } else if (!x.AllPrimitivesHasSameVertexBuffer()) { int offset = 0; foreach (var primitive in x.primitives) { var mesh = primitive.FromGltf(storage, x); var materialIndex = primitive.material; mesh.Submeshes.Add( new Submesh(offset, mesh.IndexBuffer.Count, materialIndex)); offset += mesh.IndexBuffer.Count; group.Meshes.Add(mesh); } } else { // // obsolete // // for VRM var mesh = x.SharedBufferFromGltf(storage); int offset = 0; foreach (var primitive in x.primitives) { var materialIndex = primitive.material; var count = storage.Gltf.accessors[primitive.indices].count; mesh.Submeshes.Add( new Submesh(offset, count, materialIndex)); offset += count; } group.Meshes.Add(mesh); } return(group); }
public static int AddViewTo(this VrmLib.BufferAccessor self, Vrm10Storage storage, int bufferIndex, int offset = 0, int count = 0) { var stride = self.Stride; if (count == 0) { count = self.Count; } var slice = self.Bytes.Slice(offset * stride, count * stride); return(storage.AppendToBuffer(slice)); }
static glTFImage GetTexture(Vrm10Storage storage, int index) { if (index < 0 || index >= storage.Gltf.textures.Count) { return(null); } var texture = storage.Gltf.textures[index]; if (texture.source < 0 || texture.source >= storage.Gltf.images.Count) { return(null); } return(storage.Gltf.images[texture.source]); }
static Mesh FromGltf(Vrm10Storage storage, glTFMesh x, glTFPrimitives primitive, bool isShared) { var mesh = new Mesh((TopologyType)primitive.mode) { VertexBuffer = primitive.attributes.FromGltf(storage) }; if (isShared) { // create joined index buffer mesh.IndexBuffer = storage.CreateAccessor(x.primitives.Select(y => y.indices).ToArray()); } else { mesh.IndexBuffer = storage.CreateAccessor(primitive.indices); } { gltf_mesh_extras_targetNames.TryGet(x, out List <string> targetNames); for (int i = 0; i < primitive.targets.Count; ++i) { var gltfTarget = primitive.targets[i]; string targetName = null; { targetName = targetNames[i]; } var target = new MorphTarget(targetName) { VertexBuffer = gltfTarget.FromGltf(storage) }; // validate count foreach (var kv in target.VertexBuffer) { if (kv.Value.Count != mesh.VertexBuffer.Count) { throw new Exception(); } } mesh.MorphTargets.Add(target); } } return(mesh); }
public static int AddAccessorTo(this VrmLib.BufferAccessor self, Vrm10Storage storage, int viewIndex, Action <ArraySegment <byte>, glTFAccessor> minMax = null, int offset = 0, int count = 0) { var gltf = storage.Gltf; var accessorIndex = gltf.accessors.Count; var accessor = self.CreateGltfAccessor(viewIndex, count, offset * self.Stride); if (minMax != null) { minMax(self.Bytes, accessor); } gltf.accessors.Add(accessor); return(accessorIndex); }
static glTFImage GetColorImage(Vrm10Storage storage, glTFMaterial m) { if (m.pbrMetallicRoughness == null) { return(null); } if (m.pbrMetallicRoughness.baseColorTexture == null) { return(null); } if (!m.pbrMetallicRoughness.baseColorTexture.index.TryGetValidIndex(storage.TextureCount, out int index)) { return(null); } return(GetTexture(storage, index)); }
private Model CreateGlbModel(string path) { var bytes = File.ReadAllBytes(path); if (!VrmLib.Glb.TryParse(bytes, out VrmLib.Glb glb, out Exception ex)) { throw ex; } VrmLib.Model model = null; VrmLib.IVrmStorage storage; storage = new Vrm10Storage(glb.Json.Bytes, glb.Binary.Bytes); model = VrmLib.ModelLoader.Load(storage, Path.GetFileNameWithoutExtension(path)); model.ConvertCoordinate(VrmLib.Coordinates.Unity); return(model); }
public static VertexBuffer FromGltf(this glTFAttributes attributes, Vrm10Storage storage) { var b = new VertexBuffer(); if (storage.TryCreateAccessor(attributes.POSITION, out BufferAccessor position)) { b.Add(VertexBuffer.PositionKey, position); } else { // position required throw new Exception(); } if (storage.TryCreateAccessor(attributes.NORMAL, out BufferAccessor normal)) { b.Add(VertexBuffer.NormalKey, normal); } if (storage.TryCreateAccessor(attributes.COLOR_0, out BufferAccessor color)) { b.Add(VertexBuffer.ColorKey, color); } if (storage.TryCreateAccessor(attributes.TEXCOORD_0, out BufferAccessor tex0)) { b.Add(VertexBuffer.TexCoordKey, tex0); } if (storage.TryCreateAccessor(attributes.TEXCOORD_1, out BufferAccessor tex1)) { b.Add(VertexBuffer.TexCoordKey2, tex1); } // if(storage.TryCreateAccessor(attributes.TANGENT, out BufferAccessor tangent))b.Add(VertexBuffer.TangentKey, tangent); if (storage.TryCreateAccessor(attributes.WEIGHTS_0, out BufferAccessor weights)) { b.Add(VertexBuffer.WeightKey, weights); } if (storage.TryCreateAccessor(attributes.JOINTS_0, out BufferAccessor joints)) { b.Add(VertexBuffer.JointKey, joints); } return(b); }
public static VrmLib.Image FromGltf(this glTFImage x, Vrm10Storage storage) { if (x.bufferView == -1) { // 外部参照? throw new Exception(); } var view = storage.Gltf.bufferViews[x.bufferView]; var buffer = storage.Gltf.buffers[view.buffer]; // テクスチャの用途を調べる var usage = default(VrmLib.ImageUsage); foreach (var material in storage.Gltf.materials) { var colorImage = GetColorImage(storage, material); if (colorImage == x) { usage |= VrmLib.ImageUsage.Color; } var normalImage = GetNormalImage(storage, material); if (normalImage == x) { usage |= VrmLib.ImageUsage.Normal; } } var memory = storage.GetBufferBytes(buffer); return(new VrmLib.Image(x.name, x.mimeType, usage, memory.Slice(view.byteOffset, view.byteLength))); }
public static Model CreateVrmModel(byte[] bytes, FileInfo path) { if (!Glb.TryParse(bytes, out Glb glb, out Exception ex)) { throw ex; } var json = glb.Json.Bytes.ParseAsJson(); var extensions = json["extensions"]; foreach (var kv in extensions.ObjectItems()) { switch (kv.Key.GetString()) { // case "VRM": // { // var storage = new Vrm10Storage(glb.Json.Bytes, glb.Binary.Bytes); // var model = ModelLoader.Load(storage, path.Name); // model.ConvertCoordinate(Coordinates.Unity); // return model; // } case "VRMC_vrm": { var storage = new Vrm10Storage(glb.Json.Bytes, glb.Binary.Bytes); var model = ModelLoader.Load(storage, path.Name); model.ConvertCoordinate(Coordinates.Unity); return(model); } } } // this is error // throw new NotImplementedException(); return(null); }
/// <summary> /// ModelExporter.Export で作られた Model.MeshGroups[*] を GLTF 化する /// </summary> /// <param name="src"></param> /// <param name="materials"></param> /// <param name="storage"></param> /// <param name="option"></param> /// <returns></returns> public static glTFMesh ExportMeshGroup(this MeshGroup src, List <object> materials, Vrm10Storage storage, ExportArgs option) { var gltfMesh = new glTFMesh { name = src.Name }; if (src.Meshes.Count != 1) { throw new NotImplementedException(); } foreach (var prim in src.Meshes[0].ExportMeshDivided(materials, storage, option)) { gltfMesh.primitives.Add(prim); } var targetNames = src.Meshes[0].MorphTargets.Select(x => x.Name).ToArray(); gltf_mesh_extras_targetNames.Serialize(gltfMesh, targetNames); return(gltfMesh); }
static Model Load(Vrm10Storage storage, string rootName, Coordinates coords) { if (storage == null) { return(null); } var model = new Model(coords) { AssetVersion = storage.AssetVersion, AssetGenerator = storage.AssetGenerator, AssetCopyright = storage.AssetCopyright, AssetMinVersion = storage.AssetMinVersion, Coordinates = coords, }; // node model.Root.Name = rootName; for (var i = 0; i < storage.NodeCount; ++i) { var node = storage.CreateNode(i); model.Nodes.Add(node); } for (var i = 0; i < model.Nodes.Count; ++i) { var parent = model.Nodes[i]; foreach (var j in storage.GetChildNodeIndices(i)) { var child = model.Nodes[j]; parent.Add(child); } } foreach (var x in model.Nodes) { if (x.Parent == null) { model.Root.Add(x); } } // skin model.Skins.AddRange(Enumerable.Range(0, storage.SkinCount).Select(x => storage.CreateSkin(x, model.Nodes))); // mesh model.MeshGroups.AddRange(Enumerable.Range(0, storage.MeshCount).Select(x => storage.CreateMesh(x))); // skin for (int i = 0; i < storage.NodeCount; ++i) { var(meshIndex, skinIndex) = storage.GetNodeMeshSkin(i); if (meshIndex >= 0 && meshIndex < model.MeshGroups.Count) { var node = model.Nodes[i]; var mesh = model.MeshGroups[meshIndex]; node.MeshGroup = mesh; if (skinIndex >= 0 && skinIndex < model.Skins.Count) { var skin = model.Skins[skinIndex]; mesh.Skin = skin; } } } return(model); }
/// <summary> /// VertexBufferはひとつでIndexBufferの参照が異なる /// /// VertexBuffer /// +----------------------------------+ /// | | /// +----------------------------------+ /// A A A /// | | | /// +---------+--------+--------+ /// | submesh0|submesh1|submesh2| /// +---------+--------+--------+ /// IndexBuffer /// </summary> public static Mesh SharedBufferFromGltf(this glTFMesh x, Vrm10Storage storage) { // 先頭を使う return(FromGltf(storage, x, x.primitives[0], true)); }
public static glTFMesh ExportMeshGroup(this MeshGroup src, List <object> materials, Vrm10Storage storage, ExportArgs option) { var mesh = new glTFMesh { name = src.Name }; foreach (var x in src.Meshes) { // MeshとSubmeshがGltfのPrimitiveに相当する? x.ExportMesh(materials, storage, mesh, option); } return(mesh); }
public static int AddAccessorTo(this VrmLib.BufferAccessor self, Vrm10Storage storage, int bufferIndex, // GltfBufferTargetType targetType, bool useSparse, Action <ArraySegment <byte>, glTFAccessor> minMax = null, int offset = 0, int count = 0) { if (self.ComponentType == VrmLib.AccessorValueType.FLOAT && self.AccessorType == VrmLib.AccessorVectorType.VEC3 ) { var values = self.GetSpan <Vector3>(); // 巨大ポリゴンのモデル対策にValueTupleの型をushort -> uint へ var sparseValuesWithIndex = new List <ValueTuple <int, Vector3> >(); for (int i = 0; i < values.Length; ++i) { var v = values[i]; if (v != Vector3.Zero) { sparseValuesWithIndex.Add((i, v)); } } //var status = $"{sparseIndices.Count * 14}/{values.Length * 12}"; if (useSparse && sparseValuesWithIndex.Count > 0 && // avoid empty sparse sparseValuesWithIndex.Count * 16 < values.Length * 12) { // use sparse var sparseIndexBin = new ArraySegment <byte>(new byte[sparseValuesWithIndex.Count * 4]); var sparseIndexSpan = SpanLike.Wrap <Int32>(sparseIndexBin); var sparseValueBin = new ArraySegment <byte>(new byte[sparseValuesWithIndex.Count * 12]); var sparseValueSpan = SpanLike.Wrap <Vector3>(sparseValueBin); for (int i = 0; i < sparseValuesWithIndex.Count; ++i) { var(index, value) = sparseValuesWithIndex[i]; sparseIndexSpan[i] = index; sparseValueSpan[i] = value; } var sparseIndexView = storage.AppendToBuffer(sparseIndexBin); var sparseValueView = storage.AppendToBuffer(sparseValueBin); var accessorIndex = storage.Gltf.accessors.Count; var accessor = new glTFAccessor { componentType = (glComponentType)self.ComponentType, type = self.AccessorType.ToString(), count = self.Count, byteOffset = -1, sparse = new glTFSparse { count = sparseValuesWithIndex.Count, indices = new glTFSparseIndices { componentType = (glComponentType)VrmLib.AccessorValueType.UNSIGNED_INT, bufferView = sparseIndexView, }, values = new glTFSparseValues { bufferView = sparseValueView, }, } }; if (minMax != null) { minMax(sparseValueBin, accessor); } storage.Gltf.accessors.Add(accessor); return(accessorIndex); } } var viewIndex = self.AddViewTo(storage, bufferIndex, offset, count); return(self.AddAccessorTo(storage, viewIndex, minMax, 0, count)); }
/// <summary> /// IndexBuffer毎に異なるVertexBufferを参照する /// /// VertexBuffer /// +--------+ +--------+ +--------+ /// |0 | |1 | |2 | /// +--------+ +--------+ +--------+ /// A A A /// | | | /// +---------+--------+--------+ /// | submesh0|submesh1|submesh2| /// +---------+--------+--------+ /// IndexBuffer /// </summary> public static Mesh FromGltf(this glTFPrimitives primitive, Vrm10Storage storage, glTFMesh x) { return(FromGltf(storage, x, primitive, false)); }
public static IEnumerable <(glTFNode, glTFSkin)> ExportNodes(List <Node> nodes, List <MeshGroup> groups, Vrm10Storage storage, ExportArgs option) { foreach (var node in nodes) { var gltfNode = new glTFNode { name = node.Name, }; glTFSkin gltfSkin = default; gltfNode.translation = node.LocalTranslation.ToFloat3(); gltfNode.rotation = node.LocalRotation.ToFloat4(); gltfNode.scale = node.LocalScaling.ToFloat3(); if (node.MeshGroup != null) { gltfNode.mesh = groups.IndexOfThrow(node.MeshGroup); var skin = node.MeshGroup.Skin; if (skin != null) { gltfSkin = new glTFSkin() { joints = skin.Joints.Select(joint => nodes.IndexOfThrow(joint)).ToArray() }; if (skin.InverseMatrices == null) { skin.CalcInverseMatrices(); } if (skin.InverseMatrices != null) { gltfSkin.inverseBindMatrices = skin.InverseMatrices.AddAccessorTo(storage, 0, option.sparse); } if (skin.Root != null) { gltfSkin.skeleton = nodes.IndexOf(skin.Root); } } } gltfNode.children = node.Children.Select(child => nodes.IndexOfThrow(child)).ToArray(); yield return(gltfNode, gltfSkin); } }
/// <summary> /// https://github.com/vrm-c/UniVRM/issues/800 /// /// SubMesh 単位に分割する。 /// SubMesh を Gltf の Primitive に対応させる。 /// </summary> /// <param name="mesh"></param> /// <param name="materials"></param> /// <param name="storage"></param> /// <param name="gltfMesh"></param> /// <param name="option"></param> static IEnumerable <glTFPrimitives> ExportMeshDivided(this VrmLib.Mesh mesh, List <object> materials, Vrm10Storage storage, ExportArgs option) { var bufferIndex = 0; var usedIndices = new List <int>(); var meshIndices = SpanLike.CopyFrom(mesh.IndexBuffer.GetAsIntArray()); var positions = mesh.VertexBuffer.Positions.GetSpan <UnityEngine.Vector3>().ToArray(); var normals = mesh.VertexBuffer.Normals.GetSpan <UnityEngine.Vector3>().ToArray(); var uv = mesh.VertexBuffer.TexCoords.GetSpan <UnityEngine.Vector2>().ToArray(); var hasSkin = mesh.VertexBuffer.Weights != null; var weights = mesh.VertexBuffer.Weights?.GetSpan <UnityEngine.Vector4>().ToArray(); var joints = mesh.VertexBuffer.Joints?.GetSpan <SkinJoints>().ToArray(); Func <int, int> getJointIndex = default; if (hasSkin) { getJointIndex = i => { return(i); }; } foreach (var submesh in mesh.Submeshes) { var indices = meshIndices.Slice(submesh.Offset, submesh.DrawCount).ToArray(); var hash = new HashSet <int>(indices); // mesh // index の順に attributes を蓄える var buffer = new MeshExportUtil.VertexBuffer(indices.Length, getJointIndex); usedIndices.Clear(); for (int k = 0; k < positions.Length; ++k) { if (hash.Contains(k)) { // indices から参照される頂点だけを蓄える usedIndices.Add(k); buffer.Push(k, positions[k], normals[k], uv[k]); if (getJointIndex != null) { var j = joints[k]; var w = weights[k]; var boneWeight = new UnityEngine.BoneWeight { boneIndex0 = j.Joint0, boneIndex1 = j.Joint1, boneIndex2 = j.Joint2, boneIndex3 = j.Joint3, weight0 = w.x, weight1 = w.y, weight2 = w.z, weight3 = w.w, }; buffer.Push(boneWeight); } } } var materialIndex = submesh.Material; var gltfPrimitive = buffer.ToGltfPrimitive(storage.Gltf, bufferIndex, materialIndex, indices); // blendShape for (int j = 0; j < mesh.MorphTargets.Count; ++j) { var blendShape = new MeshExportUtil.BlendShapeBuffer(indices.Length); // index の順に attributes を蓄える var morph = mesh.MorphTargets[j]; var blendShapePositions = morph.VertexBuffer.Positions.GetSpan <UnityEngine.Vector3>(); SpanLike <UnityEngine.Vector3>?blendShapeNormals = default; if (morph.VertexBuffer.Normals != null) { blendShapeNormals = morph.VertexBuffer.Normals.GetSpan <UnityEngine.Vector3>(); } foreach (var k in usedIndices) { blendShape.Push( blendShapePositions[k], blendShapeNormals.HasValue ? blendShapeNormals.Value[k] : UnityEngine.Vector3.zero ); } gltfPrimitive.targets.Add(blendShape.ToGltf(storage.Gltf, bufferIndex, !option.removeMorphNormal)); } yield return(gltfPrimitive); } }
static void ExportMesh(this Mesh mesh, List <object> materials, Vrm10Storage storage, glTFMesh gltfMesh, ExportArgs option) { // // primitive share vertex buffer // var attributeAccessorIndexMap = mesh.VertexBuffer .ToDictionary( kv => kv.Key, kv => kv.Value.AddAccessorTo( storage, 0, option.sparse, kv.Key == VertexBuffer.PositionKey ? (Action <ArraySegment <byte>, glTFAccessor>)Vec3MinMax : null ) ); List <Dictionary <string, int> > morphTargetAccessorIndexMapList = null; if (mesh.MorphTargets.Any()) { morphTargetAccessorIndexMapList = new List <Dictionary <string, int> >(); foreach (var morphTarget in mesh.MorphTargets) { var dict = new Dictionary <string, int>(); foreach (var kv in morphTarget.VertexBuffer) { if (option.removeTangent && kv.Key == VertexBuffer.TangentKey) { // remove tangent continue; } if (option.removeMorphNormal && kv.Key == VertexBuffer.NormalKey) { // normal normal continue; } if (kv.Value.Count != mesh.VertexBuffer.Count) { throw new Exception("inavlid data"); } var accessorIndex = kv.Value.AddAccessorTo(storage, 0, option.sparse, kv.Key == VertexBuffer.PositionKey ? (Action <ArraySegment <byte>, glTFAccessor>)Vec3MinMax : null); dict.Add(kv.Key, accessorIndex); } morphTargetAccessorIndexMapList.Add(dict); } } var drawCountOffset = 0; foreach (var y in mesh.Submeshes) { // index // slide index buffer accessor var indicesAccessorIndex = ExportIndices(storage, mesh.IndexBuffer, drawCountOffset, y.DrawCount, option); drawCountOffset += y.DrawCount; var prim = new glTFPrimitives { mode = (int)mesh.Topology, material = y.Material, indices = indicesAccessorIndex, attributes = new glTFAttributes(), }; gltfMesh.primitives.Add(prim); // attribute foreach (var kv in mesh.VertexBuffer) { var attributeAccessorIndex = attributeAccessorIndexMap[kv.Key]; switch (kv.Key) { case VertexBuffer.PositionKey: prim.attributes.POSITION = attributeAccessorIndex; break; case VertexBuffer.NormalKey: prim.attributes.NORMAL = attributeAccessorIndex; break; case VertexBuffer.ColorKey: prim.attributes.COLOR_0 = attributeAccessorIndex; break; case VertexBuffer.TexCoordKey: prim.attributes.TEXCOORD_0 = attributeAccessorIndex; break; case VertexBuffer.TexCoordKey2: prim.attributes.TEXCOORD_1 = attributeAccessorIndex; break; case VertexBuffer.JointKey: prim.attributes.JOINTS_0 = attributeAccessorIndex; break; case VertexBuffer.WeightKey: prim.attributes.WEIGHTS_0 = attributeAccessorIndex; break; } } // morph target if (mesh.MorphTargets.Any()) { foreach (var(t, accessorIndexMap) in Enumerable.Zip(mesh.MorphTargets, morphTargetAccessorIndexMapList, (t, v) => (t, v))) { var target = new gltfMorphTarget(); prim.targets.Add(target); foreach (var kv in t.VertexBuffer) { if (!accessorIndexMap.TryGetValue(kv.Key, out int targetAccessorIndex)) { continue; } switch (kv.Key) { case VertexBuffer.PositionKey: target.POSITION = targetAccessorIndex; break; case VertexBuffer.NormalKey: target.NORMAL = targetAccessorIndex; break; case VertexBuffer.TangentKey: target.TANGENT = targetAccessorIndex; break; default: throw new NotImplementedException(); } } } } } // target name if (mesh.MorphTargets.Any()) { gltf_mesh_extras_targetNames.Serialize(gltfMesh, mesh.MorphTargets.Select(z => z.Name)); } }