├── .editorconfig ├── .github └── workflows │ └── build_n_test.yml ├── .gitignore ├── LICENSE ├── Licenses └── MeshIO.md ├── MeshSharp.Examples ├── MeshSharp.Examples.csproj └── Program.cs ├── MeshSharp.FBX ├── Converters │ ├── FbxConverter7400.cs │ ├── FbxConverterBase.cs │ ├── IFbxConverter.cs │ ├── INodeParser.cs │ ├── NodeParser7400.cs │ └── NodeParserBase.cs ├── DeflateWithChecksum.cs ├── ErrorLevel.cs ├── Exceptions │ ├── FbxConverterException.cs │ └── FbxParserException.cs ├── FbxBinary.cs ├── FbxElementDefinition.cs ├── FbxNode.cs ├── FbxNodeCollection.cs ├── FbxProperty.cs ├── FbxPropertyFlag.cs ├── FbxProperty[T].cs ├── FbxRootNode.cs ├── FbxVersion.cs ├── MeshSharp.FBX.csproj ├── Notes.md ├── Parsers │ ├── FbxAsciiParser.cs │ ├── FbxBinaryParser.cs │ └── IFbxParser.cs ├── Readers │ ├── FbxReader.cs │ └── IFbxReader.cs └── Writers │ ├── FbxAsciiWriter.cs │ ├── FbxBinaryWriter.cs │ ├── FbxWriter.cs │ ├── FbxWriterOptions.cs │ └── IFbxWriter.cs ├── MeshSharp.GLTF ├── Class1.cs └── MeshSharp.GLTF.csproj ├── MeshSharp.OBJ ├── MeshSharp.OBJ.csproj ├── Notes.md ├── ObjAsciiWriter.cs ├── ObjMesh.cs ├── ObjReader.cs └── ObjWriter.cs ├── MeshSharp.PLY ├── FileFormatDefinition.md ├── MeshSharp.PLY.csproj ├── Notes.md ├── PlyFileFormat.cs ├── PlyHeader.cs ├── PlyMesh.cs ├── PlyWriter.cs └── Writers │ ├── PlyAsciiWriter.cs │ └── PlyWriterBase.cs ├── MeshSharp.PMX ├── Class1.cs ├── MeshSharp.PMX.csproj └── Notes.md ├── MeshSharp.STL ├── MeshSharp.STL.csproj ├── Notes.md ├── Readers │ ├── StlAsciiReader.cs │ ├── StlBinaryReader.cs │ └── StlReaderBase.cs ├── StlReader.cs ├── StlTriangle.cs ├── StlWriter.cs └── Writers │ ├── StlAsciiWriter.cs │ ├── StlBinaryWriter.cs │ └── StlWriterBase.cs ├── MeshSharp.Tests ├── Matrix4Tests.cs ├── MeshSharp.Tests.csproj ├── TransformTests.cs ├── VectorExtensionsTests.cs ├── VectorTestCaseFactory.cs ├── VectorTests.cs ├── XYZMTests.cs └── XYZTests.cs ├── MeshSharp.Viewer ├── App.axaml ├── App.axaml.cs ├── MainWindow.axaml ├── MainWindow.axaml.cs ├── MeshSharp.Viewer.csproj └── Program.cs ├── MeshSharp.sln ├── MeshSharp ├── Color.cs ├── Elements │ ├── Camera.cs │ ├── Element.cs │ ├── Geometries │ │ ├── Geometry.cs │ │ ├── Layers │ │ │ ├── LayerCollection.cs │ │ │ ├── LayerElement.cs │ │ │ ├── LayerElementBinormal.cs │ │ │ ├── LayerElementEdgeCrease.cs │ │ │ ├── LayerElementHole.cs │ │ │ ├── LayerElementMaterial.cs │ │ │ ├── LayerElementNormal.cs │ │ │ ├── LayerElementPolygonGroup.cs │ │ │ ├── LayerElementSmoothing.cs │ │ │ ├── LayerElementSpecular.cs │ │ │ ├── LayerElementTangent.cs │ │ │ ├── LayerElementUV.cs │ │ │ ├── LayerElementUserData.cs │ │ │ ├── LayerElementVertexColor.cs │ │ │ ├── LayerElementVertexCrease.cs │ │ │ ├── LayerElementVisibility.cs │ │ │ ├── LayerElementWeight.cs │ │ │ ├── MappingMode.cs │ │ │ └── ReferenceMode.cs │ │ ├── Mesh.cs │ │ └── MeshUtils.cs │ ├── Material.cs │ ├── Node.cs │ └── Scene.cs ├── IO │ ├── BinaryReaderExtensions.cs │ ├── BinaryWriterExtensions.cs │ ├── EndianReader.cs │ ├── EndianWriter.cs │ ├── IBinaryReadable.cs │ ├── IBinaryWriteable.cs │ ├── MeshReaderBase.cs │ └── MeshWriterBase.cs ├── Math │ ├── IVector.cs │ ├── Matrix4.Operators.cs │ ├── Matrix4.cs │ ├── Quaternion.cs │ ├── VectorExtensions.cs │ ├── XY.cs │ ├── XYZ.cs │ └── XYZM.cs ├── MeshSharp.csproj ├── Polygon.cs ├── Properties │ └── AssemblyInfo.cs ├── Property.cs ├── PropertyCollection.cs ├── Quad.cs ├── Transform.cs ├── Triangle.cs └── Utils.cs ├── README.md └── clean.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: Missing XML comment for publicly visible type or member 4 | dotnet_diagnostic.CS1591.severity = none 5 | 6 | [*.{cs,vb}] 7 | indent_style=tab -------------------------------------------------------------------------------- /.github/workflows/build_n_test.yml: -------------------------------------------------------------------------------- 1 | name: Build&Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v2 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Test 25 | run: dotnet test --configuration Release --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | .vs 4 | .idea 5 | *.csproj.user 6 | file_samples 7 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ds5678 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Licenses/MeshIO.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Albert Domenech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MeshSharp.Examples/MeshSharp.Examples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | AssetRipper.MeshSharp.Examples 7 | 8 | 9 | 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /MeshSharp.Examples/Program.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.FBX; 3 | using AssetRipper.MeshSharp.OBJ; 4 | using AssetRipper.MeshSharp.PLY; 5 | using AssetRipper.MeshSharp.STL; 6 | using System; 7 | 8 | namespace AssetRipper.MeshSharp.Examples 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | try 15 | { 16 | //StlConversion(); 17 | //StlExample(); 18 | //ObjExample(); 19 | PlyExample(); 20 | } 21 | catch (Exception ex) 22 | { 23 | Console.WriteLine(ex.ToString()); 24 | } 25 | 26 | Console.WriteLine("Program finished"); 27 | Console.ReadLine(); 28 | } 29 | 30 | static void FbxExample() 31 | { 32 | //string pathI = @".\..\..\..\..\file_samples\fbx\objects_ascii_2014-2015.fbx"; 33 | string pathI = @".\..\..\..\..\file_samples\fbx\test_project_arq_acsii.fbx"; 34 | //string pathO = @".\..\..\..\..\file_samples\fbx\objects_ascii_2014-2015_out.fbx"; 35 | string pathO = @".\..\..\..\..\file_samples\fbx\test_project_arq_acsii_out.fbx"; 36 | 37 | Scene scene = FbxReader.Read(pathI, ErrorLevel.Checked); 38 | FbxWriter.WriteAscii(pathO, scene); 39 | } 40 | 41 | static void StlConversion() 42 | { 43 | string pathI = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_binary.stl"; 44 | string pathO = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_out.fbx"; 45 | Scene scene = StlReader.ReadBinary(pathI); 46 | FbxWriter.WriteAscii(pathO, scene); 47 | //for some reason, the mesh gets rotated 90 degrees on the x - axis during this conversion 48 | } 49 | 50 | static void StlExample() 51 | { 52 | string pathI = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_binary.stl"; 53 | string pathO = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_ascii.stl"; 54 | Scene scene = StlReader.ReadBinary(pathI); 55 | StlWriter.WriteAscii(pathO, scene); 56 | //this does not result in rotation 57 | } 58 | 59 | static void ObjExample() 60 | { 61 | string pathI = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_binary.stl"; 62 | string pathO = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_out.obj"; 63 | Scene scene = StlReader.ReadBinary(pathI); 64 | ObjWriter.Write(pathO, scene); 65 | } 66 | 67 | static void PlyExample() 68 | { 69 | string pathI = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_binary.stl"; 70 | string pathO = @".\..\..\..\..\file_samples\stl\dev_binoculars_hudShape_1_out.ply"; 71 | Scene scene = StlReader.ReadBinary(pathI); 72 | PlyWriter.WriteAscii(pathO, scene); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Converters/FbxConverter7400.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | 3 | namespace AssetRipper.MeshSharp.FBX.Converters 4 | { 5 | public class FbxConverter7400 : FbxConverterBase 6 | { 7 | public FbxConverter7400(Scene scene) : base(scene, FbxVersion.v7400) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Converters/IFbxConverter.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX.Converters 2 | { 3 | public interface IFbxConverter 4 | { 5 | FbxVersion Version { get; } 6 | 7 | FbxRootNode ToRootNode(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Converters/INodeParser.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | 3 | namespace AssetRipper.MeshSharp.FBX.Converters 4 | { 5 | public interface INodeParser 6 | { 7 | FbxVersion Version { get; } 8 | 9 | Scene ConvertScene(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Converters/NodeParser7400.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX.Converters 2 | { 3 | /// 4 | /// 5 | /// Class to convert a node structure in the version 6 | /// 7 | public class NodeParser7400 : NodeParserBase 8 | { 9 | public NodeParser7400(FbxRootNode root) : base(root) { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MeshSharp.FBX/DeflateWithChecksum.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | 4 | namespace AssetRipper.MeshSharp.FBX 5 | { 6 | /// 7 | /// A wrapper for DeflateStream that calculates the Adler32 checksum of the payload 8 | /// 9 | public class DeflateWithChecksum : DeflateStream 10 | { 11 | private const int modAdler = 65521; 12 | private uint checksumA; 13 | private uint checksumB; 14 | 15 | /// 16 | /// Gets the Adler32 checksum at the current point in the stream 17 | /// 18 | public int Checksum 19 | { 20 | get 21 | { 22 | checksumA %= modAdler; 23 | checksumB %= modAdler; 24 | return (int)((checksumB << 16) | checksumA); 25 | } 26 | } 27 | 28 | /// 29 | public DeflateWithChecksum(Stream stream, CompressionMode mode) : base(stream, mode) 30 | { 31 | ResetChecksum(); 32 | } 33 | 34 | /// 35 | public DeflateWithChecksum(Stream stream, CompressionMode mode, bool leaveOpen) : base(stream, mode, leaveOpen) 36 | { 37 | ResetChecksum(); 38 | } 39 | 40 | // Efficiently extends the checksum with the given buffer 41 | void CalcChecksum(byte[] array, int offset, int count) 42 | { 43 | checksumA %= modAdler; 44 | checksumB %= modAdler; 45 | for (int i = offset, c = 0; i < (offset + count); i++, c++) 46 | { 47 | checksumA += array[i]; 48 | checksumB += checksumA; 49 | if (c > 4000) // This is about how many iterations it takes for B to reach IntMax 50 | { 51 | checksumA %= modAdler; 52 | checksumB %= modAdler; 53 | c = 0; 54 | } 55 | } 56 | } 57 | 58 | /// 59 | public override void Write(byte[] array, int offset, int count) 60 | { 61 | base.Write(array, offset, count); 62 | CalcChecksum(array, offset, count); 63 | } 64 | 65 | /// 66 | public override int Read(byte[] array, int offset, int count) 67 | { 68 | var ret = base.Read(array, offset, count); 69 | CalcChecksum(array, offset, count); 70 | return ret; 71 | } 72 | 73 | /// 74 | /// Initializes the checksum values 75 | /// 76 | public void ResetChecksum() 77 | { 78 | checksumA = 1; 79 | checksumB = 0; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /MeshSharp.FBX/ErrorLevel.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | /// 4 | /// Indicates when a reader should throw errors 5 | /// 6 | public enum ErrorLevel 7 | { 8 | /// 9 | /// Ignores inconsistencies unless the parser can no longer continue 10 | /// 11 | Permissive = 0, 12 | 13 | /// 14 | /// Checks data integrity, such as checksums and end points 15 | /// 16 | Checked = 1, 17 | 18 | /// 19 | /// Checks everything, including magic bytes 20 | /// 21 | Strict = 2, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Exceptions/FbxConverterException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp.FBX.Exceptions 4 | { 5 | 6 | [Serializable] 7 | public class FbxConverterException : Exception 8 | { 9 | public FbxConverterException() { } 10 | public FbxConverterException(string message) : base(message) { } 11 | public FbxConverterException(string message, Exception inner) : base(message, inner) { } 12 | protected FbxConverterException( 13 | System.Runtime.Serialization.SerializationInfo info, 14 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Exceptions/FbxParserException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AssetRipper.MeshSharp.FBX.Exceptions 5 | { 6 | /// 7 | /// An error with the FBX data input 8 | /// 9 | public class FbxException : Exception 10 | { 11 | /// 12 | /// An error at a binary stream offset 13 | /// 14 | /// 15 | /// 16 | public FbxException(long position, string 17 | message) : base($"{message}, near offset {position}") { } 18 | 19 | /// 20 | /// An error in a text file 21 | /// 22 | /// 23 | /// 24 | /// 25 | public FbxException(int line, int column, string message) : 26 | base($"{message}, near line {line} column {column}") 27 | { } 28 | 29 | /// 30 | /// An error in a node object 31 | /// 32 | /// 33 | /// 34 | /// 35 | public FbxException(Stack nodePath, int propertyID, string message) 36 | : base(message + ", at " + string.Join("/", nodePath.ToArray()) + (propertyID < 0 ? "" : $"[{propertyID}]")) { } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxBinary.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.FBX.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace AssetRipper.MeshSharp.FBX 8 | { 9 | /// 10 | /// Base class for binary stream wrappers 11 | /// 12 | internal abstract class FbxBinary : IDisposable 13 | { 14 | /// 15 | /// The size of the footer code 16 | /// 17 | protected const int footerCodeSize = 16; 18 | 19 | /// 20 | /// The namespace separator in the binary format (remember to reverse the identifiers) 21 | /// 22 | protected const string binarySeparator = "\0\x1"; 23 | 24 | /// 25 | /// The namespace separator in the ASCII format and in object data 26 | /// 27 | protected const string asciiSeparator = "::"; 28 | 29 | const string timePath1 = "FBXHeaderExtension"; 30 | const string timePath2 = "CreationTimeStamp"; 31 | static readonly Stack timePath = new Stack(new[] { timePath1, timePath2 }); 32 | 33 | // Header string, found at the top of all compliant files 34 | private static readonly byte[] headerString = Encoding.ASCII.GetBytes("Kaydara FBX Binary \0\x1a\0"); 35 | 36 | // This data was entirely calculated by me, honest. Turns out it works, fancy that! 37 | private static readonly byte[] sourceId = { 0x58, 0xAB, 0xA9, 0xF0, 0x6C, 0xA2, 0xD8, 0x3F, 0x4D, 0x47, 0x49, 0xA3, 0xB4, 0xB2, 0xE7, 0x3D }; 38 | private static readonly byte[] key = { 0xE2, 0x4F, 0x7B, 0x5F, 0xCD, 0xE4, 0xC8, 0x6D, 0xDB, 0xD8, 0xFB, 0xD7, 0x40, 0x58, 0xC6, 0x78 }; 39 | // This wasn't - it just appears at the end of every compliant file 40 | private static readonly byte[] extension = { 0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B }; 41 | 42 | // Number of null bytes between the footer code and the version 43 | private const int footerZeroes1 = 20; 44 | // Number of null bytes between the footer version and extension code 45 | private const int footerZeroes2 = 120; 46 | 47 | /// 48 | /// Checks if the first part of 'data' matches 'original' 49 | /// 50 | /// 51 | /// 52 | /// true if it does, otherwise false 53 | protected static bool CheckEqual(byte[] data, byte[] original) 54 | { 55 | for (int i = 0; i < original.Length; i++) 56 | if (data[i] != original[i]) 57 | return false; 58 | return true; 59 | } 60 | 61 | /// 62 | /// Writes the FBX header string 63 | /// 64 | /// 65 | protected static void WriteHeader(Stream stream) 66 | { 67 | stream.Write(headerString, 0, headerString.Length); 68 | } 69 | 70 | /// 71 | /// Reads the FBX header string 72 | /// 73 | /// 74 | /// true if it's compliant 75 | public static bool ReadHeader(Stream stream) 76 | { 77 | var buf = new byte[headerString.Length]; 78 | stream.Read(buf, 0, buf.Length); 79 | return CheckEqual(buf, headerString); 80 | } 81 | 82 | public abstract void Dispose(); 83 | 84 | // Turns out this is the algorithm they use to generate the footer. Who knew! 85 | static void Encrypt(byte[] a, byte[] b) 86 | { 87 | byte c = 64; 88 | for (int i = 0; i < footerCodeSize; i++) 89 | { 90 | a[i] = (byte)(a[i] ^ (byte)(c ^ b[i])); 91 | c = a[i]; 92 | } 93 | } 94 | 95 | // Gets a single timestamp component 96 | static int GetTimestampVar(FbxNode timestamp, string element) 97 | { 98 | var elementNode = timestamp[element]; 99 | if (elementNode != null && elementNode.Properties.Count > 0) 100 | { 101 | var prop = elementNode.Properties[0]; 102 | if (prop is int || prop is long) 103 | return (int)prop; 104 | } 105 | throw new FbxException(timePath, -1, "Timestamp has no " + element); 106 | } 107 | 108 | /// 109 | /// Generates the unique footer code based on the document's timestamp 110 | /// 111 | /// 112 | /// A 16-byte code 113 | protected static byte[] GenerateFooterCode(FbxNodeCollection document) 114 | { 115 | var timestamp = document.GetRelative(timePath1 + "/" + timePath2); 116 | if (timestamp == null) 117 | throw new FbxException(timePath, -1, "No creation timestamp"); 118 | try 119 | { 120 | return GenerateFooterCode( 121 | GetTimestampVar(timestamp, "Year"), 122 | GetTimestampVar(timestamp, "Month"), 123 | GetTimestampVar(timestamp, "Day"), 124 | GetTimestampVar(timestamp, "Hour"), 125 | GetTimestampVar(timestamp, "Minute"), 126 | GetTimestampVar(timestamp, "Second"), 127 | GetTimestampVar(timestamp, "Millisecond") 128 | ); 129 | } 130 | catch (ArgumentOutOfRangeException) 131 | { 132 | throw new FbxException(timePath, -1, "Invalid timestamp"); 133 | } 134 | } 135 | 136 | /// 137 | /// Generates a unique footer code based on a timestamp 138 | /// 139 | /// 140 | /// 141 | /// 142 | /// 143 | /// 144 | /// 145 | /// 146 | /// A 16-byte code 147 | protected static byte[] GenerateFooterCode( 148 | int year, int month, int day, 149 | int hour, int minute, int second, int millisecond) 150 | { 151 | if (year < 0 || year > 9999) 152 | throw new ArgumentOutOfRangeException(nameof(year)); 153 | if (month < 0 || month > 12) 154 | throw new ArgumentOutOfRangeException(nameof(month)); 155 | if (day < 0 || day > 31) 156 | throw new ArgumentOutOfRangeException(nameof(day)); 157 | if (hour < 0 || hour >= 24) 158 | throw new ArgumentOutOfRangeException(nameof(hour)); 159 | if (minute < 0 || minute >= 60) 160 | throw new ArgumentOutOfRangeException(nameof(minute)); 161 | if (second < 0 || second >= 60) 162 | throw new ArgumentOutOfRangeException(nameof(second)); 163 | if (millisecond < 0 || millisecond >= 1000) 164 | throw new ArgumentOutOfRangeException(nameof(millisecond)); 165 | 166 | var str = (byte[])sourceId.Clone(); 167 | var mangledTime = $"{second:00}{month:00}{hour:00}{day:00}{(millisecond / 10):00}{year:0000}{minute:00}"; 168 | var mangledBytes = Encoding.ASCII.GetBytes(mangledTime); 169 | Encrypt(str, mangledBytes); 170 | Encrypt(str, key); 171 | Encrypt(str, mangledBytes); 172 | return str; 173 | } 174 | 175 | /// 176 | /// Writes the FBX footer extension (NB - not the unique footer code) 177 | /// 178 | /// 179 | /// 180 | protected void WriteFooter(BinaryWriter stream, int version) 181 | { 182 | var zeroes = new byte[Math.Max(footerZeroes1, footerZeroes2)]; 183 | stream.Write(zeroes, 0, footerZeroes1); 184 | stream.Write(version); 185 | stream.Write(zeroes, 0, footerZeroes2); 186 | stream.Write(extension, 0, extension.Length); 187 | } 188 | 189 | protected static bool AllZero(byte[] array) 190 | { 191 | foreach (var b in array) 192 | if (b != 0) 193 | return false; 194 | return true; 195 | } 196 | 197 | /// 198 | /// Reads and checks the FBX footer extension (NB - not the unique footer code) 199 | /// 200 | /// 201 | /// 202 | /// true if it's compliant 203 | protected bool CheckFooter(BinaryReader stream, FbxVersion version) 204 | { 205 | var buffer = new byte[Math.Max(footerZeroes1, footerZeroes2)]; 206 | stream.Read(buffer, 0, footerZeroes1); 207 | bool correct = AllZero(buffer); 208 | var readVersion = stream.ReadInt32(); 209 | correct &= (readVersion == (int)version); 210 | stream.Read(buffer, 0, footerZeroes2); 211 | correct &= AllZero(buffer); 212 | stream.Read(buffer, 0, extension.Length); 213 | correct &= CheckEqual(buffer, extension); 214 | return correct; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxElementDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | public class FbxPropertyTemplate 4 | { 5 | public string Name { get; set; } 6 | 7 | public PropertyCollection Properties { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.FBX 4 | { 5 | /// 6 | /// Represents a node in an FBX file 7 | /// 8 | public class FbxNode : FbxNodeCollection 9 | { 10 | /// 11 | /// The node name, which is often a class type 12 | /// 13 | /// 14 | /// The name must be smaller than 256 characters to be written to a binary stream 15 | /// 16 | public string Name { get; set; } 17 | 18 | /// 19 | /// Whether the node is empty of data 20 | /// 21 | public bool IsEmpty => string.IsNullOrEmpty(Name) && Properties.Count == 0 && Nodes.Count == 0; 22 | 23 | /// 24 | /// The list of properties associated with the node 25 | /// 26 | /// 27 | /// Supported types are primitives (apart from byte and char),arrays of primitives, and strings 28 | /// 29 | public List Properties { get; } = new List(); 30 | 31 | /// 32 | /// The first property element 33 | /// 34 | public object Value 35 | { 36 | get { return Properties.Count < 1 ? null : Properties[0]; } 37 | set 38 | { 39 | if (Properties.Count < 1) 40 | Properties.Add(value); 41 | else 42 | Properties[0] = value; 43 | } 44 | } 45 | 46 | /// 47 | /// Default constructor 48 | /// 49 | public FbxNode() : base() { } 50 | 51 | public FbxNode(string name) : base() 52 | { 53 | Name = name; 54 | } 55 | 56 | public FbxNode(string name, object value) : this(name) 57 | { 58 | Value = value; 59 | } 60 | public FbxNode(string name, params object[] properties) : this(name) 61 | { 62 | Properties = new List(properties); 63 | } 64 | 65 | public override string ToString() 66 | { 67 | return $"{Name}:{Value}"; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxNodeCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace AssetRipper.MeshSharp.FBX 5 | { 6 | /// 7 | /// Base class for nodes and documents 8 | /// 9 | public abstract class FbxNodeCollection : IEnumerable 10 | { 11 | /// 12 | /// The list of child/nested nodes 13 | /// 14 | /// 15 | /// A list with one or more null elements is treated differently than an empty list, 16 | /// and represented differently in all FBX output files. 17 | /// 18 | public List Nodes { get; } = new List(); 19 | 20 | /// 21 | /// Gets a named child node 22 | /// 23 | /// 24 | /// The child node, or null 25 | public FbxNode this[string name] { get { return Nodes.Find(n => n != null && n.Name == name); } } 26 | 27 | /// 28 | /// Gets a child node, using a '/' separated path 29 | /// 30 | /// 31 | /// The child node, or null 32 | public FbxNode GetRelative(string path) 33 | { 34 | string[] tokens = path.Split('/'); 35 | FbxNodeCollection n = this; 36 | foreach (var t in tokens) 37 | { 38 | if (t == "") 39 | continue; 40 | n = n[t]; 41 | if (n == null) 42 | break; 43 | } 44 | return n as FbxNode; 45 | } 46 | 47 | public IEnumerator GetEnumerator() 48 | { 49 | return Nodes.GetEnumerator(); 50 | } 51 | 52 | IEnumerator IEnumerable.GetEnumerator() 53 | { 54 | return Nodes.GetEnumerator(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxProperty.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | 3 | namespace AssetRipper.MeshSharp.FBX 4 | { 5 | //P : ["PropName", "PropType", "Label(?)", "Flags", __values__, …] 6 | public class FbxProperty : Property 7 | { 8 | public string FbxTypeName { get; set; } 9 | 10 | public string TypeLabel { get; set; } 11 | 12 | public PropertyFlags Flags { get; set; } 13 | 14 | public FbxProperty(string name, Element owner) : base(name, owner) { } 15 | 16 | public FbxProperty(string name, Element owner, object value) : base(name, owner, value) { } 17 | 18 | public FbxProperty(string name, Element owner, object value, string typeName, string typeLabel, PropertyFlags flags) : base(name, owner, value) 19 | { 20 | this.FbxTypeName = typeName; 21 | this.TypeLabel = typeLabel; 22 | this.Flags = flags; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxPropertyFlag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp.FBX 4 | { 5 | [Flags] 6 | public enum PropertyFlags : byte 7 | { 8 | /// 9 | /// No flags 10 | /// 11 | None = 0, 12 | /// 13 | /// Not serializable 14 | /// 15 | /// 16 | /// Fbx value : 'L' 17 | /// 18 | Locked = 1, 19 | /// 20 | /// This is a user defined property 21 | /// 22 | /// 23 | /// Fbx value : 'U' 24 | /// 25 | UserDefined = 2, 26 | /// 27 | /// The property is animatable 28 | /// 29 | /// 30 | /// Fbx value : 'A' 31 | /// 32 | Animatable = 4, 33 | /// 34 | /// The property is animated 35 | /// 36 | /// 37 | /// Fbx value : '+' 38 | /// 39 | Animated = 8, 40 | /// 41 | /// The property is marked as hidden 42 | /// 43 | /// 44 | /// Fbx value : 'H' 45 | /// 46 | Hidden = 16, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxProperty[T].cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | 3 | namespace AssetRipper.MeshSharp.FBX 4 | { 5 | //P : ["PropName", "PropType", "Label(?)", "Flags", __values__, …] 6 | public class FbxProperty : FbxProperty 7 | { 8 | public new T Value { get; set; } 9 | 10 | public FbxProperty(string name, Element owner) : base(name, owner) { } 11 | 12 | public FbxProperty(string name, Element owner, T value) : base(name, owner, value) { } 13 | 14 | public FbxProperty(string name, Element owner, T value, string typeName, string typeLabel, PropertyFlags flags) : base(name, owner, value, typeName, typeLabel, flags) { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxRootNode.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | /// 4 | /// A top-level FBX node 5 | /// 6 | public class FbxRootNode : FbxNodeCollection 7 | { 8 | /// 9 | /// Describes the format and data of the document 10 | /// 11 | /// 12 | /// It isn't recommended that you change this value directly, because 13 | /// it won't change any of the document's data which can be version-specific. 14 | /// Most FBX importers can cope with any version. 15 | /// 16 | public FbxVersion Version { get; set; } = FbxVersion.v7400; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MeshSharp.FBX/FbxVersion.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | /// 4 | /// Enumerates the FBX file versions 5 | /// 6 | public enum FbxVersion 7 | { 8 | v2000 = 2000, 9 | v2001 = 2001, 10 | v3000 = 3000, 11 | v3001 = 3001, 12 | v4000 = 4000, 13 | v4001 = 4001, 14 | v4050 = 4050, 15 | v5000 = 5000, 16 | v5800 = 5800, 17 | /// 18 | /// FBX version 6.0 19 | /// 20 | v6000 = 6000, 21 | /// 22 | /// FBX version 6.1 23 | /// 24 | v6100 = 6100, 25 | /// 26 | /// FBX version 7.0 27 | /// 28 | v7000 = 7000, 29 | /// 30 | /// FBX 2011 version 31 | /// 32 | v7100 = 7100, 33 | /// 34 | /// FBX 2012 version 35 | /// 36 | v7200 = 7200, 37 | /// 38 | /// FBX 2013 version 39 | /// 40 | v7300 = 7300, 41 | /// 42 | /// FBX 2014 version 43 | /// 44 | v7400 = 7400, 45 | /// 46 | /// FBX 2016 version, adds large file (>2GB support), not compatible with older versions 47 | /// 48 | v7500 = 7500, 49 | v7600 = 7600, 50 | v7700 = 7700 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MeshSharp.FBX/MeshSharp.FBX.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | latest 6 | AssetRipper.MeshSharp.FBX 7 | ds5678 8 | AssetRipper 9 | 1.0.0.0 10 | 1.0.0.0 11 | AssetRipper.MeshSharp.FBX 12 | C# 3D fbx 13 | https://github.com/AssetRipper/MeshSharp 14 | https://github.com/AssetRipper/MeshSharp 15 | MIT 16 | git 17 | Copyright (c) 2022 ds5678 18 | MeshSharp module for the FBX format. 19 | true 20 | 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | bin\MeshSharp.FBX.xml 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Notes.md: -------------------------------------------------------------------------------- 1 | # FBX file format 2 | 3 | ## References 4 | 5 | * [Wikipedia entry](https://en.wikipedia.org/wiki/FBX) 6 | * [2013 Analysis by the Blender Foundation](https://code.blender.org/2013/08/fbx-binary-file-format-specification/) 7 | * [File Format entry](https://docs.fileformat.com/3d/fbx/) 8 | * [Archive Team entry](http://fileformats.archiveteam.org/wiki/FBX) -------------------------------------------------------------------------------- /MeshSharp.FBX/Parsers/FbxBinaryParser.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.FBX.Exceptions; 2 | using System; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Text; 6 | 7 | namespace AssetRipper.MeshSharp.FBX 8 | { 9 | /// 10 | /// Reads FBX nodes from a binary stream 11 | /// 12 | internal class FbxBinaryParser : FbxBinary, IFbxParser 13 | { 14 | private delegate object ReadPrimitive(BinaryReader reader); 15 | 16 | private readonly BinaryReader _stream; 17 | 18 | private readonly ErrorLevel _errorLevel; 19 | 20 | /// 21 | /// Creates a new reader 22 | /// 23 | /// The stream to read from 24 | /// When to throw an 25 | /// does 26 | /// not support seeking 27 | public FbxBinaryParser(Stream stream, ErrorLevel errorLevel = ErrorLevel.Checked) 28 | { 29 | if (stream == null) 30 | throw new ArgumentNullException(nameof(stream)); 31 | 32 | if (!stream.CanSeek) 33 | throw new ArgumentException("The stream must support seeking. Try reading the data into a buffer first"); 34 | 35 | this._stream = new BinaryReader(stream, Encoding.ASCII); 36 | this._errorLevel = errorLevel; 37 | } 38 | 39 | /// 40 | /// Reads a single node. 41 | /// 42 | /// 43 | /// This won't read the file header or footer, and as such will fail if the stream is a full FBX file 44 | /// 45 | /// The node 46 | /// The FBX data was malformed 47 | /// for the reader's error level 48 | public FbxNode ReadNode(FbxRootNode document) 49 | { 50 | var endOffset = document.Version >= FbxVersion.v7500 ? _stream.ReadInt64() : _stream.ReadInt32(); 51 | var numProperties = document.Version >= FbxVersion.v7500 ? _stream.ReadInt64() : _stream.ReadInt32(); 52 | var propertyListLen = document.Version >= FbxVersion.v7500 ? _stream.ReadInt64() : _stream.ReadInt32(); 53 | var nameLen = _stream.ReadByte(); 54 | var name = nameLen == 0 ? "" : Encoding.ASCII.GetString(_stream.ReadBytes(nameLen)); 55 | 56 | if (endOffset == 0) 57 | { 58 | // The end offset should only be 0 in a null node 59 | if (_errorLevel >= ErrorLevel.Checked && (numProperties != 0 || propertyListLen != 0 || !string.IsNullOrEmpty(name))) 60 | throw new FbxException(_stream.BaseStream.Position, 61 | "Invalid node; expected NULL record"); 62 | return null; 63 | } 64 | 65 | var node = new FbxNode { Name = name }; 66 | 67 | var propertyEnd = _stream.BaseStream.Position + propertyListLen; 68 | // Read properties 69 | for (int i = 0; i < numProperties; i++) 70 | node.Properties.Add(readProperty()); 71 | 72 | if (_errorLevel >= ErrorLevel.Checked && _stream.BaseStream.Position != propertyEnd) 73 | throw new FbxException(_stream.BaseStream.Position, 74 | "Too many bytes in property list, end point is " + propertyEnd); 75 | 76 | // Read nested nodes 77 | var listLen = endOffset - _stream.BaseStream.Position; 78 | if (_errorLevel >= ErrorLevel.Checked && listLen < 0) 79 | throw new FbxException(_stream.BaseStream.Position, 80 | "Node has invalid end point"); 81 | if (listLen > 0) 82 | { 83 | FbxNode nested; 84 | do 85 | { 86 | nested = ReadNode(document); 87 | node.Nodes.Add(nested); 88 | } while (nested != null); 89 | if (_errorLevel >= ErrorLevel.Checked && _stream.BaseStream.Position != endOffset) 90 | throw new FbxException(_stream.BaseStream.Position, 91 | "Too many bytes in node, end point is " + endOffset); 92 | } 93 | return node; 94 | } 95 | 96 | /// 97 | /// Reads an FBX document from the stream 98 | /// 99 | /// The top-level node 100 | /// The FBX data was malformed 101 | /// for the reader's error level 102 | public FbxRootNode Parse() 103 | { 104 | _stream.BaseStream.Position = 0; 105 | 106 | // Read header 107 | bool validHeader = ReadHeader(_stream.BaseStream); 108 | if (_errorLevel >= ErrorLevel.Strict && !validHeader) 109 | throw new FbxException(_stream.BaseStream.Position, 110 | "Invalid header string"); 111 | var document = new FbxRootNode { Version = (FbxVersion)_stream.ReadInt32() }; 112 | 113 | // Read nodes 114 | var dataPos = _stream.BaseStream.Position; 115 | FbxNode nested; 116 | do 117 | { 118 | nested = ReadNode(document); 119 | if (nested != null) 120 | document.Nodes.Add(nested); 121 | } while (nested != null); 122 | 123 | // Read footer code 124 | var footerCode = new byte[footerCodeSize]; 125 | _stream.BaseStream.Read(footerCode, 0, footerCode.Length); 126 | if (_errorLevel >= ErrorLevel.Strict) 127 | { 128 | var validCode = GenerateFooterCode(document); 129 | if (!CheckEqual(footerCode, validCode)) 130 | throw new FbxException(_stream.BaseStream.Position - footerCodeSize, 131 | "Incorrect footer code"); 132 | } 133 | 134 | // Read footer extension 135 | dataPos = _stream.BaseStream.Position; 136 | var validFooterExtension = CheckFooter(_stream, document.Version); 137 | if (_errorLevel >= ErrorLevel.Strict && !validFooterExtension) 138 | throw new FbxException(dataPos, "Invalid footer"); 139 | return document; 140 | } 141 | 142 | /// 143 | public override void Dispose() 144 | { 145 | this._stream.Dispose(); 146 | } 147 | 148 | // Reads a single property 149 | private object readProperty() 150 | { 151 | var dataType = (char)_stream.ReadByte(); 152 | switch (dataType) 153 | { 154 | case 'Y': 155 | return _stream.ReadInt16(); 156 | case 'C': 157 | return (char)_stream.ReadByte(); 158 | case 'I': 159 | return _stream.ReadInt32(); 160 | case 'F': 161 | return _stream.ReadSingle(); 162 | case 'D': 163 | return _stream.ReadDouble(); 164 | case 'L': 165 | return _stream.ReadInt64(); 166 | case 'f': 167 | return readArray(br => br.ReadSingle(), typeof(float)); 168 | case 'd': 169 | return readArray(br => br.ReadDouble(), typeof(double)); 170 | case 'l': 171 | return readArray(br => br.ReadInt64(), typeof(long)); 172 | case 'i': 173 | return readArray(br => br.ReadInt32(), typeof(int)); 174 | case 'b': 175 | return readArray(br => br.ReadBoolean(), typeof(bool)); 176 | case 'S': 177 | var len = _stream.ReadInt32(); 178 | var str = len == 0 ? "" : Encoding.ASCII.GetString(_stream.ReadBytes(len)); 179 | // Convert \0\1 to '::' and reverse the tokens 180 | if (str.Contains(binarySeparator)) 181 | { 182 | var tokens = str.Split(new[] { binarySeparator }, StringSplitOptions.None); 183 | var sb = new StringBuilder(); 184 | bool first = true; 185 | for (int i = tokens.Length - 1; i >= 0; i--) 186 | { 187 | if (!first) 188 | sb.Append(asciiSeparator); 189 | sb.Append(tokens[i]); 190 | first = false; 191 | } 192 | str = sb.ToString(); 193 | } 194 | return str; 195 | case 'R': 196 | return _stream.ReadBytes(_stream.ReadInt32()); 197 | default: 198 | throw new FbxException(_stream.BaseStream.Position - 1, 199 | "Invalid property data type `" + dataType + "'"); 200 | } 201 | } 202 | 203 | // Reads an array, decompressing it if required 204 | private Array readArray(ReadPrimitive readPrimitive, Type arrayType) 205 | { 206 | var len = _stream.ReadInt32(); 207 | var encoding = _stream.ReadInt32(); 208 | var compressedLen = _stream.ReadInt32(); 209 | var ret = Array.CreateInstance(arrayType, len); 210 | var s = _stream; 211 | var endPos = _stream.BaseStream.Position + compressedLen; 212 | if (encoding != 0) 213 | { 214 | if (_errorLevel >= ErrorLevel.Checked) 215 | { 216 | if (encoding != 1) 217 | throw new FbxException(_stream.BaseStream.Position - 1, 218 | "Invalid compression encoding (must be 0 or 1)"); 219 | var cmf = _stream.ReadByte(); 220 | if ((cmf & 0xF) != 8 || (cmf >> 4) > 7) 221 | throw new FbxException(_stream.BaseStream.Position - 1, 222 | "Invalid compression format " + cmf); 223 | var flg = _stream.ReadByte(); 224 | if (_errorLevel >= ErrorLevel.Strict && ((cmf << 8) + flg) % 31 != 0) 225 | throw new FbxException(_stream.BaseStream.Position - 1, 226 | "Invalid compression FCHECK"); 227 | if ((flg & (1 << 5)) != 0) 228 | throw new FbxException(_stream.BaseStream.Position - 1, 229 | "Invalid compression flags; dictionary not supported"); 230 | } 231 | else 232 | { 233 | _stream.BaseStream.Position += 2; 234 | } 235 | var codec = new DeflateWithChecksum(_stream.BaseStream, CompressionMode.Decompress); 236 | s = new BinaryReader(codec); 237 | } 238 | try 239 | { 240 | for (int i = 0; i < len; i++) 241 | ret.SetValue(readPrimitive(s), i); 242 | } 243 | catch (InvalidDataException) 244 | { 245 | throw new FbxException(_stream.BaseStream.Position - 1, 246 | "Compressed data was malformed"); 247 | } 248 | if (encoding != 0) 249 | { 250 | if (_errorLevel >= ErrorLevel.Checked) 251 | { 252 | _stream.BaseStream.Position = endPos - sizeof(int); 253 | var checksumBytes = new byte[sizeof(int)]; 254 | _stream.BaseStream.Read(checksumBytes, 0, checksumBytes.Length); 255 | int checksum = 0; 256 | for (int i = 0; i < checksumBytes.Length; i++) 257 | checksum = (checksum << 8) + checksumBytes[i]; 258 | if (checksum != ((DeflateWithChecksum)s.BaseStream).Checksum) 259 | throw new FbxException(_stream.BaseStream.Position, 260 | "Compressed data has invalid checksum"); 261 | } 262 | else 263 | { 264 | _stream.BaseStream.Position = endPos; 265 | } 266 | } 267 | return ret; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Parsers/IFbxParser.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | internal interface IFbxParser 4 | { 5 | FbxRootNode Parse(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Readers/FbxReader.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.FBX.Converters; 3 | using System; 4 | using System.IO; 5 | 6 | namespace AssetRipper.MeshSharp.FBX 7 | { 8 | public class FbxReader : IFbxReader 9 | { 10 | private Stream _stream; 11 | private ErrorLevel _errorLevel; 12 | 13 | public FbxReader(string path, ErrorLevel errorLevel) 14 | { 15 | if (string.IsNullOrEmpty(path)) 16 | throw new ArgumentNullException(nameof(path)); 17 | 18 | _stream = new FileStream(path, FileMode.Open); 19 | _errorLevel = errorLevel; 20 | } 21 | 22 | public FbxReader(Stream stream, ErrorLevel errorLevel) 23 | { 24 | if (stream == null) 25 | throw new ArgumentNullException(nameof(stream)); 26 | 27 | if (!stream.CanSeek) 28 | throw new ArgumentException("The stream must support seeking. Try reading the data into a buffer first"); 29 | 30 | _stream = stream; 31 | _errorLevel = errorLevel; 32 | } 33 | 34 | /// 35 | /// Read the file into an fbx scene. 36 | /// 37 | /// 38 | public Scene Read() 39 | { 40 | FbxRootNode root = this.Parse(); 41 | INodeParser converter = NodeParserBase.GetConverter(root); 42 | 43 | return converter.ConvertScene(); 44 | } 45 | 46 | /// 47 | /// Parse the document into a node structure. 48 | /// 49 | /// 50 | public FbxRootNode Parse() 51 | { 52 | IFbxParser parser = null; 53 | 54 | if (FbxBinary.ReadHeader(_stream)) 55 | { 56 | parser = new FbxBinaryParser(_stream, _errorLevel); 57 | } 58 | else 59 | { 60 | parser = new FbxAsciiParser(_stream, _errorLevel); 61 | } 62 | 63 | return parser.Parse(); 64 | } 65 | 66 | /// 67 | /// Read fbx file. 68 | /// 69 | /// 70 | /// 71 | public static Scene Read(string path, ErrorLevel errorLevel) 72 | { 73 | using (FbxReader reader = new FbxReader(path, errorLevel)) 74 | { 75 | return reader.Read(); 76 | } 77 | } 78 | 79 | /// 80 | /// Parse the document into a node structure. 81 | /// 82 | /// 83 | public static FbxRootNode Parse(string path, ErrorLevel errorLevel) 84 | { 85 | using (FbxReader reader = new FbxReader(path, errorLevel)) 86 | { 87 | return reader.Parse(); 88 | } 89 | } 90 | 91 | /// 92 | public void Dispose() 93 | { 94 | _stream.Dispose(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Readers/IFbxReader.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using System; 3 | 4 | namespace AssetRipper.MeshSharp.FBX 5 | { 6 | public interface IFbxReader : IDisposable 7 | { 8 | FbxRootNode Parse(); 9 | Scene Read(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Writers/FbxAsciiWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.FBX.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace AssetRipper.MeshSharp.FBX 8 | { 9 | /// 10 | /// Writes an FBX document in a text format 11 | /// 12 | internal class FbxAsciiWriter : IDisposable 13 | { 14 | /// 15 | /// The maximum line length in characters when outputting arrays 16 | /// 17 | /// 18 | /// Lines might end up being a few characters longer than this, visibly and otherwise, 19 | /// so don't rely on it as a hard limit in code! 20 | /// 21 | public int MaxLineLength { get; set; } = 260; 22 | 23 | /// 24 | /// If this is active the max line length will not be applied 25 | /// 26 | public bool ApplyLineMaxLength { get; set; } 27 | 28 | private readonly Stack _nodePath = new Stack(); 29 | private readonly Stream _stream; 30 | 31 | /// 32 | /// Creates a new reader 33 | /// 34 | /// 35 | public FbxAsciiWriter(Stream stream) 36 | { 37 | if (stream == null) 38 | throw new ArgumentNullException(nameof(stream)); 39 | this._stream = stream; 40 | } 41 | 42 | /// 43 | /// Writes an FBX document to the stream 44 | /// 45 | /// 46 | /// 47 | /// ASCII FBX files have no header or footer, so you can call this multiple times 48 | /// 49 | public void Write(FbxRootNode document) 50 | { 51 | if (document == null) 52 | throw new ArgumentNullException(nameof(document)); 53 | var sb = new StringBuilder(); 54 | 55 | // Write version header (a comment, but required for many importers) 56 | var vMajor = (int)document.Version / 1000; 57 | var vMinor = ((int)document.Version % 1000) / 100; 58 | var vRev = ((int)document.Version % 100) / 10; 59 | sb.Append($"; FBX {vMajor}.{vMinor}.{vRev} project file\n\n"); 60 | 61 | _nodePath.Clear(); 62 | foreach (var n in document.Nodes) 63 | { 64 | if (n == null) 65 | continue; 66 | buildString(n, sb, document.Version >= FbxVersion.v7100); 67 | sb.Append('\n'); 68 | } 69 | var b = Encoding.ASCII.GetBytes(sb.ToString()); 70 | _stream.Write(b, 0, b.Length); 71 | } 72 | 73 | /// 74 | public void Dispose() 75 | { 76 | _stream.Dispose(); 77 | } 78 | 79 | // Adds the given node text to the string 80 | private void buildString(FbxNode node, StringBuilder sb, bool writeArrayLength, int indentLevel = 0) 81 | { 82 | _nodePath.Push(node.Name ?? ""); 83 | int lineStart = sb.Length; 84 | // Write identifier 85 | for (int i = 0; i < indentLevel; i++) 86 | sb.Append('\t'); 87 | sb.Append(node.Name).Append(':'); 88 | 89 | // Write properties 90 | var first = true; 91 | for (int j = 0; j < node.Properties.Count; j++) 92 | { 93 | var p = node.Properties[j]; 94 | if (p == null) 95 | continue; 96 | if (!first) 97 | sb.Append(','); 98 | sb.Append(' '); 99 | if (p is string) 100 | { 101 | sb.Append('"').Append(p).Append('"'); 102 | } 103 | else if (p is Array) 104 | { 105 | var array = (Array)p; 106 | var elementType = p.GetType().GetElementType(); 107 | // ReSharper disable once PossibleNullReferenceException 108 | // We know it's an array, so we don't need to check for null 109 | if (array.Rank != 1 || !elementType.IsPrimitive) 110 | throw new FbxException(_nodePath, j, 111 | "Invalid array type " + p.GetType()); 112 | if (writeArrayLength) 113 | { 114 | sb.Append('*').Append(array.Length).Append(" {\n"); 115 | lineStart = sb.Length; 116 | for (int i = -1; i < indentLevel; i++) 117 | sb.Append('\t'); 118 | sb.Append("a: "); 119 | } 120 | bool pFirst = true; 121 | foreach (var v in (Array)p) 122 | { 123 | if (!pFirst) 124 | sb.Append(','); 125 | var vstr = v.ToString(); 126 | 127 | if (this.ApplyLineMaxLength) 128 | if ((sb.Length - lineStart) + vstr.Length >= MaxLineLength) 129 | { 130 | sb.Append('\n'); 131 | lineStart = sb.Length; 132 | } 133 | 134 | sb.Append(vstr); 135 | pFirst = false; 136 | } 137 | if (writeArrayLength) 138 | { 139 | sb.Append('\n'); 140 | for (int i = 0; i < indentLevel; i++) 141 | sb.Append('\t'); 142 | sb.Append('}'); 143 | } 144 | } 145 | else if (p is char) 146 | sb.Append((char)p); 147 | else if (p.GetType().IsPrimitive && p is IFormattable) 148 | sb.Append(p); 149 | else 150 | throw new FbxException(_nodePath, j, 151 | "Invalid property type " + p.GetType()); 152 | first = false; 153 | } 154 | 155 | // Write child nodes 156 | if (node.Nodes.Count > 0) 157 | { 158 | sb.Append(" {\n"); 159 | foreach (var n in node.Nodes) 160 | { 161 | if (n == null) 162 | continue; 163 | buildString(n, sb, writeArrayLength, indentLevel + 1); 164 | } 165 | for (int i = 0; i < indentLevel; i++) 166 | sb.Append('\t'); 167 | sb.Append('}'); 168 | } 169 | sb.Append('\n'); 170 | 171 | _nodePath.Pop(); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Writers/FbxBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.FBX.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | 9 | namespace AssetRipper.MeshSharp.FBX 10 | { 11 | /// 12 | /// Writes an FBX document to a binary stream 13 | /// 14 | internal class FbxBinaryWriter : FbxBinary 15 | { 16 | private readonly Stream output; 17 | private readonly MemoryStream memory; 18 | private readonly BinaryWriter stream; 19 | 20 | readonly Stack nodePath = new Stack(); 21 | 22 | /// 23 | /// The minimum size of an array in bytes before it is compressed 24 | /// 25 | public int CompressionThreshold { get; set; } = 1024; 26 | 27 | /// 28 | /// Creates a new writer 29 | /// 30 | /// 31 | public FbxBinaryWriter(Stream stream) 32 | { 33 | if (stream == null) 34 | throw new ArgumentNullException(nameof(stream)); 35 | output = stream; 36 | // Wrap in a memory stream to guarantee seeking 37 | memory = new MemoryStream(); 38 | this.stream = new BinaryWriter(memory, Encoding.ASCII); 39 | } 40 | 41 | private delegate void PropertyWriter(BinaryWriter sw, object obj); 42 | 43 | struct WriterInfo 44 | { 45 | public readonly char id; 46 | public readonly PropertyWriter writer; 47 | 48 | public WriterInfo(char id, PropertyWriter writer) 49 | { 50 | this.id = id; 51 | this.writer = writer; 52 | } 53 | } 54 | 55 | private static readonly Dictionary writePropertyActions 56 | = new Dictionary 57 | { 58 | { typeof(int), new WriterInfo('I', (sw, obj) => sw.Write((int)obj)) }, 59 | { typeof(short), new WriterInfo('Y', (sw, obj) => sw.Write((short)obj)) }, 60 | { typeof(long), new WriterInfo('L', (sw, obj) => sw.Write((long)obj)) }, 61 | 62 | #warning HACK: ulong not supported. Assume smaller than long.MaxValue 63 | { typeof(ulong), new WriterInfo('L', (sw, obj) => sw.Write((long)(ulong)obj)) }, 64 | 65 | { typeof(float), new WriterInfo('F', (sw, obj) => sw.Write((float)obj)) }, 66 | { typeof(double), new WriterInfo('D', (sw, obj) => sw.Write((double)obj)) }, 67 | { typeof(char), new WriterInfo('C', (sw, obj) => sw.Write((byte)(char)obj)) }, 68 | { typeof(byte[]), new WriterInfo('R', WriteRaw) }, 69 | { typeof(string), new WriterInfo('S', WriteString) }, 70 | // null elements indicate arrays - they are checked again with their element type 71 | { typeof(int[]), new WriterInfo('i', null) }, 72 | { typeof(long[]), new WriterInfo('l', null) }, 73 | { typeof(float[]), new WriterInfo('f', null) }, 74 | { typeof(double[]), new WriterInfo('d', null) }, 75 | { typeof(bool[]), new WriterInfo('b', null) }, 76 | }; 77 | 78 | static void WriteRaw(BinaryWriter stream, object obj) 79 | { 80 | var bytes = (byte[])obj; 81 | stream.Write(bytes.Length); 82 | stream.Write(bytes); 83 | } 84 | 85 | static void WriteString(BinaryWriter stream, object obj) 86 | { 87 | var str = obj.ToString(); 88 | // Replace "::" with \0\1 and reverse the tokens 89 | if (str.Contains(asciiSeparator)) 90 | { 91 | var tokens = str.Split(new[] { asciiSeparator }, StringSplitOptions.None); 92 | var sb = new StringBuilder(); 93 | bool first = true; 94 | for (int i = tokens.Length - 1; i >= 0; i--) 95 | { 96 | if (!first) 97 | sb.Append(binarySeparator); 98 | sb.Append(tokens[i]); 99 | first = false; 100 | } 101 | str = sb.ToString(); 102 | } 103 | var bytes = Encoding.ASCII.GetBytes(str); 104 | stream.Write(bytes.Length); 105 | stream.Write(bytes); 106 | } 107 | 108 | void WriteArray(Array array, Type elementType, PropertyWriter writer) 109 | { 110 | stream.Write(array.Length); 111 | 112 | int size = array.Length * Marshal.SizeOf(elementType); 113 | bool compress = size >= CompressionThreshold; 114 | stream.Write(compress ? 1 : 0); 115 | 116 | BinaryWriter sw = stream; 117 | DeflateWithChecksum codec = null; 118 | 119 | long compressLengthPos = stream.BaseStream.Position; 120 | stream.Write(size); // Placeholder compressed length 121 | var dataStart = stream.BaseStream.Position; 122 | if (compress) 123 | { 124 | stream.Write(new byte[] { 0x58, 0x85 }, 0, 2); // Header bytes for DeflateStream settings 125 | codec = new DeflateWithChecksum(stream.BaseStream, CompressionMode.Compress, true); 126 | sw = new BinaryWriter(codec); 127 | } 128 | foreach (var obj in array) 129 | writer(sw, obj); 130 | if (compress) 131 | { 132 | codec.Close(); // This is important - otherwise bytes can be incorrect 133 | var checksum = codec.Checksum; 134 | byte[] bytes = 135 | { 136 | (byte)((checksum >> 24) & 0xFF), 137 | (byte)((checksum >> 16) & 0xFF), 138 | (byte)((checksum >> 8) & 0xFF), 139 | (byte)(checksum & 0xFF), 140 | }; 141 | stream.Write(bytes); 142 | } 143 | 144 | // Now we can write the compressed data length, since we know the size 145 | if (compress) 146 | { 147 | var dataEnd = stream.BaseStream.Position; 148 | stream.BaseStream.Position = compressLengthPos; 149 | stream.Write((int)(dataEnd - dataStart)); 150 | stream.BaseStream.Position = dataEnd; 151 | } 152 | } 153 | 154 | void WriteProperty(object obj, int id) 155 | { 156 | if (obj == null) 157 | return; 158 | WriterInfo writerInfo; 159 | if (!writePropertyActions.TryGetValue(obj.GetType(), out writerInfo)) 160 | throw new FbxException(nodePath, id, 161 | "Invalid property type " + obj.GetType()); 162 | stream.Write((byte)writerInfo.id); 163 | // ReSharper disable once AssignNullToNotNullAttribute 164 | if (writerInfo.writer == null) // Array type 165 | { 166 | var elementType = obj.GetType().GetElementType(); 167 | WriteArray((Array)obj, elementType, writePropertyActions[elementType].writer); 168 | } 169 | else 170 | writerInfo.writer(stream, obj); 171 | } 172 | 173 | // Data for a null node 174 | static readonly byte[] nullData = new byte[13]; 175 | static readonly byte[] nullData7500 = new byte[25]; 176 | 177 | // Writes a single document to the buffer 178 | void WriteNode(FbxRootNode document, FbxNode node) 179 | { 180 | if (node == null) 181 | { 182 | var data = document.Version >= FbxVersion.v7500 ? nullData7500 : nullData; 183 | stream.BaseStream.Write(data, 0, data.Length); 184 | } 185 | else 186 | { 187 | nodePath.Push(node.Name ?? ""); 188 | byte[] name = string.IsNullOrEmpty(node.Name) ? null : Encoding.ASCII.GetBytes(node.Name); 189 | if (name != null && name.Length > byte.MaxValue) 190 | throw new FbxException(stream.BaseStream.Position, 191 | "Node name is too long"); 192 | 193 | // Header 194 | long endOffsetPos = stream.BaseStream.Position; 195 | long propertyLengthPos; 196 | if (document.Version >= FbxVersion.v7500) 197 | { 198 | stream.Write((long)0); // End offset placeholder 199 | stream.Write((long)node.Properties.Count); 200 | propertyLengthPos = stream.BaseStream.Position; 201 | stream.Write((long)0); // Property length placeholder 202 | } 203 | else 204 | { 205 | stream.Write(0); // End offset placeholder 206 | stream.Write(node.Properties.Count); 207 | propertyLengthPos = stream.BaseStream.Position; 208 | stream.Write(0); // Property length placeholder 209 | } 210 | 211 | stream.Write((byte)(name?.Length ?? 0)); 212 | if (name != null) 213 | stream.Write(name); 214 | 215 | // Write properties and length 216 | long propertyBegin = stream.BaseStream.Position; 217 | for (int i = 0; i < node.Properties.Count; i++) 218 | { 219 | WriteProperty(node.Properties[i], i); 220 | } 221 | long propertyEnd = stream.BaseStream.Position; 222 | stream.BaseStream.Position = propertyLengthPos; 223 | if (document.Version >= FbxVersion.v7500) 224 | stream.Write(propertyEnd - propertyBegin); 225 | else 226 | stream.Write((int)(propertyEnd - propertyBegin)); 227 | stream.BaseStream.Position = propertyEnd; 228 | 229 | // Write child nodes 230 | if (node.Nodes.Count > 0) 231 | { 232 | foreach (FbxNode n in node.Nodes) 233 | { 234 | if (n == null) 235 | continue; 236 | WriteNode(document, n); 237 | } 238 | WriteNode(document, null); 239 | } 240 | 241 | // Write end offset 242 | long dataEnd = stream.BaseStream.Position; 243 | stream.BaseStream.Position = endOffsetPos; 244 | if (document.Version >= FbxVersion.v7500) 245 | stream.Write(dataEnd); 246 | else 247 | stream.Write((int)dataEnd); 248 | stream.BaseStream.Position = dataEnd; 249 | 250 | nodePath.Pop(); 251 | } 252 | } 253 | 254 | /// 255 | /// Writes an FBX file to the output 256 | /// 257 | /// 258 | public void Write(FbxRootNode document) 259 | { 260 | stream.BaseStream.Position = 0; 261 | WriteHeader(stream.BaseStream); 262 | stream.Write((int)document.Version); 263 | // TODO: Do we write a top level node or not? Maybe check the version? 264 | nodePath.Clear(); 265 | foreach (FbxNode node in document.Nodes) 266 | WriteNode(document, node); 267 | WriteNode(document, null); 268 | stream.Write(GenerateFooterCode(document)); 269 | WriteFooter(stream, (int)document.Version); 270 | output.Write(memory.GetBuffer(), 0, (int)memory.Position); 271 | } 272 | 273 | /// 274 | public override void Dispose() 275 | { 276 | this.stream.Dispose(); 277 | this.memory.Dispose(); 278 | this.output.Dispose(); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Writers/FbxWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.FBX.Converters; 3 | using System; 4 | using System.IO; 5 | 6 | namespace AssetRipper.MeshSharp.FBX 7 | { 8 | public class FbxWriter : IFbxWriter, IDisposable 9 | { 10 | private Stream writeStream; 11 | private bool disposedValue; 12 | 13 | FbxVersion Version { get; set; } 14 | public Scene Scene { get; set; } 15 | 16 | #region Constructors 17 | public FbxWriter(string path, Scene scene, FbxVersion version = FbxVersion.v7400) 18 | { 19 | if (string.IsNullOrEmpty(path)) 20 | throw new ArgumentNullException(nameof(path)); 21 | 22 | writeStream = File.Create(path); 23 | Scene = scene; 24 | Version = version; 25 | } 26 | 27 | public FbxWriter(Stream stream, Scene scene, FbxVersion version = FbxVersion.v7400) 28 | { 29 | if (stream == null) 30 | throw new ArgumentNullException(nameof(stream)); 31 | 32 | writeStream = stream; 33 | Scene = scene; 34 | Version = version; 35 | } 36 | #endregion 37 | 38 | #region Instance Methods 39 | public FbxRootNode GetRootNode() 40 | { 41 | IFbxConverter converter = FbxConverterBase.GetConverter(Scene, Version); 42 | return converter.ToRootNode(); 43 | } 44 | 45 | public FbxRootNode GetRootNode(FbxVersion version) 46 | { 47 | IFbxConverter converter = FbxConverterBase.GetConverter(Scene, version); 48 | return converter.ToRootNode(); 49 | } 50 | 51 | public void WriteAscii() 52 | { 53 | using (FbxAsciiWriter writer = new FbxAsciiWriter(writeStream)) 54 | writer.Write(GetRootNode()); 55 | } 56 | 57 | public void WriteBinary() 58 | { 59 | using (FbxBinaryWriter writer = new FbxBinaryWriter(writeStream)) 60 | writer.Write(GetRootNode()); 61 | } 62 | #endregion 63 | 64 | #region Static Methods 65 | public static void WriteAscii(string path, Scene scene, FbxVersion version = FbxVersion.v7400) 66 | { 67 | new FbxWriter(path, scene, version).WriteAscii(); 68 | } 69 | public static void WriteAscii(Stream stream, Scene scene, FbxVersion version = FbxVersion.v7400) 70 | { 71 | new FbxWriter(stream, scene, version).WriteAscii(); 72 | } 73 | 74 | public static void WriteAscii(string path, FbxRootNode root) 75 | { 76 | if (path == null) 77 | throw new ArgumentNullException(nameof(path)); 78 | 79 | using (FileStream stream = new FileStream(path, FileMode.Create)) 80 | WriteAscii(stream, root); 81 | } 82 | public static void WriteAscii(Stream stream, FbxRootNode root) 83 | { 84 | using (FbxAsciiWriter writer = new FbxAsciiWriter(stream)) 85 | writer.Write(root); 86 | } 87 | 88 | public static void WriteBinary(string path, Scene scene, FbxVersion version = FbxVersion.v7400) 89 | { 90 | new FbxWriter(path, scene, version).WriteBinary(); 91 | } 92 | public static void WriteBinary(Stream stream, Scene scene, FbxVersion version = FbxVersion.v7400) 93 | { 94 | new FbxWriter(stream, scene, version).WriteBinary(); 95 | } 96 | 97 | public static void WriteBinary(string path, FbxRootNode root) 98 | { 99 | if (path == null) 100 | throw new ArgumentNullException(nameof(path)); 101 | 102 | using (FileStream stream = new FileStream(path, FileMode.Create)) 103 | WriteBinary(stream, root); 104 | } 105 | public static void WriteBinary(Stream stream, FbxRootNode root) 106 | { 107 | using (FbxBinaryWriter writer = new FbxBinaryWriter(stream)) 108 | writer.Write(root); 109 | } 110 | #endregion 111 | 112 | #region Disposal 113 | protected virtual void Dispose(bool disposing) 114 | { 115 | if (!disposedValue) 116 | { 117 | if (disposing) 118 | { 119 | writeStream.Dispose(); 120 | } 121 | 122 | // TODO: free unmanaged resources (unmanaged objects) and override finalizer 123 | writeStream = null; 124 | disposedValue = true; 125 | } 126 | } 127 | 128 | // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources 129 | // ~FbxWriter() 130 | // { 131 | // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 132 | // Dispose(disposing: false); 133 | // } 134 | 135 | public void Dispose() 136 | { 137 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 138 | Dispose(disposing: true); 139 | GC.SuppressFinalize(this); 140 | } 141 | #endregion 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Writers/FbxWriterOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace AssetRipper.MeshSharp.FBX.Writers 5 | { 6 | public class FbxWriterOptions 7 | { 8 | public GlobalSettings GlobalSettings { get; set; } 9 | 10 | public FbxWriterOptions(FbxVersion version) 11 | { 12 | 13 | } 14 | } 15 | 16 | public class GlobalSettings 17 | { 18 | public int Version { get; } 19 | 20 | public int UpAxis 21 | { 22 | get 23 | { 24 | return getPropertyValue(); 25 | } 26 | set 27 | { 28 | setPropertyValue(value); 29 | } 30 | } 31 | 32 | private PropertyCollection _properties = new PropertyCollection(null); 33 | 34 | public GlobalSettings(FbxVersion version) 35 | { 36 | switch (version) 37 | { 38 | case FbxVersion.v2000: 39 | case FbxVersion.v2001: 40 | case FbxVersion.v3000: 41 | case FbxVersion.v3001: 42 | case FbxVersion.v4000: 43 | case FbxVersion.v4001: 44 | case FbxVersion.v4050: 45 | case FbxVersion.v5000: 46 | case FbxVersion.v5800: 47 | case FbxVersion.v6000: 48 | case FbxVersion.v6100: 49 | case FbxVersion.v7000: 50 | case FbxVersion.v7100: 51 | case FbxVersion.v7200: 52 | case FbxVersion.v7300: 53 | case FbxVersion.v7400: 54 | case FbxVersion.v7500: 55 | case FbxVersion.v7600: 56 | case FbxVersion.v7700: 57 | Version = 1000; 58 | break; 59 | default: 60 | break; 61 | } 62 | 63 | _properties.Add(new FbxProperty("UpAxis", null, 1, "int", "Integer", PropertyFlags.None)); 64 | _properties.Add(new FbxProperty("UpAxisSign", null, 1, "int", "Integer", PropertyFlags.None)); 65 | _properties.Add(new FbxProperty("FrontAxis", null, 2, "int", "Integer", PropertyFlags.None)); 66 | _properties.Add(new FbxProperty("FrontAxisSign", null, 1, "int", "Integer", PropertyFlags.None)); 67 | _properties.Add(new FbxProperty("CoordAxis", null, 0, "int", "Integer", PropertyFlags.None)); 68 | _properties.Add(new FbxProperty("CoordAxisSign", null, 1, "int", "Integer", PropertyFlags.None)); 69 | _properties.Add(new FbxProperty("OriginalUpAxis", null, 2, "int", "Integer", PropertyFlags.None)); 70 | _properties.Add(new FbxProperty("OriginalUpAxisSign", null, 1, "int", "Integer", PropertyFlags.None)); 71 | _properties.Add(new FbxProperty("UnitScaleFactor", null, 100000, "double", "Number", PropertyFlags.None)); 72 | _properties.Add(new FbxProperty("OriginalUnitScaleFactor", null, 100, "double", "Number", PropertyFlags.None)); 73 | _properties.Add(new FbxProperty("AmbientColor", null, new Color(), "ColorRGB", "Color", PropertyFlags.None)); 74 | _properties.Add(new FbxProperty("DefaultCamera", null, "Producer", "KString", "", PropertyFlags.None)); 75 | //_properties.Add(new FbxProperty<>("TimeMode", null, "enum", "", "", 6)); 76 | //_properties.Add(new FbxProperty<>("TimeProtocol", null, "enum", "", "", 2)); 77 | //_properties.Add(new FbxProperty<>("SnapOnFrameMode", null, "enum", "", "", 0)); 78 | //_properties.Add(new FbxProperty<>("TimeSpanStart", null, "KTime", "Time", "", 0)); 79 | //_properties.Add(new FbxProperty<>("TimeSpanStop", null, "KTime", "Time", "", 153953860)); 80 | _properties.Add(new FbxProperty("CustomFrameRate", null, -1, "double", "Number", PropertyFlags.None)); 81 | //_properties.Add(new FbxProperty<>("TimeMarker", null, "Compound", "", "")); 82 | _properties.Add(new FbxProperty("CurrentTimeMarker", null, -1, "int", "Integer", PropertyFlags.None)); 83 | } 84 | 85 | [Obsolete] 86 | public FbxNode ToNode() 87 | { 88 | FbxNode node = new FbxNode("GlobalSettings"); 89 | 90 | node.Nodes.Add(new FbxNode("Version", Version)); 91 | 92 | FbxNode properties = new FbxNode("Properties70"); 93 | 94 | node.Nodes.Add(properties); 95 | 96 | properties.Nodes.Add(new FbxNode("P", "UpAxis", "int", "Integer", "", 1)); 97 | properties.Nodes.Add(new FbxNode("P", "UpAxisSign", "int", "Integer", "", 1)); 98 | properties.Nodes.Add(new FbxNode("P", "FrontAxis", "int", "Integer", "", 2)); 99 | properties.Nodes.Add(new FbxNode("P", "FrontAxisSign", "int", "Integer", "", 1)); 100 | properties.Nodes.Add(new FbxNode("P", "CoordAxis", "int", "Integer", "", 0)); 101 | properties.Nodes.Add(new FbxNode("P", "CoordAxisSign", "int", "Integer", "", 1)); 102 | properties.Nodes.Add(new FbxNode("P", "OriginalUpAxis", "int", "Integer", "", 2)); 103 | properties.Nodes.Add(new FbxNode("P", "OriginalUpAxisSign", "int", "Integer", "", 1)); 104 | properties.Nodes.Add(new FbxNode("P", "UnitScaleFactor", "double", "Number", "", 100)); 105 | properties.Nodes.Add(new FbxNode("P", "OriginalUnitScaleFactor", "double", "Number")); 106 | properties.Nodes.Add(new FbxNode("P", "AmbientColor", "ColorRGB", "Color", "", 0, 0, 0)); 107 | properties.Nodes.Add(new FbxNode("P", "DefaultCamera", "KString", "", "", "Producer")); 108 | properties.Nodes.Add(new FbxNode("P", "TimeMode", "enum", "", "", 6)); 109 | properties.Nodes.Add(new FbxNode("P", "TimeProtocol", "enum", "", "", 2)); 110 | properties.Nodes.Add(new FbxNode("P", "SnapOnFrameMode", "enum", "", "", 0)); 111 | properties.Nodes.Add(new FbxNode("P", "TimeSpanStart", "KTime", "Time", "", 0)); 112 | properties.Nodes.Add(new FbxNode("P", "TimeSpanStop", "KTime", "Time", "", 153953860)); 113 | properties.Nodes.Add(new FbxNode("P", "CustomFrameRate", "double", "Number", "", -1)); 114 | properties.Nodes.Add(new FbxNode("P", "TimeMarker", "Compound", "", "")); 115 | properties.Nodes.Add(new FbxNode("P", "CurrentTimeMarker", "int", "Integer", "", -1)); 116 | 117 | return node; 118 | } 119 | 120 | private T getPropertyValue([CallerMemberName] string name = null) 121 | { 122 | return (T)_properties[name].Value; 123 | } 124 | 125 | private void setPropertyValue(object value, [CallerMemberName] string name = null) 126 | { 127 | _properties[name].Value = value; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /MeshSharp.FBX/Writers/IFbxWriter.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.FBX 2 | { 3 | public interface IFbxWriter 4 | { 5 | FbxRootNode GetRootNode(); 6 | void WriteBinary(); 7 | void WriteAscii(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.GLTF/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.GLTF 2 | { 3 | public class Class1 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MeshSharp.GLTF/MeshSharp.GLTF.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | AssetRipper.MeshSharp.GLTF 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MeshSharp.OBJ/MeshSharp.OBJ.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | latest 6 | AssetRipper.MeshSharp.OBJ 7 | ds5678 8 | AssetRipper 9 | 1.0.0.0 10 | 1.0.0.0 11 | AssetRipper.MeshSharp.OBJ 12 | C# 3D obj 13 | https://github.com/AssetRipper/MeshSharp 14 | https://github.com/AssetRipper/MeshSharp 15 | MIT 16 | git 17 | Copyright (c) 2022 ds5678 18 | MeshSharp module for the OBJ format. 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | bin\MeshSharp.OBJ.xml 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /MeshSharp.OBJ/Notes.md: -------------------------------------------------------------------------------- 1 | # OBJ File format 2 | 3 | ## References 4 | 5 | * [OBJ Format - Library of Congress](https://www.loc.gov/preservation/digital/formats/fdd/fdd000507.shtml) 6 | * [Analysis by Paul Bourke](http://paulbourke.net/dataformats/obj/) 7 | * [A list of example OBJ files](https://people.math.sc.edu/Burkardt/data/obj/obj.html) -------------------------------------------------------------------------------- /MeshSharp.OBJ/ObjAsciiWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.Elements.Geometries; 3 | using AssetRipper.MeshSharp.IO; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace AssetRipper.MeshSharp.OBJ 8 | { 9 | internal class ObjAsciiWriter : MeshWriterBase 10 | { 11 | public ObjAsciiWriter(Stream stream) : base(stream) { } 12 | public ObjAsciiWriter(string path) : base(path) { } 13 | 14 | public void Write(Scene scene) 15 | { 16 | List meshes = MeshUtils.GetAllMeshes(scene); 17 | ObjMesh processedMesh = ObjMesh.ConvertFromMeshes(meshes); 18 | using (StreamWriter writer = new StreamWriter(stream)) 19 | { 20 | writer.WriteLine(processedMesh.ToAsciiString()); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MeshSharp.OBJ/ObjMesh.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements.Geometries; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AssetRipper.MeshSharp.OBJ 6 | { 7 | internal class ObjMesh 8 | { 9 | private string Name = "ObjMesh"; 10 | private readonly List vertices = new List(); 11 | private readonly List polygons = new List(); 12 | 13 | internal static ObjMesh ConvertFromMeshes(List meshes) 14 | { 15 | ObjMesh result = new ObjMesh(); 16 | result.ReadFromMeshes(meshes); 17 | return result; 18 | } 19 | 20 | private void ReadFromMeshes(List meshes) 21 | { 22 | if (meshes.Count == 1 && !string.IsNullOrEmpty(meshes[0].Name)) 23 | { 24 | Name = meshes[0].Name; 25 | } 26 | 27 | foreach (Mesh mesh in meshes) 28 | { 29 | foreach (XYZ vertex in mesh.Vertices) 30 | { 31 | if (!vertices.Contains(vertex)) 32 | vertices.Add(vertex); 33 | } 34 | foreach (Polygon polygon in mesh.Polygons) 35 | { 36 | uint[] newIndices = new uint[polygon.Indices.Length]; 37 | for (uint i = 0; i < newIndices.Length; i++) 38 | { 39 | //Note: OBJ indices start at 1, not 0 40 | newIndices[i] = (uint)vertices.IndexOf(mesh.Vertices[(int)polygon.Indices[i]]) + 1; 41 | } 42 | polygons.Add(new Polygon(newIndices)); 43 | } 44 | } 45 | } 46 | 47 | public string ToAsciiString() 48 | { 49 | StringBuilder sb = new StringBuilder(); 50 | sb.AppendLine($"g {Name}"); 51 | foreach (XYZ vertex in vertices) 52 | { 53 | sb.AppendLine($"v {vertex.X} {vertex.Y} {vertex.Z}"); 54 | } 55 | foreach (Polygon polygon in polygons) 56 | { 57 | sb.Append('f'); 58 | foreach (uint index in polygon.Indices) 59 | { 60 | sb.Append($" {index}"); 61 | } 62 | sb.AppendLine(); 63 | } 64 | return sb.ToString(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /MeshSharp.OBJ/ObjReader.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.OBJ 2 | { 3 | internal class ObjReader 4 | { 5 | public ObjReader() 6 | { 7 | 8 | } 9 | 10 | public void Read() 11 | { 12 | 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MeshSharp.OBJ/ObjWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using System.IO; 3 | 4 | namespace AssetRipper.MeshSharp.OBJ 5 | { 6 | public static class ObjWriter 7 | { 8 | public static void Write(string path, Scene scene) => new ObjAsciiWriter(path).Write(scene); 9 | public static void Write(Stream stream, Scene scene) => new ObjAsciiWriter(stream).Write(scene); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MeshSharp.PLY/FileFormatDefinition.md: -------------------------------------------------------------------------------- 1 | # The PLY Polygon File Format 2 | 3 | Author: Greg Turk 4 | 5 | ## Introduction 6 | 7 | This document presents the PLY polygon file format, a format for storing graphical objects that are described as a collection of polygons. Our goal is to provide a format that is simple and easy to implement but that is general enough to be useful for a wide range of models. The file format has two sub-formats: an ASCII representation for easily getting started, and a binary version for compact storage and for rapid saving and loading. We hope that this format will promote the exchange of graphical object between programs and also between groups of people. 8 | 9 | ## Overview 10 | 11 | Anyone who has worked in the field of computer graphics for even a short time knows about the bewildering array of storage formats for graphical objects. It seems as though every programmer creates a new file format for nearly every new programming project. The way out of this morass of formats is to create a single file format that is both flexible enough to anticipate future needs and that is simple enough so as not to drive away potential users. Once such a format is defined, a suite of utilities (both procedures and entire programs) can be written that are centered around this format. Each new utility that is added to the suite can leverage off the power of the others. 12 | 13 | The PLY format describes an object as a collection of vertices, faces and other elements, along with properties such as color and normal direction that can be attached to these elements. A PLY file contains the description of exactly one object. Sources of such objects include: hand-digitized objects, polygon objects from modeling programs, range data, triangles from marching cubes (isosurfaces from volume data), terrain data, radiosity models. Properties that might be stored with the object include: color, surface normals, texture coordinates, transparency, range data confidence, and different properties for the front and back of a polygon. 14 | 15 | The PLY format is NOT intended to be a general scene description language, a shading language or a catch-all modeling format. This means that it includes no transformation matrices, object instantiation, modeling hierarchies, or object sub-parts. It does not include parametric patches, quadric surfaces, constructive solid geometry operations, triangle strips, polygons with holes, or texture descriptions (not to be confused with texture coordinates, which it does support!). 16 | 17 | A typical PLY object definition is simply a list of (x,y,z) triples for vertices and a list of faces that are described by indices into the list of vertices. Most PLY files include this core information. Vertices and faces are two examples of "elements", and the bulk of a PLY file is its list of elements. Each element in a given file has a fixed number of "properties" that are specified for each element. The typical information in a PLY file contains just two elements, the (x,y,z) triples for vertices and the vertex indices for each face. Applications can create new properties that are attached to elements of an object. For example, the properties red, green and blue are commonly associated with vertex elements. New properties are added in such a way that old programs do not break when these new properties are encountered. Properties that are not understood by a program can either be carried along uninterpreted or can be discarded. In addition, one can create a new element type and define the properties associated with this element. Examples of new elements are edges, cells (lists of pointers to faces) and materials (ambient, diffuse and specular colors and coefficients). New elements can also be carried along or discarded by programs that do not understand them. 18 | 19 | ## File Structure 20 | 21 | This is the structure of a typical PLY file: 22 | 23 | Header Vertex List Face List (lists of other elements) 24 | 25 | The header is a series of carraige-return terminated lines of text that describe the remainder of the file. The header includes a description of each element type, including the element's name (e.g. "edge"), how many such elements are in the object, and a list of the various properties associated with the element. The header also tells whether the file is binary or ASCII. Following the header is one list of elements for each element type, presented in the order described in the header. 26 | 27 | Below is the complete ASCII description for a cube. The header of a binary version of the same object would differ only in substituting the word "binary_little_endian" or "binary_big_endian" for the word "ascii". The comments in brackets are NOT part of the file, they are annotations to this example. Comments in files are ordinary keyword-identified lines that begin with the word "comment". 28 | ``` 29 | ply 30 | format ascii 1.0 { ascii/binary, format version number } 31 | comment made by Greg Turk { comments keyword specified, like all lines } 32 | comment this file is a cube 33 | element vertex 8 { define "vertex" element, 8 of them in file } 34 | property float x { vertex contains float "x" coordinate } 35 | property float y { y coordinate is also a vertex property } 36 | property float z { z coordinate, too } 37 | element face 6 { there are 6 "face" elements in the file } 38 | property list uchar int vertex_index { "vertex_indices" is a list of ints } 39 | end_header { delimits the end of the header } 40 | 0 0 0 { start of vertex list } 41 | 0 0 1 42 | 0 1 1 43 | 0 1 0 44 | 1 0 0 45 | 1 0 1 46 | 1 1 1 47 | 1 1 0 48 | 4 0 1 2 3 { start of face list } 49 | 4 7 6 5 4 50 | 4 0 4 5 1 51 | 4 1 5 6 2 52 | 4 2 6 7 3 53 | 4 3 7 4 0 54 | ``` 55 | This example demonstrates the basic components of the header. Each part of the header is a carraige-return terminated ASCII string that begins with a keyword. Even the start and end of the header (`ply` and `end_header`) are in this form. The characters `ply` must be the first four characters of the file, since they serve as the file's magic number. Following the start of the header is the keyword `format` and a specification of either ASCII or binary format, followed by a version number. Next is the description of each of the elements in the polygon file, and within each element description is the specification of the properties. Then generic element description has this form: 56 | ``` 57 | element 58 | property 59 | property 60 | property 61 | ... 62 | ``` 63 | The properties listed after an `element` line define both the data type of the property and also the order in which the property appears for each element. There are two kinds of data types a property may have: scalar and list. Here is a list of the scalar data types a property may have: 64 | ``` 65 | name double number of bytes 66 | char character 1 67 | uchar unsigned character 1 68 | short short integer 2 69 | ushort unsigned short integer 2 70 | int integer 4 71 | uint unsigned integer 4 72 | float single-precision float 4 73 | double double-precision float 8 74 | ``` 75 | These byte counts are important and must not vary across implementations in order for these files to be portable. There is a special form of property definitions that uses the list data type: 76 | ``` 77 | property list 78 | ``` 79 | An example of this is from the cube file above: 80 | ``` 81 | property list uchar int vertex_index 82 | ``` 83 | This means that the property `vertex_index` contains first an unsigned char telling how many indices the property contains, followed by a list containing that many integers. Each integer in this variable-length list is an index to a vertex. 84 | 85 | ## Another Example 86 | 87 | Here is another cube definition: 88 | ``` 89 | ply 90 | format ascii 1.0 91 | comment author: Greg Turk 92 | comment object: another cube 93 | element vertex 8 94 | property float x 95 | property float y 96 | property float z 97 | property red uchar { start of vertex color } 98 | property green uchar 99 | property blue uchar 100 | element face 7 101 | property list uchar int vertex_index { number of vertices for each face } 102 | element edge 5 { five edges in object } 103 | property int vertex1 { index to first vertex of edge } 104 | property int vertex2 { index to second vertex } 105 | property uchar red { start of edge color } 106 | property uchar green 107 | property uchar blue end_header 108 | 0 0 0 255 0 0 { start of vertex list } 109 | 0 0 1 255 0 0 110 | 0 1 1 255 0 0 111 | 0 1 0 255 0 0 112 | 1 0 0 0 0 255 113 | 1 0 1 0 0 255 114 | 1 1 1 0 0 255 115 | 1 1 0 0 0 255 116 | 3 0 1 2 { start of face list, begin with a triangle } 117 | 3 0 2 3 { another triangle } 118 | 4 7 6 5 4 { now some quadrilaterals } 119 | 4 0 4 5 1 120 | 4 1 5 6 2 121 | 4 2 6 7 3 122 | 4 3 7 4 0 123 | 0 1 255 255 255 { start of edge list, begin with white edge } 124 | 1 2 255 255 255 125 | 2 3 255 255 255 126 | 3 0 255 255 255 127 | 2 0 0 0 0 { end with a single black line } 128 | ``` 129 | This file specifies a red, green and blue value for each vertex. To illustrate the variable-length nature of vertex_index, the first two faces of the object are triangles instead of a single square. This means that the number of faces in the object is 7. This object also contains a list of edges. Each edge contains two pointers to the vertices that delinate the edge. Each edge also has a color. The five edges defined above were specified so as to highlight the two triangles in the file. The first four edges are white, and they surround the two triangles. The final edge is black, and it is the edge that separates the triangles. 130 | 131 | ## User-Defined Elements 132 | The examples above showed the use of three elements: vertices, faces and edges. The PLY format allows users to define their own elements as well. The format for defining a new element is exactly the same as for vertices, faces and edges. Here is the section of a header that defines a material property: 133 | ``` 134 | element material 6 property ambient_red uchar { ambient color } property ambient_green uchar property ambient_blue uchar property ambient_coeff float property diffuse_red uchar { diffuse color } property diffuse_green uchar property diffuse_blue uchar property diffuse_coeff float property specular_red uchar { specular color } property specular_green uchar property specular_blue uchar property specular_coeff float property specular_power float { Phong power } 135 | ``` 136 | These lines would appear in the header directly after the specification of vertices, faces and edges. If we want each vertex to have a material specification, we might add this line to the end of the properties for a vertex: 137 | ``` 138 | property material_index int 139 | ``` 140 | This integer is now an index into the list of materials contained in the file. It may be tempting for the author of a new application to invent several new elements to be stored in PLY files. This practice should be kept to a minimum. Much better is to try adapting common elements (vertices, faces, edges, materials) to new uses, so that other programs that understand these elements might be useful in manipulating these adapted elements. Take, for example, an application that describes molecules as collections of spheres and cylinders. It would be tempting define sphere and cylinder elements for the PLY files containing the molecules. If, however, we use the vertex and edge elements for this purpose (adding the radius property to each), we can make use of programs that manipulate and display vertices and edges. Clearly one should not create special elements for triangles and quadrilaterals, but instead use the face element. What if a program does not know the adjacency between faces and vertices (so-called unshared vertices)? This is where each triangle (say) is purely a collection of three positions in space, with no notion whether some triangles have common vertices. This is a fairly common situation. Assuming there are N triangles in a given object, then 3N vertices should be written to the file, followed by N faces that simply connect up these vertices. We anticipate that a utility will be written that converts between unshared and shared vertex files. 141 | 142 | Copyright (c) 1994 The Board of Trustees of The Leland Stanford Junior University. All rights reserved. 143 | Permission to use, copy, modify and distribute this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice and this permission notice appear in all copies of this software and that you do not sell the software. 144 | 145 | THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE -------------------------------------------------------------------------------- /MeshSharp.PLY/MeshSharp.PLY.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | latest 6 | AssetRipper.MeshSharp.PLY 7 | ds5678 8 | AssetRipper 9 | 1.0.0.0 10 | 1.0.0.0 11 | AssetRipper.MeshSharp.PLY 12 | C# 3D ply 13 | https://github.com/AssetRipper/MeshSharp 14 | https://github.com/AssetRipper/MeshSharp 15 | MIT 16 | git 17 | Copyright (c) 2022 ds5678 18 | MeshSharp module for the PLY format. 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | bin\MeshSharp.PLY.xml 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /MeshSharp.PLY/Notes.md: -------------------------------------------------------------------------------- 1 | # PLY file format 2 | 3 | ## References 4 | 5 | * [Wikipedia entry](https://en.wikipedia.org/wiki/PLY_(file_format)) 6 | * [Analysis by Paul Bourke](http://paulbourke.net/dataformats/ply/) 7 | * [Some example files](https://people.math.sc.edu/Burkardt/data/ply/ply.html) -------------------------------------------------------------------------------- /MeshSharp.PLY/PlyFileFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp.PLY 4 | { 5 | internal enum PlyFileFormat 6 | { 7 | Ascii, 8 | BinaryLittleEndian, 9 | BinaryBigEndian, 10 | } 11 | 12 | internal static class PlyFileFormatExtensions 13 | { 14 | public static string ToFileString(this PlyFileFormat format) => format switch 15 | { 16 | PlyFileFormat.Ascii => "ascii", 17 | PlyFileFormat.BinaryLittleEndian => "binary_little_endian", 18 | PlyFileFormat.BinaryBigEndian => "binary_big_endian", 19 | _ => throw new ArgumentOutOfRangeException(nameof(format)), 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MeshSharp.PLY/PlyHeader.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace AssetRipper.MeshSharp.PLY 4 | { 5 | internal class PlyHeader 6 | { 7 | private const string PlyMagic = "ply"; 8 | private const string EndHeaderString = "end_header"; 9 | 10 | public PlyFileFormat Format { get; set; } 11 | public int VertexCount { get; set; } 12 | public int FaceCount { get; set; } 13 | 14 | public string ToAsciiString() 15 | { 16 | StringBuilder sb = new StringBuilder(); 17 | sb.AppendLine(PlyMagic); 18 | sb.AppendLine($"format {Format.ToFileString()} 1.0"); 19 | sb.AppendLine($"element vertex {VertexCount}"); 20 | sb.AppendLine("property float x"); 21 | sb.AppendLine("property float y"); 22 | sb.AppendLine("property float z"); 23 | sb.AppendLine($"element face {FaceCount}"); 24 | sb.AppendLine("property list uchar int vertex_index"); 25 | sb.AppendLine(EndHeaderString); 26 | return sb.ToString(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MeshSharp.PLY/PlyMesh.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements.Geometries; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AssetRipper.MeshSharp.PLY 6 | { 7 | internal class PlyMesh 8 | { 9 | private readonly List vertices = new List(); 10 | private readonly List polygons = new List(); 11 | 12 | public int VertexCount => vertices.Count; 13 | public int PolygonCount => polygons.Count; 14 | 15 | internal static PlyMesh ConvertFromMeshes(List meshes) 16 | { 17 | PlyMesh result = new PlyMesh(); 18 | result.ReadFromMeshes(meshes); 19 | return result; 20 | } 21 | 22 | private void ReadFromMeshes(List meshes) 23 | { 24 | foreach (Mesh mesh in meshes) 25 | { 26 | foreach (XYZ vertex in mesh.Vertices) 27 | { 28 | if (!vertices.Contains(vertex)) 29 | vertices.Add(vertex); 30 | } 31 | foreach (Polygon polygon in mesh.Polygons) 32 | { 33 | uint[] newIndices = new uint[polygon.Indices.Length]; 34 | for (uint i = 0; i < newIndices.Length; i++) 35 | { 36 | newIndices[i] = (uint)vertices.IndexOf(mesh.Vertices[(int)polygon.Indices[i]]); 37 | } 38 | polygons.Add(new Polygon(newIndices)); 39 | } 40 | } 41 | } 42 | 43 | public string ToAsciiString() 44 | { 45 | StringBuilder sb = new StringBuilder(); 46 | foreach (XYZ vertex in vertices) 47 | { 48 | sb.AppendLine($"{vertex.X} {vertex.Y} {vertex.Z}"); 49 | } 50 | foreach (Polygon polygon in polygons) 51 | { 52 | sb.Append(polygon.Indices.Length); 53 | foreach (uint index in polygon.Indices) 54 | { 55 | sb.Append($" {index}"); 56 | } 57 | sb.AppendLine(); 58 | } 59 | return sb.ToString(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MeshSharp.PLY/PlyWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.PLY.Writers; 3 | using System.IO; 4 | 5 | namespace AssetRipper.MeshSharp.PLY 6 | { 7 | public static class PlyWriter 8 | { 9 | public static void WriteAscii(string path, Scene scene) 10 | { 11 | new PlyAsciiWriter(path).Write(scene); 12 | } 13 | public static void WriteAscii(Stream stream, Scene scene) 14 | { 15 | new PlyAsciiWriter(stream).Write(scene); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MeshSharp.PLY/Writers/PlyAsciiWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace AssetRipper.MeshSharp.PLY.Writers 5 | { 6 | internal class PlyAsciiWriter : PlyWriterBase 7 | { 8 | public PlyAsciiWriter(Stream stream) : base(stream) 9 | { 10 | } 11 | 12 | public PlyAsciiWriter(string path) : base(path) 13 | { 14 | } 15 | 16 | protected override PlyFileFormat Format => PlyFileFormat.Ascii; 17 | 18 | protected override void WriteMeshData(PlyMesh mesh) 19 | { 20 | using StreamWriter writer = new StreamWriter(stream, Encoding.ASCII); 21 | writer.WriteLine(mesh.ToAsciiString()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MeshSharp.PLY/Writers/PlyWriterBase.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.Elements.Geometries; 3 | using AssetRipper.MeshSharp.IO; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace AssetRipper.MeshSharp.PLY.Writers 9 | { 10 | internal abstract class PlyWriterBase : MeshWriterBase 11 | { 12 | public PlyWriterBase(Stream stream) : base(stream) { } 13 | public PlyWriterBase(string path) : base(path) { } 14 | protected abstract PlyFileFormat Format { get; } 15 | 16 | public void Write(Scene scene) 17 | { 18 | List meshes = MeshUtils.GetAllMeshes(scene); 19 | PlyMesh processedMesh = PlyMesh.ConvertFromMeshes(meshes); 20 | PlyHeader header = new PlyHeader(); 21 | header.Format = Format; 22 | header.VertexCount = processedMesh.VertexCount; 23 | header.FaceCount = processedMesh.PolygonCount; 24 | using (StreamWriter writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: -1, leaveOpen: true)) 25 | { 26 | writer.Write(header.ToAsciiString()); 27 | } 28 | WriteMeshData(processedMesh); 29 | } 30 | 31 | protected abstract void WriteMeshData(PlyMesh mesh); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MeshSharp.PMX/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.PMX 2 | { 3 | public class Class1 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MeshSharp.PMX/MeshSharp.PMX.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | AssetRipper.MeshSharp.PMX 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MeshSharp.STL/MeshSharp.STL.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | latest 6 | AssetRipper.MeshSharp.STL 7 | ds5678 8 | AssetRipper 9 | 1.0.0.0 10 | 1.0.0.0 11 | AssetRipper.MeshSharp.STL 12 | C# 3D stl 13 | https://github.com/AssetRipper/MeshSharp 14 | https://github.com/AssetRipper/MeshSharp 15 | MIT 16 | git 17 | Copyright (c) 2022 ds5678 18 | MeshSharp module for the STL format. 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | bin\MeshSharp.STL.xml 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /MeshSharp.STL/Notes.md: -------------------------------------------------------------------------------- 1 | # Ascii STL format 2 | 3 | An ASCII STL file begins with the line 4 | ``` 5 | solid name 6 | ``` 7 | where **name** is an optional string (though if name is omitted there must still be a space after solid). The file continues with any number of triangles, each represented as follows: 8 | ``` 9 | facet normal ni nj nk 10 | outer loop 11 | vertex v1x v1y v1z 12 | vertex v2x v2y v2z 13 | vertex v3x v3y v3z 14 | endloop 15 | endfacet 16 | ``` 17 | 18 | where each n or v is a floating-point number in sign-mantissa-"e"-sign-exponent format, e.g., "2.648000e-002". The file concludes with 19 | ``` 20 | endsolid name 21 | ``` 22 | The structure of the format suggests that other possibilities exist (e.g., facets with more than one "loop", or loops with more than three vertices). In practice, however, all facets are simple triangles. 23 | 24 | White space (spaces, tabs, newlines) may be used anywhere in the file except within numbers or words. The spaces between "facet" and "normal" and between "outer" and "loop" are required. 25 | 26 | # Binary STL format 27 | 28 | Because ASCII STL files can be very large, a binary version of STL exists. A binary STL file has an 80-character header (which is generally ignored, but should never begin with "solid" because that may lead some software to assume that this is an ASCII STL file). Following the header is a 4-byte little-endian unsigned integer indicating the number of triangular facets in the file. Following that is data describing each triangle in turn. The file simply ends after the last triangle. 29 | 30 | Each triangle is described by twelve 32-bit floating-point numbers: three for the normal and then three for the X/Y/Z coordinate of each vertex – just as with the ASCII version of STL. After these follows a 2-byte ("short") unsigned integer that is the "attribute byte count" – in the standard format, this should be zero because most software does not understand anything else. 31 | 32 | Floating-point numbers are represented as IEEE floating-point numbers (the same format as floats in .NET) and are assumed to be little-endian, although this is not stated in documentation. 33 | 34 | # References 35 | 36 | * [Wikipedia Entry](https://en.wikipedia.org/wiki/STL_(file_format)) 37 | * [Analysis by Paul Bourke](http://paulbourke.net/dataformats/stl/) 38 | * [Analysis by Fabbers](http://www.fabbers.com/tech/STL_Format) 39 | * [Some example Ascii STL files](https://people.math.sc.edu/Burkardt/data/stla/stla.html) 40 | * [Some example Binary STL files](https://people.math.sc.edu/Burkardt/data/stlb/stlb.html) -------------------------------------------------------------------------------- /MeshSharp.STL/Readers/StlAsciiReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Numerics; 5 | 6 | namespace AssetRipper.MeshSharp.STL.Readers 7 | { 8 | internal class StlAsciiReader : StlReaderBase 9 | { 10 | public StlAsciiReader(Stream stream) : base(stream) { } 11 | public StlAsciiReader(string path) : base(path) { } 12 | 13 | protected override StlTriangle[] ReadFile() 14 | { 15 | using StreamReader reader = new(stream); 16 | string magic = ReadMagic(reader); 17 | if (magic != "solid ") 18 | throw new Exception($"Invalid magic: '{magic}"); 19 | string name = reader.ReadLine(); 20 | 21 | var result = new List(); 22 | while (true) 23 | { 24 | StlTriangle triangle = ReadTriangle(reader); 25 | if (triangle == null) 26 | break; 27 | result.Add(triangle); 28 | } 29 | return result.ToArray(); 30 | } 31 | 32 | private static string ReadMagic(StreamReader reader) 33 | { 34 | char[] buffer = new char[6]; 35 | reader.Read(buffer, 0, buffer.Length); 36 | return new string(buffer); 37 | } 38 | 39 | private static StlTriangle ReadTriangle(StreamReader reader) 40 | { 41 | string normalLine = GetTrimmedNonEmptyLine(reader); 42 | if (normalLine == null) 43 | throw new Exception("Stream ended whilst reading an STL triangle."); 44 | if (normalLine.StartsWith("endsolid")) 45 | return null;//This line ends the file 46 | 47 | Vector3 normal = ParseNormal(normalLine); 48 | 49 | string outerLoopLine = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 50 | AssertSyntax(outerLoopLine, "outer loop"); 51 | 52 | string vertex1Line = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 53 | Vector3 vertex1 = ParseVertex(vertex1Line); 54 | 55 | string vertex2Line = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 56 | Vector3 vertex2 = ParseVertex(vertex2Line); 57 | 58 | string vertex3Line = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 59 | Vector3 vertex3 = ParseVertex(vertex3Line); 60 | 61 | string endLoopLine = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 62 | AssertSyntax(endLoopLine, "endloop"); 63 | 64 | string endFacetLine = GetTrimmedNonEmptyLine(reader) ?? throw new Exception("Stream ended whilst reading an STL triangle."); 65 | AssertSyntax(endFacetLine, "endfacet"); 66 | 67 | return new StlTriangle(vertex1, vertex2, vertex3, normal); 68 | } 69 | 70 | private static string GetTrimmedNonEmptyLine(StreamReader reader) 71 | { 72 | string result; 73 | while (true) 74 | { 75 | result = reader.ReadLine()?.Trim(); 76 | if (result == null) 77 | return null; 78 | if (result.Length != 0) 79 | return result; 80 | } 81 | } 82 | 83 | private static void AssertSyntax(string line, string expectedContents) 84 | { 85 | if (line != expectedContents) 86 | throw new Exception($"Assertion failed: read '{line}' expected '{expectedContents};"); 87 | } 88 | 89 | private static float ParseFloat(string text) => float.Parse(text, System.Globalization.CultureInfo.InvariantCulture); 90 | 91 | private static Vector3 ParseNormal(string line) 92 | { 93 | string[] entries = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); 94 | if (entries[0] != "facet") 95 | throw new Exception($"Invalid facet keyword: '{entries[0]}'"); 96 | if (entries[1] != "normal") 97 | throw new Exception($"Invalid normal keyword: '{entries[1]}'"); 98 | float nx = ParseFloat(entries[2]); 99 | float ny = ParseFloat(entries[3]); 100 | float nz = ParseFloat(entries[4]); 101 | return new Vector3(nx, ny, nz); 102 | } 103 | 104 | private static Vector3 ParseVertex(string line) 105 | { 106 | string[] entries = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); 107 | if (entries[0] != "vertex") 108 | throw new Exception($"Invalid vertex keyword: '{entries[0]}'"); 109 | float vx = ParseFloat(entries[1]); 110 | float vy = ParseFloat(entries[2]); 111 | float vz = ParseFloat(entries[3]); 112 | return new Vector3(vx, vy, vz); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MeshSharp.STL/Readers/StlBinaryReader.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.IO; 2 | using System; 3 | using System.IO; 4 | 5 | namespace AssetRipper.MeshSharp.STL.Readers 6 | { 7 | internal class StlBinaryReader : StlReaderBase 8 | { 9 | public StlBinaryReader(Stream stream) : base(stream) { } 10 | public StlBinaryReader(string path) : base(path) { } 11 | 12 | protected override StlTriangle[] ReadFile() 13 | { 14 | using BinaryReader reader = new(stream); 15 | reader.ReadBytes(80); 16 | uint count = reader.ReadUInt32(); 17 | var result = new StlTriangle[count]; 18 | for (uint i = 0; i < count; i++) 19 | { 20 | result[i] = reader.ReadStruct(); 21 | } 22 | if (stream.Position != stream.Length) 23 | throw new Exception($"Read {stream.Position} but expected {stream.Length} while read STL binary file."); 24 | return result; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MeshSharp.STL/Readers/StlReaderBase.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.Elements.Geometries; 3 | using AssetRipper.MeshSharp.Elements.Geometries.Layers; 4 | using AssetRipper.MeshSharp.IO; 5 | using System.IO; 6 | 7 | namespace AssetRipper.MeshSharp.STL.Readers 8 | { 9 | internal abstract class StlReaderBase : MeshReaderBase 10 | { 11 | public StlReaderBase(Stream stream) : base(stream) { } 12 | public StlReaderBase(string path) : base(path) { } 13 | 14 | public Scene Read() 15 | { 16 | StlTriangle[] triangles = ReadFile(); 17 | Scene result = new(); 18 | result.Name = "LoadedSTL"; 19 | Node root = new(); 20 | root.Name = "Root"; 21 | result.Nodes.Add(root); 22 | Mesh mesh = ConvertToMesh(triangles); 23 | root.Children.Add(mesh); 24 | return result; 25 | } 26 | 27 | protected abstract StlTriangle[] ReadFile(); 28 | 29 | private static Mesh ConvertToMesh(StlTriangle[] triangles) 30 | { 31 | Mesh mesh = new Mesh(); 32 | mesh.Name = "CompositeMesh"; 33 | LayerElementNormal normalElement = new LayerElementNormal(mesh); 34 | mesh.Layers.Add(normalElement); 35 | normalElement.MappingInformationType = MappingMode.ByPolygon; 36 | normalElement.ReferenceInformationType = ReferenceMode.Direct; 37 | 38 | foreach (StlTriangle triangle in triangles) 39 | { 40 | uint index1 = (uint)AddVertexToList(mesh, triangle.Vertex1); 41 | uint index2 = (uint)AddVertexToList(mesh, triangle.Vertex2); 42 | uint index3 = (uint)AddVertexToList(mesh, triangle.Vertex3); 43 | XYZ normal = triangle.Normal; 44 | mesh.Polygons.Add(new Triangle(index1, index2, index3)); 45 | normalElement.Normals.Add(normal); 46 | } 47 | return mesh; 48 | } 49 | 50 | private static int AddVertexToList(Mesh mesh, XYZ vertex) 51 | { 52 | int index = mesh.Vertices.IndexOf(vertex); 53 | if (index < 0) 54 | { 55 | index = mesh.Vertices.Count; 56 | mesh.Vertices.Add(vertex); 57 | } 58 | return index; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MeshSharp.STL/StlReader.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.STL.Readers; 3 | using System.IO; 4 | 5 | namespace AssetRipper.MeshSharp.STL 6 | { 7 | public static class StlReader 8 | { 9 | public static Scene ReadAscii(string path) 10 | { 11 | return new StlAsciiReader(path).Read(); 12 | } 13 | public static Scene ReadAscii(Stream stream) 14 | { 15 | return new StlAsciiReader(stream).Read(); 16 | } 17 | public static Scene ReadBinary(string path) 18 | { 19 | return new StlBinaryReader(path).Read(); 20 | } 21 | public static Scene ReadBinary(Stream stream) 22 | { 23 | return new StlBinaryReader(stream).Read(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MeshSharp.STL/StlTriangle.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.IO; 2 | using System.IO; 3 | using System.Numerics; 4 | using System.Text; 5 | 6 | namespace AssetRipper.MeshSharp.STL 7 | { 8 | internal class StlTriangle : IBinaryReadable, IBinaryWriteable 9 | { 10 | public StlTriangle() 11 | { 12 | } 13 | 14 | public StlTriangle(Vector3 vertex1, Vector3 vertex2, Vector3 vertex3) 15 | { 16 | Vertex1 = vertex1; 17 | Vertex2 = vertex2; 18 | Vertex3 = vertex3; 19 | RecalculateNormal(); 20 | } 21 | 22 | public StlTriangle(Vector3 vertex1, Vector3 vertex2, Vector3 vertex3, Vector3 normal) 23 | { 24 | Normal = normal; 25 | Vertex1 = vertex1; 26 | Vertex2 = vertex2; 27 | Vertex3 = vertex3; 28 | } 29 | 30 | public Vector3 Normal { get; private set; } 31 | public Vector3 Vertex1 { get; private set; } 32 | public Vector3 Vertex2 { get; private set; } 33 | public Vector3 Vertex3 { get; private set; } 34 | 35 | public void RecalculateNormal() 36 | { 37 | Normal = (Vector3)XYZ.FindNormal(Vertex1, Vertex2, Vertex3); 38 | } 39 | 40 | public void Read(BinaryReader reader) 41 | { 42 | Normal = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); 43 | Vertex1 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); 44 | Vertex2 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); 45 | Vertex3 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); 46 | reader.ReadUInt16(); //attribute byte count 47 | } 48 | 49 | public void Write(BinaryWriter writer) 50 | { 51 | writer.Write(Normal.X); 52 | writer.Write(Normal.Y); 53 | writer.Write(Normal.Z); 54 | writer.Write(Vertex1.X); 55 | writer.Write(Vertex1.Y); 56 | writer.Write(Vertex1.Z); 57 | writer.Write(Vertex2.X); 58 | writer.Write(Vertex2.Y); 59 | writer.Write(Vertex2.Z); 60 | writer.Write(Vertex3.X); 61 | writer.Write(Vertex3.Y); 62 | writer.Write(Vertex3.Z); 63 | writer.Write((ushort)0); //attribute byte count is always zero 64 | } 65 | 66 | public string ToAsciiString() 67 | { 68 | StringBuilder sb = new StringBuilder(); 69 | sb.AppendLine($"facet normal {ToStringInvariant(Normal.X)} {ToStringInvariant(Normal.Y)} {ToStringInvariant(Normal.Z)}"); 70 | sb.AppendLine("\touter loop"); 71 | sb.AppendLine($"\t\tvertex {ToStringInvariant(Vertex1.X)} {ToStringInvariant(Vertex1.Y)} {ToStringInvariant(Vertex1.Z)}"); 72 | sb.AppendLine($"\t\tvertex {ToStringInvariant(Vertex2.X)} {ToStringInvariant(Vertex2.Y)} {ToStringInvariant(Vertex2.Z)}"); 73 | sb.AppendLine($"\t\tvertex {ToStringInvariant(Vertex3.X)} {ToStringInvariant(Vertex3.Y)} {ToStringInvariant(Vertex3.Z)}"); 74 | sb.AppendLine("\tendloop"); 75 | sb.AppendLine("endfacet"); 76 | return sb.ToString(); 77 | } 78 | 79 | private static string ToStringInvariant(float value) => value.ToString("e", System.Globalization.CultureInfo.InvariantCulture); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MeshSharp.STL/StlWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.STL.Writers; 3 | using System.IO; 4 | 5 | namespace AssetRipper.MeshSharp.STL 6 | { 7 | public static class StlWriter 8 | { 9 | public static void WriteAscii(string path, Scene scene) 10 | { 11 | new StlAsciiWriter(path).Write(scene); 12 | } 13 | public static void WriteAscii(Stream stream, Scene scene) 14 | { 15 | new StlAsciiWriter(stream).Write(scene); 16 | } 17 | public static void WriteBinary(string path, Scene scene) 18 | { 19 | new StlBinaryWriter(path).Write(scene); 20 | } 21 | public static void WriteBinary(Stream stream, Scene scene) 22 | { 23 | new StlBinaryWriter(stream).Write(scene); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MeshSharp.STL/Writers/StlAsciiWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace AssetRipper.MeshSharp.STL.Writers 7 | { 8 | internal class StlAsciiWriter : StlWriterBase 9 | { 10 | public StlAsciiWriter(string path) : base(path) 11 | { 12 | } 13 | 14 | public StlAsciiWriter(Stream stream) : base(stream) 15 | { 16 | } 17 | 18 | public override void Write(Scene scene) 19 | { 20 | if (scene == null) 21 | throw new ArgumentNullException(nameof(scene)); 22 | List triangles = ConvertToStlTriangles(scene); 23 | using StreamWriter writer = new StreamWriter(stream); 24 | writer.WriteLine($"solid {scene.Name ?? ""}"); 25 | foreach (StlTriangle triangle in triangles) 26 | { 27 | writer.WriteLine(triangle.ToAsciiString()); 28 | } 29 | writer.WriteLine($"endsolid {scene.Name ?? ""}"); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /MeshSharp.STL/Writers/StlBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.IO; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace AssetRipper.MeshSharp.STL.Writers 8 | { 9 | internal class StlBinaryWriter : StlWriterBase 10 | { 11 | public StlBinaryWriter(string path) : base(path) 12 | { 13 | } 14 | 15 | public StlBinaryWriter(Stream stream) : base(stream) 16 | { 17 | } 18 | 19 | public override void Write(Scene scene) 20 | { 21 | if (scene == null) 22 | throw new ArgumentNullException(nameof(scene)); 23 | List triangles = ConvertToStlTriangles(scene); 24 | using BinaryWriter writer = new BinaryWriter(stream); 25 | writer.Write(new byte[80]); 26 | writer.Write((uint)triangles.Count); 27 | foreach (StlTriangle triangle in triangles) 28 | { 29 | writer.WriteStruct(triangle); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MeshSharp.STL/Writers/StlWriterBase.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using AssetRipper.MeshSharp.Elements.Geometries; 3 | using AssetRipper.MeshSharp.Elements.Geometries.Layers; 4 | using AssetRipper.MeshSharp.IO; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Numerics; 9 | 10 | namespace AssetRipper.MeshSharp.STL.Writers 11 | { 12 | internal abstract class StlWriterBase : MeshWriterBase 13 | { 14 | public StlWriterBase(Stream stream) : base(stream) { } 15 | public StlWriterBase(string path) : base(path) { } 16 | 17 | public abstract void Write(Scene scene); 18 | 19 | protected static List ConvertToStlTriangles(Scene scene) 20 | { 21 | List meshes = MeshUtils.GetAllMeshes(scene); 22 | return ConvertToStlTriangles(meshes); 23 | } 24 | 25 | private static List ConvertToStlTriangles(List meshes) 26 | { 27 | List result = new List(); 28 | foreach (Mesh mesh in meshes) 29 | { 30 | result.AddRange(ConvertToStlTriangles(mesh)); 31 | } 32 | return result; 33 | } 34 | 35 | private static List ConvertToStlTriangles(Mesh mesh) 36 | { 37 | LayerElementNormal normalElement = (LayerElementNormal)mesh.Layers.FirstOrDefault(layer => HasCompatibleNormalValues(layer as LayerElementNormal)); 38 | bool hasNormal = normalElement != null; 39 | //Console.WriteLine($"Has normals: {hasNormal}"); 40 | List result = new List(); 41 | for (int i = 0; i < mesh.Polygons.Count; i++) 42 | { 43 | Polygon polygon = mesh.Polygons[i]; 44 | if (polygon is Triangle triangle) 45 | { 46 | XYZ v0 = mesh.Vertices[(int)triangle.Index0]; 47 | XYZ v1 = mesh.Vertices[(int)triangle.Index1]; 48 | XYZ v2 = mesh.Vertices[(int)triangle.Index2]; 49 | if (hasNormal) 50 | { 51 | result.Add(new StlTriangle((Vector3)v0, (Vector3)v1, (Vector3)v2, (Vector3)normalElement.Normals[i])); 52 | } 53 | else 54 | { 55 | result.Add(new StlTriangle((Vector3)v0, (Vector3)v1, (Vector3)v2)); 56 | } 57 | } 58 | else 59 | { 60 | Triangle[] subTriangles = polygon.ConvertToTriangles(); 61 | foreach (Triangle subTriangle in subTriangles) 62 | { 63 | XYZ v0 = mesh.Vertices[(int)subTriangle.Index0]; 64 | XYZ v1 = mesh.Vertices[(int)subTriangle.Index1]; 65 | XYZ v2 = mesh.Vertices[(int)subTriangle.Index2]; 66 | result.Add(new StlTriangle((Vector3)v0, (Vector3)v1, (Vector3)v2)); 67 | } 68 | } 69 | } 70 | return result; 71 | } 72 | 73 | private static bool HasCompatibleNormalValues(LayerElementNormal normalElement) 74 | { 75 | return normalElement != null && normalElement.MappingInformationType == MappingMode.ByPolygon && normalElement.ReferenceInformationType == ReferenceMode.Direct; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MeshSharp.Tests/Matrix4Tests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace AssetRipper.MeshSharp.Tests 4 | { 5 | public class Matrix4Tests 6 | { 7 | /* 8 | Matrix organization: 9 | |m00|m10|m20|m30| 10 | |m01|m11|m21|m31| 11 | |m02|m12|m22|m32| 12 | |m03|m13|m23|m33| 13 | */ 14 | 15 | [Fact()] 16 | public void GetDeterminantTest() 17 | { 18 | Matrix4 m = new Matrix4( 19 | 1, 2, 1, 0, 20 | 0, 3, 1, 1, 21 | -1, 0, 3, 1, 22 | 3, 1, 2, 0); 23 | 24 | Assert.Equal(16, m.GetDeterminant()); 25 | } 26 | 27 | [Fact()] 28 | public void MultiplyTest() 29 | { 30 | Matrix4 a = new Matrix4( 31 | 7, 8, 6, 2, 32 | 7, 4, 6, 9, 33 | 5, 8, 5, 3, 34 | 0, 2, 7, 8); 35 | 36 | Matrix4 b = new Matrix4( 37 | 3, 4, 0, 0, 38 | 7, 9, 2, 3, 39 | 3, 8, 4, 6, 40 | 6, 4, 2, 4); 41 | 42 | Matrix4 r = new Matrix4( 43 | 107, 156, 44, 68, 44 | 121, 148, 50, 84, 45 | 104, 144, 42, 66, 46 | 83, 106, 48, 80); 47 | 48 | Assert.Equal(r, Matrix4.Multiply(a, b)); 49 | Assert.Equal(r, a * b); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MeshSharp.Tests/MeshSharp.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | AssetRipper.MeshSharp.Tests 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MeshSharp.Tests/TransformTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace AssetRipper.MeshSharp.Tests 4 | { 5 | public class TransformTests 6 | { 7 | [Fact()] 8 | public void DecomposeTest() 9 | { 10 | System.Numerics.Matrix4x4 myMatrix = new System.Numerics.Matrix4x4((float)-1.0, (float)8.979318677493353e-11, (float)0.0, (float)0.0, (float)-8.979318677493353e-11, (float)-1.0, (float)0.0, (float)0.0, (float)0.0, (float)0.0, (float)1.0, (float)0.0, (float)-136.860107421875, (float)64.45372009277344, (float)3.8203670978546144, (float)1.0); 11 | System.Numerics.Matrix4x4.Decompose(myMatrix, out System.Numerics.Vector3 s1, out System.Numerics.Quaternion r1, out System.Numerics.Vector3 t1); 12 | 13 | Matrix4 m2 = new Matrix4( 14 | (float)-1.0, (float)8.979318677493353e-11, (float)0.0, (float)0.0, 15 | (float)-8.979318677493353e-11, (float)-1.0, (float)0.0, (float)0.0, 16 | (float)0.0, (float)0.0, (float)1.0, (float)0.0, 17 | (float)-136.860107421875, (float)64.45372009277344, (float)3.8203670978546144, (float)1.0); 18 | 19 | Transform transform = new Transform(m2.Transpose()); 20 | Transform.TryDecompose(transform, out XYZ t, out XYZ s, out Quaternion r); 21 | 22 | Assert.True(t.X == t1.X && t.Y == t1.Y && t.Z == t1.Z); 23 | Assert.True(s.X == s1.X && s.Y == s1.Y && s.Z == s1.Z); 24 | Assert.True(r.X == r1.X && r.Y == r1.Y && r.Z == r1.Z); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /MeshSharp.Tests/VectorExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace AssetRipper.MeshSharp.Tests 5 | { 6 | public class VectorExtensionsTests 7 | { 8 | [Fact()] 9 | public void GetLengthTest() 10 | { 11 | XYZ xyz = new XYZ(1, 1, 1); 12 | double result = Math.Sqrt(3); 13 | 14 | Assert.Equal(result, xyz.GetLength()); 15 | } 16 | 17 | [Fact()] 18 | public void NormalizeTest() 19 | { 20 | XYZ xyz = new XYZ(1, 1, 1); 21 | 22 | Assert.Equal(1, xyz.Normalize().GetLength()); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /MeshSharp.Tests/VectorTestCaseFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AssetRipper.MeshSharp.Tests 5 | { 6 | public class VectorTestCaseFactory 7 | { 8 | private Random _random; 9 | 10 | public VectorTestCaseFactory() 11 | { 12 | _random = new Random(); 13 | } 14 | 15 | public VectorTestCaseFactory(int seed) 16 | { 17 | _random = new Random(seed); 18 | } 19 | 20 | public (T, T, T) CreateOperationCase(Func op) 21 | where T : IVector, new() 22 | { 23 | T v1 = new T(); 24 | T v2 = new T(); 25 | T result = new T(); 26 | 27 | int n = v1.GetComponents().Length; 28 | 29 | List components1 = new List(); 30 | List components2 = new List(); 31 | List components3 = new List(); 32 | 33 | for (int i = 0; i < n; i++) 34 | { 35 | var a = _random.Next(-100, 100); 36 | var b = _random.Next(-100, 100); 37 | 38 | components1.Add(a); 39 | components2.Add(b); 40 | components3.Add(op(a, b)); 41 | } 42 | 43 | v1 = v1.SetComponents(components1.ToArray()); 44 | v2 = v2.SetComponents(components2.ToArray()); 45 | result = result.SetComponents(components3.ToArray()); 46 | 47 | return (v1, v2, result); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MeshSharp.Tests/VectorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace AssetRipper.MeshSharp.Tests 6 | { 7 | public abstract class VectorTests 8 | where T : IVector, new() 9 | { 10 | public VectorTestCaseFactory Factory { get; set; } 11 | 12 | protected readonly ITestOutputHelper output; 13 | 14 | public VectorTests(ITestOutputHelper output) 15 | { 16 | Random random = new Random(); 17 | Factory = new VectorTestCaseFactory(random.Next()); 18 | 19 | this.output = output; 20 | } 21 | 22 | [Fact] 23 | public void AdditionTest() 24 | { 25 | var test = Factory.CreateOperationCase((o, x) => o + x); 26 | writeTest(test); 27 | 28 | Assert.Equal(test.Item3, test.Item1.Add(test.Item2)); 29 | } 30 | 31 | [Fact] 32 | public void SubsctractTest() 33 | { 34 | var test = Factory.CreateOperationCase((o, x) => o - x); 35 | writeTest(test); 36 | 37 | Assert.Equal(test.Item3, test.Item1.Substract(test.Item2)); 38 | } 39 | 40 | [Fact] 41 | public void MultiplyTest() 42 | { 43 | var test = Factory.CreateOperationCase((o, x) => o * x); 44 | writeTest(test); 45 | 46 | Assert.Equal(test.Item3, test.Item1.Multiply(test.Item2)); 47 | } 48 | 49 | [Fact] 50 | public void DivideTest() 51 | { 52 | (T, T, T) test = Factory.CreateOperationCase((o, x) => o / x); 53 | writeTest(test); 54 | 55 | Assert.Equal(test.Item3, test.Item1.Divide(test.Item2)); 56 | } 57 | 58 | protected void writeTest((T, T, T) test) 59 | { 60 | output.WriteLine($"Item 1 : {test.Item1}"); 61 | output.WriteLine($"Item 2 : {test.Item2}"); 62 | output.WriteLine($"Result : {test.Item3}"); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /MeshSharp.Tests/XYZMTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | 3 | namespace AssetRipper.MeshSharp.Tests 4 | { 5 | public class XYZMTests : VectorTests 6 | { 7 | public XYZMTests(ITestOutputHelper output) : base(output) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.Tests/XYZTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | 3 | namespace AssetRipper.MeshSharp.Tests 4 | { 5 | public class XYZTests : VectorTests 6 | { 7 | public XYZTests(ITestOutputHelper output) : base(output) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp.Viewer/App.axaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MeshSharp.Viewer/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Markup.Xaml; 5 | 6 | namespace AssetRipper.MeshSharp.Viewer 7 | { 8 | public class App : Application 9 | { 10 | public override void Initialize() 11 | { 12 | AvaloniaXamlLoader.Load(this); 13 | } 14 | 15 | public override void OnFrameworkInitializationCompleted() 16 | { 17 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 18 | { 19 | desktop.MainWindow = new MainWindow 20 | { 21 | }; 22 | } 23 | 24 | base.OnFrameworkInitializationCompleted(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /MeshSharp.Viewer/MainWindow.axaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /MeshSharp.Viewer/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace AssetRipper.MeshSharp.Viewer 6 | { 7 | public class MainWindow : Window 8 | { 9 | public MainWindow() 10 | { 11 | InitializeComponent(); 12 | #if DEBUG 13 | this.AttachDevTools(); 14 | #endif 15 | } 16 | 17 | private void InitializeComponent() 18 | { 19 | AvaloniaXamlLoader.Load(this); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /MeshSharp.Viewer/MeshSharp.Viewer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0 6 | AssetRipper.MeshSharp.Viewer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MeshSharp.Viewer/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace AssetRipper.MeshSharp.Viewer 5 | { 6 | internal static class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) => BuildAvaloniaApp() 13 | .StartWithClassicDesktopLifetime(args); 14 | // Avalonia configuration, don't remove; also used by visual designer. 15 | public static AppBuilder BuildAvaloniaApp() 16 | => AppBuilder.Configure() 17 | .UsePlatformDetect() 18 | .With(new Win32PlatformOptions { UseWgl = true }) 19 | .LogToTrace(); 20 | } 21 | } -------------------------------------------------------------------------------- /MeshSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31815.197 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp", "MeshSharp\MeshSharp.csproj", "{3F391F00-A310-4681-BE94-15329D662A3D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DA7FF092-3DA1-4942-9FF6-B42F1CDBD0C6}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitignore = .gitignore 12 | LICENSE = LICENSE 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.FBX", "MeshSharp.FBX\MeshSharp.FBX.csproj", "{FE3E6007-BC34-4E40-9C65-5FA5274AA3AA}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.Examples", "MeshSharp.Examples\MeshSharp.Examples.csproj", "{55281519-37E7-42BC-B467-9D3CED013C23}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.Tests", "MeshSharp.Tests\MeshSharp.Tests.csproj", "{0DC52F2F-8232-430F-B0C9-B207F64254D7}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{006DAC54-BEE3-4878-900F-4E698B519501}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{FF66D7A6-6DCE-41DE-B30F-492988B2DF63}" 25 | ProjectSection(SolutionItems) = preProject 26 | .github\workflows\build_n_test.yml = .github\workflows\build_n_test.yml 27 | EndProjectSection 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.OBJ", "MeshSharp.OBJ\MeshSharp.OBJ.csproj", "{64101EC3-7763-43B8-ACFC-2C61CA3920FB}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.STL", "MeshSharp.STL\MeshSharp.STL.csproj", "{78B8820C-1004-46C6-88E3-0E794CD3A3AC}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.GLTF", "MeshSharp.GLTF\MeshSharp.GLTF.csproj", "{8F66FAB3-A495-4485-8CEB-FD8103EB9158}" 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.PLY", "MeshSharp.PLY\MeshSharp.PLY.csproj", "{F67F0F83-D56A-42C2-AEFA-6452C74242AB}" 36 | EndProject 37 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshSharp.PMX", "MeshSharp.PMX\MeshSharp.PMX.csproj", "{CC7D506E-C95F-41C3-9F5D-05161F988BF6}" 38 | EndProject 39 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeshSharp.Viewer", "MeshSharp.Viewer\MeshSharp.Viewer.csproj", "{F2632F2C-9AC5-4EE0-BDF3-951DDF3B2E77}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {3F391F00-A310-4681-BE94-15329D662A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {3F391F00-A310-4681-BE94-15329D662A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {3F391F00-A310-4681-BE94-15329D662A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {3F391F00-A310-4681-BE94-15329D662A3D}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {FE3E6007-BC34-4E40-9C65-5FA5274AA3AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {FE3E6007-BC34-4E40-9C65-5FA5274AA3AA}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {FE3E6007-BC34-4E40-9C65-5FA5274AA3AA}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {FE3E6007-BC34-4E40-9C65-5FA5274AA3AA}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {55281519-37E7-42BC-B467-9D3CED013C23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {55281519-37E7-42BC-B467-9D3CED013C23}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {55281519-37E7-42BC-B467-9D3CED013C23}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {55281519-37E7-42BC-B467-9D3CED013C23}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {0DC52F2F-8232-430F-B0C9-B207F64254D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {0DC52F2F-8232-430F-B0C9-B207F64254D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {0DC52F2F-8232-430F-B0C9-B207F64254D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {0DC52F2F-8232-430F-B0C9-B207F64254D7}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {64101EC3-7763-43B8-ACFC-2C61CA3920FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {64101EC3-7763-43B8-ACFC-2C61CA3920FB}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {64101EC3-7763-43B8-ACFC-2C61CA3920FB}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {64101EC3-7763-43B8-ACFC-2C61CA3920FB}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {78B8820C-1004-46C6-88E3-0E794CD3A3AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {78B8820C-1004-46C6-88E3-0E794CD3A3AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {78B8820C-1004-46C6-88E3-0E794CD3A3AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {78B8820C-1004-46C6-88E3-0E794CD3A3AC}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {8F66FAB3-A495-4485-8CEB-FD8103EB9158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {8F66FAB3-A495-4485-8CEB-FD8103EB9158}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {8F66FAB3-A495-4485-8CEB-FD8103EB9158}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {8F66FAB3-A495-4485-8CEB-FD8103EB9158}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {F67F0F83-D56A-42C2-AEFA-6452C74242AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {F67F0F83-D56A-42C2-AEFA-6452C74242AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {F67F0F83-D56A-42C2-AEFA-6452C74242AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {F67F0F83-D56A-42C2-AEFA-6452C74242AB}.Release|Any CPU.Build.0 = Release|Any CPU 79 | {CC7D506E-C95F-41C3-9F5D-05161F988BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 80 | {CC7D506E-C95F-41C3-9F5D-05161F988BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 81 | {CC7D506E-C95F-41C3-9F5D-05161F988BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 82 | {CC7D506E-C95F-41C3-9F5D-05161F988BF6}.Release|Any CPU.Build.0 = Release|Any CPU 83 | {F2632F2C-9AC5-4EE0-BDF3-951DDF3B2E77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 84 | {F2632F2C-9AC5-4EE0-BDF3-951DDF3B2E77}.Debug|Any CPU.Build.0 = Debug|Any CPU 85 | {F2632F2C-9AC5-4EE0-BDF3-951DDF3B2E77}.Release|Any CPU.ActiveCfg = Release|Any CPU 86 | {F2632F2C-9AC5-4EE0-BDF3-951DDF3B2E77}.Release|Any CPU.Build.0 = Release|Any CPU 87 | EndGlobalSection 88 | GlobalSection(SolutionProperties) = preSolution 89 | HideSolutionNode = FALSE 90 | EndGlobalSection 91 | GlobalSection(NestedProjects) = preSolution 92 | {006DAC54-BEE3-4878-900F-4E698B519501} = {DA7FF092-3DA1-4942-9FF6-B42F1CDBD0C6} 93 | {FF66D7A6-6DCE-41DE-B30F-492988B2DF63} = {006DAC54-BEE3-4878-900F-4E698B519501} 94 | EndGlobalSection 95 | GlobalSection(ExtensibilityGlobals) = postSolution 96 | SolutionGuid = {B96D2DF1-9226-4CEE-BF70-43C8318F59DC} 97 | EndGlobalSection 98 | EndGlobal 99 | -------------------------------------------------------------------------------- /MeshSharp/Color.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public struct Color 4 | { 5 | public byte R { get; set; } 6 | public byte G { get; set; } 7 | public byte B { get; set; } 8 | public byte? A { get; set; } 9 | 10 | public Color(byte r, byte g, byte b) 11 | { 12 | R = r; 13 | G = g; 14 | B = b; 15 | A = null; 16 | } 17 | 18 | public Color(byte r, byte g, byte b, byte a) : this(r, g, b) 19 | { 20 | A = a; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return string.Format($"R:{R} G:{G} B:{B} A:{A}"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Camera.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements 2 | { 3 | public class Camera : Element 4 | { 5 | #region Enums 6 | /// 7 | /// Camera's projection types 8 | /// 9 | public enum ProjectionType 10 | { 11 | /// 12 | /// The camera uses perspective projection 13 | /// 14 | Perspective, 15 | /// 16 | /// The camera uses orthographic projection 17 | /// 18 | Orthographic, 19 | } 20 | #endregion 21 | 22 | public XYZ Position { get; set; } 23 | 24 | public XYZ UpVector { get; set; } 25 | 26 | public double FieldOfView { get; set; } 27 | 28 | public double FieldOfViewX { get; set; } 29 | 30 | public double FieldOfViewY { get; set; } 31 | 32 | public Camera() : base() { } 33 | 34 | public Camera(string name) : base(name) { } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Element.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements 2 | { 3 | public class Element 4 | { 5 | /// 6 | /// Name of the element. 7 | /// 8 | public string Name { get; set; } 9 | 10 | public PropertyCollection Properties { get; } 11 | 12 | internal ulong? _id = null; 13 | 14 | /// 15 | /// Default constructor. 16 | /// 17 | public Element() 18 | { 19 | Name = string.Empty; 20 | Properties = new PropertyCollection(this); 21 | 22 | _id = Utils.CreateId(); 23 | } 24 | 25 | public Element(string name) : this() 26 | { 27 | Name = name; 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return $"{this.GetType().Name}:{Name}"; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Geometry.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements.Geometries.Layers; 2 | using System.Collections.Generic; 3 | 4 | namespace AssetRipper.MeshSharp.Elements.Geometries 5 | { 6 | public class Geometry : Element 7 | { 8 | public List Layers { get; } = new List(); 9 | 10 | public Geometry() : base() { } 11 | 12 | public Geometry(string name) : base(name) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerCollection.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerCollection //TODO: Organize and create different layers for a geometry element 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElement.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public abstract class LayerElement 4 | { 5 | public string Name { get; set; } = string.Empty; 6 | public MappingMode MappingInformationType { get; set; } 7 | public ReferenceMode ReferenceInformationType { get; set; } 8 | 9 | protected Geometry _owner; 10 | 11 | public LayerElement(Geometry owner) 12 | { 13 | _owner = owner; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementBinormal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | public class LayerElementBinormal : LayerElement 6 | { 7 | public List BiNormals { get; internal set; } = new List(); 8 | public LayerElementBinormal(Geometry owner) : base(owner) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementEdgeCrease.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementEdgeCrease : LayerElement 4 | { 5 | public LayerElementEdgeCrease(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementHole.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementHole : LayerElement 4 | { 5 | public LayerElementHole(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementMaterial.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | public class LayerElementMaterial : LayerElement 6 | { 7 | public List Materials { get; } = new List(); 8 | public LayerElementMaterial(Geometry owner) : base(owner) 9 | { 10 | MappingInformationType = MappingMode.AllSame; 11 | ReferenceInformationType = ReferenceMode.IndexToDirect; 12 | Materials.Add(0); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementNormal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | public class LayerElementNormal : LayerElement 6 | { 7 | public List Normals { get; internal set; } = new List(); 8 | public LayerElementNormal(Geometry owner) : base(owner) { } 9 | 10 | /// 11 | /// Calculate a normal vector for each triangle in the mesh 12 | /// 13 | /// True if successful and false otherwise 14 | public bool CalculateFlatNormals() 15 | { 16 | if (!(_owner is Mesh mesh)) 17 | return false; 18 | 19 | Normals.Clear(); 20 | 21 | MappingInformationType = MappingMode.ByPolygon; 22 | ReferenceInformationType = ReferenceMode.Direct; 23 | 24 | foreach (Polygon item in mesh.Polygons) 25 | { 26 | XYZ normal = XYZ.Zero; 27 | foreach (Triangle triangle in item.ConvertToTriangles()) 28 | { 29 | XYZ subnormal = XYZ.FindNormal( 30 | mesh.Vertices[(int)triangle.Index0], 31 | mesh.Vertices[(int)triangle.Index1], 32 | mesh.Vertices[(int)triangle.Index2]); 33 | normal += subnormal; 34 | } 35 | 36 | Normals.Add(normal.Normalize()); 37 | } 38 | return true; 39 | } 40 | 41 | /// 42 | /// Calculate a normal vector for each vertex in the mesh.
43 | /// This will make the mesh appear very smooth, but may cause visual issues where defined edges are intended. 44 | ///
45 | /// True if successful and false otherwise 46 | public bool CalculateVertexNormals() 47 | { 48 | if (!(_owner is Mesh mesh)) 49 | return false; 50 | 51 | Normals.Clear(); 52 | MappingInformationType = MappingMode.ByPolygonVertex; 53 | ReferenceInformationType = ReferenceMode.Direct; 54 | 55 | //Initialize an array for holding the normals during calculation 56 | int vertexCount = mesh.Vertices.Count; 57 | var vertexNormals = new XYZ[vertexCount]; 58 | 59 | //Calculate the normals 60 | foreach (Polygon item in mesh.Polygons) 61 | { 62 | foreach (Triangle triangle in item.ConvertToTriangles()) 63 | { 64 | int index0 = (int)triangle.Index0; 65 | int index1 = (int)triangle.Index1; 66 | int index2 = (int)triangle.Index2; 67 | XYZ flatnormal = XYZ.FindNormal( 68 | mesh.Vertices[index0], 69 | mesh.Vertices[index1], 70 | mesh.Vertices[index2]); 71 | vertexNormals[index0] += flatnormal; 72 | vertexNormals[index1] += flatnormal; 73 | vertexNormals[index2] += flatnormal; 74 | } 75 | } 76 | 77 | //Normalize the normals 78 | for(int i = 0; i < vertexCount; i++) 79 | { 80 | vertexNormals[i] = vertexNormals[i].Normalize(); 81 | } 82 | 83 | //Reorder by polygon vertex 84 | foreach (var polygon in mesh.Polygons) 85 | { 86 | foreach (uint index in polygon.Indices) 87 | { 88 | Normals.Add(vertexNormals[index]); 89 | } 90 | } 91 | 92 | return true; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementPolygonGroup.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementPolygonGroup : LayerElement 4 | { 5 | public LayerElementPolygonGroup(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementSmoothing.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementSmoothing : LayerElement 4 | { 5 | public LayerElementSmoothing(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementSpecular.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementSpecular : LayerElement 4 | { 5 | public LayerElementSpecular(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementTangent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | public class LayerElementTangent : LayerElement 6 | { 7 | public List Tangents { get; set; } = new List(); 8 | public LayerElementTangent(Geometry owner) : base(owner) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementUV.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | public class LayerElementUV : LayerElement 6 | { 7 | public List UV { get; internal set; } = new List(); 8 | public List UVIndex { get; internal set; } = new List(); 9 | public LayerElementUV(Geometry owner) : base(owner) { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementUserData.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementUserData : LayerElement 4 | { 5 | public LayerElementUserData(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementVertexColor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 4 | { 5 | /// 6 | /// LayerElementColor 7 | /// 8 | public class LayerElementVertexColor : LayerElement 9 | { 10 | public List Colors { get; internal set; } = new List(); 11 | public List ColorIndex { get; internal set; } = new List(); 12 | public LayerElementVertexColor(Geometry owner) : base(owner) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementVertexCrease.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementVertexCrease : LayerElement 4 | { 5 | public LayerElementVertexCrease(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementVisibility.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementVisibility : LayerElement 4 | { 5 | public LayerElementVisibility(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/LayerElementWeight.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public class LayerElementWeight : LayerElement 4 | { 5 | public LayerElementWeight(Geometry owner) : base(owner) 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/MappingMode.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public enum MappingMode 4 | { 5 | None, 6 | ByControlPoint, 7 | ByPolygonVertex, 8 | ByPolygon, 9 | ByEdge, 10 | AllSame, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Layers/ReferenceMode.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements.Geometries.Layers 2 | { 3 | public enum ReferenceMode 4 | { 5 | /// 6 | /// Direct values such as floating point numbers 7 | /// 8 | Direct, 9 | Index, 10 | IndexToDirect, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/Mesh.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries 4 | { 5 | public class Mesh : Geometry 6 | { 7 | public List Vertices { get; internal set; } = new List(); 8 | public List Polygons { get; internal set; } = new List(); 9 | 10 | public Mesh() : base() { } 11 | 12 | public Mesh(string name) : base(name) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Geometries/MeshUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements.Geometries 4 | { 5 | internal static class MeshUtils 6 | { 7 | internal static List GetAllMeshes(Scene scene) 8 | { 9 | List result = new List(); 10 | foreach (Node node in scene.Nodes) 11 | { 12 | result.AddRange(GetAllMeshes(node, true)); 13 | } 14 | return result; 15 | } 16 | 17 | internal static List GetAllMeshes(Node parentNode, bool recursively) 18 | { 19 | List result = new List(); 20 | foreach (Element child in parentNode.Children) 21 | { 22 | if (child is Mesh mesh) 23 | result.Add(mesh); 24 | else if (recursively && child is Node childNode) 25 | result.AddRange(GetAllMeshes(childNode, true)); 26 | } 27 | return result; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Material.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp.Elements 2 | { 3 | public class Material : Element 4 | { 5 | public string ShadingModel { get; set; } = "unknown"; 6 | 7 | public int? MultiLayer { get; set; } 8 | 9 | public Color AmbientColor { get; set; } 10 | public Color DiffuseColor { get; set; } 11 | public Color SpecularColor { get; set; } 12 | public double SpecularFactor { get; set; } 13 | public double ShininessExponent { get; set; } 14 | public double TransparencyFactor { get; set; } 15 | public Color EmissiveColor { get; set; } 16 | public double EmissiveFactor { get; set; } 17 | 18 | public Material() : base() { } 19 | 20 | public Material(string name) : base(name) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Node.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace AssetRipper.MeshSharp.Elements 5 | { 6 | public class Node : Element 7 | { 8 | public bool? MultiLayer { get; set; } 9 | public bool? MultiTake { get; set; } 10 | public bool Shading { get; set; } = true; 11 | public string Culling { get; set; } = "CullingOff"; 12 | 13 | public Transform Transform { get; set; } = new Transform(); 14 | 15 | public List Children { get; set; } = new List(); 16 | 17 | public Node() : base() { } 18 | 19 | public Node(string name) : base(name) { } 20 | 21 | public IEnumerable GetElements() 22 | { 23 | return Children.Where(e => e is T).Cast(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MeshSharp/Elements/Scene.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AssetRipper.MeshSharp.Elements 4 | { 5 | public class Scene : Element 6 | { 7 | public List Nodes { get; } = new List(); 8 | 9 | public Scene() : base() { } 10 | public Scene(string name) : base(name) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MeshSharp/IO/BinaryReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AssetRipper.MeshSharp.IO 4 | { 5 | internal static class BinaryReaderExtensions 6 | { 7 | public static T ReadStruct(this BinaryReader reader) where T : IBinaryReadable, new() 8 | { 9 | T result = new(); 10 | result.Read(reader); 11 | return result; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MeshSharp/IO/BinaryWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AssetRipper.MeshSharp.IO 4 | { 5 | internal static class BinaryWriterExtensions 6 | { 7 | public static void WriteStruct(this BinaryWriter writer, T value) where T : IBinaryWriteable 8 | { 9 | value.Write(writer); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MeshSharp/IO/EndianReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace AssetRipper.MeshSharp.IO 7 | { 8 | internal class EndianReader : BinaryReader 9 | { 10 | public EndianReader(Stream input) : base(input) { } 11 | public EndianReader(Stream input, Encoding encoding) : base(input, encoding) { } 12 | public EndianReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen) { } 13 | 14 | private bool isBigEndian = false; 15 | public bool IsBigEndian 16 | { 17 | get => isBigEndian; 18 | set => isBigEndian = value; 19 | } 20 | 21 | public override short ReadInt16() 22 | { 23 | if (isBigEndian) 24 | return BinaryPrimitives.ReadInt16BigEndian(base.ReadBytes(2)); 25 | else 26 | return base.ReadInt16(); 27 | } 28 | 29 | public override ushort ReadUInt16() 30 | { 31 | if (isBigEndian) 32 | return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); 33 | else 34 | return base.ReadUInt16(); 35 | } 36 | 37 | public override int ReadInt32() 38 | { 39 | if (isBigEndian) 40 | return BinaryPrimitives.ReadInt32BigEndian(base.ReadBytes(4)); 41 | else 42 | return base.ReadInt32(); 43 | } 44 | 45 | public override uint ReadUInt32() 46 | { 47 | if (isBigEndian) 48 | return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); 49 | else 50 | return base.ReadUInt32(); 51 | } 52 | 53 | public override long ReadInt64() 54 | { 55 | if (isBigEndian) 56 | return BinaryPrimitives.ReadInt64BigEndian(base.ReadBytes(8)); 57 | else 58 | return base.ReadInt64(); 59 | } 60 | 61 | public override ulong ReadUInt64() 62 | { 63 | if (isBigEndian) 64 | return BinaryPrimitives.ReadUInt64BigEndian(base.ReadBytes(8)); 65 | else 66 | return base.ReadUInt64(); 67 | } 68 | 69 | public override Half ReadHalf() 70 | { 71 | if (isBigEndian) 72 | return BinaryPrimitives.ReadHalfBigEndian(base.ReadBytes(2)); 73 | else 74 | return base.ReadHalf(); 75 | } 76 | 77 | public override float ReadSingle() 78 | { 79 | if (isBigEndian) 80 | return BinaryPrimitives.ReadSingleBigEndian(base.ReadBytes(4)); 81 | else 82 | return base.ReadSingle(); 83 | } 84 | 85 | public override double ReadDouble() 86 | { 87 | if (isBigEndian) 88 | return BinaryPrimitives.ReadDoubleBigEndian(base.ReadBytes(8)); 89 | else 90 | return base.ReadDouble(); 91 | } 92 | 93 | public override decimal ReadDecimal() 94 | { 95 | if (isBigEndian) 96 | throw new NotSupportedException(); 97 | else 98 | return base.ReadDecimal(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /MeshSharp/IO/EndianWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace AssetRipper.MeshSharp.IO 7 | { 8 | internal class EndianWriter : BinaryWriter 9 | { 10 | public EndianWriter(Stream output) : base(output) { } 11 | public EndianWriter(Stream output, Encoding encoding) : base(output, encoding) { } 12 | public EndianWriter(Stream output, Encoding encoding, bool leaveOpen) : base(output, encoding, leaveOpen) { } 13 | protected EndianWriter() { } 14 | 15 | private bool isBigEndian = false; 16 | public bool IsBigEndian 17 | { 18 | get => isBigEndian; 19 | set => isBigEndian = value; 20 | } 21 | 22 | public override void Write(short value) 23 | { 24 | if (isBigEndian) 25 | { 26 | byte[] bytes = new byte[2]; 27 | BinaryPrimitives.WriteInt16BigEndian(bytes, value); 28 | base.Write(bytes); 29 | } 30 | else 31 | { 32 | base.Write(value); 33 | } 34 | } 35 | 36 | public override void Write(ushort value) 37 | { 38 | if (isBigEndian) 39 | { 40 | byte[] bytes = new byte[2]; 41 | BinaryPrimitives.WriteUInt16BigEndian(bytes, value); 42 | base.Write(bytes); 43 | } 44 | else 45 | { 46 | base.Write(value); 47 | } 48 | } 49 | 50 | public override void Write(int value) 51 | { 52 | if (isBigEndian) 53 | { 54 | byte[] bytes = new byte[4]; 55 | BinaryPrimitives.WriteInt32BigEndian(bytes, value); 56 | base.Write(bytes); 57 | } 58 | else 59 | { 60 | base.Write(value); 61 | } 62 | } 63 | 64 | public override void Write(uint value) 65 | { 66 | if (isBigEndian) 67 | { 68 | byte[] bytes = new byte[4]; 69 | BinaryPrimitives.WriteUInt32BigEndian(bytes, value); 70 | base.Write(bytes); 71 | } 72 | else 73 | { 74 | base.Write(value); 75 | } 76 | } 77 | 78 | public override void Write(long value) 79 | { 80 | if (isBigEndian) 81 | { 82 | byte[] bytes = new byte[8]; 83 | BinaryPrimitives.WriteInt64BigEndian(bytes, value); 84 | base.Write(bytes); 85 | } 86 | else 87 | { 88 | base.Write(value); 89 | } 90 | } 91 | 92 | public override void Write(ulong value) 93 | { 94 | if (isBigEndian) 95 | { 96 | byte[] bytes = new byte[8]; 97 | BinaryPrimitives.WriteUInt64BigEndian(bytes, value); 98 | base.Write(bytes); 99 | } 100 | else 101 | { 102 | base.Write(value); 103 | } 104 | } 105 | 106 | public override void Write(Half value) 107 | { 108 | if (isBigEndian) 109 | { 110 | byte[] bytes = new byte[2]; 111 | BinaryPrimitives.WriteHalfBigEndian(bytes, value); 112 | base.Write(bytes); 113 | } 114 | else 115 | { 116 | base.Write(value); 117 | } 118 | } 119 | 120 | public override void Write(float value) 121 | { 122 | if (isBigEndian) 123 | { 124 | byte[] bytes = new byte[4]; 125 | BinaryPrimitives.WriteSingleBigEndian(bytes, value); 126 | base.Write(bytes); 127 | } 128 | else 129 | { 130 | base.Write(value); 131 | } 132 | } 133 | 134 | public override void Write(double value) 135 | { 136 | if (isBigEndian) 137 | { 138 | byte[] bytes = new byte[8]; 139 | BinaryPrimitives.WriteDoubleBigEndian(bytes, value); 140 | base.Write(bytes); 141 | } 142 | else 143 | { 144 | base.Write(value); 145 | } 146 | } 147 | 148 | public override void Write(decimal value) 149 | { 150 | if (isBigEndian) 151 | { 152 | throw new NotSupportedException(); 153 | } 154 | else 155 | { 156 | base.Write(value); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /MeshSharp/IO/IBinaryReadable.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AssetRipper.MeshSharp.IO 4 | { 5 | internal interface IBinaryReadable 6 | { 7 | void Read(BinaryReader reader); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/IO/IBinaryWriteable.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AssetRipper.MeshSharp.IO 4 | { 5 | internal interface IBinaryWriteable 6 | { 7 | void Write(BinaryWriter writer); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MeshSharp/IO/MeshReaderBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace AssetRipper.MeshSharp.IO 5 | { 6 | internal abstract class MeshReaderBase : IDisposable 7 | { 8 | private bool disposedValue; 9 | protected Stream stream; 10 | 11 | public MeshReaderBase(Stream stream) 12 | { 13 | this.stream = stream; 14 | } 15 | public MeshReaderBase(string path) : this(File.OpenRead(path)) { } 16 | 17 | #region Disposal 18 | protected virtual void Dispose(bool disposing) 19 | { 20 | if (!disposedValue) 21 | { 22 | if (disposing) 23 | { 24 | stream?.Dispose(); 25 | } 26 | 27 | stream = null; 28 | disposedValue = true; 29 | } 30 | } 31 | 32 | public void Dispose() 33 | { 34 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 35 | Dispose(disposing: true); 36 | GC.SuppressFinalize(this); 37 | } 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MeshSharp/IO/MeshWriterBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace AssetRipper.MeshSharp.IO 5 | { 6 | internal abstract class MeshWriterBase : IDisposable 7 | { 8 | private bool disposedValue; 9 | protected Stream stream; 10 | 11 | public MeshWriterBase(Stream stream) 12 | { 13 | this.stream = stream; 14 | } 15 | public MeshWriterBase(string path) : this(File.Create(path)) { } 16 | 17 | #region Disposal 18 | protected virtual void Dispose(bool disposing) 19 | { 20 | if (!disposedValue) 21 | { 22 | if (disposing) 23 | { 24 | stream?.Dispose(); 25 | } 26 | 27 | stream = null; 28 | disposedValue = true; 29 | } 30 | } 31 | 32 | public void Dispose() 33 | { 34 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 35 | Dispose(disposing: true); 36 | GC.SuppressFinalize(this); 37 | } 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MeshSharp/Math/IVector.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public interface IVector 4 | { 5 | /// 6 | /// Get the diferent components of a dimensional vector. 7 | /// 8 | /// Array with the vector components. 9 | double[] GetComponents(); 10 | 11 | /// 12 | /// Create a new instance of the same type with the given components. 13 | /// 14 | /// Components to create the new IVector 15 | /// A new instance of a dimensional vector. 16 | T SetComponents(double[] components); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MeshSharp/Math/Matrix4.Operators.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp 4 | { 5 | public partial struct Matrix4 6 | { 7 | #region Move to transform 8 | /// 9 | /// Creates a translation matrix. 10 | /// 11 | /// The amount to translate in each axis. 12 | /// The translation matrix. 13 | public static Matrix4 CreateTranslation(XYZ position) 14 | { 15 | return Matrix4.CreateTranslation(position.X, position.Y, position.Z); 16 | } 17 | 18 | /// 19 | /// Creates a translation matrix. 20 | /// 21 | /// The amount to translate on the X-axis. 22 | /// The amount to translate on the Y-axis. 23 | /// The amount to translate on the Z-axis. 24 | /// The translation matrix. 25 | public static Matrix4 CreateTranslation(double xPosition, double yPosition, double zPosition) 26 | { 27 | Matrix4 result; 28 | 29 | result.m00 = 1.0f; 30 | result.m01 = 0.0f; 31 | result.m02 = 0.0f; 32 | result.m03 = 0.0f; 33 | result.m10 = 0.0f; 34 | result.m11 = 1.0f; 35 | result.m12 = 0.0f; 36 | result.m13 = 0.0f; 37 | result.m20 = 0.0f; 38 | result.m21 = 0.0f; 39 | result.m22 = 1.0f; 40 | result.m23 = 0.0f; 41 | 42 | result.m30 = xPosition; 43 | result.m31 = yPosition; 44 | result.m32 = zPosition; 45 | result.m33 = 1.0f; 46 | 47 | return result; 48 | } 49 | 50 | /// 51 | /// Creates a scaling matrix. 52 | /// 53 | /// The vector containing the amount to scale by on each axis. 54 | /// The scaling matrix. 55 | public static Matrix4 CreateScale(XYZ scales) 56 | { 57 | return CreateScale(scales, XYZ.Zero); 58 | } 59 | 60 | /// 61 | /// Creates a scaling matrix with a center point. 62 | /// 63 | /// The vector containing the amount to scale by on each axis. 64 | /// The center point. 65 | /// The scaling matrix. 66 | public static Matrix4 CreateScale(XYZ scales, XYZ centerPoint) 67 | { 68 | Matrix4 result; 69 | 70 | double tx = centerPoint.X * (1 - scales.X); 71 | double ty = centerPoint.Y * (1 - scales.Y); 72 | double tz = centerPoint.Z * (1 - scales.Z); 73 | 74 | result.m00 = scales.X; 75 | result.m01 = 0.0f; 76 | result.m02 = 0.0f; 77 | result.m03 = 0.0f; 78 | result.m10 = 0.0f; 79 | result.m11 = scales.Y; 80 | result.m12 = 0.0f; 81 | result.m13 = 0.0f; 82 | result.m20 = 0.0f; 83 | result.m21 = 0.0f; 84 | result.m22 = scales.Z; 85 | result.m23 = 0.0f; 86 | result.m30 = tx; 87 | result.m31 = ty; 88 | result.m32 = tz; 89 | result.m33 = 1.0f; 90 | 91 | return result; 92 | } 93 | 94 | /// 95 | /// Creates a uniform scaling matrix that scales equally on each axis. 96 | /// 97 | /// The uniform scaling factor. 98 | /// The scaling matrix. 99 | public static Matrix4 CreateScale(double scale) 100 | { 101 | return CreateScale(scale, XYZ.Zero); 102 | } 103 | 104 | /// 105 | /// Creates a uniform scaling matrix that scales equally on each axis with a center point. 106 | /// 107 | /// The uniform scaling factor. 108 | /// The center point. 109 | /// The scaling matrix. 110 | public static Matrix4 CreateScale(double scale, XYZ centerPoint) 111 | { 112 | return CreateScale(new XYZ(scale), centerPoint); 113 | } 114 | 115 | /// 116 | /// Creates a rotation matrix. 117 | /// 118 | /// 119 | /// 120 | /// 121 | /// 122 | public static Matrix4 CreateRotationMatrix(double x, double y, double z) 123 | { 124 | double cosx = Math.Cos(x); 125 | double cosy = Math.Cos(y); 126 | double cosz = Math.Cos(z); 127 | 128 | double sinx = Math.Sin(x); 129 | double siny = Math.Sin(y); 130 | double sinz = Math.Sin(z); 131 | 132 | //X rotation 133 | Matrix4 rx = new Matrix4( 134 | 1, 0, 0, 0, 135 | 0, cosx, sinx, 0, 136 | 0, -sinx, cosx, 0, 137 | 0, 0, 0, 1); 138 | 139 | //Y rotation 140 | Matrix4 ry = new Matrix4( 141 | cosy, 0, -siny, 0, 142 | 0, 1, 0, 0, 143 | siny, 0, cosy, 0, 144 | 0, 0, 0, 1); 145 | 146 | //Z rotation 147 | Matrix4 rz = new Matrix4( 148 | cosz, -sinz, 0, 0, 149 | sinz, cosz, 0, 0, 150 | 0, 0, 1, 0, 151 | 0, 0, 0, 1); 152 | 153 | return rx * ry * rz; 154 | } 155 | 156 | /// 157 | /// Creates a matrix that rotates around an arbitrary vector. 158 | /// 159 | /// The axis to rotate around. 160 | /// The angle to rotate around the given axis, in radians. 161 | /// The rotation matrix. 162 | public static Matrix4 CreateFromAxisAngle(XYZ axis, double angle) 163 | { 164 | // a: angle 165 | // x, y, z: unit vector for axis. 166 | // 167 | // Rotation matrix M can compute by using below equation. 168 | // 169 | // T T 170 | // M = uu + (cos a)( I-uu ) + (sin a)S 171 | // 172 | // Where: 173 | // 174 | // u = ( x, y, z ) 175 | // 176 | // [ 0 -z y ] 177 | // S = [ z 0 -x ] 178 | // [ -y x 0 ] 179 | // 180 | // [ 1 0 0 ] 181 | // I = [ 0 1 0 ] 182 | // [ 0 0 1 ] 183 | // 184 | // 185 | // [ xx+cosa*(1-xx) yx-cosa*yx-sina*z zx-cosa*xz+sina*y ] 186 | // M = [ xy-cosa*yx+sina*z yy+cosa(1-yy) yz-cosa*yz-sina*x ] 187 | // [ zx-cosa*zx-sina*y zy-cosa*zy+sina*x zz+cosa*(1-zz) ] 188 | // 189 | double x = axis.X, y = axis.Y, z = axis.Z; 190 | double sa = (double)Math.Sin(angle), ca = (double)Math.Cos(angle); 191 | double xx = x * x, yy = y * y, zz = z * z; 192 | double xy = x * y, xz = x * z, yz = y * z; 193 | 194 | Matrix4 result = new Matrix4(); 195 | 196 | result.m00 = xx + ca * (1.0f - xx); 197 | result.m01 = xy - ca * xy + sa * z; 198 | result.m02 = xz - ca * xz - sa * y; 199 | result.m03 = 0.0f; 200 | result.m10 = xy - ca * xy - sa * z; 201 | result.m11 = yy + ca * (1.0f - yy); 202 | result.m12 = yz - ca * yz + sa * x; 203 | result.m13 = 0.0f; 204 | result.m20 = xz - ca * xz + sa * y; 205 | result.m21 = yz - ca * yz - sa * x; 206 | result.m22 = zz + ca * (1.0f - zz); 207 | result.m23 = 0.0f; 208 | result.m30 = 0.0f; 209 | result.m31 = 0.0f; 210 | result.m32 = 0.0f; 211 | result.m33 = 1.0f; 212 | 213 | return result; 214 | } 215 | 216 | /// 217 | /// Builds a matrix that scales along the x-axis, y-axis, and z-axis. 218 | /// 219 | public static Matrix4 CreateScalingMatrix(double x, double y, double z) 220 | { 221 | return new Matrix4( 222 | x, 0.0, 0.0, 0.0, 223 | 0.0, y, 0.0, 0.0, 224 | 0.0, 0.0, z, 0.0, 225 | 0.0, 0.0, 0.0, 1.0); 226 | } 227 | #endregion 228 | 229 | /// 230 | /// Multiplies two matrices. 231 | /// 232 | /// A new instance containing the result. 233 | public static Matrix4 Multiply(Matrix4 a, Matrix4 b) 234 | { 235 | Matrix4 result = new Matrix4(); 236 | 237 | var rows = a.GetRows(); 238 | var cols = b.GetCols(); 239 | 240 | result.m00 = rows[0].Dot(cols[0]); 241 | result.m10 = rows[0].Dot(cols[1]); 242 | result.m20 = rows[0].Dot(cols[2]); 243 | result.m30 = rows[0].Dot(cols[3]); 244 | result.m01 = rows[1].Dot(cols[0]); 245 | result.m11 = rows[1].Dot(cols[1]); 246 | result.m21 = rows[1].Dot(cols[2]); 247 | result.m31 = rows[1].Dot(cols[3]); 248 | result.m02 = rows[2].Dot(cols[0]); 249 | result.m12 = rows[2].Dot(cols[1]); 250 | result.m22 = rows[2].Dot(cols[2]); 251 | result.m32 = rows[2].Dot(cols[3]); 252 | result.m03 = rows[3].Dot(cols[0]); 253 | result.m13 = rows[3].Dot(cols[1]); 254 | result.m23 = rows[3].Dot(cols[2]); 255 | result.m33 = rows[3].Dot(cols[3]); 256 | 257 | return result; 258 | } 259 | 260 | /// 261 | /// Multiplies two matrices. 262 | /// 263 | /// A new instance containing the result. 264 | public static Matrix4 operator *(Matrix4 a, Matrix4 b) 265 | { 266 | return Matrix4.Multiply(a, b); 267 | } 268 | 269 | /// Multiply the matrix and a coordinate 270 | /// 271 | /// 272 | /// Result matrix 273 | public static XYZ operator *(Matrix4 matrix, XYZ value) 274 | { 275 | XYZM xyzm = new XYZM(value.X, value.Y, value.Z, 1); 276 | var rows = matrix.GetRows(); 277 | 278 | XYZ result = new XYZ 279 | { 280 | X = rows[0].Dot(xyzm), 281 | Y = rows[1].Dot(xyzm), 282 | Z = rows[2].Dot(xyzm) 283 | }; 284 | 285 | return result; 286 | } 287 | 288 | /// Multiply the matrix and XYZM 289 | /// 290 | /// 291 | /// Result matrix 292 | public static XYZM operator *(Matrix4 matrix, XYZM v) 293 | { 294 | return new XYZM( 295 | matrix.m00 * v.X + matrix.m10 * v.Y + matrix.m20 * v.Z + matrix.m30 * v.M, 296 | matrix.m01 * v.X + matrix.m11 * v.Y + matrix.m21 * v.Z + matrix.m31 * v.M, 297 | matrix.m02 * v.X + matrix.m12 * v.Y + matrix.m22 * v.Z + matrix.m32 * v.M, 298 | matrix.m03 * v.X + matrix.m13 * v.Y + matrix.m23 * v.Z + matrix.m33 * v.M); 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /MeshSharp/Math/Quaternion.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public struct Quaternion 4 | { 5 | /// 6 | /// Specifies the X-value of the vector component of the Quaternion. 7 | /// 8 | public double X; 9 | /// 10 | /// Specifies the Y-value of the vector component of the Quaternion. 11 | /// 12 | public double Y; 13 | /// 14 | /// Specifies the Z-value of the vector component of the Quaternion. 15 | /// 16 | public double Z; 17 | /// 18 | /// Specifies the rotation component of the Quaternion. 19 | /// 20 | public double W; 21 | 22 | /// 23 | /// Returns a Quaternion representing no rotation. 24 | /// 25 | public static Quaternion Identity 26 | { 27 | get { return new Quaternion(0, 0, 0, 1); } 28 | } 29 | 30 | /// 31 | /// Constructs a Quaternion from the given components. 32 | /// 33 | /// The X component of the Quaternion. 34 | /// The Y component of the Quaternion. 35 | /// The Z component of the Quaternion. 36 | /// The W component of the Quaternion. 37 | public Quaternion(double x, double y, double z, double w) 38 | { 39 | this.X = x; 40 | this.Y = y; 41 | this.Z = z; 42 | this.W = w; 43 | } 44 | 45 | /// 46 | /// Constructs a Quaternion from the given vector and rotation parts. 47 | /// 48 | /// The vector part of the Quaternion. 49 | /// The rotation part of the Quaternion. 50 | public Quaternion(XYZ vectorPart, double scalarPart) 51 | { 52 | X = vectorPart.X; 53 | Y = vectorPart.Y; 54 | Z = vectorPart.Z; 55 | W = scalarPart; 56 | } 57 | 58 | /// 59 | public override string ToString() 60 | { 61 | return $"{X},{Y},{Z},{W}"; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MeshSharp/Math/VectorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp 4 | { 5 | public static class VectorExtensions 6 | { 7 | /// 8 | /// Returns the length of the vector. 9 | /// 10 | /// The vector's length. 11 | public static double GetLength(this T vector) where T : IVector 12 | { 13 | double length = 0; 14 | 15 | foreach (var item in vector.GetComponents()) 16 | { 17 | length += item * item; 18 | } 19 | 20 | return Math.Sqrt(length); 21 | } 22 | 23 | /// 24 | /// Returns a vector with the same direction as the given vector, but with a length of 1.
25 | /// If a zero vector is provided, normalization is skipped. 26 | ///
27 | /// The vector to normalize. 28 | /// The normalized vector. 29 | public static T Normalize(this T vector) where T : IVector, new() 30 | { 31 | double length = vector.GetLength(); 32 | double[] components = vector.GetComponents(); 33 | 34 | if(length != 0) 35 | { 36 | for (int i = 0; i < components.Length; i++) 37 | { 38 | components[i] /= length; 39 | } 40 | } 41 | 42 | return new T().SetComponents(components); 43 | } 44 | 45 | /// 46 | /// Returns the dot product of two vectors. 47 | /// 48 | /// The first vector. 49 | /// The second vector. 50 | /// The dot product. 51 | public static double Dot(this T left, T right) where T : IVector 52 | { 53 | var components1 = left.GetComponents(); 54 | var components2 = right.GetComponents(); 55 | double result = 0; 56 | 57 | for (int i = 0; i < components1.Length; i++) 58 | { 59 | result += components1[i] * components2[i]; 60 | } 61 | 62 | return result; 63 | } 64 | 65 | /// 66 | /// Returns a boolean indicating whether the two given vectors are equal. 67 | /// 68 | /// The first vector to compare. 69 | /// The second vector to compare. 70 | /// True if the vectors are equal; False otherwise. 71 | public static bool IsEqual(this T left, T right) where T : IVector 72 | { 73 | var components1 = left.GetComponents(); 74 | var components2 = right.GetComponents(); 75 | 76 | for (int i = 0; i < components1.Length; i++) 77 | { 78 | if (components1[i] != components2[i]) 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | /// 86 | /// Adds two vectors together. 87 | /// 88 | /// The first source vector. 89 | /// The second source vector. 90 | /// The summed vector. 91 | public static T Add(this T left, T right) where T : IVector, new() 92 | { 93 | return applyFunctionByComponentIndex(left, right, (o, x) => o + x); 94 | } 95 | 96 | /// 97 | /// Subtracts the second vector from the first. 98 | /// 99 | /// The first source vector. 100 | /// The second source vector. 101 | /// The difference vector. 102 | public static T Substract(this T left, T right) where T : IVector, new() 103 | { 104 | return applyFunctionByComponentIndex(left, right, (o, x) => o - x); 105 | } 106 | 107 | /// 108 | /// Multiplies two vectors together. 109 | /// 110 | /// The first source vector. 111 | /// The second source vector. 112 | /// The product vector. 113 | public static T Multiply(this T left, T right) where T : IVector, new() 114 | { 115 | return applyFunctionByComponentIndex(left, right, (o, x) => o * x); 116 | } 117 | 118 | /// 119 | /// Divides the first vector by the second. 120 | /// 121 | /// The first source vector. 122 | /// The second source vector. 123 | /// The vector resulting from the division. 124 | public static T Divide(this T left, T right) where T : IVector, new() 125 | { 126 | return applyFunctionByComponentIndex(left, right, (o, x) => o / x); 127 | } 128 | 129 | /// 130 | /// Applies a function in all the components of a vector by order 131 | /// 132 | /// 133 | /// 134 | /// 135 | /// 136 | /// 137 | private static T applyFunctionByComponentIndex(this T left, T right, Func op) where T : IVector, new() 138 | { 139 | double[] components1 = left.GetComponents(); 140 | double[] components2 = right.GetComponents(); 141 | double[] result = new double[components1.Length]; 142 | 143 | for (int i = 0; i < components1.Length; i++) 144 | { 145 | result[i] = op(components1[i], components2[i]); 146 | } 147 | 148 | return new T().SetComponents(result); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /MeshSharp/Math/XY.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public struct XY : IVector 4 | { 5 | public static readonly XY Zero = new XY(0, 0); 6 | public static readonly XY AxisX = new XY(1, 0); 7 | public static readonly XY AxisY = new XY(0, 1); 8 | 9 | public double X { get; set; } 10 | public double Y { get; set; } 11 | 12 | public XY(double x, double y) 13 | { 14 | X = x; 15 | Y = y; 16 | } 17 | 18 | public XY(double[] components) : this(components[0], components[1]) { } 19 | 20 | public double[] GetComponents() 21 | { 22 | return new double[] { X, Y }; 23 | } 24 | 25 | public XY SetComponents(double[] components) 26 | { 27 | return new XY(components); 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return $"{X},{Y}"; 33 | } 34 | 35 | #region Conversion Operators 36 | public static implicit operator XY(System.Numerics.Vector2 vector) => new XY(vector.X, vector.Y); 37 | public static explicit operator System.Numerics.Vector2(XY vector) 38 | { 39 | return new System.Numerics.Vector2((float)vector.X, (float)vector.Y); 40 | } 41 | #endregion 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MeshSharp/Math/XYZ.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public struct XYZ : IVector 4 | { 5 | public static readonly XYZ Zero = new XYZ(0, 0, 0); 6 | public static readonly XYZ AxisX = new XYZ(1, 0, 0); 7 | public static readonly XYZ AxisY = new XYZ(0, 1, 0); 8 | public static readonly XYZ AxisZ = new XYZ(0, 0, 1); 9 | 10 | public double X { get; set; } 11 | public double Y { get; set; } 12 | public double Z { get; set; } 13 | 14 | public XYZ(double x, double y, double z) 15 | { 16 | X = x; 17 | Y = y; 18 | Z = z; 19 | } 20 | 21 | /// 22 | /// Constructs a vector whose elements are all the single specified value. 23 | /// 24 | /// The element to fill the vector with. 25 | public XYZ(double value) : this(value, value, value) { } 26 | 27 | public XYZ(double[] components) : this(components[0], components[1], components[2]) { } 28 | 29 | /// 30 | /// Computes the cross product of two coordinates. 31 | /// 32 | /// The first coordinate. 33 | /// The second coordinate. 34 | /// The cross product. 35 | public static XYZ Cross(XYZ xyz1, XYZ xyz2) 36 | { 37 | return new XYZ( 38 | xyz1.Y * xyz2.Z - xyz1.Z * xyz2.Y, 39 | xyz1.Z * xyz2.X - xyz1.X * xyz2.Z, 40 | xyz1.X * xyz2.Y - xyz1.Y * xyz2.X); 41 | } 42 | 43 | public static XYZ FindNormal(XYZ point1, XYZ point2, XYZ point3) 44 | { 45 | XYZ a = point2 - point1; 46 | XYZ b = point3 - point1; 47 | 48 | // N = Cross(a, b) 49 | XYZ n = XYZ.Cross(a, b); 50 | XYZ normal = n.Normalize(); 51 | 52 | return normal; 53 | } 54 | 55 | /// 56 | public double[] GetComponents() 57 | { 58 | return new double[] { X, Y, Z }; 59 | } 60 | 61 | /// 62 | public XYZ SetComponents(double[] components) 63 | { 64 | return new XYZ(components); 65 | } 66 | 67 | /// 68 | public override bool Equals(object obj) 69 | { 70 | if (!(obj is XYZ other)) 71 | return false; 72 | 73 | return this.X == other.X && this.Y == other.Y && this.Z == other.Z; 74 | } 75 | 76 | /// 77 | public override int GetHashCode() 78 | { 79 | return this.X.GetHashCode() ^ this.Y.GetHashCode() ^ this.Z.GetHashCode(); 80 | } 81 | 82 | /// 83 | public override string ToString() 84 | { 85 | return $"{X},{Y},{Z}"; 86 | } 87 | 88 | #region Operators 89 | 90 | /// 91 | /// Adds two vectors together. 92 | /// 93 | /// The first source vector. 94 | /// The second source vector. 95 | /// The summed vector. 96 | public static XYZ operator +(XYZ left, XYZ right) 97 | { 98 | return left.Add(right); 99 | } 100 | 101 | /// 102 | /// Subtracts the second vector from the first. 103 | /// 104 | /// The first source vector. 105 | /// The second source vector. 106 | /// The difference vector. 107 | public static XYZ operator -(XYZ left, XYZ right) 108 | { 109 | return left.Substract(right); 110 | } 111 | 112 | /// 113 | /// Multiplies two vectors together. 114 | /// 115 | /// The first source vector. 116 | /// The second source vector. 117 | /// The product vector. 118 | public static XYZ operator *(XYZ left, XYZ right) 119 | { 120 | return left.Multiply(right); 121 | } 122 | 123 | /// 124 | /// Multiplies a vector by the given scalar. 125 | /// 126 | /// The source vector. 127 | /// The scalar value. 128 | /// The scaled vector. 129 | public static XYZ operator *(XYZ left, double scalar) 130 | { 131 | return left * new XYZ(scalar); 132 | } 133 | 134 | /// 135 | /// Multiplies a vector by the given scalar. 136 | /// 137 | /// The scalar value. 138 | /// The source vector. 139 | /// The scaled vector. 140 | public static XYZ operator *(double scalar, XYZ vector) 141 | { 142 | return new XYZ(scalar) * vector; 143 | } 144 | 145 | /// 146 | /// Divides the first vector by the second. 147 | /// 148 | /// The first source vector. 149 | /// The second source vector. 150 | /// The vector resulting from the division. 151 | public static XYZ operator /(XYZ left, XYZ right) 152 | { 153 | return left.Divide(right); 154 | } 155 | 156 | /// 157 | /// Divides the vector by the given scalar. 158 | /// 159 | /// The source vector. 160 | /// The scalar value. 161 | /// The result of the division. 162 | public static XYZ operator /(XYZ xyz, float value) 163 | { 164 | float invDiv = 1.0f / value; 165 | 166 | return new XYZ(xyz.X * invDiv, 167 | xyz.Y * invDiv, 168 | xyz.Z * invDiv); 169 | } 170 | 171 | /// 172 | /// Negates a given vector. 173 | /// 174 | /// The source vector. 175 | /// The negated vector. 176 | public static XYZ operator -(XYZ value) 177 | { 178 | return Zero.Substract(value); 179 | } 180 | 181 | /// 182 | /// Returns a boolean indicating whether the two given vectors are equal. 183 | /// 184 | /// The first vector to compare. 185 | /// The second vector to compare. 186 | /// True if the vectors are equal; False otherwise. 187 | public static bool operator ==(XYZ left, XYZ right) 188 | { 189 | return (left.X == right.X && 190 | left.Y == right.Y && 191 | left.Z == right.Z); 192 | } 193 | 194 | /// 195 | /// Returns a boolean indicating whether the two given vectors are not equal. 196 | /// 197 | /// The first vector to compare. 198 | /// The second vector to compare. 199 | /// True if the vectors are not equal; False if they are equal. 200 | public static bool operator !=(XYZ left, XYZ right) 201 | { 202 | return (left.X != right.X || 203 | left.Y != right.Y || 204 | left.Z != right.Z); 205 | } 206 | #endregion 207 | 208 | #region Conversion Operators 209 | public static implicit operator XYZ(System.Numerics.Vector3 vector) => new XYZ(vector.X, vector.Y, vector.Z); 210 | public static explicit operator System.Numerics.Vector3(XYZ vector) 211 | { 212 | return new System.Numerics.Vector3((float)vector.X, (float)vector.Y, (float)vector.Z); 213 | } 214 | #endregion 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /MeshSharp/Math/XYZM.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public struct XYZM : IVector 4 | { 5 | public static readonly XYZM Zero = new XYZM(0, 0, 0, 0); 6 | public static readonly XYZM AxisX = new XYZM(1, 0, 0, 0); 7 | public static readonly XYZM AxisY = new XYZM(0, 1, 0, 0); 8 | public static readonly XYZM AxisZ = new XYZM(0, 0, 1, 0); 9 | public static readonly XYZM AxisM = new XYZM(0, 0, 0, 1); 10 | 11 | public double X { get; set; } 12 | public double Y { get; set; } 13 | public double Z { get; set; } 14 | public double M { get; set; } 15 | 16 | public XYZM(double x, double y, double z, double m) 17 | { 18 | X = x; 19 | Y = y; 20 | Z = z; 21 | M = m; 22 | } 23 | 24 | /// 25 | /// Constructs a vector whose elements are all the single specified value. 26 | /// 27 | /// The element to fill the vector with. 28 | public XYZM(double value) : this(value, value, value, value) { } 29 | 30 | public XYZM(double[] components) : this(components[0], components[1], components[2], components[3]) { } 31 | 32 | /// 33 | public double[] GetComponents() 34 | { 35 | return new double[] { X, Y, Z, M }; 36 | } 37 | 38 | /// 39 | public XYZM SetComponents(double[] components) 40 | { 41 | return new XYZM(components); 42 | } 43 | 44 | /// 45 | public override bool Equals(object obj) 46 | { 47 | if (!(obj is XYZM other)) 48 | return false; 49 | 50 | return this.X == other.X && this.Y == other.Y && this.Z == other.Z && this.M == other.M; 51 | } 52 | 53 | /// 54 | public override int GetHashCode() 55 | { 56 | return this.X.GetHashCode() ^ this.Y.GetHashCode() ^ this.Z.GetHashCode() ^ this.M.GetHashCode(); 57 | } 58 | 59 | /// 60 | public override string ToString() 61 | { 62 | return $"{X},{Y},{Z},{M}"; 63 | } 64 | 65 | #region Operators 66 | /// 67 | /// Adds two vectors together. 68 | /// 69 | /// The first source vector. 70 | /// The second source vector. 71 | /// The summed vector. 72 | public static XYZM operator +(XYZM left, XYZM right) 73 | { 74 | return left.Add(right); 75 | } 76 | 77 | /// 78 | /// Subtracts the second vector from the first. 79 | /// 80 | /// The first source vector. 81 | /// The second source vector. 82 | /// The difference vector. 83 | public static XYZM operator -(XYZM left, XYZM right) 84 | { 85 | return left.Substract(right); 86 | } 87 | 88 | /// 89 | /// Multiplies two vectors together. 90 | /// 91 | /// The first source vector. 92 | /// The second source vector. 93 | /// The product vector. 94 | public static XYZM operator *(XYZM left, XYZM right) 95 | { 96 | return left.Multiply(right); 97 | } 98 | 99 | /// 100 | /// Multiplies a vector by the given scalar. 101 | /// 102 | /// The source vector. 103 | /// The scalar value. 104 | /// The scaled vector. 105 | public static XYZM operator *(XYZM left, double scalar) 106 | { 107 | return left * new XYZM(scalar); 108 | } 109 | 110 | /// 111 | /// Multiplies a vector by the given scalar. 112 | /// 113 | /// The scalar value. 114 | /// The source vector. 115 | /// The scaled vector. 116 | public static XYZM operator *(double scalar, XYZM vector) 117 | { 118 | return new XYZM(scalar) * vector; 119 | } 120 | 121 | /// 122 | /// Divides the first vector by the second. 123 | /// 124 | /// The first source vector. 125 | /// The second source vector. 126 | /// The vector resulting from the division. 127 | public static XYZM operator /(XYZM left, XYZM right) 128 | { 129 | return left.Divide(right); 130 | } 131 | 132 | /// 133 | /// Divides the vector by the given scalar. 134 | /// 135 | /// The source vector. 136 | /// The scalar value. 137 | /// The result of the division. 138 | public static XYZM operator /(XYZM xyzm, double value) 139 | { 140 | return xyzm.Divide(new XYZM(value)); 141 | } 142 | 143 | /// 144 | /// Negates a given vector. 145 | /// 146 | /// The source vector. 147 | /// The negated vector. 148 | public static XYZM operator -(XYZM value) 149 | { 150 | return Zero.Substract(value); 151 | } 152 | 153 | /// 154 | /// Returns a boolean indicating whether the two given vectors are equal. 155 | /// 156 | /// The first vector to compare. 157 | /// The second vector to compare. 158 | /// True if the vectors are equal; False otherwise. 159 | public static bool operator ==(XYZM left, XYZM right) 160 | { 161 | return left.IsEqual(right); 162 | } 163 | 164 | /// 165 | /// Returns a boolean indicating whether the two given vectors are not equal. 166 | /// 167 | /// The first vector to compare. 168 | /// The second vector to compare. 169 | /// True if the vectors are not equal; False if they are equal. 170 | public static bool operator !=(XYZM left, XYZM right) 171 | { 172 | return !left.IsEqual(right); 173 | } 174 | #endregion 175 | 176 | #region Conversion Operators 177 | public static implicit operator XYZM(System.Numerics.Vector4 vector) => new XYZM(vector.X, vector.Y, vector.Z, vector.W); 178 | public static explicit operator System.Numerics.Vector4(XYZM vector) 179 | { 180 | return new System.Numerics.Vector4((float)vector.X, (float)vector.Y, (float)vector.Z, (float)vector.M); 181 | } 182 | #endregion 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /MeshSharp/MeshSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | latest 6 | AssetRipper.MeshSharp 7 | disable 8 | ds5678 9 | AssetRipper 10 | 1.0.0.0 11 | 1.0.0.0 12 | AssetRipper.MeshSharp 13 | C# 3D 14 | https://github.com/AssetRipper/MeshSharp 15 | https://github.com/AssetRipper/MeshSharp 16 | MIT 17 | git 18 | Copyright (c) 2022 ds5678 19 | 3D library in pure C# that allows reading and writing in multiple formats. 20 | true 21 | 22 | 23 | 24 | true 25 | 26 | 27 | 28 | false 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | bin\MeshSharp.xml 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /MeshSharp/Polygon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp 4 | { 5 | public class Polygon 6 | { 7 | public uint[] Indices { get; } 8 | 9 | public Polygon(uint[] indices) 10 | { 11 | if (indices == null) 12 | throw new ArgumentNullException(nameof(indices)); 13 | if (indices.Length < 3) 14 | throw new ArgumentException("Polygons must have at least 3 sides", nameof(indices)); 15 | this.Indices = indices; 16 | } 17 | 18 | public Triangle[] ConvertToTriangles() 19 | { 20 | Triangle[] result = new Triangle[Indices.Length - 2]; 21 | uint index0 = Indices[0]; 22 | for (int i = 0; i + 2 < Indices.Length; i++) 23 | { 24 | result[i] = new Triangle(index0, Indices[i + 1], Indices[i + 2]); 25 | } 26 | return result; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MeshSharp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("MeshSharp.FBX")] 4 | [assembly: InternalsVisibleTo("MeshSharp.GLTF")] 5 | [assembly: InternalsVisibleTo("MeshSharp.OBJ")] 6 | [assembly: InternalsVisibleTo("MeshSharp.PLY")] 7 | [assembly: InternalsVisibleTo("MeshSharp.PMX")] 8 | [assembly: InternalsVisibleTo("MeshSharp.STL")] 9 | -------------------------------------------------------------------------------- /MeshSharp/Property.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using System; 3 | 4 | namespace AssetRipper.MeshSharp 5 | { 6 | public class Property 7 | { 8 | public string Name { get; } 9 | 10 | public virtual object Value { get; set; } 11 | 12 | protected Element _owner; 13 | 14 | public Property(string name, Element owner) 15 | { 16 | Name = name; 17 | _owner = owner; 18 | } 19 | 20 | public Property(string name, Element owner, object value) : this(name, owner) 21 | { 22 | Value = value; 23 | } 24 | 25 | public static Property CreateTypedProperty(Property property) 26 | { 27 | Property typed = new Property(property.Name, property._owner); 28 | 29 | if (!property.Value.GetType().IsEquivalentTo(typeof(T))) 30 | throw new ArgumentException($"Value is not equivalent to {typeof(T).FullName}", nameof(property)); 31 | 32 | typed.Value = (T)Convert.ChangeType(property.Value, typeof(T)); 33 | 34 | return typed; 35 | } 36 | } 37 | 38 | public class Property : Property 39 | { 40 | public new T Value { get; set; } 41 | 42 | public Property(string name, Element owner) : base(name, owner) { } 43 | 44 | public Property(string name, Element owner, T value) : base(name, owner, value) { } 45 | } 46 | } -------------------------------------------------------------------------------- /MeshSharp/PropertyCollection.cs: -------------------------------------------------------------------------------- 1 | using AssetRipper.MeshSharp.Elements; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace AssetRipper.MeshSharp 7 | { 8 | public class PropertyCollection : IEnumerable 9 | { 10 | public Property this[int index] { get { return _properties.Values.ElementAt(index); } } 11 | 12 | public Property this[string name] { get { return _properties[name]; } } 13 | 14 | public int Count { get { return _properties.Count; } } 15 | 16 | private readonly Dictionary _properties = new Dictionary(); 17 | private Element _owner; 18 | 19 | public PropertyCollection(Element owner) 20 | { 21 | _owner = owner; 22 | } 23 | 24 | public void Add(Property property) 25 | { 26 | _properties.Add(property.Name, property); 27 | } 28 | 29 | public bool Contains(string name) 30 | { 31 | return _properties.ContainsKey(name); 32 | } 33 | 34 | public void Remove(Property property) 35 | { 36 | this.Remove(property.Name); 37 | } 38 | 39 | public void Remove(string name) 40 | { 41 | _properties.Remove(name); 42 | } 43 | 44 | /// 45 | public IEnumerator GetEnumerator() 46 | { 47 | return _properties.Values.GetEnumerator(); 48 | } 49 | 50 | /// 51 | IEnumerator IEnumerable.GetEnumerator() 52 | { 53 | return _properties.Values.GetEnumerator(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MeshSharp/Quad.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public class Quad : Polygon 4 | { 5 | public uint Index0 6 | { 7 | get => Indices[0]; 8 | set => Indices[0] = value; 9 | } 10 | public uint Index1 11 | { 12 | get => Indices[1]; 13 | set => Indices[1] = value; 14 | } 15 | public uint Index2 16 | { 17 | get => Indices[2]; 18 | set => Indices[2] = value; 19 | } 20 | public uint Index3 21 | { 22 | get => Indices[3]; 23 | set => Indices[3] = value; 24 | } 25 | 26 | public Quad(uint i0, uint i1, uint i2, uint i3) : base(new uint[] { i0, i1, i2, i3 }) { } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MeshSharp/Transform.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp 4 | { 5 | public class Transform 6 | { 7 | public XYZ Translation 8 | { 9 | get 10 | { 11 | return new XYZ(_matrix.m30, _matrix.m31, _matrix.m32); 12 | } 13 | set 14 | { 15 | _matrix.m30 = value.X; 16 | _matrix.m31 = value.Y; 17 | _matrix.m32 = value.Z; 18 | } 19 | } 20 | 21 | public XYZ Scale 22 | { 23 | get 24 | { 25 | double x = new XYZ(this._matrix.m00, this._matrix.m01, this._matrix.m02).GetLength(); 26 | double y = new XYZ(this._matrix.m10, this._matrix.m11, this._matrix.m12).GetLength(); 27 | double z = new XYZ(this._matrix.m20, this._matrix.m21, this._matrix.m22).GetLength(); 28 | 29 | return new XYZ(x, y, z); 30 | } 31 | set 32 | { 33 | if (value.X == 0 || value.Y == 0 || value.Z == 0) 34 | throw new ArgumentException(); 35 | 36 | double x = new XYZ(this._matrix.m00, this._matrix.m01, this._matrix.m02).GetLength(); 37 | double y = new XYZ(this._matrix.m10, this._matrix.m11, this._matrix.m12).GetLength(); 38 | double z = new XYZ(this._matrix.m20, this._matrix.m21, this._matrix.m22).GetLength(); 39 | 40 | this._matrix *= Matrix4.CreateScalingMatrix(1.0 / x * value.X, 1.0 / y * value.Y, 1.0 / z * value.Z); 41 | } 42 | } 43 | 44 | public XYZ Rotation 45 | { 46 | get { throw new NotImplementedException(); } 47 | set 48 | { 49 | _matrix *= Matrix4.CreateRotationMatrix(value.X, value.Y, value.Z); 50 | } 51 | } 52 | 53 | public Quaternion Quaternion 54 | { 55 | get { throw new NotImplementedException(); } 56 | set 57 | { 58 | //// Compute rotation matrix. 59 | //double x2 = value.X + value.X; 60 | //double y2 = value.Y + value.Y; 61 | //double z2 = value.Z + value.Z; 62 | 63 | //double wx2 = value.W * x2; 64 | //double wy2 = value.W * y2; 65 | //double wz2 = value.W * z2; 66 | //double xx2 = value.X * x2; 67 | //double xy2 = value.X * y2; 68 | //double xz2 = value.X * z2; 69 | //double yy2 = value.Y * y2; 70 | //double yz2 = value.Y * z2; 71 | //double zz2 = value.Z * z2; 72 | 73 | //double q11 = 1.0f - yy2 - zz2; 74 | //double q21 = xy2 - wz2; 75 | //double q31 = xz2 + wy2; 76 | 77 | //double q12 = xy2 + wz2; 78 | //double q22 = 1.0f - xx2 - zz2; 79 | //double q32 = yz2 - wx2; 80 | 81 | //double q13 = xz2 - wy2; 82 | //double q23 = yz2 + wx2; 83 | //double q33 = 1.0f - xx2 - yy2; 84 | 85 | //Matrix4 result = new Matrix4(); 86 | 87 | //// First row 88 | //result.m00 = this._matrix.m00 * q11 + this._matrix.m10 * q21 + this._matrix.m20 * q31; 89 | //result.m10 = this._matrix.m00 * q12 + this._matrix.m10 * q22 + this._matrix.m20 * q32; 90 | //result.m20 = this._matrix.m00 * q13 + this._matrix.m10 * q23 + this._matrix.m20 * q33; 91 | //result.m30 = this._matrix.m30; 92 | 93 | //// Second row 94 | //result.m01 = this._matrix.m01 * q11 + this._matrix.m11 * q21 + this._matrix.m21 * q31; 95 | //result.m11 = this._matrix.m01 * q12 + this._matrix.m11 * q22 + this._matrix.m21 * q32; 96 | //result.m21 = this._matrix.m01 * q13 + this._matrix.m11 * q23 + this._matrix.m21 * q33; 97 | //result.m31 = this._matrix.m31; 98 | 99 | //// Third row 100 | //result.m02 = this._matrix.m02 * q11 + this._matrix.m12 * q21 + this._matrix.m22 * q31; 101 | //result.m12 = this._matrix.m02 * q12 + this._matrix.m12 * q22 + this._matrix.m22 * q32; 102 | //result.m22 = this._matrix.m02 * q13 + this._matrix.m12 * q23 + this._matrix.m22 * q33; 103 | //result.m32 = this._matrix.m32; 104 | 105 | //// Fourth row 106 | //result.m03 = this._matrix.m03 * q11 + this._matrix.m13 * q21 + this._matrix.m23 * q31; 107 | //result.m13 = this._matrix.m03 * q12 + this._matrix.m13 * q22 + this._matrix.m23 * q32; 108 | //result.m23 = this._matrix.m03 * q13 + this._matrix.m13 * q23 + this._matrix.m23 * q33; 109 | //result.m33 = this._matrix.m33; 110 | 111 | //this._matrix = result; 112 | } 113 | } 114 | 115 | private Matrix4 _matrix; 116 | 117 | public Transform() 118 | { 119 | _matrix = Matrix4.Identity; 120 | Translation = XYZ.Zero; 121 | Rotation = XYZ.Zero; 122 | Quaternion = Quaternion.Identity; 123 | Scale = new XYZ(1, 1, 1); 124 | _matrix.m33 = 1; 125 | } 126 | 127 | public Transform(Matrix4 matrix) 128 | { 129 | this._matrix = matrix; 130 | } 131 | 132 | public XYZ ApplyTransform(XYZ xyz) 133 | { 134 | return _matrix * xyz; 135 | } 136 | 137 | public static bool TryDecompose(Transform transform, out XYZ translation, out XYZ scaling, out Quaternion rotation) 138 | { 139 | Matrix4 matrix = transform._matrix; 140 | 141 | translation = new XYZ(); 142 | scaling = new XYZ(); 143 | rotation = new Quaternion(); 144 | var XYZDouble = new XYZ(); 145 | 146 | if (matrix.m33 == 0.0) 147 | return false; 148 | 149 | //matrix = matrix.Normalize(); 150 | Matrix4 matrix4_3 = matrix; 151 | matrix4_3.m03 = 0.0; 152 | matrix4_3.m13 = 0.0; 153 | matrix4_3.m23 = 0.0; 154 | matrix4_3.m33 = 1.0; 155 | 156 | if (matrix4_3.GetDeterminant() == 0.0) 157 | return false; 158 | 159 | if (matrix.m03 != 0.0 || matrix.m13 != 0.0 || matrix.m23 != 0.0) 160 | { 161 | if (!Matrix4.Inverse(matrix, out Matrix4 inverse)) 162 | { 163 | return false; 164 | } 165 | 166 | matrix.m03 = matrix.m13 = matrix.m23 = 0.0; 167 | matrix.m33 = 1.0; 168 | } 169 | 170 | translation.X = matrix.m30; 171 | matrix.m30 = 0.0; 172 | translation.Y = matrix.m31; 173 | matrix.m31 = 0.0; 174 | translation.Z = matrix.m32; 175 | matrix.m32 = 0.0; 176 | 177 | XYZ[] cols = new XYZ[3] 178 | { 179 | new XYZ(matrix.m00, matrix.m01, matrix.m02), 180 | new XYZ(matrix.m10, matrix.m11, matrix.m12), 181 | new XYZ(matrix.m20, matrix.m21, matrix.m22) 182 | }; 183 | 184 | //List cols = matrix.GetCols(); 185 | 186 | scaling.X = cols[0].GetLength(); 187 | cols[0] = cols[0].Normalize(); 188 | XYZDouble.X = cols[0].Dot(cols[1]); 189 | cols[1] = cols[1] * 1 + cols[0] * -XYZDouble.X; 190 | 191 | scaling.Y = cols[1].GetLength(); 192 | cols[1] = cols[1].Normalize(); 193 | XYZDouble.Y = cols[0].Dot(cols[2]); 194 | cols[2] = cols[2] * 1 + cols[0] * -XYZDouble.Y; 195 | 196 | XYZDouble.Z = cols[1].Dot(cols[2]); 197 | cols[2] = cols[2] * 1 + cols[1] * -XYZDouble.Z; 198 | scaling.Z = cols[2].GetLength(); 199 | cols[2] = cols[2].Normalize(); 200 | 201 | XYZ rhs = XYZ.Cross(cols[1], cols[2]); 202 | if (cols[0].Dot(rhs) < 0.0) 203 | { 204 | for (int index = 0; index < 3; ++index) 205 | { 206 | scaling.X *= -1.0; 207 | cols[index].X *= -1.0; 208 | cols[index].Y *= -1.0; 209 | cols[index].Z *= -1.0; 210 | } 211 | } 212 | 213 | double trace = cols[0].X + cols[1].Y + cols[2].Z + 1.0; 214 | double qx; 215 | double qy; 216 | double qz; 217 | double qw; 218 | 219 | if (trace > 0) 220 | { 221 | double s = 0.5 / Math.Sqrt(trace); 222 | qx = (cols[2].Y - cols[1].Z) * s; 223 | qy = (cols[0].Z - cols[2].X) * s; 224 | qz = (cols[1].X - cols[0].Y) * s; 225 | qw = 0.25 / s; 226 | } 227 | else if (cols[0].X > cols[1].Y && cols[0].X > cols[2].Z) 228 | { 229 | double s = Math.Sqrt(1.0 + cols[0].X - cols[1].Y - cols[2].Z) * 2.0; 230 | qx = 0.25 * s; 231 | qy = (cols[0].Y + cols[1].X) / s; 232 | qz = (cols[0].Z + cols[2].X) / s; 233 | qw = (cols[2].Y - cols[1].Z) / s; 234 | } 235 | else if (cols[1].Y > cols[2].Z) 236 | { 237 | double s = Math.Sqrt(1.0 + cols[1].Y - cols[0].X - cols[2].Z) * 2.0; 238 | qx = (cols[0].Y + cols[1].X) / s; 239 | qy = 0.25 * s; 240 | qz = (cols[1].Z + cols[2].Y) / s; 241 | qw = (cols[0].Z - cols[2].X) / s; 242 | } 243 | else 244 | { 245 | double s = Math.Sqrt(1.0 + cols[2].Z - cols[0].X - cols[1].Y) * 2.0; 246 | qx = (cols[0].Z + cols[2].X) / s; 247 | qy = (cols[1].Z + cols[2].Y) / s; 248 | qz = 0.25 * s; 249 | qw = (cols[1].X - cols[0].Y) / s; 250 | } 251 | 252 | rotation.X = qx; 253 | rotation.Y = qy; 254 | rotation.Z = qz; 255 | rotation.W = -qw; 256 | 257 | return true; 258 | } 259 | } 260 | 261 | 262 | } 263 | -------------------------------------------------------------------------------- /MeshSharp/Triangle.cs: -------------------------------------------------------------------------------- 1 | namespace AssetRipper.MeshSharp 2 | { 3 | public class Triangle : Polygon 4 | { 5 | public uint Index0 6 | { 7 | get => Indices[0]; 8 | set => Indices[0] = value; 9 | } 10 | public uint Index1 11 | { 12 | get => Indices[1]; 13 | set => Indices[1] = value; 14 | } 15 | public uint Index2 16 | { 17 | get => Indices[2]; 18 | set => Indices[2] = value; 19 | } 20 | 21 | /// 22 | /// Default constructor. 23 | /// 24 | public Triangle() : this(0, 0, 0) { } 25 | 26 | /// 27 | /// Setup a triangle with the 3 indexes. 28 | /// 29 | /// 30 | /// 31 | /// 32 | public Triangle(uint i0, uint i1, uint i2) : base(new uint[] { i0, i1, i2 }) { } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MeshSharp/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AssetRipper.MeshSharp 4 | { 5 | internal static class Utils 6 | { 7 | /// 8 | ///Avoid duplicated ids in the tight loops. 9 | /// 10 | private static readonly Random _random = new Random(); 11 | 12 | private static readonly object _syncLock = new object(); 13 | 14 | /// 15 | /// Creates an id as a long. 16 | /// 17 | /// 18 | public static ulong CreateId() 19 | { 20 | lock (_syncLock) 21 | { 22 | byte[] buffer = new byte[8]; 23 | _random.NextBytes(buffer); 24 | return (ulong)Math.Abs(BitConverter.ToInt64(buffer, 0)); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshSharp ![Build](https://img.shields.io/github/workflow/status/ds5678/MeshSharp/Build&Test/master) 2 | 3 | C# 3D library. 4 | 5 | > Warning: API currently unstable. Expect undocumented breaking changes for the foreseeable future 6 | 7 | ## MeshSharp.DAE 8 | 9 | Not yet implemented. 10 | 11 | ## MeshSharp.FBX 12 | 13 | **Current features**: 14 | 15 | * Read and Write FBX binary files 16 | * Not tested 17 | * Read and Write FBX ASCII files 18 | * Seems to be working 19 | 20 | ## MeshSharp.GLTF 21 | 22 | Not yet implemented. 23 | 24 | ## MeshSharp.OBJ 25 | 26 | * Reading 27 | * Not yet implemented 28 | * Writing 29 | * Currently, only vertices and faces can be written 30 | 31 | ## MeshSharp.PLY 32 | 33 | * Write PLY ASCII files 34 | 35 | ## MeshSharp.PMX 36 | 37 | Not yet implemented 38 | 39 | ## MeshSharp.STL 40 | 41 | * Read and Write STL binary files 42 | * Read and Write STL ASCII files 43 | 44 | 45 | ## Credits 46 | 47 | This is a fork of [MeshIO](https://github.com/DomCR/MeshIO) licensed under the [MIT license](Licenses/MeshIO.md). -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | rm -f -r ./MeshSharp/bin/ 2 | rm -f -r ./MeshSharp/obj/ 3 | rm -f -r ./MeshSharp.Examples/bin/ 4 | rm -f -r ./MeshSharp.Examples/obj/ 5 | rm -f -r ./MeshSharp.FBX/bin/ 6 | rm -f -r ./MeshSharp.FBX/obj/ 7 | rm -f -r ./MeshSharp.GLTF/bin/ 8 | rm -f -r ./MeshSharp.GLTF/obj/ 9 | rm -f -r ./MeshSharp.OBJ/bin/ 10 | rm -f -r ./MeshSharp.OBJ/obj/ 11 | rm -f -r ./MeshSharp.PLY/bin/ 12 | rm -f -r ./MeshSharp.PLY/obj/ 13 | rm -f -r ./MeshSharp.PMX/bin/ 14 | rm -f -r ./MeshSharp.PMX/obj/ 15 | rm -f -r ./MeshSharp.STL/bin/ 16 | rm -f -r ./MeshSharp.STL/obj/ 17 | rm -f -r ./MeshSharp.Tests/bin/ 18 | rm -f -r ./MeshSharp.Tests/obj/ 19 | rm -f -r ./MeshSharp.Viewer/bin/ 20 | rm -f -r ./MeshSharp.Viewer/obj/ 21 | 22 | --------------------------------------------------------------------------------