├── .gitignore ├── LICENSE ├── README.md └── src ├── RadiantMapToObj.App ├── FodyWeavers.xml ├── Program.cs ├── RadiantMapToObj.App.csproj └── icon.ico ├── RadiantMapToObj.Tests ├── RadiantMapToObj.Tests.csproj └── VectorTests.cs ├── RadiantMapToObj.sln ├── RadiantMapToObj ├── Configuration │ ├── Filter.cs │ ├── Filters.cs │ └── TextureSettings.cs ├── Grid.cs ├── Internal │ ├── Conversion │ │ ├── BrushConversionHelper.cs │ │ ├── DisplacementConversionHelper.cs │ │ ├── MapConversionHelper.cs │ │ └── PatchConversionHelper.cs │ ├── EnumerableExtension.cs │ ├── Parsing │ │ ├── CommonParsingHelper.cs │ │ ├── Hammer │ │ │ ├── DisplacementParsingHelper.cs │ │ │ └── VmfParsingHelper.cs │ │ ├── MapParser.cs │ │ └── Radiant │ │ │ ├── BrushParsingHelper.cs │ │ │ ├── PatchParsingHelper.cs │ │ │ └── RadiantMapParsingHelper.cs │ └── TextureLoading │ │ └── TextureFinderHelper.cs ├── Plane.cs ├── Quake │ ├── Brush.cs │ ├── ClippingPlane.cs │ ├── Hammer │ │ ├── DisplacementClippingPlane.cs │ │ └── DisplacementInfo.cs │ ├── IQuakeEntity.cs │ ├── PlaneTexture.cs │ ├── QuakeMap.cs │ └── Radiant │ │ └── Patch.cs ├── RadiantMapToObj.csproj ├── TextureFinder.cs ├── Vector.cs └── Wavefront │ ├── Edge.cs │ ├── Face.cs │ ├── ObjObject.cs │ ├── TextureCoordinate.cs │ ├── Vertex.cs │ └── WavefrontObj.cs ├── Ruleset.ruleset └── stylecop.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,csharp,rider 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,csharp,rider 4 | 5 | ### Csharp ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*[.json, .xml, .info] 147 | 148 | # Visual Studio code coverage results 149 | *.coverage 150 | *.coveragexml 151 | 152 | # NCrunch 153 | _NCrunch_* 154 | .*crunch*.local.xml 155 | nCrunchTemp_* 156 | 157 | # MightyMoose 158 | *.mm.* 159 | AutoTest.Net/ 160 | 161 | # Web workbench (sass) 162 | .sass-cache/ 163 | 164 | # Installshield output folder 165 | [Ee]xpress/ 166 | 167 | # DocProject is a documentation generator add-in 168 | DocProject/buildhelp/ 169 | DocProject/Help/*.HxT 170 | DocProject/Help/*.HxC 171 | DocProject/Help/*.hhc 172 | DocProject/Help/*.hhk 173 | DocProject/Help/*.hhp 174 | DocProject/Help/Html2 175 | DocProject/Help/html 176 | 177 | # Click-Once directory 178 | publish/ 179 | 180 | # Publish Web Output 181 | *.[Pp]ublish.xml 182 | *.azurePubxml 183 | # Note: Comment the next line if you want to checkin your web deploy settings, 184 | # but database connection strings (with potential passwords) will be unencrypted 185 | *.pubxml 186 | *.publishproj 187 | 188 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 189 | # checkin your Azure Web App publish settings, but sensitive information contained 190 | # in these scripts will be unencrypted 191 | PublishScripts/ 192 | 193 | # NuGet Packages 194 | *.nupkg 195 | # NuGet Symbol Packages 196 | *.snupkg 197 | # The packages folder can be ignored because of Package Restore 198 | **/[Pp]ackages/* 199 | # except build/, which is used as an MSBuild target. 200 | !**/[Pp]ackages/build/ 201 | # Uncomment if necessary however generally it will be regenerated when needed 202 | #!**/[Pp]ackages/repositories.config 203 | # NuGet v3's project.json files produces more ignorable files 204 | *.nuget.props 205 | *.nuget.targets 206 | 207 | # Microsoft Azure Build Output 208 | csx/ 209 | *.build.csdef 210 | 211 | # Microsoft Azure Emulator 212 | ecf/ 213 | rcf/ 214 | 215 | # Windows Store app package directories and files 216 | AppPackages/ 217 | BundleArtifacts/ 218 | Package.StoreAssociation.xml 219 | _pkginfo.txt 220 | *.appx 221 | *.appxbundle 222 | *.appxupload 223 | 224 | # Visual Studio cache files 225 | # files ending in .cache can be ignored 226 | *.[Cc]ache 227 | # but keep track of directories ending in .cache 228 | !?*.[Cc]ache/ 229 | 230 | # Others 231 | ClientBin/ 232 | ~$* 233 | *~ 234 | *.dbmdl 235 | *.dbproj.schemaview 236 | *.jfm 237 | *.pfx 238 | *.publishsettings 239 | orleans.codegen.cs 240 | 241 | # Including strong name files can present a security risk 242 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 243 | #*.snk 244 | 245 | # Since there are multiple workflows, uncomment next line to ignore bower_components 246 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 247 | #bower_components/ 248 | 249 | # RIA/Silverlight projects 250 | Generated_Code/ 251 | 252 | # Backup & report files from converting an old project file 253 | # to a newer Visual Studio version. Backup files are not needed, 254 | # because we have git ;-) 255 | _UpgradeReport_Files/ 256 | Backup*/ 257 | UpgradeLog*.XML 258 | UpgradeLog*.htm 259 | ServiceFabricBackup/ 260 | *.rptproj.bak 261 | 262 | # SQL Server files 263 | *.mdf 264 | *.ldf 265 | *.ndf 266 | 267 | # Business Intelligence projects 268 | *.rdl.data 269 | *.bim.layout 270 | *.bim_*.settings 271 | *.rptproj.rsuser 272 | *- [Bb]ackup.rdl 273 | *- [Bb]ackup ([0-9]).rdl 274 | *- [Bb]ackup ([0-9][0-9]).rdl 275 | 276 | # Microsoft Fakes 277 | FakesAssemblies/ 278 | 279 | # GhostDoc plugin setting file 280 | *.GhostDoc.xml 281 | 282 | # Node.js Tools for Visual Studio 283 | .ntvs_analysis.dat 284 | node_modules/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # CodeRush personal settings 311 | .cr/personal 312 | 313 | # Python Tools for Visual Studio (PTVS) 314 | __pycache__/ 315 | *.pyc 316 | 317 | # Cake - Uncomment if you are using it 318 | # tools/** 319 | # !tools/packages.config 320 | 321 | # Tabs Studio 322 | *.tss 323 | 324 | # Telerik's JustMock configuration file 325 | *.jmconfig 326 | 327 | # BizTalk build output 328 | *.btp.cs 329 | *.btm.cs 330 | *.odx.cs 331 | *.xsd.cs 332 | 333 | # OpenCover UI analysis results 334 | OpenCover/ 335 | 336 | # Azure Stream Analytics local run output 337 | ASALocalRun/ 338 | 339 | # MSBuild Binary and Structured Log 340 | *.binlog 341 | 342 | # NVidia Nsight GPU debugger configuration file 343 | *.nvuser 344 | 345 | # MFractors (Xamarin productivity tool) working folder 346 | .mfractor/ 347 | 348 | # Local History for Visual Studio 349 | .localhistory/ 350 | 351 | # BeatPulse healthcheck temp database 352 | healthchecksdb 353 | 354 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 355 | MigrationBackup/ 356 | 357 | # Ionide (cross platform F# VS Code tools) working folder 358 | .ionide/ 359 | 360 | ### Rider ### 361 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 362 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 363 | 364 | # User-specific stuff 365 | .idea/**/workspace.xml 366 | .idea/**/tasks.xml 367 | .idea/**/usage.statistics.xml 368 | .idea/**/dictionaries 369 | .idea/**/shelf 370 | 371 | # Generated files 372 | .idea/**/contentModel.xml 373 | 374 | # Sensitive or high-churn files 375 | .idea/**/dataSources/ 376 | .idea/**/dataSources.ids 377 | .idea/**/dataSources.local.xml 378 | .idea/**/sqlDataSources.xml 379 | .idea/**/dynamic.xml 380 | .idea/**/uiDesigner.xml 381 | .idea/**/dbnavigator.xml 382 | 383 | # Gradle 384 | .idea/**/gradle.xml 385 | .idea/**/libraries 386 | 387 | # Gradle and Maven with auto-import 388 | # When using Gradle or Maven with auto-import, you should exclude module files, 389 | # since they will be recreated, and may cause churn. Uncomment if using 390 | # auto-import. 391 | # .idea/artifacts 392 | # .idea/compiler.xml 393 | # .idea/jarRepositories.xml 394 | # .idea/modules.xml 395 | # .idea/*.iml 396 | # .idea/modules 397 | # *.iml 398 | # *.ipr 399 | 400 | # CMake 401 | cmake-build-*/ 402 | 403 | # Mongo Explorer plugin 404 | .idea/**/mongoSettings.xml 405 | 406 | # File-based project format 407 | *.iws 408 | 409 | # IntelliJ 410 | out/ 411 | 412 | # mpeltonen/sbt-idea plugin 413 | .idea_modules/ 414 | 415 | # JIRA plugin 416 | atlassian-ide-plugin.xml 417 | 418 | # Cursive Clojure plugin 419 | .idea/replstate.xml 420 | 421 | # Crashlytics plugin (for Android Studio and IntelliJ) 422 | com_crashlytics_export_strings.xml 423 | crashlytics.properties 424 | crashlytics-build.properties 425 | fabric.properties 426 | 427 | # Editor-based Rest Client 428 | .idea/httpRequests 429 | 430 | # Android studio 3.1+ serialized cache file 431 | .idea/caches/build_file_checksums.ser 432 | 433 | ### VisualStudioCode ### 434 | .vscode/* 435 | !.vscode/tasks.json 436 | !.vscode/launch.json 437 | *.code-workspace 438 | 439 | ### VisualStudioCode Patch ### 440 | # Ignore all local history of files 441 | .history 442 | .ionide 443 | 444 | ### VisualStudio ### 445 | 446 | # User-specific files 447 | 448 | # User-specific files (MonoDevelop/Xamarin Studio) 449 | 450 | # Mono auto generated files 451 | 452 | # Build results 453 | 454 | # Visual Studio 2015/2017 cache/options directory 455 | # Uncomment if you have tasks that create the project's static files in wwwroot 456 | 457 | # Visual Studio 2017 auto generated files 458 | 459 | # MSTest test Results 460 | 461 | # NUnit 462 | 463 | # Build Results of an ATL Project 464 | 465 | # Benchmark Results 466 | 467 | # .NET Core 468 | 469 | # StyleCop 470 | 471 | # Files built by Visual Studio 472 | 473 | # Chutzpah Test files 474 | 475 | # Visual C++ cache files 476 | 477 | # Visual Studio profiler 478 | 479 | # Visual Studio Trace Files 480 | 481 | # TFS 2012 Local Workspace 482 | 483 | # Guidance Automation Toolkit 484 | 485 | # ReSharper is a .NET coding add-in 486 | 487 | # TeamCity is a build add-in 488 | 489 | # DotCover is a Code Coverage Tool 490 | 491 | # AxoCover is a Code Coverage Tool 492 | 493 | # Coverlet is a free, cross platform Code Coverage Tool 494 | 495 | # Visual Studio code coverage results 496 | 497 | # NCrunch 498 | 499 | # MightyMoose 500 | 501 | # Web workbench (sass) 502 | 503 | # Installshield output folder 504 | 505 | # DocProject is a documentation generator add-in 506 | 507 | # Click-Once directory 508 | 509 | # Publish Web Output 510 | # Note: Comment the next line if you want to checkin your web deploy settings, 511 | # but database connection strings (with potential passwords) will be unencrypted 512 | 513 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 514 | # checkin your Azure Web App publish settings, but sensitive information contained 515 | # in these scripts will be unencrypted 516 | 517 | # NuGet Packages 518 | # NuGet Symbol Packages 519 | # The packages folder can be ignored because of Package Restore 520 | # except build/, which is used as an MSBuild target. 521 | # Uncomment if necessary however generally it will be regenerated when needed 522 | # NuGet v3's project.json files produces more ignorable files 523 | 524 | # Microsoft Azure Build Output 525 | 526 | # Microsoft Azure Emulator 527 | 528 | # Windows Store app package directories and files 529 | 530 | # Visual Studio cache files 531 | # files ending in .cache can be ignored 532 | # but keep track of directories ending in .cache 533 | 534 | # Others 535 | 536 | # Including strong name files can present a security risk 537 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 538 | 539 | # Since there are multiple workflows, uncomment next line to ignore bower_components 540 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 541 | 542 | # RIA/Silverlight projects 543 | 544 | # Backup & report files from converting an old project file 545 | # to a newer Visual Studio version. Backup files are not needed, 546 | # because we have git ;-) 547 | 548 | # SQL Server files 549 | 550 | # Business Intelligence projects 551 | 552 | # Microsoft Fakes 553 | 554 | # GhostDoc plugin setting file 555 | 556 | # Node.js Tools for Visual Studio 557 | 558 | # Visual Studio 6 build log 559 | 560 | # Visual Studio 6 workspace options file 561 | 562 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 563 | 564 | # Visual Studio LightSwitch build output 565 | 566 | # Paket dependency manager 567 | 568 | # FAKE - F# Make 569 | 570 | # CodeRush personal settings 571 | 572 | # Python Tools for Visual Studio (PTVS) 573 | 574 | # Cake - Uncomment if you are using it 575 | # tools/** 576 | # !tools/packages.config 577 | 578 | # Tabs Studio 579 | 580 | # Telerik's JustMock configuration file 581 | 582 | # BizTalk build output 583 | 584 | # OpenCover UI analysis results 585 | 586 | # Azure Stream Analytics local run output 587 | 588 | # MSBuild Binary and Structured Log 589 | 590 | # NVidia Nsight GPU debugger configuration file 591 | 592 | # MFractors (Xamarin productivity tool) working folder 593 | 594 | # Local History for Visual Studio 595 | 596 | # BeatPulse healthcheck temp database 597 | 598 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 599 | 600 | # Ionide (cross platform F# VS Code tools) working folder 601 | 602 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,csharp,rider 603 | 604 | # FodyWeavers 605 | *.xsd 606 | **/launchSettings.json 607 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wesley Baartman 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RadiantMapToWavefrontObj 2 | A project that allows the conversion of Quake engine based brush maps to 3 | 3D models. 4 | Converts (Gtk)Radiant .map files to Wavefront .obj formatted files. 5 | 6 | ### Usage: 7 | Drag and drop a .map file onto the .exe 8 | 9 | #### Program arguments: 10 | - `autoclose` (e.g. `-autoclose=true`) 11 | - `scale` (e.g. `-scale=0.01`) 12 | - `filter` (e.g. `-filter=cod1filter.txt`) 13 | 14 | ### Confirmed working for: 15 | - GtkRadiant 1.6.5 Wolfenstein: Enemy Territory *(brush+patches)* 16 | - GtkRadiant 1.5.0 Wolfenstein: Enemy Territory *(brush+patches)* 17 | - CoD Radiant *(brush+patches)* 18 | - CoDWaW Radiant *(brush)* 19 | 20 | (*if there are any .map formats not supported, please notify me*) 21 | 22 | ### Links 23 | - [Latest Release](https://github.com/CptWesley/RadiantMapToWavefrontObj/releases/latest) 24 | - [SCRUM](https://waffle.io/CptWesley/RadiantMapToWavefrontObj) 25 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.App/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.App/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text.RegularExpressions; 5 | using RadiantMapToObj.Configuration; 6 | using RadiantMapToObj.Quake; 7 | using RadiantMapToObj.Wavefront; 8 | 9 | namespace RadiantMapToObj.App 10 | { 11 | /// 12 | /// Entry class Program. 13 | /// 14 | internal static class Program 15 | { 16 | private static double scale = 0.01; 17 | private static bool autoclose; 18 | private static Filter textureFilter = Filters.Empty; 19 | 20 | /// 21 | /// Defines the entry point of the application. 22 | /// 23 | /// The arguments. 24 | public static void Main(string[] args) 25 | { 26 | Version version = Assembly.GetExecutingAssembly().GetName().Version!; 27 | Console.WriteLine("RadiantMapToWavefrontObj version " + version.Major + '.' + version.Minor + '.' + version.Build); 28 | 29 | bool success = false; 30 | 31 | // Check for each argument if it is a .map we should convert. 32 | foreach (string arg in args) 33 | { 34 | if (File.Exists(arg)) 35 | { 36 | ConvertFile(arg); 37 | success = true; 38 | } 39 | else 40 | { 41 | HandleArgument(arg); 42 | } 43 | } 44 | 45 | if (!success) 46 | { 47 | Console.WriteLine("Invalid file."); 48 | } 49 | 50 | // Wait for console input before closing. 51 | Console.WriteLine("\nPress any key to close this window..."); 52 | 53 | if (!autoclose) 54 | { 55 | Console.ReadKey(); 56 | } 57 | } 58 | 59 | /// 60 | /// Converts the file. 61 | /// 62 | /// The path. 63 | private static void ConvertFile(string path) 64 | { 65 | Console.WriteLine("Parsing file: " + path + "..."); 66 | 67 | DateTime startTime = DateTime.Now; 68 | 69 | QuakeMap map = QuakeMap.ParseFile(path); 70 | WavefrontObj obj = map.ToObj(); 71 | 72 | obj.FilterTextures(textureFilter); 73 | 74 | string fileNameBase = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path)); 75 | 76 | obj.SaveFile(fileNameBase + ".obj", scale); 77 | obj.SaveMaterialFile(fileNameBase + ".mtl", new TextureFinder(new TextureSettings())); 78 | 79 | DateTime endTime = DateTime.Now; 80 | Console.WriteLine("Finished in: " + (endTime - startTime).TotalMilliseconds + "ms."); 81 | } 82 | 83 | /// 84 | /// Handles the argument. 85 | /// 86 | /// The argument. 87 | private static void HandleArgument(string arg) 88 | { 89 | string pattern = @"-(\w+)=(\S+)"; 90 | Regex regex = new Regex(pattern, RegexOptions.IgnoreCase); 91 | Match m = regex.Match(arg); 92 | if (m.Success) 93 | { 94 | string type = m.Groups[1].ToString(); 95 | string mode = m.Groups[2].ToString(); 96 | 97 | if (type == "autoclose") 98 | { 99 | if (mode == "false" || mode == "0") 100 | { 101 | autoclose = false; 102 | } 103 | else if (mode == "true" || mode == "1") 104 | { 105 | autoclose = true; 106 | } 107 | } 108 | else if (type == "scale") 109 | { 110 | if (double.TryParse(mode, out double scale)) 111 | { 112 | Program.scale = scale; 113 | } 114 | } 115 | else if (type == "filter") 116 | { 117 | textureFilter = Filter.Load(mode); 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.App/RadiantMapToObj.App.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net5.0;net48 5 | enable 6 | 9 7 | ../Ruleset.ruleset 8 | bin/$(AssemblyName).xml 9 | icon.ico 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers 34 | 35 | 36 | all 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.App/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CptWesley/RadiantMapToWavefrontObj/82cce922906901e0b80ebc25e0f30b700d109ae2/src/RadiantMapToObj.App/icon.ico -------------------------------------------------------------------------------- /src/RadiantMapToObj.Tests/RadiantMapToObj.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | 9 5 | false 6 | ../Ruleset.ruleset 7 | bin/$(AssemblyName).xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers 29 | 30 | 31 | all 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.Tests/VectorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | using static AssertNet.Assertions; 5 | 6 | namespace RadiantMapToObj.Tests 7 | { 8 | /// 9 | /// Test class for the class. 10 | /// 11 | public static class VectorTests 12 | { 13 | /// 14 | /// Checks that the constructor functions correctly. 15 | /// 16 | [Fact] 17 | public static void ConstructorTest() 18 | { 19 | Vector v = new Vector(1, 2, 3); 20 | AssertThat(v.X).IsEqualTo(1); 21 | AssertThat(v.Y).IsEqualTo(2); 22 | AssertThat(v.Z).IsEqualTo(3); 23 | } 24 | 25 | /// 26 | /// Checks that the length functions correctly. 27 | /// 28 | [Fact] 29 | public static void LengthTest() 30 | { 31 | Vector v = new Vector(1, 2, 3); 32 | AssertThat(v.SquareLength).IsEqualTo(14); 33 | AssertThat(v.Length).IsEqualTo(Math.Sqrt(14)); 34 | } 35 | 36 | /// 37 | /// Checks that the length functions correctly. 38 | /// 39 | [Fact] 40 | public static void UnitTest() 41 | { 42 | Vector v = new Vector(1, 2, 3); 43 | Vector u = v.Unit; 44 | AssertThat(u.Length).IsEqualTo(1); 45 | AssertThat(u.DirectionEquals(v)).IsTrue(); 46 | } 47 | 48 | /// 49 | /// Checks that the direction functions correctly. 50 | /// 51 | [Fact] 52 | public static void DirectionTest() 53 | { 54 | Vector v1 = new Vector(1, 0, 0); 55 | Vector v2 = new Vector(2, 0, 0); 56 | Vector v3 = new Vector(0, 1, 0); 57 | AssertThat(v1.DirectionEquals(v2)).IsTrue(); 58 | AssertThat(v1.DirectionEquals(v3)).IsFalse(); 59 | } 60 | 61 | /// 62 | /// Checks that the addition functions correctly. 63 | /// 64 | [Fact] 65 | public static void AddTest() 66 | => AssertThat(new Vector(10, 20, 30) + new Vector(1, 2, 3)).IsEqualTo(new Vector(11, 22, 33)); 67 | 68 | /// 69 | /// Checks that the subtraction functions correctly. 70 | /// 71 | [Fact] 72 | public static void SubtractTest() 73 | => AssertThat(new Vector(10, 20, 30) - new Vector(1, 2, 3)).IsEqualTo(new Vector(9, 18, 27)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/RadiantMapToObj.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30709.64 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadiantMapToObj", "RadiantMapToObj\RadiantMapToObj.csproj", "{E825322C-6885-4798-8ED5-8D5BC43EB536}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadiantMapToObj.App", "RadiantMapToObj.App\RadiantMapToObj.App.csproj", "{EA9D08E2-439D-478E-AE96-F52A551A2EF0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadiantMapToObj.Tests", "RadiantMapToObj.Tests\RadiantMapToObj.Tests.csproj", "{9CFD150C-BA9D-453F-B5E2-AF56844F2076}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {E825322C-6885-4798-8ED5-8D5BC43EB536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {E825322C-6885-4798-8ED5-8D5BC43EB536}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {E825322C-6885-4798-8ED5-8D5BC43EB536}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {E825322C-6885-4798-8ED5-8D5BC43EB536}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {EA9D08E2-439D-478E-AE96-F52A551A2EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {EA9D08E2-439D-478E-AE96-F52A551A2EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {EA9D08E2-439D-478E-AE96-F52A551A2EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {EA9D08E2-439D-478E-AE96-F52A551A2EF0}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {9CFD150C-BA9D-453F-B5E2-AF56844F2076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {9CFD150C-BA9D-453F-B5E2-AF56844F2076}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {9CFD150C-BA9D-453F-B5E2-AF56844F2076}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {9CFD150C-BA9D-453F-B5E2-AF56844F2076}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {83F1DAC2-4FA6-45A9-B1EB-D8665D4B1739} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Configuration/Filter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace RadiantMapToObj.Configuration 7 | { 8 | /// 9 | /// Represents a texture filter. 10 | /// 11 | public class Filter 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The filtered textures. 17 | /// The included filters. 18 | /// The list of textures that are not filtered. 19 | public Filter(IEnumerable textures, IEnumerable includes, IEnumerable ignores) 20 | => (Textures, Includes, Ignores) = (textures, includes, ignores); 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The filtered textures. 26 | /// The included filters. 27 | public Filter(IEnumerable textures, IEnumerable includes) 28 | : this(textures, includes, Array.Empty()) 29 | { 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The filtered textures. 36 | public Filter(IEnumerable textures) 37 | : this(textures, Array.Empty()) 38 | { 39 | } 40 | 41 | /// 42 | /// Initializes a new instance of the class. 43 | /// 44 | /// The included filters. 45 | public Filter(IEnumerable includes) 46 | : this(Array.Empty(), includes) 47 | { 48 | } 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | public Filter() 54 | : this(Array.Empty()) 55 | { 56 | } 57 | 58 | /// 59 | /// Gets the filtered textures. 60 | /// 61 | public IEnumerable Textures { get; } 62 | 63 | /// 64 | /// Gets the included filters. 65 | /// 66 | public IEnumerable Includes { get; } 67 | 68 | /// 69 | /// Gets the textures that are not filtered out. 70 | /// 71 | public IEnumerable Ignores { get; } 72 | 73 | /// 74 | /// Tries to load a specific filter from the given fileName. 75 | /// If the file does not exist, try to load a pre-implemented filter. 76 | /// 77 | /// Name of the file. 78 | /// The loaded filter. 79 | public static Filter Load(string fileName) 80 | { 81 | if (File.Exists(fileName)) 82 | { 83 | return new Filter(File.ReadAllLines(fileName)); 84 | } 85 | 86 | return fileName?.ToUpperInvariant() switch 87 | { 88 | "COD" => Filters.CallOfDuty, 89 | "ET" => Filters.EnemyTerritory, 90 | "RADIANT" => Filters.Radiant, 91 | "HAMMER" => Filters.Hammer, 92 | _ => throw new ArgumentException($"Could not find filter for '{fileName}'.", nameof(fileName)), 93 | }; 94 | } 95 | 96 | /// 97 | /// Determines whether this filter contains a texture. 98 | /// 99 | /// The texture to check. 100 | /// true if this filter contains the given texture; otherwise, false. 101 | public bool Contains(string texture) 102 | { 103 | if (IsIgnored(texture)) 104 | { 105 | return false; 106 | } 107 | 108 | foreach (string pattern in Textures) 109 | { 110 | if (Matches(pattern, texture)) 111 | { 112 | return true; 113 | } 114 | } 115 | 116 | foreach (Filter include in Includes) 117 | { 118 | if (include.Contains(texture)) 119 | { 120 | return true; 121 | } 122 | } 123 | 124 | return false; 125 | } 126 | 127 | private static bool Matches(string pattern, string texture) 128 | { 129 | string regexPattern = Regex.Escape(pattern).Replace("/", "\\/").Replace("\\*", ".*"); 130 | return Regex.IsMatch(texture, regexPattern); 131 | } 132 | 133 | private bool IsIgnored(string texture) 134 | { 135 | foreach (string pattern in Ignores) 136 | { 137 | if (Matches(pattern, texture)) 138 | { 139 | return true; 140 | } 141 | } 142 | 143 | foreach (Filter include in Includes) 144 | { 145 | if (include.IsIgnored(texture)) 146 | { 147 | return true; 148 | } 149 | } 150 | 151 | return false; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Configuration/Filters.cs: -------------------------------------------------------------------------------- 1 | namespace RadiantMapToObj.Configuration 2 | { 3 | /// 4 | /// Holds numerous pre-implemented filters. 5 | /// 6 | public static class Filters 7 | { 8 | /// 9 | /// Represents an empty filter. 10 | /// 11 | public static readonly Filter Empty = new Filter(); 12 | 13 | /// 14 | /// Represents a filter for the hammer editor tools. 15 | /// 16 | public static readonly Filter Hammer = new Filter(new string[] { "TOOLS/*" }); 17 | 18 | /// 19 | /// Represents a filter for the radiant editor utility textures. 20 | /// 21 | public static readonly Filter Radiant = new Filter(new string[] { "common/*" }); 22 | 23 | /// 24 | /// Represents a filter for Enemy Territory maps. 25 | /// 26 | public static readonly Filter EnemyTerritory = new Filter(new string[] { "skies/*" }, new Filter[] { Radiant }, new[] { "common/terrain" }); 27 | 28 | /// 29 | /// Represents a filter for CallOfDuty maps. 30 | /// 31 | public static readonly Filter CallOfDuty = new Filter(new string[] { "sky/*" }, new Filter[] { Radiant }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Configuration/TextureSettings.cs: -------------------------------------------------------------------------------- 1 | namespace RadiantMapToObj.Configuration 2 | { 3 | /// 4 | /// Contains the information for texture settings. 5 | /// 6 | public class TextureSettings 7 | { 8 | /// 9 | /// Gets or sets a value indicating whether texture exporting is enabled. 10 | /// 11 | public bool Enabled { get; set; } = true; 12 | 13 | /// 14 | /// Gets or sets the search path for finding textures. 15 | /// 16 | public string SearchPath { get; set; } = string.Empty; 17 | 18 | /// 19 | /// Gets or sets a value indicating whether an exact match for the texture should be found. 20 | /// 21 | public bool ExactMatch { get; set; } 22 | 23 | /// 24 | /// Gets or sets a value indicating whether archives (zip files) should be included in the search. 25 | /// 26 | public bool IncludeArchives { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Grid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RadiantMapToObj 6 | { 7 | /// 8 | /// Represents a readonly 2 dimensional grid. 9 | /// 10 | /// The type of elements in the grid. 11 | public class Grid 12 | { 13 | private readonly bool transposed; 14 | private readonly T[][] grid; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The grid. 20 | public Grid(T[][] grid) 21 | : this(grid, false) 22 | { 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The grid. 29 | /// Indicates whether or not the grid is transposed.. 30 | private Grid(T[][] grid, bool transposed) 31 | { 32 | this.grid = grid; 33 | this.transposed = transposed; 34 | } 35 | 36 | /// 37 | /// Gets the width. 38 | /// 39 | public int Width 40 | => transposed ? grid.Length : grid[0].Length; 41 | 42 | /// 43 | /// Gets the height. 44 | /// 45 | public int Height 46 | => transposed ? grid[0].Length : grid.Length; 47 | 48 | /// 49 | /// Gets all elements. 50 | /// 51 | public IEnumerable Elements => grid.SelectMany(x => x); 52 | 53 | /// 54 | /// Gets the transpose. 55 | /// 56 | public Grid Transpose => new Grid(grid, !transposed); 57 | 58 | /// 59 | /// Gets the element at the specified x and y position. 60 | /// 61 | /// The x position. 62 | /// The y position. 63 | /// The element at the given coordinate. 64 | public T this[int x, int y] 65 | => transposed ? grid[x][y] : grid[y][x]; 66 | 67 | /// 68 | public override string ToString() 69 | => string.Join(Environment.NewLine, grid.Select(x => string.Join(" ", x))); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Conversion/BrushConversionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RadiantMapToObj.Configuration; 5 | using RadiantMapToObj.Quake; 6 | using RadiantMapToObj.Quake.Hammer; 7 | using RadiantMapToObj.Wavefront; 8 | 9 | namespace RadiantMapToObj.Internal.Conversion 10 | { 11 | /// 12 | /// Provides helper functions for converting brushes to objects. 13 | /// 14 | internal static class BrushConversionHelper 15 | { 16 | private static readonly TextureFinder TextureFinder = new TextureFinder(new TextureSettings()); 17 | 18 | /// 19 | /// Converts a radiant brush to an obj object. 20 | /// 21 | /// The brush. 22 | /// A newly created obj object. 23 | public static ObjObject Convert(Brush brush) 24 | { 25 | IEnumerable vertices = FindIntersections(brush.ClippingPlanes); 26 | 27 | DisplacementClippingPlane? displacement = brush.ClippingPlanes.FirstOrDefault(x => x is DisplacementClippingPlane) as DisplacementClippingPlane; 28 | if (displacement != null) 29 | { 30 | return DisplacementConversionHelper.Convert(displacement, vertices); 31 | } 32 | 33 | IEnumerable faces = CreateFaces(vertices, brush.ClippingPlanes); 34 | return new ObjObject(vertices, faces); 35 | } 36 | 37 | /// 38 | /// Create a list of all intersection points of each set of three clipping planes. 39 | /// 40 | /// The planes. 41 | /// Gets all intersections of the given planes. 42 | private static IEnumerable FindIntersections(IEnumerable planes) 43 | { 44 | List intersections = new List(); 45 | 46 | // Check every unique combination of three clipping planes and see if we can find an intersection point. 47 | int i = 0; 48 | foreach (ClippingPlane planeI in planes) 49 | { 50 | int j = ++i; 51 | foreach (ClippingPlane planeJ in planes.Skip(j)) 52 | { 53 | foreach (ClippingPlane planeK in planes.Skip(++j)) 54 | { 55 | if (ClippingPlane.FindIntersection(planeI, planeJ, planeK, out Vector? intersection)) 56 | { 57 | // Checks if there does not exist a clipping plane with which we are in front. 58 | // Would result in vertices being added outside of our object. 59 | bool rightSide = true; 60 | foreach (ClippingPlane planeL in planes) 61 | { 62 | if (planeL != planeI && planeL != planeJ && planeL != planeK) 63 | { 64 | double dot = Vector.DotProduct(intersection!, planeL.Normal) - planeL.D; 65 | if (dot > 0) 66 | { 67 | rightSide = false; 68 | break; 69 | } 70 | } 71 | } 72 | 73 | if (rightSide && !intersections.Contains(intersection!)) 74 | { 75 | intersections.Add(intersection!); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | return intersections; 83 | } 84 | 85 | /// 86 | /// Use all vertices to create triangle faces of the object. 87 | /// 88 | /// The vertices. 89 | /// The planes. 90 | /// The faces of the object. 91 | private static IEnumerable CreateFaces(IEnumerable vertices, IEnumerable planes) 92 | { 93 | List faces = new List(); 94 | 95 | foreach (ClippingPlane plane in planes) 96 | { 97 | IEnumerable verts = plane.FindVerticesInPlane(vertices); 98 | 99 | foreach (Face face in BowyerWatson(verts, plane.Texture.Name)) 100 | { 101 | Face fixedFace = FixNormal(face, plane.Normal); 102 | Face texturedFace = ApplyTextures(fixedFace, plane); 103 | faces.Add(texturedFace); 104 | } 105 | } 106 | 107 | return faces; 108 | } 109 | 110 | /// 111 | /// Apply Bowyer-Watson algorithm to triangulate all the points in a plane. 112 | /// Pseudo code taken from related wikipedia page and provided on the side in comments. 113 | /// 114 | /// The vertices. 115 | /// The texture of the faces. 116 | /// The faces creates by the bowyer watson algorithm. 117 | private static IEnumerable BowyerWatson(IEnumerable vertices, string texture) 118 | { 119 | if (vertices is null) 120 | { 121 | throw new ArgumentNullException(nameof(vertices)); 122 | } 123 | 124 | if (!vertices.CountAtLeast(3)) 125 | { 126 | Console.WriteLine("WARNING found plane with less than 3 vertices."); 127 | return Array.Empty(); 128 | 129 | // TODO Handle this better 130 | // throw new ArgumentException("Requires at least 3 vertices.", nameof(vertices)); 131 | } 132 | 133 | HashSet triangles = new HashSet(); 134 | 135 | // Add super triangle to list. 136 | Face superTriangle = FindSuperTriangle(vertices, texture); 137 | triangles.Add(superTriangle); 138 | 139 | // for each point in pointList do 140 | foreach (Vector v in vertices) 141 | { 142 | // badTriangles := empty set 143 | HashSet badTriangles = new HashSet(); 144 | 145 | // for each triangle in triangulation do 146 | foreach (Face triangle in triangles) 147 | { 148 | // if point is inside circumcircle of triangle 149 | if (InCircumsphere(v, triangle)) 150 | { 151 | // add triangle to badTriangles 152 | badTriangles.Add(triangle); 153 | } 154 | } 155 | 156 | // polygon := empty set 157 | HashSet polygon = new HashSet(); 158 | 159 | // for each triangle in badTriangles do 160 | foreach (Face triangle in badTriangles) 161 | { 162 | // for each edge in triangle do 163 | foreach (Edge edge in triangle.GetEdges()) 164 | { 165 | bool shared = false; 166 | 167 | // if edge is not shared by any other triangles in badTriangles 168 | foreach (Face otherTriangle in badTriangles) 169 | { 170 | if (triangle == otherTriangle) 171 | { 172 | continue; 173 | } 174 | 175 | if (otherTriangle.GetEdges().Contains(edge) || otherTriangle.GetEdges().Contains(edge.Inverse)) 176 | { 177 | shared = true; 178 | break; 179 | } 180 | } 181 | 182 | if (!shared) 183 | { 184 | // add edge to polygon 185 | polygon.Add(edge); 186 | } 187 | } 188 | } 189 | 190 | // for each triangle in badTriangles do 191 | foreach (Face triangle in badTriangles) 192 | { 193 | // remove triangle from triangulation 194 | triangles.Remove(triangle); 195 | } 196 | 197 | // for each edge in polygon do 198 | foreach (Edge e in polygon) 199 | { 200 | // newTri := form a triangle from edge to point + add newTri to triangulation 201 | triangles.Add(new Face(e.A, e.B, v, texture)); 202 | } 203 | } 204 | 205 | HashSet result = new HashSet(); 206 | 207 | // for each triangle in triangulation 208 | foreach (Face t in triangles) 209 | { 210 | IEnumerable curVertices = t.Vertices; 211 | 212 | // if triangle contains a vertex from original super-triangle 213 | if (!curVertices.Contains(superTriangle.A) 214 | && !curVertices.Contains(superTriangle.B) 215 | && !curVertices.Contains(superTriangle.C)) 216 | { 217 | // remove triangle from triangulation 218 | result.Add(t); 219 | } 220 | } 221 | 222 | // return triangulation 223 | return result; 224 | } 225 | 226 | /// 227 | /// Finds the Bowyer-Watson super triangle of a set of vertices. 228 | /// 229 | /// The vertices. 230 | /// The texture of the faces. 231 | /// The found super triangle. 232 | private static Face FindSuperTriangle(IEnumerable vertices, string texture) 233 | { 234 | // Setup super triangle. 235 | double minX, maxX, minY, maxY, minZ, maxZ; 236 | 237 | minX = minY = minZ = double.MaxValue; 238 | maxX = maxY = maxZ = double.MinValue; 239 | 240 | foreach (Vector v in vertices) 241 | { 242 | if (v.X < minX) 243 | { 244 | minX = v.X; 245 | } 246 | 247 | if (v.X > maxX) 248 | { 249 | maxX = v.X; 250 | } 251 | 252 | if (v.Y < minY) 253 | { 254 | minY = v.Y; 255 | } 256 | 257 | if (v.Y > maxY) 258 | { 259 | maxY = v.Y; 260 | } 261 | 262 | if (v.Z < minZ) 263 | { 264 | minZ = v.Z; 265 | } 266 | 267 | if (v.Z > maxZ) 268 | { 269 | maxZ = v.Z; 270 | } 271 | } 272 | 273 | Plane plane = new Plane(vertices.Get(0), vertices.Get(1), vertices.Get(2)); 274 | 275 | Vector a = new Vector(minX, minY, minZ); 276 | Vector b = new Vector(maxX, maxY, maxZ); 277 | 278 | Vector ab = b - a; 279 | a -= 10 * ab; 280 | b += 10 * ab; 281 | 282 | Vector triBase = Vector.CrossProduct(ab, plane.Normal).Unit; 283 | 284 | double length = (b - a).Length; 285 | 286 | Vector c = a + (triBase * length); 287 | Vector d = a - (triBase * length); 288 | 289 | return new Face(b, c, d, texture); 290 | } 291 | 292 | /// 293 | /// Checks if a point lies in the circumsphere of a face. 294 | /// 295 | /// The point. 296 | /// The face. 297 | /// True if the point is in the circumsphere, false otherwise. 298 | private static bool InCircumsphere(Vector point, Face face) 299 | { 300 | Tuple cs = face.GetCircumsphere(); 301 | if (point.Distance(cs.Item1) < cs.Item2) 302 | { 303 | return true; 304 | } 305 | 306 | return false; 307 | } 308 | 309 | /// 310 | /// Fix normals of faces pointing in the wrong direction. 311 | /// 312 | /// The face. 313 | /// The normal. 314 | private static Face FixNormal(Face face, Vector normal) 315 | { 316 | // Check if the normal is correct, if not, invert the face. 317 | if (VerifyNormal(face, normal)) 318 | { 319 | return face; 320 | } 321 | 322 | return new Face(face.C, face.B, face.A, face.Texture); 323 | } 324 | 325 | /// 326 | /// Checks if a normal of a face is equal to a certain normal. 327 | /// 328 | /// The face. 329 | /// The normal. 330 | /// True if the normal is equal, false otherwise. 331 | private static bool VerifyNormal(Face face, Vector normal) 332 | { 333 | Vector v1 = face.B - face.A; 334 | Vector v2 = face.C - face.A; 335 | 336 | Vector faceNormal = -Vector.CrossProduct(v1, v2); 337 | if (normal.DirectionEquals(faceNormal)) 338 | { 339 | return true; 340 | } 341 | 342 | return false; 343 | } 344 | 345 | private static Face ApplyTextures(Face face, ClippingPlane plane) 346 | { 347 | (int width, int height) = TextureFinder.FindSize(face.Texture); 348 | 349 | if (width == 0 || height == 0) 350 | { 351 | return face; 352 | } 353 | 354 | Vector[] vectors = face.Vertices.ToArray(); 355 | 356 | Vertex v1 = CreateTexturedVertex(plane, vectors[0], width, height); 357 | Vertex v2 = CreateTexturedVertex(plane, vectors[1], width, height); 358 | Vertex v3 = CreateTexturedVertex(plane, vectors[2], width, height); 359 | 360 | return new Face(v1, v2, v3, face.Texture); 361 | } 362 | 363 | private static Vertex CreateTexturedVertex(ClippingPlane plane, Vector point, int width, int height) 364 | { 365 | double yzr = Vector.DotProduct(new Vector(1, 0, 0), plane.Normal); 366 | double xzr = Vector.DotProduct(new Vector(0, 1, 0), plane.Normal); 367 | double xyr = Vector.DotProduct(new Vector(0, 0, 1), plane.Normal); 368 | double yz = Math.Abs(yzr); 369 | double xz = Math.Abs(xzr); 370 | double xy = Math.Abs(xyr); 371 | 372 | double rad = Math.PI / 180 * -plane.Texture.Rotation; 373 | double crot = Math.Cos(rad); 374 | double srot = Math.Sin(rad); 375 | 376 | double xOffset = plane.Texture.OffsetX; 377 | double yOffset = plane.Texture.OffsetY; 378 | 379 | double x; 380 | double y; 381 | if (yz >= xz && yz >= xy) 382 | { 383 | x = -point.Y; 384 | y = point.Z; 385 | } 386 | else if (xz >= xy) 387 | { 388 | x = -point.X; 389 | y = point.Z; 390 | } 391 | else 392 | { 393 | x = point.X; 394 | y = point.Y; 395 | xOffset = -xOffset; 396 | } 397 | 398 | x /= -plane.Texture.ScaleX; 399 | y /= -plane.Texture.ScaleY; 400 | 401 | x = Math.Round(x); 402 | y = Math.Round(y); 403 | 404 | double a = x; 405 | double b = y; 406 | 407 | x = (crot * a) + (srot * b); 408 | y = -(srot * a) + (crot * b); 409 | 410 | x -= xOffset; 411 | y -= yOffset; 412 | 413 | double u = x / width; 414 | double v = y / height; 415 | 416 | return new Vertex(point.X, point.Y, point.Z, u, v); 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Conversion/DisplacementConversionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RadiantMapToObj.Quake.Hammer; 5 | using RadiantMapToObj.Wavefront; 6 | 7 | namespace RadiantMapToObj.Internal.Conversion 8 | { 9 | /// 10 | /// Provides conversion methods for displacements. 11 | /// 12 | internal static class DisplacementConversionHelper 13 | { 14 | /// 15 | /// Converts a hammer displacement to an obj object. 16 | /// 17 | /// The clipping plane containing the displacement. 18 | /// The vertices of the brush. 19 | /// A newly created obj object. 20 | public static ObjObject Convert(DisplacementClippingPlane displacementPlane, IEnumerable vertices) 21 | { 22 | Vector[] verticesInPlane = vertices.Where(x => x.OnPlane(displacementPlane)).ToArray(); 23 | 24 | if (verticesInPlane.Length != 4) 25 | { 26 | Console.WriteLine("Error - Not enough vertices for displacement."); 27 | return new ObjObject(Array.Empty(), Array.Empty()); 28 | } 29 | 30 | DisplacementInfo dispInfo = displacementPlane.DisplacementInfo; 31 | 32 | Grid grid = CreateGrid(verticesInPlane, dispInfo.Dimensions); 33 | grid = ApplyOffsets(grid, dispInfo.Offsets); 34 | grid = ApplyDistances(grid, dispInfo.Normals, dispInfo.Elevation, dispInfo.Distances); 35 | return ObjFromGrid(grid, displacementPlane.Texture.Name); 36 | } 37 | 38 | private static Grid CreateGrid(Vector[] vertices, int dimensions) 39 | { 40 | Vector a = vertices[3]; 41 | Vector b = vertices[0]; 42 | Vector c = vertices[2]; 43 | Vector d = vertices[1]; 44 | 45 | Vector[] left = CreateRow(a, c, dimensions); 46 | Vector[] right = CreateRow(d, b, dimensions); 47 | 48 | Vector[][] grid = new Vector[dimensions][]; 49 | 50 | for (int i = 0; i < dimensions; i++) 51 | { 52 | grid[i] = CreateRow(left[i], right[i], dimensions); 53 | } 54 | 55 | return new Grid(grid); 56 | } 57 | 58 | private static Vector[] CreateRow(Vector a, Vector b, int dimensions) 59 | { 60 | Vector[] result = new Vector[dimensions]; 61 | 62 | Vector movement = b - a; 63 | Vector normal = movement.Unit; 64 | double spacing = movement.Length / (dimensions - 1); 65 | 66 | for (int i = 0; i < result.Length; i++) 67 | { 68 | result[i] = a + (normal * spacing * i); 69 | } 70 | 71 | return result; 72 | } 73 | 74 | private static Grid ApplyOffsets(Grid grid, Grid offsets) 75 | { 76 | Vector[][] newGrid = new Vector[grid.Height][]; 77 | 78 | for (int y = 0; y < grid.Height; y++) 79 | { 80 | newGrid[y] = new Vector[grid.Width]; 81 | for (int x = 0; x < grid.Width; x++) 82 | { 83 | newGrid[y][x] = grid[x, y] - offsets[grid.Width - 1 - x, y]; 84 | } 85 | } 86 | 87 | return new Grid(newGrid); 88 | } 89 | 90 | private static Grid ApplyDistances(Grid grid, Grid normals, double elevation, Grid distances) 91 | { 92 | Vector[][] newGrid = new Vector[grid.Height][]; 93 | 94 | for (int y = 0; y < grid.Height; y++) 95 | { 96 | newGrid[y] = new Vector[grid.Width]; 97 | for (int x = 0; x < grid.Width; x++) 98 | { 99 | newGrid[y][x] = grid[x, y] - (normals[grid.Width - 1 - x, y] * (elevation + distances[grid.Width - 1 - x, y])); 100 | } 101 | } 102 | 103 | return new Grid(newGrid); 104 | } 105 | 106 | private static ObjObject ObjFromGrid(Grid grid, string texture) 107 | { 108 | List faces = new List(); 109 | 110 | for (int x = 0; x < grid.Width - 1; ++x) 111 | { 112 | for (int y = 0; y < grid.Height - 1; ++y) 113 | { 114 | faces.Add(new Face(grid[x, y], grid[x + 1, y], grid[x, y + 1], texture)); 115 | faces.Add(new Face(grid[x, y + 1], grid[x + 1, y], grid[x + 1, y + 1], texture)); 116 | } 117 | } 118 | 119 | return new ObjObject(grid.Elements, faces); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Conversion/MapConversionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using RadiantMapToObj.Quake; 3 | using RadiantMapToObj.Wavefront; 4 | 5 | namespace RadiantMapToObj.Internal.Conversion 6 | { 7 | /// 8 | /// Provides helper functions for converting maps to objs. 9 | /// 10 | internal static class MapConversionHelper 11 | { 12 | /// 13 | /// Converts a RadiantMap object to a WavefrontObj object. 14 | /// 15 | /// The radiant map to convert. 16 | /// A wavefront object created from a given radiant map. 17 | internal static WavefrontObj Convert(QuakeMap map) 18 | => new WavefrontObj(map.Entities.Select(x => x.ToObjObject())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Conversion/PatchConversionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RadiantMapToObj.Quake.Radiant; 3 | using RadiantMapToObj.Wavefront; 4 | 5 | namespace RadiantMapToObj.Internal.Conversion 6 | { 7 | /// 8 | /// Provides helper functions for converting patches to objects. 9 | /// 10 | internal static class PatchConversionHelper 11 | { 12 | /// 13 | /// Converts a radiant patch to an obj object. 14 | /// 15 | /// The patch. 16 | /// A newly created obj object. 17 | internal static ObjObject Convert(Patch patch) 18 | { 19 | IEnumerable vertices = patch.Vertices; 20 | IEnumerable faces = CreateFaces(patch); 21 | return new ObjObject(vertices, faces); 22 | } 23 | 24 | /// 25 | /// Creates faces from a patch. 26 | /// 27 | /// The patch. 28 | /// The faces that fill the grid. 29 | private static IEnumerable CreateFaces(Patch patch) 30 | { 31 | List faces = new List(); 32 | 33 | for (int x = 0; x < patch.Width - 1; ++x) 34 | { 35 | for (int y = 0; y < patch.Height - 1; ++y) 36 | { 37 | faces.Add(new Face(patch[x, y], patch[x + 1, y], patch[x, y + 1], string.Empty)); 38 | faces.Add(new Face(patch[x, y + 1], patch[x + 1, y], patch[x + 1, y + 1], string.Empty)); 39 | } 40 | } 41 | 42 | return faces; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/EnumerableExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RadiantMapToObj.Internal 6 | { 7 | /// 8 | /// Extension class for . 9 | /// 10 | internal static class EnumerableExtension 11 | { 12 | /// 13 | /// Get index of element. 14 | /// 15 | /// Type of element to be found. 16 | /// The enumerable. 17 | /// The element. 18 | /// Index of element. 19 | public static int IndexOf(this IEnumerable enumerable, T element) 20 | { 21 | int i = 0; 22 | foreach (T val in enumerable) 23 | { 24 | if ((val is null && element is null) || (val != null && val.Equals(element))) 25 | { 26 | return i; 27 | } 28 | 29 | i++; 30 | } 31 | 32 | return -1; 33 | } 34 | 35 | /// 36 | /// Gets the element at the given index. 37 | /// 38 | /// Type of element to be found. 39 | /// The enumerable. 40 | /// The index. 41 | /// The element at the given index. 42 | public static T Get(this IEnumerable enumerable, int index) 43 | => enumerable.Skip(index).First(); 44 | 45 | /// 46 | /// Checks that the length of an enumerable is at least a given count. 47 | /// 48 | /// Type of element in the enumerable. 49 | /// The enumerable. 50 | /// The count. 51 | /// True if count is at least the given value, false otherwise. 52 | public static bool CountAtLeast(this IEnumerable enumerable, int count) 53 | { 54 | int i = 0; 55 | foreach (T element in enumerable) 56 | { 57 | if (++i == count) 58 | { 59 | return true; 60 | } 61 | } 62 | 63 | return false; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/CommonParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Globalization; 3 | using Warpstone; 4 | using static Warpstone.Parsers.BasicParsers; 5 | 6 | namespace RadiantMapToObj.Internal.Parsing 7 | { 8 | /// 9 | /// Provides parsers for common things. 10 | /// 11 | [SuppressMessage("Ordering Rules", "SA1202", Justification = "Order is important for instantiation.")] 12 | internal static class CommonParsingHelper 13 | { 14 | private static readonly IParser Whitespace = CompiledRegex(@"\s+"); 15 | private static readonly IParser Comment = CompiledRegex(@"//.*"); 16 | 17 | /// 18 | /// Parses mandatory layout. 19 | /// 20 | internal static readonly IParser Layout = OneOrMore(Or(Comment, Whitespace)).Transform(x => string.Join(string.Empty, x)).WithName("layout"); 21 | 22 | /// 23 | /// Parses optional layout. 24 | /// 25 | internal static readonly IParser OptionalLayout = Or(Layout, Create(string.Empty)); 26 | 27 | private static readonly IParser String 28 | = Char('"').Then(CompiledRegex(@"([^""]|\\"")*")).ThenSkip(Char('"')) 29 | .WithName("string") 30 | .Transform(x => string.Join(string.Empty, x)); 31 | 32 | /// 33 | /// Parses a field. 34 | /// 35 | internal static readonly IParser<(string, string)> Field 36 | = String.ThenSkip(OptionalLayout).ThenAdd(String); 37 | 38 | /// 39 | /// Parses texture names. 40 | /// 41 | internal static readonly IParser TextureName = CompiledRegex(@"[\w\/\-\\@#\.]+"); 42 | 43 | /// 44 | /// Parses a single double. 45 | /// 46 | internal static readonly IParser Double = CompiledRegex(@"-?((\.[0-9]+)|(([1-9][0-9]*|0)(\.[0-9]+)?))(e-?[0-9]+)?").WithName("double").Transform(x => double.Parse(x, CultureInfo.InvariantCulture)); 47 | 48 | /// 49 | /// Parses a vector. 50 | /// 51 | internal static readonly IParser Vertex 52 | = String("(") 53 | .ThenSkip(OptionalLayout) 54 | .Then(Double) 55 | .ThenSkip(Layout) 56 | .ThenAdd(Double) 57 | .ThenSkip(Layout) 58 | .ThenAdd(Double) 59 | .ThenSkip(OptionalLayout) 60 | .ThenSkip(String(")")) 61 | .Transform((x, y, z) => new Vector(x, y, z)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/Hammer/DisplacementParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq; 3 | using RadiantMapToObj.Quake.Hammer; 4 | using Warpstone; 5 | using static RadiantMapToObj.Internal.Parsing.CommonParsingHelper; 6 | using static RadiantMapToObj.Internal.Parsing.Hammer.VmfParsingHelper; 7 | using static Warpstone.Parsers.BasicParsers; 8 | 9 | namespace RadiantMapToObj.Internal.Parsing.Hammer 10 | { 11 | /// 12 | /// Provides functionality for parsing displacements. 13 | /// 14 | internal static class DisplacementParsingHelper 15 | { 16 | private static readonly IParser StartPos 17 | = OptionalLayout 18 | .ThenSkip(String("[")) 19 | .ThenSkip(OptionalLayout) 20 | .Then(Multiple(Double, Layout, 3)) 21 | .ThenSkip(OptionalLayout) 22 | .ThenSkip(String("]")) 23 | .ThenSkip(OptionalLayout) 24 | .Transform(x => new Vector(x[0], x[1], x[2])); 25 | 26 | private static readonly IParser DoubleRow = Multiple(Double, Layout, 5, 17).Transform(x => x.ToArray()); 27 | private static readonly IParser SingleVector = Multiple(Double, Layout, 3).Transform(x => new Vector(x[0], x[1], x[2])); 28 | private static readonly IParser VectorRow = Multiple(SingleVector, Layout, 5, 17).Transform(x => x.ToArray()); 29 | 30 | /// 31 | /// Converts a vmf class to a displacement. 32 | /// 33 | /// The class. 34 | /// The displacement info. 35 | internal static DisplacementInfo ToDispInfo(VmfClass c) 36 | { 37 | int power = int.Parse(c.Fields.First(x => x.Name == "power").Value, CultureInfo.InvariantCulture); 38 | int dimensions = PowerToDimensions(power); 39 | Vector startPos = StartPos.Parse(c.Fields.First(x => x.Name == "startposition").Value); 40 | double elevation = Double.Parse(c.Fields.First(x => x.Name == "elevation").Value); 41 | Grid normals = GetGrid(VectorRow, c.Classes.First(x => x.Name == "normals"), dimensions); 42 | Grid distances = GetGrid(DoubleRow, c.Classes.First(x => x.Name == "distances"), dimensions); 43 | Grid offsets = GetGrid(VectorRow, c.Classes.First(x => x.Name == "offsets"), dimensions); 44 | Grid offsetNormals = GetGrid(VectorRow, c.Classes.First(x => x.Name == "offset_normals"), dimensions); 45 | 46 | return new DisplacementInfo(dimensions, startPos, elevation, normals, distances, offsets, offsetNormals); 47 | } 48 | 49 | private static int PowerToDimensions(int power) 50 | => power switch 51 | { 52 | 2 => 5, 53 | 3 => 9, 54 | _ => 17, 55 | }; 56 | 57 | private static Grid GetGrid(IParser rowParser, VmfClass c, int dimensions) 58 | { 59 | T[][] result = new T[dimensions][]; 60 | 61 | for (int i = 0; i < dimensions; i++) 62 | { 63 | string value = c.Fields.First(x => x.Name == $"row{i}").Value; 64 | result[i] = rowParser.Parse(value); 65 | } 66 | 67 | return new Grid(result); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/Hammer/VmfParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using RadiantMapToObj.Quake; 5 | using RadiantMapToObj.Quake.Hammer; 6 | using Warpstone; 7 | using static RadiantMapToObj.Internal.Parsing.CommonParsingHelper; 8 | using static Warpstone.Parsers.BasicParsers; 9 | 10 | namespace RadiantMapToObj.Internal.Parsing.Hammer 11 | { 12 | /// 13 | /// Provides helper methods for parsing Hammer maps. 14 | /// 15 | [SuppressMessage("Ordering Rules", "SA1202", Justification = "Order is important for instantiation.")] 16 | internal static class VmfParsingHelper 17 | { 18 | private static readonly IParser<(Vector, Vector, Vector)> Vertices 19 | = OptionalLayout 20 | .Then(Multiple(Vertex, OptionalLayout, 3)) 21 | .ThenSkip(OptionalLayout) 22 | .Transform(x => (x[0], x[1], x[2])); 23 | 24 | private static readonly IParser Element = Or(Lazy(() => Field), Lazy(() => Class)); 25 | 26 | private static readonly IParser Field = CommonParsingHelper.Field.Transform((n, v) => new VmfField(n, v)); 27 | 28 | private static readonly IParser Class 29 | = CompiledRegex("[a-zA-Z0-9_]+") 30 | .ThenSkip(OptionalLayout) 31 | .ThenSkip(String("{")) 32 | .ThenSkip(OptionalLayout) 33 | .ThenAdd(Many(Element, OptionalLayout)) 34 | .ThenSkip(OptionalLayout) 35 | .ThenSkip(String("}")) 36 | .Transform((n, c) => new VmfClass(n, c.Where(x => x is VmfField).Select(x => x as VmfField)!, c.Where(x => x is VmfClass).Select(x => x as VmfClass)!)); 37 | 38 | private static readonly IParser> Solids 39 | = OptionalLayout 40 | .Then(Many(Element, OptionalLayout)) 41 | .ThenSkip(OptionalLayout) 42 | .Transform(GetSolids) 43 | .Transform(x => x.Select(ToEntity)); 44 | 45 | /// 46 | /// Parses a .vmf file. 47 | /// 48 | internal static readonly IParser Vmf 49 | = Solids 50 | .Transform(x => new QuakeMap(x)) 51 | .ThenEnd(); 52 | 53 | private static IQuakeEntity ToEntity(VmfClass c) 54 | { 55 | List planes = new List(); 56 | 57 | foreach (VmfClass side in c.Classes.Where(x => x.Name == "side")) 58 | { 59 | string textureName = side.Fields.First(x => x.Name == "material").Value; 60 | PlaneTexture texture = new PlaneTexture(textureName, 0, 0, 0, 1, 1); 61 | string planeText = side.Fields.First(x => x.Name == "plane").Value; 62 | (Vector v1, Vector v2, Vector v3) = Vertices.Parse(planeText); 63 | 64 | VmfClass? dispInfo = side.Classes.FirstOrDefault(x => x.Name == "dispinfo"); 65 | if (dispInfo != null) 66 | { 67 | planes.Add(new DisplacementClippingPlane(v1, v2, v3, texture, DisplacementParsingHelper.ToDispInfo(dispInfo))); 68 | } 69 | else 70 | { 71 | planes.Add(new ClippingPlane(v1, v2, v3, texture)); 72 | } 73 | } 74 | 75 | return new Brush(planes); 76 | } 77 | 78 | private static IEnumerable GetSolids(IEnumerable elements) 79 | { 80 | IEnumerable classes = elements.Where(x => x is VmfClass).Select(x => x as VmfClass)!; 81 | IEnumerable result = classes.Where(x => x.Name == "solid")!; 82 | 83 | foreach (VmfClass c in result) 84 | { 85 | yield return c; 86 | } 87 | 88 | foreach (VmfClass c in classes) 89 | { 90 | foreach (VmfClass r in GetSolids(c.Classes)) 91 | { 92 | yield return r; 93 | } 94 | } 95 | } 96 | 97 | internal abstract record VmfElement; 98 | 99 | internal record VmfField(string Name, string Value) : VmfElement; 100 | 101 | internal record VmfClass(string Name, IEnumerable Fields, IEnumerable Classes) : VmfElement; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/MapParser.cs: -------------------------------------------------------------------------------- 1 | using RadiantMapToObj.Internal.Parsing.Hammer; 2 | using RadiantMapToObj.Internal.Parsing.Radiant; 3 | using RadiantMapToObj.Quake; 4 | using static Warpstone.Parsers.BasicParsers; 5 | 6 | namespace RadiantMapToObj.Internal.Parsing 7 | { 8 | /// 9 | /// Provides functionality for parsing any kind of map. 10 | /// 11 | internal static class MapParser 12 | { 13 | /// 14 | /// Parses a .map file to our radiant map object. 15 | /// 16 | /// The content of the .map file. 17 | /// The parsed radiant map. 18 | public static QuakeMap Parse(string input) 19 | => Or(VmfParsingHelper.Vmf, RadiantMapParsingHelper.Map).Parse(input); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/Radiant/BrushParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using RadiantMapToObj.Quake; 3 | using Warpstone; 4 | using static RadiantMapToObj.Internal.Parsing.CommonParsingHelper; 5 | using static Warpstone.Parsers.BasicParsers; 6 | 7 | namespace RadiantMapToObj.Internal.Parsing.Radiant 8 | { 9 | /// 10 | /// Provides helper methods for parsing brushes. 11 | /// 12 | [SuppressMessage("Ordering Rules", "SA1202", Justification = "Order is important for instantiation.")] 13 | internal static class BrushParsingHelper 14 | { 15 | private static readonly IParser Texture 16 | = TextureName 17 | .ThenSkip(Layout) 18 | .ThenAdd(Double) 19 | .ThenSkip(Layout) 20 | .ThenAdd(Double) 21 | .ThenSkip(Layout) 22 | .ThenAdd(Double) 23 | .ThenSkip(Layout) 24 | .ThenAdd(Double) 25 | .ThenSkip(Layout) 26 | .ThenAdd(Double) 27 | .Transform((t, x, y, r, xs, ys) => new PlaneTexture(t, x, y, r, xs, ys)); 28 | 29 | private static readonly IParser ClippingPlane 30 | = Vertex 31 | .ThenSkip(OptionalLayout) 32 | .ThenAdd(Vertex) 33 | .ThenSkip(OptionalLayout) 34 | .ThenAdd(Vertex) 35 | .ThenSkip(OptionalLayout) 36 | .ThenAdd(Texture) 37 | .ThenSkip(Layout) 38 | .ThenSkip(CompiledRegex(@".*")) 39 | .Transform((a, b, c, t) => new ClippingPlane(a, b, c, t)); 40 | 41 | /// 42 | /// Parses a brush. 43 | /// 44 | internal static readonly IParser Brush 45 | = String("{") 46 | .ThenSkip(OptionalLayout) 47 | .Then(Many(ClippingPlane, Layout)) 48 | .ThenSkip(OptionalLayout) 49 | .ThenSkip(String("}")) 50 | .Transform(x => new Brush(x)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/Radiant/PatchParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using RadiantMapToObj.Quake.Radiant; 5 | using Warpstone; 6 | using Warpstone.Parsers; 7 | using static RadiantMapToObj.Internal.Parsing.CommonParsingHelper; 8 | using static Warpstone.Parsers.BasicParsers; 9 | 10 | namespace RadiantMapToObj.Internal.Parsing.Radiant 11 | { 12 | /// 13 | /// Provides helper methods for parsing patches. 14 | /// 15 | [SuppressMessage("Ordering Rules", "SA1202", Justification = "Order is important for instantiation.")] 16 | internal static class PatchParsingHelper 17 | { 18 | private static readonly IParser VertexUvPatchDef2 19 | = String("(") 20 | .ThenSkip(OptionalLayout) 21 | .Then(Multiple(Double, Layout, 5)) 22 | .ThenSkip(OptionalLayout) 23 | .ThenSkip(String(")")) 24 | .Transform(x => -new Vector(x[0], x[1], x[2])); 25 | 26 | private static readonly IParser VertexRowPatchDef2 27 | = String("(") 28 | .ThenSkip(OptionalLayout) 29 | .Then(Many(VertexUvPatchDef2, OptionalLayout)) 30 | .ThenSkip(OptionalLayout) 31 | .ThenSkip(String(")")) 32 | .Transform(x => x.ToArray()); 33 | 34 | private static readonly IParser> VertexGridPatchDef2 35 | = String("(") 36 | .ThenSkip(OptionalLayout) 37 | .Then(Many(VertexRowPatchDef2, OptionalLayout)) 38 | .ThenSkip(OptionalLayout) 39 | .ThenSkip(String(")")) 40 | .Transform(x => new Grid(x.ToArray())); 41 | 42 | private static readonly IParser> GridSizePatchDef2 43 | = String("(") 44 | .ThenSkip(OptionalLayout) 45 | .Then(Multiple(Double, Layout, 5)) 46 | .ThenSkip(OptionalLayout) 47 | .ThenSkip(String(")")); 48 | 49 | private static readonly IParser PatchDef2 50 | = String("{") 51 | .ThenSkip(OptionalLayout) 52 | .ThenSkip(String("patchDef2")) 53 | .ThenSkip(OptionalLayout) 54 | .ThenSkip(String("{")) 55 | .ThenSkip(OptionalLayout) 56 | .Then(TextureName) 57 | .ThenSkip(OptionalLayout) 58 | .ThenSkip(GridSizePatchDef2) 59 | .ThenSkip(OptionalLayout) 60 | .ThenAdd(VertexGridPatchDef2) 61 | .ThenSkip(OptionalLayout) 62 | .ThenSkip(String("}")) 63 | .ThenSkip(OptionalLayout) 64 | .ThenSkip(String("}")) 65 | .Transform((t, g) => new Patch(g)); 66 | 67 | private static readonly IParser VertexUvPatchDef3 68 | = String("(") 69 | .ThenSkip(OptionalLayout) 70 | .Then(Multiple(Double, Layout, 10)) 71 | .ThenSkip(OptionalLayout) 72 | .ThenSkip(String(")")) 73 | .Transform(x => -new Vector(x[0], x[1], x[2])); 74 | 75 | private static readonly IParser VertexRowPatchDef3 76 | = String("(") 77 | .ThenSkip(OptionalLayout) 78 | .Then(Many(VertexUvPatchDef3, OptionalLayout)) 79 | .ThenSkip(OptionalLayout) 80 | .ThenSkip(String(")")) 81 | .Transform(x => x.ToArray()); 82 | 83 | private static readonly IParser> VertexGridPatchDef3 84 | = String("(") 85 | .ThenSkip(OptionalLayout) 86 | .Then(Many(VertexRowPatchDef3, OptionalLayout)) 87 | .ThenSkip(OptionalLayout) 88 | .ThenSkip(String(")")) 89 | .Transform(x => new Grid(x.ToArray())); 90 | 91 | private static readonly IParser> GridSizePatchDef3 92 | = String("(") 93 | .ThenSkip(OptionalLayout) 94 | .Then(Multiple(Double, Layout, 7)) 95 | .ThenSkip(OptionalLayout) 96 | .ThenSkip(String(")")); 97 | 98 | private static readonly IParser PatchDef3 99 | = String("{") 100 | .ThenSkip(OptionalLayout) 101 | .ThenSkip(Or(String("patchTerrainDef3"), String("patchDef5"))) 102 | .ThenSkip(OptionalLayout) 103 | .ThenSkip(String("{")) 104 | .ThenSkip(OptionalLayout) 105 | .Then(TextureName) 106 | .ThenSkip(OptionalLayout) 107 | .ThenSkip(GridSizePatchDef3) 108 | .ThenSkip(OptionalLayout) 109 | .ThenAdd(VertexGridPatchDef3) 110 | .ThenSkip(OptionalLayout) 111 | .ThenSkip(String("}")) 112 | .ThenSkip(OptionalLayout) 113 | .ThenSkip(String("}")) 114 | .Transform((t, g) => new Patch(g)); 115 | 116 | /// 117 | /// Parses a patch. 118 | /// 119 | internal static readonly IParser Patch 120 | = Or(PatchDef3, PatchDef2); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/Parsing/Radiant/RadiantMapParsingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using RadiantMapToObj.Quake; 5 | using Warpstone; 6 | using static RadiantMapToObj.Internal.Parsing.CommonParsingHelper; 7 | using static Warpstone.Parsers.BasicParsers; 8 | 9 | namespace RadiantMapToObj.Internal.Parsing.Radiant 10 | { 11 | /// 12 | /// Provides helper methods for parsing maps. 13 | /// 14 | [SuppressMessage("Ordering Rules", "SA1202", Justification = "Order is important for instantiation.")] 15 | internal static class RadiantMapParsingHelper 16 | { 17 | private static readonly IParser Entity 18 | = Or(PatchParsingHelper.Patch, BrushParsingHelper.Brush); 19 | 20 | private static readonly IParser> EntityContent 21 | = String("{") 22 | .ThenSkip(OptionalLayout) 23 | .Then(Many(Field, OptionalLayout)) 24 | .ThenSkip(OptionalLayout) 25 | .Then(Many(Entity, OptionalLayout)) 26 | .ThenSkip(OptionalLayout) 27 | .ThenSkip(String("}")); 28 | 29 | private static readonly IParser> Entities = Many(EntityContent, OptionalLayout).Transform(x => x.SelectMany(x => x)); 30 | 31 | /// 32 | /// Parses a radiant map. 33 | /// 34 | internal static readonly IParser Map = OptionalLayout.Then(Entities).ThenSkip(OptionalLayout).ThenEnd().Transform(x => new QuakeMap(x)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Internal/TextureLoading/TextureFinderHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.Linq; 5 | using RadiantMapToObj.Configuration; 6 | using SixLabors.ImageSharp; 7 | 8 | namespace RadiantMapToObj.Internal.TextureLoading 9 | { 10 | /// 11 | /// Internal class for finding textures. 12 | /// 13 | internal static class TextureFinderHelper 14 | { 15 | /// 16 | /// Finds the size of the texture with the given search settings. 17 | /// 18 | /// The settings for the texture search. 19 | /// The texture. 20 | /// The size of the image or (0, 0) if the image couldn't be loaded. 21 | [SuppressMessage("Microsoft.Design", "CA1031", Justification = "Any exception should be dealt with.")] 22 | public static (int Width, int Height, string Extension) Find(TextureSettings settings, string texture) 23 | { 24 | (Stream? stream, string? path) = FindStream(settings, texture); 25 | 26 | if (stream is null) 27 | { 28 | return (0, 0, path); 29 | } 30 | 31 | try 32 | { 33 | using var image = Image.Load(stream); 34 | return (image.Width, image.Height, path); 35 | } 36 | catch 37 | { 38 | try 39 | { 40 | using var tga = Pfim.Pfim.FromStream(stream); 41 | return (tga.Width, tga.Height, path); 42 | } 43 | catch 44 | { 45 | return (0, 0, path); 46 | } 47 | } 48 | } 49 | 50 | /// 51 | /// Finds the specified texture with the given search settings. 52 | /// 53 | /// The settings for the texture search. 54 | /// The texture name. 55 | /// The data stream of the texture. 56 | private static (Stream? Stream, string Extension) FindStream(TextureSettings settings, string texture) 57 | => FindInDirectory(settings, texture.Split('/', '\\'), new List()); 58 | 59 | private static (Stream? Stream, string Extension) FindInDirectory(TextureSettings settings, string[] texture, List path) 60 | { 61 | string searchPath = string.IsNullOrWhiteSpace(settings.SearchPath) ? "./" : settings.SearchPath; 62 | string fullPath = Path.Combine(path.Prepend(searchPath).ToArray()); 63 | DirectoryInfo di = new DirectoryInfo(fullPath); 64 | int missing = MissingMatchCount(texture, path); 65 | 66 | if (settings.ExactMatch && missing > 0 && missing == texture.Length - 1) 67 | { 68 | return (null, string.Empty); 69 | } 70 | 71 | if (missing == 0) 72 | { 73 | foreach (FileInfo file in di.GetFiles()) 74 | { 75 | if (Path.GetFileNameWithoutExtension(file.Name) == texture[texture.Length - 1]) 76 | { 77 | return (file.OpenRead(), file.Extension); 78 | } 79 | } 80 | } 81 | 82 | foreach (var subdir in di.GetDirectories()) 83 | { 84 | List newPath = new List(path); 85 | newPath.Add(subdir.Name); 86 | (Stream?, string) result = FindInDirectory(settings, texture, newPath); 87 | if (result.Item1 != null) 88 | { 89 | return result; 90 | } 91 | } 92 | 93 | return (null, string.Empty); 94 | } 95 | 96 | private static int MissingMatchCount(string[] texture, List path) 97 | { 98 | int matched = 0; 99 | 100 | for (int i = texture.Length - 2; i >= 0; i--) 101 | { 102 | if (path.Count - matched - 1 < path.Count && path.Count - matched - 1 >= 0 && texture[i] == path[path.Count - matched - 1]) 103 | { 104 | matched++; 105 | } 106 | } 107 | 108 | return texture.Length - matched - 1; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Plane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RadiantMapToObj 5 | { 6 | /// 7 | /// Represents a plane in 3D space. 8 | /// 9 | public class Plane 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The first vertex. 15 | /// The second vertex. 16 | /// The third vertex. 17 | public Plane(Vector v1, Vector v2, Vector v3) 18 | { 19 | if (v1 is null) 20 | { 21 | throw new ArgumentNullException(nameof(v1)); 22 | } 23 | 24 | if (v2 is null) 25 | { 26 | throw new ArgumentNullException(nameof(v2)); 27 | } 28 | 29 | if (v3 is null) 30 | { 31 | throw new ArgumentNullException(nameof(v3)); 32 | } 33 | 34 | Vector vector1 = new Vector(v2.X - v1.X, v2.Y - v1.Y, v2.Z - v1.Z).Unit; 35 | Vector vector2 = new Vector(v3.X - v1.X, v3.Y - v1.Y, v3.Z - v1.Z).Unit; 36 | 37 | Normal = Vector.CrossProduct(vector1, vector2).Unit; 38 | D = -((A * v2.X) + (B * v2.Y) + (C * v2.Z)); 39 | 40 | Vector1 = v1; 41 | Vector2 = v2; 42 | Vector3 = v3; 43 | } 44 | 45 | /// 46 | /// Gets the A value of the plane. 47 | /// 48 | public double A => Normal.X; 49 | 50 | /// 51 | /// Gets the B value of the plane. 52 | /// 53 | public double B => Normal.Y; 54 | 55 | /// 56 | /// Gets the C value of the plane. 57 | /// 58 | public double C => Normal.Z; 59 | 60 | /// 61 | /// Gets the plane facing direction. 62 | /// 63 | public Vector Normal { get; } 64 | 65 | /// 66 | /// Gets the D value of the plane. 67 | /// 68 | public double D { get; } 69 | 70 | /// 71 | /// Gets the first vector. 72 | /// 73 | public Vector Vector1 { get; } 74 | 75 | /// 76 | /// Gets the second vector. 77 | /// 78 | public Vector Vector2 { get; } 79 | 80 | /// 81 | /// Gets the third vector. 82 | /// 83 | public Vector Vector3 { get; } 84 | 85 | /// 86 | public override string ToString() 87 | => $"<{A}, {B}, {C}, {D}>"; 88 | 89 | /// 90 | /// Creates a collection of all vertices that lie on this plane. 91 | /// 92 | /// The vertices. 93 | /// A collection of all vertices in the plane. 94 | public IEnumerable FindVerticesInPlane(IEnumerable vertices) 95 | { 96 | if (vertices is null) 97 | { 98 | throw new ArgumentNullException(nameof(vertices)); 99 | } 100 | 101 | List res = new List(); 102 | foreach (Vector v in vertices) 103 | { 104 | if (v.OnPlane(this)) 105 | { 106 | res.Add(v); 107 | } 108 | } 109 | 110 | return res; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/Brush.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RadiantMapToObj.Internal.Conversion; 3 | using RadiantMapToObj.Wavefront; 4 | 5 | namespace RadiantMapToObj.Quake 6 | { 7 | /// 8 | /// Class for Brush. 9 | /// 10 | public class Brush : IQuakeEntity 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The clipping planes. 16 | public Brush(IEnumerable clippingPlanes) 17 | => ClippingPlanes = clippingPlanes; 18 | 19 | /// 20 | /// Gets the clipping planes. 21 | /// 22 | public IEnumerable ClippingPlanes { get; } 23 | 24 | /// 25 | public ObjObject ToObjObject() 26 | => BrushConversionHelper.Convert(this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/ClippingPlane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RadiantMapToObj.Quake 4 | { 5 | /// 6 | /// Class for ClippingPlane. 7 | /// 8 | /// 9 | public class ClippingPlane : Plane 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Vertex 1. 15 | /// Vertex 2. 16 | /// Vertex 3. 17 | /// The texture. 18 | public ClippingPlane(Vector v1, Vector v2, Vector v3, PlaneTexture texture) 19 | : base(v1, v2, v3) 20 | => Texture = texture; 21 | 22 | /// 23 | /// Gets the texture. 24 | /// 25 | public PlaneTexture Texture { get; } 26 | 27 | /// 28 | /// Checks if three clipping planes intersect and if so, returns an intersection point. 29 | /// 30 | /// Plane a. 31 | /// Plane b. 32 | /// Plane c. 33 | /// The intersection. 34 | /// If an intersection exists. 35 | public static bool FindIntersection(Plane a, Plane b, Plane c, out Vector? intersection) 36 | { 37 | if (a is null) 38 | { 39 | throw new ArgumentNullException(nameof(a)); 40 | } 41 | 42 | if (b is null) 43 | { 44 | throw new ArgumentNullException(nameof(b)); 45 | } 46 | 47 | if (c is null) 48 | { 49 | throw new ArgumentNullException(nameof(c)); 50 | } 51 | 52 | // Calculates the possible intersection point using the Cramer's rule. 53 | double det = Determinant(a.A, a.B, a.C, b.A, b.B, b.C, c.A, c.B, c.C); 54 | if (det >= -1e-6 && det <= 1e-6) 55 | { 56 | intersection = null; 57 | return false; 58 | } 59 | 60 | double x = Determinant(a.D, a.B, a.C, b.D, b.B, b.C, c.D, c.B, c.C) / det; 61 | double y = Determinant(a.A, a.D, a.C, b.A, b.D, b.C, c.A, c.D, c.C) / det; 62 | double z = Determinant(a.A, a.B, a.D, b.A, b.B, b.D, c.A, c.B, c.D) / det; 63 | 64 | intersection = new Vector(x, y, z); 65 | 66 | return true; 67 | } 68 | 69 | // Calculates the determinant of a 2x2 matrix. (Can be done less verbose...) 70 | private static double Determinant(double a11, double a12, double a21, double a22) 71 | => (a11 * a22) - (a12 * a21); 72 | 73 | // Calculates the determinant of a 3x3 matrix. (Can definitely be done less verbose...) 74 | private static double Determinant(double a11, double a12, double a13, double a21, double a22, double a23, double a31, double a32, double a33) 75 | => (a11 * Determinant(a22, a23, a32, a33)) - (a12 * Determinant(a21, a23, a31, a33)) + (a13 * Determinant(a21, a22, a31, a32)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/Hammer/DisplacementClippingPlane.cs: -------------------------------------------------------------------------------- 1 | namespace RadiantMapToObj.Quake.Hammer 2 | { 3 | /// 4 | /// Represents a clipping plane which should become a displacement. 5 | /// 6 | /// 7 | public class DisplacementClippingPlane : ClippingPlane 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The first vector. 13 | /// The second vector. 14 | /// The third vector. 15 | /// The texture. 16 | /// The displacement info. 17 | public DisplacementClippingPlane(Vector v1, Vector v2, Vector v3, PlaneTexture texture, DisplacementInfo displacement) 18 | : base(v1, v2, v3, texture) 19 | => DisplacementInfo = displacement; 20 | 21 | /// 22 | /// Gets the displacement information. 23 | /// 24 | public DisplacementInfo DisplacementInfo { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/Hammer/DisplacementInfo.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 2 | #pragma warning disable CS1573 3 | #pragma warning disable CS1572 4 | 5 | namespace RadiantMapToObj.Quake.Hammer 6 | { 7 | /// 8 | /// A data holder for displacement info. 9 | /// 10 | /// The dimensions of the displacement. 11 | /// The bottom left corner coordinates. 12 | /// Universal displacement added to vertex normal added to all points. 13 | /// The direction of each vertex. 14 | /// The distance each vertex is moved towards the normal. 15 | /// The position offset for each vertex. 16 | /// The offset towards the direction of each vertex. 17 | public record DisplacementInfo(int Dimensions, Vector StartingPosition, double Elevation, Grid Normals, Grid Distances, Grid Offsets, Grid OffsetNormals); 18 | } 19 | 20 | #pragma warning restore CS1591 21 | #pragma warning restore CS1573 22 | #pragma warning restore CS1572 -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/IQuakeEntity.cs: -------------------------------------------------------------------------------- 1 | using RadiantMapToObj.Wavefront; 2 | 3 | namespace RadiantMapToObj.Quake 4 | { 5 | /// 6 | /// Interface for radiant entities. 7 | /// 8 | public interface IQuakeEntity 9 | { 10 | /// 11 | /// Converts this entity into an instance. 12 | /// 13 | /// A new instance. 14 | ObjObject ToObjObject(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/PlaneTexture.cs: -------------------------------------------------------------------------------- 1 | namespace RadiantMapToObj.Quake 2 | { 3 | /// 4 | /// Class for representing plane texture information. 5 | /// 6 | public class PlaneTexture 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name. 12 | /// The x offset. 13 | /// The y offset. 14 | /// The rotation in degrees. 15 | /// The x scale. 16 | /// The y scale. 17 | public PlaneTexture(string name, double offsetX, double offsetY, double rotation, double scaleX, double scaleY) 18 | => (Name, OffsetX, OffsetY, Rotation, ScaleX, ScaleY) = (name, offsetX, offsetY, rotation, scaleX, scaleY); 19 | 20 | /// 21 | /// Gets the name. 22 | /// 23 | public string Name { get; } 24 | 25 | /// 26 | /// Gets the x offset. 27 | /// 28 | public double OffsetX { get; } 29 | 30 | /// 31 | /// Gets the y offset. 32 | /// 33 | public double OffsetY { get; } 34 | 35 | /// 36 | /// Gets the rotation. 37 | /// 38 | public double Rotation { get; } 39 | 40 | /// 41 | /// Gets the x scale. 42 | /// 43 | public double ScaleX { get; } 44 | 45 | /// 46 | /// Gets the x scale. 47 | /// 48 | public double ScaleY { get; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/QuakeMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using RadiantMapToObj.Internal.Conversion; 5 | using RadiantMapToObj.Internal.Parsing; 6 | using RadiantMapToObj.Wavefront; 7 | 8 | namespace RadiantMapToObj.Quake 9 | { 10 | /// 11 | /// Represents a radiant map. 12 | /// 13 | public class QuakeMap 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The entities. 19 | public QuakeMap(IEnumerable entities) 20 | => Entities = entities; 21 | 22 | /// 23 | /// Gets the entities. 24 | /// 25 | public IEnumerable Entities { get; } 26 | 27 | /// 28 | /// Parses .map file formatted content to a map. 29 | /// 30 | /// The .map content. 31 | /// The parsed radiant map. 32 | public static QuakeMap Parse(string content) 33 | => MapParser.Parse(content); 34 | 35 | /// 36 | /// Parses a .map file to our radiant map object. 37 | /// 38 | /// The path. 39 | /// The parsed radiant map. 40 | public static QuakeMap ParseFile(string path) 41 | => Parse(File.ReadAllText(path)); 42 | 43 | /// 44 | public override string ToString() 45 | { 46 | StringBuilder sb = new StringBuilder(); 47 | 48 | int i = 0; 49 | foreach (IQuakeEntity entity in Entities) 50 | { 51 | sb.AppendLine($"Entity {i++}"); 52 | sb.AppendLine(entity.ToString()); 53 | } 54 | 55 | return sb.ToString(); 56 | } 57 | 58 | /// 59 | /// Converts the map to a instance. 60 | /// 61 | /// A new instance. 62 | public WavefrontObj ToObj() 63 | => MapConversionHelper.Convert(this); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Quake/Radiant/Patch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RadiantMapToObj.Internal.Conversion; 3 | using RadiantMapToObj.Wavefront; 4 | 5 | namespace RadiantMapToObj.Quake.Radiant 6 | { 7 | /// 8 | /// Represents a radiant patch. 9 | /// 10 | public class Patch : IQuakeEntity 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The grid. 16 | public Patch(Grid grid) 17 | => Grid = grid; 18 | 19 | /// 20 | /// Gets the grid. 21 | /// 22 | public Grid Grid { get; } 23 | 24 | /// 25 | /// Gets the width. 26 | /// 27 | public int Width => Grid.Width; 28 | 29 | /// 30 | /// Gets the height. 31 | /// 32 | public int Height => Grid.Height; 33 | 34 | /// 35 | /// Gets all vertices. 36 | /// 37 | public IEnumerable Vertices => Grid.Elements; 38 | 39 | /// 40 | /// Gets the with at the specified x and y coordinates. 41 | /// 42 | /// The x coordinate. 43 | /// The y coordinate. 44 | /// The vertex at the given coordinate. 45 | public Vector this[int x, int y] => Grid[x, y]; 46 | 47 | /// 48 | public ObjObject ToObjObject() 49 | => PatchConversionHelper.Convert(this); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/RadiantMapToObj.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | enable 5 | 9 6 | ../Ruleset.ruleset 7 | bin/$(AssemblyName).xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | compile 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers 22 | 23 | 24 | 25 | 26 | all 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/TextureFinder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RadiantMapToObj.Configuration; 3 | using RadiantMapToObj.Internal.TextureLoading; 4 | 5 | namespace RadiantMapToObj 6 | { 7 | /// 8 | /// Class responsible for finding textures. 9 | /// 10 | public class TextureFinder 11 | { 12 | private Dictionary savedValues = new Dictionary(); 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The settings. 18 | public TextureFinder(TextureSettings settings) 19 | => Settings = settings; 20 | 21 | /// 22 | /// Gets or sets the settings. 23 | /// 24 | public TextureSettings Settings { get; set; } 25 | 26 | /// 27 | /// Finds the size of the given texture. 28 | /// 29 | /// The texture name. 30 | /// The size of the texture. 31 | public (int Width, int Height) FindSize(string texture) 32 | { 33 | if (savedValues.TryGetValue(texture, out (int, int, string) value)) 34 | { 35 | return (value.Item1, value.Item2); 36 | } 37 | 38 | value = TextureFinderHelper.Find(Settings, texture); 39 | savedValues.Add(texture, value); 40 | return (value.Item1, value.Item2); 41 | } 42 | 43 | /// 44 | /// Finds the extension of the given texture. 45 | /// 46 | /// The texture name. 47 | /// The extension of the texture. 48 | public string FindExtension(string texture) 49 | { 50 | if (savedValues.TryGetValue(texture, out (int, int, string) value)) 51 | { 52 | return value.Item3; 53 | } 54 | 55 | value = TextureFinderHelper.Find(Settings, texture); 56 | savedValues.Add(texture, value); 57 | return value.Item3; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Vector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace RadiantMapToObj 5 | { 6 | /// 7 | /// Represents a point in 3D space. 8 | /// 9 | [SuppressMessage("Microsoft.Usage", "CA2225", Justification = "Would make code more convoluted.")] 10 | public class Vector : IEquatable 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The x-axis value. 16 | /// The y-axis value. 17 | /// The z-axis value. 18 | public Vector(double x, double y, double z) 19 | => (X, Y, Z) = (x, y, z); 20 | 21 | /// 22 | /// Gets the x-axis value. 23 | /// 24 | public double X { get; } 25 | 26 | /// 27 | /// Gets the y-axis value. 28 | /// 29 | public double Y { get; } 30 | 31 | /// 32 | /// Gets the z-axis value. 33 | /// 34 | public double Z { get; } 35 | 36 | /// 37 | /// Gets the length of a vector. 38 | /// 39 | public double Length => Math.Sqrt(SquareLength); 40 | 41 | /// 42 | /// Gets the squared length of a vector. 43 | /// 44 | public double SquareLength => (X * X) + (Y * Y) + (Z * Z); 45 | 46 | /// 47 | /// Gets the unit vector of this vector. 48 | /// 49 | public Vector Unit 50 | { 51 | get 52 | { 53 | double length = Length; 54 | return new Vector(X / length, Y / length, Z / length); 55 | } 56 | } 57 | 58 | /// 59 | /// Implements the operator ==. 60 | /// 61 | /// The first vector. 62 | /// The second vector. 63 | /// The result of the operator. 64 | public static bool operator ==(Vector a, Vector b) 65 | { 66 | if (a is null) 67 | { 68 | return b is null; 69 | } 70 | 71 | return a.Equals(b); 72 | } 73 | 74 | /// 75 | /// Implements the operator !=. 76 | /// 77 | /// The first vector. 78 | /// The second vector. 79 | /// The result of the operator. 80 | public static bool operator !=(Vector a, Vector b) 81 | => !(a == b); 82 | 83 | /// 84 | /// Implements the operator +. 85 | /// 86 | /// The first vector. 87 | /// The second vector. 88 | /// The result of the operator. 89 | public static Vector operator +(Vector a, Vector b) 90 | { 91 | if (a is null) 92 | { 93 | throw new ArgumentNullException(nameof(a)); 94 | } 95 | 96 | if (b is null) 97 | { 98 | throw new ArgumentNullException(nameof(b)); 99 | } 100 | 101 | return new Vector(a.X + b.X, a.Y + b.Y, a.Z + b.Z); 102 | } 103 | 104 | /// 105 | /// Implements the operator -. 106 | /// 107 | /// The first vector. 108 | /// The second vector. 109 | /// The result of the operator. 110 | public static Vector operator -(Vector a, Vector b) 111 | { 112 | if (a is null) 113 | { 114 | throw new ArgumentNullException(nameof(a)); 115 | } 116 | 117 | if (b is null) 118 | { 119 | throw new ArgumentNullException(nameof(b)); 120 | } 121 | 122 | return new Vector(a.X - b.X, a.Y - b.Y, a.Z - b.Z); 123 | } 124 | 125 | /// 126 | /// Implements the operator -. 127 | /// 128 | /// The first vector. 129 | /// The result of the operator. 130 | public static Vector operator -(Vector a) 131 | => -1 * a; 132 | 133 | /// 134 | /// Implements the operator *. 135 | /// 136 | /// The first vector. 137 | /// The second vector. 138 | /// The result of the operator. 139 | public static Vector operator *(Vector a, Vector b) 140 | { 141 | if (a is null) 142 | { 143 | throw new ArgumentNullException(nameof(a)); 144 | } 145 | 146 | if (b is null) 147 | { 148 | throw new ArgumentNullException(nameof(b)); 149 | } 150 | 151 | return new Vector(a.X * b.X, a.Y * b.Y, a.Z * b.Z); 152 | } 153 | 154 | /// 155 | /// Implements the operator *. 156 | /// 157 | /// The first vector. 158 | /// The second vector. 159 | /// The result of the operator. 160 | public static Vector operator *(Vector a, double b) 161 | { 162 | if (a is null) 163 | { 164 | throw new ArgumentNullException(nameof(a)); 165 | } 166 | 167 | return new Vector(a.X * b, a.Y * b, a.Z * b); 168 | } 169 | 170 | /// 171 | /// Implements the operator *. 172 | /// 173 | /// The first vector. 174 | /// The second vector. 175 | /// The result of the operator. 176 | public static Vector operator *(double a, Vector b) 177 | { 178 | if (b is null) 179 | { 180 | throw new ArgumentNullException(nameof(b)); 181 | } 182 | 183 | return new Vector(a * b.X, a * b.Y, a * b.Z); 184 | } 185 | 186 | /// 187 | /// Implements the operator *. 188 | /// 189 | /// The first vector. 190 | /// The second vector. 191 | /// The result of the operator. 192 | public static Vector operator /(Vector a, Vector b) 193 | { 194 | if (a is null) 195 | { 196 | throw new ArgumentNullException(nameof(a)); 197 | } 198 | 199 | if (b is null) 200 | { 201 | throw new ArgumentNullException(nameof(b)); 202 | } 203 | 204 | return new Vector(a.X / b.X, a.Y / b.Y, a.Z / b.Z); 205 | } 206 | 207 | /// 208 | /// Implements the operator /. 209 | /// 210 | /// The first vector. 211 | /// The second vector. 212 | /// The result of the operator. 213 | public static Vector operator /(Vector a, double b) 214 | { 215 | if (a is null) 216 | { 217 | throw new ArgumentNullException(nameof(a)); 218 | } 219 | 220 | return new Vector(a.X / b, a.Y / b, a.Z / b); 221 | } 222 | 223 | /// 224 | /// Calculates the cross product between two vectors. 225 | /// 226 | /// The first vector. 227 | /// The second vector. 228 | /// The cross product of the two vectors. 229 | public static Vector CrossProduct(Vector a, Vector b) 230 | { 231 | if (a is null) 232 | { 233 | throw new ArgumentNullException(nameof(a)); 234 | } 235 | 236 | if (b is null) 237 | { 238 | throw new ArgumentNullException(nameof(b)); 239 | } 240 | 241 | double x = (a.Y * b.Z) - (a.Z * b.Y); 242 | double y = (a.Z * b.X) - (a.X * b.Z); 243 | double z = (a.X * b.Y) - (a.Y * b.X); 244 | return new Vector(x, y, z); 245 | } 246 | 247 | /// 248 | /// Calculates the dot product between two vectors. 249 | /// 250 | /// The first vector. 251 | /// The second vector. 252 | /// The dot product of the two vectors. 253 | public static double DotProduct(Vector a, Vector b) 254 | { 255 | if (a is null) 256 | { 257 | throw new ArgumentNullException(nameof(a)); 258 | } 259 | 260 | if (b is null) 261 | { 262 | throw new ArgumentNullException(nameof(b)); 263 | } 264 | 265 | return (a.X * b.X) + (a.Y * b.Y) + (a.Z * b.Z); 266 | } 267 | 268 | /// 269 | /// Get the distance between this point and another given point. 270 | /// 271 | /// The other point. 272 | /// The distance between the two points. 273 | public double Distance(Vector other) 274 | { 275 | if (other is null) 276 | { 277 | throw new ArgumentNullException(nameof(other)); 278 | } 279 | 280 | double dX = X - other.X; 281 | double dY = Y - other.Y; 282 | double dZ = Z - other.Z; 283 | return Math.Sqrt((dX * dX) + (dY * dY) + (dZ * dZ)); 284 | } 285 | 286 | /// 287 | public override string ToString() 288 | => $"<{X}, {Y}, {Z}>"; 289 | 290 | /// 291 | public bool Equals(Vector? other) 292 | { 293 | if (other is null) 294 | { 295 | return false; 296 | } 297 | 298 | return ApproximatelyEquals(X, other.X) && ApproximatelyEquals(Y, other.Y) && ApproximatelyEquals(Z, other.Z); 299 | } 300 | 301 | /// 302 | public override bool Equals(object obj) 303 | { 304 | if (obj is Vector other) 305 | { 306 | return Equals(other); 307 | } 308 | 309 | return false; 310 | } 311 | 312 | /// 313 | public override int GetHashCode() 314 | => (int)Math.Floor((X * 2) + (Y * 4) + (Z * 8)); 315 | 316 | /// 317 | /// Checks if the vector coordinates lie on a plane. 318 | /// 319 | /// The plane. 320 | /// True if the vector is on the plane, false otherwise. 321 | public bool OnPlane(Plane plane) 322 | { 323 | if (plane is null) 324 | { 325 | throw new ArgumentNullException(nameof(plane)); 326 | } 327 | 328 | double left = (X * plane.A) + (Y * plane.B) + (Z * plane.C); 329 | double right = plane.D; 330 | bool res = left >= right - 1e-6 && left <= right + 1e-6; 331 | return res; 332 | } 333 | 334 | /// 335 | /// Checks if two vectors have the same direction. 336 | /// 337 | /// The other vector. 338 | /// True if direction is equal, false otherwise. 339 | public bool DirectionEquals(Vector other) 340 | { 341 | if (other is null) 342 | { 343 | throw new ArgumentNullException(nameof(other)); 344 | } 345 | 346 | return Unit.Equals(other.Unit); 347 | } 348 | 349 | /// 350 | /// Checks if two doubles are roughly equal. 351 | /// 352 | /// The first double. 353 | /// The second double. 354 | /// True if they are roughly equal. 355 | private static bool ApproximatelyEquals(double a, double b) 356 | { 357 | double delta = a - b; 358 | if (delta >= -1e-6 && delta <= 1e-6) 359 | { 360 | return true; 361 | } 362 | 363 | return false; 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/Edge.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RadiantMapToObj.Wavefront 4 | { 5 | /// 6 | /// Class for Edge. 7 | /// 8 | public class Edge : IEquatable 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Vertex a of the edge. 14 | /// Vertex b of the edge. 15 | public Edge(Vector a, Vector b) 16 | { 17 | A = a; 18 | B = b; 19 | } 20 | 21 | /// 22 | /// Gets one of the vertices of the edge. 23 | /// 24 | public Vector A { get; } 25 | 26 | /// 27 | /// Gets one of the vertices of the edge. 28 | /// 29 | public Vector B { get; } 30 | 31 | /// 32 | /// Gets the vector. 33 | /// 34 | /// the Vector. 35 | public Vector Vector 36 | => B - A; 37 | 38 | /// 39 | /// Gets the length. 40 | /// 41 | public double Length 42 | => Vector.Length; 43 | 44 | /// 45 | /// Gets the inverse. 46 | /// 47 | /// Inverse of Edge. 48 | public Edge Inverse 49 | => new Edge(B, A); 50 | 51 | /// 52 | /// Implements the operator ==. 53 | /// 54 | /// One of the edges. 55 | /// Another edge. 56 | /// 57 | /// The result of the equals operator. 58 | /// 59 | public static bool operator ==(Edge a, Edge b) 60 | { 61 | if (a is null) 62 | { 63 | return b is null; 64 | } 65 | 66 | return a.Equals(b); 67 | } 68 | 69 | /// 70 | /// Implements the operator !=. 71 | /// 72 | /// One of the edges. 73 | /// The other of the edges. 74 | /// 75 | /// The result of the non-equality operator. 76 | /// 77 | public static bool operator !=(Edge a, Edge b) 78 | => !(a == b); 79 | 80 | /// 81 | public override string ToString() 82 | => $"<{A}, {B}>"; 83 | 84 | /// 85 | public override bool Equals(object obj) 86 | { 87 | if (obj is Edge that) 88 | { 89 | Equals(that); 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /// 96 | public override int GetHashCode() 97 | => A.GetHashCode() + (2 * B.GetHashCode()); 98 | 99 | /// 100 | public bool Equals(Edge? other) 101 | { 102 | if (other is null) 103 | { 104 | return false; 105 | } 106 | 107 | return A == other.A && B == other.B; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/Face.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RadiantMapToObj.Wavefront 5 | { 6 | /// 7 | /// Class for Faces. 8 | /// 9 | public class Face : IEquatable 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The first vertex. 15 | /// The second vertex. 16 | /// The third vertex. 17 | /// The texture. 18 | public Face(Vector a, Vector b, Vector c, string texture) 19 | => (A, B, C, Texture) = (a, b, c, texture); 20 | 21 | /// 22 | /// Gets the texture. 23 | /// 24 | public string Texture { get; } 25 | 26 | /// 27 | /// Gets the first vertex. 28 | /// 29 | public Vector A { get; } 30 | 31 | /// 32 | /// Gets the second vertex. 33 | /// 34 | public Vector B { get; } 35 | 36 | /// 37 | /// Gets the third vertex. 38 | /// 39 | public Vector C { get; } 40 | 41 | /// 42 | /// Gets the vertices. 43 | /// 44 | public IEnumerable Vertices 45 | => new Vector[] { A, B, C }; 46 | 47 | /// 48 | /// Implements the operator ==. 49 | /// 50 | /// a. 51 | /// The b. 52 | /// 53 | /// The result of the double equals operator. 54 | /// 55 | public static bool operator ==(Face a, Face b) 56 | { 57 | if (a is null) 58 | { 59 | return b is null; 60 | } 61 | 62 | return a.Equals(b); 63 | } 64 | 65 | /// 66 | /// Implements the operator !=. 67 | /// 68 | /// a. 69 | /// The b. 70 | /// 71 | /// The result of the operator. 72 | /// 73 | public static bool operator !=(Face a, Face b) 74 | => !(a == b); 75 | 76 | /// 77 | /// Gets the circumsphere of this triangle. 78 | /// 79 | /// Circumsphere of the triangle. 80 | public Tuple GetCircumsphere() 81 | { 82 | Vector v0 = B - A; 83 | Vector v1 = C - A; 84 | 85 | Vector vx = Vector.CrossProduct(v0, v1); 86 | 87 | Vector centerVector = ((Vector.CrossProduct(vx, v0) * v1.SquareLength) + (Vector.CrossProduct(v1, vx) * v0.SquareLength)) / (2 * vx.SquareLength); 88 | Vector center = A + centerVector; 89 | 90 | double radius = centerVector.Length; 91 | 92 | return new Tuple(center, radius); 93 | } 94 | 95 | /// 96 | /// Gets the edges. 97 | /// 98 | /// Edges of the face. 99 | public IEnumerable GetEdges() 100 | { 101 | HashSet edges = new HashSet(); 102 | edges.Add(new Edge(A, B)); 103 | edges.Add(new Edge(B, C)); 104 | edges.Add(new Edge(C, A)); 105 | return edges; 106 | } 107 | 108 | /// 109 | /// Determines whether this face contains a vertex. 110 | /// 111 | /// The vertex. 112 | /// 113 | /// true if [contains] [the specified vertex]; otherwise, false. 114 | /// 115 | public bool Contains(Vector vertex) 116 | => A == vertex || B == vertex || C == vertex; 117 | 118 | /// 119 | public override string ToString() 120 | => $"({A}, {B}, {C})"; 121 | 122 | /// 123 | public bool Equals(Face? other) 124 | { 125 | if (other is null) 126 | { 127 | return false; 128 | } 129 | 130 | return A == other.A && B == other.B && C == other.C; 131 | } 132 | 133 | /// 134 | public override bool Equals(object obj) 135 | { 136 | if (obj is Face other) 137 | { 138 | return Equals(other); 139 | } 140 | 141 | return false; 142 | } 143 | 144 | /// 145 | public override int GetHashCode() 146 | => (A.GetHashCode() * 2) + (B.GetHashCode() * 4) + (C.GetHashCode() * 8); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/ObjObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using RadiantMapToObj.Configuration; 7 | using RadiantMapToObj.Internal; 8 | 9 | namespace RadiantMapToObj.Wavefront 10 | { 11 | /// 12 | /// Represents Wavefront Obj objects. 13 | /// 14 | public class ObjObject 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The vertices. 20 | /// The faces. 21 | public ObjObject(IEnumerable vertices, IEnumerable faces) 22 | { 23 | Vertices = vertices.ToList(); 24 | Faces = faces.ToList(); 25 | Cleanup(); 26 | } 27 | 28 | /// 29 | /// Gets the vertices. 30 | /// 31 | public IEnumerable Vertices { get; private set; } 32 | 33 | /// 34 | /// Gets the faces. 35 | /// 36 | public IEnumerable Faces { get; private set; } 37 | 38 | /// 39 | /// Gets the texture coordinates. 40 | /// 41 | public IEnumerable TextureCoordinates { get; private set; } 42 | 43 | /// 44 | /// Converts to .obj file content. 45 | /// 46 | /// The name of the object. 47 | /// The scale. 48 | /// The face vector offset. 49 | /// The face texture coordinate offset. 50 | /// The .obj file content. 51 | public string ToCode(string name, double scale, int faceVectorOffset, int faceTextureOffset) 52 | { 53 | StringBuilder sb = new StringBuilder(); 54 | sb.Append("o ").AppendLine(name); 55 | 56 | // Write vertices. 57 | foreach (Vector vertex in Vertices) 58 | { 59 | string x = ToCoordinateString(-vertex.X * scale); 60 | string y = ToCoordinateString(-vertex.Z * scale); 61 | string z = ToCoordinateString(vertex.Y * scale); 62 | sb.Append("v ").Append(x).Append(' ').Append(y).Append(' ').AppendLine(z); 63 | } 64 | 65 | // Write texture coordinates. 66 | foreach (TextureCoordinate uv in TextureCoordinates) 67 | { 68 | string u = ToCoordinateString(uv.U); 69 | string v = ToCoordinateString(uv.V); 70 | 71 | sb.Append("vt ").Append(u).Append(' ').AppendLine(v); 72 | } 73 | 74 | // Write faces. 75 | foreach (Face face in Faces) 76 | { 77 | string v1 = GetVertexString(face.A, faceVectorOffset, faceTextureOffset); 78 | string v2 = GetVertexString(face.B, faceVectorOffset, faceTextureOffset); 79 | string v3 = GetVertexString(face.C, faceVectorOffset, faceTextureOffset); 80 | sb.Append("usemtl ").AppendLine(face.Texture); 81 | sb.Append("f ").Append(v1).Append(' ').Append(v2).Append(' ').Append(v3).AppendLine(); 82 | } 83 | 84 | return sb.ToString(); 85 | } 86 | 87 | /// 88 | /// Removes all textures that are in the filter. 89 | /// 90 | /// The filter. 91 | public void FilterTextures(Filter filter) 92 | { 93 | if (filter is null) 94 | { 95 | throw new ArgumentNullException(nameof(filter)); 96 | } 97 | 98 | List newFaces = new List(); 99 | 100 | foreach (Face face in Faces) 101 | { 102 | if (!filter.Contains(face.Texture)) 103 | { 104 | newFaces.Add(face); 105 | } 106 | } 107 | 108 | Faces = newFaces; 109 | 110 | Cleanup(); 111 | } 112 | 113 | private static string ToCoordinateString(double x) 114 | { 115 | string result = x.ToString("0.000000", CultureInfo.InvariantCulture); 116 | 117 | if (x == 0 && result[0] == '-') 118 | { 119 | return result.Substring(1); 120 | } 121 | 122 | return result; 123 | } 124 | 125 | private string GetVertexString(Vector v, int faceVectorOffset, int faceTextureOffset) 126 | { 127 | int vi = Vertices.IndexOf(v) + 1 + faceVectorOffset; 128 | string result = vi.ToString(CultureInfo.InvariantCulture); 129 | 130 | if (v is Vertex vrt) 131 | { 132 | TextureCoordinate uv = new TextureCoordinate(vrt.U, vrt.V); 133 | int vti = TextureCoordinates.IndexOf(uv) + 1 + faceTextureOffset; 134 | result += "/" + vti.ToString(CultureInfo.InvariantCulture); 135 | } 136 | 137 | return result; 138 | } 139 | 140 | /// 141 | /// Removes all vertices without faces. 142 | /// 143 | private void Cleanup() 144 | { 145 | if (Faces == null) 146 | { 147 | return; 148 | } 149 | 150 | List newVertices = new List(); 151 | 152 | foreach (Vector vertex in Vertices) 153 | { 154 | bool contained = false; 155 | 156 | foreach (Face face in Faces) 157 | { 158 | if (face.Contains(vertex)) 159 | { 160 | contained = true; 161 | break; 162 | } 163 | } 164 | 165 | if (contained) 166 | { 167 | newVertices.Add(vertex); 168 | } 169 | } 170 | 171 | Vertices = newVertices; 172 | TextureCoordinates = Faces.SelectMany(x => x.Vertices).Where(x => x is Vertex).Select(x => new TextureCoordinate(((Vertex)x).U, ((Vertex)x).V)).Distinct().ToList(); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/TextureCoordinate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RadiantMapToObj 4 | { 5 | /// 6 | /// Represents a uv coordinate. 7 | /// 8 | public class TextureCoordinate : IEquatable 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The u coordinate. 14 | /// The v coordinate. 15 | public TextureCoordinate(double u, double v) 16 | => (U, V) = (u, v); 17 | 18 | /// 19 | /// Gets the u coordinate. 20 | /// 21 | public double U { get; } 22 | 23 | /// 24 | /// Gets the v coordinate. 25 | /// 26 | public double V { get; } 27 | 28 | /// 29 | /// Implements the operator ==. 30 | /// 31 | /// The first coordinate. 32 | /// The second coordinate. 33 | /// The result of the operator. 34 | public static bool operator ==(TextureCoordinate a, TextureCoordinate b) 35 | { 36 | if (a is null) 37 | { 38 | return b is null; 39 | } 40 | 41 | return a.Equals(b); 42 | } 43 | 44 | /// 45 | /// Implements the operator !=. 46 | /// 47 | /// The first coordinate. 48 | /// The second coordinate. 49 | /// The result of the operator. 50 | public static bool operator !=(TextureCoordinate a, TextureCoordinate b) 51 | => !(a == b); 52 | 53 | /// 54 | public override string ToString() 55 | => $"<{U}, {V}>"; 56 | 57 | /// 58 | public bool Equals(TextureCoordinate? other) 59 | { 60 | if (other is null) 61 | { 62 | return false; 63 | } 64 | 65 | return ApproximatelyEquals(U, other.U) && ApproximatelyEquals(V, other.V); 66 | } 67 | 68 | /// 69 | public override bool Equals(object obj) 70 | { 71 | if (obj is TextureCoordinate other) 72 | { 73 | return Equals(other); 74 | } 75 | 76 | return false; 77 | } 78 | 79 | /// 80 | public override int GetHashCode() 81 | => (int)Math.Floor((U * 3) + (V * 6)); 82 | 83 | /// 84 | /// Checks if two doubles are roughly equal. 85 | /// 86 | /// The first double. 87 | /// The second double. 88 | /// True if they are roughly equal. 89 | private static bool ApproximatelyEquals(double a, double b) 90 | { 91 | double delta = a - b; 92 | if (delta >= -1e-6 && delta <= 1e-6) 93 | { 94 | return true; 95 | } 96 | 97 | return false; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/Vertex.cs: -------------------------------------------------------------------------------- 1 | namespace RadiantMapToObj.Wavefront 2 | { 3 | /// 4 | /// A 3d vertex with a texture. 5 | /// 6 | /// 7 | public class Vertex : Vector 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The x coordinate. 13 | /// The y coordinate. 14 | /// The z coordinate. 15 | /// The u coordinate. 16 | /// The v coordinate. 17 | public Vertex(double x, double y, double z, double u, double v) 18 | : base(x, y, z) 19 | => (U, V) = (u, v); 20 | 21 | /// 22 | /// Gets the u coordinate. 23 | /// 24 | public double U { get; } 25 | 26 | /// 27 | /// Gets the v coordinate. 28 | /// 29 | public double V { get; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/RadiantMapToObj/Wavefront/WavefrontObj.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using RadiantMapToObj.Configuration; 7 | 8 | namespace RadiantMapToObj.Wavefront 9 | { 10 | /// 11 | /// Represents a wavefront obj file. 12 | /// 13 | public class WavefrontObj 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The objects. 19 | public WavefrontObj(IEnumerable objects) 20 | { 21 | Objects = objects.ToList(); 22 | Cleanup(); 23 | } 24 | 25 | /// 26 | /// Gets the objects. 27 | /// 28 | public IEnumerable Objects { get; private set; } 29 | 30 | /// 31 | /// Removes all faces containing a texture listed in the filter from all subobjects. 32 | /// 33 | /// The filter. 34 | public void FilterTextures(Filter filter) 35 | { 36 | if (filter is null) 37 | { 38 | throw new ArgumentNullException(nameof(filter)); 39 | } 40 | 41 | foreach (ObjObject obj in Objects) 42 | { 43 | obj.FilterTextures(filter); 44 | } 45 | 46 | Cleanup(); 47 | } 48 | 49 | /// 50 | /// Converts the object to .obj file content. 51 | /// 52 | /// The name of the file. 53 | /// The scale. 54 | /// The object represented in .obj file content format. 55 | public string ToCode(string fileName, double scale) 56 | { 57 | StringBuilder sb = new StringBuilder(); 58 | sb.AppendLine("# Exported using Wesley Baartman's RadiantMapToObj software."); 59 | sb.AppendLine("# https://github.com/CptWesley/RadiantMapToWavefrontObj"); 60 | sb.Append("mtllib ").Append(fileName).AppendLine(".mtl"); 61 | 62 | int faceVectorOffset = 0; 63 | int faceTextureOffset = 0; 64 | 65 | // Adds code for each object contained. 66 | int i = 0; 67 | foreach (ObjObject obj in Objects) 68 | { 69 | sb.AppendLine(obj.ToCode($"Object_{i++}", scale, faceVectorOffset, faceTextureOffset)); 70 | faceVectorOffset += obj.Vertices.Count(); 71 | faceTextureOffset += obj.TextureCoordinates.Count(); 72 | } 73 | 74 | return sb.ToString(); 75 | } 76 | 77 | /// 78 | /// Converts the object into .mtl file content. 79 | /// 80 | /// The texture finder. 81 | /// The content for the .mtl file. 82 | public string ToMaterialCode(TextureFinder textureFinder) 83 | { 84 | if (textureFinder is null) 85 | { 86 | throw new ArgumentNullException(nameof(textureFinder)); 87 | } 88 | 89 | StringBuilder sb = new StringBuilder(); 90 | HashSet savedTextures = new HashSet(); 91 | foreach (ObjObject obj in Objects) 92 | { 93 | foreach (string texture in obj.Faces.Select(x => x.Texture)) 94 | { 95 | if (!savedTextures.Contains(texture)) 96 | { 97 | savedTextures.Add(texture); 98 | sb.Append("newmtl ").AppendLine(texture); 99 | sb.Append("map_Kd ").Append(texture).AppendLine(textureFinder.FindExtension(texture)); 100 | } 101 | } 102 | } 103 | 104 | return sb.ToString(); 105 | } 106 | 107 | /// 108 | /// Saves this object to an .obj file. 109 | /// 110 | /// The path. 111 | /// The scale. 112 | public void SaveFile(string path, double scale) 113 | => File.WriteAllText(path, ToCode(Path.GetFileNameWithoutExtension(path), scale)); 114 | 115 | /// 116 | /// Saves the .mtl file. 117 | /// 118 | /// The path. 119 | /// The texture finder. 120 | public void SaveMaterialFile(string path, TextureFinder textureFinder) 121 | => File.WriteAllText(path, ToMaterialCode(textureFinder)); 122 | 123 | /// 124 | /// Removes objects that lack faces or vertices. 125 | /// 126 | private void Cleanup() 127 | => Objects = Objects.Where(obj => obj.Faces != null && obj.Faces.Any() && obj.Vertices.Any()).ToList(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Ruleset.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4 7 | }, 8 | "maintainabilityRules": { 9 | "topLevelTypes": [ "class", "interface", "struct" ] 10 | }, 11 | "orderingRules": { 12 | "usingDirectivesPlacement": "outsideNamespace", 13 | "elementOrder": [ "kind", "constant", "accessibility", "static", "readonly" ] 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------