mirror of
https://github.com/ficed/Braver.git
synced 2024-11-23 05:10:08 +00:00
CrossSlash;
-add battle model export, move some GLTF export code into ancestor class -add option to swap triangle winding
This commit is contained in:
parent
e7724a5a33
commit
7ad0855f8b
140
CrossSlash/BattleExportGuiWindow.cs
Normal file
140
CrossSlash/BattleExportGuiWindow.cs
Normal file
@ -0,0 +1,140 @@
|
||||
// This program and the accompanying materials are made available under the terms of the
|
||||
// Eclipse Public License v2.0 which accompanies this distribution, and is available at
|
||||
// https://www.eclipse.org/legal/epl-v20.html
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace CrossSlash {
|
||||
public class BattleExportGuiWindow : Window {
|
||||
private Label _lblLGP, _lblGLB;
|
||||
private string _lgpFile, _glbFile;
|
||||
private CheckBox _chkSRGB, _chkSwapWinding;
|
||||
private TextField _txtModel, _txtScale;
|
||||
|
||||
public BattleExportGuiWindow() {
|
||||
Title = "CrossSlash Exporter (Ctrl-Q to Quit)";
|
||||
|
||||
var btnLGP = new Button {
|
||||
Text = "Select battle LGP file",
|
||||
Width = Dim.Percent(25),
|
||||
};
|
||||
btnLGP.Clicked += BtnLGP_Clicked;
|
||||
|
||||
_lblLGP = new Label {
|
||||
Text = "(No LGP selected)",
|
||||
X = Pos.Right(btnLGP) + 1,
|
||||
};
|
||||
|
||||
Label lblModel = new Label {
|
||||
Y = Pos.Bottom(btnLGP) + 1,
|
||||
Width = Dim.Percent(25),
|
||||
Text = "Model Code",
|
||||
};
|
||||
_txtModel = new TextField {
|
||||
Y = lblModel.Y,
|
||||
X = Pos.Right(lblModel),
|
||||
Width = Dim.Fill(1),
|
||||
};
|
||||
|
||||
Label lblScale = new Label {
|
||||
Y = Pos.Bottom(lblModel) + 1,
|
||||
Width = Dim.Percent(25),
|
||||
Text = "Scale",
|
||||
};
|
||||
_txtScale = new TextField {
|
||||
Y = lblScale.Y,
|
||||
X = Pos.Right(lblScale),
|
||||
Width = Dim.Fill(1),
|
||||
Text = "1",
|
||||
};
|
||||
|
||||
_chkSRGB = new CheckBox {
|
||||
Checked = true,
|
||||
Text = "Convert colours from SRGB->Linear",
|
||||
Y = Pos.Bottom(_txtScale) + 1,
|
||||
};
|
||||
_chkSwapWinding = new CheckBox {
|
||||
Checked = false,
|
||||
Text = "Swap triangle winding",
|
||||
Y = Pos.Bottom(_chkSRGB) + 1,
|
||||
};
|
||||
|
||||
var btnGLB = new Button {
|
||||
Text = "Save GLB As",
|
||||
Width = Dim.Percent(25),
|
||||
Y = Pos.Bottom(_chkSwapWinding) + 1,
|
||||
};
|
||||
btnGLB.Clicked += BtnGLB_Clicked;
|
||||
|
||||
_lblGLB = new Label {
|
||||
X = Pos.Right(btnGLB) + 1,
|
||||
Y = btnGLB.Y,
|
||||
Text = "(No file selected)",
|
||||
};
|
||||
|
||||
var btnExport = new Button {
|
||||
Y = Pos.Bottom(btnGLB) + 1,
|
||||
Width = Dim.Fill(1),
|
||||
Text = "Export"
|
||||
};
|
||||
btnExport.Clicked += BtnExport_Clicked;
|
||||
|
||||
Add(btnLGP, _lblLGP, lblModel, _txtModel, lblScale, _txtScale, _chkSRGB, _chkSwapWinding, btnGLB, _lblGLB, btnExport);
|
||||
}
|
||||
|
||||
private void BtnGLB_Clicked() {
|
||||
var d = new SaveDialog(
|
||||
"Save As", "Save output model to which file",
|
||||
new List<string> { ".glb" }
|
||||
);
|
||||
Application.Run(d);
|
||||
if (!d.Canceled && d.FileName != null) {
|
||||
_lblGLB.Text = _glbFile = (string)d.FilePath;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnLGP_Clicked() {
|
||||
var d = new OpenDialog(
|
||||
"Open LGP", "Choose the battle.lgp file to read models from",
|
||||
new List<string> { ".lgp" }
|
||||
);
|
||||
Application.Run(d);
|
||||
if (!d.Canceled && d.FilePaths.Any()) {
|
||||
_lblLGP.Text = _lgpFile = d.FilePaths[0];
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnExport_Clicked() {
|
||||
try {
|
||||
if (!File.Exists(_lgpFile))
|
||||
throw new Exception("No LGP file selected");
|
||||
if (string.IsNullOrEmpty(_glbFile))
|
||||
throw new Exception("No GLB save as filename selected");
|
||||
if (string.IsNullOrWhiteSpace(_txtModel.Text.ToString()))
|
||||
throw new Exception("No model file selected");
|
||||
if (!float.TryParse(_txtScale.Text.ToString(), out float scale))
|
||||
throw new Exception("No scale specified");
|
||||
|
||||
using (var lgp = new Ficedula.FF7.LGPFile(_lgpFile)) {
|
||||
var exporter = new Ficedula.FF7.Exporters.BattleModel(lgp) {
|
||||
ConvertSRGBToLinear = _chkSRGB.Checked,
|
||||
SwapWinding = _chkSwapWinding.Checked,
|
||||
Scale = scale,
|
||||
};
|
||||
var model = exporter.BuildSceneFromModel(_txtModel.Text.ToString());
|
||||
model.SaveGLB(_glbFile);
|
||||
}
|
||||
MessageBox.Query("Success", "Export Succeeded", "OK");
|
||||
} catch (Exception ex) {
|
||||
MessageBox.ErrorQuery("Error", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.1.2</Version>
|
||||
<Version>0.1.3</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -12,17 +12,17 @@ using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace CrossSlash {
|
||||
public class ExportGuiWindow : Window {
|
||||
public class FieldExportGuiWindow : Window {
|
||||
|
||||
private Label _lblLGP, _lblGLB;
|
||||
private ListView _lvHRCs;
|
||||
private TextView _txtAnims;
|
||||
private List<string> _hrcFiles;
|
||||
private CheckBox _chkSRGB;
|
||||
private CheckBox _chkSRGB, _chkSwapWinding;
|
||||
|
||||
private string _lgpFile, _glbFile;
|
||||
|
||||
public ExportGuiWindow() {
|
||||
public FieldExportGuiWindow() {
|
||||
Title = "CrossSlash Exporter (Ctrl-Q to Quit)";
|
||||
|
||||
var btnLGP = new Button {
|
||||
@ -67,11 +67,16 @@ namespace CrossSlash {
|
||||
Text = "Convert colours from SRGB->Linear",
|
||||
Y = Pos.Bottom(_txtAnims) + 1,
|
||||
};
|
||||
_chkSwapWinding = new CheckBox {
|
||||
Checked = false,
|
||||
Text = "Swap triangle winding",
|
||||
Y = Pos.Bottom(_chkSRGB) + 1,
|
||||
};
|
||||
|
||||
var btnGLB = new Button {
|
||||
Text = "Save GLB As",
|
||||
Width = Dim.Percent(25),
|
||||
Y = Pos.Bottom(_chkSRGB) + 1,
|
||||
Y = Pos.Bottom(_chkSwapWinding) + 1,
|
||||
};
|
||||
btnGLB.Clicked += BtnGLB_Clicked;
|
||||
|
||||
@ -88,7 +93,7 @@ namespace CrossSlash {
|
||||
};
|
||||
btnExport.Clicked += BtnExport_Clicked;
|
||||
|
||||
Add(btnLGP, _lblLGP, lblHRC, _lvHRCs, lblAnims, _txtAnims, _chkSRGB, btnGLB, _lblGLB, btnExport);
|
||||
Add(btnLGP, _lblLGP, lblHRC, _lvHRCs, lblAnims, _txtAnims, _chkSRGB, _chkSwapWinding, btnGLB, _lblGLB, btnExport);
|
||||
}
|
||||
|
||||
private void BtnExport_Clicked() {
|
||||
@ -110,6 +115,7 @@ namespace CrossSlash {
|
||||
using (var lgp = new Ficedula.FF7.LGPFile(_lgpFile)) {
|
||||
var exporter = new Ficedula.FF7.Exporters.FieldModel(lgp) {
|
||||
ConvertSRGBToLinear = _chkSRGB.Checked,
|
||||
SwapWinding = _chkSwapWinding.Checked,
|
||||
};
|
||||
var model = exporter.BuildScene(_hrcFiles[_lvHRCs.SelectedItem], anims);
|
||||
model.SaveGLB(_glbFile);
|
@ -12,29 +12,58 @@ System.Globalization.CultureInfo.CurrentCulture =
|
||||
System.Globalization.CultureInfo.DefaultThreadCurrentCulture =
|
||||
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
Console.WriteLine("CrossSlash");
|
||||
Console.WriteLine("CrossSlash");
|
||||
|
||||
switch (args.Length) {
|
||||
case 0:
|
||||
Application.Run<ExportGuiWindow>();
|
||||
Application.Run<SplashWindow>();
|
||||
Application.Shutdown();
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
Console.WriteLine("USAGE: CrossSlash [OutputGLBFile] [LGPFile] [HRCFile] {/ConvertSRGB} [Anim] [Anim] [Anim]...");
|
||||
Console.WriteLine("USAGE: CrossSlash [OutputGLBFile] [LGPFile] [HRCFile] {/ConvertSRGB} {/SwapWinding} [Anim] [Anim] [Anim]...");
|
||||
Console.WriteLine(@"e.g. CrossSlash C:\temp\tifa.glb C:\games\FF7\data\field\char.lgp AAGB.HRC ABCD.a ABCE.a");
|
||||
Console.WriteLine(@"or:");
|
||||
Console.WriteLine(@"CrossSlash [OutputGLBFile] [LGPFile] [BattleModelCode] {/ConvertSRGB} {/SwapWinding} {/Scale:1}");
|
||||
Console.WriteLine("");
|
||||
Console.WriteLine("Specify /ConvertSRGB to convert vertex colours from SRGB to linear when exporting");
|
||||
Console.WriteLine("Specify /SwapWinding to swap triangle winding order");
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"Opening LGP {args[1]}...");
|
||||
using (var lgp = new Ficedula.FF7.LGPFile(args[1])) {
|
||||
var exporter = new Ficedula.FF7.Exporters.FieldModel(lgp);
|
||||
Console.WriteLine($"Exporting model {args[2]}...");
|
||||
exporter.ConvertSRGBToLinear = args.Any(s => s.Equals("/ConvertSRGB", StringComparison.InvariantCultureIgnoreCase));
|
||||
var model = exporter.BuildScene(args[2], args.Skip(3).Where(s => !s.StartsWith("/")));
|
||||
Console.WriteLine($"Saving output to {args[0]}...");
|
||||
model.SaveGLB(args[0]);
|
||||
|
||||
void Configure(Ficedula.FF7.Exporters.ModelBase exporter) {
|
||||
exporter.ConvertSRGBToLinear = args.Any(s => s.Equals("/ConvertSRGB", StringComparison.InvariantCultureIgnoreCase));
|
||||
exporter.SwapWinding = args.Any(s => s.Equals("/SwapWinding", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
if (args[2].EndsWith(".HRC", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
var exporter = new Ficedula.FF7.Exporters.FieldModel(lgp);
|
||||
Configure(exporter);
|
||||
|
||||
Console.WriteLine($"Exporting model {args[2]}...");
|
||||
var model = exporter.BuildScene(args[2], args.Skip(3).Where(s => !s.StartsWith("/")));
|
||||
Console.WriteLine($"Saving output to {args[0]}...");
|
||||
model.SaveGLB(args[0]);
|
||||
} else if (args[2].Length == 2) {
|
||||
var exporter = new Ficedula.FF7.Exporters.BattleModel(lgp);
|
||||
Configure(exporter);
|
||||
exporter.Scale = float.Parse(
|
||||
args
|
||||
.Where(s => s.StartsWith("/Scale:", StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(s => s.Substring("/Scale:".Length))
|
||||
.FirstOrDefault()
|
||||
?? exporter.Scale.ToString()
|
||||
);
|
||||
Console.WriteLine($"Exporting model {args[2]}...");
|
||||
var bmodel = exporter.BuildSceneFromModel(args[2]);
|
||||
Console.WriteLine($"Saving output to {args[0]}...");
|
||||
bmodel.SaveGLB(args[0]);
|
||||
} else
|
||||
throw new Exception("Unrecognised export type");
|
||||
|
||||
Console.WriteLine("Done");
|
||||
}
|
||||
break;
|
||||
|
@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"CrossSlash": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "C:\\temp\\test.glb C:\\games\\FF7\\data\\field\\char.lgp AAGB.HRC ABCE.a ABCF.a DIDF.a APGB.a BZAC.a ACGA.a DIEA.a CDGF.a ACGB.a"
|
||||
"commandLineArgs": "C:\\temp\\b_aeris.glb C:\\games\\FF7\\data\\battle\\battle.lgp RV /Scale:0.1 /ConvertSRGB"
|
||||
}
|
||||
}
|
||||
}
|
44
CrossSlash/SplashWindow.cs
Normal file
44
CrossSlash/SplashWindow.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// This program and the accompanying materials are made available under the terms of the
|
||||
// Eclipse Public License v2.0 which accompanies this distribution, and is available at
|
||||
// https://www.eclipse.org/legal/epl-v20.html
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace CrossSlash {
|
||||
public class SplashWindow : Window {
|
||||
public SplashWindow() {
|
||||
|
||||
Title = "CrossSlash FF7 exporter";
|
||||
|
||||
Button btnField = new Button {
|
||||
Width = Dim.Fill(2),
|
||||
Text = "Field Model Export",
|
||||
Y = Pos.At(4),
|
||||
};
|
||||
btnField.Clicked += () => {
|
||||
Application.RequestStop();
|
||||
Application.Run<FieldExportGuiWindow>();
|
||||
};
|
||||
|
||||
Button btnBattle = new Button {
|
||||
Width = Dim.Fill(2),
|
||||
Text = "Battle Model Export",
|
||||
Y = Pos.Bottom(btnField) + 2,
|
||||
};
|
||||
btnBattle.Clicked += () => {
|
||||
Application.RequestStop();
|
||||
Application.Run<BattleExportGuiWindow>();
|
||||
};
|
||||
|
||||
Add(btnField, btnBattle);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
168
Ficedula.FF7.Exporters/BattleModel.cs
Normal file
168
Ficedula.FF7.Exporters/BattleModel.cs
Normal file
@ -0,0 +1,168 @@
|
||||
// This program and the accompanying materials are made available under the terms of the
|
||||
// Eclipse Public License v2.0 which accompanies this distribution, and is available at
|
||||
// https://www.eclipse.org/legal/epl-v20.html
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
using Ficedula.FF7.Battle;
|
||||
using SharpGLTF.Materials;
|
||||
using SharpGLTF.Scenes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Exporters {
|
||||
public class BattleModel : ModelBase {
|
||||
private LGPFile _lgp;
|
||||
|
||||
public float Scale { get; set; } = 1f;
|
||||
|
||||
public BattleModel(LGPFile lgp) {
|
||||
_lgp = lgp;
|
||||
}
|
||||
|
||||
private SharpGLTF.Schema2.ModelRoot BuildScene(string skeleton, string anims, IEnumerable<string> texs, Func<string?> nextFile) {
|
||||
var scene = new SceneBuilder();
|
||||
|
||||
Animations animations;
|
||||
using (var s = _lgp.Open(anims))
|
||||
animations = new Animations(s);
|
||||
BBone rootBone;
|
||||
using(var s = _lgp.Open(skeleton))
|
||||
rootBone = BBone.Decode(s);
|
||||
|
||||
var textures = texs
|
||||
.Select(t => {
|
||||
using (var s = _lgp.TryOpen(t))
|
||||
return s == null ? null : new Ficedula.FF7.TexFile(s);
|
||||
})
|
||||
.Where(tex => tex != null)
|
||||
.ToArray();
|
||||
|
||||
var materials = ConvertTextures(textures);
|
||||
var orderedMaterials = textures.Select(t => materials[t]).ToList();
|
||||
MaterialBuilder defMaterial = GetUntexturedMaterial();
|
||||
|
||||
List<PFile> pFiles = new();
|
||||
foreach (var bone in rootBone.ThisAndDescendants().Where(b => b.PFileIndex != null).OrderBy(b => b.PFileIndex.Value)) {
|
||||
while (pFiles.Count <= bone.PFileIndex.Value)
|
||||
pFiles.Add(null);
|
||||
using (var s = _lgp.Open(nextFile()))
|
||||
pFiles[bone.PFileIndex.Value] = new PFile(s);
|
||||
}
|
||||
|
||||
var firstFrame = animations.Anims[0].Frames[0];
|
||||
|
||||
int maxBone = 0;
|
||||
var allNodes = new Dictionary<int, NodeBuilder>();
|
||||
|
||||
void Descend(SceneBuilder scene, BBone bone, NodeBuilder node, System.Numerics.Vector3 translation, System.Numerics.Vector3? scale) {
|
||||
maxBone = Math.Max(maxBone, bone.Index);
|
||||
|
||||
var rotation = System.Numerics.Quaternion.CreateFromYawPitchRoll(
|
||||
(360 * firstFrame.Rotations[bone.Index + 1].rY / 4096f) * (float)Math.PI / 180,
|
||||
(360 * firstFrame.Rotations[bone.Index + 1].rX / 4096f) * (float)Math.PI / 180,
|
||||
(360 * firstFrame.Rotations[bone.Index + 1].rZ / 4096f) * (float)Math.PI / 180
|
||||
);
|
||||
|
||||
node.LocalTransform = SharpGLTF.Transforms.AffineTransform.CreateFromAny(
|
||||
null, scale ?? System.Numerics.Vector3.One, rotation, translation
|
||||
);
|
||||
|
||||
foreach (var child in bone.Children) {
|
||||
var c = node.CreateNode(child.Index.ToString());
|
||||
allNodes[child.Index] = c;
|
||||
Descend(scene, child, c, new System.Numerics.Vector3(0, 0, bone.Length), null);
|
||||
}
|
||||
}
|
||||
|
||||
void DescendMesh(SceneBuilder scene, BBone bone, NodeBuilder node, Matrix4x4 transform, NodeBuilder[] joints) {
|
||||
if (bone.PFileIndex != null) {
|
||||
foreach (var mesh in BuildMeshes(pFiles[bone.PFileIndex.Value], orderedMaterials, defMaterial, bone.Index)) {
|
||||
scene.AddSkinnedMesh(mesh, transform, joints);
|
||||
}
|
||||
}
|
||||
foreach (var child in bone.Children) {
|
||||
var childNode = allNodes[child.Index];
|
||||
var childTransform = childNode.LocalMatrix * transform;
|
||||
DescendMesh(scene, child, childNode, childTransform, joints);
|
||||
}
|
||||
}
|
||||
|
||||
var root = new NodeBuilder("-1");
|
||||
Descend(scene, rootBone, root, System.Numerics.Vector3.Zero, new System.Numerics.Vector3(Scale, -Scale, Scale));
|
||||
DescendMesh(
|
||||
scene, rootBone, root, root.LocalMatrix,
|
||||
allNodes.Where(kv => kv.Key >= 0).OrderBy(kv => kv.Key).Select(kv => kv.Value).ToArray()
|
||||
);
|
||||
scene.AddNode(root);
|
||||
|
||||
var settings = SceneBuilderSchema2Settings.Default;
|
||||
settings.UseStridedBuffers = false;
|
||||
var model = scene.ToGltf2(settings);
|
||||
|
||||
foreach (var anim in animations.Anims) {
|
||||
if (anim == null)
|
||||
continue;
|
||||
if (anim.Bones <= (maxBone + 1))
|
||||
continue;
|
||||
|
||||
var mAnim = model.CreateAnimation();
|
||||
mAnim.Name = "Anim" + (model.LogicalAnimations.Count - 1);
|
||||
|
||||
foreach (var node in model.LogicalNodes) {
|
||||
int c = 0;
|
||||
if (!int.TryParse(node.Name, out int boneIndex))
|
||||
continue;
|
||||
|
||||
Dictionary<float, System.Numerics.Quaternion> rots = new Dictionary<float, System.Numerics.Quaternion>();
|
||||
Dictionary<float, System.Numerics.Vector3> trans = new Dictionary<float, System.Numerics.Vector3>();
|
||||
|
||||
foreach (var frame in anim.Frames) {
|
||||
if (node.VisualRoot == node)
|
||||
trans[c / 15f] = new System.Numerics.Vector3(frame.X, -frame.Y, frame.Z) * Scale;
|
||||
|
||||
var rotation = System.Numerics.Quaternion.CreateFromYawPitchRoll(
|
||||
(360 * frame.Rotations[boneIndex + 1].rY / 4096f) * (float)Math.PI / 180,
|
||||
(360 * frame.Rotations[boneIndex + 1].rX / 4096f) * (float)Math.PI / 180,
|
||||
(360 * frame.Rotations[boneIndex + 1].rZ / 4096f) * (float)Math.PI / 180
|
||||
);
|
||||
|
||||
rots[c / 15f] = rotation;
|
||||
|
||||
c++;
|
||||
}
|
||||
|
||||
if (node.VisualRoot == node)
|
||||
mAnim.CreateTranslationChannel(node, trans);
|
||||
mAnim.CreateRotationChannel(node, rots);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public SharpGLTF.Schema2.ModelRoot BuildSceneFromModel(string modelCode) {
|
||||
//TODO - very similar to code in main Braver project - move into Ficedula.FF7
|
||||
var texs = Enumerable.Range((int)'c', 10).Select(i => modelCode + "a" + ((char)i).ToString());
|
||||
int codecounter = 12;
|
||||
Func<string?> NextData = () => {
|
||||
char c1, c2;
|
||||
string data;
|
||||
do {
|
||||
if (codecounter >= 260)
|
||||
return null;
|
||||
c1 = (char)('a' + (codecounter / 26));
|
||||
c2 = (char)('a' + (codecounter % 26));
|
||||
codecounter++;
|
||||
data = modelCode + c1.ToString() + c2.ToString();
|
||||
} while (!_lgp.Exists(data));
|
||||
return data;
|
||||
};
|
||||
return BuildScene(modelCode + "aa", modelCode + "da", texs, NextData);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,101 +16,13 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ficedula.FF7.Exporters {
|
||||
public class FieldModel {
|
||||
public class FieldModel : ModelBase {
|
||||
private LGPFile _lgp;
|
||||
|
||||
public bool ConvertSRGBToLinear { get; set; }
|
||||
|
||||
public FieldModel(LGPFile lgp) {
|
||||
_lgp = lgp;
|
||||
}
|
||||
|
||||
private static double SRGBToLinear(double value) {
|
||||
const double a = 0.055;
|
||||
if (value <= 0.04045)
|
||||
return value / 12.92;
|
||||
else
|
||||
return Math.Pow((value + a) / (1 + a), 2.4);
|
||||
}
|
||||
|
||||
private Vector4 UnpackColour(uint colour) {
|
||||
var c = new Vector4(
|
||||
(colour & 0xff) / 255f,
|
||||
((colour >> 8) & 0xff) / 255f,
|
||||
((colour >> 16) & 0xff) / 255f,
|
||||
((colour >> 24) & 0xff) / 255f
|
||||
);
|
||||
if (ConvertSRGBToLinear) {
|
||||
c = new Vector4(
|
||||
(float)SRGBToLinear(c.X),
|
||||
(float)SRGBToLinear(c.Y),
|
||||
(float)SRGBToLinear(c.Z),
|
||||
(float)SRGBToLinear(c.W)
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private IEnumerable<IMeshBuilder<MaterialBuilder>> BuildMeshes(PFile poly, IEnumerable<MaterialBuilder> materials, MaterialBuilder defMaterial, int boneIndex) {
|
||||
foreach (var group in poly.Chunks) {
|
||||
if (group.Texture != null) {
|
||||
var mesh = new MeshBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>();
|
||||
for (int i = 0; i < group.Indices.Count; i += 3) {
|
||||
var v0 = group.Verts[group.Indices[i]];
|
||||
var v1 = group.Verts[group.Indices[i + 1]];
|
||||
var v2 = group.Verts[group.Indices[i + 2]];
|
||||
|
||||
var vb0 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v0.Position, v0.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v0.Colour), v0.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb1 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v1.Position, v1.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v1.Colour), v1.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb2 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v2.Position, v2.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v2.Colour), v2.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
|
||||
mesh.UsePrimitive(materials.ElementAt(group.Texture.Value))
|
||||
.AddTriangle(vb0, vb1, vb2);
|
||||
}
|
||||
yield return mesh;
|
||||
} else {
|
||||
var mesh = new MeshBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>();
|
||||
for (int i = 0; i < group.Indices.Count; i += 3) {
|
||||
var v0 = group.Verts[group.Indices[i]];
|
||||
var v1 = group.Verts[group.Indices[i + 1]];
|
||||
var v2 = group.Verts[group.Indices[i + 2]];
|
||||
|
||||
var vb0 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v0.Position, v0.Normal),
|
||||
new VertexColor1(UnpackColour(v0.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb1 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v1.Position, v1.Normal),
|
||||
new VertexColor1(UnpackColour(v1.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb2 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v2.Position, v2.Normal),
|
||||
new VertexColor1(UnpackColour(v2.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
|
||||
mesh.UsePrimitive(defMaterial)
|
||||
.AddTriangle(vb0, vb1, vb2);
|
||||
}
|
||||
yield return mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SharpGLTF.Schema2.ModelRoot BuildScene(string modelHRC, IEnumerable<string> animFiles) {
|
||||
var scene = new SceneBuilder();
|
||||
|
||||
@ -119,41 +31,17 @@ namespace Ficedula.FF7.Exporters {
|
||||
.Select(file => new { Anim = new Field.FieldAnim(_lgp.Open(file)), Name = Path.GetFileNameWithoutExtension(file) })
|
||||
.ToList();
|
||||
|
||||
Dictionary<TexFile, MaterialBuilder> materials = new();
|
||||
|
||||
var allTextures = model.Bones
|
||||
var materials = ConvertTextures(
|
||||
model.Bones
|
||||
.SelectMany(bone => bone.Polygons)
|
||||
.SelectMany(poly => poly.Textures);
|
||||
foreach (var texture in allTextures) {
|
||||
byte[] data = texture
|
||||
.ToBitmap(0)
|
||||
.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100)
|
||||
.ToArray();
|
||||
var mat = new MaterialBuilder("Mat" + materials.Count)
|
||||
.WithDoubleSide(false)
|
||||
.WithUnlitShader()
|
||||
.WithAlpha(SharpGLTF.Materials.AlphaMode.BLEND)
|
||||
.WithChannelImage(KnownChannel.BaseColor, new SharpGLTF.Memory.MemoryImage(data));
|
||||
//.WithChannelImage(KnownChannel.Diffuse, new SharpGLTF.Memory.MemoryImage(tex));
|
||||
materials[texture] = mat;
|
||||
}
|
||||
|
||||
MaterialBuilder defMaterial = new MaterialBuilder("Def")
|
||||
.WithDoubleSide(false)
|
||||
.WithAlpha(SharpGLTF.Materials.AlphaMode.OPAQUE)
|
||||
.WithBaseColor(Vector4.One)
|
||||
//.WithBaseColor(new System.Numerics.Vector4(1, 1, 1, 0.5f))
|
||||
.WithUnlitShader();
|
||||
|
||||
int maxBone = 0;
|
||||
.SelectMany(poly => poly.Textures)
|
||||
);
|
||||
MaterialBuilder defMaterial = GetUntexturedMaterial();
|
||||
|
||||
var firstFrame = animations[0].Anim.Frames[0];
|
||||
|
||||
var allNodes = new Dictionary<int, NodeBuilder>();
|
||||
|
||||
void Descend(SceneBuilder scene, HRCModel.Bone bone, NodeBuilder node, Vector3 translation, Vector3? scale) {
|
||||
maxBone = Math.Max(maxBone, bone.Index);
|
||||
|
||||
var rotation = Quaternion.CreateFromYawPitchRoll(
|
||||
firstFrame.Rotation.Y * (float)Math.PI / 180,
|
||||
firstFrame.Rotation.X * (float)Math.PI / 180,
|
||||
@ -187,7 +75,7 @@ namespace Ficedula.FF7.Exporters {
|
||||
Descend(scene, model.Root, root, Vector3.Zero, null);
|
||||
DescendMesh(
|
||||
scene, model.Root, root, root.LocalMatrix,
|
||||
allNodes.Where(kv => kv.Key >= 0).Select(kv => kv.Value).ToArray()
|
||||
allNodes.Where(kv => kv.Key >= 0).OrderBy(kv => kv.Key).Select(kv => kv.Value).ToArray()
|
||||
);
|
||||
scene.AddNode(root);
|
||||
|
||||
|
137
Ficedula.FF7.Exporters/ModelBase.cs
Normal file
137
Ficedula.FF7.Exporters/ModelBase.cs
Normal file
@ -0,0 +1,137 @@
|
||||
// This program and the accompanying materials are made available under the terms of the
|
||||
// Eclipse Public License v2.0 which accompanies this distribution, and is available at
|
||||
// https://www.eclipse.org/legal/epl-v20.html
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.Geometry;
|
||||
using SharpGLTF.Materials;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Exporters {
|
||||
public class ModelBase {
|
||||
public bool ConvertSRGBToLinear { get; set; }
|
||||
public bool SwapWinding { get; set; }
|
||||
|
||||
|
||||
private static double SRGBToLinear(double value) {
|
||||
const double a = 0.055;
|
||||
if (value <= 0.04045)
|
||||
return value / 12.92;
|
||||
else
|
||||
return Math.Pow((value + a) / (1 + a), 2.4);
|
||||
}
|
||||
|
||||
protected Vector4 UnpackColour(uint colour) {
|
||||
var c = new Vector4(
|
||||
(colour & 0xff) / 255f,
|
||||
((colour >> 8) & 0xff) / 255f,
|
||||
((colour >> 16) & 0xff) / 255f,
|
||||
((colour >> 24) & 0xff) / 255f
|
||||
);
|
||||
if (ConvertSRGBToLinear) {
|
||||
c = new Vector4(
|
||||
(float)SRGBToLinear(c.X),
|
||||
(float)SRGBToLinear(c.Y),
|
||||
(float)SRGBToLinear(c.Z),
|
||||
(float)SRGBToLinear(c.W)
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
protected IEnumerable<IMeshBuilder<MaterialBuilder>> BuildMeshes(PFile poly,
|
||||
IEnumerable<MaterialBuilder> materials, MaterialBuilder defMaterial, int boneIndex) {
|
||||
foreach (var group in poly.Chunks) {
|
||||
if (group.Texture != null) {
|
||||
var mesh = new MeshBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>();
|
||||
for (int i = 0; i < group.Indices.Count; i += 3) {
|
||||
var v0 = group.Verts[group.Indices[i]];
|
||||
var v1 = group.Verts[group.Indices[i + (SwapWinding ? 2 : 1)]];
|
||||
var v2 = group.Verts[group.Indices[i + (SwapWinding ? 1 : 2)]];
|
||||
|
||||
var vb0 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v0.Position, v0.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v0.Colour), v0.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb1 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v1.Position, v1.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v1.Colour), v1.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb2 = new VertexBuilder<VertexPositionNormal, VertexColor1Texture1, VertexJoints4>(
|
||||
new VertexPositionNormal(v2.Position, v2.Normal),
|
||||
new VertexColor1Texture1(UnpackColour(v2.Colour), v2.TexCoord),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
|
||||
mesh.UsePrimitive(materials.ElementAt(group.Texture.Value))
|
||||
.AddTriangle(vb0, vb1, vb2);
|
||||
}
|
||||
yield return mesh;
|
||||
} else {
|
||||
var mesh = new MeshBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>();
|
||||
for (int i = 0; i < group.Indices.Count; i += 3) {
|
||||
var v0 = group.Verts[group.Indices[i]];
|
||||
var v1 = group.Verts[group.Indices[i + (SwapWinding ? 2 : 1)]];
|
||||
var v2 = group.Verts[group.Indices[i + (SwapWinding ? 1 : 2)]];
|
||||
|
||||
var vb0 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v0.Position, v0.Normal),
|
||||
new VertexColor1(UnpackColour(v0.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb1 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v1.Position, v1.Normal),
|
||||
new VertexColor1(UnpackColour(v1.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
var vb2 = new VertexBuilder<VertexPositionNormal, VertexColor1, VertexJoints4>(
|
||||
new VertexPositionNormal(v2.Position, v2.Normal),
|
||||
new VertexColor1(UnpackColour(v2.Colour)),
|
||||
new VertexJoints4(boneIndex)
|
||||
);
|
||||
|
||||
mesh.UsePrimitive(defMaterial)
|
||||
.AddTriangle(vb0, vb1, vb2);
|
||||
}
|
||||
yield return mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Dictionary<TexFile, MaterialBuilder> ConvertTextures(IEnumerable<TexFile> texs) {
|
||||
Dictionary<TexFile, MaterialBuilder> materials = new();
|
||||
foreach (var texture in texs) {
|
||||
byte[] data = texture
|
||||
.ToBitmap(0)
|
||||
.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100)
|
||||
.ToArray();
|
||||
var mat = new MaterialBuilder("Mat" + materials.Count)
|
||||
.WithDoubleSide(false)
|
||||
.WithUnlitShader()
|
||||
.WithAlpha(SharpGLTF.Materials.AlphaMode.BLEND)
|
||||
.WithChannelImage(KnownChannel.BaseColor, new SharpGLTF.Memory.MemoryImage(data));
|
||||
//.WithChannelImage(KnownChannel.Diffuse, new SharpGLTF.Memory.MemoryImage(tex));
|
||||
materials[texture] = mat;
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
protected MaterialBuilder GetUntexturedMaterial() {
|
||||
return new MaterialBuilder("Def")
|
||||
.WithDoubleSide(false)
|
||||
.WithAlpha(SharpGLTF.Materials.AlphaMode.OPAQUE)
|
||||
.WithBaseColor(Vector4.One)
|
||||
//.WithBaseColor(new System.Numerics.Vector4(1, 1, 1, 0.5f))
|
||||
.WithUnlitShader();
|
||||
}
|
||||
}
|
||||
}
|
@ -75,6 +75,8 @@ namespace Ficedula.FF7 {
|
||||
.ToDictionary(e => e.FullPath, e => e, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public bool Exists(string name) => _entries.ContainsKey(name);
|
||||
|
||||
public Stream? TryOpen(string name) {
|
||||
if (_entries.TryGetValue(name, out Entry? e)) {
|
||||
_source.Position = e.Offset + 20;
|
||||
|
Loading…
Reference in New Issue
Block a user