Skip to content

Commit 10d4ece

Browse files
committed
Add fbxskel export and conversion support
1 parent 2344ffa commit 10d4ece

File tree

3 files changed

+99
-19
lines changed

3 files changed

+99
-19
lines changed

ContentEditor.App/CustomizedFileLoaders/CommonMeshResource.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public partial class CommonMeshResource(string Name, Workspace workspace) : IRes
1818

1919
public Assimp.Scene Scene
2020
{
21-
get => _scene ??= ConvertMeshToAssimpScene(NativeMesh, Name, false, false, false, false);
21+
get => _scene ??= ConvertMeshToAssimpScene(NativeMesh, Name, false, false, false, false, null);
2222
set => _scene = value;
2323
}
2424

@@ -80,26 +80,37 @@ public void WriteTo(string filepath)
8080
NativeMesh.WriteTo(filepath);
8181
}
8282

83-
public void ExportToFile(string filepath, bool includeLodsShadows, bool includeOcc, IEnumerable<MotFileBase>? mots = null)
83+
public void ExportToFile(
84+
string filepath,
85+
bool includeLodsShadows,
86+
bool includeOcc,
87+
FbxSkelFile? skeleton = null,
88+
IEnumerable<MotFileBase>? mots = null,
89+
IEnumerable<CommonMeshResource>? additionalMeshes = null
90+
)
8491
{
8592
using AssimpContext context = new AssimpContext();
8693

8794
var ext = PathUtils.GetExtensionWithoutPeriod(filepath);
8895
string exportFormat = context.GetFormatIDFromExtension(ext);
89-
var scene = GetSceneForExport(ext, false, includeLodsShadows, includeOcc);
90-
if (mots == null || !mots.Any()) {
91-
context.ExportFile(scene, filepath, exportFormat);
92-
return;
93-
}
96+
var scene = GetSceneForExport(ext, false, includeLodsShadows, includeOcc, skeleton);
9497

9598
if (_mesh == null) {
9699
Logger.Error("Missing mesh file, can't export");
97100
return;
98101
}
99102

100-
foreach (var mot in mots) {
101-
if (mot is MotFile mm) {
102-
AddMotToScene(scene, mm, ext);
103+
if (mots != null) {
104+
foreach (var mot in mots) {
105+
if (mot is MotFile mm) {
106+
AddMotToScene(scene, mm, ext);
107+
}
108+
}
109+
}
110+
if (additionalMeshes?.Any() == true) {
111+
foreach (var addm in additionalMeshes) {
112+
var extra = addm.GetSceneForExport(ext, false, includeLodsShadows, includeOcc, skeleton);
113+
scene.Meshes.AddRange(extra.Meshes);
103114
}
104115
}
105116
context.ExportFile(scene, filepath, exportFormat);
@@ -135,12 +146,12 @@ internal void PreloadMeshBuffers()
135146

136147
}
137148

138-
private Assimp.Scene GetSceneForExport(string targetFileExtension, bool allowCache, bool includeLodsShadows, bool includeOcc)
149+
private Assimp.Scene GetSceneForExport(string targetFileExtension, bool allowCache, bool includeLodsShadows, bool includeOcc, FbxSkelFile? skeleton = null)
139150
{
140151
var isGltf = targetFileExtension == "glb" || targetFileExtension == "gltf";
141152
if (allowCache && _scene != null && !isGltf) return _scene;
142153

143-
Assimp.Scene scene = ConvertMeshToAssimpScene(NativeMesh, Name, isGltf, includeLodsShadows, includeLodsShadows, includeOcc);
154+
Assimp.Scene scene = ConvertMeshToAssimpScene(NativeMesh, Name, isGltf, includeLodsShadows, includeLodsShadows, includeOcc, skeleton);
144155
if (allowCache) _scene = scene;
145156
return scene;
146157
}

ContentEditor.App/CustomizedFileLoaders/MeshConversion/AssimpMeshExport.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private static void AddMotToScene(Assimp.Scene scene, MotFile mot, string export
130130
scene.Animations.Add(anim);
131131
}
132132

133-
private static Assimp.Scene ConvertMeshToAssimpScene(MeshFile file, string rootName, bool isGltf, bool includeAllLods, bool includeShadows, bool includeOcclusion)
133+
private static Assimp.Scene ConvertMeshToAssimpScene(MeshFile file, string rootName, bool isGltf, bool includeAllLods, bool includeShadows, bool includeOcclusion, FbxSkelFile? skeleton)
134134
{
135135
// NOTE: every matrix needs to be transposed, assimp expects them transposed compared to default System.Numeric.Matrix4x4 for some shit ass reason
136136
// NOTE2: assimp currently forces vert deduplication for gltf export so we may lose some vertices (https://github.com/assimp/assimp/issues/6349)
@@ -151,6 +151,7 @@ private static Assimp.Scene ConvertMeshToAssimpScene(MeshFile file, string rootN
151151
var bones = file.BoneData?.Bones;
152152

153153
var includeShapeKeys = false;
154+
var extraBones = new List<Node>();
154155
if (bones?.Count > 0 && file.MeshBuffer!.Weights.Length > 0) {
155156
// insert root bones first to ensure all parents exist
156157
Node boneRoot = scene.RootNode;
@@ -189,6 +190,27 @@ private static Assimp.Scene ConvertMeshToAssimpScene(MeshFile file, string rootN
189190
parentBone.Children.Add(boneNode);
190191
}
191192

193+
if (skeleton != null) {
194+
foreach (var refBone in skeleton.Bones) {
195+
var (exportId, exportBone) = boneDict.FirstOrDefault(bb => bb.Value.Name == refBone.name);
196+
if (exportBone == null) {
197+
var parentRef = refBone.parentIndex == -1 ? null : skeleton.Bones[refBone.parentIndex];
198+
Node? parent = null;
199+
if (parentRef != null) {
200+
parent = boneRoot.FindNode(parentRef.name);
201+
if (parent == null) {
202+
throw new NotImplementedException("Unordered ref skel bones currently not supported");
203+
}
204+
}
205+
exportBone = new Node(refBone.name, parent);
206+
parent?.Children.Add(exportBone);
207+
extraBones.Add(exportBone);
208+
}
209+
210+
exportBone.Transform = Matrix4x4.Transpose(Matrix4x4.CreateScale(refBone.scale) * Matrix4x4.CreateFromQuaternion(refBone.rotation) * Matrix4x4.CreateTranslation(refBone.position));
211+
}
212+
}
213+
192214
if (includeShapeKeys) {
193215
if (isGltf) {
194216
Logger.Warn($"GLTF exporter does not support enough bones to include shape keys. Mesh will not behave correctly when re-imported. Consider using a different file format.");
@@ -220,30 +242,30 @@ static void RecursiveDuplicateShapeBones(Node parent, HashSet<string> boneNames,
220242
for (int i = 0; i < file.MeshData.LODs.Count; i++) {
221243
var lod = file.MeshData.LODs[i];
222244
if (i == 0) {
223-
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, includeAllLods ? "lod0_" : "");
245+
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, includeAllLods ? "lod0_" : "", extraBones);
224246
if (!includeAllLods) break;
225247
} else {
226-
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, $"lod{i}_");
248+
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, $"lod{i}_", extraBones);
227249
}
228250
}
229251
}
230252
if (includeShadows && file.ShadowMesh != null) {
231253
for (int i = 0; i < file.ShadowMesh.LODs.Count; i++) {
232254
var lod = file.ShadowMesh.LODs[i];
233-
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, $"shadow_lod{i}_");
255+
ExportLod(file, isGltf, scene, bones, includeShapeKeys, lod, $"shadow_lod{i}_", extraBones);
234256
}
235257
}
236258
if (includeOcclusion && file.OccluderMesh != null) {
237259
if (scene.MaterialCount == 0) {
238260
scene.Materials.Add(new Material() { Name = "default" });
239261
}
240-
ExportLod(file, isGltf, scene, bones, includeShapeKeys, file.OccluderMesh, $"occ_");
262+
ExportLod(file, isGltf, scene, bones, includeShapeKeys, file.OccluderMesh, $"occ_", extraBones);
241263
}
242264

243265
return scene;
244266
}
245267

246-
private static void ExportLod(MeshFile file, bool isGltf, Assimp.Scene scene, List<MeshBone>? bones, bool includeShapeKeys, MeshLOD lod, string namePrefix)
268+
private static void ExportLod(MeshFile file, bool isGltf, Assimp.Scene scene, List<MeshBone>? bones, bool includeShapeKeys, MeshLOD lod, string namePrefix, List<Node> extraBones)
247269
{
248270
var bounds = file.MeshData?.boundingBox ?? new AABB();
249271
foreach (var mesh in lod.MeshGroups) {
@@ -311,6 +333,19 @@ private static void ExportLod(MeshFile file, bool isGltf, Assimp.Scene scene, Li
311333
}
312334
}
313335
}
336+
foreach (var extraBone in extraBones) {
337+
var parentTx = Matrix4x4.Identity;
338+
var parent = extraBone.Parent;
339+
while (parent != null) {
340+
parentTx = parentTx * Matrix4x4.Transpose(parent.Transform);
341+
parent = parent.Parent;
342+
}
343+
344+
Matrix4x4.Invert(parentTx, out parentTx);
345+
var newBone = new Bone() { Name = extraBone.Name };
346+
newBone.OffsetMatrix = Matrix4x4.Transpose(parentTx);
347+
aiMesh.Bones.Add(newBone);
348+
}
314349

315350
if (sub.Buffer.ExtraWeights != null) {
316351
for (int vertId = 0; vertId < sub.ExtraWeights.Length; ++vertId) {

ContentEditor.App/Imgui/App/MeshViewer.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public class MeshViewer : FileEditor, IDisposable, IFocusableFileHandleReference
6767
private bool isSynced;
6868

6969
private bool exportAnimations = true;
70+
private bool exportFbxskel = true;
71+
private bool exportFullCollection = true;
7072
private bool exportCurrentAnimationOnly;
7173
private bool exportInProgress;
7274
private bool exportLods = false;
@@ -569,7 +571,8 @@ private void ShowImportExportMenu()
569571
}
570572
}
571573

572-
assmesh.ExportToFile(exportPath, exportLods, exportOcclusion, mots);
574+
// exportFullCollection ? meshContexts.Skip(1).Select(c => c.MeshFile) :
575+
assmesh.ExportToFile(exportPath, exportLods, exportOcclusion, exportFbxskel ? PrimaryAnimator?.skeleton : null, mots, null);
573576
} catch (Exception e) {
574577
Logger.Error(e, "Mesh export failed");
575578
} finally {
@@ -614,7 +617,9 @@ private void ShowImportExportMenu()
614617
if (PrimaryAnimator?.File != null) {
615618
ImGui.Checkbox("Include animations", ref exportAnimations);
616619
if (exportAnimations) ImGui.Checkbox("Selected animation only", ref exportCurrentAnimationOnly);
620+
if (PrimaryAnimator?.skeleton != null) ImGui.Checkbox("Export with merged skeleton", ref exportFbxskel);
617621
}
622+
// if (meshContexts.Count > 1) ImGui.Checkbox("Export all meshes", ref exportFullCollection);
618623
if (mesh.NativeMesh.MeshData?.LODs.Count > 1 || mesh.NativeMesh.ShadowMesh?.LODs.Count > 0) ImGui.Checkbox("Include LODs and Shadow Mesh", ref exportLods);
619624
if (mesh.NativeMesh.OccluderMesh?.MeshGroups.Count > 0) ImGui.Checkbox("Include Occlusion Mesh", ref exportOcclusion);
620625
if (showImportSettings) {
@@ -662,6 +667,35 @@ private void ShowImportExportMenu()
662667
PlatformUtils.ShowSaveFileDialog((path) => exportMesh.SaveAs(path), defaultFilename);
663668
}
664669
}
670+
if (mesh.NativeMesh.BoneData?.Bones.Count > 0) {
671+
ImGui.SeparatorText("Convert Skeleton");
672+
if (ImGui.Button($"{AppIcons.SI_GenericConvert} Save Skeleton")) {
673+
var hasVersion = Workspace.Env.TryGetFileExtensionVersion("fbxskel", out var version)
674+
|| Workspace.Env.TryGetFileExtensionVersion("skeleton", out version)
675+
|| Workspace.Env.TryGetFileExtensionVersion("refskel", out version);
676+
if (!hasVersion) {
677+
Logger.Warn("Could not determine correct skeleton format version. You might need to manually specify the correct version.");
678+
version = 8;
679+
}
680+
681+
var skeleton = new FbxSkelFile(new FileHandler());
682+
foreach (var bone in mesh.NativeMesh.BoneData.Bones.OrderBy(b => b.index)) {
683+
Matrix4x4.Decompose(bone.localTransform.ToSystem(), out var scale, out var rot, out var pos);
684+
skeleton.Bones.Add(new ReeLib.FbxSkel.RefBone() {
685+
name = bone.name,
686+
position = pos,
687+
rotation = rot,
688+
scale = scale,
689+
symmetryIndex = (short)(bone.Symmetry?.index ?? bone.index),
690+
parentIndex = (short)(bone.Parent?.index ?? -1),
691+
});
692+
}
693+
PlatformUtils.ShowSaveFileDialog((file) => {
694+
skeleton.WriteTo(file);
695+
}, null, new FileFilter("Skeleton File", $"skeleton.{version}", $"fbxskel.{version}", $"refskel.{version}"));
696+
}
697+
ImGui.SameLine();
698+
}
665699
}
666700

667701
private bool UpdateAnimData()

0 commit comments

Comments
 (0)