CrossSlash;

-add battle model export, move some GLTF export code into ancestor class
-add option to swap triangle winding
This commit is contained in:
ficedula 2023-09-18 23:15:35 +01:00
parent e7724a5a33
commit 7ad0855f8b
10 changed files with 549 additions and 135 deletions

View 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");
}
}
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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"
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@ -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);

View 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();
}
}
}

View File

@ -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;