├── .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 |
--------------------------------------------------------------------------------