using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Windows.Forms; using FlatSharp.Attributes; using pkNX.Containers; using pkNX.Game; using pkNX.Structures; using pkNX.Structures.FlatBuffers; using pkNX.Structures.FlatBuffers.Arceus; using pkNX.Structures.FlatBuffers.SWSH; using static System.Buffers.Binary.BinaryPrimitives; using Material = pkNX.Structures.FlatBuffers.Arceus.Material; using Mesh = pkNX.Structures.FlatBuffers.Arceus.Mesh; using Bone = pkNX.Structures.FlatBuffers.Arceus.Bone; using SamplerState = pkNX.Structures.FlatBuffers.Arceus.SamplerState; using UVWrapMode = pkNX.Structures.FlatBuffers.Arceus.UVWrapMode; using Material8 = pkNX.Structures.FlatBuffers.SWSH.Material; using Mesh8 = pkNX.Structures.FlatBuffers.SWSH.Mesh; using Bone8 = pkNX.Structures.FlatBuffers.SWSH.Bone; using SamplerState8 = pkNX.Structures.FlatBuffers.SWSH.SamplerState; using UVWrapMode8 = pkNX.Structures.FlatBuffers.SWSH.UVWrapMode; namespace pkNX.WinForms; // Model loading Tasks // y | 2 | Layout PLA model structure classes // y | 3 | Load PLA models // y | 2 | Layout SWSH model structure classes // y | 2 | Load SWSH models // p | 0 | Convert SWSH models to PLA // n | 1 | - Textures // p | 3 | - Materials // n | 3 | - Constant buffers (material params) // n | 4 | - Shaders // p | 2 | - Mesh information // p | 2 | - Mesh buffers / vertex layout // p | 3 | - Skeleton // p | 3 | - LOD structure // p | 3 | - Other properties // n | 2 | Save PLA models // x | 35 | // TODO: Material conversion // What do do with vertex color. Why are there two entries? // TODO's per file type // Config -> fill in missing fields // Model -> Auto generate LODs, Field_06 // MMT -> MaterialSwitches, MaterialProperties // Mesh -> Split eyes into submesh and assign eye shader, maybe sort entries? // SubMesh -> Material name might need to be converted to snake_case // MeshShape -> BoneWeight[] Possibly this is just the sum of all blend indices + weights on the shape // MeshBuffer -> Update BLEND_INDICES, Possibly need to remove vertex color // Material -> Properly tackle this, Material name might need to be converted to snake_case // Skeleton -> Name of first bone should be updated, might need to snake_case all names // TODO SWSH unused properties: // GFBPokeConfig -> Version, SpeciesId, FormId, Origin, Height, HeightAdjust, FieldAdjust, AABB, // InframeHeight, RegionId, Motion // MaterialEntries // GFBModel -> Version?, TextureFiles, All shaders // Mesh8 -> SortPriority (only used rarely) // Skeleton8 -> Effect and IsVisible // Probably need some sort of intermediate class structure // PLA animation structure classes // Load PLA animations // SWSH animation structure classes // Load SWSH animations // Convert SWSH animations to PLA // Save PLA animations // Remaining Tasks // Particle Effects // Other Effects // Shader converter public partial class ModelConverter : Form { private GameManager ROM; private int SpeciesId; private string FileName { get; set; } = ""; private string BasePath { get; set; } = ""; private string ModelPath => BasePath + "mdl/"; private string AnimationsPath => BasePath + "anm/"; private readonly FolderContainer PokemonModelDir; private readonly FolderContainer SWSHPokemonModelDir; public ModelConverter(GameManager rom) { ROM = rom; InitializeComponent(); PokemonModelDir = (FolderContainer)ROM[GameFile.PokemonArchiveFolder]; PokemonModelDir.Initialize(); CB_Species.Items.AddRange(PokemonModelDir.GetFileNames().Where(x => x != "pokeconfig.gfpak").ToArray()); CB_Species.SelectedIndex = 165; SWSHPokemonModelDir = (FolderContainer)ROM[GameFile.Debug_SWSHPokemonArchiveFolder]; SWSHPokemonModelDir.Initialize(); CB_SWSHSpecies.Items.AddRange(SWSHPokemonModelDir.GetFileNames().ToArray()); CB_SWSHSpecies.SelectedIndex = 1; } [FlatBufferTable, TypeConverter(typeof(ExpandableObjectConverter))] public class MeshMaterialWrapper { [FlatBufferItem(0)] public string Name { get; set; } = string.Empty; [FlatBufferItem(1)] public Material[] Materials { get; set; } = []; } [FlatBufferTable, TypeConverter(typeof(ExpandableObjectConverter))] public class PokemonModelGfpak { [FlatBufferItem(0)] public PokeConfig Config { get; set; } = new(); [FlatBufferItem(1)] public Model Model { get; set; } = Model.Empty; [FlatBufferItem(2)] public MultiMaterialTable MMT { get; set; } = MultiMaterialTable.Empty; [FlatBufferItem(3)] public Mesh[] Meshes { get; set; } = []; [FlatBufferItem(4)] public MeshBufferTable[] MeshDataBuffers { get; set; } = []; [FlatBufferItem(5)] public Material[] DefaultMaterials { get; set; } = []; [FlatBufferItem(6)] public MeshMaterialWrapper[] MeshMaterials { get; set; } = []; [FlatBufferItem(7)] public Skeleton Skeleton { get; set; } = new(); [FlatBufferItem(8)] public string[] UsedTextures { get; set; } = []; } [TypeConverter(typeof(ExpandableObjectConverter))] private class SWSHModelWrapper { public GFBPokeConfig Config { get; set; } = new(); public GFBModel GFBModel { get; set; } = new(); public GFBModel GFBModelRare { get; set; } = new(); public string[] UsedTextures { get; set; } = []; } private readonly PokemonModelGfpak PLAModel = new(); private readonly SWSHModelWrapper SWSHModel = new(); private readonly PokemonModelGfpak Result = new(); public ModelConverter(FolderContainer swshPokemonModelDir) { ROM = null!; PokemonModelDir = null!; SWSHPokemonModelDir = swshPokemonModelDir; } private void UpdatePLAModel() { // TODO: Paths are located in 'Pokémon Resource Table', should load through there if (CB_Species.SelectedItem is not string selectedFile) return; FileName = Path.GetFileNameWithoutExtension(selectedFile); SpeciesId = int.Parse(FileName.Substring(2, 4)); // TODO: Skip these for now if (int.Parse(FileName.Substring(7, 2)) != 0) return; BasePath = $"bin/pokemon/pm{SpeciesId:0000}/{FileName}/"; var pack = new GFPack(PokemonModelDir.GetFileData(selectedFile) ?? []); PLAModel.Config = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(BasePath + $"{FileName}.trpokecfg")); Debug.Assert((int)PLAModel.Config.SizeIndex <= 3, "Here's one!"); Debug.Assert((int)PLAModel.Config.Reserved09 == 0.0f, "Here's one!"); LoadModel(pack); } private void UpdateSWSHModel() { if (CB_SWSHSpecies.SelectedItem is not string selectedFile) return; FileName = Path.GetFileNameWithoutExtension(selectedFile); SpeciesId = int.Parse(FileName.Substring(2, 4)); BasePath = $"bin/pokemon/{FileName}/"; var pack = new GFPack(SWSHPokemonModelDir.GetFileData($"{FileName}.gfpak") ?? []); SWSHModel.Config = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(BasePath + $"{FileName}.gfbpokecfg")); LoadSWSHModel(pack); } private void LoadModel(GFPack pack) { PLAModel.MMT = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{FileName}.trmmt")); PLAModel.Model = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{FileName}.trmdl")); PLAModel.Skeleton = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{PLAModel.Model.Skeleton.Filename}")); Debug.Assert(PLAModel.MMT.Reserved00 == 0, "Here's one!"); Debug.Assert(PLAModel.MMT.Reserved01 == 0, "Here's one!"); Debug.Assert(PLAModel.Model.Reserved00 == 0, "Here's one!"); foreach (var lod in PLAModel.Model.LODs) { Debug.Assert(lod.Type == "Custom", "Here's one!"); } Debug.Assert(PLAModel.Skeleton.Reserved00 == 0, "Here's one!"); foreach (var node in PLAModel.Skeleton.Nodes!) { Debug.Assert(node.Type is NodeType.Transform or NodeType.Joint or NodeType.Locator, "Here's one!"); } foreach (var boneParam in PLAModel.Skeleton.Bones!) { Debug.Assert(boneParam.Field01 == 1, "Here's one!"); } LoadMaterials(pack); LoadMaterialTable(pack); LoadMeshes(pack); PG_Test.SelectedObject = PLAModel; } private void LoadSWSHModel(GFPack pack) { SWSHModel.GFBModel = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{FileName}.gfbmdl")); SWSHModel.GFBModelRare = FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{FileName}_rare.gfbmdl")); PG_Test_SWSH.SelectedObject = SWSHModel; } private void LoadMaterials(GFPack pack) { PLAModel.DefaultMaterials = PLAModel.Model.Materials .Select(x => FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{x}"))) .ToArray(); PLAModel.UsedTextures = PLAModel.DefaultMaterials.SelectMany(material => material.MaterialPasses.SelectMany(pass => pass.TextureParameters.Select(texture => texture.TextureFile) ) ).ToHashSet().ToArray(); foreach (var material in PLAModel.DefaultMaterials) { foreach (var pass in material.MaterialPasses) { var values = pass.Shaders[0].ShaderValues; var layerCount = int.Parse(values.First(x => x.PropertyBinding.Equals("NumMaterialLayer")).StringValue!); Debug.Assert(layerCount == 5, "Here's one!"); var vertexBaseColor = bool.Parse(values.FirstOrDefault(x => x.PropertyBinding.Equals("EnableVertexBaseColor"))?.StringValue ?? "False"); Debug.Assert(!vertexBaseColor, "Here's one!"); } } } private void LoadMaterialTable(GFPack pack) { PLAModel.MeshMaterials = PLAModel.MMT.Material.Select( x => new MeshMaterialWrapper { Name = x.Name!, Materials = x.FileNames.Select( fileName => FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{fileName}")) ).ToArray(), } ).ToArray(); } private void LoadMeshes(GFPack pack) { PLAModel.Meshes = PLAModel.Model.Meshes .Select(x => FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{x.Filename}"))) .ToArray(); foreach (var mesh in PLAModel.Meshes) { Debug.Assert(mesh.Reserved00 == 0, "Here's one!"); foreach (var shape in mesh.Shapes) { Debug.Assert(shape.IndexLayoutFormat == IndexLayoutFormat.UINT16, "Here's one!"); Debug.Assert(shape.Field05 == 0, "Here's one!"); Debug.Assert(shape.Field06 == 0, "Here's one!"); Debug.Assert(shape.Field07 == 0, "Here's one!"); Debug.Assert(shape.Field08 == 0, "Here's one!"); Debug.Assert(string.IsNullOrEmpty(shape.Field11), "Here's one!"); foreach (var attribute in shape.VertexLayout) { foreach (var attr in attribute.Elements) { Debug.Assert(attr.Slot == 0, "Here's one!"); Debug.Assert(attr.SemanticName <= InputLayoutSemanticName.BLEND_WEIGHTS, "Here's one!"); Debug.Assert(attr.Format is InputLayoutFormat.NONE or InputLayoutFormat.RGBA_8_UNORM or InputLayoutFormat.RGBA_8_UNSIGNED or InputLayoutFormat.RGBA_16_UNORM or InputLayoutFormat.RGBA_16_FLOAT or InputLayoutFormat.RG_32_FLOAT or InputLayoutFormat.RGB_32_FLOAT or InputLayoutFormat.RGBA_32_FLOAT, "Here's one!"); } } foreach (var subMesh in shape.SubMeshes) { Debug.Assert(subMesh.Field02 == 0, "Here's one!"); Debug.Assert(subMesh.Field04 is 0, "Here's one!"); } } } LoadMeshBuffers(PLAModel.Meshes, pack); } private void LoadMeshBuffers(Structures.FlatBuffers.Arceus.Mesh[] trMeshes, GFPack pack) { PLAModel.MeshDataBuffers = trMeshes.Select(x => x.BufferFileName) .Select(x => FlatBufferConverter.DeserializeFrom(pack.GetDataFullPath(ModelPath + $"{x}"))) .ToArray(); for (var i = 0; i < PLAModel.MeshDataBuffers.Length; i++) { var mesh = PLAModel.Meshes[i]; var meshBuffer = PLAModel.MeshDataBuffers[i]; Debug.Assert(meshBuffer.Field00 == 0, "Here's one!"); for (var j = 0; j < meshBuffer.Buffers.Count; j++) { var buffer = meshBuffer.Buffers[j]; var shape = mesh.Shapes[j]; buffer.VertexBuffer[0].Debug_InputLayout = shape.VertexLayout[0]; } } } private void B_Convert_Click(object sender, EventArgs e) { ConvertToConfig(); ConvertToModel(); PG_Converted.SelectedObject = Result; } private void ConvertToConfig() { // TODO: // SWSHModel.Config.MajorVer; // SWSHModel.Config.MinorVer; // SWSHModel.Config.SpeciesId; // SWSHModel.Config.FormId; // SWSHModel.Config.Name; // SWSHModel.Config.JpName; // SWSHModel.Config.SpeciesOrigin; // SWSHModel.Config.Height; // SWSHModel.Config.AdjustHeight; // SWSHModel.Config.FieldAdjust; // SWSHModel.Config.MinBX; // SWSHModel.Config.MinBY; // SWSHModel.Config.MinBZ; // SWSHModel.Config.MaxBX; // SWSHModel.Config.MaxBY; // SWSHModel.Config.MaxBZ; // SWSHModel.Config.InframeHeight; // SWSHModel.Config.RegionId; // SWSHModel.Config.WaitMotionBRate; // SWSHModel.Config.WaitMotionCRate; // SWSHModel.Config.Undef26; // SWSHModel.Config.Undef27; // SWSHModel.Config.MaterialEntries; // SWSHModel.Config.SpeciesModelProperty; Result.Config.Field01 = 1.32f; // TODO Result.Config.Field02 = 1.98f; // TODO Result.Config.Field03 = 5.45f; // TODO Result.Config.Field10YOffset = 0f; // TODO Result.Config.Field11YOffset = -0.07f; // TODO Result.Config.Field12YOffset = 0f; // TODO Result.Config.SizeIndex = SWSHModel.Config.SizeIndex; Result.Config.InframeVerticalRotYOrigin = SWSHModel.Config.InframeVerticalRotYOrigin / 100; Result.Config.InframeBottomYOffset = SWSHModel.Config.InframeBottomYOffset / 100; Result.Config.InframeCenterYOffset = SWSHModel.Config.InframeCenterYOffset / 100; Result.Config.InframeLeftRotation = SWSHModel.Config.InframeLeftRotation; Result.Config.InframeRightRotation = SWSHModel.Config.InframeRightRotation; } private void ConvertToSkeleton() { if (SWSHModel.GFBModel.SkeletonNodes is not { } skeleton) return; // TODO: // Result.Skeleton.SizeType; // Result.Skeleton.Bones[]; // Bones bone matrix is maya’s transform matrix inverted // Result.Skeleton.Iks[]; // TODO: Probably need to ignore bones @ Mesh8.BoneId; // TODO: New structure ends with lod groups // TODO: Separate locators // TODO: Figure out and apply the new bone type structure var transformNodes = new List(); int rigStart = -1; int rigIndex = 0; for (int i = 0; i < skeleton.Count; ++i) { var bone8 = skeleton[i]; if (bone8.Type == BoneType.Transparency_Group) continue; // TODO // TODO: // bone8.Effect; // bone8.IsRigged; // TODO: Should be converted using bone8.IsRigged. Only IsRigged bones are used in vertex blend_indices. // Meaning an array of IsRigged bones is made and blend_indices index into this array. // This might actually one to one convert into rigIndex // TODO: Most entries of type Joint in swsh, but are of type Transform in PLA transformNodes.Add(new() { Name = bone8.Name, TransformData = new Transform { Scale = bone8.Scale, Rotate = bone8.Rotation, Translate = bone8.Translation / 100, // Scale down swsh models by 100 }, ScalePivot = bone8.ScalePivot, RotatePivot = bone8.RotatePivot, ParentIdx = bone8.ParentIdx, // TODO: If some are removed, this id needs to be corrected RigIdx = bone8.IsVisible ? rigIndex++ : -1, LocatorBone = string.Empty, Type = (NodeType)bone8.Type, // TODO: A lot of these seem to be converted into transform nodes instead of joint nodes }); if (bone8.IsVisible && rigStart == -1) { rigStart = i; } } int rigEnd = rigIndex; Debug.Assert(rigStart > 0, "This skeleton seems to not be skinned."); Result.Skeleton.RigOffset = rigStart - 2; // By default we always skip the first two 2 nodes. Any additional offset should be marked. TODO: Is this true? Result.Skeleton.Nodes = transformNodes.ToArray(); // TODO: Result.Skeleton.Bones = new Bone[rigEnd]; } private static InputLayoutFormat ConvertInputLayoutFormat(VertexAttribute attribute) { var result = (attribute.Format, attribute.Count) switch { (DataType.UByte, 4) => InputLayoutFormat.RGBA_8_UNSIGNED, (DataType.HalfFloat, 4) => InputLayoutFormat.RGBA_16_FLOAT, (DataType.UShort, 4) => InputLayoutFormat.RGBA_16_UNORM, // ??? (DataType.Float, 4) => InputLayoutFormat.RGBA_32_FLOAT, (DataType.FixedPoint, 4) => InputLayoutFormat.RGBA_8_UNORM, (DataType.Float, 2) => InputLayoutFormat.RG_32_FLOAT, (DataType.Float, 3) => InputLayoutFormat.RGB_32_FLOAT, _ => InputLayoutFormat.NONE, }; Debug.Assert(result != InputLayoutFormat.NONE, "Error: Conversion resulted in VertexLayoutType.NONE!"); return result; } private static (InputLayoutSemanticName Semantic, uint Index) ConvertInputLayoutSemantic(VertexAttribute attribute) { var result = attribute.Type switch { VertexAttributeType.Position => (InputLayoutSemanticName.POSITION, 0u), VertexAttributeType.Normal => (InputLayoutSemanticName.NORMAL, 0u), VertexAttributeType.Tangent => (InputLayoutSemanticName.TANGENT, 0u), VertexAttributeType.Texcoord_0 => (InputLayoutSemanticName.TEXCOORD, 0u), VertexAttributeType.Texcoord_1 => (InputLayoutSemanticName.TEXCOORD, 1u), VertexAttributeType.Texcoord_2 => (InputLayoutSemanticName.TEXCOORD, 2u), VertexAttributeType.Texcoord_3 => (InputLayoutSemanticName.TEXCOORD, 3u), VertexAttributeType.Color_0 => (InputLayoutSemanticName.COLOR, 0u), VertexAttributeType.Color_1 => (InputLayoutSemanticName.COLOR, 1u), VertexAttributeType.Color_2 => (InputLayoutSemanticName.COLOR, 2u), VertexAttributeType.Color_3 => (InputLayoutSemanticName.COLOR, 3u), VertexAttributeType.Group_Idx => (InputLayoutSemanticName.BLEND_INDICES, 0u), VertexAttributeType.Group_Weight => (InputLayoutSemanticName.BLEND_WEIGHTS, 0u), _ => (InputLayoutSemanticName.NONE, 0u), }; Debug.Assert(result.Item1 != InputLayoutSemanticName.NONE, "Error: Conversion resulted in InputLayoutSemanticName.NONE!"); return result; } private static uint SizeOfInputLayoutFormat(InputLayoutFormat format) { var result = format switch { InputLayoutFormat.RGBA_8_UNSIGNED => 4u, InputLayoutFormat.RGBA_16_FLOAT => 8u, InputLayoutFormat.RGBA_16_UNORM => 8u, InputLayoutFormat.RGBA_32_FLOAT => 16u, InputLayoutFormat.RGBA_8_UNORM => 4u, InputLayoutFormat.RG_32_FLOAT => 8u, InputLayoutFormat.RGB_32_FLOAT => 12u, _ => 0u, }; Debug.Assert(result != 0u, $"Error: Size of {format} resulted in '0'!"); return result; } private void ConvertToMesh(string sourceFileName, string resultFileName) { IList shapes = SWSHModel.GFBModel.Mesh!; IList buffers = SWSHModel.GFBModel.Shapes!; IList bones = SWSHModel.GFBModel.SkeletonNodes!; IList materials = SWSHModel.GFBModel.Materials!; var meshShapes = new List(); var meshBuffers = new List(); foreach (var shape in shapes) { Shape buffer = buffers[(int)shape.ShapeId]; Bone8 bone = bones[(int)shape.BoneId]; string subMeshName = bone.Name!; subMeshName = subMeshName.Replace(sourceFileName + "_", "", StringComparison.InvariantCultureIgnoreCase); subMeshName = subMeshName.Replace("skin", "", StringComparison.InvariantCultureIgnoreCase); subMeshName = subMeshName.ToLowerInvariant(); // TODO: // shape.SortPriority; var inputLayout = new InputLayoutElement[buffer.Attributes!.Count]; uint inputLayoutSize; { uint layoutOffset = 0; for (var i = 0; i < buffer.Attributes.Count; i++) { var attribute = buffer.Attributes[i]; var type = ConvertInputLayoutFormat(attribute); var slot = ConvertInputLayoutSemantic(attribute); inputLayout[i] = new InputLayoutElement { Format = type, SemanticName = slot.Semantic, SemanticIndex = slot.Index, Offset = layoutOffset, }; layoutOffset += SizeOfInputLayoutFormat(type); } inputLayoutSize = layoutOffset; } var subMeshes = new SubMesh[buffer.Polygons!.Count]; uint indexCount; { uint offset = 0; for (var i = 0; i < buffer.Polygons.Count; i++) { var subMesh = buffer.Polygons[i]; var mat = materials[(int)subMesh.MaterialId]; subMeshes[i] = new SubMesh { IndexCount = (uint)subMesh.Indices!.Count, IndexOffset = offset, AppliedMaterial = mat.Name, // TODO }; offset += (uint)subMesh.Indices.Count; } indexCount = offset; } var indexBuffer = new byte[indexCount * 2]; { int offset = 0; Span dst = indexBuffer.AsSpan(); foreach (var subMesh in buffer.Polygons) { foreach (var index in subMesh.Indices!) { WriteUInt16LittleEndian(dst[offset..], index); offset += 2; } } } var uniqueColors0 = new List(); var uniqueColors1 = new List(); VertexWrapper[][] oldVertices = ((ReadOnlySpan)(buffer.Vertices ?? Memory.Empty).Span).GetArray(data => { var vertexData = new VertexWrapper[inputLayout.Length]; for (var j = 0; j < inputLayout.Length; j++) { var layout = inputLayout[j]; var offset = (int)layout.Offset; vertexData[j] = new VertexWrapper(layout, layout.Format switch { InputLayoutFormat.RGBA_8_UNORM => new Vec4f(new Unorm8(data[offset]), new Unorm8(data[offset + 1]), new Unorm8(data[offset + 2]), new Unorm8(data[offset + 3])), InputLayoutFormat.RGBA_8_UNSIGNED => new Vec4i(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]), InputLayoutFormat.RGBA_16_UNORM => new Vec4f(new Unorm16(ReadUInt16LittleEndian(data[offset..])), new Unorm16(ReadUInt16LittleEndian(data[(offset + 2)..])), new Unorm16(ReadUInt16LittleEndian(data[(offset + 4)..])), new Unorm16(ReadUInt16LittleEndian(data[(offset + 6)..]))), InputLayoutFormat.RGBA_16_FLOAT => new Vec4f((float)ReadHalfLittleEndian(data[offset..]), (float)ReadHalfLittleEndian(data[(offset + 2)..]), (float)ReadHalfLittleEndian(data[(offset + 4)..]), (float)ReadHalfLittleEndian(data[(offset + 6)..])), InputLayoutFormat.RG_32_FLOAT => new Vec2f(ReadSingleLittleEndian(data[offset..]), ReadSingleLittleEndian(data[(offset + 4)..])), InputLayoutFormat.RGB_32_FLOAT => new Vec3f(ReadSingleLittleEndian(data[offset..]), ReadSingleLittleEndian(data[(offset + 4)..]), ReadSingleLittleEndian(data[(offset + 8)..])), InputLayoutFormat.RGBA_32_FLOAT => new Vec4f(ReadSingleLittleEndian(data[offset..]), ReadSingleLittleEndian(data[(offset + 4)..]), ReadSingleLittleEndian(data[(offset + 8)..]), ReadSingleLittleEndian(data[(offset + 12)..])), InputLayoutFormat.NONE => throw new IndexOutOfRangeException(), _ => throw new IndexOutOfRangeException(), }); switch (layout.SemanticName) { case InputLayoutSemanticName.POSITION: vertexData[j].Data = (Vec3f)vertexData[j].Data / 100; // Scale down swsh models by 100 break; case InputLayoutSemanticName.COLOR when layout.SemanticIndex == 0: { // TODO: Convert to layer mask format var color = Color4f.FromByteColor((Vec4i)vertexData[j].Data); if (!uniqueColors0.Contains(color)) uniqueColors0.Add(color); // TODO: Only a single color on this layer?? // {{ R: 0.69803923, G: 0.49803925, B: 1, A: 1 }} break; } case InputLayoutSemanticName.COLOR when layout.SemanticIndex == 1: { // TODO: Convert to layer mask format var color = Color4f.FromByteColor((Vec4i)vertexData[j].Data); if (!uniqueColors1.Contains(color)) uniqueColors1.Add(color); // TODO: This layer contained 3 different colors // {{ R: 0.5019608, G: 0, B: 0.5411765, A: 1 }} // {{ R: 0.49411768, G: 0, B: 0.5411765, A: 1 }} // {{ R: 0.49803925, G: 0, B: 0.5411765, A: 1 }} break; } case InputLayoutSemanticName.COLOR: Debug.Assert(false, "Unhandled color semantic"); break; case InputLayoutSemanticName.BLEND_INDICES: // TODO: Might need to update these indices break; } } return vertexData; } , (int)inputLayoutSize); var vertexBuffer = new byte[oldVertices.Length * inputLayoutSize]; { int offset = 0; Span dst = vertexBuffer.AsSpan(); foreach (VertexWrapper[] vertices in oldVertices) { foreach (VertexWrapper vertex in vertices) { var layout = vertex.LayoutElement; switch (layout.Format) { case InputLayoutFormat.RGBA_8_UNORM: { var data = (Vec4f)vertex.Data; dst[offset + 0] = Unorm8.FromFloat(data.X); dst[offset + 1] = Unorm8.FromFloat(data.Y); dst[offset + 2] = Unorm8.FromFloat(data.Z); dst[offset + 3] = Unorm8.FromFloat(data.W); } break; case InputLayoutFormat.RGBA_8_UNSIGNED: { var data = (Vec4i)vertex.Data; dst[offset + 0] = (byte)data.X; dst[offset + 1] = (byte)data.Y; dst[offset + 2] = (byte)data.Z; dst[offset + 3] = (byte)data.W; } break; case InputLayoutFormat.RGBA_16_UNORM: { var data = (Vec4f)vertex.Data; WriteUInt16LittleEndian(dst[(offset + 0)..], Unorm16.FromFloat(data.X)); WriteUInt16LittleEndian(dst[(offset + 2)..], Unorm16.FromFloat(data.Y)); WriteUInt16LittleEndian(dst[(offset + 4)..], Unorm16.FromFloat(data.Z)); WriteUInt16LittleEndian(dst[(offset + 6)..], Unorm16.FromFloat(data.W)); } break; case InputLayoutFormat.RGBA_16_FLOAT: { var data = (Vec4f)vertex.Data; WriteHalfLittleEndian(dst[(offset + 0)..], (Half)data.X); WriteHalfLittleEndian(dst[(offset + 2)..], (Half)data.Y); WriteHalfLittleEndian(dst[(offset + 4)..], (Half)data.Z); WriteHalfLittleEndian(dst[(offset + 6)..], (Half)data.W); } break; case InputLayoutFormat.RG_32_FLOAT: { var data = (Vec2f)vertex.Data; WriteSingleLittleEndian(dst[(offset + 0)..], data.X); WriteSingleLittleEndian(dst[(offset + 4)..], data.Y); } break; case InputLayoutFormat.RGB_32_FLOAT: { var data = (Vec3f)vertex.Data; WriteSingleLittleEndian(dst[(offset + 0)..], data.X); WriteSingleLittleEndian(dst[(offset + 4)..], data.Y); WriteSingleLittleEndian(dst[(offset + 8)..], data.Z); } break; case InputLayoutFormat.RGBA_32_FLOAT: { var data = (Vec4f)vertex.Data; WriteSingleLittleEndian(dst[(offset + 0)..], data.X); WriteSingleLittleEndian(dst[(offset + 4)..], data.Y); WriteSingleLittleEndian(dst[(offset + 8)..], data.Z); WriteSingleLittleEndian(dst[(offset + 12)..], data.W); } break; case InputLayoutFormat.NONE: default: throw new ArgumentOutOfRangeException(); } offset += (int)SizeOfInputLayoutFormat(layout.Format); } } } { var bounds = shape.Bounds / 100; // Scale down swsh models by 100 meshShapes.Add(new MeshShape { MeshShapeName = $"{resultFileName}_{subMeshName}_mesh_shape", MeshName = $"{resultFileName}_{subMeshName}_mesh", VertexLayout = [ new() { Elements = inputLayout, Size = [new() { Size = inputLayoutSize }], }, ], BoundingSphere = new Sphere(bounds), Bounds = bounds, IndexLayoutFormat = IndexLayoutFormat.UINT16, // Always Uint16 on all PLA pokemon models BoneWeights = [], // TODO: All bones affected by the sub-mesh + some weight SubMeshes = subMeshes, }); } { meshBuffers.Add(new MeshBuffer { IndexBuffer = [new() { Data = indexBuffer }], VertexBuffer = [ new() { Data = vertexBuffer, Debug_InputLayout = new VertexAttributeLayout { Elements = inputLayout, Size = [new() { Size = inputLayoutSize }], } } ], }); } } Result.Meshes = [ new() { BufferFileName = $"{resultFileName}.trmbf", Shapes = meshShapes.ToArray(), }, ]; Result.MeshDataBuffers = [ new() { Buffers = meshBuffers.ToArray(), }, ]; } private void ConvertToMeshShape() { } private StringParameter[] ConvertStringParams(Flag[] flags) { // StringParam possible values: // EnableBaseColorMap < Maybe 'required'? // "EnableBaseColorMap" -> Allowed values: "True", "False" | shader_bool: 1 // "EnableNormalMap" -> Allowed values: "True", "False" // "EnableMetallicMap" -> Allowed values: "True", "False" // "EnableRoughnessMap" -> Allowed values: "True", "False" // "EnableEmissionColorMap" -> Allowed values: "True", "False" // "EnableAOMap" -> Allowed values: "True", "False" // "EnableAlphaTest" -> Allowed values: "True", "False" (oldMaterial.TextureAlphaTest == 0).ToString() // "NumMaterialLayer" -> Allowed values: "1", "2", "3", "4", "5" // "BillboardType" -> Allowed values: "Disable", "AxisXYZ", "AxisY" // "WindReceiverType" -> Allowed values: "Disable", "Simple", "Standard", "SimpleLeaf" // "EnableWindMaskMap" -> Allowed values: "True", "False" // "EnableBaseColorMap1" -> Allowed values: "True", "False" // "EnableNormalMap1" -> Allowed values: "True", "False" // "EnableNormalMap2" -> Allowed values: "True", "False" // "EnableAOMap1" -> Allowed values: "True", "False" // "EnableAOMap2" -> Allowed values: "True", "False" // "LayerMaskSource" -> Allowed values: "Const", "Texture", "VertexColor" | shader_bool: 1 // "LayerMaskSwizzle" -> Allowed values: "RGBA", "R111", "A111" // "LayerBaseMaskSource" -> Allowed values: "One", "OneMinusLayerMaskSum" | shader_bool: 1 // "EnableVertexBaseColor" -> Allowed values: "True", "False" // "WeatherLayerMaskSource" -> Allowed values: "Const", "Texture" // "EnableDepthFade" -> Allowed values: "True", "False" // "EnablePackedMap" -> Allowed values: "True", "False" // "EnableUVScaleOffsetNormal" -> Allowed values: "True", "False" // Global params: // "EnableDeferredRendering" -> Allowed values: "True", "False" // "InstancingType" -> Allowed values: "Disable", "World", "Detail" // "EnableGrassCollisionMap" -> Allowed values: "True", "False" // "NumRequiredUV" -> Allowed values: "1", "2" // "EnableWeatherLayer" -> Allowed values: "True", "False" // "EnableDisplacementMap" -> Allowed values: "True", "False" // "EnableLerpBaseColorEmission" -> Allowed values: "True", "False" // EnableUVScaleOffsetNormal -> enables float4 { UVScaleOffsetNormal: { R: 1, G: 1, B: 0, A: 0 } } static string ConvertParamName(string oldName) { var result = oldName switch { "useColorTex" => "EnableBaseColorMap", "SwitchEmissionMaskTexUV" => "", "EmissionMaskUse" => "", "SwitchPriority" => "", "Layer1Enable" => "", "SwitchAmbientTexUV" => "", "AmbientMapEnable" => "EnableAOMap", // TODO: New format only has R channel "SwitchNormalMapUV" => "", "NormalMapEnable" => "EnableNormalMap", "LightTableEnable" => "", "SpecularMaskEnable" => "", "BaseColorAddEnable" => "", "SphereMapEnable" => "", "SphereMaskEnable" => "", "RimMaskEnable" => "", "alphaShell" => "", "EffectVal" => "", "NormalEdgeEnable" => "", "OutLineIDEnable" => "", "OutLineColFixed" => "", // Global flags "FogEnable" => "", "DiscardEnable" => "", // FloatParam: DiscardValue? "CastShadow" => "", "ReceiveShadow" => "", "TextureAlphaTestEnable" => "", "ShadowMapPrevEnable" => "", "LayerCalcMulti" => "", "FireMaskPathEnable" => "", "GPUInstancingEnable" => "", "Wireframe" => "", "DepthWrite" => "", "DepthTest" => "", "IsErase" => "", "MayaPreviewEnable" => "", _ => string.Empty, }; //Debug.Assert(!string.IsNullOrEmpty(result), $"Error: Couldn't convert flag with name: {oldName}!"); return result; } var floats = new StringParameter[flags.Length]; for (var i = 0; i < flags.Length; i++) { var flag = flags[i]; floats[i] = new StringParameter { PropertyBinding = ConvertParamName(flag.FlagName!), StringValue = flag.FlagEnable.ToString(), }; } /*new StringParameter[] { new("EnableAlphaTest", (oldMaterial.TextureAlphaTest == 0).ToString()), // TODO new("NumMaterialLayer", "5"), // TODO: Adds the ParamName + LayerX parameters new("EnableLerpBaseColorEmission", "?"), // TODO new("EnableVertexBaseColor", "?"), // TODO }*/ return floats; } private FloatParameter[] ConvertFloatParams(FloatParam[] floatParams) { static string ConvertParamName(string oldName) { string? result = oldName switch { "0" => "DiscardValue", "1" => "Metallic", "2" => "Roughness", "3" => "NormalHeight", "4" => "EmissionIntensity", "5" => "XLayer#", "ColorUVScaleU" => "", "ColorUVScaleV" => "", "ColorUVTranslateU" => "", "ColorBaseU" => "", "ColorUVTranslateV" => "", "ColorBaseV" => "", "ConstantColor0Val" => "", "Layer1UVScaleU" => "", "Layer1UVScaleV" => "", "Layer1UVTranslateU" => "", "Layer1BaseU" => "", "Layer1UVTranslateV" => "", "Layer1BaseV" => "", "EmissionMaskVal" => "", "ConstantColorSd0Val" => "", "ConstantColor1Val" => "", "ConstantColorSd1Val" => "", "ColorLerpValue" => "", "L1ConstantColor0Val" => "", "L1AddColor0Val" => "", "L1ConstantColor1Val" => "", "L1AddColor1Val" => "", "L1ConstantColorSd0Val" => "", "L1ConstantColorSd1Val" => "", "Layer1OverLerpValue" => "", "NormalMapUVScaleU" => "", "NormalMapUVScaleV" => "", "LightTblIndex" => "", "LightMul" => "", "SpecularPower" => "", "SpecularScale" => "", "SphereMapColorVal" => "", "RimColorVal" => "", "RimPower" => "", "RimStrength" => "", "OnGameEmissionVal" => "", "ConstantColorVal" => "", "ConstantAlpha" => "", "OnGameColorVal" => "", "OnGameAlpha" => "", "OutLineID" => "", "ProgID" => "", "Def0_OneMin1_FreCol" => "", "DistortionIntensity" => "", "Sin01" => "", "ScaleUV" => "", "EffectTexTranslateU" => "", "EffectTexTranslateV" => "", "EffectTexRotate" => "", "EffectTexScaleU" => "", "EffectTexScaleV" => "", "EffectColPower" => "", // Uber values "CullMode" => "", "LightSetNo" => "", "ShaderType" => "", "Priority" => "", "MipMapBias" => "", "PreMultiplieMode" => "", "BlendMode" => "", "ColorMapUvIndex" => "", "Layer1UvIdx" => "", "EmissionMaskTexSS" => "", "AmbientTexSS" => "", "NormalMapTexSS" => "", "Col0TexSS" => "", "LyCol0TexSS" => "", "PolygonOffset" => "", _ => string.Empty, }; // Discard null result, this means the value no longer applies //Debug.Assert(!result.Equals(string.Empty), $"Error: Couldn't convert float param with name: {oldName}!"); return result; } var floats = new FloatParameter[floatParams.Length]; for (var i = 0; i < floatParams.Length; i++) { var floatParam = floatParams[i]; floats[i] = new FloatParameter { PropertyBinding = ConvertParamName(floatParam.ValueName!), FloatValue = floatParam.Value, }; } return floats; } private Float4Parameter[] ConvertColorParams(Color3Param[] colorParams) { static string ConvertParamName(string oldName) { string? result = oldName switch { "0" => "", "ConstantColor0" => "", "ConstantColorSd0" => "", "ConstantColor1" => "", "ConstantColorSd1" => "", "L1ConstantColor0" => "", "L1AddColor0" => "", "L1ConstantColor1" => "", "L1AddColor1" => "", "L1ConstantColorSd0" => "", "L1ConstantColorSd1" => "", "DeepShadowColor" => "", "SpecularColor" => "", "SphereMapColor" => "", "RimColor" => "", "RimColorShadow" => "", "ConstantColor" => "", "OnGameColor" => "", "OutLineCol" => "", "EffectColor01" => "", _ => string.Empty, }; // Discard null result, this means the value no longer applies //Debug.Assert(!result.Equals(string.Empty), $"Error: Couldn't convert float param with name: {oldName}!"); return result; } var colors = new Float4Parameter[colorParams.Length]; for (var i = 0; i < colorParams.Length; i++) { var colorParam = colorParams[i]; colors[i] = new Float4Parameter { PropertyBinding = ConvertParamName(colorParam.ColorName!), ColorValue = new() { R = colorParam.Color?.R ?? 0, G = colorParam.Color?.G ?? 0, B = colorParam.Color?.B ?? 0 }, }; } return colors; } private enum StandardShaderProperties { // StringParam possible values: // "EnableBaseColorMap" -> Allowed values: "True", "False" | shader_bool: 1 < Maybe 'required'? // "EnableNormalMap" -> Allowed values: "True", "False" // "EnableMetallicMap" -> Allowed values: "True", "False" // "EnableRoughnessMap" -> Allowed values: "True", "False" // "EnableEmissionColorMap" -> Allowed values: "True", "False" // "EnableAOMap" -> Allowed values: "True", "False" // "EnableAlphaTest" -> Allowed values: "True", "False" (oldMaterial.TextureAlphaTest == 0).ToString() // "NumMaterialLayer" -> Allowed values: "1", "2", "3", "4", "5" // "BillboardType" -> Allowed values: "Disable", "AxisXYZ", "AxisY" // "WindReceiverType" -> Allowed values: "Disable", "Simple", "Standard", "SimpleLeaf" // "EnableWindMaskMap" -> Allowed values: "True", "False" // "EnableBaseColorMap1" -> Allowed values: "True", "False" // "EnableNormalMap1" -> Allowed values: "True", "False" // "EnableNormalMap2" -> Allowed values: "True", "False" // "EnableAOMap1" -> Allowed values: "True", "False" // "EnableAOMap2" -> Allowed values: "True", "False" // "LayerMaskSource" -> Allowed values: "Const", "Texture", "VertexColor" | shader_bool: 1 // "LayerMaskSwizzle" -> Allowed values: "RGBA", "R111", "A111" // "LayerBaseMaskSource" -> Allowed values: "One", "OneMinusLayerMaskSum" | shader_bool: 1 // "EnableVertexBaseColor" -> Allowed values: "True", "False" // "WeatherLayerMaskSource" -> Allowed values: "Const", "Texture" // "EnableDepthFade" -> Allowed values: "True", "False" // "EnablePackedMap" -> Allowed values: "True", "False" // "EnableUVScaleOffsetNormal" -> Allowed values: "True", "False" // Global params: // "EnableDeferredRendering" -> Allowed values: "True", "False" // "InstancingType" -> Allowed values: "Disable", "World", "Detail" // "EnableGrassCollisionMap" -> Allowed values: "True", "False" // "NumRequiredUV" -> Allowed values: "1", "2" // "EnableWeatherLayer" -> Allowed values: "True", "False" // "EnableDisplacementMap" -> Allowed values: "True", "False" // "EnableLerpBaseColorEmission" -> Allowed values: "True", "False" }; private class StandardShader { public static StandardShader FromSWSHStandardShader(SWSHStandardShader shader) { return new StandardShader() { EnableBaseColorMap = shader.UseColorTex, EnableNormalMap = shader.NormalMapEnable, // EnableAOMap = shader.AmbientMapEnable, // EnableVertexBaseColor = shader.BaseColorAddEnable, // TODO: This seems incorrect }; } public bool EnableBaseColorMap { get; set; } = true; public bool EnableNormalMap { get; set; } = true; public bool EnableMetallicMap { get; set; } = false; public bool EnableRoughnessMap { get; set; } = false; public bool EnableEmissionColorMap { get; set; } = false; public bool EnableAOMap { get; set; } = false; public bool EnableAlphaTest { get; set; } = false; public int NumMaterialLayer { get; set; } = 5; public bool BillboardType { get; set; } public bool WindReceiverType { get; set; } public bool EnableWindMaskMap { get; set; } public bool EnableBaseColorMap1 { get; set; } public bool EnableNormalMap1 { get; set; } public bool EnableNormalMap2 { get; set; } public bool EnableAOMap1 { get; set; } public bool EnableAOMap2 { get; set; } public bool LayerMaskSource { get; set; } public bool LayerMaskSwizzle { get; set; } public bool LayerBaseMaskSource { get; set; } public bool EnableVertexBaseColor { get; set; } public bool WeatherLayerMaskSource { get; set; } public bool EnableDepthFade { get; set; } public bool EnablePackedMap { get; set; } public bool EnableUVScaleOffsetNormal { get; set; } = true; // Global params: public bool EnableDeferredRendering { get; set; } public bool InstancingType { get; set; } public bool EnableGrassCollisionMap { get; set; } public int NumRequiredUV { get; set; } = 1; public bool EnableWeatherLayer { get; set; } public bool EnableDisplacementMap { get; set; } public bool EnableLerpBaseColorEmission { get; set; } public StringParameter[] BuildStringParams() { var stringParams = new List { new() { PropertyBinding = "EnableBaseColorMap", StringValue = EnableBaseColorMap.ToString() }, new() { PropertyBinding = "EnableNormalMap", StringValue = EnableNormalMap.ToString() }, new() { PropertyBinding = "EnableParallaxMap",StringValue = "False" }, // TODO: This one seems to not be allowed by the default shader, yet it's always added? new() { PropertyBinding = "EnableMetallicMap", StringValue = EnableMetallicMap.ToString() }, new() { PropertyBinding = "EnableRoughnessMap", StringValue = EnableRoughnessMap.ToString() }, new() { PropertyBinding = "EnableEmissionColorMap", StringValue = EnableEmissionColorMap.ToString() }, new() { PropertyBinding = "EnableAOMap", StringValue = EnableAOMap.ToString() }, new() { PropertyBinding = "EnableAlphaTest", StringValue = EnableAlphaTest.ToString() }, new() { PropertyBinding = "NumMaterialLayer", StringValue = NumMaterialLayer.ToString() }, new() { PropertyBinding = "EnableUVScaleOffsetNormal", StringValue = EnableUVScaleOffsetNormal.ToString() }, new() { PropertyBinding = "EnableVertexBaseColor", StringValue = EnableVertexBaseColor.ToString() }, }; //if (false) //{ // stringParams.Add(new() { PropertyBinding = "BillboardType", StringValue = BillboardType.ToString() }); // stringParams.Add(new() { PropertyBinding = "WindReceiverType", StringValue = WindReceiverType.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableWindMaskMap", StringValue = EnableWindMaskMap.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableBaseColorMap1", StringValue = EnableBaseColorMap1.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableNormalMap1", StringValue = EnableNormalMap1.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableNormalMap2", StringValue = EnableNormalMap2.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableAOMap1", StringValue = EnableAOMap1.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableAOMap2", StringValue = EnableAOMap2.ToString() }); // stringParams.Add(new() { PropertyBinding = "LayerMaskSource", StringValue = LayerMaskSource.ToString() }); // stringParams.Add(new() { PropertyBinding = "LayerMaskSwizzle", StringValue = LayerMaskSwizzle.ToString() }); // stringParams.Add(new() { PropertyBinding = "LayerBaseMaskSource", StringValue = LayerBaseMaskSource.ToString() }); // stringParams.Add(new() { PropertyBinding = "WeatherLayerMaskSource", StringValue = WeatherLayerMaskSource.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableDepthFade", StringValue = EnableDepthFade.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnablePackedMap", StringValue = EnablePackedMap.ToString() }); // // stringParams.Add(new() { PropertyBinding = "EnableDeferredRendering", StringValue = EnableDeferredRendering.ToString() }); // stringParams.Add(new() { PropertyBinding = "InstancingType", StringValue = InstancingType.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableGrassCollisionMap", StringValue = EnableGrassCollisionMap.ToString() }); // stringParams.Add(new() { PropertyBinding = "NumRequiredUV", StringValue = NumRequiredUV.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableWeatherLayer", StringValue = EnableWeatherLayer.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableDisplacementMap", StringValue = EnableDisplacementMap.ToString() }); // stringParams.Add(new() { PropertyBinding = "EnableLerpBaseColorEmission", StringValue = EnableLerpBaseColorEmission.ToString() }); //} return [.. stringParams]; } } private class SWSHStandardShader { public SWSHStandardShader(Material8 source) { // TODO var oldUberFlags = source.StaticParam!.UberFlags!.ToDictionary(flag => flag.FlagName!, flag => flag.FlagEnable.ToString()); foreach (var flag in source.Flags!) { switch (flag.FlagName) { case "useColorTex": UseColorTex = flag.FlagEnable; break; case "SwitchEmissionMaskTexUV": SwitchEmissionMaskTexUV = flag.FlagEnable; break; case "EmissionMaskUse": EmissionMaskUse = flag.FlagEnable; break; case "SwitchPriority": SwitchPriority = flag.FlagEnable; break; case "Layer1Enable": Layer1Enable = flag.FlagEnable; break; case "SwitchAmbientTexUV": SwitchAmbientTexUV = flag.FlagEnable; break; case "AmbientMapEnable": AmbientMapEnable = flag.FlagEnable; break; case "SwitchNormalMapUV": SwitchNormalMapUV = flag.FlagEnable; break; case "NormalMapEnable": NormalMapEnable = flag.FlagEnable; break; case "LightTableEnable": LightTableEnable = flag.FlagEnable; break; case "SpecularMaskEnable": SpecularMaskEnable = flag.FlagEnable; break; case "BaseColorAddEnable": BaseColorAddEnable = flag.FlagEnable; break; case "SphereMapEnable": SphereMapEnable = flag.FlagEnable; break; case "SphereMaskEnable": SphereMaskEnable = flag.FlagEnable; break; case "RimMaskEnable": RimMaskEnable = flag.FlagEnable; break; case "alphaShell": AlphaShell = flag.FlagEnable; break; case "EffectVal": EffectVal = flag.FlagEnable; break; case "NormalEdgeEnable": NormalEdgeEnable = flag.FlagEnable; break; case "OutLineIDEnable": OutLineIDEnable = flag.FlagEnable; break; case "OutLineColFixed": OutLineColFixed = flag.FlagEnable; break; // Global flags case "FogEnable": FogEnable = flag.FlagEnable; break; case "DiscardEnable": DiscardEnable = flag.FlagEnable; break; case "CastShadow": CastShadow = flag.FlagEnable; break; case "ReceiveShadow": ReceiveShadow = flag.FlagEnable; break; case "TextureAlphaTestEnable": TextureAlphaTestEnable = flag.FlagEnable; break; case "ShadowMapPrevEnable": ShadowMapPrevEnable = flag.FlagEnable; break; case "LayerCalcMulti": LayerCalcMulti = flag.FlagEnable; break; case "FireMaskPathEnable": FireMaskPathEnable = flag.FlagEnable; break; case "GPUInstancingEnable": GPUInstancingEnable = flag.FlagEnable; break; case "Wireframe": Wireframe = flag.FlagEnable; break; case "DepthWrite": DepthWrite = flag.FlagEnable; break; case "DepthTest": DepthTest = flag.FlagEnable; break; case "IsErase": IsErase = flag.FlagEnable; break; case "MayaPreviewEnable": MayaPreviewEnable = flag.FlagEnable; break; default: Debug.Assert(false, $"Error: Couldn't convert shader flag with name: {flag.FlagName}!"); break; } } var valueLookup = source.Values!.ToDictionary(flag => flag.ValueName!, flag => flag.Value); UVScaleOffset = new Vec4f(valueLookup["ColorUVScaleU"], valueLookup["ColorUVScaleV"], valueLookup["ColorBaseU"], valueLookup["ColorBaseV"]); UVScaleOffsetNormal = new Vec4f(valueLookup["NormalMapUVScaleU"], valueLookup["NormalMapUVScaleV"]); // TODO: Set EnableUVScaleOffsetNormal if changed } public bool UseColorTex { get; set; } public bool SwitchEmissionMaskTexUV { get; set; } public bool EmissionMaskUse { get; set; } public bool SwitchPriority { get; set; } public bool Layer1Enable { get; set; } public bool SwitchAmbientTexUV { get; set; } public bool AmbientMapEnable { get; set; } public bool SwitchNormalMapUV { get; set; } public bool NormalMapEnable { get; set; } public bool LightTableEnable { get; set; } public bool SpecularMaskEnable { get; set; } public bool BaseColorAddEnable { get; set; } public bool SphereMapEnable { get; set; } public bool SphereMaskEnable { get; set; } public bool RimMaskEnable { get; set; } public bool AlphaShell { get; set; } public bool EffectVal { get; set; } public bool NormalEdgeEnable { get; set; } public bool OutLineIDEnable { get; set; } public bool OutLineColFixed { get; set; } // Global flags public bool FogEnable { get; set; } public bool DiscardEnable { get; set; } public bool CastShadow { get; set; } public bool ReceiveShadow { get; set; } public bool TextureAlphaTestEnable { get; set; } public bool ShadowMapPrevEnable { get; set; } public bool LayerCalcMulti { get; set; } public bool FireMaskPathEnable { get; set; } public bool GPUInstancingEnable { get; set; } public bool Wireframe { get; set; } public bool DepthWrite { get; set; } public bool DepthTest { get; set; } public bool IsErase { get; set; } public bool MayaPreviewEnable { get; set; } public Vec4f UVScaleOffset { get; set; } public Vec4f UVScaleOffsetNormal { get; set; } public bool IsTextureBound(string samplerName) { return samplerName switch { "Col0Tex" => UseColorTex, "EmissionMaskTex" => EmissionMaskUse, "LyCol0Tex" => false, // Layer1Enable, // TODO "AmbientTex" => false, // TODO: Temp set to false; AmbientMapEnable, "NormalMapTex" => NormalMapEnable, "LightTblTex" => false, // Don't need this anymore, so don't care "SphereMapTex" => SphereMapEnable, "EffectTex" => false, // TODO: EffectVal? _ => false, }; } } private (TextureParameter[], SamplerState[]) ConvertTextureParams(IList oldTextures, SWSHStandardShader shader) { static (string, int) ConvertSamplerNameAndPriority(string swshSamplerName) { var result = swshSamplerName switch { "0" => ("LayerMaskMap", 1), "1" => ("MetallicMap", 3), "2" => ("RoughnessMap", 4), "Col0Tex" => ("BaseColorMap", 0), "EmissionMaskTex" => ("", -1), "LyCol0Tex" => ("", -1), "AmbientTex" => ("AOMap", 3), // TODO: New format only has R channel "NormalMapTex" => ("NormalMap", 2), // TODO: New format is in tangent space instead of object space "LightTblTex" => ("", -1), "SphereMapTex" => ("", -1), "EffectTex" => ("", -1), _ => (string.Empty, -1), }; //Debug.Assert(!string.IsNullOrEmpty(result), $"Error: Couldn't convert sampler with name: {swshSamplerName}!"); return result; } static UVWrapMode FromOldWrapMode(UVWrapMode8 mode) => mode switch { UVWrapMode8.CLAMP => UVWrapMode.REPEAT, UVWrapMode8.BORDER => UVWrapMode.Border, UVWrapMode8.WRAP => UVWrapMode.CLAMP_TO_EDGE, UVWrapMode8.MIRROR => UVWrapMode.MIRROR, _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unknown uv mode"), }; IList files = SWSHModel.GFBModel.TextureFiles!; var textureBindings = new List<(TextureParameter, SamplerState)>(oldTextures.Count); foreach (var oldTexture in oldTextures) { if (!shader.IsTextureBound(oldTexture.SamplerName!)) continue; var (name, priority) = ConvertSamplerNameAndPriority(oldTexture.SamplerName!); textureBindings.Add((new TextureParameter { PropertyBinding = name, TextureSlot = (uint)priority, // TODO: priority sort TextureFile = files[(int)oldTexture.TextureIndex] + ".bntx", }, new SamplerState { RepeatU = FromOldWrapMode(oldTexture.Settings!.RepeatU), RepeatV = FromOldWrapMode(oldTexture.Settings.RepeatV), RepeatW = FromOldWrapMode(oldTexture.Settings.Repeat2), BorderColor = oldTexture.Settings.BorderColor, //SamplerState0 = oldTexture.Settings.Filtermode, //SamplerState1 = oldTexture.Settings.MipMapBias })); } textureBindings.Insert(1, (new TextureParameter { PropertyBinding = "LayerMaskMap", TextureSlot = 1, TextureFile = files[0].Replace("col", "lym") + ".bntx", }, new())); var ordered = textureBindings.OrderBy(x => x.Item1.TextureSlot); TextureParameter[] textures = ordered.Select(x => x.Item1).ToArray(); SamplerState[] samplers = ordered.Select(x => x.Item2) // Always two more then the texture count that are always Wrap. Probably some default samplers for deferred render buffers .Append(new() { RepeatU = UVWrapMode.CLAMP_TO_EDGE, RepeatV = UVWrapMode.CLAMP_TO_EDGE }) .Append(new() { RepeatU = UVWrapMode.CLAMP_TO_EDGE, RepeatV = UVWrapMode.CLAMP_TO_EDGE }) .ToArray(); return (textures, samplers); } private MaterialPass FromStandardShaderParams(Material8 oldMaterial) { var oldShader = new SWSHStandardShader(oldMaterial); var newShader = StandardShader.FromSWSHStandardShader(oldShader); var (textures, samplers) = ConvertTextureParams(oldMaterial.Textures!, oldShader); return new MaterialPass { Name = oldMaterial.Name!, Shaders = [ new () { ShaderName = "Standard", ShaderValues = newShader.BuildStringParams(), }, ], FloatParameters = [ new() { PropertyBinding = "DiscardValue", FloatValue = 0 }, new() { PropertyBinding = "NormalHeight", FloatValue = 1 }, new() { PropertyBinding = "EmissionIntensity", FloatValue = 0 }, new() { PropertyBinding = "EmissionIntensityLayer1", FloatValue = 0 }, new() { PropertyBinding = "EmissionIntensityLayer2", FloatValue = 0 }, new() { PropertyBinding = "EmissionIntensityLayer3", FloatValue = 0 }, new() { PropertyBinding = "EmissionIntensityLayer4", FloatValue = 0 }, new() { PropertyBinding = "Roughness", FloatValue = 0.5f }, new() { PropertyBinding = "RoughnessLayer1", FloatValue = 0.5f }, new() { PropertyBinding = "RoughnessLayer2", FloatValue = 0.5f }, new() { PropertyBinding = "RoughnessLayer3", FloatValue = 0.5f }, new() { PropertyBinding = "RoughnessLayer4", FloatValue = 0.5f }, new() { PropertyBinding = "Metallic", FloatValue = 0 }, new() { PropertyBinding = "MetallicLayer1", FloatValue = 0 }, new() { PropertyBinding = "MetallicLayer2", FloatValue = 0 }, new() { PropertyBinding = "MetallicLayer3", FloatValue = 0 }, new() { PropertyBinding = "MetallicLayer4", FloatValue = 0 }, new() { PropertyBinding = "LayerMaskScale1", FloatValue = 1 }, new() { PropertyBinding = "LayerMaskScale2", FloatValue = 1 }, new() { PropertyBinding = "LayerMaskScale3", FloatValue = 1 }, new() { PropertyBinding = "LayerMaskScale4", FloatValue = 1 }, ], TextureParameters = textures, Samplers = samplers, Float4LightParameters = [], // TODO Float4Parameters = [ new() { PropertyBinding = "UVScaleOffset" , ColorValue = new Color4f(oldShader.UVScaleOffset) }, new() { PropertyBinding = "UVScaleOffsetNormal", ColorValue = new Color4f(oldShader.UVScaleOffsetNormal) }, new() { PropertyBinding = "BaseColorLayer1" , ColorValue = new() }, new() { PropertyBinding = "BaseColorLayer2" , ColorValue = new() }, new() { PropertyBinding = "BaseColorLayer3" , ColorValue = new() }, new() { PropertyBinding = "BaseColorLayer4" , ColorValue = new() }, new() { PropertyBinding = "EmissionColorLayer1", ColorValue = new() }, new() { PropertyBinding = "EmissionColorLayer2", ColorValue = new() }, new() { PropertyBinding = "EmissionColorLayer3", ColorValue = new() }, new() { PropertyBinding = "EmissionColorLayer4", ColorValue = new() }, new() { PropertyBinding = "EmissionColor" , ColorValue = new() }, ], IntParameters = [ new() { PropertyBinding = "CastShadow", IntValue = oldMaterial.CastShadow }, new() { PropertyBinding = "ReceiveShadow", IntValue =1 }, // TODO: might want to force this to 1 new() { PropertyBinding = "CategoryLabel", IntValue = 2 }, // TODO new() { PropertyBinding = "UVIndexLayerMask", IntValue = -1 }, // TODO ], WriteMask = new WriteMaskData(), // TODO IntExtra = new IntExtraData(), // TODO AlphaType = "Opaque", // TODO Field05 = [], Field08 = [], Field10 = [], Field11 = [], Field12 = [], }; } private MaterialPass FromEyeShaderParams(Material8 oldMaterial) { // "EnableNormalMap" -> Allowed values: "True", "False" // "EnableParallaxMap" -> Allowed values: "True", "False" // "EnableMetallicMap" -> Allowed values: "True", "False" // "EnableRoughnessMap" -> Allowed values: "True", "False" // "EnableEmissionColorMap" -> Allowed values: "True", "False" // "EnableAOMap" -> Allowed values: "True", "False" // "EyelidType" -> Allowed values: "None", "Upper", "Lower", "All" // "NumMaterialLayer" -> Allowed values: "1", "2", "3", "4", "5" // "EnableHighlight" -> Allowed values: "True", "False" // "UVTransformMode" -> Allowed values: "SRT", "T" // "EnableOverrideColor" -> Allowed values: "True", "False" // Global: // "EnableWeatherLayer" -> Allowed values: "True", "False" return new MaterialPass { Name = oldMaterial.Name!, Shaders = [ new() { ShaderName = "Eye", ShaderValues = [ new() { PropertyBinding = "EnableNormalMap", StringValue = "True" }, new() { PropertyBinding = "EnableParallaxMap", StringValue = "False" }, new() { PropertyBinding = "EnableMetallicMap", StringValue = "False" }, new() { PropertyBinding = "EnableRoughnessMap", StringValue = "False" }, new() { PropertyBinding = "EnableEmissionColorMap", StringValue = "False" }, new() { PropertyBinding = "EnableAOMap", StringValue = "True" }, new() { PropertyBinding = "NumMaterialLayer", StringValue = "1" }, ], }, ], FloatParameters = [], TextureParameters = [], // TODO Samplers = [], // TODO Float4LightParameters = [], // TODO Float4Parameters = [], // TODO IntParameters = [], // TODO WriteMask = new WriteMaskData(), // TODO IntExtra = new IntExtraData(), // TODO AlphaType = "", // TODO Field05 = [], Field08 = [], Field10 = [], Field11 = [], Field12 = [], }; } private void ConvertToMaterial() { var materialPasses = new List(); foreach (var material in SWSHModel.GFBModel.Materials!) { // TODO: // material.Shader; // material.SortPriority; // material.DepthWrite; // material.DepthTest; // material.LightSetNum; // material.BlendMode; // material.CullMode; // material.VertexShaderFileId; // material.GeomShaderFileId; // material.FragShaderFileId; // material.ReceiveShadow; // material.CastShadow; // material.SelfShadow; // material.TextureAlphaTest; // material.DepthComparisonFunction; // material.DepthBias; // material.Field_18; // material.Field_19; materialPasses.Add(FromStandardShaderParams(material)); //materialPasses.Add(FromEyeShaderParams(material)); } Result.DefaultMaterials = [ new() { MaterialPasses = materialPasses.ToArray(), }, ]; // TODO: Assign shiny colors Result.MeshMaterials = [ new() { Name = "rare", Materials = [ new() { Reserved00 = 0, MaterialPasses = materialPasses.ToArray(), }, ], }, ]; } private void ConvertToMultiMaterialTable(string resultFileName) { Debug.Assert(Result.Meshes.Length != 0, "Meshes should be converted before materials"); var allShapes = new List(); foreach (var mesh in Result.Meshes) { foreach (var shape in mesh.Shapes) { allShapes.Add(new MaterialSwitch { Name = shape.MeshShapeName, Flags = 1, // TODO }); } } Result.MMT.Material = [ new () { Name = "rare", FileNames = [$"{resultFileName}_rare.trmtr"], MaterialSwitches = allShapes.ToArray(), MaterialProperties = [], // TODO }, ]; } private void ConvertToModel() { string resultFileName = $"pm{(ushort)SWSHModel.Config.SpeciesId:0000}_00_00"; string sourceFileName = $"pm{(ushort)SWSHModel.Config.SpeciesId:0000}_00"; Result.Model.Meshes = [ new() { Filename = $"{resultFileName}.trmsh" }, ]; Result.Model.Skeleton = new FileReference { Filename = $"{resultFileName}.trskl" }; Result.Model.Materials = [$"{resultFileName}.trmtr"]; // SWSH models don't have LOD's, so we only need one Result.Model.LODs = [ new() { Type = "Custom", Entries = [ new() { Index = 0 }, ], }, ]; Result.Model.Bounds = SWSHModel.GFBModel.BoundingBox / 100; Result.Model.SphereBounds = new Sphere(Result.Model.Bounds!); ConvertToSkeleton(); ConvertToMesh(sourceFileName, resultFileName); ConvertToMeshShape(); ConvertToMaterial(); ConvertToMultiMaterialTable(resultFileName); } private void CB_Species_SelectedIndexChanged(object sender, EventArgs e) { UpdatePLAModel(); } private void CB_SWSHSpecies_SelectedIndexChanged(object sender, EventArgs e) { UpdateSWSHModel(); } /*public static T DeepClone(T obj) where T : class, new() { var data = FlatBufferConverter.SerializeFrom(obj); return FlatBufferConverter.DeserializeFrom(data); }*/ private void B_Save_Click(object sender, EventArgs e) { //Result = DeepClone(PLAModel); //Result.DefaultMaterials[0].MaterialPasses[3] = DeepClone(Result.DefaultMaterials[0].MaterialPasses[2]); //PG_Converted.SelectedObject = Result; //return; SpeciesId = (ushort)SWSHModel.Config.SpeciesId; string resultFileName = $"pm{SpeciesId:0000}_00_00"; FileName = resultFileName; BasePath = Path.Combine(PokemonModelDir.FilePath!, $"bin/pokemon/pm{SpeciesId:0000}/{FileName}/"); PokemonModelDir.AddFile(BasePath + $"{FileName}.trpokecfg", Result.Config.SerializeFrom()); PokemonModelDir.AddFile(ModelPath + $"{FileName}.trmmt", Result.MMT.SerializeFrom()); PokemonModelDir.AddFile(ModelPath + $"{FileName}.trmdl", Result.Model.SerializeFrom()); PokemonModelDir.AddFile(ModelPath + $"{Result.Model.Skeleton.Filename}", Result.Skeleton.SerializeFrom()); PokemonModelDir.AddFile(ModelPath + $"{FileName}.trpokecfg", Result.Config.SerializeFrom()); for (var i = 0; i < Result.Model.Materials.Count; i++) { var materialName = Result.Model.Materials[i]; PokemonModelDir.AddFile(ModelPath + $"{materialName}", Result.DefaultMaterials[i].SerializeFrom()); } for (var i = 0; i < Result.MeshMaterials.Length; i++) { for (var j = 0; j < Result.MeshMaterials[i].Materials.Length; j++) { if (Result.MMT.Material[i].Name == "normal") continue; // The default material was already created var materialName = Result.MMT.Material[i].FileNames[j]; var material = Result.MeshMaterials[i].Materials[j]; PokemonModelDir.AddFile(ModelPath + $"{materialName}", material.SerializeFrom()); } } for (var i = 0; i < Result.Model.Meshes.Count; i++) { var meshName = Result.Model.Meshes[i].Filename; PokemonModelDir.AddFile(ModelPath + $"{meshName}", Result.Meshes[i].SerializeFrom()); } for (var i = 0; i < Result.Meshes.Length; i++) { var meshBufferName = Result.Meshes[i].BufferFileName; PokemonModelDir.AddFile(ModelPath + $"{meshBufferName}", Result.MeshDataBuffers[i].SerializeFrom()); } PokemonModelDir.Dump(); } }