Some cleanup and using new features / intellisense recommendations.

This commit is contained in:
Ottermandias 2024-01-06 18:37:52 +01:00
parent 51bb9cf7cd
commit 677c9bd801
5 changed files with 117 additions and 128 deletions

View file

@ -13,24 +13,17 @@ namespace Penumbra.Import.Models.Export;
public class MeshExporter
{
public class Mesh
public class Mesh(IEnumerable<IMeshBuilder<MaterialBuilder>> meshes, NodeBuilder[]? joints)
{
private IMeshBuilder<MaterialBuilder>[] _meshes;
private NodeBuilder[]? _joints;
public Mesh(IMeshBuilder<MaterialBuilder>[] meshes, NodeBuilder[]? joints)
{
_meshes = meshes;
_joints = joints;
}
public void AddToScene(SceneBuilder scene)
{
foreach (var mesh in _meshes)
if (_joints == null)
foreach (var mesh in meshes)
{
if (joints == null)
scene.AddRigidMesh(mesh, Matrix4x4.Identity);
else
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints);
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints);
}
}
}
@ -43,9 +36,11 @@ public class MeshExporter
private const byte MaximumMeshBufferStreams = 3;
private readonly MdlFile _mdl;
private readonly byte _lod;
private readonly ushort _meshIndex;
private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex];
private readonly byte _lod;
private readonly ushort _meshIndex;
private MdlStructs.MeshStruct XivMesh
=> _mdl.Meshes[_meshIndex];
private readonly Dictionary<ushort, int>? _boneIndexMap;
@ -53,10 +48,10 @@ public class MeshExporter
private readonly Type _materialType;
private readonly Type _skinningType;
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary<string, int>? boneNameMap)
{
_mdl = mdl;
_lod = lod;
_mdl = mdl;
_lod = lod;
_meshIndex = meshIndex;
if (boneNameMap != null)
@ -76,11 +71,12 @@ public class MeshExporter
if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null)
Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided.");
Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}");
Penumbra.Log.Debug(
$"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}");
}
/// <summary> Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. </summary>
private Dictionary<ushort, int>? BuildBoneIndexMap(Dictionary<string, int> boneNameMap)
/// <summary> Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. </summary>
private Dictionary<ushort, int>? BuildBoneIndexMap(IReadOnlyDictionary<string, int> boneNameMap)
{
// A BoneTableIndex of 255 means that this mesh is not skinned.
if (XivMesh.BoneTableIndex == 255)
@ -105,11 +101,10 @@ public class MeshExporter
/// <summary> Build glTF meshes for this XIV mesh. </summary>
private IMeshBuilder<MaterialBuilder>[] BuildMeshes()
{
var indices = BuildIndices();
var indices = BuildIndices();
var vertices = BuildVertices();
// NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base.
if (XivMesh.SubMeshCount == 0)
return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)];
@ -117,7 +112,8 @@ public class MeshExporter
.Skip(XivMesh.SubMeshIndex)
.Take(XivMesh.SubMeshCount)
.WithIndex()
.Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount))
.Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices,
(int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount))
.ToArray();
}
@ -161,7 +157,7 @@ public class MeshExporter
}
var primitiveVertices = meshBuilder.Primitives.First().Vertices;
var shapeNames = new List<string>();
var shapeNames = new List<string>();
foreach (var shape in _mdl.Shapes)
{
@ -177,24 +173,28 @@ public class MeshExporter
)
.Where(shapeValue =>
shapeValue.BaseIndicesIndex >= indexBase
&& shapeValue.BaseIndicesIndex < indexBase + indexCount
&& shapeValue.BaseIndicesIndex < indexBase + indexCount
)
.ToList();
if (shapeValues.Count == 0) continue;
if (shapeValues.Count == 0)
continue;
var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count);
shapeNames.Add(shape.ShapeName);
foreach (var shapeValue in shapeValues)
{
morphBuilder.SetVertex(
primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(),
vertices[shapeValue.ReplacingVertexIndex].GetGeometry()
);
}
}
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>() {
{"targetNames", shapeNames}
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
{
{ "targetNames", shapeNames },
});
return meshBuilder;
@ -249,23 +249,25 @@ public class MeshExporter
}
/// <summary> Read a vertex attribute of the specified type from a vertex buffer stream. </summary>
private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader)
private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.UInt => reader.ReadBytes(4),
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f),
MdlFile.VertexType.UInt => reader.ReadBytes(4),
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f,
reader.ReadByte() / 255f),
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()),
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(),
(float)reader.ReadHalf()),
_ => throw new ArgumentOutOfRangeException()
_ => throw new ArgumentOutOfRangeException(),
};
}
/// <summary> Get the vertex geometry type for this mesh's vertex usages. </summary>
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private static Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
{
if (!usages.ContainsKey(MdlFile.VertexUsage.Position))
throw new Exception("Mesh does not contain position vertex elements.");
@ -304,16 +306,16 @@ public class MeshExporter
}
/// <summary> Get the vertex material type for this mesh's vertex usages. </summary>
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private static Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
{
var uvCount = 0;
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type))
uvCount = type switch
{
MdlFile.VertexType.Half2 => 1,
MdlFile.VertexType.Half4 => 2,
MdlFile.VertexType.Half2 => 1,
MdlFile.VertexType.Half4 => 2,
MdlFile.VertexType.Single4 => 2,
_ => throw new Exception($"Unexpected UV vertex type {type}.")
_ => throw new Exception($"Unexpected UV vertex type {type}."),
};
var materialUsages = (
@ -323,11 +325,11 @@ public class MeshExporter
return materialUsages switch
{
(2, true) => typeof(VertexColor1Texture2),
(2, true) => typeof(VertexColor1Texture2),
(2, false) => typeof(VertexTexture2),
(1, true) => typeof(VertexColor1Texture1),
(1, true) => typeof(VertexColor1Texture1),
(1, false) => typeof(VertexTexture1),
(0, true) => typeof(VertexColor1),
(0, true) => typeof(VertexColor1),
(0, false) => typeof(VertexEmpty),
_ => throw new Exception("Unreachable."),
@ -377,7 +379,7 @@ public class MeshExporter
}
/// <summary> Get the vertex skinning type for this mesh's vertex usages. </summary>
private Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
{
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
return typeof(VertexJoints4);
@ -400,7 +402,8 @@ public class MeshExporter
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
var bindings = Enumerable.Range(0, 4)
.Select(bindingIndex => {
.Select(bindingIndex =>
{
// NOTE: I've not seen any files that throw this error that aren't completely broken.
var xivBoneIndex = indices[bindingIndex];
if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex))
@ -417,44 +420,44 @@ public class MeshExporter
/// <summary> Clamps any tangent W value other than 1 to -1. </summary>
/// <remarks> Some XIV models seemingly store -1 as 0, this patches over that. </remarks>
private Vector4 FixTangentVector(Vector4 tangent)
private static Vector4 FixTangentVector(Vector4 tangent)
=> tangent with { W = tangent.W == 1 ? 1 : -1 };
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private Vector2 ToVector2(object data)
private static Vector2 ToVector2(object data)
=> data switch
{
Vector2 v2 => v2,
Vector3 v3 => new Vector2(v3.X, v3.Y),
Vector4 v4 => new Vector2(v4.X, v4.Y),
_ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}")
_ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}"),
};
/// <summary> Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private Vector3 ToVector3(object data)
private static Vector3 ToVector3(object data)
=> data switch
{
Vector2 v2 => new Vector3(v2.X, v2.Y, 0),
Vector3 v3 => v3,
Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z),
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"),
};
/// <summary> Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. </summary>
private Vector4 ToVector4(object data)
private static Vector4 ToVector4(object data)
=> data switch
{
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1),
Vector4 v4 => v4,
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"),
};
/// <summary> Convert a vertex attribute value to a byte array. </summary>
private byte[] ToByteArray(object data)
private static byte[] ToByteArray(object data)
=> data switch
{
byte[] value => value,
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}")
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"),
};
}

View file

@ -6,26 +6,17 @@ namespace Penumbra.Import.Models.Export;
public class ModelExporter
{
public class Model
public class Model(List<MeshExporter.Mesh> meshes, GltfSkeleton? skeleton)
{
private List<MeshExporter.Mesh> _meshes;
private GltfSkeleton? _skeleton;
public Model(List<MeshExporter.Mesh> meshes, GltfSkeleton? skeleton)
{
_meshes = meshes;
_skeleton = skeleton;
}
public void AddToScene(SceneBuilder scene)
{
// If there's a skeleton, the root node should be added before we add any potentially skinned meshes.
var skeletonRoot = _skeleton?.Root;
var skeletonRoot = skeleton?.Root;
if (skeletonRoot != null)
scene.AddNode(skeletonRoot);
// Add all the meshes to the scene.
foreach (var mesh in _meshes)
foreach (var mesh in meshes)
mesh.AddToScene(scene);
}
}
@ -64,10 +55,8 @@ public class ModelExporter
NodeBuilder? root = null;
var names = new Dictionary<string, int>();
var joints = new List<NodeBuilder>();
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
foreach (var bone in skeleton.Bones)
{
var bone = skeleton.Bones[boneIndex];
if (names.ContainsKey(bone.Name)) continue;
var node = new NodeBuilder(bone.Name);
@ -93,10 +82,10 @@ public class ModelExporter
if (root == null)
return null;
return new()
return new GltfSkeleton
{
Root = root,
Joints = joints.ToArray(),
Joints = [.. joints],
Names = names,
};
}

View file

@ -3,14 +3,9 @@ using SharpGLTF.Scenes;
namespace Penumbra.Import.Models.Export;
/// <summary> Representation of a skeleton within XIV. </summary>
public class XivSkeleton
public class XivSkeleton(XivSkeleton.Bone[] bones)
{
public Bone[] Bones;
public XivSkeleton(Bone[] bones)
{
Bones = bones;
}
public Bone[] Bones = bones;
public struct Bone
{

View file

@ -16,17 +16,18 @@ public static unsafe class HavokConverter
/// <param name="hkx"> A byte array representing the .hkx file. </param>
public static string HkxToXml(byte[] hkx)
{
const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
| hkSerializeUtil.SaveOptionBits.TextFormat
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
var tempHkx = CreateTempFile();
File.WriteAllBytes(tempHkx, hkx);
var resource = Read(tempHkx);
File.Delete(tempHkx);
if (resource == null) throw new Exception("Failed to read havok file.");
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
| hkSerializeUtil.SaveOptionBits.TextFormat
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
if (resource == null)
throw new Exception("Failed to read havok file.");
var file = Write(resource, options);
file.Close();
@ -41,17 +42,19 @@ public static unsafe class HavokConverter
/// <param name="xml"> A string representing the .xml file. </param>
public static byte[] XmlToHkx(string xml)
{
const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
var tempXml = CreateTempFile();
File.WriteAllText(tempXml, xml);
var resource = Read(tempXml);
File.Delete(tempXml);
if (resource == null) throw new Exception("Failed to read havok file.");
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
if (resource == null)
throw new Exception("Failed to read havok file.");
g
var file = Write(resource, options);
file.Close();
@ -74,7 +77,7 @@ public static unsafe class HavokConverter
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1];
loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default };
loadOptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int> { Storage = (int)hkSerializeUtil.LoadOptionBits.Default };
loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
@ -92,37 +95,42 @@ public static unsafe class HavokConverter
)
{
var tempFile = CreateTempFile();
var path = Marshal.StringToHGlobalAnsi(tempFile);
var oStream = new hkOstream();
var path = Marshal.StringToHGlobalAnsi(tempFile);
var oStream = new hkOstream();
oStream.Ctor((byte*)path);
var result = stackalloc hkResult[1];
var saveOptions = new hkSerializeUtil.SaveOptions()
{
Flags = new() { Storage = (int)optionBits }
Flags = new hkFlags<hkSerializeUtil.SaveOptionBits, int> { Storage = (int)optionBits },
};
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
try
{
var name = "hkRootLevelContainer";
const string name = "hkRootLevelContainer";
var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry);
if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource.");
if (resourcePtr == null)
throw new Exception("Failed to retrieve havok root level container resource.");
var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name);
if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type.");
if (hkRootLevelContainerClass == null)
throw new Exception("Failed to retrieve havok root level container type.");
hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions);
}
finally { oStream.Dtor(); }
finally
{
oStream.Dtor();
}
if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file.");
if (result->Result == hkResult.hkResultEnum.Failure)
throw new Exception("Failed to serialize havok file.");
return new FileStream(tempFile, FileMode.Open);
}

View file

@ -15,16 +15,15 @@ public static class SkeletonConverter
var mainSkeletonId = GetMainSkeletonId(document);
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']");
if (skeletonNode == null)
throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}.");
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']")
?? throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}.");
var referencePose = ReadReferencePose(skeletonNode);
var parentIndices = ReadParentIndices(skeletonNode);
var boneNames = ReadBoneNames(skeletonNode);
var boneNames = ReadBoneNames(skeletonNode);
if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length)
throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})");
throw new InvalidDataException(
$"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})");
var bones = referencePose
.Zip(parentIndices, boneNames)
@ -33,9 +32,9 @@ public static class SkeletonConverter
var (transform, parentIndex, name) = values;
return new XivSkeleton.Bone()
{
Transform = transform,
Transform = transform,
ParentIndex = parentIndex,
Name = name,
Name = name,
};
})
.ToArray();
@ -63,14 +62,14 @@ public static class SkeletonConverter
{
return ReadArray(
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
node =>
n =>
{
var raw = ReadVec12(node);
var raw = ReadVec12(n);
return new XivSkeleton.Transform()
{
Translation = new(raw[0], raw[1], raw[2]),
Rotation = new(raw[4], raw[5], raw[6], raw[7]),
Scale = new(raw[8], raw[9], raw[10]),
Translation = new Vector3(raw[0], raw[1], raw[2]),
Rotation = new Quaternion(raw[4], raw[5], raw[6], raw[7]),
Scale = new Vector3(raw[8], raw[9], raw[10]),
};
}
);
@ -82,11 +81,11 @@ public static class SkeletonConverter
{
var array = node.ChildNodes
.Cast<XmlNode>()
.Where(node => node.NodeType != XmlNodeType.Comment)
.Select(node =>
.Where(n => n.NodeType != XmlNodeType.Comment)
.Select(n =>
{
var text = node.InnerText.Trim()[1..];
// TODO: surely there's a less shit way to do this i mean seriously
var text = n.InnerText.Trim()[1..];
// TODO: surely there's a less shit way to do this I mean seriously
return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber)));
})
.ToArray();
@ -100,24 +99,20 @@ public static class SkeletonConverter
/// <summary> Read the bone parent relations for a skeleton. </summary>
/// <param name="node"> XML node for the skeleton. </param>
private static int[] ReadParentIndices(XmlNode node)
{
// todo: would be neat to genericise array between bare and children
return CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
=> CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
.InnerText
.Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Split((char[]) [' ', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(int.Parse)
.ToArray();
}
/// <summary> Read the names of bones in a skeleton. </summary>
/// <param name="node"> XML node for the skeleton. </param>
private static string[] ReadBoneNames(XmlNode node)
{
return ReadArray(
=> ReadArray(
CheckExists(node.SelectSingleNode("array[@name='bones']")),
node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText
n => CheckExists(n.SelectSingleNode("string[@name='name']")).InnerText
);
}
/// <summary> Read an XML tagfile array, converting it via the provided conversion function. </summary>
/// <param name="node"> Tagfile XML array node. </param>
@ -125,10 +120,9 @@ public static class SkeletonConverter
private static T[] ReadArray<T>(XmlNode node, Func<XmlNode, T> convert)
{
var element = (XmlElement)node;
var size = int.Parse(element.GetAttribute("size"));
var array = new T[size];
var size = int.Parse(element.GetAttribute("size"));
var array = new T[size];
foreach (var (childNode, index) in element.ChildNodes.Cast<XmlElement>().WithIndex())
array[index] = convert(childNode);