├── kn5 converter ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── kn5 converter.csproj └── Program.cs ├── kn5 converter.sln ├── .gitattributes └── .gitignore /kn5 converter/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kn5 converter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual C# Express 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kn5 converter", "kn5 converter\kn5 converter.csproj", "{507C9510-A849-426B-9B8C-45F237DA7DE6}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|x86 = Debug|x86 9 | Release|x86 = Release|x86 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {507C9510-A849-426B-9B8C-45F237DA7DE6}.Debug|x86.ActiveCfg = Debug|x86 13 | {507C9510-A849-426B-9B8C-45F237DA7DE6}.Debug|x86.Build.0 = Debug|x86 14 | {507C9510-A849-426B-9B8C-45F237DA7DE6}.Release|x86.ActiveCfg = Release|x86 15 | {507C9510-A849-426B-9B8C-45F237DA7DE6}.Release|x86.Build.0 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /kn5 converter/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("kn5 converter")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("kn5 converter")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("49682c3a-5b7a-4988-80dd-3cb3ac6f1a66")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /kn5 converter/kn5 converter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {507C9510-A849-426B-9B8C-45F237DA7DE6} 9 | Exe 10 | Properties 11 | kn5_converter 12 | kn5conv 13 | v3.5 14 | Client 15 | 512 16 | 17 | 18 | x86 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 59 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /kn5 converter/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | 7 | namespace kn5_converter 8 | { 9 | class Program 10 | { 11 | public class kn5Model 12 | { 13 | public string modelDir; 14 | public string modelName; 15 | 16 | public int version; 17 | public List textures = new List(); 18 | public List usedTex = new List(); 19 | public List materials = new List(); 20 | public List nodes = new List(); 21 | } 22 | 23 | public class kn5Material 24 | { 25 | public string name = "Default"; 26 | public string shader = ""; 27 | public float ksAmbient = 0.6f; 28 | public float ksDiffuse = 0.6f; 29 | public float ksSpecular = 0.9f; 30 | public float ksSpecularEXP = 1.0f; 31 | public float diffuseMult = 1.0f; 32 | public float normalMult = 1.0f; 33 | public float useDetail = 0.0f; 34 | public float detailUVMultiplier = 1.0f; 35 | 36 | public string txDiffuse; 37 | public string txNormal; 38 | public string txDetail; 39 | 40 | public string shaderProps = ""; 41 | } 42 | 43 | public class kn5Texture 44 | { 45 | public string filename; 46 | public float UVScaling = 1.0f; 47 | } 48 | 49 | public class kn5Node 50 | { 51 | public int type = 1; 52 | public string name = "Default"; 53 | 54 | public float[,] tmatrix = new float[4, 4] { { 1.0f, 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 0.0f, 1.0f } }; 55 | public float[,] hmatrix = new float[4, 4] { { 1.0f, 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 0.0f, 1.0f } }; 56 | 57 | public float[] translation = new float[3] { 0.0f, 0.0f, 0.0f }; 58 | public float[] rotation = new float[3] { 0.0f, 0.0f, 0.0f }; 59 | public float[] scaling = new float[3] { 1.0f, 1.0f, 1.0f }; 60 | 61 | public int vertexCount; 62 | public float[] position; 63 | public float[] normal; 64 | public float[] texture0; 65 | 66 | public ushort[] indices; 67 | 68 | public int materialID = -1; 69 | 70 | //public List children; //do I really wanna do this? no 71 | public int parentID = -1; 72 | } 73 | 74 | static string[] outputTypes = new string[3] {"fbx", "obj", "objZMhack"}; 75 | //static string currentPath = AppDomain.CurrentDomain.BaseDirectory; 76 | 77 | static void Main(string[] args) 78 | { 79 | System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); 80 | SearchOption recurse = SearchOption.AllDirectories; 81 | 82 | //string input = @"D:\Assetto Corsa\content\"; 83 | string input = ""; 84 | 85 | switch (args.Length) 86 | { 87 | case 0: 88 | input = AppDomain.CurrentDomain.BaseDirectory; 89 | recurse = SearchOption.TopDirectoryOnly; 90 | goto default; 91 | case 1: 92 | if (GetOutputTypes(args[0])) { goto case 0; } 93 | else { input = args[0]; } 94 | break; 95 | case 2: 96 | if (GetOutputTypes(args[0])) { input = args[1]; } 97 | else if (GetOutputTypes(args[1])) { input = args[0]; } //maybe the user can't follow instructions 98 | break; 99 | default: 100 | Console.WriteLine("Assetto Corsa kn5 model converter\nby Chipicao - KotsChopShop.com\n\nUsage: kn5conv.exe [-fbx|-obj|-objZMhack] [input_file/folder]"); 101 | break; 102 | } 103 | 104 | if (File.Exists(input)) 105 | { 106 | input = Path.GetFullPath(input); //in case only filename is entered 107 | 108 | string currentModel = MakeRelative(input, (Path.GetDirectoryName(input) + "\\")); 109 | Console.WriteLine("Reading {0}", currentModel); 110 | 111 | var theModel = readKN5(input); 112 | //Console.WriteLine("\rConverting {0}", currentModel); 113 | 114 | foreach (string format in outputTypes) 115 | { 116 | switch (format.ToLower()) 117 | { 118 | case "fbx": 119 | ExportFBX(theModel); 120 | break; 121 | case "obj": 122 | ExportOBJ(theModel, false); 123 | break; 124 | case "objzmhack": 125 | ExportOBJ(theModel, true); 126 | break; 127 | } 128 | } 129 | } 130 | else if (Directory.Exists(input)) 131 | { 132 | input = Path.GetFullPath(input + "\\"); 133 | string[] inputFiles = Directory.GetFiles(input, "*.kn5", recurse); 134 | Console.WriteLine("Found {0} files.", inputFiles.Length); 135 | 136 | foreach (var inputFile in inputFiles) 137 | { 138 | string currentModel = MakeRelative(inputFile, input); 139 | Console.WriteLine("Reading {0}", currentModel); 140 | 141 | var theModel = readKN5(inputFile); 142 | //Console.WriteLine("\rConverting {0}", currentModel); 143 | 144 | foreach (string format in outputTypes) 145 | { 146 | switch (format.ToLower()) 147 | { 148 | case "fbx": 149 | ExportFBX(theModel); 150 | break; 151 | case "obj": 152 | ExportOBJ(theModel, false); 153 | break; 154 | case "objzmhack": 155 | ExportOBJ(theModel, true); 156 | break; 157 | } 158 | } 159 | } 160 | 161 | Console.WriteLine("Finished. Press any key to exit..."); 162 | Console.ReadKey(); 163 | } 164 | else { Console.WriteLine("Invalid input file/folder: {0}\n\nUsage: M3G2FBX.exe [-u] [input_file/folder]", input); } 165 | } 166 | 167 | private static bool GetOutputTypes(string arg) 168 | { 169 | string[] outputArgs = arg.Split(new char[1] { '-' }); 170 | 171 | //don't just split the types, also check if they are valid 172 | //TODO make case insensitive 173 | string[] newOutputTypes = outputArgs.Intersect(outputTypes, StringComparer.OrdinalIgnoreCase).ToArray(); 174 | 175 | if (newOutputTypes.Length > 0) 176 | { 177 | outputTypes = newOutputTypes; 178 | return true; 179 | } 180 | else { return false; } 181 | } 182 | 183 | 184 | private static kn5Model readKN5(string kn5File) 185 | { 186 | using (BinaryReader binStream = new BinaryReader(File.OpenRead(kn5File))) 187 | { 188 | string magicNumber = ReadStr(binStream, 6); 189 | if (magicNumber == "sc6969") 190 | { 191 | kn5Model newModel = new kn5Model(); 192 | newModel.modelDir = Path.GetDirectoryName(kn5File) + "\\"; 193 | newModel.modelName = Path.GetFileNameWithoutExtension(kn5File); 194 | 195 | newModel.version = binStream.ReadInt32(); 196 | if (newModel.version > 5) { int unknownNo = binStream.ReadInt32(); } //673425 197 | 198 | #region extract textures 199 | Directory.CreateDirectory(newModel.modelDir + "texture"); 200 | int texCount = binStream.ReadInt32(); 201 | for (int t = 0; t < texCount; t++) 202 | { 203 | int texType = binStream.ReadInt32(); 204 | string texName = ReadStr(binStream, binStream.ReadInt32()); 205 | int texSize = binStream.ReadInt32(); 206 | newModel.textures.Add(texName); 207 | 208 | if (File.Exists(newModel.modelDir + "texture\\" + texName)) 209 | { 210 | binStream.BaseStream.Position += texSize; 211 | } 212 | else 213 | { 214 | byte[] texBuffer = binStream.ReadBytes(texSize); 215 | using (BinaryWriter texWriter = new BinaryWriter(File.Create(newModel.modelDir + "texture\\" + texName))) 216 | { 217 | texWriter.Write(texBuffer); 218 | } 219 | } 220 | } 221 | #endregion 222 | 223 | #region read materials 224 | int matCount = binStream.ReadInt32(); 225 | for (int m = 0; m < matCount; m++) 226 | { 227 | kn5Material newMaterial = new kn5Material(); 228 | 229 | newMaterial.name = ReadStr(binStream, binStream.ReadInt32()); 230 | newMaterial.shader = ReadStr(binStream, binStream.ReadInt32()); 231 | short ashort = binStream.ReadInt16(); 232 | if (newModel.version > 4) { int azero = binStream.ReadInt32(); } 233 | 234 | int propCount = binStream.ReadInt32(); 235 | for (int p = 0; p < propCount; p++) 236 | { 237 | string propName = ReadStr(binStream, binStream.ReadInt32()); 238 | float propValue = binStream.ReadSingle(); 239 | newMaterial.shaderProps += propName + " = " + propValue.ToString() + "&cr;&lf;"; 240 | 241 | switch (propName) 242 | { 243 | case "ksAmbient": 244 | newMaterial.ksAmbient = propValue; 245 | break; 246 | case "ksDiffuse": 247 | newMaterial.ksDiffuse = propValue; 248 | break; 249 | case "ksSpecular": 250 | newMaterial.ksSpecular = propValue; 251 | break; 252 | case "ksSpecularEXP": 253 | newMaterial.ksSpecularEXP = propValue; 254 | break; 255 | case "diffuseMult": 256 | newMaterial.diffuseMult = propValue; 257 | break; 258 | case "normalMult": 259 | newMaterial.normalMult = propValue; 260 | break; 261 | case "useDetail": 262 | newMaterial.useDetail = propValue; 263 | break; 264 | case "detailUVMultiplier": 265 | newMaterial.detailUVMultiplier = propValue; 266 | break; 267 | } 268 | 269 | binStream.BaseStream.Position += 36; 270 | } 271 | 272 | int textures = binStream.ReadInt32(); 273 | for (int t = 0; t < textures; t++) 274 | { 275 | string sampleName = ReadStr(binStream, binStream.ReadInt32()); 276 | int sampleSlot = binStream.ReadInt32(); 277 | string texName = ReadStr(binStream, binStream.ReadInt32()); 278 | 279 | newMaterial.shaderProps += sampleName + " = " + texName + "&cr;&lf;"; 280 | 281 | switch (sampleName) 282 | { 283 | case "txDiffuse": 284 | newMaterial.txDiffuse = texName; 285 | break; 286 | case "txNormal": 287 | newMaterial.txNormal = texName; 288 | break; 289 | case "txDetail": 290 | newMaterial.txDetail = texName; 291 | break; 292 | } 293 | } 294 | 295 | newModel.materials.Add(newMaterial); 296 | } 297 | #endregion 298 | 299 | readNodes(binStream, newModel.nodes, -1); //recursive 300 | 301 | return newModel; 302 | } 303 | else 304 | { 305 | Console.WriteLine("Unknown file type."); 306 | return null; 307 | } 308 | } 309 | } 310 | 311 | private static void readNodes(BinaryReader modelStream, List nodeList, int parentID) 312 | { 313 | kn5Node newNode = new kn5Node(); 314 | newNode.parentID = parentID; 315 | 316 | newNode.type = modelStream.ReadInt32(); 317 | newNode.name = ReadStr(modelStream, modelStream.ReadInt32()); 318 | int childrenCount = modelStream.ReadInt32(); 319 | byte abyte = modelStream.ReadByte(); 320 | 321 | switch (newNode.type) 322 | { 323 | #region dummy node 324 | case 1: //dummy 325 | { 326 | newNode.tmatrix[0, 0] = modelStream.ReadSingle(); 327 | newNode.tmatrix[0, 1] = modelStream.ReadSingle(); 328 | newNode.tmatrix[0, 2] = modelStream.ReadSingle(); 329 | newNode.tmatrix[0, 3] = modelStream.ReadSingle(); 330 | newNode.tmatrix[1, 0] = modelStream.ReadSingle(); 331 | newNode.tmatrix[1, 1] = modelStream.ReadSingle(); 332 | newNode.tmatrix[1, 2] = modelStream.ReadSingle(); 333 | newNode.tmatrix[1, 3] = modelStream.ReadSingle(); 334 | newNode.tmatrix[2, 0] = modelStream.ReadSingle(); 335 | newNode.tmatrix[2, 1] = modelStream.ReadSingle(); 336 | newNode.tmatrix[2, 2] = modelStream.ReadSingle(); 337 | newNode.tmatrix[2, 3] = modelStream.ReadSingle(); 338 | newNode.tmatrix[3, 0] = modelStream.ReadSingle(); 339 | newNode.tmatrix[3, 1] = modelStream.ReadSingle(); 340 | newNode.tmatrix[3, 2] = modelStream.ReadSingle(); 341 | newNode.tmatrix[3, 3] = modelStream.ReadSingle(); 342 | 343 | newNode.translation = new float[3] { newNode.tmatrix[3, 0], newNode.tmatrix[3, 1], newNode.tmatrix[3, 2] }; 344 | newNode.rotation = MatrixToEuler(newNode.tmatrix); 345 | newNode.scaling = ScaleFromMatrix(newNode.tmatrix); 346 | 347 | break; 348 | } 349 | #endregion 350 | #region mesh node 351 | case 2: //mesh 352 | { 353 | byte bbyte = modelStream.ReadByte(); 354 | byte cbyte = modelStream.ReadByte(); 355 | byte dbyte = modelStream.ReadByte(); 356 | 357 | newNode.vertexCount = modelStream.ReadInt32(); 358 | newNode.position = new float[newNode.vertexCount * 3]; 359 | newNode.normal = new float[newNode.vertexCount * 3]; 360 | newNode.texture0 = new float[newNode.vertexCount * 2]; 361 | 362 | for (int v = 0; v < newNode.vertexCount; v++) 363 | { 364 | newNode.position[v * 3] = modelStream.ReadSingle(); 365 | newNode.position[v * 3 + 1] = modelStream.ReadSingle(); 366 | newNode.position[v * 3 + 2] = modelStream.ReadSingle(); 367 | 368 | newNode.normal[v * 3] = modelStream.ReadSingle(); 369 | newNode.normal[v * 3 + 1] = modelStream.ReadSingle(); 370 | newNode.normal[v * 3 + 2] = modelStream.ReadSingle(); 371 | 372 | newNode.texture0[v * 2] = modelStream.ReadSingle(); 373 | newNode.texture0[v * 2 + 1] = 1 - modelStream.ReadSingle(); 374 | 375 | modelStream.BaseStream.Position += 12; //tangents 376 | } 377 | 378 | int indexCount = modelStream.ReadInt32(); 379 | newNode.indices = new ushort[indexCount]; 380 | for (int i = 0; i < indexCount; i++) 381 | { 382 | newNode.indices[i] = modelStream.ReadUInt16(); 383 | } 384 | 385 | newNode.materialID = modelStream.ReadInt32(); 386 | modelStream.BaseStream.Position += 29; 387 | 388 | break; 389 | } 390 | #endregion 391 | #region animated mesh 392 | case 3: //animated mesh 393 | { 394 | byte bbyte = modelStream.ReadByte(); 395 | byte cbyte = modelStream.ReadByte(); 396 | byte dbyte = modelStream.ReadByte(); 397 | 398 | int boneCount = modelStream.ReadInt32(); 399 | for (int b = 0; b < boneCount; b++) 400 | { 401 | string boneName = ReadStr(modelStream, modelStream.ReadInt32()); 402 | modelStream.BaseStream.Position += 64; //transformation matrix 403 | } 404 | 405 | newNode.vertexCount = modelStream.ReadInt32(); 406 | newNode.position = new float[newNode.vertexCount * 3]; 407 | newNode.normal = new float[newNode.vertexCount * 3]; 408 | newNode.texture0 = new float[newNode.vertexCount * 2]; 409 | 410 | for (int v = 0; v < newNode.vertexCount; v++) 411 | { 412 | newNode.position[v * 3] = modelStream.ReadSingle(); 413 | newNode.position[v * 3 + 1] = modelStream.ReadSingle(); 414 | newNode.position[v * 3 + 2] = modelStream.ReadSingle(); 415 | 416 | newNode.normal[v * 3] = modelStream.ReadSingle(); 417 | newNode.normal[v * 3 + 1] = modelStream.ReadSingle(); 418 | newNode.normal[v * 3 + 2] = modelStream.ReadSingle(); 419 | 420 | newNode.texture0[v * 2] = modelStream.ReadSingle(); 421 | newNode.texture0[v * 2 + 1] = 1 - modelStream.ReadSingle(); 422 | 423 | modelStream.BaseStream.Position += 44; //tangents & weights 424 | } 425 | 426 | int indexCount = modelStream.ReadInt32(); 427 | newNode.indices = new ushort[indexCount]; 428 | for (int i = 0; i < indexCount; i++) 429 | { 430 | newNode.indices[i] = modelStream.ReadUInt16(); 431 | } 432 | 433 | newNode.materialID = modelStream.ReadInt32(); 434 | modelStream.BaseStream.Position += 12; 435 | 436 | break; 437 | } 438 | #endregion 439 | } 440 | 441 | if (parentID < 0) { newNode.hmatrix = newNode.tmatrix; } 442 | else { newNode.hmatrix = matrixMult(newNode.tmatrix, nodeList[parentID].hmatrix); } 443 | 444 | nodeList.Add(newNode); 445 | int currentID = nodeList.IndexOf(newNode); 446 | 447 | for (int c = 0; c < childrenCount; c++) 448 | { 449 | readNodes(modelStream, nodeList, currentID); 450 | } 451 | } 452 | 453 | private static float[,] matrixMult(float[,] ma, float[,] mb) 454 | { 455 | float[,] mm = new float[4, 4]; 456 | 457 | for (int i = 0; i < 4; i++) 458 | { 459 | for (int j = 0; j < 4; j++) 460 | { 461 | mm[i, j] = ma[i, 0] * mb[0, j] + ma[i, 1] * mb[1, j] + ma[i, 2] * mb[2, j] + ma[i, 3] * mb[3, j]; 462 | } 463 | } 464 | 465 | /* 466 | mm[0, 0] = ma00*mb00 + ma01*mb10 + ma02*mb20 + ma03*mb30 467 | mm[0, 1] = ma00*mb01 + ma01*mb11 + ma02*mb21 + ma03*mb31 468 | mm[0, 2] = ma00*mb02 + ma01*mb12 + ma02*mb22 + ma03*mb32 469 | mm[0, 3] = ma00*mb03 + ma01*mb13 + ma02*mb23 + ma03*mb33 470 | 471 | mm[1, 1] = ma10*mb00 + ma11*mb10 + ma12*mb20 + ma13*mb30 472 | mm[1, 1] = ma10*mb01 + ma11*mb11 + ma12*mb21 + ma13*mb31 473 | mm[1, 2] = ma10*mb02 + ma11*mb12 + ma12*mb22 + ma13*mb32 474 | mm[1, 3] = ma10*mb03 + ma11*mb13 + ma12*mb23 + ma13*mb33 475 | 476 | mm[2, 0] = ma20*mb00 + ma21*mb10 + ma22*mb20 + ma23*mb30 477 | mm[2, 1] = ma20*mb01 + ma21*mb11 + ma22*mb21 + ma23*mb31 478 | mm[2, 2] = ma20*mb02 + ma21*mb12 + ma22*mb22 + ma23*mb32 479 | mm[2, 3] = ma20*mb03 + ma21*mb13 + ma22*mb23 + ma23*mb33 480 | 481 | mm[3, 0] = ma30*mb00 + ma31*mb10 + ma32*mb20 + ma33*mb30 482 | mm[3, 1] = ma30*mb01 + ma31*mb11 + ma32*mb21 + ma33*mb31 483 | mm[3, 2] = ma30*mb02 + ma31*mb12 + ma32*mb22 + ma33*mb32 484 | mm[3, 3] = ma30*mb03 + ma31*mb13 + ma32*mb23 + ma33*mb33*/ 485 | 486 | return mm; 487 | } 488 | 489 | private static float[] MatrixToEuler(float[,] transf) 490 | { 491 | double heading = 0; 492 | double attitude = 0; 493 | double bank = 0; 494 | //original code by Martin John Baker for right-handed coordinate system 495 | /*if (transf[0, 1] > 0.998) 496 | { // singularity at north pole 497 | heading = Math.Atan2(transf[0, 2], transf[2, 2]); 498 | attitude = Math.PI / 2; 499 | bank = 0; 500 | } 501 | if (transf[0, 1] < -0.998) 502 | { // singularity at south pole 503 | heading = Math.Atan2(transf[0, 2], transf[2, 2]); 504 | attitude = -Math.PI / 2; 505 | bank = 0; 506 | } 507 | 508 | heading = Math.Atan2(-transf[2, 0], transf[0, 0]); 509 | bank = Math.Atan2(-transf[1, 2], transf[1, 1]); 510 | attitude = Math.Asin(transf[1, 0]);*/ 511 | 512 | //left handed 513 | if (transf[0, 1] > 0.998) 514 | { // singularity at north pole 515 | heading = Math.Atan2(-transf[1, 0], transf[1, 1]); 516 | attitude = -Math.PI / 2; 517 | bank = 0; 518 | } 519 | else if (transf[0, 1] < -0.998) 520 | { // singularity at south pole 521 | heading = Math.Atan2(-transf[1, 0], transf[1, 1]); 522 | attitude = Math.PI / 2; 523 | bank = 0; 524 | } 525 | else 526 | { 527 | heading = Math.Atan2(transf[0, 1], transf[0, 0]); 528 | bank = Math.Atan2(transf[1, 2], transf[2, 2]); 529 | attitude = Math.Asin(-transf[0, 2]); 530 | } 531 | 532 | 533 | //alternative code by Mike Day, Insomniac Games 534 | /*bank = Math.Atan2(transf[1, 2], transf[2, 2]); 535 | 536 | double c2 = Math.Sqrt(transf[0, 0] * transf[0, 0] + transf[0, 1] * transf[0, 1]); 537 | attitude = Math.Atan2(-transf[0, 2], c2); 538 | 539 | double s1 = Math.Sin(bank); 540 | double c1 = Math.Cos(bank); 541 | heading = Math.Atan2(s1 * transf[2, 0] - c1 * transf[1, 0], c1 * transf[1, 1] - s1 * transf[2, 1]);*/ 542 | 543 | attitude *= 180 / Math.PI; 544 | heading *= 180 / Math.PI; 545 | bank *= 180 / Math.PI; 546 | 547 | return new float[3] { (float)bank, (float)attitude, (float)heading }; 548 | } 549 | 550 | private static float[] ScaleFromMatrix(float[,] transf) 551 | { 552 | double scaleX = Math.Sqrt(transf[0, 0] * transf[0, 0] + transf[1, 0] * transf[1, 0] + transf[2, 0] * transf[2, 0]); 553 | double scaleY = Math.Sqrt(transf[0, 1] * transf[0, 1] + transf[1, 1] * transf[1, 1] + transf[2, 1] * transf[2, 1]); 554 | double scaleZ = Math.Sqrt(transf[0, 2] * transf[0, 2] + transf[1, 2] * transf[1, 2] + transf[2, 2] * transf[2, 2]); 555 | 556 | return new float[3] { (float)scaleX, (float)scaleY, (float)scaleZ }; 557 | } 558 | 559 | private static string ReadStr(BinaryReader str, int len) 560 | { 561 | //int len = str.ReadInt32(); 562 | byte[] stringData = new byte[len]; 563 | str.Read(stringData, 0, len); 564 | var result = System.Text.Encoding.UTF8.GetString(stringData); 565 | return result; 566 | } 567 | 568 | 569 | private static void ExportOBJ(kn5Model srcModel, bool ZMhack) 570 | { 571 | string modelFilename = srcModel.modelName; 572 | if (ZMhack) { modelFilename += "_ZMhack"; } 573 | 574 | if (!File.Exists(srcModel.modelDir + modelFilename + ".obj")) 575 | { 576 | Console.WriteLine("Exporting {0}.obj", modelFilename); 577 | 578 | #region write MTL 579 | using (StreamWriter MTLwriter = new StreamWriter(File.Create(srcModel.modelDir + modelFilename + ".mtl"))) 580 | { 581 | StringBuilder sb = new StringBuilder(); 582 | 583 | foreach (var srcMat in srcModel.materials) 584 | { 585 | sb.AppendFormat("newmtl {0}\r\n", srcMat.name.Replace(' ', '_')); 586 | sb.AppendFormat("Ka {0} {0} {0}\r\n", srcMat.ksAmbient); 587 | sb.AppendFormat("Kd {0} {0} {0}\r\n", srcMat.ksDiffuse); 588 | sb.AppendFormat("Ks {0} {0} {0}\r\n", srcMat.ksSpecular); 589 | sb.AppendFormat("Ns {0}\r\n", srcMat.ksSpecularEXP); 590 | sb.AppendFormat("illum 2\r\n", srcMat.ksSpecular); 591 | //add function to search for textures and get relative path 592 | if (srcMat.useDetail == 1.0f && srcMat.txDetail != null) 593 | { 594 | sb.AppendFormat("map_Kd texture\\{0}\r\n", srcMat.txDetail); 595 | if (srcMat.txDiffuse != null) { sb.AppendFormat("map_Ks texture\\{0}\r\n", srcMat.txDiffuse); } 596 | } 597 | else if (srcMat.txDiffuse != null) { sb.AppendFormat("map_Kd texture\\{0}\r\n", srcMat.txDiffuse); } 598 | if (srcMat.txNormal != null) { sb.AppendFormat("bump texture\\{0}\r\n", srcMat.txNormal); } 599 | sb.Append("\r\n"); 600 | } 601 | 602 | MTLwriter.Write(sb); 603 | } 604 | #endregion 605 | 606 | #region write OBJ 607 | using (StreamWriter OBJwriter = new StreamWriter(File.Create(srcModel.modelDir + modelFilename + ".obj"))) 608 | { 609 | StringBuilder sb = new StringBuilder(); 610 | sb.AppendFormat("# Assetto Corsa model\r\n# Exported with kn5 Converter by Chipicao on {0}\r\n", DateTime.Now); 611 | sb.AppendFormat("\r\nmtllib {0}.mtl\r\n", modelFilename); 612 | 613 | int vertexPad = 1; 614 | 615 | foreach (var srcNode in srcModel.nodes) 616 | { 617 | switch (srcNode.type) 618 | { 619 | case 1: 620 | { 621 | if (ZMhack) 622 | { 623 | //create dummy box 624 | srcNode.vertexCount = 24; 625 | srcNode.position = new float[72] { 0.05f, -0.05f, 0.05f, 0.05f, 0.05f, 0.05f, -0.05f, 0.05f, 0.05f, -0.05f, -0.05f, 0.05f, 0.05f, 0.05f, -0.05f, 0.05f, 0.05f, 0.05f, 0.05f, -0.05f, 0.05f, 0.05f, -0.05f, -0.05f, -0.05f, 0.05f, 0.05f, 0.05f, 0.05f, 0.05f, 0.05f, 0.05f, -0.05f, -0.05f, 0.05f, -0.05f, -0.05f, 0.05f, -0.05f, 0.05f, 0.05f, -0.05f, 0.05f, -0.05f, -0.05f, -0.05f, -0.05f, -0.05f, -0.05f, -0.05f, 0.05f, -0.05f, 0.05f, 0.05f, -0.05f, 0.05f, -0.05f, -0.05f, -0.05f, -0.05f, 0.05f, -0.05f, -0.05f, 0.05f, -0.05f, 0.05f, -0.05f, -0.05f, 0.05f, -0.05f, -0.05f, -0.05f }; 626 | srcNode.normal = new float[72] { -0f, 0f, 1f, -0f, 0f, 1f, -0f, 0f, 1f, -0f, 0f, 1f, 1f, 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, -0f, 1f, 0f, -0f, 1f, 0f, -0f, 1f, 0f, -0f, 1f, 0f, -0f, 0f, -1f, -0f, 0f, -1f, -0f, 0f, -1f, -0f, 0f, -1f, -1f, 0f, 0f, -1f, 0f, 0f, -1f, 0f, 0f, -1f, 0f, 0f, -0f, -1f, 0f, -0f, -1f, 0f, -0f, -1f, 0f, -0f, -1f, 0f }; 627 | srcNode.texture0 = new float[48] { 0f, 1f, 1f, 1f, 1f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f, 0f, 0f, 0f, 1f, 1f, 1f, 1f, 0f, 0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f, 0f, 0f }; 628 | srcNode.indices = new ushort[36] { 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 }; 629 | goto case 3; 630 | } 631 | break; 632 | } 633 | case 2: 634 | case 3: 635 | { 636 | sb.AppendFormat("\r\ng {0}", srcNode.name.Replace(' ', '_')); 637 | if (ZMhack && srcNode.parentID >= 0) { sb.AppendFormat(" {0}", srcModel.nodes[srcNode.parentID].name.Replace(' ', '_')); } 638 | sb.AppendFormat("\r\n"); 639 | 640 | for (int v = 0; v < srcNode.vertexCount; v++) 641 | { 642 | var x = srcNode.position[v * 3]; 643 | var y = srcNode.position[v * 3 + 1]; 644 | var z = srcNode.position[v * 3 + 2]; 645 | 646 | float vx = srcNode.hmatrix[0, 0] * x + srcNode.hmatrix[1, 0] * y + srcNode.hmatrix[2, 0] * z + srcNode.hmatrix[3, 0]; 647 | float vy = srcNode.hmatrix[0, 1] * x + srcNode.hmatrix[1, 1] * y + srcNode.hmatrix[2, 1] * z + srcNode.hmatrix[3, 1]; 648 | float vz = srcNode.hmatrix[0, 2] * x + srcNode.hmatrix[1, 2] * y + srcNode.hmatrix[2, 2] * z + srcNode.hmatrix[3, 2]; 649 | 650 | sb.AppendFormat("v {0} {1} {2}\r\n", vx, vy, vz); 651 | } 652 | OBJwriter.Write(sb); 653 | sb.Length = 0; 654 | 655 | for (int v = 0; v < srcNode.vertexCount; v++) 656 | { 657 | var x = srcNode.normal[v * 3]; 658 | var y = srcNode.normal[v * 3 + 1]; 659 | var z = srcNode.normal[v * 3 + 2]; 660 | 661 | //transforming normal vectors requires the transposed inverse matrix 662 | //transformation matrices SHOULD be normalized (assumption, mother of all fuckups) 663 | //so in theory, the inverse of a normalized matrix is the transposed matrix, which transposed again is the matrix itself 664 | float nx = srcNode.hmatrix[0, 0] * x + srcNode.hmatrix[1, 0] * y + srcNode.hmatrix[2, 0] * z; 665 | float ny = srcNode.hmatrix[0, 1] * x + srcNode.hmatrix[1, 1] * y + srcNode.hmatrix[2, 1] * z; 666 | float nz = srcNode.hmatrix[0, 2] * x + srcNode.hmatrix[1, 2] * y + srcNode.hmatrix[2, 2] * z; 667 | 668 | sb.AppendFormat("vn {0} {1} {2}\r\n", nx, ny, nz); 669 | } 670 | OBJwriter.Write(sb); 671 | sb.Length = 0; 672 | 673 | float UVmult = 1.0f; 674 | if (srcNode.materialID >= 0) 675 | { 676 | if (srcModel.materials[srcNode.materialID].useDetail == 0.0f) { UVmult = srcModel.materials[srcNode.materialID].diffuseMult; } 677 | else { UVmult = srcModel.materials[srcNode.materialID].detailUVMultiplier; } 678 | } 679 | 680 | for (int v = 0; v < srcNode.vertexCount; v++) 681 | { 682 | var tx = srcNode.texture0[v * 2] * UVmult; 683 | var ty = srcNode.texture0[v * 2 + 1] * UVmult; 684 | 685 | sb.AppendFormat("vt {0} {1}\r\n", tx, ty); 686 | } 687 | OBJwriter.Write(sb); 688 | sb.Length = 0; 689 | 690 | if (srcNode.materialID >= 0) { sb.AppendFormat("\r\nusemtl {0}\r\n", srcModel.materials[srcNode.materialID].name.Replace(' ', '_')); } 691 | else { sb.AppendFormat("\r\nusemtl Default\r\n"); } 692 | for (int i = 0; i < srcNode.indices.Length / 3; i++) 693 | { 694 | var i1 = srcNode.indices[i * 3] + vertexPad; 695 | var i2 = srcNode.indices[i * 3 + 1] + vertexPad; 696 | var i3 = srcNode.indices[i * 3 + 2] + vertexPad; 697 | 698 | sb.AppendFormat("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\r\n", i1, i2, i3); 699 | } 700 | OBJwriter.Write(sb); 701 | sb.Length = 0; 702 | 703 | vertexPad += srcNode.vertexCount; 704 | break; 705 | } 706 | } 707 | } 708 | } 709 | #endregion 710 | } 711 | else { Console.WriteLine("File already exists: {0}.obj", modelFilename); } 712 | } 713 | 714 | private static void ExportFBX(kn5Model srcModel) 715 | { 716 | if (!File.Exists(srcModel.modelDir + srcModel.modelName + ".fbx")) 717 | { 718 | Console.WriteLine("Exporting {0}.fbx", srcModel.modelName); 719 | 720 | using (StreamWriter FBXwriter = new StreamWriter(File.Create(srcModel.modelDir + srcModel.modelName + ".fbx"))) 721 | { 722 | StringBuilder fbx = new StringBuilder(); 723 | var timestamp = DateTime.Now; 724 | 725 | StringBuilder ob = new StringBuilder(); //objects builder 726 | //ob.Append("\nObjects: {"); 727 | 728 | StringBuilder cb = new StringBuilder(); //connections builder 729 | cb.Append("\n}\n");//Objects end 730 | cb.Append("\nConnections: {"); 731 | 732 | int FBXgeometryCount = 0; 733 | foreach (var srcNode in srcModel.nodes.Where(n => n.type > 1)) { FBXgeometryCount++; } 734 | 735 | #region build materials first to get used texture count 736 | for (int m = 0; m < srcModel.materials.Count; m++) 737 | { 738 | var srcMat = srcModel.materials[m]; 739 | 740 | ob.Append("\n\tMaterial: " + (400000 + m) + ", \"Material::" + srcMat.name + "\", \"\" {"); 741 | ob.Append("\n\t\tVersion: 102"); 742 | ob.Append("\n\t\tShadingModel: \"phong\""); 743 | ob.Append("\n\t\tMultiLayer: 0"); 744 | ob.Append("\n\t\tProperties70: {"); 745 | ob.Append("\n\t\t\tP: \"ShadingModel\", \"KString\", \"\", \"\", \"phong\""); 746 | ob.AppendFormat("\n\t\t\tP: \"AmbientColor\", \"Color\", \"\", \"A\",{0},{0},{0}", srcMat.ksAmbient); 747 | ob.AppendFormat("\n\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",{0},{0},{0}", srcMat.ksDiffuse); 748 | ob.AppendFormat("\n\t\t\tP: \"SpecularColor\", \"Color\", \"\", \"A\",{0},{0},{0}", srcMat.ksSpecular); 749 | ob.AppendFormat("\n\t\t\tP: \"SpecularFactor\", \"Number\", \"\", \"A\",{0}", srcMat.ksSpecularEXP / 100f); 750 | //ob.AppendFormat("\n\t\t\tP: \"ShininessExponent\", \"Number\", \"\", \"A\",{0}", srcMat.ksSpecularEXP); 751 | //ob.AppendFormat("\n\t\t\tP: \"Shininess\", \"double\", \"Number\", \"\",{0}", srcMat.ksSpecularEXP); 752 | ob.Append("\n\t\t}"); 753 | ob.Append("\n\t}"); 754 | 755 | //try to provide some level of texture instancing by using a custom class to match file and UV scaling 756 | 757 | int txDiffuseID = srcModel.usedTex.FindIndex(s => s.filename == srcMat.txDiffuse && s.UVScaling == srcMat.diffuseMult); 758 | if (txDiffuseID < 0 && srcMat.txDiffuse != null) //add texture to instance list 759 | { 760 | srcModel.usedTex.Add(new kn5Texture() { filename = srcMat.txDiffuse, UVScaling = srcMat.diffuseMult }); 761 | txDiffuseID = srcModel.usedTex.Count - 1; 762 | } 763 | 764 | if (srcMat.useDetail == 1.0f && srcMat.txDetail != null) 765 | { 766 | int txDetailID = srcModel.usedTex.FindIndex(s => s.filename == srcMat.txDetail && s.UVScaling == srcMat.detailUVMultiplier); 767 | if (txDetailID < 0) 768 | { 769 | srcModel.usedTex.Add(new kn5Texture() { filename = srcMat.txDetail, UVScaling = srcMat.detailUVMultiplier }); 770 | txDetailID = srcModel.usedTex.Count - 1; 771 | } 772 | 773 | cb.Append("\n\tC: \"OP\"," + (500000 + txDetailID) + "," + (400000 + m) + ", \"DiffuseColor\""); 774 | 775 | //use txDiffuse(AO) as specular map 776 | cb.Append("\n\tC: \"OP\"," + (500000 + txDiffuseID) + "," + (400000 + m) + ", \"SpecularColor\""); 777 | } 778 | else 779 | { 780 | cb.Append("\n\tC: \"OP\"," + (500000 + txDiffuseID) + "," + (400000 + m) + ", \"DiffuseColor\""); 781 | 782 | cb.Append("\n\tC: \"OP\"," + (500000 + txDiffuseID) + "," + (400000 + m) + ", \"TransparentColor\""); 783 | } 784 | 785 | if (srcMat.txNormal != null) 786 | { 787 | int txNormalID = srcModel.usedTex.FindIndex(s => s.filename == srcMat.txNormal && s.UVScaling == srcMat.normalMult); 788 | if (txNormalID < 0) 789 | { 790 | srcModel.usedTex.Add(new kn5Texture() { filename = srcMat.txNormal, UVScaling = srcMat.normalMult }); 791 | txNormalID = srcModel.usedTex.Count - 1; 792 | } 793 | 794 | cb.Append("\n\tC: \"OP\"," + (500000 + txNormalID) + "," + (400000 + m) + ", \"NormalMap\""); 795 | } 796 | } 797 | #endregion 798 | 799 | #region write generic FBX data 800 | fbx.Append("; FBX 7.1.0 project file"); 801 | fbx.Append("\nFBXHeaderExtension: {\n\tFBXHeaderVersion: 1003\n\tFBXVersion: 7100\n\tCreationTimeStamp: {\n\t\tVersion: 1000"); 802 | fbx.Append("\n\t\tYear: " + timestamp.Year); 803 | fbx.Append("\n\t\tMonth: " + timestamp.Month); 804 | fbx.Append("\n\t\tDay: " + timestamp.Day); 805 | fbx.Append("\n\t\tHour: " + timestamp.Hour); 806 | fbx.Append("\n\t\tMinute: " + timestamp.Minute); 807 | fbx.Append("\n\t\tSecond: " + timestamp.Second); 808 | fbx.Append("\n\t\tMillisecond: " + timestamp.Millisecond); 809 | fbx.Append("\n\t}\n\tCreator: \"kn5 converter by Chipicao\"\n}\n"); 810 | 811 | fbx.Append("\nGlobalSettings: {"); 812 | fbx.Append("\n\tVersion: 1000"); 813 | fbx.Append("\n\tProperties70: {"); 814 | fbx.Append("\n\t\tP: \"UpAxis\", \"int\", \"Integer\", \"\",1"); 815 | fbx.Append("\n\t\tP: \"UpAxisSign\", \"int\", \"Integer\", \"\",1"); 816 | fbx.Append("\n\t\tP: \"FrontAxis\", \"int\", \"Integer\", \"\",2"); 817 | fbx.Append("\n\t\tP: \"FrontAxisSign\", \"int\", \"Integer\", \"\",1"); 818 | fbx.Append("\n\t\tP: \"CoordAxis\", \"int\", \"Integer\", \"\",0"); 819 | fbx.Append("\n\t\tP: \"CoordAxisSign\", \"int\", \"Integer\", \"\",1"); 820 | fbx.Append("\n\t\tP: \"OriginalUpAxis\", \"int\", \"Integer\", \"\",1"); 821 | fbx.Append("\n\t\tP: \"OriginalUpAxisSign\", \"int\", \"Integer\", \"\",1"); 822 | fbx.Append("\n\t\tP: \"UnitScaleFactor\", \"double\", \"Number\", \"\",1"); 823 | fbx.Append("\n\t\tP: \"OriginalUnitScaleFactor\", \"double\", \"Number\", \"\",1"); 824 | //sb.Append("\n\t\tP: \"AmbientColor\", \"ColorRGB\", \"Color\", \"\",0,0,0"); 825 | //sb.Append("\n\t\tP: \"DefaultCamera\", \"KString\", \"\", \"\", \"Producer Perspective\""); 826 | //sb.Append("\n\t\tP: \"TimeMode\", \"enum\", \"\", \"\",6"); 827 | //sb.Append("\n\t\tP: \"TimeProtocol\", \"enum\", \"\", \"\",2"); 828 | //sb.Append("\n\t\tP: \"SnapOnFrameMode\", \"enum\", \"\", \"\",0"); 829 | //sb.Append("\n\t\tP: \"TimeSpanStart\", \"KTime\", \"Time\", \"\",0"); 830 | //sb.Append("\n\t\tP: \"TimeSpanStop\", \"KTime\", \"Time\", \"\",153953860000"); 831 | //sb.Append("\n\t\tP: \"CustomFrameRate\", \"double\", \"Number\", \"\",-1"); 832 | //sb.Append("\n\t\tP: \"TimeMarker\", \"Compound\", \"\", \"\""); 833 | //sb.Append("\n\t\tP: \"CurrentTimeMarker\", \"int\", \"Integer\", \"\",-1"); 834 | fbx.Append("\n\t}\n}\n"); 835 | 836 | fbx.Append("\nDocuments: {"); 837 | fbx.Append("\n\tCount: 1"); 838 | fbx.Append("\n\tDocument: 1234567890, \"\", \"Scene\" {"); 839 | fbx.Append("\n\t\tProperties70: {"); 840 | fbx.Append("\n\t\t\tP: \"SourceObject\", \"object\", \"\", \"\""); 841 | fbx.Append("\n\t\t\tP: \"ActiveAnimStackName\", \"KString\", \"\", \"\", \"\""); 842 | fbx.Append("\n\t\t}"); 843 | fbx.Append("\n\t\tRootNode: 0"); 844 | fbx.Append("\n\t}\n}\n"); 845 | fbx.Append("\nReferences: {\n}\n"); 846 | 847 | fbx.Append("\nDefinitions: {"); 848 | fbx.Append("\n\tVersion: 100"); 849 | fbx.AppendFormat("\n\tCount: {0}", 1 + srcModel.nodes.Count + FBXgeometryCount + srcModel.materials.Count + srcModel.usedTex.Count * 2); 850 | 851 | fbx.Append("\n\tObjectType: \"GlobalSettings\" {"); 852 | fbx.Append("\n\t\tCount: 1"); 853 | fbx.Append("\n\t}"); 854 | 855 | fbx.Append("\n\tObjectType: \"Model\" {"); 856 | fbx.Append("\n\t\tCount: " + srcModel.nodes.Count); 857 | fbx.Append("\n\t}"); 858 | 859 | fbx.Append("\n\tObjectType: \"Geometry\" {"); 860 | fbx.Append("\n\t\tCount: " + FBXgeometryCount); 861 | fbx.Append("\n\t}"); 862 | 863 | fbx.Append("\n\tObjectType: \"Material\" {"); 864 | fbx.Append("\n\t\tCount: " + srcModel.materials.Count); 865 | fbx.Append("\n\t}"); 866 | 867 | fbx.Append("\n\tObjectType: \"Texture\" {"); 868 | fbx.Append("\n\t\tCount: " + srcModel.usedTex.Count); 869 | fbx.Append("\n\t}"); 870 | 871 | fbx.Append("\n\tObjectType: \"Video\" {"); 872 | fbx.Append("\n\t\tCount: " + srcModel.usedTex.Count); 873 | fbx.Append("\n\t}"); 874 | fbx.Append("\n}\n"); 875 | fbx.Append("\nObjects: {"); 876 | 877 | FBXwriter.Write(fbx); 878 | fbx.Length = 0; 879 | //write previously built materials 880 | FBXwriter.Write(ob); 881 | ob.Length = 0; 882 | #endregion 883 | 884 | #region write Texture & Video data 885 | for (int t = 0; t < srcModel.usedTex.Count; t++) 886 | { 887 | string textureName = srcModel.usedTex[t].filename; 888 | string textureFile = srcModel.modelDir + "texture\\" + textureName; 889 | string relativePath = "texture\\" + textureName; 890 | 891 | //search for texture if doesn't exist 892 | //later 893 | 894 | ob.Append("\n\tTexture: " + (500000 + t) + ", \"Texture::" + textureName + "\", \"\" {"); 895 | ob.Append("\n\t\tType: \"TextureVideoClip\""); 896 | ob.Append("\n\t\tVersion: 202"); 897 | ob.Append("\n\t\tTextureName: \"Texture::" + textureName + "\""); 898 | ob.Append("\n\t\tProperties70: {"); 899 | ob.AppendFormat("\n\t\t\tP: \"Translation\", \"Vector\", \"\", \"A\",{0},{0},1", 0.5f * (1 - srcModel.usedTex[t].UVScaling)); 900 | ob.AppendFormat("\n\t\t\tP: \"Scaling\", \"Vector\", \"\", \"A\",{0},{0},1", srcModel.usedTex[t].UVScaling); 901 | ob.Append("\n\t\t\tP: \"UVSet\", \"KString\", \"\", \"\", \"UVChannel_1\""); 902 | ob.Append("\n\t\t\tP: \"UseMaterial\", \"bool\", \"\", \"\",1"); 903 | ob.Append("\n\t\t}"); 904 | ob.Append("\n\t\tMedia: \"Video::" + textureName + "\""); 905 | ob.Append("\n\t\tFileName: \"" + textureFile + "\""); 906 | ob.Append("\n\t\tRelativeFilename: \"" + relativePath + "\""); 907 | ob.Append("\n\t\tTexture_Alpha_Source: \"Alpha_Black\""); 908 | ob.Append("\n\t}"); 909 | 910 | ob.Append("\n\tVideo: " + (600000 + t) + ", \"Video::" + textureName + "\", \"Clip\" {"); 911 | ob.Append("\n\t\tType: \"Clip\""); 912 | ob.Append("\n\t\tProperties70: {"); 913 | ob.Append("\n\t\t\tP: \"Path\", \"KString\", \"XRefUrl\", \"\", \"" + textureFile + "\""); 914 | ob.Append("\n\t\t}"); 915 | ob.Append("\n\t\tFileName: \"" + textureFile + "\""); 916 | ob.Append("\n\t\tRelativeFilename: \"" + relativePath + "\""); 917 | ob.Append("\n\t}"); 918 | 919 | //connect video to texture 920 | cb.Append("\n\tC: \"OO\"," + (600000 + t) + "," + (500000 + t)); 921 | } 922 | 923 | FBXwriter.Write(ob); 924 | ob.Length = 0; 925 | #endregion 926 | 927 | #region write Model & Geometry data 928 | for (int n = 0; n < srcModel.nodes.Count; n++) 929 | { 930 | var srcNode = srcModel.nodes[n]; 931 | 932 | #region if Mesh node 933 | if (srcNode.type > 1) 934 | { 935 | StringBuilder vb = new StringBuilder(); 936 | StringBuilder ib = new StringBuilder(); 937 | 938 | //write Geometry 939 | ob.Append("\n\tGeometry: " + (100000 + n) + ", \"Geometry::\", \"Mesh\" {"); 940 | ob.Append("\n\t\tProperties70: {"); 941 | var randomColor = RandomColorGenerator((100000 + n).ToString()); 942 | ob.AppendFormat("\n\t\t\tP: \"Color\", \"ColorRGB\", \"Color\", \"\",{0},{1},{2}", ((float)randomColor[0] / 255), ((float)randomColor[1] / 255), ((float)randomColor[2] / 255)); 943 | ob.Append("\n\t\t}"); 944 | 945 | ob.AppendFormat("\n\t\tVertices: *{0} {{\n\t\t\ta: ", (srcNode.vertexCount * 3)); 946 | foreach (var v in srcNode.position) { vb.AppendFormat("{0},", v); } 947 | vb.Length -= 1; //remove last , 948 | ob.Append(SplitLine(vb.ToString())); 949 | ob.Append("\n\t\t}"); 950 | vb.Length = 0; 951 | 952 | ob.AppendFormat("\n\t\tPolygonVertexIndex: *{0} {{\n\t\t\ta: ", srcNode.indices.Length); 953 | for (int f = 0; f < (srcNode.indices.Length / 3); f++) 954 | { 955 | ib.Append(srcNode.indices[f * 3]); 956 | ib.Append(','); 957 | ib.Append(srcNode.indices[f * 3 + 1]); 958 | ib.Append(','); 959 | ib.Append(-1 - srcNode.indices[f * 3 + 2]); 960 | ib.Append(','); 961 | } 962 | ib.Length -= 1; //remove last , 963 | ob.Append(SplitLine(ib.ToString())); 964 | ob.Append("\n\t\t}"); 965 | ib.Length = 0; 966 | ob.Append("\n\t\tGeometryVersion: 124"); 967 | 968 | ob.Append("\n\t\tLayerElementNormal: 0 {"); 969 | ob.Append("\n\t\t\tVersion: 101"); 970 | ob.Append("\n\t\t\tName: \"\""); 971 | ob.Append("\n\t\t\tMappingInformationType: \"ByVertice\""); 972 | ob.Append("\n\t\t\tReferenceInformationType: \"Direct\""); 973 | ob.AppendFormat("\n\t\t\tNormals: *{0} {{\n\t\t\ta: ", (srcNode.vertexCount * 3)); 974 | foreach (var v in srcNode.normal) { vb.AppendFormat("{0},", v); } 975 | vb.Length -= 1; //remove last , 976 | ob.Append(SplitLine(vb.ToString())); 977 | ob.Append("\n\t\t\t}\n\t\t}"); 978 | vb.Length = 0; 979 | 980 | ob.Append("\n\t\tLayerElementUV: 0 {"); 981 | ob.Append("\n\t\t\tVersion: 101"); 982 | ob.Append("\n\t\t\tName: \"UVChannel_0\""); 983 | ob.Append("\n\t\t\tMappingInformationType: \"ByVertice\""); 984 | ob.Append("\n\t\t\tReferenceInformationType: \"Direct\""); 985 | ob.AppendFormat("\n\t\t\tUV: *{0} {{\n\t\t\ta: ", (srcNode.vertexCount * 2)); 986 | foreach (var v in srcNode.texture0) { vb.AppendFormat("{0},", v); } 987 | vb.Length -= 1; //remove last , 988 | ob.Append(SplitLine(vb.ToString())); 989 | ob.Append("\n\t\t\t}\n\t\t}"); 990 | vb.Length = 0; 991 | 992 | ob.Append("\n\t\tLayerElementMaterial: 0 {"); 993 | ob.Append("\n\t\t\tVersion: 101"); 994 | ob.Append("\n\t\t\tName: \"\""); 995 | ob.Append("\n\t\t\tMappingInformationType: \"AllSame\""); 996 | ob.Append("\n\t\t\tReferenceInformationType: \"IndexToDirect\""); 997 | ob.Append("\n\t\t\tMaterials: *1 {"); 998 | ob.Append("\n\t\t\t\t0"); 999 | ob.Append("\n\t\t\t}"); 1000 | ob.Append("\n\t\t}"); 1001 | 1002 | ob.Append("\n\t\tLayer: 0 {"); 1003 | ob.Append("\n\t\t\tVersion: 100"); 1004 | ob.Append("\n\t\t\tLayerElement: {"); 1005 | ob.Append("\n\t\t\t\tType: \"LayerElementNormal\""); 1006 | ob.Append("\n\t\t\t\tTypedIndex: 0"); 1007 | ob.Append("\n\t\t\t}"); 1008 | ob.Append("\n\t\t\tLayerElement: {"); 1009 | ob.Append("\n\t\t\t\tType: \"LayerElementMaterial\""); 1010 | ob.Append("\n\t\t\t\tTypedIndex: 0"); 1011 | ob.Append("\n\t\t\t}"); 1012 | ob.Append("\n\t\t\tLayerElement: {"); 1013 | ob.Append("\n\t\t\t\tType: \"LayerElementTexture\""); 1014 | ob.Append("\n\t\t\t\tTypedIndex: 0"); 1015 | ob.Append("\n\t\t\t}"); 1016 | ob.Append("\n\t\t\tLayerElement: {"); 1017 | ob.Append("\n\t\t\t\tType: \"LayerElementBumpTextures\""); 1018 | ob.Append("\n\t\t\t\tTypedIndex: 0"); 1019 | ob.Append("\n\t\t\t}"); 1020 | ob.Append("\n\t\t\tLayerElement: {"); 1021 | ob.Append("\n\t\t\t\tType: \"LayerElementUV\""); 1022 | ob.Append("\n\t\t\t\tTypedIndex: 0"); 1023 | ob.Append("\n\t\t\t}"); 1024 | ob.Append("\n\t\t}"); //Layer 0 end 1025 | ob.Append("\n\t}"); //Geometry end 1026 | 1027 | //connect Geometry to Model 1028 | cb.Append("\n\tC: \"OO\"," + (100000 + n) + "," + (200000 + n)); 1029 | //connect Material to Model 1030 | if (srcNode.materialID > -1) { cb.Append("\n\tC: \"OO\"," + (400000 + srcNode.materialID) + "," + (200000 + n)); } 1031 | 1032 | ob.Append("\n\tModel: " + (200000 + n) + ", \"Model::" + srcNode.name + "\", \"Mesh\" {"); 1033 | } 1034 | #endregion 1035 | else { ob.Append("\n\tModel: " + (200000 + n) + ", \"Model::" + srcNode.name + "\", \"Null\" {"); } 1036 | 1037 | ob.Append("\n\t\tVersion: 232"); 1038 | ob.Append("\n\t\tProperties70: {"); 1039 | ob.Append("\n\t\t\tP: \"InheritType\", \"enum\", \"\", \"\",1"); 1040 | ob.Append("\n\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",0,0,0"); 1041 | ob.Append("\n\t\t\tP: \"DefaultAttributeIndex\", \"int\", \"Integer\", \"\",0"); 1042 | ob.Append("\n\t\t\tP: \"Lcl Translation\", \"Lcl Translation\", \"\", \"A\"," + srcNode.translation[0] + "," + srcNode.translation[1] + "," + srcNode.translation[2]); 1043 | ob.Append("\n\t\t\tP: \"Lcl Rotation\", \"Lcl Rotation\", \"\", \"A\"," + srcNode.rotation[0] + "," + srcNode.rotation[1] + "," + srcNode.rotation[2]); 1044 | ob.Append("\n\t\t\tP: \"Lcl Scaling\", \"Lcl Scaling\", \"\", \"A\"," + srcNode.scaling[0] + "," + srcNode.scaling[1] + "," + srcNode.scaling[2]); 1045 | //ob.Append("\n\t\t\tP: \"UDP3DSMAX\", \"KString\", \"\", \"U\", \"Exported_with = kn5 converter by Chipicao&cr;&lf;\""); 1046 | if (srcNode.type > 1 && srcNode.materialID > -1) 1047 | { 1048 | var srcMat = srcModel.materials[srcNode.materialID]; 1049 | /*ob.Append("\n\t\t\tP: \"UDP3DSMAX\", \"KString\", \"\", \"U\", \""); 1050 | ob.AppendFormat("diffuseMult = {0}&cr;&lf;", srcMat.diffuseMult); 1051 | ob.AppendFormat("normalMult = {0}&cr;&lf;", srcMat.normalMult); 1052 | ob.AppendFormat("useDetail = {0}&cr;&lf;", srcMat.useDetail); 1053 | ob.AppendFormat("detailUVMultiplier = {0}&cr;&lf;", srcMat.detailUVMultiplier); 1054 | ob.Append("\"");*/ 1055 | ob.AppendFormat("\n\t\t\tP: \"UDP3DSMAX\", \"KString\", \"\", \"U\", \"{0}\"", srcMat.shaderProps); 1056 | } 1057 | //ob.Append("\n\t\t\tP: \"MaxHandle\", \"int\", \"Integer\", \"UH\"," + (j + 2 + pmodel.nodeList.Count)); 1058 | ob.Append("\n\t\t}"); 1059 | ob.Append("\n\t\tShading: T"); 1060 | ob.Append("\n\t\tCulling: \"CullingOff\""); 1061 | ob.Append("\n\t}"); //Model end 1062 | 1063 | //connect Model to parent 1064 | if (srcNode.parentID < 0) { cb.Append("\n\tC: \"OO\"," + (200000 + n) + ",0"); } 1065 | else { cb.Append("\n\tC: \"OO\"," + (200000 + n) + "," + (200000 + srcNode.parentID)); } 1066 | 1067 | FBXwriter.Write(ob); 1068 | ob.Length = 0; 1069 | } 1070 | #endregion 1071 | 1072 | cb.Append("\n}");//Connections end 1073 | FBXwriter.Write(cb); 1074 | } 1075 | } 1076 | else { Console.WriteLine("File already exists: {0}.fbx", srcModel.modelName); } 1077 | } 1078 | 1079 | private static byte[] RandomColorGenerator(string name) 1080 | { 1081 | int nameHash = name.GetHashCode(); 1082 | Random r = new Random(nameHash); 1083 | //Random r = new Random(DateTime.Now.Millisecond); 1084 | 1085 | byte red = (byte)r.Next(0, 255); 1086 | byte green = (byte)r.Next(0, 255); 1087 | byte blue = (byte)r.Next(0, 255); 1088 | 1089 | return new byte[3] { red, green, blue }; 1090 | } 1091 | 1092 | private static string SplitLine(string inputLine) //for FBX 2011 1093 | { 1094 | string outputLines = inputLine; 1095 | int vbSplit = 0; 1096 | for (int v = 0; v < inputLine.Length / 2048; v++) 1097 | { 1098 | vbSplit += 2048; 1099 | if (vbSplit < outputLines.Length) 1100 | { 1101 | vbSplit = outputLines.IndexOf(",", vbSplit) + 1; 1102 | if (vbSplit > 0) { outputLines = outputLines.Insert(vbSplit, "\n"); } 1103 | } 1104 | } 1105 | return outputLines; 1106 | } 1107 | 1108 | private static string MakeRelative(string filePath, string referencePath) 1109 | { 1110 | if (filePath != "" && referencePath != "") 1111 | { 1112 | var fileUri = new Uri(filePath); 1113 | var referenceUri = new Uri(referencePath); 1114 | return referenceUri.MakeRelativeUri(fileUri).ToString().Replace('/', Path.DirectorySeparatorChar); 1115 | } 1116 | else 1117 | { 1118 | return ""; 1119 | } 1120 | } 1121 | 1122 | } 1123 | } 1124 | --------------------------------------------------------------------------------