├── .github └── workflows │ └── CD.yaml ├── .gitignore ├── App.config ├── ApplicationModule.cs ├── Controllers ├── AbstractExportController.cs ├── ConfigController.cs ├── FileOpenController.cs ├── GUI │ ├── AnimationController.cs │ ├── CameraController.cs │ ├── GUIExportController.cs │ ├── GUILBDController.cs │ ├── GUIMOMController.cs │ ├── GUITIMController.cs │ ├── GUITIXController.cs │ ├── GUITMDController.cs │ ├── TreeController.cs │ └── VRAMController.cs ├── Headless │ ├── HeadlessConfigController.cs │ ├── HeadlessExportController.cs │ ├── HeadlessLBDController.cs │ ├── HeadlessMOMController.cs │ ├── HeadlessTIMController.cs │ ├── HeadlessTIXController.cs │ ├── HeadlessTMDController.cs │ └── HeadlessVRAMController.cs ├── Interface │ ├── IAnimationController.cs │ ├── ICameraController.cs │ ├── IConfigController.cs │ ├── IExportController.cs │ ├── IFileFormatController.cs │ ├── IFileOpenController.cs │ ├── ITreeController.cs │ ├── IUpdateCheckerController.cs │ ├── IVRAMController.cs │ └── MeshExportFormat.cs └── UpdateCheckerController.cs ├── Fonts ├── IconsFontAwesome5.cs ├── fa-regular-400.ttf └── fa-solid-900.ttf ├── GUI ├── Components │ ├── AnimatedMeshListTreeNode.cs │ ├── ApplicationArea.cs │ ├── Columns.cs │ ├── ContextMenu.cs │ ├── FileDialog.cs │ ├── FramebufferArea.cs │ ├── GenericDialog.cs │ ├── InfoDialog.cs │ ├── LBDTileTreeNode.cs │ ├── MainMenuBar.cs │ ├── MeshListTreeNode.cs │ ├── Modal.cs │ ├── TreeNode.cs │ └── TreeView.cs ├── ImGuiComponent.cs └── ImGuiRenderer.cs ├── Graphics ├── Camera.cs ├── Framebuffer.cs ├── GLBuffer.cs ├── Headless │ ├── HeadlessMesh.cs │ ├── HeadlessTexture2D.cs │ └── HeadlessVertexArray.cs ├── IBindable.cs ├── IDisposable.cs ├── IRenderable.cs ├── ITexture2D.cs ├── IVertexArray.cs ├── IVertexArrayExtensions.cs ├── Material.cs ├── Mesh.cs ├── Shader.cs ├── Texture2D.cs ├── Vertex.cs ├── VertexArray.cs └── VertexAttrib.cs ├── GuiApplication.cs ├── Headless ├── AbstractHeadlessCommand.cs ├── ExportLevelCommand.cs ├── GlobalOptions.cs ├── HeadlessException.cs └── IHeadlessCommand.cs ├── HeadlessApplication.cs ├── LICENSE ├── LSDView.csproj ├── LSDView.sln ├── Math ├── Convert.cs └── Transform.cs ├── Models ├── AbstractDocument.cs ├── Dream.cs ├── IDocument.cs ├── LBDDocument.cs ├── LSDViewConfig.cs ├── MOMDocument.cs ├── TIMDocument.cs ├── TIXDocument.cs └── TMDDocument.cs ├── OpenTK.dll.config ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── Shaders ├── basic.frag ├── basic.vert ├── texture.frag ├── texture.vert ├── untextured.frag └── untextured.vert ├── Util ├── ConsoleUtil.cs ├── ImageUtil.cs ├── LibLSDUtil.cs ├── LoggingUtil.cs ├── MeshUtil.cs ├── ObjBuilder.cs ├── PathUtil.cs └── PlyBuilder.cs ├── Version.cs ├── appicon.ico ├── img ├── correct-shading.png ├── screenshot.png ├── split-normals.png └── strange-shading.png └── packages.config /.github/workflows/CD.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | buildAndPush: 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | name: Checkout Code 13 | - name: Extract tag name 14 | run: | 15 | echo "GIT_TAG=$(git tag --sort=-creatordate | head -n 1)" >> $env:GITHUB_ENV 16 | - name: Inject version number 17 | uses: cschleiden/replace-tokens@v1.0 18 | with: 19 | files: "**" 20 | env: 21 | VERSION: ${{ env.GIT_TAG }} 22 | - name: Add msbuild to PATH 23 | uses: microsoft/setup-msbuild@v1.0.2 24 | - name: Setup NuGet 25 | uses: nuget/setup-nuget@v1 26 | with: 27 | nuget-version: '5.x' 28 | - name: Restore NuGet packages 29 | run: nuget restore 30 | - name: Build solution 31 | run: msbuild LSDView.sln /p:Configuration=Release 32 | - name: Zip build 33 | run: Compress-Archive -Path bin/Release/* -DestinationPath LSDView.zip 34 | - name: Get Release 35 | id: get-release 36 | uses: bruceadams/get-release@v1.2.0 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Upload Release Asset 40 | uses: actions/upload-release-asset@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | upload_url: ${{ steps.get-release.outputs.upload_url }} 45 | asset_path: LSDView.zip 46 | asset_name: LSDView.zip 47 | asset_content_type: application/zip 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | /TestFiles 291 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ApplicationModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using libLSD.Formats; 3 | using LSDView.Controller; 4 | using LSDView.Controllers; 5 | using LSDView.Controllers.GUI; 6 | using LSDView.Controllers.Headless; 7 | using LSDView.Controllers.Interface; 8 | using LSDView.Models; 9 | 10 | namespace LSDView 11 | { 12 | public class ApplicationModule : Module 13 | { 14 | public bool Headless { get; set; } 15 | 16 | protected override void Load(ContainerBuilder builder) 17 | { 18 | builder.RegisterType().AsSelf().As(); 19 | builder.RegisterType().AsSelf().As(); 20 | builder.RegisterType().AsSelf().As(); 21 | 22 | if (Headless) 23 | { 24 | registerHeadlessTypes(builder); 25 | } 26 | else 27 | { 28 | registerGUITypes(builder); 29 | } 30 | } 31 | 32 | protected void registerGUITypes(ContainerBuilder builder) 33 | { 34 | builder.RegisterType().AsSelf().As().SingleInstance(); 35 | builder.RegisterType().AsSelf().As().SingleInstance(); 36 | builder.RegisterType().AsSelf().As().SingleInstance(); 37 | builder.RegisterType().AsSelf().As>() 38 | .SingleInstance(); 39 | builder.RegisterType().AsSelf().As>() 40 | .SingleInstance(); 41 | builder.RegisterType().AsSelf().As>() 42 | .SingleInstance(); 43 | builder.RegisterType().AsSelf().As>() 44 | .SingleInstance(); 45 | builder.RegisterType().AsSelf().As>() 46 | .SingleInstance(); 47 | builder.RegisterType().AsSelf().As().SingleInstance(); 48 | builder.RegisterType().AsSelf().As().SingleInstance(); 49 | } 50 | 51 | protected void registerHeadlessTypes(ContainerBuilder builder) 52 | { 53 | builder.RegisterType().AsSelf().As().SingleInstance(); 54 | builder.RegisterType().AsSelf().As>() 55 | .SingleInstance(); 56 | builder.RegisterType().AsSelf().As>() 57 | .SingleInstance(); 58 | builder.RegisterType().AsSelf().As>() 59 | .SingleInstance(); 60 | builder.RegisterType().AsSelf().As>() 61 | .SingleInstance(); 62 | builder.RegisterType().AsSelf().As>() 63 | .SingleInstance(); 64 | builder.RegisterType().AsSelf().As().SingleInstance(); 65 | builder.RegisterType().AsSelf().As().SingleInstance(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Controllers/AbstractExportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Imaging; 5 | using System.IO; 6 | using libLSD.Formats; 7 | using libLSD.Interfaces; 8 | using LSDView.Controllers.Interface; 9 | using LSDView.Graphics; 10 | using LSDView.Graphics.Headless; 11 | using LSDView.Models; 12 | using LSDView.Util; 13 | using OpenTK; 14 | using Serilog; 15 | 16 | namespace LSDView.Controllers 17 | { 18 | public abstract class AbstractExportController : IExportController 19 | { 20 | /// 21 | /// Export a LibLSD type as it was originally. 22 | /// 23 | /// The LibLSD type (MOM, TIX, TOD, etc). 24 | /// The path to write the file to. 25 | public void ExportOriginal(IWriteable original, string filePath) 26 | { 27 | Log.Information($"Exporting original to: {filePath}"); 28 | 29 | using (BinaryWriter bw = new BinaryWriter(File.Open(filePath, FileMode.Create))) 30 | { 31 | original.Write(bw); 32 | } 33 | } 34 | 35 | /// 36 | /// Export a TIM file to a common image format. 37 | /// 38 | /// The TIM file. 39 | /// The index of the CLUT to export with. 40 | /// The file path to export to. 41 | /// The image format to export to. 42 | public void ExportImage(TIM tim, int clutIndex, string filePath, ImageFormat format) 43 | { 44 | Log.Information($"Exporting image ({format}) to: {filePath}"); 45 | 46 | var image = LibLSDUtil.GetImageDataFromTIM(tim, clutIndex, flip: false); 47 | Bitmap bmp = ImageUtil.ImageDataToBitmap(image.data, image.width, image.height); 48 | bmp.Save(filePath, format); 49 | } 50 | 51 | /// 52 | /// Export the TIM files in a TIX to common image formats. 53 | /// 54 | /// The TIX file. 55 | /// The file path to export to. 56 | /// Whether or not to separate images in the output. 57 | /// The image format to export to. 58 | public void ExportImages(TIX tix, string filePath, bool separate, ImageFormat format) 59 | { 60 | if (separate) 61 | { 62 | Log.Information($"Exporting images ({format}) in TIX to: {filePath}"); 63 | 64 | var allTims = tix.AllTIMs; 65 | for (int i = 0; i < allTims.Count; i++) 66 | { 67 | var fileName = Path.GetFileNameWithoutExtension(filePath); 68 | var ext = Path.GetExtension(filePath); 69 | var dir = Path.GetDirectoryName(filePath); 70 | ExportImage(allTims[i], 0, Path.Combine(dir, $"{fileName}-{i}{ext}"), format); 71 | } 72 | } 73 | else 74 | { 75 | ITexture2D tixTex = LibLSDUtil.TIXToTexture2D(tix, headless: true, flip: false); 76 | ExportTexture(tixTex, filePath, format); 77 | } 78 | } 79 | 80 | public void ExportTexture(ITexture2D tex, string filePath, ImageFormat format) 81 | { 82 | float[] imageData = tex.GetData(); 83 | Bitmap bmp = ImageUtil.ImageDataToBitmap(imageData, tex.Width, tex.Height); 84 | bmp.Save(filePath, format); 85 | } 86 | 87 | public void ExportMesh(IRenderable mesh, string filePath, MeshExportFormat format) 88 | { 89 | switch (format) 90 | { 91 | case MeshExportFormat.OBJ: 92 | ExportOBJ(mesh, filePath); 93 | break; 94 | case MeshExportFormat.PLY: 95 | ExportPLY(mesh, filePath); 96 | break; 97 | default: 98 | throw new ArgumentOutOfRangeException(nameof(format), format, null); 99 | } 100 | } 101 | 102 | public void ExportMeshes(IEnumerable meshes, string filePath, MeshExportFormat format) 103 | { 104 | switch (format) 105 | { 106 | case MeshExportFormat.OBJ: 107 | ExportOBJ(meshes, filePath); 108 | break; 109 | case MeshExportFormat.PLY: 110 | ExportPLY(meshes, filePath); 111 | break; 112 | default: 113 | throw new ArgumentOutOfRangeException(nameof(format), format, null); 114 | } 115 | } 116 | 117 | /// 118 | /// Export a renderable mesh to an OBJ file. 119 | /// 120 | /// The mesh to export. 121 | /// The path to export the OBJ file to. 122 | public void ExportOBJ(IRenderable mesh, string filePath) 123 | { 124 | Log.Information($"Exporting OBJ mesh to: {filePath}"); 125 | 126 | if (String.IsNullOrEmpty(Path.GetExtension(filePath))) 127 | { 128 | filePath += ".obj"; 129 | } 130 | 131 | var objFile = MeshUtil.RenderableToObjFile(mesh); 132 | File.WriteAllText(filePath, objFile); 133 | } 134 | 135 | /// 136 | /// Export a list of renderable meshes to an OBJ file. 137 | /// 138 | /// The meshes to export. 139 | /// The path to export the OBJ file to. 140 | public void ExportOBJ(IEnumerable meshes, string filePath) 141 | { 142 | Log.Information($"Exporting OBJ meshes to: {filePath}"); 143 | 144 | if (String.IsNullOrEmpty(Path.GetExtension(filePath))) 145 | { 146 | filePath += ".obj"; 147 | } 148 | 149 | var objFile = MeshUtil.RenderableListToObjFile(meshes); 150 | File.WriteAllText(filePath, objFile); 151 | } 152 | 153 | 154 | /// 155 | /// Export a renderable mesh to an PLY file. 156 | /// 157 | /// The mesh to export. 158 | /// The path to export the PLY file to. 159 | public void ExportPLY(IRenderable mesh, string filePath) 160 | { 161 | Log.Information($"Exporting PLY mesh to: {filePath}"); 162 | 163 | if (String.IsNullOrEmpty(Path.GetExtension(filePath))) 164 | { 165 | filePath += ".ply"; 166 | } 167 | 168 | var plyFile = MeshUtil.RenderableToPlyFile(mesh); 169 | File.WriteAllText(filePath, plyFile); 170 | } 171 | 172 | /// 173 | /// Export a list of renderable meshes to an PLY file. 174 | /// 175 | /// The meshes to export. 176 | /// The path to export the PLY file to. 177 | public void ExportPLY(IEnumerable meshes, string filePath) 178 | { 179 | Log.Information($"Exporting PLY meshes to: {filePath}"); 180 | 181 | if (String.IsNullOrEmpty(Path.GetExtension(filePath))) 182 | { 183 | filePath += ".ply"; 184 | } 185 | 186 | var plyFile = MeshUtil.RenderableListToPlyFile(meshes); 187 | File.WriteAllText(filePath, plyFile); 188 | } 189 | 190 | /// 191 | /// Export an entire dream to a 3D format with textures. 192 | /// 193 | /// The dream we're exporting. 194 | /// Whether or not chunks should be combined into a single mesh. 195 | /// The mesh format to export the level mesh in. 196 | /// The directory we're exporting meshes to. 197 | public void ExportDream(Dream dream, bool combineChunks, MeshExportFormat exportFormat, string exportDirectory) 198 | { 199 | Log.Information($"Exporting dream to: {exportDirectory}"); 200 | 201 | Log.Information($"Dream width: {dream.LevelWidth}"); 202 | 203 | // create the directory 204 | Directory.CreateDirectory(exportDirectory); 205 | 206 | // combine tilelayouts of each LBDDocument into single renderables 207 | List chunkRenderables = new List(); 208 | for (int i = 0; i < dream.Chunks.Count; i++) 209 | { 210 | Log.Information($"Processing dream chunk {i + 1}/{dream.Chunks.Count}..."); 211 | 212 | var lbdChunk = dream.Chunks[i]; 213 | var chunkRenderable = HeadlessMesh.CombineMeshes(lbdChunk.TileLayout.ToArray()); 214 | 215 | // position the LBD chunk based on tiling (if we're tiling) 216 | if (dream.LevelWidth > 0) 217 | { 218 | int xPos = i % dream.LevelWidth; 219 | int yPos = i / dream.LevelWidth; 220 | 221 | // handle staggered tiling, every other row 222 | int xMod = 0; 223 | if (yPos % 2 == 1) 224 | { 225 | xMod = Dream.CHUNK_DIMENSION / 2; // stagger by half the width of a chunk 226 | } 227 | 228 | chunkRenderable.Transform.Position = new Vector3(xPos * Dream.CHUNK_DIMENSION - xMod, 0, 229 | yPos * Dream.CHUNK_DIMENSION); 230 | } 231 | 232 | chunkRenderables.Add(chunkRenderable); 233 | } 234 | 235 | string pathToLevel = Path.Combine(exportDirectory, "dream"); 236 | if (combineChunks) 237 | { 238 | // if we're combining chunks, we now need to combine everything into a single renderable 239 | IRenderable dreamRenderable = HeadlessMesh.CombineMeshes(chunkRenderables.ToArray()); 240 | ExportMesh(dreamRenderable, pathToLevel, exportFormat); 241 | } 242 | else 243 | { 244 | // otherwise we can export the list of chunk renderables 245 | ExportMeshes(chunkRenderables, pathToLevel, exportFormat); 246 | } 247 | 248 | // now, export each TIXDocument texture set as a combined VRAM export 249 | string pathToTextureSet = Path.Combine(exportDirectory, "textureset"); 250 | string[] textureSetNames = { "-normal.png", "-kanji.png", "-downer.png", "-upper.png" }; 251 | for (int i = 0; i < 4; i++) 252 | { 253 | Log.Information($"Exporting texture set {i + 1}/4..."); 254 | var textureSetTix = dream.TextureSets[i].Document; 255 | string exportPath = pathToTextureSet + textureSetNames[i]; 256 | ExportImages(textureSetTix, exportPath, separate: false, ImageFormat.Png); 257 | } 258 | 259 | Log.Information("Dream export complete"); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Controllers/ConfigController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using LSDView.Controllers.Interface; 3 | using LSDView.Models; 4 | using Newtonsoft.Json; 5 | using Serilog; 6 | 7 | namespace LSDView.Controllers 8 | { 9 | public class ConfigController : IConfigController 10 | { 11 | public LSDViewConfig Config { get; protected set; } 12 | 13 | protected const string CONFIG_FILE = "LSDViewConfig.json"; 14 | protected const int MAX_RECENT_FILES = 10; 15 | 16 | public ConfigController() 17 | { 18 | ensureConfigExists(); 19 | deserializeConfig(); 20 | } 21 | 22 | public void Save() { serializeConfig(); } 23 | 24 | public virtual void AddRecentFile(string recentFile) 25 | { 26 | if (Config.RecentFiles.Count + 1 > MAX_RECENT_FILES) 27 | { 28 | Config.RecentFiles.RemoveAt(MAX_RECENT_FILES - 1); 29 | } 30 | 31 | Config.RecentFiles.Insert(0, recentFile); 32 | Save(); 33 | } 34 | 35 | protected void deserializeConfig() 36 | { 37 | Log.Information("Deserializing LSDViewConfig.json"); 38 | Config = JsonConvert.DeserializeObject(File.ReadAllText(CONFIG_FILE)); 39 | } 40 | 41 | protected void serializeConfig() 42 | { 43 | Log.Information("Serializing LSDViewConfig.json"); 44 | File.WriteAllText(CONFIG_FILE, JsonConvert.SerializeObject(Config)); 45 | } 46 | 47 | protected void ensureConfigExists() 48 | { 49 | if (!File.Exists(CONFIG_FILE)) 50 | { 51 | Log.Information("LSDViewConfig.json not found - creating anew"); 52 | Config = new LSDViewConfig(); 53 | serializeConfig(); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Controllers/FileOpenController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Models; 5 | using Serilog; 6 | 7 | namespace LSDView.Controllers 8 | { 9 | public class FileOpenController : IFileOpenController 10 | { 11 | protected readonly IFileFormatController _lbdController; 12 | protected readonly IFileFormatController _tmdController; 13 | protected readonly IFileFormatController _momController; 14 | protected readonly IFileFormatController _timController; 15 | protected readonly IFileFormatController _tixController; 16 | protected readonly IConfigController _configController; 17 | 18 | public FileOpenController(IFileFormatController lbdController, 19 | IFileFormatController tmdController, 20 | IFileFormatController momController, 21 | IFileFormatController timController, 22 | IFileFormatController tixController, 23 | IConfigController configController) 24 | { 25 | _lbdController = lbdController; 26 | _tmdController = tmdController; 27 | _momController = momController; 28 | _timController = timController; 29 | _tixController = tixController; 30 | _configController = configController; 31 | } 32 | 33 | public void OpenFile(string filePath) 34 | { 35 | _configController.AddRecentFile(filePath); 36 | Log.Information($"Loading file from: {filePath}"); 37 | var ext = Path.GetExtension(filePath)?.ToLowerInvariant(); 38 | switch (ext) 39 | { 40 | case ".lbd": 41 | _lbdController.Load(filePath); 42 | break; 43 | case ".tmd": 44 | _tmdController.Load(filePath); 45 | break; 46 | case ".mom": 47 | _momController.Load(filePath); 48 | break; 49 | case ".tim": 50 | _timController.Load(filePath); 51 | break; 52 | case ".tix": 53 | _tixController.Load(filePath); 54 | break; 55 | default: 56 | Log.Error($"Unable to open file {filePath}, unsupported type."); 57 | break; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Controllers/GUI/AnimationController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using libLSD.Formats; 3 | using libLSD.Formats.Packets; 4 | using LSDView.Controllers.Interface; 5 | using LSDView.Math; 6 | using LSDView.Models; 7 | using OpenTK; 8 | 9 | namespace LSDView.Controllers.GUI 10 | { 11 | public class AnimationController : IAnimationController 12 | { 13 | private TOD _animation; 14 | 15 | public TOD Animation 16 | { 17 | get => _animation; 18 | set 19 | { 20 | _animation = value; 21 | CurrentFrame = -1; 22 | } 23 | } 24 | 25 | public MOMDocument Focus { get; private set; } 26 | public bool Active { get; set; } 27 | public int CurrentFrame { get; set; } 28 | 29 | public const double TICK = 1d / 60; 30 | 31 | protected double time = 0; 32 | 33 | protected readonly Dictionary _objectTable; 34 | 35 | protected class AnimationObjectTableEntry 36 | { 37 | public int TmdID; 38 | public readonly Transform Transform; 39 | 40 | public AnimationObjectTableEntry() 41 | { 42 | TmdID = -1; 43 | Transform = new Transform(); 44 | } 45 | } 46 | 47 | protected bool Ready => Animation.Header.ID != 0 && Focus != null; 48 | 49 | public AnimationController() 50 | { 51 | Active = false; 52 | Focus = null; 53 | CurrentFrame = -1; 54 | _objectTable = new Dictionary {[0] = new AnimationObjectTableEntry()}; 55 | } 56 | 57 | public void SetFocus(MOMDocument mom, int animationIdx = 0) 58 | { 59 | Active = true; 60 | Focus = mom; 61 | if (mom != null) Animation = mom.Document.MOS.TODs[animationIdx]; 62 | CurrentFrame = -1; 63 | } 64 | 65 | public void Update(double dt) 66 | { 67 | if (!Active || !Ready) return; 68 | 69 | double tickRate = Animation.Header.Resolution * TICK; 70 | 71 | time += dt; 72 | if (time > tickRate) 73 | { 74 | time = 0; 75 | 76 | CurrentFrame++; 77 | 78 | processFrame(CurrentFrame); 79 | } 80 | } 81 | 82 | protected void processFrame(int frameNumber) 83 | { 84 | if (frameNumber >= Animation.Header.NumberOfFrames - 1) 85 | { 86 | frameNumber = 0; 87 | CurrentFrame = 0; 88 | } 89 | 90 | if (Animation.Frames.Length <= 0) return; 91 | 92 | TODFrame frame = Animation.Frames[frameNumber]; 93 | 94 | if (frame.Packets == null) 95 | return; 96 | 97 | foreach (var packet in frame.Packets) 98 | { 99 | if (packet.Data is TODObjectControlPacketData) 100 | { 101 | handleObjectControlPacket(packet); 102 | } 103 | else if (packet.Data is TODObjectIDPacketData) 104 | { 105 | handleObjectIDPacket(packet); 106 | } 107 | else if (packet.Data is TODCoordinatePacketData) 108 | { 109 | handleCoordinatePacket(packet); 110 | } 111 | } 112 | } 113 | 114 | protected void handleObjectControlPacket(TODPacket packet) 115 | { 116 | TODObjectControlPacketData packetData = packet.Data as TODObjectControlPacketData; 117 | if (packetData.ObjectControl == TODObjectControlPacketData.ObjectControlType.Create) 118 | { 119 | _objectTable[packet.ObjectID] = new AnimationObjectTableEntry(); 120 | } 121 | else if (packetData.ObjectControl == TODObjectControlPacketData.ObjectControlType.Kill) 122 | { 123 | _objectTable.Remove(packet.ObjectID); 124 | } 125 | } 126 | 127 | protected void handleObjectIDPacket(TODPacket packet) 128 | { 129 | TODObjectIDPacketData packetData = packet.Data as TODObjectIDPacketData; 130 | if (packet.PacketType == TODPacket.PacketTypes.TMDDataID) 131 | { 132 | // create mapping in object table 133 | _objectTable[packet.ObjectID].TmdID = packetData.ObjectID; 134 | Focus.Models.ObjectMeshes[packetData.ObjectID - 1].Transform.Parent = 135 | _objectTable[packet.ObjectID].Transform; 136 | } 137 | else if (packet.PacketType == TODPacket.PacketTypes.ParentObjectID) 138 | { 139 | // set object parent 140 | Transform parentTransform = _objectTable[packetData.ObjectID].Transform; 141 | _objectTable[packet.ObjectID].Transform.Parent = parentTransform; 142 | } 143 | } 144 | 145 | protected void handleCoordinatePacket(TODPacket packet) 146 | { 147 | TODCoordinatePacketData packetData = packet.Data as TODCoordinatePacketData; 148 | 149 | Transform objTransform = _objectTable[packet.ObjectID].Transform; 150 | if (packetData.HasScale) 151 | { 152 | if (packetData.MatrixType == TODPacketData.PacketDataType.Absolute) 153 | { 154 | objTransform.Scale = new Vector3(packetData.ScaleX / 4096f, packetData.ScaleY / 4096f, 155 | packetData.ScaleZ / 4096f); 156 | } 157 | else 158 | { 159 | objTransform.Scale *= new Vector3(packetData.ScaleX / 4096f, packetData.ScaleY / 4096f, 160 | packetData.ScaleZ / 4096f); 161 | } 162 | } 163 | 164 | if (packetData.HasTranslation) 165 | { 166 | if (packetData.MatrixType == TODPacketData.PacketDataType.Absolute) 167 | { 168 | objTransform.Position = 169 | new Vector3(packetData.TransX, packetData.TransY, packetData.TransZ) / 2048f; 170 | } 171 | else 172 | { 173 | objTransform.Translate(new Vector3(packetData.TransX, packetData.TransY, packetData.TransZ) / 174 | 2048f); 175 | } 176 | } 177 | 178 | if (packetData.HasRotation) 179 | { 180 | float pitch = MathHelper.DegreesToRadians(packetData.RotX / 4096f); 181 | float yaw = MathHelper.DegreesToRadians(packetData.RotY / 4096f); 182 | float roll = MathHelper.DegreesToRadians(packetData.RotZ / 4096f); 183 | 184 | if (packetData.MatrixType == TODPacketData.PacketDataType.Absolute) 185 | { 186 | objTransform.Rotation = Quaternion.Identity; 187 | objTransform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitX, pitch); 188 | objTransform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitY, yaw); 189 | objTransform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, roll); 190 | } 191 | else 192 | { 193 | objTransform.Rotate(pitch, Vector3.UnitX, true); 194 | objTransform.Rotate(yaw, Vector3.UnitY, true); 195 | objTransform.Rotate(roll, Vector3.UnitZ, true); 196 | } 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Controllers/GUI/CameraController.cs: -------------------------------------------------------------------------------- 1 | using LSDView.Controllers.Interface; 2 | using LSDView.Graphics; 3 | using OpenTK; 4 | using OpenTK.Input; 5 | 6 | namespace LSDView.Controllers.GUI 7 | { 8 | public class CameraController : ICameraController 9 | { 10 | protected Camera _cam; 11 | protected MouseState _lastMouseState; 12 | protected float _lastScroll; 13 | protected bool _dragging; 14 | 15 | protected Vector3 _arcBallTarget = Vector3.Zero; 16 | protected float _arcBallDistance = 10f; 17 | 18 | protected const float MIN_ARCBALL_DISTANCE = 1f; 19 | protected const float MAX_ARCBALL_DISTANCE = 80f; 20 | protected const float PAN_SPEED = 0.02f; 21 | 22 | public void ProvideCamera(Camera cam) { _cam = cam; } 23 | 24 | public void Update() 25 | { 26 | if (!GuiApplication.Instance.Visible || !GuiApplication.Instance.Focused) return; 27 | 28 | var mouseState = Mouse.GetCursorState(); 29 | 30 | if (mouseState.IsButtonDown(MouseButton.Left)) arcBallRotation(mouseState); 31 | if (mouseState.IsButtonDown(MouseButton.Right)) panning(mouseState); 32 | scrollZooming(mouseState); 33 | 34 | _lastMouseState = mouseState; 35 | _lastScroll = mouseState.Scroll.Y; 36 | } 37 | 38 | public void RecenterView() 39 | { 40 | _arcBallTarget = Vector3.Zero; 41 | _cam.ArcBall(0, 0, _arcBallTarget, _arcBallDistance); 42 | } 43 | 44 | protected Vector2 dragDelta(MouseState state) 45 | { 46 | return new Vector2(state.X, state.Y) - new Vector2(_lastMouseState.X, _lastMouseState.Y); 47 | } 48 | 49 | protected float scrollDelta(MouseState state) { return state.Scroll.Y - _lastScroll; } 50 | 51 | protected void arcBallRotation(MouseState state) 52 | { 53 | var delta = dragDelta(state); 54 | _cam.ArcBall(MathHelper.DegreesToRadians(-delta.X), MathHelper.DegreesToRadians(delta.Y), 55 | _arcBallTarget, _arcBallDistance); 56 | } 57 | 58 | protected void panning(MouseState state) 59 | { 60 | var delta = dragDelta(state); 61 | var translationVec = _cam.Transform.Right * delta.X + _cam.Transform.Up * delta.Y; 62 | translationVec *= PAN_SPEED; 63 | _cam.Transform.Translate(translationVec); 64 | _arcBallTarget += translationVec; 65 | } 66 | 67 | protected void scrollZooming(MouseState state) 68 | { 69 | _arcBallDistance -= scrollDelta(state); 70 | _arcBallDistance = MathHelper.Clamp(_arcBallDistance, MIN_ARCBALL_DISTANCE, MAX_ARCBALL_DISTANCE); 71 | _cam.ArcBall(0, 0, _arcBallTarget, _arcBallDistance); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Controllers/GUI/GUIExportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LSDView.GUI.GUIComponents; 3 | 4 | namespace LSDView.Controllers.GUI 5 | { 6 | public class GUIExportController : AbstractExportController 7 | { 8 | protected FileDialog _fileExportDialog; 9 | 10 | public void ProvideFileExportDialog(FileDialog fileExportDialog) { _fileExportDialog = fileExportDialog; } 11 | 12 | public void OpenDialog(Action onSubmit, string fileSaveType) 13 | { 14 | GuiApplication.Instance.NextGuiRender += () => 15 | { 16 | _fileExportDialog.ShowDialog(onSubmit, fileSaveType: fileSaveType); 17 | }; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Controllers/GUI/GUILBDController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using libLSD.Formats; 4 | using LSDView.Controllers.Interface; 5 | using LSDView.Graphics; 6 | using LSDView.Models; 7 | using LSDView.Util; 8 | 9 | namespace LSDView.Controllers.GUI 10 | { 11 | public class GUILBDController : IFileFormatController 12 | { 13 | protected readonly ITreeController _treeController; 14 | protected readonly IVRAMController _vramController; 15 | protected readonly IFileFormatController _tmdController; 16 | protected readonly IFileFormatController _momController; 17 | protected readonly Shader _shader; 18 | 19 | public GUILBDController(ITreeController treeController, 20 | IVRAMController vramController, 21 | IFileFormatController tmdController, 22 | IFileFormatController momController) 23 | { 24 | _treeController = treeController; 25 | _vramController = vramController; 26 | _tmdController = tmdController; 27 | _momController = momController; 28 | _shader = new Shader("basic", "Shaders/basic"); 29 | } 30 | 31 | public LBD Load(string lbdPath) 32 | { 33 | var lbd = LibLSDUtil.LoadLBD(lbdPath); 34 | 35 | LBDDocument document = CreateDocument(lbd); 36 | _treeController.PopulateWithDocument(document, Path.GetFileName(lbdPath)); 37 | 38 | return lbd; 39 | } 40 | 41 | public LBDDocument CreateDocument(LBD lbd) 42 | { 43 | TMDDocument tileTmd = _tmdController.CreateDocument(lbd.Tiles); 44 | List tileLayout = new List(); 45 | 46 | int tileNo = 0; 47 | foreach (LBDTile tile in lbd.TileLayout) 48 | { 49 | int x = tileNo / lbd.Header.TileWidth; 50 | int y = tileNo % lbd.Header.TileWidth; 51 | 52 | if (tile.DrawTile) 53 | { 54 | tileLayout.AddRange(LibLSDUtil.CreateLBDTileMesh(tile, lbd.ExtraTiles, x, y, lbd.Tiles, _shader, 55 | _vramController.VRAM, headless: false)); 56 | } 57 | 58 | tileNo++; 59 | } 60 | 61 | List entities = null; 62 | if (lbd.Header.HasMML) 63 | { 64 | entities = new List(); 65 | foreach (MOM mom in lbd.MML?.MOMs) 66 | { 67 | entities.Add(_momController.CreateDocument(mom)); 68 | } 69 | } 70 | 71 | return new LBDDocument(lbd, tileTmd, tileLayout, entities); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Controllers/GUI/GUIMOMController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Models; 5 | using LSDView.Util; 6 | 7 | namespace LSDView.Controllers.GUI 8 | { 9 | public class GUIMOMController : IFileFormatController 10 | { 11 | protected readonly ITreeController _treeController; 12 | protected readonly IFileFormatController _tmdController; 13 | 14 | public GUIMOMController(ITreeController treeController, 15 | IFileFormatController tmdController) 16 | { 17 | _treeController = treeController; 18 | _tmdController = tmdController; 19 | } 20 | 21 | public MOM Load(string momPath) 22 | { 23 | var mom = LibLSDUtil.LoadMOM(momPath); 24 | 25 | MOMDocument document = CreateDocument(mom); 26 | _treeController.PopulateWithDocument(document, Path.GetFileName(momPath)); 27 | 28 | return mom; 29 | } 30 | 31 | public MOMDocument CreateDocument(MOM mom) 32 | { 33 | TMDDocument models = _tmdController.CreateDocument(mom.TMD); 34 | 35 | return new MOMDocument(mom, models); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Controllers/GUI/GUITIMController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Models; 6 | using LSDView.Util; 7 | 8 | namespace LSDView.Controllers.GUI 9 | { 10 | public class GUITIMController : IFileFormatController 11 | { 12 | protected readonly ITreeController _treeController; 13 | protected readonly Shader _shader; 14 | 15 | public GUITIMController(ITreeController treeController) 16 | { 17 | _treeController = treeController; 18 | _shader = new Shader("texture", "Shaders/texture"); 19 | } 20 | 21 | public TIM Load(string timPath) 22 | { 23 | var tim = LibLSDUtil.LoadTIM(timPath); 24 | 25 | TIMDocument document = CreateDocument(tim); 26 | _treeController.PopulateWithDocument(document, Path.GetFileName(timPath)); 27 | 28 | return tim; 29 | } 30 | 31 | public TIMDocument CreateDocument(TIM tim) 32 | { 33 | // 1 if not using CLUT, otherwise number of CLUTs 34 | int numMeshes = tim.ColorLookup?.NumberOfCLUTs ?? 1; 35 | Mesh[] timMeshes = new Mesh[numMeshes]; 36 | 37 | for (int i = 0; i < numMeshes; i++) 38 | { 39 | var img = LibLSDUtil.GetImageDataFromTIM(tim, clutIndex: i); 40 | Mesh textureMesh = Mesh.CreateQuad(_shader); 41 | ITexture2D tex = new Texture2D(img.width, img.height, img.data); 42 | textureMesh.Textures.Add(tex); 43 | timMeshes[i] = textureMesh; 44 | } 45 | 46 | return new TIMDocument(tim, timMeshes); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Controllers/GUI/GUITIXController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using libLSD.Formats; 4 | using LSDView.Controllers.Interface; 5 | using LSDView.Models; 6 | using LSDView.Util; 7 | 8 | namespace LSDView.Controllers.GUI 9 | { 10 | public class GUITIXController : IFileFormatController 11 | { 12 | protected readonly ITreeController _treeController; 13 | protected readonly IFileFormatController _timController; 14 | 15 | public GUITIXController(ITreeController treeController, IFileFormatController timController) 16 | { 17 | _treeController = treeController; 18 | _timController = timController; 19 | } 20 | 21 | public TIX Load(string tixPath) 22 | { 23 | var tix = LibLSDUtil.LoadTIX(tixPath); 24 | 25 | TIXDocument document = CreateDocument(tix); 26 | _treeController.PopulateWithDocument(document, Path.GetFileName(tixPath)); 27 | 28 | return tix; 29 | } 30 | 31 | public TIXDocument CreateDocument(TIX tix) 32 | { 33 | List tims = new List(); 34 | foreach (var tim in tix.AllTIMs) 35 | { 36 | tims.Add(_timController.CreateDocument(tim)); 37 | } 38 | 39 | return new TIXDocument(tix, tims); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Controllers/GUI/GUITMDController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using libLSD.Formats; 4 | using LSDView.Controllers.Interface; 5 | using LSDView.Graphics; 6 | using LSDView.Models; 7 | using LSDView.Util; 8 | 9 | namespace LSDView.Controllers.GUI 10 | { 11 | public class GUITMDController : IFileFormatController 12 | { 13 | protected readonly ITreeController _treeController; 14 | protected readonly IVRAMController _vramController; 15 | protected readonly Shader _shader; 16 | 17 | public GUITMDController(ITreeController treeController, IVRAMController vramController) 18 | { 19 | _treeController = treeController; 20 | _vramController = vramController; 21 | _shader = new Shader("basic", "Shaders/basic"); 22 | } 23 | 24 | public TMD Load(string tmdPath) 25 | { 26 | var tmd = LibLSDUtil.LoadTMD(tmdPath); 27 | 28 | TMDDocument document = CreateDocument(tmd); 29 | _treeController.PopulateWithDocument(document, Path.GetFileName(tmdPath)); 30 | 31 | return tmd; 32 | } 33 | 34 | public TMDDocument CreateDocument(TMD tmd) 35 | { 36 | List objectMeshes = 37 | LibLSDUtil.CreateMeshesFromTMD(tmd, _shader, _vramController.VRAM, headless: false); 38 | return new TMDDocument(tmd, objectMeshes); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Controllers/GUI/VRAMController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Util; 6 | using OpenTK.Graphics; 7 | using Serilog; 8 | 9 | namespace LSDView.Controller 10 | { 11 | public class VRAMController : IVRAMController 12 | { 13 | public ITexture2D VRAM { get => _vram; protected set => _vram = value; } 14 | public TIX Tix { get; protected set; } 15 | 16 | public bool VRAMLoaded { get; protected set; } 17 | 18 | private ITexture2D _vram; 19 | 20 | public VRAMController() 21 | { 22 | VRAM = Texture2D.Fill(Color4.White, LibLSDUtil.VRAM_WIDTH, LibLSDUtil.VRAM_HEIGHT); 23 | VRAMLoaded = false; 24 | } 25 | 26 | public void LoadTIXIntoVRAM(string tixPath) 27 | { 28 | Log.Information($"Loading TIX from {tixPath} into virtual VRAM"); 29 | 30 | using (BinaryReader br = new BinaryReader(File.Open(tixPath, FileMode.Open))) 31 | { 32 | Tix = new TIX(br); 33 | } 34 | 35 | Log.Information("Successfully loaded TIX"); 36 | 37 | LibLSDUtil.TIXToTexture2D(Tix, ref _vram, flip: true); 38 | 39 | VRAMLoaded = true; 40 | } 41 | 42 | public void ClearVRAM() { VRAM.Clear(); } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessConfigController.cs: -------------------------------------------------------------------------------- 1 | using LSDView.Controllers.GUI; 2 | 3 | namespace LSDView.Controllers.Headless 4 | { 5 | public class HeadlessConfigController : ConfigController 6 | { 7 | public override void AddRecentFile(string recentFile) 8 | { 9 | // do nothing 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessExportController.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers.Headless 2 | { 3 | public class HeadlessExportController : AbstractExportController 4 | { 5 | // intentionally empty 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessLBDController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Models; 6 | using LSDView.Util; 7 | 8 | namespace LSDView.Controllers.Headless 9 | { 10 | public class HeadlessLBDController : IFileFormatController 11 | { 12 | protected readonly IVRAMController _vramController; 13 | protected readonly IFileFormatController _tmdController; 14 | protected readonly IFileFormatController _momController; 15 | 16 | public HeadlessLBDController(IVRAMController vramController, 17 | IFileFormatController tmdController, 18 | IFileFormatController momController) 19 | { 20 | _vramController = vramController; 21 | _tmdController = tmdController; 22 | _momController = momController; 23 | } 24 | 25 | public LBD Load(string path) { return LibLSDUtil.LoadLBD(path); } 26 | 27 | public LBDDocument CreateDocument(LBD lbd) 28 | { 29 | TMDDocument tileTmd = _tmdController.CreateDocument(lbd.Tiles); 30 | List tileLayout = new List(); 31 | 32 | int tileNo = 0; 33 | foreach (LBDTile tile in lbd.TileLayout) 34 | { 35 | int x = tileNo / lbd.Header.TileWidth; 36 | int y = tileNo % lbd.Header.TileWidth; 37 | 38 | if (tile.DrawTile) 39 | { 40 | tileLayout.AddRange(LibLSDUtil.CreateLBDTileMesh(tile, lbd.ExtraTiles, x, y, lbd.Tiles, null, 41 | _vramController.VRAM, headless: true)); 42 | } 43 | 44 | tileNo++; 45 | } 46 | 47 | List entities = null; 48 | if (lbd.Header.HasMML) 49 | { 50 | entities = new List(); 51 | foreach (MOM mom in lbd.MML?.MOMs) 52 | { 53 | entities.Add(_momController.CreateDocument(mom)); 54 | } 55 | } 56 | 57 | return new LBDDocument(lbd, tileTmd, tileLayout, entities); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessMOMController.cs: -------------------------------------------------------------------------------- 1 | using libLSD.Formats; 2 | using LSDView.Controllers.Interface; 3 | using LSDView.Models; 4 | using LSDView.Util; 5 | 6 | namespace LSDView.Controllers.Headless 7 | { 8 | public class HeadlessMOMController : IFileFormatController 9 | { 10 | protected readonly IFileFormatController _tmdController; 11 | 12 | public HeadlessMOMController(IFileFormatController tmdController) 13 | { 14 | _tmdController = tmdController; 15 | } 16 | 17 | public MOM Load(string path) { return LibLSDUtil.LoadMOM(path); } 18 | 19 | public MOMDocument CreateDocument(MOM mom) 20 | { 21 | TMDDocument models = _tmdController.CreateDocument(mom.TMD); 22 | 23 | return new MOMDocument(mom, models); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessTIMController.cs: -------------------------------------------------------------------------------- 1 | using libLSD.Formats; 2 | using LSDView.Controllers.Interface; 3 | using LSDView.Graphics; 4 | using LSDView.Graphics.Headless; 5 | using LSDView.Models; 6 | using LSDView.Util; 7 | 8 | namespace LSDView.Controllers.Headless 9 | { 10 | public class HeadlessTIMController : IFileFormatController 11 | { 12 | public TIM Load(string path) { return LibLSDUtil.LoadTIM(path); } 13 | 14 | public TIMDocument CreateDocument(TIM tim) 15 | { 16 | // 1 if not using CLUT, otherwise number of CLUTs 17 | int numMeshes = tim.ColorLookup?.NumberOfCLUTs ?? 1; 18 | IRenderable[] timMeshes = new IRenderable[numMeshes]; 19 | 20 | for (int i = 0; i < numMeshes; i++) 21 | { 22 | var img = LibLSDUtil.GetImageDataFromTIM(tim, clutIndex: i); 23 | HeadlessMesh textureMesh = HeadlessMesh.CreateQuad(); 24 | ITexture2D tex = new HeadlessTexture2D(img.width, img.height, img.data); 25 | textureMesh.Textures.Add(tex); 26 | timMeshes[i] = textureMesh; 27 | } 28 | 29 | return new TIMDocument(tim, timMeshes); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessTIXController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Models; 5 | using LSDView.Util; 6 | 7 | namespace LSDView.Controllers.Headless 8 | { 9 | public class HeadlessTIXController : IFileFormatController 10 | { 11 | protected readonly IFileFormatController _timController; 12 | 13 | public HeadlessTIXController(IFileFormatController timController) 14 | { 15 | _timController = timController; 16 | } 17 | 18 | public TIX Load(string path) { return LibLSDUtil.LoadTIX(path); } 19 | 20 | public TIXDocument CreateDocument(TIX tix) 21 | { 22 | List tims = new List(); 23 | foreach (var tim in tix.AllTIMs) 24 | { 25 | tims.Add(_timController.CreateDocument(tim)); 26 | } 27 | 28 | return new TIXDocument(tix, tims); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessTMDController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Models; 6 | using LSDView.Util; 7 | 8 | namespace LSDView.Controllers.Headless 9 | { 10 | public class HeadlessTMDController : IFileFormatController 11 | { 12 | public TMD Load(string path) { return LibLSDUtil.LoadTMD(path); } 13 | 14 | public TMDDocument CreateDocument(TMD tmd) 15 | { 16 | List objectMeshes = 17 | LibLSDUtil.CreateMeshesFromTMD(tmd, null, null, headless: true); 18 | return new TMDDocument(tmd, objectMeshes); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Controllers/Headless/HeadlessVRAMController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using libLSD.Formats; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Util; 6 | using Serilog; 7 | 8 | namespace LSDView.Controllers.Headless 9 | { 10 | public class HeadlessVRAMController : IVRAMController 11 | { 12 | public ITexture2D VRAM { get; protected set; } 13 | public TIX Tix { get; protected set; } 14 | public bool VRAMLoaded { get; protected set; } 15 | 16 | public void LoadTIXIntoVRAM(string tixPath) 17 | { 18 | Log.Information($"Loading TIX from {tixPath} into virtual VRAM"); 19 | 20 | using (BinaryReader br = new BinaryReader(File.Open(tixPath, FileMode.Open))) 21 | { 22 | Tix = new TIX(br); 23 | } 24 | 25 | Log.Information("Successfully loaded TIX"); 26 | 27 | if (VRAM != null) 28 | { 29 | disposeOldVRAM(); 30 | } 31 | 32 | VRAM = LibLSDUtil.TIXToTexture2D(Tix, headless: true, flip: false); 33 | 34 | VRAMLoaded = true; 35 | } 36 | 37 | public void ClearVRAM() { VRAM.Clear(); } 38 | 39 | protected void disposeOldVRAM() 40 | { 41 | VRAM.Dispose(); 42 | VRAM = null; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Controllers/Interface/IAnimationController.cs: -------------------------------------------------------------------------------- 1 | using libLSD.Formats; 2 | using LSDView.Models; 3 | 4 | namespace LSDView.Controllers.Interface 5 | { 6 | public interface IAnimationController 7 | { 8 | TOD Animation { get; } 9 | MOMDocument Focus { get; } 10 | bool Active { get; } 11 | int CurrentFrame { get; } 12 | 13 | void SetFocus(MOMDocument mom, int animationIdx = 0); 14 | void Update(double dt); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Controllers/Interface/ICameraController.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers.Interface 2 | { 3 | public interface ICameraController 4 | { 5 | void Update(); 6 | void RecenterView(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Controllers/Interface/IConfigController.cs: -------------------------------------------------------------------------------- 1 | using LSDView.Models; 2 | 3 | namespace LSDView.Controllers.Interface 4 | { 5 | public interface IConfigController 6 | { 7 | LSDViewConfig Config { get; } 8 | 9 | void Save(); 10 | void AddRecentFile(string recentFile); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Controllers/Interface/IExportController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing.Imaging; 3 | using libLSD.Formats; 4 | using libLSD.Interfaces; 5 | using LSDView.Graphics; 6 | 7 | namespace LSDView.Controllers.Interface 8 | { 9 | public interface IExportController 10 | { 11 | void ExportOriginal(IWriteable original, string filePath); 12 | void ExportImage(TIM tim, int clutIndex, string filePath, ImageFormat format); 13 | void ExportImages(TIX tix, string filePath, bool separate, ImageFormat format); 14 | void ExportTexture(ITexture2D tex, string filePath, ImageFormat format); 15 | void ExportMesh(IRenderable mesh, string filePath, MeshExportFormat format); 16 | void ExportMeshes(IEnumerable meshes, string filePath, MeshExportFormat format); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Controllers/Interface/IFileFormatController.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers.Interface 2 | { 3 | public interface IFileFormatController 4 | { 5 | TFile Load(string path); 6 | TDocument CreateDocument(TFile file); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Controllers/Interface/IFileOpenController.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers.Interface 2 | { 3 | public interface IFileOpenController 4 | { 5 | void OpenFile(string filePath); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Controllers/Interface/ITreeController.cs: -------------------------------------------------------------------------------- 1 | using LSDView.GUI.Components; 2 | using LSDView.Models; 3 | using OpenTK; 4 | 5 | namespace LSDView.Controllers.Interface 6 | { 7 | public interface ITreeController 8 | { 9 | TreeView Tree { get; } 10 | 11 | void PopulateWithDocument(IDocument doc, string rootName); 12 | void RenderSelectedNode(Matrix4 view, Matrix4 projection); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Controllers/Interface/IUpdateCheckerController.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers.Interface 2 | { 3 | public interface IUpdateCheckerController 4 | { 5 | bool IsUpdateAvailable(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Controllers/Interface/IVRAMController.cs: -------------------------------------------------------------------------------- 1 | using libLSD.Formats; 2 | using LSDView.Graphics; 3 | 4 | namespace LSDView.Controllers.Interface 5 | { 6 | public interface IVRAMController 7 | { 8 | ITexture2D VRAM { get; } 9 | TIX Tix { get; } 10 | bool VRAMLoaded { get; } 11 | 12 | void LoadTIXIntoVRAM(string tixPath); 13 | void ClearVRAM(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Controllers/Interface/MeshExportFormat.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Controllers 2 | { 3 | public enum MeshExportFormat 4 | { 5 | OBJ, 6 | PLY 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Controllers/UpdateCheckerController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using LSDView.Controllers.Interface; 3 | using Newtonsoft.Json.Linq; 4 | using Serilog; 5 | 6 | namespace LSDView.Controllers 7 | { 8 | public class UpdateCheckerController : IUpdateCheckerController 9 | { 10 | protected const string UPDATE_API_URL = "https://api.github.com/repos/figglewatts/lsdview/releases/latest"; 11 | 12 | public bool IsUpdateAvailable() 13 | { 14 | // if we're developing locally, always return false 15 | if (Version.String.Equals("#{VERSION}#")) return false; 16 | 17 | using (WebClient wc = new WebClient()) 18 | { 19 | wc.Headers.Add("User-Agent", "LSDView"); 20 | try 21 | { 22 | var json = wc.DownloadString(UPDATE_API_URL); 23 | dynamic releaseInfo = JObject.Parse(json); 24 | string releaseName = releaseInfo.name; 25 | return !Version.String.Equals(releaseName); 26 | } 27 | catch (WebException exception) 28 | { 29 | Log.Warning($"Unable to check for update: {exception}"); 30 | return false; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Fonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/Fonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /Fonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/Fonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /GUI/Components/AnimatedMeshListTreeNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LSDView.Controllers.Interface; 4 | using LSDView.Graphics; 5 | using LSDView.Models; 6 | 7 | namespace LSDView.GUI.Components 8 | { 9 | public class AnimatedMeshListTreeNode : MeshListTreeNode 10 | { 11 | protected int _animation { get; } 12 | protected MOMDocument _entity { get; } 13 | protected readonly IAnimationController _animationController; 14 | 15 | public AnimatedMeshListTreeNode(string text, 16 | List meshes, 17 | MOMDocument entity, 18 | int animation, 19 | IAnimationController animationController, 20 | IEnumerable children = null, 21 | Action onSelect = null, 22 | ContextMenu contextMenu = null) : base(text, meshes, children, onSelect, contextMenu) 23 | { 24 | _animation = animation; 25 | _animationController = animationController; 26 | _entity = entity; 27 | } 28 | 29 | protected override void internalOnSelect() { _animationController.SetFocus(_entity, _animation); } 30 | 31 | public override void OnDeselect() { _animationController.SetFocus(null); } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GUI/Components/ApplicationArea.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using ImGuiNET; 3 | 4 | namespace LSDView.GUI.Components 5 | { 6 | public class ApplicationArea : ImGuiComponent 7 | { 8 | private const int TITLEBAR_HEIGHT = 19; 9 | 10 | private readonly ImGuiIOPtr _io; 11 | 12 | public ApplicationArea() { _io = ImGui.GetIO(); } 13 | 14 | protected override void renderSelf() 15 | { 16 | ImGui.SetNextWindowPos(new Vector2(0, TITLEBAR_HEIGHT), ImGuiCond.Always, Vector2.Zero); 17 | ImGui.SetNextWindowSize(new Vector2(_io.DisplaySize.X, _io.DisplaySize.Y - TITLEBAR_HEIGHT), 18 | ImGuiCond.Always); 19 | ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0); 20 | if (ImGui.Begin("", 21 | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | 22 | ImGuiWindowFlags.NoSavedSettings | 23 | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus)) 24 | { 25 | renderChildren(); 26 | 27 | ImGui.End(); 28 | } 29 | 30 | ImGui.PopStyleVar(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GUI/Components/Columns.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiNET; 4 | 5 | namespace LSDView.GUI.Components 6 | { 7 | public class Columns : ImGuiComponent 8 | { 9 | public int Count { get; private set; } 10 | protected readonly List _content; 11 | protected readonly float[] _widths; 12 | 13 | private readonly bool[] _setWidths; 14 | 15 | public Columns(int count, List content, float[] widths = null) 16 | { 17 | Count = count; 18 | 19 | if (Count != content.Count) 20 | { 21 | throw new ArgumentException("Column count needs to be equal to content count!"); 22 | } 23 | 24 | if (widths == null) widths = new float[Count]; 25 | _widths = widths; 26 | _setWidths = new bool[Count]; 27 | 28 | _content = content; 29 | } 30 | 31 | protected override void renderSelf() 32 | { 33 | ImGui.Columns(Count, GetHashCode().ToString(), false); 34 | for (int i = 0; i < Count; i++) 35 | { 36 | // if we haven't already set the width and the width is valid, then set it 37 | if (!_setWidths[i] && _widths[i] > 0) 38 | { 39 | ImGui.SetColumnWidth(i, _widths[i]); 40 | _setWidths[i] = true; 41 | } 42 | 43 | _content[i].Render(); 44 | ImGui.NextColumn(); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GUI/Components/ContextMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiNET; 4 | 5 | namespace LSDView.GUI.Components 6 | { 7 | public class ContextMenu 8 | { 9 | public readonly Dictionary MenuItems; 10 | 11 | public ContextMenu(Dictionary menuItems) { MenuItems = menuItems; } 12 | 13 | public void Render() 14 | { 15 | if (ImGui.BeginPopupContextItem()) 16 | { 17 | foreach (var item in MenuItems) 18 | { 19 | if (ImGui.Selectable(item.Key)) item.Value(); 20 | } 21 | 22 | ImGui.EndPopup(); 23 | } 24 | } 25 | 26 | public bool Equals(ContextMenu other) { return Equals(MenuItems, other.MenuItems); } 27 | 28 | public override bool Equals(object obj) { return obj is ContextMenu other && Equals(other); } 29 | 30 | public override int GetHashCode() { return (MenuItems != null ? MenuItems.GetHashCode() : 0); } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GUI/Components/FileDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Numerics; 6 | using IconFonts; 7 | using ImGuiNET; 8 | 9 | namespace LSDView.GUI.GUIComponents 10 | { 11 | public class FileDialog : ImGuiComponent 12 | { 13 | public enum DialogType 14 | { 15 | Open, 16 | Save 17 | } 18 | 19 | public DialogType Type { get; } 20 | public string FilePath { get; private set; } 21 | 22 | public string FileSearchPattern 23 | { 24 | set 25 | { 26 | setFileSearchPattern(value); 27 | invalidateFileList(); 28 | } 29 | } 30 | 31 | public string InitialDir { get => _initialDir; set => _initialDir = value; } 32 | 33 | private string _fileSaveType = ""; 34 | private string[] _fileSearchPattern = new string[0]; 35 | private bool _open = true; 36 | private bool _lastOpen = false; 37 | private string _currentDir; 38 | private int _selectedFile = -1; 39 | private string _bottomBarText = ""; 40 | private readonly Vector2 _dialogStartSize = new Vector2(400, 300); 41 | private readonly List _directoriesInCurrentDir; 42 | private readonly List _filesInCurrentDir; 43 | private string _initialDir; 44 | private event Action OnDialogAccept; 45 | 46 | public FileDialog(string dir, DialogType type) 47 | : base() 48 | { 49 | FilePath = dir; 50 | _initialDir = dir; 51 | Type = type; 52 | _currentDir = _initialDir; 53 | _directoriesInCurrentDir = new List(); 54 | _filesInCurrentDir = new List(); 55 | updateFilesInCurrentDir(); 56 | } 57 | 58 | public void ShowDialog(Action onDialogAccept, 59 | string fileSearchPattern = "", 60 | string fileSaveType = "") 61 | { 62 | OnDialogAccept = onDialogAccept; 63 | setFileSearchPattern(fileSearchPattern); 64 | _fileSaveType = fileSaveType; 65 | FilePath = _initialDir; 66 | _currentDir = _initialDir; 67 | invalidateFileList(); 68 | ImGui.OpenPopup(Type == DialogType.Open 69 | ? $"Open file...##{GetHashCode()}" 70 | : $"Save file...##{GetHashCode()}"); 71 | _open = true; 72 | } 73 | 74 | public bool CurrentDirectoryExists() { return Directory.Exists(_currentDir); } 75 | 76 | protected override void renderSelf() 77 | { 78 | _lastOpen = _open; 79 | 80 | if (Type == DialogType.Open) 81 | { 82 | renderFileOpenDialog(); 83 | } 84 | else 85 | { 86 | renderFileSaveDialog(); 87 | } 88 | 89 | if (_open == false && _lastOpen == true) 90 | { 91 | resetDefaults(); 92 | } 93 | } 94 | 95 | private void setFileSearchPattern(string searchPattern) 96 | { 97 | if (string.IsNullOrEmpty(searchPattern)) return; 98 | _fileSearchPattern = searchPattern.Split('|'); 99 | } 100 | 101 | private void resetDefaults() 102 | { 103 | OnDialogAccept = null; 104 | _fileSearchPattern = new[] {""}; 105 | _fileSaveType = ""; 106 | FilePath = _initialDir; 107 | _currentDir = _initialDir; 108 | _directoriesInCurrentDir.Clear(); 109 | _filesInCurrentDir.Clear(); 110 | } 111 | 112 | private void renderFileOpenDialog() 113 | { 114 | ImGui.SetNextWindowSize(_dialogStartSize, ImGuiCond.FirstUseEver); 115 | if (ImGui.BeginPopupModal($"Open file...##{GetHashCode()}", ref _open)) 116 | { 117 | renderTopBar(); 118 | renderFileList(); 119 | renderBottomBar(); 120 | 121 | ImGui.EndPopup(); 122 | } 123 | } 124 | 125 | private void renderFileSaveDialog() 126 | { 127 | ImGui.SetNextWindowSize(_dialogStartSize, ImGuiCond.FirstUseEver); 128 | if (ImGui.BeginPopupModal($"Save file...##{GetHashCode()}", ref _open)) 129 | { 130 | renderTopBar(); 131 | renderFileList(); 132 | renderBottomBar(); 133 | 134 | ImGui.EndPopup(); 135 | } 136 | } 137 | 138 | private void invalidateFileList() 139 | { 140 | _selectedFile = -1; 141 | updateDirectoriesInCurrentDir(); 142 | updateFilesInCurrentDir(); 143 | } 144 | 145 | private void goToParentDir() 146 | { 147 | DirectoryInfo parentDir = Directory.GetParent(_currentDir); 148 | _currentDir = parentDir?.ToString() ?? _currentDir; 149 | invalidateFileList(); 150 | } 151 | 152 | private void updateDirectoriesInCurrentDir() 153 | { 154 | _directoriesInCurrentDir.Clear(); 155 | 156 | if (!Directory.Exists(_currentDir)) return; 157 | string[] dirs = Directory.GetDirectories(_currentDir, "*", SearchOption.TopDirectoryOnly); 158 | 159 | foreach (string dir in dirs) 160 | { 161 | _directoriesInCurrentDir.Add(Path.GetFileName(dir.TrimEnd(Path.DirectorySeparatorChar))); 162 | } 163 | } 164 | 165 | private void updateFilesInCurrentDir() 166 | { 167 | _filesInCurrentDir.Clear(); 168 | 169 | if (!Directory.Exists(_currentDir)) return; 170 | var files = Directory.EnumerateFiles(_currentDir, "*.*").Where(checkFileAgainstSearchPattern); 171 | foreach (string file in files) 172 | { 173 | _filesInCurrentDir.Add(Path.GetFileName(file)); 174 | } 175 | } 176 | 177 | private bool checkFileAgainstSearchPattern(string file) 178 | { 179 | foreach (var pattern in _fileSearchPattern) 180 | { 181 | if (file.EndsWith(pattern, StringComparison.OrdinalIgnoreCase)) return true; 182 | } 183 | 184 | return false; 185 | } 186 | 187 | private bool indexInFilesListIsDirectory(int i) { return i < _directoriesInCurrentDir.Count; } 188 | 189 | private void renderTopBar() 190 | { 191 | Vector2 pos = ImGui.GetCursorScreenPos(); 192 | ImGui.SetCursorScreenPos(new Vector2(pos.X, pos.Y + 5)); 193 | ImGui.Text(FontAwesome5.FolderOpen); 194 | ImGui.SameLine(); 195 | Vector2 posAfter = ImGui.GetCursorScreenPos(); 196 | ImGui.SetCursorScreenPos(new Vector2(posAfter.X, pos.Y)); 197 | ImGui.PushItemWidth(-40); 198 | bool modified = false; 199 | modified = ImGui.InputText("##current-dir", ref _currentDir, 2048); 200 | 201 | if (CurrentDirectoryExists()) 202 | { 203 | ImGui.SameLine(); 204 | if (ImGui.Button("Up", new Vector2(30, 0))) 205 | { 206 | goToParentDir(); 207 | } 208 | } 209 | 210 | if (modified) 211 | { 212 | invalidateFileList(); 213 | } 214 | } 215 | 216 | private void renderFileList() 217 | { 218 | // if the dialog is in save mode we can't select files 219 | if (Type == DialogType.Save) _selectedFile = -1; 220 | 221 | ImGui.BeginChild("fileSelect", new Vector2(-1, -24), true, ImGuiWindowFlags.None); 222 | if (!CurrentDirectoryExists()) 223 | { 224 | ImGui.Text("Directory does not exist!"); 225 | } 226 | else if (_filesInCurrentDir.Count <= 0 && _directoriesInCurrentDir.Count <= 0) 227 | { 228 | ImGui.Text("Directory is empty!"); 229 | } 230 | else 231 | { 232 | int i = 0; 233 | foreach (string dir in _directoriesInCurrentDir) 234 | { 235 | ImGui.PushID(i); 236 | if (ImGui.Selectable($"{FontAwesome5.Folder} {dir}", _selectedFile == i, 237 | ImGuiSelectableFlags.AllowDoubleClick)) 238 | { 239 | _selectedFile = i; 240 | 241 | if (Type == DialogType.Open) _bottomBarText = dir; 242 | 243 | if (ImGui.IsMouseDoubleClicked(0)) 244 | { 245 | _currentDir = Path.Combine(_currentDir, dir); 246 | invalidateFileList(); 247 | ImGui.PopID(); 248 | break; 249 | } 250 | } 251 | 252 | ImGui.PopID(); 253 | i++; 254 | } 255 | 256 | foreach (string file in _filesInCurrentDir) 257 | { 258 | ImGui.PushID(i); 259 | if (ImGui.Selectable($"{FontAwesome5.File} {file}", _selectedFile == i, 260 | ImGuiSelectableFlags.AllowDoubleClick)) 261 | { 262 | _selectedFile = i; 263 | 264 | _bottomBarText = Path.GetFileNameWithoutExtension(file); 265 | 266 | FilePath = Path.Combine(_currentDir, file); 267 | 268 | if (ImGui.IsMouseDoubleClicked(0)) 269 | { 270 | dialogAccept(); 271 | ImGui.PopID(); 272 | break; 273 | } 274 | } 275 | 276 | ImGui.PopID(); 277 | i++; 278 | } 279 | } 280 | 281 | ImGui.EndChild(); 282 | } 283 | 284 | private void renderBottomBar() 285 | { 286 | if (!CurrentDirectoryExists()) return; 287 | 288 | if (Type == DialogType.Save) 289 | { 290 | ImGui.PushItemWidth(-48 - (_fileSaveType.Length * 7) - 16); 291 | } 292 | else 293 | { 294 | ImGui.PushItemWidth(-48 - 9); 295 | } 296 | 297 | ImGui.InputText("##bottombar", ref _bottomBarText, 2048); 298 | //ImGuiNETExtensions.InputText("##bottombar", ref _bottomBarText); 299 | 300 | if (Type == DialogType.Save) 301 | { 302 | ImGui.SameLine(); 303 | ImGui.Text(_fileSaveType); 304 | } 305 | 306 | ImGui.SameLine(); 307 | if (Type == DialogType.Open) 308 | { 309 | if (ImGui.Button("Open", new Vector2(48, 0))) 310 | { 311 | if (_selectedFile >= _directoriesInCurrentDir.Count) 312 | { 313 | // selected file was a file 314 | dialogAccept(); 315 | } 316 | else 317 | { 318 | // it was a directory 319 | _currentDir = Path.Combine(_currentDir, _bottomBarText); 320 | invalidateFileList(); 321 | } 322 | } 323 | } 324 | else 325 | { 326 | if (ImGui.Button("Save", new Vector2(48, 0))) 327 | { 328 | FilePath = Path.Combine(_currentDir, _bottomBarText + _fileSaveType); 329 | dialogAccept(); 330 | } 331 | } 332 | } 333 | 334 | private void dialogAccept() 335 | { 336 | OnDialogAccept?.Invoke(FilePath); 337 | _open = false; 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /GUI/Components/FramebufferArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using ImGuiNET; 4 | using LSDView.Graphics; 5 | 6 | namespace LSDView.GUI.Components 7 | { 8 | public class FramebufferArea : ImGuiComponent 9 | { 10 | private Vector2 _lastDimension; 11 | private readonly Framebuffer _framebuffer; 12 | 13 | public FramebufferArea(Framebuffer frameBuffer) { _framebuffer = frameBuffer; } 14 | 15 | protected override void renderSelf() 16 | { 17 | var region = ImGui.GetContentRegionAvail(); 18 | if (_lastDimension != region) 19 | { 20 | _framebuffer.Resize((int)region.X, (int)region.Y); 21 | _lastDimension = region; 22 | } 23 | 24 | ImGui.Image((IntPtr)_framebuffer.Texture.Handle, region, new Vector2(0, 1), new Vector2(1, 0)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GUI/Components/GenericDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSDView.GUI.Components 4 | { 5 | public class GenericDialog : ImGuiComponent 6 | { 7 | private readonly Action _dialogContents; 8 | 9 | public GenericDialog(Action dialogContents) { _dialogContents = dialogContents; } 10 | 11 | protected override void renderSelf() { _dialogContents(); } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GUI/Components/InfoDialog.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace LSDView.GUI.GUIComponents 4 | { 5 | public class InfoDialog : ImGuiComponent 6 | { 7 | public enum DialogType 8 | { 9 | Info, 10 | Warning, 11 | Error 12 | } 13 | 14 | public DialogType Type { get; } 15 | public string Message { get; } 16 | 17 | // TODO: configurable buttons... 18 | 19 | public InfoDialog(DialogType type, string message) 20 | : base() 21 | { 22 | Type = type; 23 | Message = message; 24 | } 25 | 26 | protected override void renderSelf() { ImGui.Text(Message); } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GUI/Components/LBDTileTreeNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiNET; 4 | using libLSD.Formats; 5 | using LSDView.Graphics; 6 | using OpenTK; 7 | 8 | namespace LSDView.GUI.Components 9 | { 10 | public class LBDTileTreeNode : MeshListTreeNode 11 | { 12 | protected readonly IRenderable _tileMesh; 13 | 14 | public LBDTileTreeNode(string text, 15 | IRenderable tileMesh, 16 | LBDTile tile, 17 | List meshes) : base(text, meshes) 18 | { 19 | _tileMesh = tileMesh; 20 | 21 | var footstepSoundAndCollision = Convert.ToString(tile.FootstepSoundAndCollision, 2); 22 | var unknown = tile.UnknownFlag == 1 ? "true" : "false"; 23 | _tooltip = () => 24 | { 25 | ImGui.Text($"{footstepSoundAndCollision} - footstep/collision"); 26 | ImGui.Text($"{unknown} - unknown"); 27 | ImGui.Text($"{tile.UnknownFlag}"); 28 | }; 29 | } 30 | 31 | protected override void internalOnSelect() 32 | { 33 | _tileMesh.Material.SetParameter("ColorTint", new Vector4(1.5f, 1.5f, 1.5f, 1f), Vector4.One); 34 | } 35 | 36 | public override void OnDeselect() { _tileMesh.Material.SetParameter("ColorTint", Vector4.One, Vector4.One); } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GUI/Components/MainMenuBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing.Imaging; 3 | using ImGuiNET; 4 | using LSDView.Controllers.GUI; 5 | using LSDView.Controllers.Interface; 6 | using LSDView.GUI.Components; 7 | using LSDView.Util; 8 | using OpenTK; 9 | using Serilog; 10 | 11 | namespace LSDView.GUI.GUIComponents 12 | { 13 | public class MainMenuBar : ImGuiComponent 14 | { 15 | private readonly FileDialog _openDialog; 16 | private readonly FileDialog _openVramDialog; 17 | 18 | private bool _openFileOpenDialog = false; 19 | private bool _openVramOpenDialog = false; 20 | 21 | private readonly IFileOpenController _fileOpenController; 22 | private readonly IVRAMController _vramController; 23 | private readonly IConfigController _configController; 24 | private readonly ICameraController _cameraController; 25 | private readonly GUIExportController _exportController; 26 | 27 | private string _gameDataPathFieldValue; 28 | 29 | public MainMenuBar(IFileOpenController fileOpenController, 30 | IVRAMController vramController, 31 | IConfigController configController, 32 | ICameraController cameraController, 33 | GUIExportController exportController) 34 | { 35 | _configController = configController; 36 | _gameDataPathFieldValue = _configController.Config.GameDataPath; 37 | _openDialog = new FileDialog(_configController.Config.GameDataPath, FileDialog.DialogType.Open); 38 | _openVramDialog = new FileDialog(_configController.Config.GameDataPath, FileDialog.DialogType.Open); 39 | _fileOpenController = fileOpenController; 40 | _vramController = vramController; 41 | _cameraController = cameraController; 42 | _exportController = exportController; 43 | 44 | _configController.Config.OnGameDataPathChange += () => 45 | _openDialog.InitialDir = _configController.Config.GameDataPath; 46 | _configController.Config.OnGameDataPathChange += () => 47 | _openVramDialog.InitialDir = _configController.Config.GameDataPath; 48 | } 49 | 50 | public void OpenSetGameDataPath() 51 | { 52 | createModal("Set game data path...", new GenericDialog(setGameDataPathDialog), 53 | new Vector2(500, 85)); 54 | } 55 | 56 | protected override void renderSelf() 57 | { 58 | if (ImGui.BeginMainMenuBar()) 59 | { 60 | if (ImGui.BeginMenu("File")) 61 | { 62 | renderFileMenu(); 63 | ImGui.EndMenu(); 64 | } 65 | 66 | if (ImGui.BeginMenu("VRAM")) 67 | { 68 | renderVramMenu(); 69 | ImGui.EndMenu(); 70 | } 71 | 72 | if (ImGui.BeginMenu("Help")) 73 | { 74 | renderHelpMenu(); 75 | ImGui.EndMenu(); 76 | } 77 | } 78 | 79 | ImGui.EndMainMenuBar(); 80 | 81 | if (_openFileOpenDialog) 82 | { 83 | _openDialog.ShowDialog(path => _fileOpenController.OpenFile(path), ".lbd|.tix|.mom|.tmd|.tim"); 84 | _openFileOpenDialog = false; 85 | } 86 | 87 | if (_openVramOpenDialog) 88 | { 89 | _openVramDialog.ShowDialog(path => _vramController.LoadTIXIntoVRAM(path), ".tix"); 90 | _openVramOpenDialog = false; 91 | } 92 | 93 | _openDialog.Render(); 94 | _openVramDialog.Render(); 95 | } 96 | 97 | private void renderFileMenu() 98 | { 99 | if (ImGui.MenuItem("Open")) 100 | { 101 | _openFileOpenDialog = true; 102 | } 103 | 104 | if (ImGui.BeginMenu("Open Recent")) 105 | { 106 | if (_configController.Config.RecentFiles.Count > 0) 107 | { 108 | string fileToOpen = null; 109 | foreach (var recentFile in _configController.Config.RecentFiles) 110 | { 111 | try 112 | { 113 | var relPath = PathUtil.MakeRelative(recentFile, 114 | _configController.Config.GameDataPath); 115 | if (ImGui.MenuItem(relPath)) 116 | { 117 | fileToOpen = recentFile; 118 | break; 119 | } 120 | } 121 | catch (UriFormatException e) 122 | { 123 | Log.Warning($"Invalid recent file: '{recentFile}', invalid URI: {e}"); 124 | } 125 | } 126 | 127 | if (fileToOpen != null) _fileOpenController.OpenFile(fileToOpen); 128 | } 129 | else 130 | { 131 | ImGui.MenuItem("No recent files!"); 132 | } 133 | 134 | ImGui.EndMenu(); 135 | } 136 | 137 | ImGui.Separator(); 138 | 139 | if (ImGui.MenuItem("Set game data path...")) 140 | { 141 | OpenSetGameDataPath(); 142 | } 143 | } 144 | 145 | private void setGameDataPathDialog() 146 | { 147 | ImGui.PushItemWidth(-1); 148 | ImGui.InputText("##gamedata", ref _gameDataPathFieldValue, 1024); 149 | ImGui.PopItemWidth(); 150 | ImGui.Spacing(); 151 | ImGui.SameLine(ImGui.GetWindowWidth() - 30); 152 | if (ImGui.Button("Ok")) 153 | { 154 | _configController.Config.GameDataPath = _gameDataPathFieldValue; 155 | _configController.Save(); 156 | destroyModal("Set game data path..."); 157 | } 158 | } 159 | 160 | private void renderVramMenu() 161 | { 162 | if (ImGui.MenuItem("Load VRAM")) 163 | { 164 | _openVramOpenDialog = true; 165 | } 166 | 167 | if (!_vramController.VRAMLoaded) return; 168 | 169 | ImGui.Separator(); 170 | if (ImGui.MenuItem("Export VRAM...")) 171 | { 172 | _exportController.OpenDialog( 173 | filePath => 174 | { 175 | _exportController.ExportImages(_vramController.Tix, filePath, separate: false, ImageFormat.Png); 176 | }, 177 | ".png"); 178 | } 179 | } 180 | 181 | private void renderHelpMenu() 182 | { 183 | if (ImGui.MenuItem("About LSDView")) 184 | { 185 | createModal("About", new InfoDialog(InfoDialog.DialogType.Info, $@"LSDView {Version.String} 186 | LSD: Dream Emulator data viewer 187 | https://github.com/Figglewatts/LSDView 188 | 189 | Made by Figglewatts, 2020"), 190 | new Vector2(300, 100)); 191 | } 192 | 193 | if (ImGui.MenuItem("Recenter view")) 194 | { 195 | _cameraController.RecenterView(); 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /GUI/Components/MeshListTreeNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LSDView.Graphics; 4 | 5 | namespace LSDView.GUI.Components 6 | { 7 | public class MeshListTreeNode : TreeNode 8 | { 9 | public readonly List Meshes; 10 | 11 | public MeshListTreeNode(string text, 12 | List meshes, 13 | IEnumerable children = null, 14 | Action onSelect = null, 15 | ContextMenu contextMenu = null) : 16 | base(text, children, onSelect, contextMenu) 17 | { 18 | Meshes = meshes; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GUI/Components/Modal.cs: -------------------------------------------------------------------------------- 1 | using LSDView.GUI.GUIComponents; 2 | using OpenTK; 3 | 4 | namespace LSDView.GUI.Components 5 | { 6 | public class Modal : ImGuiComponent 7 | { 8 | public string Name { get; } 9 | public string Content { get; } 10 | 11 | public Modal(string name, string content) 12 | { 13 | Name = name; 14 | Content = content; 15 | } 16 | 17 | public void ShowModal() 18 | { 19 | createModal(Name, new InfoDialog(InfoDialog.DialogType.Info, Content), new Vector2(500, 50)); 20 | } 21 | 22 | protected override void renderSelf() { } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GUI/Components/TreeNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using ImGuiNET; 5 | 6 | namespace LSDView.GUI.Components 7 | { 8 | public class TreeNode : ImGuiComponent 9 | { 10 | public string Text; 11 | public ImGuiTreeNodeFlags Flags; 12 | public Action OnSelect; 13 | 14 | public TreeNode(string text, 15 | IEnumerable children = null, 16 | Action onSelect = null, 17 | ContextMenu contextMenu = null) 18 | : base(contextMenu) 19 | { 20 | if (children != null) 21 | { 22 | _children.AddRange(children); 23 | } 24 | else 25 | { 26 | Flags |= ImGuiTreeNodeFlags.Leaf; 27 | } 28 | 29 | OnSelect = onSelect; 30 | Text = text; 31 | } 32 | 33 | public void AddNode(TreeNode node) 34 | { 35 | AddChild(node); 36 | Flags &= ~ImGuiTreeNodeFlags.Leaf; 37 | } 38 | 39 | public void AddNodes(IEnumerable nodes) 40 | { 41 | AddChildren(nodes); 42 | Flags &= ~ImGuiTreeNodeFlags.Leaf; 43 | } 44 | 45 | public void OnSelectInChildren(Action onSelect) 46 | { 47 | OnSelect = onSelect; 48 | foreach (var child in _children.OfType()) 49 | { 50 | child.OnSelectInChildren(onSelect); 51 | } 52 | } 53 | 54 | public void FlagsInChildren(ImGuiTreeNodeFlags flags) 55 | { 56 | Flags |= flags; 57 | foreach (var child in _children.OfType()) 58 | { 59 | child.FlagsInChildren(flags); 60 | } 61 | } 62 | 63 | protected virtual void internalOnSelect() { } 64 | 65 | public virtual void OnDeselect() { } 66 | 67 | protected override void renderSelf() 68 | { 69 | bool show = ImGui.TreeNodeEx(Text, Flags); 70 | if (ImGui.IsItemClicked()) 71 | { 72 | OnSelect?.Invoke(this); 73 | internalOnSelect(); 74 | } 75 | 76 | if (_tooltip != null && ImGui.IsItemHovered()) 77 | { 78 | ImGui.BeginTooltip(); 79 | _tooltip(); 80 | ImGui.EndTooltip(); 81 | } 82 | 83 | _contextMenu?.Render(); 84 | 85 | if (show) 86 | { 87 | renderChildren(); 88 | ImGui.TreePop(); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /GUI/Components/TreeView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ImGuiNET; 3 | 4 | namespace LSDView.GUI.Components 5 | { 6 | public class TreeView : ImGuiComponent where T : TreeNode 7 | { 8 | public List Nodes; 9 | 10 | public TreeNode Selected => _selected; 11 | 12 | private TreeNode _selected = null; 13 | 14 | public TreeView() { Nodes = new List(); } 15 | 16 | public void Deselect() 17 | { 18 | if (_selected == null) return; 19 | 20 | _selected.Flags &= ~ImGuiTreeNodeFlags.Selected; 21 | _selected.OnDeselect(); 22 | _selected = null; 23 | } 24 | 25 | public void SetNode(T node) 26 | { 27 | Nodes.Clear(); 28 | Nodes.Add(node); 29 | _selected = node; 30 | node.Flags |= ImGuiTreeNodeFlags.Selected; 31 | } 32 | 33 | protected override void renderSelf() 34 | { 35 | ImGui.BeginChild("tree"); 36 | foreach (var node in Nodes) 37 | { 38 | node.OnSelectInChildren(select); 39 | node.FlagsInChildren(ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.DefaultOpen | 40 | ImGuiTreeNodeFlags.OpenOnDoubleClick); 41 | node.Render(); 42 | } 43 | 44 | ImGui.EndChild(); 45 | } 46 | 47 | private void select(TreeNode node) 48 | { 49 | if (_selected != null) 50 | { 51 | _selected.Flags &= ~ImGuiTreeNodeFlags.Selected; 52 | _selected.OnDeselect(); 53 | } 54 | 55 | _selected = node; 56 | node.Flags |= ImGuiTreeNodeFlags.Selected; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /GUI/ImGuiComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiNET; 4 | using LSDView.GUI.Components; 5 | using OpenTK; 6 | 7 | namespace LSDView.GUI 8 | { 9 | public abstract class ImGuiComponent 10 | { 11 | public bool Show = true; 12 | 13 | protected readonly List _children; 14 | protected ContextMenu _contextMenu; 15 | protected Action _tooltip; 16 | 17 | private static int ModalCount = 0; 18 | private readonly Dictionary _modals; 19 | private readonly Queue _modalsToCreate; 20 | private readonly Queue _modalsToDestroy; 21 | 22 | protected ImGuiComponent(ContextMenu contextMenu = null, Action tooltip = null) 23 | { 24 | _children = new List(); 25 | _modals = new Dictionary(); 26 | _modalsToCreate = new Queue(); 27 | _modalsToDestroy = new Queue(); 28 | _contextMenu = contextMenu; 29 | _tooltip = tooltip; 30 | } 31 | 32 | public void Render() 33 | { 34 | if (Show) 35 | { 36 | renderSelf(); 37 | } 38 | 39 | renderModals(); 40 | } 41 | 42 | protected abstract void renderSelf(); 43 | 44 | public virtual void AddChild(ImGuiComponent component) { _children.Add(component); } 45 | 46 | public virtual void AddChildren(IEnumerable components) { _children.AddRange(components); } 47 | 48 | protected void renderChildren() 49 | { 50 | foreach (var child in _children) 51 | { 52 | if (child.Show) child.Render(); 53 | } 54 | } 55 | 56 | private void renderModals() 57 | { 58 | while (_modalsToCreate.Count > 0) 59 | { 60 | var modalName = _modalsToCreate.Dequeue(); 61 | var modal = _modals[modalName]; 62 | ImGui.SetNextWindowSize(new System.Numerics.Vector2(modal.InitialSize.X, modal.InitialSize.Y)); 63 | ImGui.OpenPopup(modal.Id); 64 | } 65 | 66 | while (_modalsToDestroy.Count > 0) 67 | { 68 | _modals.Remove(_modalsToDestroy.Dequeue()); 69 | } 70 | 71 | foreach (KeyValuePair kv in _modals) 72 | { 73 | if (ImGui.BeginPopupModal(kv.Value.Id, ref kv.Value.Active)) 74 | { 75 | kv.Value.Component.Render(); 76 | ImGui.EndPopup(); 77 | } 78 | 79 | if (!kv.Value.Active) 80 | { 81 | _modalsToDestroy.Enqueue(kv.Key); 82 | } 83 | } 84 | } 85 | 86 | protected void createModal(string name, ImGuiComponent component, Vector2 size) 87 | { 88 | string actualName = $"{name}##{ModalCount++}"; 89 | 90 | _modals[name] = new Modal(actualName, component, size); 91 | _modalsToCreate.Enqueue(name); 92 | } 93 | 94 | protected void destroyModal(string name) 95 | { 96 | if (!_modals.ContainsKey(name)) return; 97 | _modalsToDestroy.Enqueue(name); 98 | } 99 | 100 | internal class Modal 101 | { 102 | public readonly string Id; 103 | public bool Active = true; 104 | public readonly ImGuiComponent Component; 105 | public readonly Vector2 InitialSize; 106 | 107 | public Modal(string id, ImGuiComponent component, Vector2 initialSize) 108 | { 109 | Id = id; 110 | Component = component; 111 | InitialSize = initialSize; 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Graphics/Camera.cs: -------------------------------------------------------------------------------- 1 | using LSDView.Math; 2 | using OpenTK; 3 | 4 | namespace LSDView.Graphics 5 | { 6 | public class Camera 7 | { 8 | public Transform Transform { get; private set; } 9 | 10 | public Matrix4 View => Matrix4.LookAt(Transform.Position, Transform.Position + Transform.Forward, Transform.Up); 11 | 12 | public Camera() { Transform = new Transform(); } 13 | 14 | public void LookAt(Vector3 pos) 15 | { 16 | Vector3 forward = Transform.Forward; 17 | Vector3 newForward = Vector3.Normalize(pos - Transform.Position); 18 | Vector3 rotAxis = Vector3.Cross(forward, newForward); 19 | float angle = Vector3.CalculateAngle(forward, newForward); 20 | 21 | // rotate to face the pos 22 | Transform.Rotate(angle, rotAxis, false); 23 | } 24 | 25 | /// 26 | /// Rotate around a target at a distance. 27 | /// 28 | /// The angle to rotate around the target from east to west 29 | /// The angle to rotate around the target from north to south 30 | /// The target 31 | /// The distance the camera should be away from the target 32 | public void ArcBall(float longitude, float latitude, Vector3 target, float distance) 33 | { 34 | Transform.Position = Quaternion.FromAxisAngle(Transform.Right, latitude) * 35 | ((Transform.Position - target).Normalized() * distance) + target; 36 | Transform.Position = Quaternion.FromAxisAngle(Transform.Up, longitude) * 37 | ((Transform.Position - target).Normalized() * distance) + target; 38 | LookAt(target); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Graphics/Framebuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTK.Graphics.OpenGL4; 3 | 4 | namespace LSDView.Graphics 5 | { 6 | public class Framebuffer : IDisposable, IBindable 7 | { 8 | public int Width { get; private set; } 9 | public int Height { get; private set; } 10 | public FramebufferTarget Target { get; private set; } 11 | 12 | public Texture2D Texture => _colorAttachment; 13 | 14 | private readonly Texture2D _colorAttachment; 15 | private readonly Texture2D _depthAttachment; 16 | private readonly int _handle; 17 | 18 | public Framebuffer(int width, 19 | int height, 20 | FramebufferTarget target) 21 | { 22 | Width = width; 23 | Height = height; 24 | Target = target; 25 | 26 | _handle = GL.GenFramebuffer(); 27 | _colorAttachment = new Texture2D(Width, Height); 28 | _depthAttachment = new Texture2D(Width, Height, null, PixelInternalFormat.DepthComponent32, 29 | PixelFormat.DepthComponent, PixelType.UnsignedInt); 30 | 31 | Bind(); 32 | 33 | GL.FramebufferTexture2D(target, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, 34 | _colorAttachment.Handle, 0); 35 | GL.FramebufferTexture2D(target, FramebufferAttachment.DepthAttachment, TextureTarget.Texture2D, 36 | _depthAttachment.Handle, 0); 37 | 38 | Unbind(); 39 | } 40 | 41 | public void Resize(int newWidth, int newHeight) 42 | { 43 | _colorAttachment.Bind(); 44 | GL.TexImage2D(TextureTarget.Texture2D, 0, _colorAttachment.InternalFormat, newWidth, newHeight, 0, 45 | _colorAttachment.Format, _colorAttachment.PixelType, IntPtr.Zero); 46 | _colorAttachment.Unbind(); 47 | 48 | _depthAttachment.Bind(); 49 | GL.TexImage2D(TextureTarget.Texture2D, 0, _depthAttachment.InternalFormat, newWidth, newHeight, 0, 50 | _depthAttachment.Format, _depthAttachment.PixelType, IntPtr.Zero); 51 | _depthAttachment.Unbind(); 52 | 53 | Width = newWidth; 54 | Height = newHeight; 55 | } 56 | 57 | public void Dispose() 58 | { 59 | _colorAttachment.Dispose(); 60 | _depthAttachment.Dispose(); 61 | GL.DeleteFramebuffer(_handle); 62 | } 63 | 64 | public void Bind() { GL.BindFramebuffer(Target, _handle); } 65 | public void Unbind() { GL.BindFramebuffer(Target, 0); } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Graphics/GLBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTK.Graphics.OpenGL4; 3 | 4 | namespace LSDView.Graphics 5 | { 6 | public class GLBuffer : IBindable, IDisposable where T : struct 7 | { 8 | private readonly int _handle; 9 | private readonly T[] _elements; 10 | private readonly int _elementSize; 11 | private readonly BufferTarget _target; 12 | 13 | public T[] Elements => _elements; 14 | public int Length => _elements.Length; 15 | 16 | public GLBuffer(T[] data, int elementSize, BufferTarget target = BufferTarget.ArrayBuffer) 17 | { 18 | _elements = data; 19 | _elementSize = elementSize; 20 | _target = target; 21 | 22 | _handle = GL.GenBuffer(); 23 | Bind(); 24 | GL.BufferData(target, (IntPtr)(_elementSize * _elements.Length), _elements, BufferUsageHint.StaticDraw); 25 | Unbind(); 26 | } 27 | 28 | public void Bind() { GL.BindBuffer(_target, _handle); } 29 | 30 | public void Unbind() { GL.BindBuffer(_target, 0); } 31 | 32 | public void Dispose() { GL.DeleteBuffer(_handle); } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Graphics/Headless/HeadlessMesh.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using LSDView.Math; 4 | using OpenTK; 5 | 6 | namespace LSDView.Graphics.Headless 7 | { 8 | /// 9 | /// An implementation of IRenderable that doesn't need an OpenGL context. 10 | /// 11 | public class HeadlessMesh : IRenderable 12 | { 13 | public Transform Transform { get; } 14 | public List Textures { get; } 15 | public Material Material { get; } 16 | public IVertexArray Verts { get; } 17 | 18 | public HeadlessMesh(Vertex[] vertices, int[] indices) 19 | { 20 | Verts = new HeadlessVertexArray(vertices, indices); 21 | Transform = new Transform(); 22 | Textures = new List(); 23 | Material = null; 24 | } 25 | 26 | public static HeadlessMesh CreateQuad() 27 | { 28 | Vector3[] vertPositions = 29 | { 30 | new Vector3(-1, -1, 0), 31 | new Vector3(-1, 1, 0), 32 | new Vector3(1, 1, 0), 33 | new Vector3(1, -1, 0) 34 | }; 35 | 36 | Vector2[] vertUVs = 37 | { 38 | new Vector2(0, 0), 39 | new Vector2(0, 1), 40 | new Vector2(1, 1), 41 | new Vector2(1, 0) 42 | }; 43 | 44 | return new HeadlessMesh( 45 | new[] 46 | { 47 | new Vertex( 48 | vertPositions[0], null, vertUVs[0]), 49 | new Vertex( 50 | vertPositions[1], null, vertUVs[1]), 51 | new Vertex( 52 | vertPositions[2], null, vertUVs[2]), 53 | new Vertex( 54 | vertPositions[3], null, vertUVs[3]) 55 | }, 56 | new[] { 1, 0, 2, 2, 0, 3 } 57 | ); 58 | } 59 | 60 | public void Render(Matrix4 viewMatrix, Matrix4 projectionMatrix) 61 | { 62 | // intentionally empty 63 | } 64 | 65 | public void Dispose() 66 | { 67 | // intentionally empty 68 | } 69 | 70 | public static HeadlessMesh CombineMeshes(params IRenderable[] renderables) 71 | { 72 | var totalVertexCount = renderables.Sum(r => r.Verts.Vertices.Length); 73 | var totalIndexCount = renderables.Sum(r => r.Verts.Length); 74 | 75 | var verts = new Vertex[totalVertexCount]; 76 | var indices = new int[totalIndexCount]; 77 | 78 | int vertsRunningCount = 0; 79 | int indicesRunningCount = 0; 80 | foreach (var renderable in renderables) 81 | { 82 | renderable.Verts.CopyVertices(verts, vertsRunningCount, renderable.Transform); 83 | renderable.Verts.CopyIndices(indices, vertsRunningCount, indicesRunningCount); 84 | 85 | vertsRunningCount += renderable.Verts.Vertices.Length; 86 | indicesRunningCount += renderable.Verts.Length; 87 | } 88 | 89 | return new HeadlessMesh(verts, indices); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Graphics/Headless/HeadlessTexture2D.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics.Headless 2 | { 3 | public class HeadlessTexture2D : ITexture2D 4 | { 5 | public int Width { get; } 6 | public int Height { get; } 7 | 8 | protected readonly float[] _data; 9 | 10 | public HeadlessTexture2D(int width, int height, float[] data) 11 | { 12 | Width = width; 13 | Height = height; 14 | _data = data; 15 | } 16 | 17 | public void Dispose() 18 | { 19 | // intentionally empty 20 | } 21 | 22 | public void Bind() 23 | { 24 | // intentionally empty 25 | } 26 | 27 | public void Unbind() 28 | { 29 | // intentionally empty 30 | } 31 | 32 | public void SubImage(float[] data, int x, int y, int width, int height) 33 | { 34 | // this basically just emulates glTexSubImage2D() 35 | int componentsWritten = 0; 36 | int tStart = Height - y - 1 - (height - 1); 37 | for (int t = tStart; t < tStart + height; t++) 38 | { 39 | for (int s = x; s < x + width; s++) 40 | { 41 | int idx = t * Width * 4 + s * 4; 42 | _data[idx] = data[componentsWritten]; 43 | _data[idx + 1] = data[componentsWritten + 1]; 44 | _data[idx + 2] = data[componentsWritten + 2]; 45 | _data[idx + 3] = data[componentsWritten + 3]; 46 | componentsWritten += 4; 47 | } 48 | } 49 | } 50 | 51 | public float[] GetData() => _data; 52 | 53 | public void Clear() 54 | { 55 | for (int i = 0; i < Width * Height; i++) 56 | { 57 | _data[i] = 1; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Graphics/Headless/HeadlessVertexArray.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics.Headless 2 | { 3 | /// 4 | /// An implementation of IVertexArray that doesn't need an OpenGL context. 5 | /// 6 | public class HeadlessVertexArray : IVertexArray 7 | { 8 | public Vertex[] Vertices => _verts; 9 | public int[] Indices => _indices; 10 | public int Length => _indices.Length; 11 | public int Tris => Length / 3; 12 | 13 | protected readonly Vertex[] _verts; 14 | protected readonly int[] _indices; 15 | 16 | public HeadlessVertexArray(Vertex[] vertices, int[] indices) 17 | { 18 | _verts = vertices; 19 | _indices = indices; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Graphics/IBindable.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics 2 | { 3 | public interface IBindable 4 | { 5 | void Bind(); 6 | void Unbind(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Graphics/IDisposable.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics 2 | { 3 | public interface IDisposable 4 | { 5 | void Dispose(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Graphics/IRenderable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LSDView.Math; 3 | using OpenTK; 4 | 5 | namespace LSDView.Graphics 6 | { 7 | public interface IRenderable : IDisposable 8 | { 9 | Transform Transform { get; } 10 | List Textures { get; } 11 | Material Material { get; } 12 | IVertexArray Verts { get; } 13 | 14 | 15 | void Render(Matrix4 viewMatrix, Matrix4 projectionMatrix); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Graphics/ITexture2D.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics 2 | { 3 | public interface ITexture2D : IDisposable, IBindable 4 | { 5 | int Width { get; } 6 | int Height { get; } 7 | 8 | void SubImage(float[] data, int x, int y, int width, int height); 9 | float[] GetData(); 10 | void Clear(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Graphics/IVertexArray.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Graphics 2 | { 3 | public interface IVertexArray 4 | { 5 | Vertex[] Vertices { get; } 6 | int[] Indices { get; } 7 | int Length { get; } 8 | int Tris { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Graphics/IVertexArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using LSDView.Math; 2 | 3 | namespace LSDView.Graphics 4 | { 5 | public static class IVertexArrayExtensions 6 | { 7 | public static void CopyVertices(this IVertexArray vertexArray, 8 | Vertex[] vertices, 9 | int startIndex = 0, 10 | Transform transform = null) 11 | { 12 | for (int i = 0; i < vertexArray.Vertices.Length; i++) 13 | { 14 | vertices[startIndex + i] = vertexArray.Vertices[i]; 15 | 16 | // if a transform is given, transform the copied vertices with it 17 | if (transform != null) vertices[startIndex + i].Transform(transform); 18 | } 19 | } 20 | 21 | public static void CopyIndices(this IVertexArray vertexArray, int[] indices, int indexBase, int startIndex = 0) 22 | { 23 | for (int i = 0; i < vertexArray.Length; i++) 24 | { 25 | indices[startIndex + i] = vertexArray.Indices[i] + indexBase; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Graphics/Material.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using OpenTK; 4 | 5 | namespace LSDView.Graphics 6 | { 7 | public class Material : IBindable 8 | { 9 | protected struct MaterialParameter 10 | { 11 | public readonly MaterialParameterType ParamType; 12 | public readonly object Value; 13 | public readonly object DefaultValue; 14 | 15 | public MaterialParameter(MaterialParameterType type, object value, object defaultValue) 16 | { 17 | ParamType = type; 18 | Value = value; 19 | DefaultValue = defaultValue; 20 | } 21 | 22 | public void ApplyTo(Shader shader, string withUniformName) 23 | { 24 | switch (ParamType) 25 | { 26 | case MaterialParameterType.Bool: 27 | shader.Uniform(withUniformName, (bool)Value); 28 | break; 29 | case MaterialParameterType.Int: 30 | shader.Uniform(withUniformName, (int)Value); 31 | break; 32 | case MaterialParameterType.Float: 33 | shader.Uniform(withUniformName, (float)Value); 34 | break; 35 | case MaterialParameterType.Matrix3: 36 | shader.Uniform(withUniformName, false, (Matrix3)Value); 37 | break; 38 | case MaterialParameterType.Matrix4: 39 | shader.Uniform(withUniformName, false, (Matrix4)Value); 40 | break; 41 | case MaterialParameterType.Vector2: 42 | shader.Uniform(withUniformName, (Vector2)Value); 43 | break; 44 | case MaterialParameterType.Vector3: 45 | shader.Uniform(withUniformName, (Vector3)Value); 46 | break; 47 | case MaterialParameterType.Vector4: 48 | shader.Uniform(withUniformName, (Vector4)Value); 49 | break; 50 | default: 51 | throw new ArgumentOutOfRangeException(nameof(ParamType), ParamType, 52 | "Unknown MaterialParameterType"); 53 | } 54 | } 55 | 56 | public void UnapplyTo(Shader shader, string withUniformName) 57 | { 58 | switch (ParamType) 59 | { 60 | case MaterialParameterType.Bool: 61 | shader.Uniform(withUniformName, (bool)DefaultValue); 62 | break; 63 | case MaterialParameterType.Int: 64 | shader.Uniform(withUniformName, (int)DefaultValue); 65 | break; 66 | case MaterialParameterType.Float: 67 | shader.Uniform(withUniformName, (float)DefaultValue); 68 | break; 69 | case MaterialParameterType.Matrix3: 70 | shader.Uniform(withUniformName, false, (Matrix3)DefaultValue); 71 | break; 72 | case MaterialParameterType.Matrix4: 73 | shader.Uniform(withUniformName, false, (Matrix4)DefaultValue); 74 | break; 75 | case MaterialParameterType.Vector2: 76 | shader.Uniform(withUniformName, (Vector2)DefaultValue); 77 | break; 78 | case MaterialParameterType.Vector3: 79 | shader.Uniform(withUniformName, (Vector3)DefaultValue); 80 | break; 81 | case MaterialParameterType.Vector4: 82 | shader.Uniform(withUniformName, (Vector4)DefaultValue); 83 | break; 84 | default: 85 | throw new ArgumentOutOfRangeException(nameof(ParamType), ParamType, 86 | "Unknown MaterialParameterType"); 87 | } 88 | } 89 | } 90 | 91 | protected enum MaterialParameterType 92 | { 93 | Bool, 94 | Int, 95 | Float, 96 | Matrix4, 97 | Matrix3, 98 | Vector2, 99 | Vector3, 100 | Vector4 101 | } 102 | 103 | public readonly Shader Shader; 104 | protected readonly Dictionary _parameters; 105 | 106 | public Material(Shader shader) 107 | { 108 | Shader = shader; 109 | _parameters = new Dictionary(); 110 | } 111 | 112 | public void SetParameter(string name, bool value, bool defaultValue = default) 113 | { 114 | _parameters[name] = new MaterialParameter(MaterialParameterType.Bool, value, defaultValue); 115 | } 116 | 117 | public void SetParameter(string name, int value, int defaultValue = default) 118 | { 119 | _parameters[name] = new MaterialParameter(MaterialParameterType.Int, value, defaultValue); 120 | } 121 | 122 | public void SetParameter(string name, float value, float defaultValue = default) 123 | { 124 | _parameters[name] = new MaterialParameter(MaterialParameterType.Float, value, defaultValue); 125 | } 126 | 127 | public void SetParameter(string name, Matrix4 value, Matrix4 defaultValue = default) 128 | { 129 | _parameters[name] = new MaterialParameter(MaterialParameterType.Matrix4, value, defaultValue); 130 | } 131 | 132 | public void SetParameter(string name, Matrix3 value, Matrix3 defaultValue = default) 133 | { 134 | _parameters[name] = new MaterialParameter(MaterialParameterType.Matrix3, value, defaultValue); 135 | } 136 | 137 | public void SetParameter(string name, Vector2 value, Vector2 defaultValue = default) 138 | { 139 | _parameters[name] = new MaterialParameter(MaterialParameterType.Vector2, value, defaultValue); 140 | } 141 | 142 | public void SetParameter(string name, Vector3 value, Vector3 defaultValue = default) 143 | { 144 | _parameters[name] = new MaterialParameter(MaterialParameterType.Vector3, value, defaultValue); 145 | } 146 | 147 | public void SetParameter(string name, Vector4 value, Vector4 defaultValue = default) 148 | { 149 | _parameters[name] = new MaterialParameter(MaterialParameterType.Vector4, value, defaultValue); 150 | } 151 | 152 | protected void apply() 153 | { 154 | foreach (var paramElt in _parameters) 155 | { 156 | var paramName = paramElt.Key; 157 | var param = paramElt.Value; 158 | param.ApplyTo(Shader, paramName); 159 | } 160 | } 161 | 162 | protected void unapply() 163 | { 164 | foreach (var paramElt in _parameters) 165 | { 166 | var paramName = paramElt.Key; 167 | var param = paramElt.Value; 168 | param.UnapplyTo(Shader, paramName); 169 | } 170 | } 171 | 172 | public void Bind() 173 | { 174 | Shader.Bind(); 175 | apply(); 176 | } 177 | 178 | public void Unbind() 179 | { 180 | unapply(); 181 | Shader.Unbind(); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Graphics/Mesh.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using LSDView.Math; 4 | using OpenTK; 5 | using OpenTK.Graphics.OpenGL4; 6 | 7 | namespace LSDView.Graphics 8 | { 9 | public class Mesh : IRenderable 10 | { 11 | protected readonly VertexArray _verts; 12 | 13 | public IVertexArray Verts => _verts; 14 | 15 | public Material Material { get; set; } 16 | public Transform Transform { get; set; } 17 | public List Textures { get; set; } 18 | 19 | public Mesh(Vertex[] vertices, int[] indices, Shader shader) 20 | { 21 | _verts = new VertexArray(vertices, indices); 22 | Material = new Material(shader); 23 | Transform = new Transform(); 24 | Textures = new List(); 25 | } 26 | 27 | public static Mesh CreateQuad(Shader shader) 28 | { 29 | Vector3[] vertPositions = 30 | { 31 | new Vector3(-1, -1, 0), 32 | new Vector3(-1, 1, 0), 33 | new Vector3(1, 1, 0), 34 | new Vector3(1, -1, 0) 35 | }; 36 | 37 | Vector2[] vertUVs = 38 | { 39 | new Vector2(0, 0), 40 | new Vector2(0, 1), 41 | new Vector2(1, 1), 42 | new Vector2(1, 0) 43 | }; 44 | 45 | return new Mesh( 46 | new[] 47 | { 48 | new Vertex( 49 | vertPositions[0], null, vertUVs[0]), 50 | new Vertex( 51 | vertPositions[1], null, vertUVs[1]), 52 | new Vertex( 53 | vertPositions[2], null, vertUVs[2]), 54 | new Vertex( 55 | vertPositions[3], null, vertUVs[3]) 56 | }, 57 | new[] { 1, 0, 2, 2, 0, 3 }, 58 | shader 59 | ); 60 | } 61 | 62 | public void Render(Matrix4 view, Matrix4 projection) 63 | { 64 | _verts.Bind(); 65 | bindTextures(); 66 | Material.Bind(); 67 | Material.Shader.Uniform("Projection", false, projection); 68 | Material.Shader.Uniform("View", false, view); 69 | Material.Shader.Uniform("Model", false, Transform.Matrix); 70 | GL.DrawElements(PrimitiveType.Triangles, _verts.Length, DrawElementsType.UnsignedInt, 0); 71 | Material.Unbind(); 72 | unbindTextures(); 73 | _verts.Unbind(); 74 | } 75 | 76 | public void Dispose() { _verts.Dispose(); } 77 | 78 | public void ClearTextures() 79 | { 80 | foreach (ITexture2D tex in Textures) 81 | { 82 | tex.Dispose(); 83 | } 84 | 85 | Textures.Clear(); 86 | } 87 | 88 | public static Mesh CombineMeshes(Shader shader = null, params IRenderable[] renderables) 89 | { 90 | var totalVertexCount = renderables.Sum(r => r.Verts.Vertices.Length); 91 | var totalIndexCount = renderables.Sum(r => r.Verts.Length); 92 | 93 | var verts = new Vertex[totalVertexCount]; 94 | var indices = new int[totalIndexCount]; 95 | 96 | int vertsRunningCount = 0; 97 | int indicesRunningCount = 0; 98 | foreach (var renderable in renderables) 99 | { 100 | renderable.Verts.CopyVertices(verts, vertsRunningCount, renderable.Transform); 101 | renderable.Verts.CopyIndices(indices, indicesRunningCount); 102 | 103 | vertsRunningCount += renderable.Verts.Vertices.Length; 104 | indicesRunningCount += renderable.Verts.Length; 105 | } 106 | 107 | return new Mesh(verts, indices, shader); 108 | } 109 | 110 | protected void bindTextures() 111 | { 112 | for (int i = 0; i < Textures.Count; i++) 113 | { 114 | GL.ActiveTexture(TextureUnit.Texture0 + i); 115 | Textures[i].Bind(); 116 | } 117 | 118 | GL.ActiveTexture(TextureUnit.Texture0); 119 | } 120 | 121 | protected void unbindTextures() 122 | { 123 | for (int i = 0; i < Textures.Count; i++) 124 | { 125 | GL.ActiveTexture(TextureUnit.Texture0 + i); 126 | Textures[i].Unbind(); 127 | } 128 | 129 | GL.ActiveTexture(TextureUnit.Texture0); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Graphics/Shader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using OpenTK; 4 | using OpenTK.Graphics.OpenGL4; 5 | using Serilog; 6 | 7 | namespace LSDView.Graphics 8 | { 9 | public class Shader : IBindable, IDisposable 10 | { 11 | private int _vertHandle; 12 | private int _fragHandle; 13 | private int _progHandle; 14 | 15 | public string Name { get; } 16 | 17 | public Shader(string name, string path) 18 | { 19 | this.Name = name; 20 | compileAndLink(path); 21 | } 22 | 23 | private void compileAndLink(string path) 24 | { 25 | // load and compile vertex shader 26 | _vertHandle = loadSource(path, ShaderType.VertexShader); 27 | GL.CompileShader(_vertHandle); 28 | checkCompileErr(_vertHandle, ShaderType.VertexShader); 29 | 30 | // load and compile fragment shader 31 | _fragHandle = loadSource(path, ShaderType.FragmentShader); 32 | GL.CompileShader(_fragHandle); 33 | checkCompileErr(_fragHandle, ShaderType.FragmentShader); 34 | 35 | // link the shader program 36 | _progHandle = GL.CreateProgram(); 37 | GL.AttachShader(_progHandle, _vertHandle); 38 | GL.AttachShader(_progHandle, _fragHandle); 39 | GL.LinkProgram(_progHandle); 40 | checkLinkErr(_progHandle); 41 | 42 | // clean up unused handles 43 | GL.DeleteShader(_vertHandle); 44 | GL.DeleteShader(_fragHandle); 45 | _vertHandle = _fragHandle = 0; 46 | } 47 | 48 | private int loadSource(string path, ShaderType type) 49 | { 50 | string completePath = path; 51 | if (type == ShaderType.VertexShader) 52 | path += ".vert"; 53 | else if (type == ShaderType.FragmentShader) 54 | path += ".frag"; 55 | else 56 | throw new ArgumentException("loadSource expects ShaderType to be Vertex or Fragment shader"); 57 | 58 | string source = File.ReadAllText(path); 59 | int handle = GL.CreateShader(type); 60 | GL.ShaderSource(handle, source); 61 | return handle; 62 | } 63 | 64 | private bool checkCompileErr(int shader, ShaderType type) 65 | { 66 | GL.GetShader(shader, ShaderParameter.CompileStatus, out int success); 67 | if (success != 1) 68 | { 69 | string infoLog = GL.GetShaderInfoLog(shader); 70 | Log.Error("Could not compile {0}: {1}", type.ToString(), Name); 71 | Log.Error("Info log: {0}", infoLog); 72 | } 73 | 74 | return success == 1; 75 | } 76 | 77 | private bool checkLinkErr(int program) 78 | { 79 | GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int success); 80 | if (success != 1) 81 | { 82 | string infoLog = GL.GetProgramInfoLog(program); 83 | Log.Error("Could not link program: {0}", Name); 84 | Log.Error("Info log: {0}", infoLog); 85 | } 86 | 87 | return success == 1; 88 | } 89 | 90 | private int getUniformLocation(string name) { return GL.GetUniformLocation(_progHandle, name); } 91 | 92 | public void Uniform(string name, bool value) { GL.Uniform1(getUniformLocation(name), value ? 1 : 0); } 93 | 94 | public void Uniform(string name, int value) { GL.Uniform1(getUniformLocation(name), value); } 95 | 96 | public void Uniform(string name, float value) { GL.Uniform1(getUniformLocation(name), value); } 97 | 98 | public void Uniform(string name, bool transpose, Matrix4 value) 99 | { 100 | GL.UniformMatrix4(getUniformLocation(name), transpose, ref value); 101 | } 102 | 103 | public void Uniform(string name, bool transpose, Matrix3 value) 104 | { 105 | GL.UniformMatrix3(getUniformLocation(name), transpose, ref value); 106 | } 107 | 108 | public void Uniform(string name, Vector2 value) { GL.Uniform2(getUniformLocation(name), value); } 109 | 110 | public void Uniform(string name, Vector3 value) { GL.Uniform3(getUniformLocation(name), value); } 111 | 112 | public void Uniform(string name, Vector4 value) { GL.Uniform4(getUniformLocation(name), value); } 113 | 114 | public int GetAttribLocation(string name) { return GL.GetAttribLocation(_progHandle, name); } 115 | 116 | public void Bind() { GL.UseProgram(_progHandle); } 117 | 118 | public void Unbind() { GL.UseProgram(0); } 119 | 120 | public void Dispose() { GL.DeleteProgram(_progHandle); } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Graphics/Texture2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTK.Graphics; 3 | using OpenTK.Graphics.OpenGL4; 4 | 5 | namespace LSDView.Graphics 6 | { 7 | public class Texture2D : ITexture2D 8 | { 9 | public int Width { get; } 10 | public int Height { get; } 11 | 12 | public PixelInternalFormat InternalFormat { get; } 13 | public PixelFormat Format { get; } 14 | public PixelType PixelType { get; } 15 | 16 | public int Handle => _handle; 17 | 18 | private readonly int _handle; 19 | 20 | public Texture2D( 21 | int width, 22 | int height, 23 | float[] data = null, 24 | PixelInternalFormat internalFormat = PixelInternalFormat.Rgba32f, 25 | PixelFormat colorFormat = PixelFormat.Rgba, 26 | PixelType dataType = PixelType.Float) 27 | { 28 | _handle = GL.GenTexture(); 29 | Bind(); 30 | Width = width; 31 | Height = height; 32 | InternalFormat = internalFormat; 33 | Format = colorFormat; 34 | PixelType = dataType; 35 | 36 | if (data != null) 37 | { 38 | GL.TexImage2D(TextureTarget.Texture2D, 0, internalFormat, width, height, 0, colorFormat, 39 | dataType, data); 40 | } 41 | else 42 | { 43 | GL.TexImage2D(TextureTarget.Texture2D, 0, internalFormat, Width, Height, 0, colorFormat, dataType, 44 | IntPtr.Zero); 45 | } 46 | 47 | GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, 48 | (int)TextureWrapMode.ClampToEdge); 49 | GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, 50 | (int)TextureWrapMode.ClampToEdge); 51 | GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, 52 | (int)TextureMinFilter.Nearest); 53 | GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, 54 | (int)TextureMagFilter.Nearest); 55 | Unbind(); 56 | } 57 | 58 | public static Texture2D Fill(Color4 color, int width, int height) 59 | { 60 | float[] colorData = new float[width * height * 4]; 61 | for (int i = 0; i < width * height * 4; i += 4) 62 | { 63 | colorData[i] = color.R; 64 | colorData[i + 1] = color.G; 65 | colorData[i + 2] = color.B; 66 | colorData[i + 3] = color.A; 67 | } 68 | 69 | return new Texture2D(width, height, colorData); 70 | } 71 | 72 | public void Dispose() { GL.DeleteTexture(_handle); } 73 | 74 | public void Bind() { GL.BindTexture(TextureTarget.Texture2D, _handle); } 75 | 76 | public void Unbind() { GL.BindTexture(TextureTarget.Texture2D, 0); } 77 | 78 | public void SubImage(float[] data, int x, int y, int width, int height) 79 | { 80 | Bind(); 81 | GL.TexSubImage2D(TextureTarget.Texture2D, 0, x, y, width, height, PixelFormat.Rgba, PixelType.Float, data); 82 | Unbind(); 83 | } 84 | 85 | public float[] GetData() 86 | { 87 | if (PixelType != PixelType.Float) 88 | { 89 | throw new NotSupportedException( 90 | "Unable to get pixel data for texture with pixel type not equal to 'Float'"); 91 | } 92 | 93 | if (Format != PixelFormat.Rgba) 94 | { 95 | throw new NotSupportedException("Unable to get pixel data for texture with non-RGBA format"); 96 | } 97 | 98 | float[] tex = new float[Width * Height * 4]; 99 | Bind(); 100 | GL.GetTexImage(TextureTarget.Texture2D, 0, PixelFormat.Rgba, PixelType.Float, tex); 101 | Unbind(); 102 | 103 | // flip the texture data 104 | float[] flippedTex = new float[Width * Height * 4]; 105 | int i = 0; 106 | for (int y = Height - 1; y >= 0; y--) 107 | { 108 | for (int x = 0; x < Width; x++) 109 | { 110 | flippedTex[i] = tex[y * (Width * 4) + (x * 4)]; 111 | flippedTex[i + 1] = tex[y * (Width * 4) + (x * 4) + 1]; 112 | flippedTex[i + 2] = tex[y * (Width * 4) + (x * 4) + 2]; 113 | flippedTex[i + 3] = tex[y * (Width * 4) + (x * 4) + 3]; 114 | i += 4; 115 | } 116 | } 117 | 118 | return flippedTex; 119 | } 120 | 121 | public void Clear() 122 | { 123 | GL.ClearTexImage(_handle, 0, PixelFormat.Rgba, PixelType.Float, new float[] { 1, 1, 1, 1 }); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Graphics/Vertex.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using LSDView.Math; 3 | using OpenTK; 4 | 5 | namespace LSDView.Graphics 6 | { 7 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 8 | public struct Vertex 9 | { 10 | public static readonly int Size = (3 + 3 + 2 + 4) * 4; // size in bytes 11 | public Vector3 Position; 12 | public Vector3 Normal; 13 | public Vector2 TexCoord; 14 | public Vector4 Color; 15 | 16 | public Vertex(Vector3 position, 17 | Vector3? normal = null, 18 | Vector2? texCoord = null, 19 | Vector4? color = null) 20 | { 21 | Position = position; 22 | Normal = normal ?? Vector3.Zero; 23 | TexCoord = texCoord ?? Vector2.Zero; 24 | Color = color ?? Vector4.One; 25 | } 26 | 27 | /// 28 | /// Manually applies a transformation to this vertex's position. 29 | /// This should only be used once, as multiple times will keep reapplying the transform. 30 | /// 31 | /// The Transform to apply to this vertex. 32 | public void Transform(Transform transform) { Position = (new Vector4(Position, 1) * transform.Matrix).Xyz; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Graphics/VertexArray.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | 3 | namespace LSDView.Graphics 4 | { 5 | public class VertexArray : IVertexArray, IBindable, IDisposable 6 | { 7 | private readonly int _handle; 8 | private readonly GLBuffer _vertexBuffer; 9 | private readonly GLBuffer _indexBuffer; 10 | 11 | private readonly VertexAttrib[] _attribs = new VertexAttrib[] 12 | { 13 | new VertexAttrib(0, 3, VertexAttribPointerType.Float, Vertex.Size, 0), // in_Position 14 | new VertexAttrib(1, 3, VertexAttribPointerType.Float, Vertex.Size, 3 * 4), // in_Normal 15 | new VertexAttrib(2, 2, VertexAttribPointerType.Float, Vertex.Size, (3 + 3) * 4), // in_TexCoord 16 | new VertexAttrib(3, 4, VertexAttribPointerType.Float, Vertex.Size, (3 + 3 + 2) * 4) // in_Color 17 | }; 18 | 19 | public Vertex[] Vertices => _vertexBuffer.Elements; 20 | public int[] Indices => _indexBuffer.Elements; 21 | public int Length => _indexBuffer.Length; 22 | public int Tris => Length / 3; 23 | 24 | public VertexArray(Vertex[] vertices, int[] indices) 25 | { 26 | _handle = GL.GenVertexArray(); 27 | Bind(); 28 | 29 | _vertexBuffer = new GLBuffer(vertices, Vertex.Size, BufferTarget.ArrayBuffer); 30 | _indexBuffer = new GLBuffer(indices, sizeof(int), BufferTarget.ElementArrayBuffer); 31 | 32 | _vertexBuffer.Bind(); 33 | _indexBuffer.Bind(); 34 | foreach (var attrib in _attribs) 35 | { 36 | attrib.Set(); 37 | } 38 | 39 | Unbind(); 40 | } 41 | 42 | public void Bind() { GL.BindVertexArray(_handle); } 43 | 44 | public void Unbind() { GL.BindVertexArray(0); } 45 | 46 | public void Dispose() 47 | { 48 | _vertexBuffer.Dispose(); 49 | _indexBuffer.Dispose(); 50 | GL.DeleteVertexArray(_handle); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Graphics/VertexAttrib.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | 3 | namespace LSDView.Graphics 4 | { 5 | public class VertexAttrib 6 | { 7 | private readonly int _index; 8 | private readonly int _size; 9 | private readonly VertexAttribPointerType _type; 10 | private readonly bool _normalize; 11 | private readonly int _stride; 12 | private readonly int _offset; 13 | 14 | public VertexAttrib(int index, 15 | int size, 16 | VertexAttribPointerType type, 17 | int stride, 18 | int offset, 19 | bool normalize = false) 20 | { 21 | _index = index; 22 | _size = size; 23 | _type = type; 24 | _stride = stride; 25 | _offset = offset; 26 | _normalize = normalize; 27 | } 28 | 29 | public void Set() 30 | { 31 | GL.EnableVertexAttribArray(_index); 32 | GL.VertexAttribPointer(_index, _size, _type, _normalize, _stride, _offset); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GuiApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using Autofac; 5 | using LSDView.Controller; 6 | using LSDView.Controllers; 7 | using LSDView.Controllers.GUI; 8 | using LSDView.Graphics; 9 | using LSDView.GUI; 10 | using LSDView.GUI.Components; 11 | using LSDView.GUI.GUIComponents; 12 | using OpenTK; 13 | using OpenTK.Graphics; 14 | using OpenTK.Graphics.ES30; 15 | using OpenTK.Input; 16 | using Serilog; 17 | using FramebufferTarget = OpenTK.Graphics.OpenGL4.FramebufferTarget; 18 | using Vector2 = System.Numerics.Vector2; 19 | 20 | namespace LSDView 21 | { 22 | public class GuiApplication : GameWindow 23 | { 24 | public static GuiApplication Instance = null; 25 | 26 | public Action NextUpdateActions; 27 | public Action NextGuiRender; 28 | 29 | public Matrix4 ProjectionMatrix => _proj; 30 | public Camera Camera => _cam; 31 | public Vector2 Dimensions => new Vector2(Width, Height); 32 | 33 | private const int WINDOW_WIDTH = 800; 34 | private const int WINDOW_HEIGHT = 600; 35 | private const string WINDOW_TITLE = "LSDView"; 36 | private const int GL_MAJOR_VERSION = 4; 37 | private const int GL_MINOR_VERSION = 0; 38 | 39 | private readonly List _guiComponents; 40 | 41 | private readonly Camera _cam; 42 | private Matrix4 _proj; 43 | private readonly Framebuffer _fbo; 44 | 45 | // ui 46 | private FileDialog _fileExportDialog; 47 | private Modal _updateAvailableModal; 48 | 49 | // -------------- 50 | 51 | // controllers 52 | private TreeController _treeController; 53 | private VRAMController _vramController; 54 | private CameraController _cameraController; 55 | private ConfigController _configController; 56 | private FileOpenController _fileOpenController; 57 | private AnimationController _animationController; 58 | private GUIExportController _exportController; 59 | private UpdateCheckerController _updateCheckerController; 60 | 61 | // -------------- 62 | 63 | public GuiApplication(ILifetimeScope scope) : base(WINDOW_WIDTH, WINDOW_HEIGHT, GraphicsMode.Default, 64 | WINDOW_TITLE, 65 | GameWindowFlags.Default, DisplayDevice.Default, GL_MAJOR_VERSION, GL_MINOR_VERSION, 66 | GraphicsContextFlags.Default) 67 | { 68 | Instance = this; 69 | 70 | createControllers(scope); 71 | 72 | Icon = new Icon(typeof(GuiApplication), "appicon.ico"); 73 | 74 | GL.Enable(EnableCap.DepthTest); 75 | GL.Enable(EnableCap.CullFace); 76 | 77 | _cam = new Camera(); 78 | _cameraController.ProvideCamera(_cam); 79 | _cam.Transform.Translate(new Vector3(0, 0, -3)); 80 | _fbo = new Framebuffer(WINDOW_WIDTH, WINDOW_HEIGHT, FramebufferTarget.Framebuffer); 81 | 82 | _proj = Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(60f), 83 | (float)_fbo.Width / _fbo.Height, 0.1f, 100f); 84 | 85 | ImGuiRenderer.Init(); 86 | 87 | _guiComponents = new List(); 88 | 89 | ApplicationArea area = new ApplicationArea(); 90 | 91 | TreeView outlineView = new TreeView(); 92 | _treeController.SetTree(outlineView); 93 | 94 | area.AddChild(new Columns(2, new List 95 | { outlineView, new FramebufferArea(_fbo) }, new[] { 250f, -1 })); 96 | 97 | var menuBar = new MainMenuBar(_fileOpenController, _vramController, _configController, _cameraController, 98 | _exportController); 99 | 100 | if (string.IsNullOrWhiteSpace(_configController.Config.GameDataPath)) 101 | { 102 | // show the set game data path dialog if it hasn't yet been set 103 | menuBar.OpenSetGameDataPath(); 104 | } 105 | 106 | _updateAvailableModal = new Modal("New update available!", 107 | "Download at https://github.com/Figglewatts/LSDView/releases/latest"); 108 | 109 | _fileExportDialog = new FileDialog(_configController.Config.GameDataPath, FileDialog.DialogType.Save); 110 | _configController.Config.OnGameDataPathChange += () => 111 | _fileExportDialog.InitialDir = _configController.Config.GameDataPath; 112 | _exportController.ProvideFileExportDialog(_fileExportDialog); 113 | 114 | _guiComponents.Add(area); 115 | _guiComponents.Add(menuBar); 116 | _guiComponents.Add(_fileExportDialog); 117 | _guiComponents.Add(_updateAvailableModal); 118 | 119 | if (_updateCheckerController.IsUpdateAvailable()) 120 | { 121 | _updateAvailableModal.ShowModal(); 122 | } 123 | } 124 | 125 | private void createControllers(ILifetimeScope scope) 126 | { 127 | _configController = scope.Resolve(); 128 | _exportController = scope.Resolve(); 129 | _animationController = scope.Resolve(); 130 | _treeController = scope.Resolve(); 131 | _vramController = scope.Resolve(); 132 | _cameraController = scope.Resolve(); 133 | _fileOpenController = scope.Resolve(); 134 | _updateCheckerController = scope.Resolve(); 135 | } 136 | 137 | protected override void OnResize(EventArgs e) 138 | { 139 | GL.Viewport(0, 0, Width, Height); 140 | ImGuiRenderer.Resize(Width, Height); 141 | } 142 | 143 | protected override void OnLoad(EventArgs e) 144 | { 145 | CursorVisible = true; 146 | GL.ClearColor(0.2f, 0.2f, 0.2f, 1); 147 | 148 | Log.Information("Initialized LSDView"); 149 | } 150 | 151 | protected override void OnUnload(EventArgs e) { ImGuiRenderer.Shutdown(); } 152 | 153 | protected override void OnUpdateFrame(FrameEventArgs e) 154 | { 155 | HandleInput(); 156 | 157 | NextUpdateActions?.Invoke(); 158 | NextUpdateActions = null; 159 | 160 | _cameraController.Update(); 161 | _animationController.Update(e.Time); 162 | } 163 | 164 | protected override void OnRenderFrame(FrameEventArgs e) 165 | { 166 | GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); 167 | 168 | ImGuiRenderer.BeginFrame(e.Time); 169 | 170 | foreach (var component in _guiComponents) 171 | { 172 | component.Render(); 173 | } 174 | 175 | NextGuiRender?.Invoke(); 176 | NextGuiRender = null; 177 | 178 | ImGuiRenderer.EndFrame(); 179 | 180 | _fbo.Bind(); 181 | GL.Viewport(0, 0, _fbo.Width, _fbo.Height); 182 | _proj = Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(60f), 183 | (float)_fbo.Width / _fbo.Height, 0.1f, 100f); 184 | GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); 185 | 186 | _treeController.RenderSelectedNode(_cam.View, _proj); 187 | 188 | _fbo.Unbind(); 189 | 190 | SwapBuffers(); 191 | } 192 | 193 | protected override void OnKeyPress(KeyPressEventArgs e) 194 | { 195 | if (!Focused || !Visible) return; 196 | ImGuiRenderer.AddKeyChar(e.KeyChar); 197 | } 198 | 199 | protected override void OnMouseMove(MouseMoveEventArgs e) 200 | { 201 | if (!Focused || !Visible) return; 202 | ImGuiRenderer.UpdateMousePos(e.X, e.Y); 203 | } 204 | 205 | private void HandleInput() { } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Headless/AbstractHeadlessCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommandLine; 3 | using LSDView.Util; 4 | using Serilog; 5 | 6 | namespace LSDView.Headless 7 | { 8 | public abstract class AbstractHeadlessCommand : IHeadlessCommand 9 | { 10 | public abstract Type OptionsType { get; } 11 | public abstract void Register(ref ParserResult parserResult); 12 | 13 | protected void handleGlobalOptions(GlobalOptions options) 14 | { 15 | LoggingUtil.LoggingLevelSwitch.MinimumLevel = options.Verbosity; 16 | Log.Information($"Setting verbosity to: {options.Verbosity}"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Headless/ExportLevelCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using CommandLine; 5 | using LSDView.Controllers; 6 | using LSDView.Controllers.Headless; 7 | using LSDView.Models; 8 | 9 | namespace LSDView.Headless 10 | { 11 | public class ExportLevelCommand : AbstractHeadlessCommand 12 | { 13 | public override Type OptionsType => typeof(ExportLevelOptions); 14 | 15 | [Verb("exportdream", HelpText = "Export a directory of LBD files to a single mesh and textures.")] 16 | public class ExportLevelOptions : GlobalOptions 17 | { 18 | [Option('i', "input", Required = true, HelpText = "The folder containing LBD files to export.")] 19 | public string Input { get; set; } 20 | 21 | [Option('o', "output", Required = true, HelpText = "The folder to export the complete dream to.")] 22 | public string Output { get; set; } 23 | 24 | [Option('w', "width", Default = -1, HelpText = "The width of the dream in LBD chunks. " + 25 | "If not specified, will attempt to automatically " + 26 | "figure it out.")] 27 | public int Width { get; set; } 28 | 29 | [Option('f', "format", Default = "ply", HelpText = "The format to export to. Valid values: 'obj', 'ply'. ")] 30 | public string Format { get; set; } 31 | 32 | [Option('s', "separate-objects", Default = false, HelpText = "Whether or not to separate LBD files in the" + 33 | " exported mesh, or combine them into one mesh.")] 34 | public bool SeparateObjects { get; set; } 35 | } 36 | 37 | protected readonly HeadlessLBDController _lbdController; 38 | protected readonly HeadlessTIXController _tixController; 39 | protected readonly HeadlessExportController _exportController; 40 | 41 | public ExportLevelCommand(HeadlessLBDController lbdController, 42 | HeadlessTIXController tixController, 43 | HeadlessExportController exportController) 44 | { 45 | _lbdController = lbdController; 46 | _tixController = tixController; 47 | _exportController = exportController; 48 | } 49 | 50 | public override void Register(ref ParserResult parserResult) 51 | { 52 | parserResult.WithParsed(Handle); 53 | } 54 | 55 | public void Handle(ExportLevelOptions options) 56 | { 57 | handleGlobalOptions(options); 58 | 59 | if (!Directory.Exists(options.Input)) 60 | { 61 | string errorMessage = $"Input directory '{options.Input}' did not exist"; 62 | throw new HeadlessException(errorMessage); 63 | } 64 | 65 | // determine export format 66 | var exportFormat = determineExportFormat(options.Format); 67 | 68 | if (options.Width == -1) 69 | { 70 | // auto detect level width 71 | var detectedWidth = detectDreamWidth(options.Input); 72 | if (detectedWidth == -1) 73 | { 74 | throw new HeadlessException("Unable to auto detect dream width: directory must be named " + 75 | "in STGXX format, like the original game: you can also " + 76 | "supply a custom dream width with the --width flag"); 77 | } 78 | } 79 | 80 | // gather LBD files 81 | var dreamChunkFiles = Directory.GetFiles(options.Input, "*.lbd", SearchOption.AllDirectories); 82 | if (dreamChunkFiles.Length <= 0) 83 | { 84 | string errorMessage = $"Unable to export dream: no LBD files found in '{options.Input}'"; 85 | throw new HeadlessException(errorMessage); 86 | } 87 | 88 | // load LBD files 89 | List dreamChunks = new List(dreamChunkFiles.Length); 90 | foreach (var dreamChunkFile in dreamChunkFiles) 91 | { 92 | var lbd = _lbdController.Load(dreamChunkFile); 93 | dreamChunks.Add(_lbdController.CreateDocument(lbd)); 94 | } 95 | 96 | // gather TIX files 97 | var textureSetFiles = Directory.GetFiles(options.Input, "*.tix"); 98 | if (textureSetFiles.Length != 4) 99 | { 100 | string errorMessage = 101 | $"Unable to export dream: there should be exactly 4 TIX files in '{options.Input}'" + 102 | $", found: {textureSetFiles.Length} instead"; 103 | throw new HeadlessException(errorMessage); 104 | } 105 | 106 | // load TIX files 107 | List textureSets = new List(textureSetFiles.Length); 108 | foreach (var textureSetFile in textureSetFiles) 109 | { 110 | var tix = _tixController.Load(textureSetFile); 111 | textureSets.Add(_tixController.CreateDocument(tix)); 112 | } 113 | 114 | // export the dream 115 | Dream toExport = new Dream(dreamChunks, options.Width, textureSets); 116 | _exportController.ExportDream(toExport, !options.SeparateObjects, exportFormat, options.Output); 117 | } 118 | 119 | protected MeshExportFormat determineExportFormat(string format) 120 | { 121 | format = format.ToLowerInvariant(); 122 | switch (format.ToLowerInvariant()) 123 | { 124 | case "obj": 125 | return MeshExportFormat.OBJ; 126 | case "ply": 127 | return MeshExportFormat.PLY; 128 | default: 129 | throw new HeadlessException($"Unrecognized mesh export format '{format}': not in (obj, ply)"); 130 | } 131 | } 132 | 133 | /// 134 | /// Dream chunk widths by directory name, to support in auto-detecting dream widths in detectDreamWidth(). 135 | /// Source is: https://docs.lsdrevamped.net/lsd-de-research/static-analysis/file-formats#lbd 136 | /// 137 | protected readonly Dictionary _dreamWidthsByName = new Dictionary() 138 | { 139 | { "stg00", 0 }, // bright moon cottage 140 | { "stg01", 3 }, // pit & temple 141 | { "stg02", 6 }, // kyoto 142 | { "stg03", 16 }, // the natural world 143 | { "stg04", 6 }, // happytown 144 | { "stg05", 5 }, // violence district 145 | { "stg06", 0 }, // moonlight tower 146 | { "stg07", 5 }, // temple dojo 147 | { "stg08", 1 }, // flesh tunnels 148 | { "stg09", 1 }, // clockwork machines 149 | { "stg10", 3 }, // long hallway 150 | { "stg11", 4 }, // sun faces heave 151 | { "stg12", 4 }, // black space 152 | { "stg13", 2 } // monument park 153 | }; 154 | 155 | /// 156 | /// Attempt to auto detect the width of a dream based on the name of the directory. 157 | /// If it cannot auto-detect the width, it will return 0. 158 | /// 159 | /// The directory containing the dream's LBD files. 160 | /// The auto-detected width, or -1 if unable to detect. 161 | protected int detectDreamWidth(string inputPath) 162 | { 163 | if (!inputPath.EndsWith(Path.DirectorySeparatorChar.ToString())) 164 | { 165 | inputPath += Path.DirectorySeparatorChar; 166 | } 167 | 168 | var dirName = Path.GetFileName(Path.GetDirectoryName(inputPath))?.ToLowerInvariant(); 169 | if (dirName == null) return -1; 170 | 171 | if (_dreamWidthsByName.ContainsKey(dirName)) 172 | { 173 | return _dreamWidthsByName[dirName]; 174 | } 175 | 176 | return -1; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Headless/GlobalOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using Serilog.Events; 3 | 4 | namespace LSDView.Headless 5 | { 6 | public class GlobalOptions 7 | { 8 | [Option('v', "verbosity", Default = LogEventLevel.Information, HelpText = "The verbosity of the logging.")] 9 | public LogEventLevel Verbosity { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Headless/HeadlessException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSDView.Headless 4 | { 5 | /// 6 | /// Exception for errors when running headless commands. 7 | /// 8 | public class HeadlessException : Exception 9 | { 10 | public HeadlessException(string message) : base(message) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Headless/IHeadlessCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommandLine; 3 | 4 | namespace LSDView.Headless 5 | { 6 | public interface IHeadlessCommand 7 | { 8 | void Register(ref ParserResult parserResult); 9 | 10 | Type OptionsType { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /HeadlessApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Autofac; 4 | using CommandLine; 5 | using LSDView.Controllers.Headless; 6 | using LSDView.Headless; 7 | using LSDView.Util; 8 | using Serilog; 9 | 10 | namespace LSDView 11 | { 12 | public class HeadlessApplication 13 | { 14 | private const string HEADLESS_LOG_FORMAT = 15 | "{Level:u} | {Message:lj}{NewLine}{Exception}"; 16 | 17 | protected readonly AbstractHeadlessCommand[] Commands; 18 | 19 | protected readonly HeadlessExportController _exportController; 20 | protected readonly HeadlessLBDController _lbdController; 21 | protected readonly HeadlessMOMController _momController; 22 | protected readonly HeadlessTIMController _timController; 23 | protected readonly HeadlessTIXController _tixController; 24 | protected readonly HeadlessTMDController _tmdController; 25 | 26 | public HeadlessApplication(ILifetimeScope scope) 27 | { 28 | _exportController = scope.Resolve(); 29 | _lbdController = scope.Resolve(); 30 | _momController = scope.Resolve(); 31 | _timController = scope.Resolve(); 32 | _tixController = scope.Resolve(); 33 | _tmdController = scope.Resolve(); 34 | 35 | Commands = new AbstractHeadlessCommand[] 36 | { 37 | new ExportLevelCommand(_lbdController, _tixController, _exportController) 38 | }; 39 | } 40 | 41 | public int Run(string[] args) 42 | { 43 | // create a console window for this application (or assign it to the existing console window) 44 | if (!ConsoleUtil.AttachConsole(-1)) ConsoleUtil.AllocConsole(); 45 | 46 | // set up special console logging for headless mode 47 | using (var log = new LoggerConfiguration() 48 | .MinimumLevel.ControlledBy(LoggingUtil.LoggingLevelSwitch) 49 | .WriteTo.Logger(Log.Logger) 50 | .WriteTo.Console(outputTemplate: HEADLESS_LOG_FORMAT) 51 | .CreateLogger()) 52 | { 53 | Log.Logger = log; 54 | 55 | // parse the arguments, and log any errors 56 | int exitCode = 0; 57 | try 58 | { 59 | var commandTypes = Commands.Select(c => c.OptionsType).ToArray(); 60 | var parseResult = Parser.Default.ParseArguments(args, commandTypes); 61 | foreach (var command in Commands) 62 | { 63 | command.Register(ref parseResult); 64 | } 65 | } 66 | catch (HeadlessException e) 67 | { 68 | Log.Fatal(e.Message); 69 | exitCode = 1; 70 | } 71 | catch (Exception e) 72 | { 73 | var errString = $"{e.Message}\n{e.StackTrace}"; 74 | Log.Fatal(errString); 75 | exitCode = 1; 76 | } 77 | finally 78 | { 79 | ConsoleUtil.FreeConsole(); 80 | } 81 | 82 | return exitCode; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sam Gibson 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 | -------------------------------------------------------------------------------- /LSDView.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSDView", "LSDView.csproj", "{0138A340-4992-44EC-9BF6-4812B41A9C4A}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {0138A340-4992-44EC-9BF6-4812B41A9C4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {0138A340-4992-44EC-9BF6-4812B41A9C4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {0138A340-4992-44EC-9BF6-4812B41A9C4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {0138A340-4992-44EC-9BF6-4812B41A9C4A}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /Math/Convert.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace LSDView.Math 3 | { 4 | public class Convert 5 | { 6 | public static byte ToByte(float a) 7 | { 8 | return (byte)System.Math.Round(a * 255.0); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Math/Transform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK; 2 | 3 | namespace LSDView.Math 4 | { 5 | public class Transform 6 | { 7 | private Vector3 _position; 8 | 9 | public Vector3 Position 10 | { 11 | get => _position; 12 | set 13 | { 14 | _position = value; 15 | recomputeMatrix(); 16 | } 17 | } 18 | 19 | private Vector3 _scale; 20 | 21 | public Vector3 Scale 22 | { 23 | get => _scale; 24 | set 25 | { 26 | _scale = value; 27 | recomputeMatrix(); 28 | } 29 | } 30 | 31 | private Quaternion _rotation; 32 | 33 | public Quaternion Rotation 34 | { 35 | get => _rotation; 36 | set 37 | { 38 | _rotation = value; 39 | recomputeMatrix(); 40 | } 41 | } 42 | 43 | private Matrix4 _matrix; 44 | 45 | public Matrix4 Matrix 46 | { 47 | get 48 | { 49 | if (Parent == null) 50 | { 51 | return _matrix; 52 | } 53 | 54 | return _matrix * Parent.Matrix; 55 | } 56 | private set => _matrix = value; 57 | } 58 | 59 | public Vector3 Up => (Vector3.UnitY * new Matrix3(Matrix)).Normalized(); 60 | public Vector3 Forward => (Vector3.UnitZ * new Matrix3(Matrix)).Normalized(); 61 | public Vector3 Right => (Vector3.UnitX * new Matrix3(Matrix)).Normalized(); 62 | 63 | public Matrix4 InverseMatrix => 64 | Matrix4.CreateTranslation(-Position) 65 | * Matrix4.CreateFromQuaternion(Rotation.Inverted()) 66 | * Matrix4.CreateScale(Vector3.Divide(Vector3.One, Scale)); 67 | 68 | public Transform Parent { get; set; } 69 | 70 | public Transform() 71 | { 72 | Position = Vector3.Zero; 73 | Scale = Vector3.One; 74 | Rotation = Quaternion.Identity; 75 | Matrix = Matrix4.Identity; 76 | Parent = null; 77 | } 78 | 79 | public Vector3 ToWorld(Vector3 pos) 80 | { 81 | Vector4 worldPos = new Vector4(pos, 1) * Matrix; 82 | return new Vector3(worldPos); 83 | } 84 | 85 | public Vector3 ToLocal(Vector3 pos) 86 | { 87 | Vector4 localPos = new Vector4(pos, 1) * InverseMatrix; 88 | return new Vector3(localPos); 89 | } 90 | 91 | public Transform Translate(Vector3 v) 92 | { 93 | Matrix = Matrix4.CreateTranslation(v) * Matrix; 94 | Position += v; 95 | return this; 96 | } 97 | 98 | public Transform Rescale(Vector3 s) 99 | { 100 | Scale *= s; 101 | Matrix = Matrix4.CreateScale(s) * Matrix; 102 | return this; 103 | } 104 | 105 | public Transform Rotate(Quaternion q, bool local) 106 | { 107 | if (local) 108 | { 109 | Matrix = Matrix * Matrix4.CreateFromQuaternion(q); 110 | Rotation *= q; 111 | } 112 | else 113 | { 114 | Matrix = Matrix4.CreateFromQuaternion(q) * Matrix; 115 | Rotation = q * Rotation; 116 | } 117 | 118 | return this; 119 | } 120 | 121 | public Transform Rotate(Vector3 euler, bool local) 122 | { 123 | Quaternion rot = Quaternion.FromEulerAngles(euler); 124 | return Rotate(rot, local); 125 | } 126 | 127 | public Transform Rotate(float angle, Vector3 axis, bool local) 128 | { 129 | Quaternion rot = Quaternion.FromAxisAngle(axis, angle); 130 | return Rotate(rot, local); 131 | } 132 | 133 | private void recomputeMatrix() 134 | { 135 | Matrix = Matrix4.CreateFromQuaternion(Rotation) * 136 | Matrix4.CreateScale(Scale) * Matrix4.CreateTranslation(Position); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Models/AbstractDocument.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView.Models 2 | { 3 | public abstract class AbstractDocument 4 | { 5 | public abstract T Document { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Models/Dream.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LSDView.Models 4 | { 5 | public class Dream 6 | { 7 | public const int CHUNK_DIMENSION = 20; 8 | 9 | public List Chunks { get; } 10 | public int LevelWidth { get; protected set; } 11 | public List TextureSets { get; } 12 | 13 | public Dream(List chunks, int levelWidth, List textureSets) 14 | { 15 | Chunks = chunks; 16 | TextureSets = textureSets; 17 | LevelWidth = levelWidth; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Models/IDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSDView.Models 4 | { 5 | public interface IDocument 6 | { 7 | DocumentType Type { get; } 8 | EventHandler OnLoad { get; set; } 9 | EventHandler OnUnload { get; set; } 10 | } 11 | 12 | public enum DocumentType 13 | { 14 | TMD, 15 | TIM, 16 | MOM, 17 | TIX, 18 | LBD 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Models/LBDDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using libLSD.Formats; 4 | using LSDView.Graphics; 5 | 6 | namespace LSDView.Models 7 | { 8 | public class LBDDocument : AbstractDocument, IDocument 9 | { 10 | public DocumentType Type => DocumentType.LBD; 11 | 12 | public EventHandler OnLoad { get; set; } 13 | 14 | public EventHandler OnUnload { get; set; } 15 | 16 | public override LBD Document { get; } 17 | 18 | public TMDDocument TilesTMD { get; } 19 | 20 | public List TileLayout { get; } 21 | 22 | public List Entities { get; } 23 | 24 | public LBDDocument(LBD lbd, TMDDocument tilesTmd, List tileLayout, List entities) 25 | { 26 | Document = lbd; 27 | TilesTMD = tilesTmd; 28 | TileLayout = tileLayout; 29 | Entities = entities; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Models/LSDViewConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace LSDView.Models 6 | { 7 | [JsonObject] 8 | public class LSDViewConfig 9 | { 10 | public string GameDataPath 11 | { 12 | get => _gameDataPath; 13 | set 14 | { 15 | _gameDataPath = value; 16 | OnGameDataPathChange?.Invoke(); 17 | } 18 | } 19 | 20 | public List RecentFiles = new List(); 21 | 22 | [JsonIgnore] public Action OnGameDataPathChange; 23 | 24 | private string _gameDataPath = ""; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Models/MOMDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using libLSD.Formats; 3 | 4 | namespace LSDView.Models 5 | { 6 | public class MOMDocument : AbstractDocument, IDocument 7 | { 8 | public override MOM Document { get; } 9 | public DocumentType Type => DocumentType.MOM; 10 | public EventHandler OnLoad { get; set; } 11 | public EventHandler OnUnload { get; set; } 12 | 13 | public TMDDocument Models { get; } 14 | 15 | public MOMDocument(MOM mom, TMDDocument models) 16 | { 17 | Document = mom; 18 | Models = models; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Models/TIMDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using libLSD.Formats; 3 | using LSDView.Graphics; 4 | 5 | namespace LSDView.Models 6 | { 7 | public class TIMDocument : AbstractDocument, IDocument 8 | { 9 | public override TIM Document { get; } 10 | public DocumentType Type => DocumentType.TIM; 11 | public EventHandler OnLoad { get; set; } 12 | public EventHandler OnUnload { get; set; } 13 | 14 | public IRenderable[] TextureMeshes { get; } 15 | 16 | public TIMDocument(TIM tim, IRenderable[] textureMeshes) 17 | { 18 | Document = tim; 19 | TextureMeshes = textureMeshes; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Models/TIXDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using libLSD.Formats; 4 | 5 | namespace LSDView.Models 6 | { 7 | public class TIXDocument : AbstractDocument, IDocument 8 | { 9 | public override TIX Document { get; } 10 | public DocumentType Type => DocumentType.TIX; 11 | public EventHandler OnLoad { get; set; } 12 | public EventHandler OnUnload { get; set; } 13 | 14 | public List TIMs { get; } 15 | 16 | public TIXDocument(TIX tix, List tims) 17 | { 18 | Document = tix; 19 | TIMs = tims; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Models/TMDDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using libLSD.Formats; 4 | using LSDView.Graphics; 5 | 6 | namespace LSDView.Models 7 | { 8 | public class TMDDocument : AbstractDocument, IDocument 9 | { 10 | public override TMD Document { get; } 11 | public DocumentType Type => DocumentType.TMD; 12 | public EventHandler OnLoad { get; set; } 13 | public EventHandler OnUnload { get; set; } 14 | 15 | public List ObjectMeshes { get; } 16 | 17 | public TMDDocument(TMD tmd, List objectMeshes) 18 | { 19 | Document = tmd; 20 | ObjectMeshes = objectMeshes; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /OpenTK.dll.config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/OpenTK.dll.config -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using LSDView.Util; 4 | using Serilog; 5 | using Serilog.Enrichers.WithCaller; 6 | 7 | namespace LSDView 8 | { 9 | public static class Program 10 | { 11 | private static IContainer Container { get; set; } 12 | 13 | private const string LOG_FORMAT = "{Timestamp} <{Caller}> {Level:u} | {Message:lj}{NewLine}{Exception}"; 14 | 15 | public static int Main(string[] args) 16 | { 17 | AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler; 18 | 19 | using (var log = new LoggerConfiguration() 20 | .WriteTo.File("app.log", fileSizeLimitBytes: null, 21 | outputTemplate: LOG_FORMAT) 22 | .Enrich.WithCaller() 23 | .MinimumLevel.ControlledBy(LoggingUtil.LoggingLevelSwitch) 24 | .CreateLogger()) 25 | { 26 | Log.Logger = log; 27 | Log.Information("LSDView has started!"); 28 | 29 | var builder = new ContainerBuilder(); 30 | bool headless = args.Length > 0; 31 | builder.RegisterModule(new ApplicationModule { Headless = headless }); 32 | Container = builder.Build(); 33 | 34 | int exitCode = 0; 35 | using (var scope = Container.BeginLifetimeScope()) 36 | { 37 | if (headless) 38 | { 39 | Log.Information($"Entering batch mode due to presence of {args.Length} args"); 40 | exitCode = new HeadlessApplication(scope).Run(args); 41 | } 42 | else 43 | { 44 | Log.Information("Entering GUI mode"); 45 | new GuiApplication(scope).Run(); 46 | } 47 | } 48 | 49 | return exitCode; 50 | } 51 | } 52 | 53 | 54 | private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) 55 | { 56 | Exception e = (Exception)args.ExceptionObject; 57 | Log.Fatal($"{e.Message}\n{e.StackTrace}"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("LSDView")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Figglewatts")] 11 | [assembly: AssemblyProduct("LSDView")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | // Setting ComVisible to false makes the types in this assembly not visible 16 | // to COM components. If you need to access a type in this assembly from 17 | // COM, set the ComVisible attribute to true on that type. 18 | [assembly: ComVisible(false)] 19 | 20 | // The following GUID is for the ID of the typelib if this project is exposed to COM 21 | [assembly: Guid("0138A340-4992-44EC-9BF6-4812B41A9C4A")] 22 | 23 | // Version information for an assembly consists of the following four values: 24 | // 25 | // Major Version 26 | // Minor Version 27 | // Build Number 28 | // Revision 29 | // 30 | // You can specify all the values or you can default the Build and Revision Numbers 31 | // by using the '*' as shown below: 32 | // [assembly: AssemblyVersion("1.0.*")] 33 | [assembly: AssemblyVersion("1.0.0.0")] 34 | [assembly: AssemblyFileVersion("1.0.0.0")] 35 | [assembly: AssemblyInformationalVersion("#{VERSION}#")] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LSDView 2 | 3 | Data file viewer for LSD Dream Emulator. 4 | 5 | ![Screenshot](img/screenshot.png) 6 | 7 | ## Features 8 | 9 | - View/export 3D models 10 | - View/export levels 11 | - View animations 12 | - View/export textures 13 | - Export 3D models as OBJ or PLY 14 | - Export textures as PNG 15 | - Headless mode for converting entire dreams 16 | 17 | ## User guide 18 | 19 | LSDView can load and view many files from PlayStation 1 game 'LSD: Dream Emulator' 20 | 21 | ### On the first launch 22 | 23 | When you first run LSDView you should set the path to where your LSD data is. You will be given a dialog prompting you 24 | to do this on your first launch of the program. 25 | 26 | ### Formats 27 | 28 | Currently supported file formats are: 29 | 30 | - TMD (PlayStation models) 31 | - Can export to OBJ and PLY 32 | - TIM (PlayStation textures) 33 | - Can export to PNG 34 | - TIX (Archives of TIM files, used to load collections of textures into VRAM) 35 | - Can export all to PNG 36 | - LBD (Sections of levels in LSD, also contains models in level with animations) 37 | - Can export to OBJ and PLY 38 | - MOM (Containers for 3D meshes and their animations) 39 | - Can export models to OBJ and PLY, not animations 40 | 41 | ### Controls 42 | 43 | - The 3D view can be rotated (in an Arcball fashion) by clicking and dragging 44 | - Scroll wheel can be used to zoom in and out 45 | - Click and drag with the right mouse button to pan around 46 | - If you want to recenter the view, there's a button for this in the help menu. 47 | 48 | ### Textures 49 | 50 | When you view a 3D model from a level (STG00 to STG13) initially it will be untextured. To texture it, you need to load 51 | textures into VRAM. To do this, click on the VRAM menu, then 'Load VRAM', and choose a TIX file from the level. Each TIX 52 | file is a different texture set. 53 | 54 | If you load a TIX file from a different level you can emulate the glitch texture set. Try it out! 55 | 56 | ### Headless mode 57 | 58 | If you run LSDView in a terminal with command-line arguments, you can use it 'headless' (i.e. without a graphical 59 | frontend) mode. This is where CPU-intensive tasks can be performed. At the moment there is just one 60 | command, `exportdream`, which can export an entire dream. 61 | 62 | Try it out by opening a terminal and running `LSDView.exe exportdream --help`! 63 | 64 | ### Exporting models 65 | 66 | You can export 3D models from LSDView to OBJ and PLY formats. This includes the LBD level tiles themselves. These 67 | exported models contain UVs into a combined texture atlas for the entire level. 68 | 69 | It's recommended to export to PLY as opposed to OBJ, as the PLY format supports vertex colour data (OBJ does not). 70 | 71 | This texture atlas can be exported from the 'VRAM' menu after loading VRAM. Simply click on the 'VRAM' menu, then 72 | click 'Export VRAM...'. If you use this exported image as the texture for any OBJ files exported from LSDView, then the 73 | textures should be applied correctly. There may be some errors for polygons that use vertex colouring as the OBJ file 74 | format does not support storage of vertex colour information. 75 | 76 | #### OBJ export shading issue (Blender) 77 | 78 | If you export as OBJ and you get shading errors that look like this: 79 | ![Shading issues](img/strange-shading.png) 80 | 81 | Then you can fix this in Blender by pressing this button under 'Object data properties' (with the object selected): 82 | ![Split normals](img/split-normals.png) 83 | 84 | And the shading should now look normal: 85 | ![Fixed shading](img/correct-shading.png) 86 | 87 | ## Development guide 88 | 89 | 1. Clone the repo 90 | 2. Run a NuGet restore 91 | 3. You're good to go 92 | 93 | Make sure to follow the standard C# coding conventions 94 | seen [here.](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions) 95 | Additionally, Line lengths must not exceed 120 characters in length. 96 | -------------------------------------------------------------------------------- /Shaders/basic.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec4 vertexColor; 4 | in vec2 texCoord; 5 | 6 | out vec4 out_FragColor; 7 | 8 | uniform sampler2D uTexture; 9 | uniform vec4 ColorTint = vec4(1.0, 1.0, 1.0, 1.0); 10 | 11 | void main() 12 | { 13 | vec4 texColor = texture(uTexture, texCoord); 14 | vec4 vertColor = vertexColor; 15 | 16 | if (texColor.a < 0.1) 17 | discard; 18 | 19 | out_FragColor = vertexColor * texColor * ColorTint; 20 | } -------------------------------------------------------------------------------- /Shaders/basic.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform mat4 Projection; 4 | uniform mat4 View; 5 | uniform mat4 Model; 6 | 7 | layout (location = 0) in vec3 in_Position; 8 | layout (location = 1) in vec3 in_Normal; 9 | layout (location = 2) in vec2 in_TexCoord; 10 | layout (location = 3) in vec4 in_Color; 11 | 12 | out vec4 vertexColor; 13 | out vec2 texCoord; 14 | 15 | void main() 16 | { 17 | gl_Position = Projection * View * Model * vec4(in_Position, 1.0); 18 | vertexColor = in_Color; 19 | texCoord = in_TexCoord; 20 | } -------------------------------------------------------------------------------- /Shaders/texture.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | out vec4 out_FragColor; 4 | 5 | in vec2 texCoord; 6 | 7 | uniform sampler2D uTexture; 8 | 9 | void main() 10 | { 11 | out_FragColor = texture(uTexture, texCoord); 12 | } -------------------------------------------------------------------------------- /Shaders/texture.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec3 in_Position; 4 | layout (location = 1) in vec3 in_Normal; 5 | layout (location = 2) in vec2 in_TexCoord; 6 | layout (location = 3) in vec4 in_Color; 7 | 8 | out vec2 texCoord; 9 | 10 | void main() 11 | { 12 | gl_Position = vec4(in_Position, 1.0); 13 | texCoord = in_TexCoord; 14 | } -------------------------------------------------------------------------------- /Shaders/untextured.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec4 vertexColor; 4 | 5 | out vec4 out_FragColor; 6 | 7 | void main() 8 | { 9 | vec4 vertColor = vertexColor; 10 | 11 | out_FragColor = vertexColor; 12 | } -------------------------------------------------------------------------------- /Shaders/untextured.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform mat4 Projection; 4 | uniform mat4 View; 5 | uniform mat4 Model; 6 | 7 | layout (location = 0) in vec3 in_Position; 8 | layout (location = 1) in vec3 in_Normal; 9 | layout (location = 2) in vec2 in_TexCoord; 10 | layout (location = 3) in vec4 in_Color; 11 | 12 | out vec4 vertexColor; 13 | 14 | void main() 15 | { 16 | gl_Position = Projection * View * Model * vec4(in_Position, 1.0); 17 | vertexColor = in_Color; 18 | } -------------------------------------------------------------------------------- /Util/ConsoleUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace LSDView.Util 4 | { 5 | public static class ConsoleUtil 6 | { 7 | [DllImport("kernel32.dll")] 8 | public static extern bool AttachConsole(int dwProcessId); 9 | 10 | [DllImport("kernel32.dll")] 11 | public static extern bool FreeConsole(); 12 | 13 | [DllImport("kernel32.dll")] 14 | public static extern bool AllocConsole(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Util/ImageUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Imaging; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace LSDView.Util 6 | { 7 | public static class ImageUtil 8 | { 9 | public static byte[] ImageFloatDataToByteData(float[] image) 10 | { 11 | byte[] byteData = new byte[image.Length]; 12 | 13 | for (int i = 0; i < image.Length; i++) 14 | { 15 | byteData[i] = (byte)(image[i] * 255); 16 | } 17 | 18 | return byteData; 19 | } 20 | 21 | public static byte[] ImageDataToARGBFromRGBA(byte[] image) 22 | { 23 | byte[] argb = new byte[image.Length]; 24 | 25 | for (int i = 0; i < image.Length; i += 4) 26 | { 27 | argb[i] = image[i + 2]; 28 | argb[i + 1] = image[i + 1]; 29 | argb[i + 2] = image[i]; 30 | argb[i + 3] = image[i + 3]; 31 | } 32 | 33 | return argb; 34 | } 35 | 36 | public static Bitmap ImageDataToBitmap(float[] image, int width, int height) 37 | { 38 | byte[] byteColors = ImageDataToARGBFromRGBA(ImageFloatDataToByteData(image)); 39 | 40 | Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); 41 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, 42 | bmp.PixelFormat); 43 | Marshal.Copy(byteColors, 0, bmpData.Scan0, byteColors.Length); 44 | bmp.UnlockBits(bmpData); 45 | 46 | return bmp; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Util/LibLSDUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using libLSD.Formats; 4 | using libLSD.Formats.Packets; 5 | using libLSD.Types; 6 | using LSDView.Graphics; 7 | using LSDView.Graphics.Headless; 8 | using OpenTK; 9 | using Serilog; 10 | 11 | namespace LSDView.Util 12 | { 13 | public static class LibLSDUtil 14 | { 15 | public const int VRAM_WIDTH = 2056; 16 | public const int VRAM_HEIGHT = 512; 17 | public const int TEX_PAGE_WIDTH = 128; 18 | public const int TEX_PAGE_HEIGHT = 256; 19 | public const int TEX_PAGE_PER_ROW = 16; 20 | public const int DOUBLE_BUFFER_WIDTH = TEX_PAGE_WIDTH * 5; 21 | 22 | public static LBD LoadLBD(string filePath) 23 | { 24 | Log.Information($"Loading LBD from: {filePath}"); 25 | 26 | LBD lbd; 27 | using (BinaryReader br = new BinaryReader(File.Open(filePath, FileMode.Open))) 28 | { 29 | lbd = new LBD(br); 30 | } 31 | 32 | Log.Information("Successfully loaded LBD"); 33 | return lbd; 34 | } 35 | 36 | public static MOM LoadMOM(string filePath) 37 | { 38 | Log.Information($"Loading MOM from: {filePath}"); 39 | 40 | MOM mom; 41 | using (BinaryReader br = new BinaryReader(File.Open(filePath, FileMode.Open))) 42 | { 43 | mom = new MOM(br); 44 | } 45 | 46 | Log.Information("Successfully loaded MOM"); 47 | return mom; 48 | } 49 | 50 | public static TIM LoadTIM(string filePath) 51 | { 52 | Log.Information($"Loading TIM from: {filePath}"); 53 | 54 | TIM tim; 55 | using (BinaryReader br = new BinaryReader(File.Open(filePath, FileMode.Open))) 56 | { 57 | tim = new TIM(br); 58 | } 59 | 60 | Log.Information("Successfully loaded TIM"); 61 | return tim; 62 | } 63 | 64 | public static TIX LoadTIX(string filePath) 65 | { 66 | Log.Information($"Loading TIX from: {filePath}"); 67 | 68 | TIX tix; 69 | using (BinaryReader br = new BinaryReader(File.Open(filePath, FileMode.Open))) 70 | { 71 | tix = new TIX(br); 72 | } 73 | 74 | Log.Information("Successfully loaded TIX"); 75 | return tix; 76 | } 77 | 78 | public static TMD LoadTMD(string filePath) 79 | { 80 | Log.Information($"Loading TMD from: {filePath}"); 81 | 82 | TMD tmd; 83 | using (BinaryReader br = new BinaryReader(File.Open(filePath, FileMode.Open))) 84 | { 85 | tmd = new TMD(br); 86 | } 87 | 88 | Log.Information("Successfully loaded TMD"); 89 | return tmd; 90 | } 91 | 92 | public static List CreateMeshesFromTMD(TMD tmd, Shader shader, ITexture2D vram, bool headless) 93 | { 94 | List meshList = new List(); 95 | 96 | foreach (var obj in tmd.ObjectTable) 97 | { 98 | IRenderable objMesh = MeshFromTMDObject(obj, shader, headless); 99 | objMesh.Textures.Add(vram); 100 | meshList.Add(objMesh); 101 | } 102 | 103 | return meshList; 104 | } 105 | 106 | public static IRenderable MeshFromTMDObject(TMDObject obj, Shader shader, bool headless) 107 | { 108 | Vec3[] verts = new Vec3[obj.NumVertices]; 109 | List vertList = new List(); 110 | List indices = new List(); 111 | 112 | for (int i = 0; i < obj.NumVertices; i++) 113 | { 114 | verts[i] = obj.Vertices[i] / 2048f; 115 | } 116 | 117 | foreach (var prim in obj.Primitives) 118 | { 119 | if (prim.Type != TMDPrimitivePacket.Types.POLYGON) 120 | continue; 121 | 122 | ITMDPrimitivePacket primitivePacket = prim.PacketData; 123 | ITMDTexturedPrimitivePacket texPrimitivePacket = prim.PacketData as ITMDTexturedPrimitivePacket; 124 | ITMDColoredPrimitivePacket colPrimitivePacket = prim.PacketData as ITMDColoredPrimitivePacket; 125 | ITMDLitPrimitivePacket litPrimitivePacket = prim.PacketData as ITMDLitPrimitivePacket; 126 | 127 | List polyIndices = new List(); 128 | int[] packetIndices = new int[primitivePacket.Vertices.Length]; 129 | for (int i = 0; i < primitivePacket.Vertices.Length; i++) 130 | { 131 | int vertIndex = primitivePacket.Vertices[i]; 132 | packetIndices[i] = vertList.Count; 133 | 134 | Vector3 vertPos = new Vector3(verts[vertIndex].X, verts[vertIndex].Y, verts[vertIndex].Z); 135 | Vector4 vertCol = Vector4.One; 136 | Vector3 vertNorm = Vector3.Zero; 137 | Vector2 vertUV = Vector2.One; 138 | 139 | // handle packet colour 140 | if (colPrimitivePacket != null) 141 | { 142 | Vec3 packetVertCol = colPrimitivePacket.Colors[colPrimitivePacket.Colors.Length > 1 ? i : 0]; 143 | vertCol = new Vector4(packetVertCol.X, packetVertCol.Y, packetVertCol.Z, 1f); 144 | } 145 | 146 | // handle packet normals 147 | if (litPrimitivePacket != null) 148 | { 149 | TMDNormal packetVertNorm = 150 | obj.Normals[ 151 | litPrimitivePacket.Normals[litPrimitivePacket.Normals.Length > 1 ? i : 0]]; 152 | vertNorm = new Vector3(packetVertNorm.X, packetVertNorm.Y, 153 | packetVertNorm.Z); 154 | } 155 | 156 | // handle packet UVs 157 | if (texPrimitivePacket != null) 158 | { 159 | int texPage = texPrimitivePacket.Texture.TexturePageNumber; 160 | 161 | // the PSX VRAM is split into 32 texture pages, 16 on top and 16 on bottom 162 | // a texture page is 128x256 pixels large 163 | // pages 0-4 and 16-20 are used as a double buffer, with a width of 640 (5*128) 164 | 165 | // the X position of the texture page is it's row index multiplied by the width 166 | // we're also subtracting the width of the double buffer as we don't want to include 167 | // it - we're using modern graphics so this will be on the video card! 168 | int texPageXPos = ((texPage % TEX_PAGE_PER_ROW) * TEX_PAGE_WIDTH) - DOUBLE_BUFFER_WIDTH; 169 | 170 | // more simply, the Y position of the texture page is 0 or the height of a texture page, based 171 | // on if the texture page number is on the 2nd row of pages or not 172 | int texPageYPos = texPage <= TEX_PAGE_PER_ROW ? TEX_PAGE_HEIGHT : 0; 173 | 174 | int uvIndex = i * 2; // 2 UVs per vertex 175 | 176 | // the UV information we get from the TMD model is UVs into a specific texture page, using 177 | // only that texture page's coordinate system 178 | // here, we're adding the texture page position offsets (into VRAM) to the TMD UVs, transforming 179 | // them into the VRAM coordinate space 180 | int vramXPos = texPageXPos + texPrimitivePacket.UVs[uvIndex]; 181 | 182 | // we're subtracting from the texture page height for the Y position as OpenGL uses flipped 183 | // Y coordinates for textures compared with the PSX 184 | int vramYPos = texPageYPos + (TEX_PAGE_HEIGHT - texPrimitivePacket.UVs[uvIndex + 1]); 185 | 186 | // finally, we're normalizing the UVs from pixels 187 | float uCoord = vramXPos / (float)VRAM_WIDTH; 188 | float vCoord = vramYPos / (float)VRAM_HEIGHT; 189 | 190 | vertUV = new Vector2(uCoord, vCoord); 191 | } 192 | 193 | vertList.Add(new Vertex(vertPos, vertNorm, vertUV, vertCol)); 194 | } 195 | 196 | bool isQuad = (prim.Options & TMDPrimitivePacket.OptionsFlags.Quad) != 0; 197 | 198 | polyIndices.Add(packetIndices[1]); 199 | polyIndices.Add(packetIndices[0]); 200 | polyIndices.Add(packetIndices[2]); 201 | 202 | if (isQuad) 203 | { 204 | polyIndices.Add(packetIndices[1]); 205 | polyIndices.Add(packetIndices[2]); 206 | polyIndices.Add(packetIndices[3]); 207 | } 208 | 209 | // if primitive is double sided poly we need to add other side with reverse winding 210 | if ((prim.Flags & TMDPrimitivePacket.PrimitiveFlags.DoubleSided) != 0) 211 | { 212 | polyIndices.Add(packetIndices[0]); 213 | polyIndices.Add(packetIndices[1]); 214 | polyIndices.Add(packetIndices[2]); 215 | 216 | if (isQuad) 217 | { 218 | polyIndices.Add(packetIndices[2]); 219 | polyIndices.Add(packetIndices[1]); 220 | polyIndices.Add(packetIndices[3]); 221 | } 222 | } 223 | 224 | indices.AddRange(polyIndices); 225 | } 226 | 227 | return headless 228 | ? (IRenderable)new HeadlessMesh(vertList.ToArray(), indices.ToArray()) 229 | : (IRenderable)new Mesh(vertList.ToArray(), indices.ToArray(), shader); 230 | } 231 | 232 | public static List CreateLBDTileMesh(LBDTile tile, 233 | LBDTile[] extraTiles, 234 | int x, 235 | int y, 236 | TMD tilesTmd, 237 | Shader shader, 238 | ITexture2D vram, 239 | bool headless) 240 | { 241 | List returnMeshList = new List(); 242 | returnMeshList.Add(createSingleLBDTileMesh(tile, x, y, tilesTmd, shader, vram, headless)); 243 | 244 | LBDTile currentTile = tile; 245 | int i = 0; 246 | while (currentTile.ExtraTileIndex >= 0 && i <= 1) 247 | { 248 | LBDTile extraTile = extraTiles[currentTile.ExtraTileIndex]; 249 | returnMeshList.Add(createSingleLBDTileMesh(extraTile, x, y, tilesTmd, shader, vram, headless)); 250 | currentTile = extraTile; 251 | i++; 252 | } 253 | 254 | return returnMeshList; 255 | } 256 | 257 | private static IRenderable createSingleLBDTileMesh(LBDTile tile, 258 | int x, 259 | int y, 260 | TMD tilesTmd, 261 | Shader shader, 262 | ITexture2D vram, 263 | bool headless) 264 | { 265 | TMDObject tileObj = tilesTmd.ObjectTable[tile.TileType]; 266 | IRenderable tileMesh = MeshFromTMDObject(tileObj, shader, headless); 267 | 268 | switch (tile.TileDirection) 269 | { 270 | case LBDTile.TileDirections.Deg90: 271 | { 272 | tileMesh.Transform.Rotation = 273 | Quaternion.FromAxisAngle(Vector3.UnitY, MathHelper.DegreesToRadians(90)); 274 | break; 275 | } 276 | case LBDTile.TileDirections.Deg180: 277 | { 278 | tileMesh.Transform.Rotation = 279 | Quaternion.FromAxisAngle(Vector3.UnitY, MathHelper.DegreesToRadians(180)); 280 | break; 281 | } 282 | case LBDTile.TileDirections.Deg270: 283 | { 284 | tileMesh.Transform.Rotation = 285 | Quaternion.FromAxisAngle(Vector3.UnitY, MathHelper.DegreesToRadians(270)); 286 | break; 287 | } 288 | } 289 | 290 | tileMesh.Transform.Position = new Vector3(x, tile.TileHeight, y); 291 | 292 | tileMesh.Textures.Add(vram); 293 | 294 | return tileMesh; 295 | } 296 | 297 | public static ITexture2D TIXToTexture2D(TIX tix, bool headless, bool flip = true) 298 | { 299 | float[] texData = new float[VRAM_WIDTH * VRAM_HEIGHT * 4]; 300 | ITexture2D tex = headless 301 | ? (ITexture2D)new HeadlessTexture2D(VRAM_WIDTH, VRAM_HEIGHT, texData) 302 | : (ITexture2D)new Texture2D(VRAM_WIDTH, VRAM_HEIGHT, texData); 303 | TIXToTexture2D(tix, ref tex, flip); 304 | return tex; 305 | } 306 | 307 | public static void TIXToTexture2D(TIX tix, ref ITexture2D tex, bool flip = true) 308 | { 309 | foreach (var chunk in tix.Chunks) 310 | { 311 | foreach (var tim in chunk.TIMs) 312 | { 313 | var image = GetImageDataFromTIM(tim, flip: flip); 314 | 315 | int actualXPos = (tim.PixelData.XPosition - 320) * 2; 316 | int actualYPos = 512 - tim.PixelData.YPosition - image.height; 317 | Log.Debug( 318 | $"[{tim.PixelData.XPosition}, {tim.PixelData.YPosition}] -> ({actualXPos}, {actualYPos})"); 319 | 320 | tex.SubImage(image.data, actualXPos, actualYPos, image.width, image.height); 321 | } 322 | } 323 | } 324 | 325 | public static (float[] data, int width, int height) GetImageDataFromTIM(TIM tim, 326 | int clutIndex = 0, 327 | bool flip = true) 328 | { 329 | IColor[,] imageColors = tim.GetImage(clutIndex); 330 | int width = imageColors.GetLength(1); 331 | int height = imageColors.GetLength(0); 332 | float[] imageData = ImageColorsToData(imageColors, width, height, flip); 333 | return (imageData, width, height); 334 | } 335 | 336 | public static float[] ImageColorsToData(IColor[,] imageColors, int width, int height, bool flip = true) 337 | { 338 | float[] imageData = new float[imageColors.Length * 4]; 339 | 340 | int i = 0; 341 | if (flip) 342 | { 343 | for (int y = height - 1; y >= 0; y--) 344 | { 345 | for (int x = 0; x < width; x++) 346 | { 347 | IColor col = imageColors[y, x]; 348 | SetImageColors(ref i, col, ref imageData); 349 | } 350 | } 351 | } 352 | else 353 | { 354 | for (int y = 0; y < height; y++) 355 | { 356 | for (int x = 0; x < width; x++) 357 | { 358 | IColor col = imageColors[y, x]; 359 | SetImageColors(ref i, col, ref imageData); 360 | } 361 | } 362 | } 363 | 364 | return imageData; 365 | } 366 | 367 | private static void SetImageColors(ref int i, IColor col, ref float[] imageData) 368 | { 369 | imageData[i] = col.Red; 370 | imageData[i + 1] = col.Green; 371 | imageData[i + 2] = col.Blue; 372 | imageData[i + 3] = col.Alpha; 373 | i += 4; 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /Util/LoggingUtil.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | 3 | namespace LSDView.Util 4 | { 5 | public static class LoggingUtil 6 | { 7 | public static readonly LoggingLevelSwitch LoggingLevelSwitch = new LoggingLevelSwitch(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Util/MeshUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using LSDView.Graphics; 4 | using OpenTK; 5 | 6 | namespace LSDView.Util 7 | { 8 | public static class MeshUtil 9 | { 10 | public static string RenderableToObjFile(IRenderable mesh) 11 | { 12 | int vertCount = mesh.Verts.Vertices.Length; 13 | int triCount = mesh.Verts.Tris; 14 | 15 | ObjBuilder objString = new ObjBuilder(); 16 | WriteOBJHeader(objString, vertCount, triCount); 17 | 18 | // fix OBJ export transformation 19 | Matrix4 rotateX180 = Matrix4.CreateRotationX(MathHelper.DegreesToRadians(-180)); 20 | 21 | foreach (var vert in mesh.Verts.Vertices) 22 | { 23 | objString.Vertex(Vector3.TransformPosition(vert.Position, mesh.Transform.Matrix * rotateX180)); 24 | } 25 | 26 | foreach (var vert in mesh.Verts.Vertices) 27 | { 28 | objString.Normal(vert.Normal); 29 | } 30 | 31 | foreach (var vert in mesh.Verts.Vertices) 32 | { 33 | objString.UV(vert.TexCoord); 34 | } 35 | 36 | ObjFaceBuilder faceBuilder = new ObjFaceBuilder(); 37 | for (int i = 0; i < mesh.Verts.Indices.Length; i += 3) 38 | { 39 | // plus 1 here as OBJ is not zero-indexed 40 | int idx1 = mesh.Verts.Indices[i] + 1; 41 | int idx2 = mesh.Verts.Indices[i + 1] + 1; 42 | int idx3 = mesh.Verts.Indices[i + 2] + 1; 43 | faceBuilder.Vertex(idx1, idx1, idx1); 44 | faceBuilder.Vertex(idx2, idx2, idx2); 45 | faceBuilder.Vertex(idx3, idx3, idx3); 46 | objString.Face(faceBuilder.Build()); 47 | faceBuilder.Clear(); 48 | } 49 | 50 | return objString.ToString(); 51 | } 52 | 53 | public static string RenderableListToObjFile(IEnumerable meshes, bool combine = false) 54 | { 55 | int vertCount = 0; 56 | int triCount = 0; 57 | IEnumerable renderables = meshes as IRenderable[] ?? meshes.ToArray(); 58 | foreach (var mesh in renderables) 59 | { 60 | vertCount += mesh.Verts.Vertices.Length; 61 | triCount += mesh.Verts.Tris; 62 | } 63 | 64 | ObjBuilder objString = new ObjBuilder(); 65 | WriteOBJHeader(objString, vertCount, triCount); 66 | 67 | List positions = new List(); 68 | List normals = new List(); 69 | List uvs = new List(); 70 | 71 | // fix OBJ export transformation 72 | Matrix4 rotateX180 = Matrix4.CreateRotationX(MathHelper.DegreesToRadians(-180)); 73 | 74 | foreach (var mesh in renderables) 75 | { 76 | foreach (var vert in mesh.Verts.Vertices) 77 | { 78 | positions.Add(Vector3.TransformPosition(vert.Position, mesh.Transform.Matrix * rotateX180)); 79 | normals.Add(vert.Normal); 80 | uvs.Add(vert.TexCoord); 81 | } 82 | } 83 | 84 | foreach (var pos in positions) 85 | { 86 | objString.Vertex(pos); 87 | } 88 | 89 | foreach (var norm in normals) 90 | { 91 | objString.Normal(norm); 92 | } 93 | 94 | foreach (var uv in uvs) 95 | { 96 | objString.UV(uv); 97 | } 98 | 99 | int faceBase = 0; 100 | int meshNumber = 0; 101 | foreach (var mesh in renderables) 102 | { 103 | // if we're not combining the meshes then we need to make groups for each 104 | if (!combine) objString.Group($"Mesh {meshNumber}"); 105 | 106 | ObjFaceBuilder faceBuilder = new ObjFaceBuilder(); 107 | for (int i = 0; i < mesh.Verts.Length; i += 3) 108 | { 109 | int objIndex1 = faceBase + mesh.Verts.Indices[i] + 1; 110 | int objIndex2 = faceBase + mesh.Verts.Indices[i + 1] + 1; 111 | int objIndex3 = faceBase + mesh.Verts.Indices[i + 2] + 1; 112 | faceBuilder.Vertex(objIndex1, objIndex1, objIndex1); 113 | faceBuilder.Vertex(objIndex2, objIndex2, objIndex2); 114 | faceBuilder.Vertex(objIndex3, objIndex3, objIndex3); 115 | objString.Face(faceBuilder.Build()); 116 | faceBuilder.Clear(); 117 | } 118 | 119 | meshNumber++; 120 | faceBase += mesh.Verts.Vertices.Length; 121 | } 122 | 123 | return objString.ToString(); 124 | } 125 | 126 | 127 | public static string RenderableToPlyFile(IRenderable mesh) 128 | { 129 | int vertCount = mesh.Verts.Vertices.Length; 130 | int triCount = mesh.Verts.Tris; 131 | 132 | PlyBuilder plyString = new PlyBuilder(); 133 | plyString.WriteHeader(vertCount, triCount); 134 | 135 | // Multply the transform matrix by -90 along x to Fix PLY Co-ord Space 136 | Matrix4 rotateX90 = Matrix4.CreateRotationX(MathHelper.DegreesToRadians(-90)); 137 | 138 | // Vertex Elements 139 | foreach (var vert in mesh.Verts.Vertices) 140 | { 141 | plyString.BuildVertexElement( 142 | Vector3.TransformPosition(vert.Position, 143 | mesh.Transform.Matrix * rotateX90), 144 | vert.Normal, vert.TexCoord, vert.Color); 145 | } 146 | 147 | // Face Indices 148 | for (var i = 0; i < mesh.Verts.Indices.Length; i += 3) 149 | { 150 | int idx1 = mesh.Verts.Indices[i]; 151 | int idx2 = mesh.Verts.Indices[i + 1]; 152 | int idx3 = mesh.Verts.Indices[i + 2]; 153 | plyString.BuildFaceElement(idx1, idx2, idx3); 154 | } 155 | 156 | 157 | return plyString.ToString(); 158 | } 159 | 160 | public static string RenderableListToPlyFile(IEnumerable meshes) 161 | { 162 | int vertCount = 0; 163 | int triCount = 0; 164 | IEnumerable renderables = meshes as IRenderable[] ?? meshes.ToArray(); 165 | foreach (var mesh in renderables) 166 | { 167 | vertCount += mesh.Verts.Vertices.Length; 168 | triCount += mesh.Verts.Tris; 169 | } 170 | 171 | PlyBuilder plyString = new PlyBuilder(); 172 | plyString.WriteHeader(vertCount, triCount); 173 | 174 | // Multply the transform matrix by -90 along x to Fix PLY Co-ord Space 175 | Matrix4 rotateX90 = Matrix4.CreateRotationX(MathHelper.DegreesToRadians(-90)); 176 | 177 | // Vertex Elements 178 | foreach (var mesh in renderables) 179 | { 180 | foreach (var vert in mesh.Verts.Vertices) 181 | { 182 | plyString.BuildVertexElement( 183 | Vector3.TransformPosition(vert.Position, 184 | mesh.Transform.Matrix * rotateX90), 185 | vert.Normal, vert.TexCoord, vert.Color); 186 | } 187 | } 188 | 189 | // Face Indices 190 | int faceBase = 0; 191 | foreach (var mesh in renderables) 192 | { 193 | for (var i = 0; i < mesh.Verts.Indices.Length; i += 3) 194 | { 195 | int idx1 = faceBase + mesh.Verts.Indices[i]; 196 | int idx2 = faceBase + mesh.Verts.Indices[i + 1]; 197 | int idx3 = faceBase + mesh.Verts.Indices[i + 2]; 198 | plyString.BuildFaceElement(idx1, idx2, idx3); 199 | } 200 | 201 | faceBase += mesh.Verts.Vertices.Length; 202 | } 203 | 204 | return plyString.ToString(); 205 | } 206 | 207 | 208 | private static void WriteOBJHeader(ObjBuilder builder, int verts, int faces) 209 | { 210 | builder.Comment($"Generated by LSDView {Version.String}, created by Figglewatts, 2020"); 211 | builder.Comment("https://github.com/Figglewatts/LSDView"); 212 | builder.Comment($"Vertices: {verts}"); 213 | builder.Comment($"Faces: {faces}"); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Util/ObjBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using OpenTK; 4 | 5 | namespace LSDView.Util 6 | { 7 | public class ObjBuilder 8 | { 9 | private readonly StringBuilder _builder; 10 | 11 | public ObjBuilder() { _builder = new StringBuilder(); } 12 | 13 | public ObjBuilder Comment(string comment) 14 | { 15 | _builder.AppendLine($"# {comment}"); 16 | return this; 17 | } 18 | 19 | public ObjBuilder Vertex(Vector3 pos) 20 | { 21 | _builder.AppendLine($"v {pos.X} {pos.Y} {pos.Z}"); 22 | return this; 23 | } 24 | 25 | public ObjBuilder Normal(Vector3 normal) 26 | { 27 | _builder.AppendLine($"vn {normal.X} {normal.Y} {normal.Z}"); 28 | return this; 29 | } 30 | 31 | public ObjBuilder UV(Vector2 uv) 32 | { 33 | _builder.AppendLine($"vt {uv.X} {uv.Y}"); 34 | return this; 35 | } 36 | 37 | public ObjBuilder Group(string name) 38 | { 39 | _builder.AppendLine($"g {name}"); 40 | return this; 41 | } 42 | 43 | public ObjBuilder Face(ObjFaceBuilder.ObjFace face) 44 | { 45 | _builder.Append("f "); 46 | for (int i = 0; i < face.Verts.Length; i++) 47 | { 48 | var f = face[i]; 49 | _builder.Append($"{f.v}"); 50 | if (f.t != -1) 51 | { 52 | _builder.Append($"/{f.t}"); 53 | } 54 | 55 | if (f.n != -1) 56 | { 57 | _builder.Append($"/{f.n}"); 58 | } 59 | 60 | _builder.Append(" "); 61 | } 62 | 63 | _builder.AppendLine(); 64 | return this; 65 | } 66 | 67 | public override string ToString() { return _builder.ToString(); } 68 | } 69 | 70 | public class ObjFaceBuilder 71 | { 72 | public List Vertices { get; } 73 | public List Normals { get; } 74 | public List UVs { get; } 75 | 76 | public ObjFaceBuilder() 77 | { 78 | Vertices = new List(); 79 | Normals = new List(); 80 | UVs = new List(); 81 | } 82 | 83 | public ObjFaceBuilder Vertex(int v, int? n = null, int? t = null) 84 | { 85 | Vertices.Add(v); 86 | Normals.Add(n ?? -1); 87 | UVs.Add(t ?? -1); 88 | return this; 89 | } 90 | 91 | public ObjFaceBuilder Clear() 92 | { 93 | Vertices.Clear(); 94 | Normals.Clear(); 95 | UVs.Clear(); 96 | return this; 97 | } 98 | 99 | public ObjFace Build() { return new ObjFace(this); } 100 | 101 | public class ObjFace 102 | { 103 | public int[] Verts { get; } 104 | public int[] Normals { get; } 105 | public int[] UVs { get; } 106 | 107 | public ObjFace(ObjFaceBuilder builder) 108 | { 109 | Verts = builder.Vertices.ToArray(); 110 | Normals = builder.Normals.ToArray(); 111 | UVs = builder.UVs.ToArray(); 112 | } 113 | 114 | public (int v, int n, int t) this[int i] => (Verts[i], Normals[i], UVs[i]); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Util/PathUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSDView.Util 4 | { 5 | public static class PathUtil 6 | { 7 | public static string MakeRelative(string fullPath, string relativeRoot) 8 | { 9 | var fullUri = new Uri(fullPath); 10 | var relativeRootUri = new Uri(relativeRoot); 11 | 12 | return relativeRootUri.MakeRelativeUri(fullUri).ToString(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Util/PlyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using LSDView.Math; 3 | using OpenTK; 4 | 5 | namespace LSDView.Util 6 | { 7 | public class PlyBuilder 8 | { 9 | private readonly StringBuilder _builder; 10 | 11 | public PlyBuilder() { _builder = new StringBuilder(); } 12 | 13 | public PlyBuilder WriteHeader(int verts, int faces) 14 | { 15 | _builder.AppendLine($"ply\nformat ascii 1.0\n" + // Required header data 16 | // The first two lines of any PLY file *MUST* be "ply", followed by the format. 17 | $"comment Generated by LSDView {Version.String}, created by Figglewatts, 2020\n" + 18 | $"comment LSDView may be found at https://github.com/Figglewatts/LSDView \n" + 19 | $"element vertex {verts}\n" + // Vertex Count 20 | $"property float x\nproperty float y\nproperty float z\n" + // Vertex Positions 21 | $"property float nx\nproperty float ny\nproperty float nz\n" + // Vertex Normals 22 | $"property float s\nproperty float t\n" + // Scale 23 | $"property uchar red\nproperty uchar green\nproperty uchar blue\nproperty uchar a\n" + // Vertex Colors 24 | $"element face {faces}\nproperty list uchar uint vertex_indices\n" + // Face indices 25 | $"end_header"); // Yes, this looks messy. This is just how PLY works 26 | return this; 27 | } 28 | 29 | public PlyBuilder BuildVertexElement(Vector3 pos, Vector3 normal, Vector2 uv, Vector4 col) 30 | { 31 | _builder.AppendLine($"{pos.X} {pos.Y} {pos.Z}" + 32 | $" {normal.X} {normal.Y} {normal.Z}" + 33 | $" {uv.X} {uv.Y}" + 34 | $" {Convert.ToByte(col.X)} {Convert.ToByte(col.Y)} {Convert.ToByte(col.Z)} {Convert.ToByte(col.W)}"); 35 | return this; 36 | } 37 | 38 | public PlyBuilder BuildFaceElement(int vert1, int vert2, int vert3) 39 | { 40 | // the first number is how many vertices there are per face. Manually assigning 3 to make it all triangles. 41 | _builder.AppendLine($"3 {vert1} {vert2} {vert3}"); 42 | 43 | return this; 44 | } 45 | 46 | public PlyBuilder Comment(string comment) 47 | { 48 | _builder.AppendLine($"comment {comment}"); 49 | return this; 50 | } 51 | 52 | public override string ToString() { return _builder.ToString(); } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Version.cs: -------------------------------------------------------------------------------- 1 | namespace LSDView 2 | { 3 | public static class Version 4 | { 5 | public static string String = "#{VERSION}#"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/appicon.ico -------------------------------------------------------------------------------- /img/correct-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/img/correct-shading.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/img/screenshot.png -------------------------------------------------------------------------------- /img/split-normals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/img/split-normals.png -------------------------------------------------------------------------------- /img/strange-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figglewatts/LSDView/168adbc970a30f572472b2a07181166d198ce1af/img/strange-shading.png -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------