├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.jp.md ├── README.md ├── build ├── .gitignore ├── nsis │ ├── favicon.ico │ ├── setup.nsi │ └── tools │ │ ├── certmgr.exe │ │ ├── makensis.exe │ │ ├── msixexec.exe │ │ ├── plugins │ │ ├── System.dll │ │ └── nsExec.dll │ │ └── stubs │ │ ├── uninst │ │ ├── zlib │ │ └── zlib_solid └── nsis_build.bat ├── screen-shot ├── demo-01.png └── demo-02.png └── src ├── .editorconfig ├── .gitignore ├── GenshinModelViewer.Logging ├── .gitignore ├── GenshinModelViewer.Logging.csproj └── Logger.cs ├── GenshinModelViewer.Packaging ├── .gitignore ├── GenshinModelViewer.Packaging.wapproj ├── GenshinModelViewer.Packaging.wapproj.user ├── GenshinModelViewer.Packaging_TemporaryKey.pfx ├── Images │ ├── LockScreenLogo.scale-200.png │ ├── SplashScreen.scale-200.png │ ├── Square150x150Logo.scale-200.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── StoreLogo.png │ └── Wide310x150Logo.scale-200.png └── Package.appxmanifest ├── GenshinModelViewer.sln └── GenshinModelViewer ├── .gitignore ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── Core ├── BitmapUtils.cs ├── BrushAnimation.cs ├── ElementHelper.cs ├── FullScreenHandler.cs ├── MMDFormat.cs ├── PMXLoaderScript.cs ├── PMXProvider.cs ├── PfimxUtils.cs ├── SevenZipHelper.cs ├── SevenZipStock.cs └── StoryboardUtils.cs ├── GenshinModelViewer.csproj ├── GenshinModelViewer.csproj.user ├── GlobalUsings.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Models └── ForDispatcher.cs ├── Properties └── PublishProfiles │ ├── FolderProfile.pubxml │ └── FolderProfile.pubxml.user ├── Resources ├── 7z.dll ├── Resources.xaml ├── UI_AvatarIcon_Side_Yunjin.ico ├── UI_AvatarIcon_Side_Yunjin.png ├── fonts │ ├── GenshinIcons │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── GenshinIcons.json │ │ ├── demo-files │ │ │ ├── demo.css │ │ │ └── demo.js │ │ ├── demo.html │ │ ├── fonts │ │ │ ├── GenshinIcons.eot │ │ │ ├── GenshinIcons.svg │ │ │ ├── GenshinIcons.ttf │ │ │ └── GenshinIcons.woff │ │ └── style.css │ ├── KhaenriahNeue-Chasm-2.000.otf │ ├── README.md │ ├── Segoe Fluent Icons.ttf │ └── Segoe MDL2 Assets.ttf └── svgs │ ├── anemo.svg │ ├── cryo.svg │ ├── dendro.svg │ ├── electro.svg │ ├── geo.svg │ ├── hydro.svg │ └── pyro.svg └── Views ├── HelixViewer.xaml ├── HelixViewer.xaml.cs ├── MessageBox ├── MessageDialog.xaml └── MessageDialog.xaml.cs ├── ModelSelectionDialog.xaml ├── ModelSelectionDialog.xaml.cs └── UserControl └── ObservableUserControl.cs /.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 | *.7z 7 | models/* 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Genshin Model Viewer 2 | 3 | ## Changelog 4 | 5 | v1.1.0 6 | 7 | - Updated to `.NET 6`. 8 | - Fixed crash when loading archive file. 9 | 10 | v1.0.0 11 | 12 | - First edition publish. 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | ・[English](README.en.md) ・[中文](README.md) ・[日本語](README.jp.md) 2 | 3 | # Genshin Model Viewer 4 | 5 | It also supports the preview of DMM models in other PMX formats. 6 | 7 | > Attributes: Support arbitrary rotation and translation! Support model threading up! 8 | 9 | ## Model format 10 | 11 | > Support model file format: `*.pmx` `*.7z` `*.zip` 12 | 13 | Tips: The archive type will automatically import the first PMX file in non decompression mode. 14 | 15 | > Support model file ref texture formats:`*.png` `*.bmp` `*.dds` `*.tga` etc.. 16 | 17 | [Model Download](https://www.aplaybox.com/u/680828836) 18 | 19 | ## Usages 20 | 21 | ![demo2](screen-shot/demo-01.png) 22 | 23 | ![demo1](screen-shot/demo-02.png) 24 | 25 | > Click to select the model file or drag the file to the window. 26 | 27 | | Input | Operation | 28 | | ------------ | ---------- | 29 | | Left Mouse | Pan | 30 | | Middle Mouse | Pan & Zoom | 31 | | Right Mouse | Rotate | 32 | 33 | ## Runtime Environment 34 | 35 | `Win10 x64` `.NET 6` 36 | 37 | ## Download 38 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/total)](https://github.com/emako/genshin-model-viewer/releases) 39 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/latest/total)](https://github.com/emako/genshin-model-viewer/releases) 40 | 41 | > Download page: https://github.com/emako/genshin-model-viewer/releases 42 | > 43 | 44 | ## TODO 45 | 46 | - [ ] Support importing folder. 47 | 48 | 49 | - [ ] Supports multiple PMX files when importing folders or archive files, and provides options. 50 | - [ ] Supports full screen mode and rewriting the form title bar. 51 | - [ ] Supports more interactive ui buttons. 52 | 53 | ## FAQs 54 | 55 | > Texture error after importing model? 56 | 57 | - Texture added with the Sphere algorithm are not supported. At the same time, the DMM model officially released by the original Genshin Impact not use Sphere. 58 | 59 | --- 60 | 61 | Welcome to propose [Issues](https://github.com/emako/genshin-model-viewer/issues). 62 | 63 | ## Changelog 64 | 65 | [CHANGELOG.md](CHANGELOG.md) 66 | 67 | ## Dependency 68 | 69 | - [HelixToolkit](https://github.com/helix-toolkit/helix-toolkit) 70 | - [QuickLook.Plugin.HelixViewer](https://github.com/ShiinaManatsu/QuickLook.Plugin.HelixViewer) 71 | 72 | - [Pfim](https://github.com/nickbabcock/Pfim) 73 | 74 | - [SevenZipSharp](https://github.com/squid-box/SevenZipSharp) 75 | - [7-Zip](https://www.7-zip.org/) 76 | 77 | ## License 78 | 79 | [LICENSE](LICENSE) 80 | 81 | -------------------------------------------------------------------------------- /README.jp.md: -------------------------------------------------------------------------------- 1 | ・[English](README.en.md) ・[中文](README.md) ・[日本語](README.jp.md) 2 | 3 | # 原神モデルビューア 4 | 5 | 他のPMX形式DMMモデルのプレビューもサポートされています。 6 | 7 | > 目玉:任意回転移動可!モデル内部観察可! 8 | 9 | ## モデルのフォーマット 10 | 11 | > サポートされているモデルのフォーマット: `*.pmx` `*.7z` `*.zip` 12 | 13 | ※アーカイブタイプのファイルは自動的に非解凍方式を最初の`*.pmx`の方を導入する。 14 | 15 | > サポートされているモデルが引用するテクスチャのフォーマット:`*.png` `*.bmp` `*.dds` `*.tga` など。 16 | 17 | [モデルのダウンロード](https://www.aplaybox.com/u/680828836) 18 | 19 | ## 使い方 20 | 21 | ![demo2](screen-shot/demo-01.png) 22 | 23 | ![demo1](screen-shot/demo-02.png) 24 | 25 | > モデルファイルを選択する、又はウィンドウにドラッグ&ドロップして始まる。 26 | 27 | | ソースインプット | 效果 | 28 | | ---------------------- | --------------------------- | 29 | | マウスの左ボタン | トランスレーション | 30 | | マウスの真ん中のボタン | トランスレーション & ズーム | 31 | | マウスの右ボタン | 回転 | 32 | 33 | ## 実行環境 34 | 35 | `Win10 x64` `.NET 6` 36 | 37 | ## ダウンロード 38 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/total)](https://github.com/emako/genshin-model-viewer/releases) 39 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/latest/total)](https://github.com/emako/genshin-model-viewer/releases) 40 | 41 | > ダウンロードページ:https://github.com/emako/genshin-model-viewer/releases 42 | > 43 | 44 | ## タスク 45 | 46 | - [ ] インポートフォルダのサポート。 47 | 48 | 49 | - [ ] フォルダまたはアーカイブのインポート時に複数のpmxファイルの中から1つ選択できる機能。 50 | - [ ] フルスクリーンモードとWindowのタイトルバーのリペイントをサポート。 51 | - [ ] 更なるUIボタンの追加。 52 | 53 | ## よくある質問 54 | 55 | > モデルのとある部分のテクスチャのレンダリング異常? 56 | 57 | - Sphereアルゴリズムが組み込まれたモデルはサポートされていません。この場合はモデルのテクスチャ異常として表現されますが、現在原神公式リリースされているDMMモデルではこのような方法は使用されていないので、原神のモデルにはこのような問題はないはずです。 58 | 59 | --- 60 | 61 | [Issues](https://github.com/emako/genshin-model-viewer/issues) へようこそ。 62 | 63 | ## 変更履歴 64 | 65 | [CHANGELOG.md](CHANGELOG.md) 66 | 67 | ## 依存ライブラリ 68 | 69 | - [HelixToolkit](https://github.com/helix-toolkit/helix-toolkit) 70 | - [QuickLook.Plugin.HelixViewer](https://github.com/ShiinaManatsu/QuickLook.Plugin.HelixViewer) 71 | 72 | - [Pfim](https://github.com/nickbabcock/Pfim) 73 | 74 | - [SevenZipSharp](https://github.com/squid-box/SevenZipSharp) 75 | - [7-Zip](https://www.7-zip.org/) 76 | 77 | ## ライセンス 78 | 79 | [LICENSE](LICENSE) 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ・[English](README.en.md) ・[中文](README.md) ・[日本語](README.jp.md) 2 | 3 | # 原神模型预览器 4 | 5 | 同时亦支持其他PMX格式DMM模型的预览。 6 | 7 | > 特性:支持任意旋转平移!支持穿模! 8 | 9 | ## 模型格式 10 | 11 | > 支持模型文件格式: `*.pmx` `*.7z` `*.zip` 12 | 13 | ※归档类型将自动非解压方式导入首个`*.pmx`文件 14 | 15 | > 支持模型引用的贴图格式:`*.png` `*.bmp` `*.dds` `*.tga` 等 16 | 17 | [模型下载](https://www.aplaybox.com/u/680828836) 18 | 19 | ## 使用方法 20 | 21 | ![demo2](screen-shot/demo-01.png) 22 | 23 | ![demo1](screen-shot/demo-02.png) 24 | 25 | > 点击选择模型文件或将文件拖拽到窗口里。 26 | 27 | | 操作源 | 效果 | 28 | | -------- | --------------------------------- | 29 | | 鼠标左键 | 按住拖拽**平移** | 30 | | 鼠标中键 | 按住拖拽**平移** 滚轮上下**缩放** | 31 | | 鼠标右键 | 按住拖拽**旋转** | 32 | 33 | ## 运行环境 34 | 35 | `Win10 x64` `.NET 6` 36 | 37 | ## 程序下载 38 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/total)](https://github.com/emako/genshin-model-viewer/releases) 39 | [![GitHub downloads](https://img.shields.io/github/downloads/emako/genshin-model-viewer/latest/total)](https://github.com/emako/genshin-model-viewer/releases) 40 | 41 | > 下载页:https://github.com/emako/genshin-model-viewer/releases 42 | > 43 | 44 | ## 开发任务 45 | 46 | - [ ] 支持导入文件夹 47 | - [ ] 支持在导入文件夹或归档文件时多个pmx文件则提供选项 48 | - [ ] 支持全屏模式以及重写窗体标题栏 49 | - [ ] 支持交互的按钮 50 | 51 | 52 | ## 常见问题 53 | 54 | > 导入模型后发现贴图异常? 55 | 56 | - 不支持加入了Sphere算法的贴图,此时将会表现为贴图异常,同时目前原神的官方发布的DMM模型没有使用此类贴图方法,所以原神的模型应该不会有此类问题。 57 | 58 | --- 59 | 60 | 欢迎提出[Issues](https://github.com/emako/genshin-model-viewer/issues) 61 | 62 | ## 更新日志 63 | 64 | [CHANGELOG.md](CHANGELOG.md) 65 | 66 | ## 依赖支持 67 | 68 | - [HelixToolkit](https://github.com/helix-toolkit/helix-toolkit) 69 | - [QuickLook.Plugin.HelixViewer](https://github.com/ShiinaManatsu/QuickLook.Plugin.HelixViewer) 70 | 71 | - [Pfim](https://github.com/nickbabcock/Pfim) 72 | 73 | - [SevenZipSharp](https://github.com/squid-box/SevenZipSharp) 74 | - [7-Zip](https://www.7-zip.org/) 75 | 76 | ## 许可证 77 | 78 | [LICENSE](LICENSE) 79 | 80 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | App.cer 2 | App.msix 3 | App_Setup.exe 4 | GenshinModelViewer*Setup*.exe 5 | GenshinModelViewer*.7z 6 | -------------------------------------------------------------------------------- /build/nsis/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/favicon.ico -------------------------------------------------------------------------------- /build/nsis/setup.nsi: -------------------------------------------------------------------------------- 1 | !define PRODUCT_NAME "App" 2 | !define PRODUCT_VERSION "1.0.0.0" 3 | !define PRODUCT_PUBLISHER "ema" 4 | !define PRODUCT_WEB_SITE "https://github.com/emako" 5 | !define PRODUCT_LEGAL "Licensed under MIT" 6 | 7 | VIProductVersion "${PRODUCT_VERSION}" 8 | VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}" 9 | VIAddVersionKey "ProductName" "${PRODUCT_NAME}" 10 | VIAddVersionKey "CompanyName" "${PRODUCT_PUBLISHER}" 11 | VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}" 12 | VIAddVersionKey "InternalName" "${PRODUCT_NAME}" 13 | VIAddVersionKey "FileDescription" "${PRODUCT_NAME}_Setup" 14 | VIAddVersionKey "Comments" "${PRODUCT_WEB_SITE}" 15 | VIAddVersionKey "LegalCopyright" "${PRODUCT_LEGAL}" 16 | 17 | !addplugindir plugins 18 | 19 | Icon "favicon.ico" 20 | Name "${PRODUCT_NAME}" 21 | OutFile "..\${PRODUCT_NAME}_Setup.exe" 22 | RequestExecutionLevel admin 23 | Page custom MsixSetup 24 | 25 | Function MsixSetup 26 | Call CheckMutex 27 | InitPluginsDir 28 | SetOutPath "$PLUGINSDIR" 29 | File "tools\certmgr.exe" 30 | File "..\app.cer" 31 | nsExec::ExecToLog 'certmgr.exe -add app.cer -s -r localMachine AuthRoot' 32 | File "tools\msixexec.exe" 33 | File "..\app.msixbundle" 34 | nsExec::ExecToLog 'msixexec.exe app.msixbundle' 35 | FunctionEnd 36 | 37 | Function CheckMutex 38 | System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}_SetupMutex") i .r1 ?e' 39 | Pop $R0 40 | StrCmp $R0 0 +3 41 | MessageBox MB_OK|MB_ICONEXCLAMATION "Setup is already running." 42 | Abort 43 | FunctionEnd 44 | 45 | Section 46 | SectionEnd 47 | -------------------------------------------------------------------------------- /build/nsis/tools/certmgr.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/certmgr.exe -------------------------------------------------------------------------------- /build/nsis/tools/makensis.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/makensis.exe -------------------------------------------------------------------------------- /build/nsis/tools/msixexec.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/msixexec.exe -------------------------------------------------------------------------------- /build/nsis/tools/plugins/System.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/plugins/System.dll -------------------------------------------------------------------------------- /build/nsis/tools/plugins/nsExec.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/plugins/nsExec.dll -------------------------------------------------------------------------------- /build/nsis/tools/stubs/uninst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/stubs/uninst -------------------------------------------------------------------------------- /build/nsis/tools/stubs/zlib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/stubs/zlib -------------------------------------------------------------------------------- /build/nsis/tools/stubs/zlib_solid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/build/nsis/tools/stubs/zlib_solid -------------------------------------------------------------------------------- /build/nsis_build.bat: -------------------------------------------------------------------------------- 1 | cd /d %~dp0 2 | rename GenshinModelViewer.Packaging_*_x64.cer App.cer 3 | rename GenshinModelViewer.Packaging_*_x64.msixbundle App.msixbundle 4 | del App_Setup.exe 5 | nsis\tools\makensis .\nsis\setup.nsi 6 | del GenshinModelViewer_Setup.exe 7 | rename App_Setup.exe GenshinModelViewer_Setup.exe 8 | @pause 9 | -------------------------------------------------------------------------------- /screen-shot/demo-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/screen-shot/demo-01.png -------------------------------------------------------------------------------- /screen-shot/demo-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/screen-shot/demo-02.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0066: Convert switch statement to expression 4 | dotnet_diagnostic.IDE0066.severity = silent 5 | -------------------------------------------------------------------------------- /src/.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 | .vs/* 7 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Logging/.gitignore: -------------------------------------------------------------------------------- 1 | .vs/* 2 | bin/* 3 | obj/* 4 | *.user 5 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Logging/GenshinModelViewer.Logging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-windows 5 | enable 6 | enable 7 | 10.0 8 | x64 9 | x64 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Logging/Logger.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Globalization; 3 | using System.IO.Abstractions; 4 | using System.Reflection; 5 | 6 | namespace GenshinModelViewer.Logging 7 | { 8 | public static class Logger 9 | { 10 | internal static readonly FileSystem FileSystem = new(); 11 | internal static readonly IPath Path = FileSystem.Path; 12 | internal static readonly IDirectory Directory = FileSystem.Directory; 13 | 14 | public static readonly string ApplicationLogPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"GenshinModelViewer\logs"); 15 | 16 | static Logger() 17 | { 18 | if (!Directory.Exists(ApplicationLogPath)) 19 | { 20 | Directory.CreateDirectory(ApplicationLogPath); 21 | } 22 | 23 | string logFilePath = Path.Combine(ApplicationLogPath, DateTime.Now.ToString(@"yyyyMMdd", CultureInfo.InvariantCulture) + ".log"); 24 | Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); 25 | Trace.AutoFlush = true; 26 | } 27 | 28 | public static void Info(params object[] values) 29 | { 30 | Log("INFO", string.Join(" ", values)); 31 | } 32 | 33 | public static void Warn(params object[] values) 34 | { 35 | Log("ERROR", string.Join(" ", values)); 36 | } 37 | 38 | public static void Error(params object[] values) 39 | { 40 | Log("ERROR", string.Join(" ", values)); 41 | } 42 | 43 | public static void Fatal(params object[] values) 44 | { 45 | Log("ERROR", string.Join(" ", values)); 46 | #if DEBUG 47 | Debugger.Break(); 48 | #endif 49 | } 50 | 51 | public static void Exception(Exception e, string message = null!) 52 | { 53 | Log( 54 | (message ?? string.Empty) + Environment.NewLine + 55 | e?.Message + Environment.NewLine + 56 | "Inner exception: " + Environment.NewLine + 57 | e?.InnerException?.Message + Environment.NewLine + 58 | "Stack trace: " + Environment.NewLine + 59 | e?.StackTrace, 60 | "ERROR"); 61 | #if DEBUG 62 | Debugger.Break(); 63 | #endif 64 | } 65 | 66 | private static void Log(string type, string message) 67 | { 68 | Trace.Write(type + "|" + DateTime.Now.ToString(@"yyyy-MM-dd|HH:mm:ss.fff", CultureInfo.InvariantCulture)); 69 | Trace.Write("|" + GetCallerInfo()); 70 | Trace.WriteLine("|" + message); 71 | } 72 | 73 | private static string GetCallerInfo() 74 | { 75 | StackTrace stackTrace = new(); 76 | 77 | MethodBase methodName = stackTrace.GetFrame(3)?.GetMethod()!; 78 | string? className = methodName?.DeclaringType?.Name; 79 | return className + "|" + methodName?.Name; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/.gitignore: -------------------------------------------------------------------------------- 1 | .vs/* 2 | [Bb]in/ 3 | [Oo]bj/ 4 | AppPackages/* 5 | BundleArtifacts/* 6 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/GenshinModelViewer.Packaging.wapproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15.0 5 | 6 | 7 | 8 | Debug 9 | x86 10 | 11 | 12 | Release 13 | x86 14 | 15 | 16 | Debug 17 | x64 18 | 19 | 20 | Release 21 | x64 22 | 23 | 24 | Debug 25 | ARM 26 | 27 | 28 | Release 29 | ARM 30 | 31 | 32 | Debug 33 | ARM64 34 | 35 | 36 | Release 37 | ARM64 38 | 39 | 40 | Debug 41 | AnyCPU 42 | 43 | 44 | Release 45 | AnyCPU 46 | 47 | 48 | 49 | $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ 50 | 51 | 52 | 53 | db2aa1a0-3ba7-4f73-af4e-814930588810 54 | 10.0.22000.0 55 | 10.0.18362.0 56 | en-US 57 | True 58 | $(NoWarn);NU1702 59 | False 60 | A7C792A1E97C61397F171DFFDD4618D5B081B139 61 | SHA256 62 | False 63 | True 64 | x64 65 | 0 66 | ..\GenshinModelViewer\GenshinModelViewer.csproj 67 | 68 | 69 | Always 70 | 71 | 72 | Always 73 | 74 | 75 | Always 76 | 77 | 78 | Always 79 | 80 | 81 | Always 82 | 83 | 84 | Always 85 | 86 | 87 | Always 88 | 89 | 90 | Always 91 | 92 | 93 | Always 94 | 95 | 96 | Always 97 | 98 | 99 | 100 | Designer 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/GenshinModelViewer.Packaging.wapproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | AppHostLocalDebugger 5 | False 6 | 7 | 8 | SideloadOnly 9 | False 10 | x64 11 | False 12 | 13 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/GenshinModelViewer.Packaging_TemporaryKey.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/GenshinModelViewer.Packaging_TemporaryKey.pfx -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/StoreLogo.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Images/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer.Packaging/Images/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/GenshinModelViewer.Packaging/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 8 | 9 | 13 | 14 | 15 | GenshinModelViewer 16 | ema 17 | Images\StoreLogo.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/GenshinModelViewer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenshinModelViewer", "GenshinModelViewer\GenshinModelViewer.csproj", "{8B68854B-3B1B-404A-A6C3-5484D0E7E6C1}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CCDD4262-ED2A-473D-8462-09CBBBA3183A}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "GenshinModelViewer.Packaging", "GenshinModelViewer.Packaging\GenshinModelViewer.Packaging.wapproj", "{DB2AA1A0-3BA7-4F73-AF4E-814930588810}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenshinModelViewer.Logging", "GenshinModelViewer.Logging\GenshinModelViewer.Logging.csproj", "{1A1722EB-F536-4724-AB27-7FFDDB4E6A39}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|x64 = Debug|x64 20 | Release|x64 = Release|x64 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {8B68854B-3B1B-404A-A6C3-5484D0E7E6C1}.Debug|x64.ActiveCfg = Debug|x64 24 | {8B68854B-3B1B-404A-A6C3-5484D0E7E6C1}.Debug|x64.Build.0 = Debug|x64 25 | {8B68854B-3B1B-404A-A6C3-5484D0E7E6C1}.Release|x64.ActiveCfg = Release|x64 26 | {8B68854B-3B1B-404A-A6C3-5484D0E7E6C1}.Release|x64.Build.0 = Release|x64 27 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Debug|x64.ActiveCfg = Debug|x64 28 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Debug|x64.Build.0 = Debug|x64 29 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Debug|x64.Deploy.0 = Debug|x64 30 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Release|x64.ActiveCfg = Release|x64 31 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Release|x64.Build.0 = Release|x64 32 | {DB2AA1A0-3BA7-4F73-AF4E-814930588810}.Release|x64.Deploy.0 = Release|x64 33 | {1A1722EB-F536-4724-AB27-7FFDDB4E6A39}.Debug|x64.ActiveCfg = Debug|x64 34 | {1A1722EB-F536-4724-AB27-7FFDDB4E6A39}.Debug|x64.Build.0 = Debug|x64 35 | {1A1722EB-F536-4724-AB27-7FFDDB4E6A39}.Release|x64.ActiveCfg = Release|x64 36 | {1A1722EB-F536-4724-AB27-7FFDDB4E6A39}.Release|x64.Build.0 = Release|x64 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {ABC40303-8E30-45A6-B826-D23031B2D2AA} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/.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 | .vs/* 7 | 8 | [Dd]ebug/ 9 | [Dd]ebugPublic/ 10 | [Rr]elease/ 11 | [Rr]eleases/ 12 | x64/ 13 | x86/ 14 | bld/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | [Ll]og/ 18 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/App.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 41 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace GenshinModelViewer 4 | { 5 | public partial class App : Application 6 | { 7 | public App() 8 | { 9 | Logger.Info("Application startup."); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] 4 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/BitmapUtils.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows.Media; 3 | using System.Windows.Media.Imaging; 4 | using Bitmap = System.Drawing.Bitmap; 5 | using ImageFormat = System.Drawing.Imaging.ImageFormat; 6 | 7 | namespace GenshinModelViewer.Core 8 | { 9 | internal static class BitmapUtils 10 | { 11 | public static ImageSource ToBitmapImage(Stream stream) 12 | { 13 | BitmapImage bitmapImage = new(); 14 | 15 | stream.Position = 0; 16 | bitmapImage.BeginInit(); 17 | bitmapImage.CacheOption = BitmapCacheOption.OnLoad; 18 | bitmapImage.StreamSource = stream; 19 | bitmapImage.EndInit(); 20 | bitmapImage.Freeze(); 21 | return bitmapImage; 22 | } 23 | 24 | public static BitmapImage ToBitmapImage(this Bitmap bitmap, ImageFormat imageFormat = null) 25 | { 26 | MemoryStream stream = new(); 27 | bitmap.Save(stream, imageFormat ?? ImageFormat.Png); 28 | BitmapImage image = new(); 29 | image.BeginInit(); 30 | image.StreamSource = stream; 31 | image.EndInit(); 32 | return image; 33 | } 34 | 35 | public static BitmapImage ToBitmapImage(this Bitmap bitmap) 36 | { 37 | using MemoryStream memory = new(); 38 | bitmap.Save(memory, ImageFormat.Png); 39 | memory.Position = 0; 40 | 41 | BitmapImage bitmapImage = new(); 42 | bitmapImage.BeginInit(); 43 | bitmapImage.StreamSource = memory; 44 | bitmapImage.CacheOption = BitmapCacheOption.OnLoad; 45 | bitmapImage.EndInit(); 46 | bitmapImage.Freeze(); 47 | 48 | return bitmapImage; 49 | } 50 | 51 | public static Bitmap ToBitmap(this BitmapImage bitmapImage) 52 | { 53 | using MemoryStream outStream = new(); 54 | BmpBitmapEncoder enc = new(); 55 | enc.Frames.Add(BitmapFrame.Create(bitmapImage)); 56 | enc.Save(outStream); 57 | Bitmap bitmap = new(outStream); 58 | 59 | return bitmap; 60 | } 61 | 62 | public static void Save(this ImageSource imageSource, string fileName) 63 | { 64 | (imageSource as BitmapSource)?.Save(fileName); 65 | } 66 | 67 | public static void Save(this BitmapSource bitmapSource, string fileName) 68 | { 69 | PngBitmapEncoder encoder = new(); 70 | 71 | encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); 72 | 73 | string dir = new FileInfo(fileName).DirectoryName; 74 | if (!Directory.Exists(dir)) 75 | { 76 | Directory.CreateDirectory(dir); 77 | } 78 | 79 | using Stream stream = File.Create(fileName); 80 | 81 | encoder.Save(stream); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/BrushAnimation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Animation; 7 | 8 | namespace GenshinModelViewer.Core 9 | { 10 | public class BrushAnimation : AnimationTimeline 11 | { 12 | public Brush From 13 | { 14 | get { return (Brush)GetValue(FromProperty); } 15 | set { SetValue(FromProperty, value); } 16 | } 17 | 18 | public static readonly DependencyProperty FromProperty = 19 | DependencyProperty.Register("From", typeof(Brush), typeof(BrushAnimation)); 20 | 21 | public Brush To 22 | { 23 | get { return (Brush)GetValue(ToProperty); } 24 | set { SetValue(ToProperty, value); } 25 | } 26 | 27 | public static readonly DependencyProperty ToProperty = 28 | DependencyProperty.Register("To", typeof(Brush), typeof(BrushAnimation)); 29 | 30 | public override Type TargetPropertyType => typeof(Brush); 31 | 32 | public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock) 33 | { 34 | return GetCurrentValue(defaultOriginValue as Brush, defaultDestinationValue as Brush, animationClock); 35 | } 36 | 37 | protected override Freezable CreateInstanceCore() 38 | { 39 | return new BrushAnimation(); 40 | } 41 | 42 | public object GetCurrentValue(Brush defaultOriginValue, Brush defaultDestinationValue, AnimationClock animationClock) 43 | { 44 | if (!animationClock.CurrentProgress.HasValue) 45 | return Brushes.Transparent; 46 | 47 | defaultOriginValue = this.From ?? defaultOriginValue; 48 | defaultDestinationValue = this.To ?? defaultDestinationValue; 49 | 50 | if (animationClock.CurrentProgress.Value == 0) 51 | return defaultOriginValue; 52 | if (animationClock.CurrentProgress.Value == 1) 53 | return defaultDestinationValue; 54 | 55 | if (To != null) 56 | { 57 | if (defaultDestinationValue is SolidColorBrush && ((SolidColorBrush)defaultDestinationValue).Color.A < 255 58 | && (!(defaultOriginValue is SolidColorBrush) || (((SolidColorBrush)defaultOriginValue).Color.A == 255))) 59 | { 60 | return GetVisualBrush(defaultDestinationValue, defaultOriginValue, 1 - animationClock.CurrentProgress.Value); 61 | } 62 | else 63 | { 64 | return GetVisualBrush(defaultOriginValue, defaultDestinationValue, animationClock.CurrentProgress.Value); 65 | } 66 | } 67 | else 68 | { 69 | if (defaultOriginValue is SolidColorBrush && ((SolidColorBrush)defaultOriginValue).Color.A < 255 70 | && (!(defaultDestinationValue is SolidColorBrush) || (((SolidColorBrush)defaultDestinationValue).Color.A == 255))) 71 | { 72 | return GetVisualBrush(defaultOriginValue, defaultDestinationValue, animationClock.CurrentProgress.Value); 73 | } 74 | else 75 | { 76 | return GetVisualBrush(defaultDestinationValue, defaultOriginValue, 1 - animationClock.CurrentProgress.Value); 77 | } 78 | } 79 | } 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public static VisualBrush GetVisualBrush(Brush background, Brush foreground, double opacity) 83 | { 84 | return new VisualBrush(new Border() 85 | { 86 | Width = 1, 87 | Height = 1, 88 | Background = background, 89 | 90 | Child = new Border() 91 | { 92 | Background = foreground, 93 | Opacity = opacity 94 | } 95 | }); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/ElementHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | 5 | namespace GenshinModelViewer.Core 6 | { 7 | public static class ElementHelper 8 | { 9 | public static void ForEachDeep(this UIElement element, Action action) where T : UIElement 10 | { 11 | if (element is Panel panel) 12 | { 13 | panel.Children.ForEachDeep(action); 14 | } 15 | else if (element is Border border) 16 | { 17 | if (typeof(T) == typeof(Border)) 18 | { 19 | action(element as T); 20 | } 21 | else 22 | { 23 | border.Child.ForEachDeep(action); 24 | } 25 | } 26 | else 27 | { 28 | if (element is T t) 29 | action(t); 30 | } 31 | } 32 | 33 | public static void ForEach(this UIElementCollection elements, Action action) where T : UIElement 34 | { 35 | foreach (var element in elements) 36 | { 37 | if (element is T t) 38 | action(t); 39 | } 40 | } 41 | 42 | public static void ForEachDeep(this UIElementCollection elements, Action action) where T : UIElement 43 | { 44 | elements.ForEach(element => 45 | { 46 | element.ForEachDeep(action); 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/FullScreenHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Forms; 3 | using System.Windows.Interop; 4 | 5 | namespace GenshinModelViewer.Core 6 | { 7 | public static class FullScreenHandler 8 | { 9 | private class FullScreenInfo 10 | { 11 | public double Left { get; set; } 12 | public double Top { get; set; } 13 | public double Width { get; set; } 14 | public double Height { get; set; } 15 | public WindowState WindowState { get; set; } 16 | public WindowStyle WindowStyle { get; set; } 17 | public ResizeMode ResizeMode { get; set; } 18 | 19 | public FullScreenInfo(Window window) 20 | { 21 | Left = window.Left; 22 | Top = window.Top; 23 | Width = window.Width; 24 | Height = window.Height; 25 | WindowState = window.WindowState; 26 | WindowStyle = window.WindowStyle; 27 | ResizeMode = window.ResizeMode; 28 | } 29 | } 30 | 31 | private static FullScreenInfo GetFullScreenInfo(DependencyObject obj) 32 | => (FullScreenInfo)obj.GetValue(FullScreenProperty); 33 | private static void SetFullScreenInfo(DependencyObject obj, FullScreenInfo value) 34 | => obj.SetValue(FullScreenProperty, value); 35 | private static readonly DependencyProperty FullScreenProperty 36 | = DependencyProperty.RegisterAttached("FullScreenInfo", typeof(FullScreenInfo), typeof(FullScreenHandler), new PropertyMetadata(null)); 37 | 38 | public static void SetFullScreen(this Window window, bool? restore = null) 39 | { 40 | if (restore != null) 41 | { 42 | if (restore == true) 43 | { 44 | window.RestoreFullScreen(); 45 | } 46 | else 47 | { 48 | window.MakeFullScreen(); 49 | } 50 | } 51 | else 52 | { 53 | window.ReserveFullScreen(); 54 | } 55 | } 56 | 57 | private static void ReserveFullScreen(this Window window) 58 | { 59 | if (GetFullScreenInfo(window) == null) 60 | { 61 | window.MakeFullScreen(); 62 | } 63 | else 64 | { 65 | window.RestoreFullScreen(); 66 | } 67 | } 68 | 69 | private static void MakeFullScreen(this Window window) 70 | { 71 | Screen screen = Screen.FromHandle(new WindowInteropHelper(window).Handle); 72 | FullScreenInfo info = new(window); 73 | 74 | SetFullScreenInfo(window, info); 75 | window.Left = screen.Bounds.Left; 76 | window.Top = screen.Bounds.Top; 77 | window.Width = screen.Bounds.Width; 78 | window.Height = screen.Bounds.Height; 79 | window.WindowState = WindowState.Normal; 80 | window.WindowStyle = WindowStyle.None; 81 | window.ResizeMode = ResizeMode.NoResize; 82 | } 83 | 84 | private static void RestoreFullScreen(this Window window) 85 | { 86 | FullScreenInfo info = GetFullScreenInfo(window); 87 | 88 | if (info == null) return; 89 | SetFullScreenInfo(window, null); 90 | window.Left = info.Left; 91 | window.Top = info.Top; 92 | window.Width = info.Width; 93 | window.Height = info.Height; 94 | window.WindowState = info.WindowState; 95 | window.WindowStyle = info.WindowStyle; 96 | window.ResizeMode = info.ResizeMode; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/MMDFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | using System.Windows.Media.Media3D; 6 | 7 | namespace GenshinModelViewer.Core 8 | { 9 | // PMXフォーマットクラス 10 | // Reference URL: 11 | // http://blog.goo.ne.jp/torisu_tetosuki/e/209ad341d3ece2b1b4df24abf619d6e4 12 | // http://mikudan.blog120.fc2.com/blog-entry-280.html 13 | public class PMXFormat 14 | { 15 | public MetaHeader meta_header; 16 | public Header header; 17 | public VertexList vertex_list; 18 | public FaceVertexList face_vertex_list; 19 | public TextureList texture_list; 20 | public MaterialList material_list; 21 | public BoneList bone_list; 22 | public MorphList morph_list; 23 | public DisplayFrameList display_frame_list; 24 | public RigidbodyList rigidbody_list; 25 | public RigidbodyJointList rigidbody_joint_list; 26 | 27 | public class MetaHeader 28 | { 29 | public string path; // フルパス 30 | public string name; // 拡張子とパス抜きのファイルの名前 31 | public string folder; // ファイル名抜きのパス 32 | } 33 | 34 | public class Header 35 | { 36 | public enum StringCode { 37 | Utf16le, 38 | Utf8, 39 | } 40 | public enum IndexSize { 41 | Byte1 = 1, 42 | Byte2 = 2, 43 | Byte4 = 4, 44 | } 45 | public byte[] magic; // "PMX " 46 | public float version; // 00 00 80 3F == 1.00 47 | 48 | public byte dataSize; 49 | public StringCode encodeMethod; 50 | public byte additionalUV; 51 | public IndexSize vertexIndexSize; 52 | public IndexSize textureIndexSize; 53 | public IndexSize materialIndexSize; 54 | public IndexSize boneIndexSize; 55 | public IndexSize morphIndexSize; 56 | public IndexSize rigidbodyIndexSize; 57 | 58 | public string model_name; 59 | public string model_english_name; 60 | public string comment; 61 | public string english_comment; 62 | } 63 | 64 | public class VertexList 65 | { 66 | public Vertex[] vertex; // 頂点データ(38bytes/頂点) 67 | } 68 | 69 | public class Vertex 70 | { 71 | public enum WeightMethod { 72 | BDEF1, 73 | BDEF2, 74 | BDEF4, 75 | SDEF, 76 | QDEF, 77 | } 78 | public Point3D pos; // x, y, z // 座標 79 | public Point3D normal_vec; // nx, ny, nz // 法線ベクトル 80 | public Point uv; // u, v // UV座標 // MMDは頂点UV 81 | public Point4D[] add_uv; // x,y,z,w 82 | public BoneWeight bone_weight; 83 | public float edge_magnification; 84 | 85 | } 86 | 87 | public interface BoneWeight 88 | { 89 | Vertex.WeightMethod method {get;} 90 | uint bone1_ref {get;} 91 | uint bone2_ref {get;} 92 | uint bone3_ref {get;} 93 | uint bone4_ref {get;} 94 | float bone1_weight {get;} 95 | float bone2_weight {get;} 96 | float bone3_weight {get;} 97 | float bone4_weight {get;} 98 | Point3D c_value {get;} 99 | Point3D r0_value {get;} 100 | Point3D r1_value {get;} 101 | } 102 | 103 | public class BDEF1 : BoneWeight 104 | { 105 | public Vertex.WeightMethod method {get{return Vertex.WeightMethod.BDEF1;}} 106 | public uint bone1_ref {get; set;} 107 | public uint bone2_ref {get{return 0;}} 108 | public uint bone3_ref {get{return 0;}} 109 | public uint bone4_ref {get{return 0;}} 110 | public float bone1_weight {get{return 1.0f;}} 111 | public float bone2_weight {get{return 0.0f;}} 112 | public float bone3_weight {get{return 0.0f;}} 113 | public float bone4_weight {get{return 0.0f;}} 114 | public Point3D c_value {get{return new Point3D(0,0,0);}} 115 | public Point3D r0_value {get{return new Point3D(0,0,0);}} 116 | public Point3D r1_value {get{return new Point3D(0,0,0);}} 117 | } 118 | public class BDEF2 : BoneWeight 119 | { 120 | public Vertex.WeightMethod method {get{return Vertex.WeightMethod.BDEF2;}} 121 | public uint bone1_ref {get; set;} 122 | public uint bone2_ref {get; set;} 123 | public float bone1_weight {get; set;} 124 | public uint bone3_ref {get{return 0;}} 125 | public uint bone4_ref {get{return 0;}} 126 | public float bone2_weight {get{return 1.0f - bone1_weight;}} 127 | public float bone3_weight {get{return 0.0f;}} 128 | public float bone4_weight {get{return 0.0f;}} 129 | public Point3D c_value {get{return new Point3D(0,0,0);}} 130 | public Point3D r0_value {get{return new Point3D(0,0,0);}} 131 | public Point3D r1_value {get{return new Point3D(0,0,0);}} 132 | } 133 | public class BDEF4 : BoneWeight 134 | { 135 | public Vertex.WeightMethod method {get{return Vertex.WeightMethod.BDEF4;}} 136 | public uint bone1_ref {get; set;} 137 | public uint bone2_ref {get; set;} 138 | public uint bone3_ref {get; set;} 139 | public uint bone4_ref {get; set;} 140 | public float bone1_weight {get; set;} 141 | public float bone2_weight {get; set;} 142 | public float bone3_weight {get; set;} 143 | public float bone4_weight {get; set;} 144 | public Point3D c_value {get{return new Point3D(0,0,0);}} 145 | public Point3D r0_value {get{return new Point3D(0,0,0);}} 146 | public Point3D r1_value {get{return new Point3D(0,0,0);}} 147 | } 148 | public class SDEF : BoneWeight 149 | { 150 | public Vertex.WeightMethod method {get{return Vertex.WeightMethod.SDEF;}} 151 | public uint bone1_ref {get; set;} 152 | public uint bone2_ref {get; set;} 153 | public float bone1_weight {get; set;} 154 | public Point3D c_value {get; set;} 155 | public Point3D r0_value {get; set;} 156 | public Point3D r1_value {get; set;} 157 | public uint bone3_ref {get{return 0;}} 158 | public uint bone4_ref {get{return 0;}} 159 | public float bone2_weight {get{return 1.0f - bone1_weight;}} 160 | public float bone3_weight {get{return 0.0f;}} 161 | public float bone4_weight {get{return 0.0f;}} 162 | } 163 | public class QDEF : BoneWeight 164 | { 165 | public Vertex.WeightMethod method {get{return Vertex.WeightMethod.QDEF;}} 166 | public uint bone1_ref {get; set;} 167 | public uint bone2_ref {get; set;} 168 | public uint bone3_ref {get; set;} 169 | public uint bone4_ref {get; set;} 170 | public float bone1_weight {get; set;} 171 | public float bone2_weight {get; set;} 172 | public float bone3_weight {get; set;} 173 | public float bone4_weight {get; set;} 174 | public Point3D c_value {get{return new Point3D(0,0,0);}} 175 | public Point3D r0_value {get{return new Point3D(0,0,0);}} 176 | public Point3D r1_value {get{return new Point3D(0,0,0);}} 177 | } 178 | 179 | // 面頂点リスト 180 | public class FaceVertexList 181 | { 182 | public uint[] face_vert_index; // 頂点番号(3個/面) 183 | } 184 | 185 | public class TextureList 186 | { 187 | public string[] texture_file; // 100byte * 10個固定 188 | } 189 | 190 | public class MaterialList 191 | { 192 | public Material[] material; // 材質データ(70bytes/material) 193 | } 194 | 195 | public class Material 196 | { 197 | [Flags] 198 | public enum Flag { 199 | Reversible = 1<< 0, //両面描画 200 | CastShadow = 1<< 1, //地面影 201 | CastSelfShadow = 1<< 2, //セルフシャドウマップへの描画 202 | ReceiveSelfShadow = 1<< 3, //セルフシャドウの描画 203 | Edge = 1<< 4, //エッジ描画 204 | } 205 | public enum SphereMode { 206 | Null, //無し 207 | MulSphere, //乗算スフィア 208 | AddSphere, //加算スフィア 209 | SubTexture, //サブテクスチャ 210 | } 211 | public string name; 212 | public string english_name; 213 | 214 | public Color diffuse_color; // dr, dg, db, da // 減衰色 215 | public Color specular_color; // sr, sg, sb // 光沢色 216 | public float specularity; 217 | public Color ambient_color; // mr, mg, mb // 環境色(ambient) 218 | public Flag flag; 219 | public Color edge_color; // r, g, b, a 220 | public float edge_size; 221 | public uint usually_texture_index; 222 | public uint sphere_texture_index; 223 | public SphereMode sphere_mode; 224 | public byte common_toon; 225 | public uint toon_texture_index; 226 | public string memo; 227 | public uint face_vert_count; // 面頂点数 // インデックスに変換する場合は、材質0から順に加算 228 | } 229 | 230 | public class BoneList 231 | { 232 | public Bone[] bone; // ボーンデータ(39bytes/bone) 233 | } 234 | 235 | public class Bone 236 | { 237 | [Flags] 238 | public enum Flag { 239 | Connection = 1<< 0, //接続先(PMD子ボーン指定)表示方法(ON:ボーンで指定、OFF:座標オフセットで指定) 240 | Rotatable = 1<< 1, //回転可能 241 | Movable = 1<< 2, //移動可能 242 | DisplayFlag = 1<< 3, //表示 243 | CanOperate = 1<< 4, //操作可 244 | IkFlag = 1<< 5, //IK 245 | AddLocal = 1<< 7, //ローカル付与 | 付与対象(ON:親のローカル変形量、OFF:ユーザー変形値/IKリンク/多重付与) 246 | AddRotation = 1<< 8, //回転付与 247 | AddMove = 1<< 9, //移動付与 248 | FixedAxis = 1<<10, //軸固定 249 | LocalAxis = 1<<11, //ローカル軸 250 | PhysicsTransform = 1<<12, //物理後変形 251 | ExternalParentTransform = 1<<13, //外部親変形 252 | } 253 | public string bone_name; // ボーン名 254 | public string bone_english_name; 255 | public Point3D bone_position; 256 | public uint parent_bone_index; // 親ボーン番号(ない場合はuint.MaxValue) 257 | public int transform_level; 258 | public Flag bone_flag; 259 | public Point3D position_offset; 260 | public uint connection_index; 261 | public uint additional_parent_index; 262 | public float additional_rate; 263 | public Point3D axis_vector; 264 | public Point3D x_axis_vector; 265 | public Point3D z_axis_vector; 266 | public uint key_value; 267 | public IK_Data ik_data; 268 | } 269 | 270 | public class IK_Data 271 | { 272 | public uint ik_bone_index; // IKボーン番号 273 | public uint iterations; // 再帰演算回数 // IK値1 274 | public float limit_angle; 275 | public IK_Link[] ik_link; 276 | } 277 | 278 | public class IK_Link 279 | { 280 | public uint target_bone_index; 281 | public byte angle_limit; 282 | public Point3D lower_limit; 283 | public Point3D upper_limit; 284 | } 285 | 286 | public class MorphList 287 | { 288 | public MorphData[] morph_data; // 表情データ((25+16*skin_vert_count)/skin) 289 | } 290 | 291 | public class MorphData 292 | { 293 | public enum Panel { 294 | Base, 295 | EyeBrow, 296 | Eye, 297 | Lip, 298 | Other, 299 | } 300 | public enum MorphType { 301 | Group, 302 | Vertex, 303 | Bone, 304 | Uv, 305 | Adduv1, 306 | Adduv2, 307 | Adduv3, 308 | Adduv4, 309 | Material, 310 | 311 | Flip, 312 | Impulse, 313 | } 314 | public string morph_name; // 表情名 315 | public string morph_english_name; // 表情英名 316 | public Panel handle_panel; 317 | public MorphType morph_type; 318 | public MorphOffset[] morph_offset; 319 | } 320 | 321 | public interface MorphOffset {}; 322 | 323 | public class VertexMorphOffset : MorphOffset 324 | { 325 | public uint vertex_index; 326 | public Point3D position_offset; 327 | } 328 | public class UVMorphOffset : MorphOffset 329 | { 330 | public uint vertex_index; 331 | public Point4D uv_offset; 332 | } 333 | public class BoneMorphOffset : MorphOffset 334 | { 335 | public uint bone_index; 336 | public Point3D move_value; 337 | public Quaternion rotate_value; 338 | } 339 | public class MaterialMorphOffset : MorphOffset 340 | { 341 | public enum OffsetMethod { 342 | Mul, 343 | Add, 344 | } 345 | public uint material_index; 346 | public OffsetMethod offset_method; 347 | public Color diffuse; 348 | public Color specular; 349 | public float specularity; 350 | public Color ambient; 351 | public Color edge_color; 352 | public float edge_size; 353 | public Color texture_coefficient; 354 | public Color sphere_texture_coefficient; 355 | public Color toon_texture_coefficient; 356 | } 357 | public class GroupMorphOffset : MorphOffset 358 | { 359 | public uint morph_index; 360 | public float morph_rate; 361 | } 362 | 363 | public class ImpulseMorphOffset : MorphOffset 364 | { 365 | public uint rigidbody_index; 366 | public byte local_flag; 367 | public Point3D move_velocity; 368 | public Point3D rotation_torque; 369 | } 370 | 371 | public class DisplayFrameList 372 | { 373 | public DisplayFrame[] display_frame; 374 | } 375 | 376 | public class DisplayFrame 377 | { 378 | public string display_name; 379 | public string display_english_name; 380 | public byte special_frame_flag; 381 | public DisplayElement[] display_element; 382 | } 383 | 384 | public class DisplayElement 385 | { 386 | public byte element_target; 387 | public uint element_target_index; 388 | } 389 | 390 | public class RigidbodyList 391 | { 392 | public Rigidbody[] rigidbody; 393 | } 394 | 395 | /// 396 | /// 剛体 397 | /// 398 | public class Rigidbody 399 | { 400 | public enum ShapeType { 401 | Sphere, //球 402 | Box, //箱 403 | Capsule, //カプセル 404 | } 405 | public enum OperationType { 406 | Static, //ボーン追従 407 | Dynamic, //物理演算 408 | DynamicAndPosAdjust, //物理演算(Bone位置合せ) 409 | } 410 | public string name; // 諸データ:名称 ,20byte 411 | public string english_name; // 諸データ:名称 ,20byte 412 | public uint rel_bone_index;// 諸データ:関連ボーン番号 413 | public byte group_index; // 諸データ:グループ 414 | public ushort ignore_collision_group; 415 | public ShapeType shape_type; // 形状:タイプ(0:球、1:箱、2:カプセル) 416 | public Point3D shape_size; 417 | public Point3D collider_position; // 位置:位置(x, y, z) 418 | public Point3D collider_rotation; // 位置:回転(rad(x), rad(y), rad(z)) 419 | public float weight; // 諸データ:質量 // 00 00 80 3F // 1.0 420 | public float position_dim; // 諸データ:移動減 // 00 00 00 00 421 | public float rotation_dim; // 諸データ:回転減 // 00 00 00 00 422 | public float recoil; // 諸データ:反発力 // 00 00 00 00 423 | public float friction; // 諸データ:摩擦力 // 00 00 00 00 424 | public OperationType operation_type; // 諸データ:タイプ(0:Bone追従、1:物理演算、2:物理演算(Bone位置合せ)) // 00 // Bone追従 425 | } 426 | 427 | public class RigidbodyJointList 428 | { 429 | public Joint[] joint; 430 | } 431 | 432 | public class Joint 433 | { 434 | public enum OperationType { 435 | Spring6DOF, //スプリング6DOF 436 | } 437 | public string name; // 20byte 438 | public string english_name; 439 | public OperationType operation_type; 440 | public uint rigidbody_a; // 諸データ:剛体A 441 | public uint rigidbody_b; // 諸データ:剛体B 442 | public Point3D position; // 諸データ:位置(x, y, z) // 諸データ:位置合せでも設定可 443 | public Point3D rotation; // 諸データ:回転(rad(x), rad(y), rad(z)) 444 | public Point3D constrain_pos_lower; // 制限:移動1(x, y, z) 445 | public Point3D constrain_pos_upper; // 制限:移動2(x, y, z) 446 | public Point3D constrain_rot_lower; // 制限:回転1(rad(x), rad(y), rad(z)) 447 | public Point3D constrain_rot_upper; // 制限:回転2(rad(x), rad(y), rad(z)) 448 | public Point3D spring_position; // ばね:移動(x, y, z) 449 | public Point3D spring_rotation; // ばね:回転(rad(x), rad(y), rad(z)) 450 | } 451 | } 452 | } 453 | 454 | namespace PMD 455 | { 456 | // PMDのフォーマットクラス 457 | public class PMDFormat 458 | { 459 | public string path; // フルパス 460 | public string name; // 拡張子とパス抜きのファイルの名前 461 | public string folder; // ファイル名抜きのパス 462 | 463 | public Header head; 464 | public VertexList vertex_list; 465 | public FaceVertexList face_vertex_list; 466 | public MaterialList material_list; 467 | public BoneList bone_list; 468 | public IKList ik_list; 469 | public SkinList skin_list; 470 | public SkinNameList skin_name_list; 471 | public BoneNameList bone_name_list; 472 | public BoneDisplayList bone_display_list; 473 | public EnglishHeader eg_head; 474 | public EnglishBoneNameList eg_bone_name_list; 475 | public EnglishSkinNameList eg_skin_name_list; 476 | public EnglishBoneDisplayList eg_bone_display_list; 477 | public ToonTextureList toon_texture_list; 478 | public RigidbodyList rigidbody_list; 479 | public RigidbodyJointList rigidbody_joint_list; 480 | 481 | public class Header 482 | { 483 | public byte[] magic; // "Pmd" 484 | public float version; // 00 00 80 3F == 1.00 485 | public string model_name; 486 | public string comment; 487 | } 488 | 489 | public class VertexList 490 | { 491 | public uint vert_count; // 頂点数 492 | public Vertex[] vertex; // 頂点データ(38bytes/頂点) 493 | } 494 | 495 | public class Vertex 496 | { 497 | public Point3D pos; // x, y, z // 座標 498 | public Point3D normal_vec; // nx, ny, nz // 法線ベクトル 499 | public Point uv; // u, v // UV座標 // MMDは頂点UV 500 | public ushort[] bone_num; // ボーン番号1、番号2 // モデル変形(頂点移動)時に影響 501 | public byte bone_weight; // ボーン1に与える影響度 // min:0 max:100 // ボーン2への影響度は、(100 - bone_weight) 502 | public byte edge_flag; // 0:通常、1:エッジ無効 // エッジ(輪郭)が有効の場合 503 | } 504 | 505 | // 面頂点リスト 506 | public class FaceVertexList 507 | { 508 | public uint face_vert_count; // 頂点数 509 | public ushort[] face_vert_index; // 頂点番号(3個/面) 510 | } 511 | 512 | public class MaterialList 513 | { 514 | public uint material_count; // 材質数 515 | public Material[] material; // 材質データ(70bytes/material) 516 | } 517 | 518 | public class Material 519 | { 520 | public Color diffuse_color; // dr, dg, db // 減衰色 521 | public float alpha; 522 | public float specularity; 523 | public Color specular_color; // sr, sg, sb // 光沢色 524 | public Color mirror_color; // mr, mg, mb // 環境色(ambient) 525 | public byte toon_index; // toon??.bmp // 0.bmp:0xFF, 1(01).bmp:0x00 ・・・ 10.bmp:0x09 526 | public byte edge_flag; // 輪郭、影 527 | public uint face_vert_count; // 面頂点数 // インデックスに変換する場合は、材質0から順に加算 528 | public string texture_file_name; // テクスチャファイル名またはスフィアファイル名 // 20バイトぎりぎりまで使える(終端の0x00は無くても動く) 529 | public string sphere_map_name; // スフィアマップ用 530 | 531 | /* 532 | テクスチャファイル名またはスフィアファイル名の補足: 533 | 534 | テクスチャファイルにスフィアファイルを乗算または加算する場合 535 | (MMD 5.12以降) 536 | "テクスチャ名.bmp*スフィア名.sph" で乗算 537 | "テクスチャ名.bmp*スフィア名.spa" で加算 538 | 539 | (MMD 5.11) 540 | "テクスチャ名.bmp/スフィア名.sph" で乗算 541 | 542 | (MMD 5.09あたり-) 543 | "テクスチャ名.bmp" または "スフィア名.sph" 544 | */ 545 | } 546 | 547 | public class BoneList 548 | { 549 | public ushort bone_count; // ボーン数 550 | public Bone[] bone; // ボーンデータ(39bytes/bone) 551 | } 552 | 553 | public class Bone 554 | { 555 | public string bone_name; // ボーン名 556 | public ushort parent_bone_index; // 親ボーン番号(ない場合は0xFFFF) 557 | public ushort tail_pos_bone_index; // tail位置のボーン番号(チェーン末端の場合は0xFFFF) // 親:子は1:多なので、主に位置決め用 558 | public byte bone_type; // ボーンの種類 559 | public ushort ik_parent_bone_index; // IKボーン番号(影響IKボーン。ない場合は0) 560 | public Point3D bone_head_pos; // x, y, z // ボーンのヘッドの位置 561 | 562 | /* 563 | ・ボーンの種類 564 | 0:回転 1:回転と移動 2:IK 3:不明 4:IK影響下 5:回転影響下 6:IK接続先 7:非表示 8:捻り 9:回転運動 565 | */ 566 | } 567 | 568 | public class IKList 569 | { 570 | public ushort ik_data_count; // IKデータ数 571 | public IK[] ik_data; // IKデータ((11+2*ik_chain_length)/IK) 572 | } 573 | 574 | public class IK 575 | { 576 | public ushort ik_bone_index; // IKボーン番号 577 | public ushort ik_target_bone_index; // IKターゲットボーン番号 // IKボーンが最初に接続するボーン 578 | public byte ik_chain_length; // IKチェーンの長さ(子の数) 579 | public ushort iterations; // 再帰演算回数 // IK値1 580 | public float control_weight; // IKの影響度 // IK値2 581 | public ushort[] ik_child_bone_index; // IK影響下のボーン番号 582 | } 583 | 584 | public class SkinList 585 | { 586 | public ushort skin_count; // 表情数 587 | public SkinData[] skin_data; // 表情データ((25+16*skin_vert_count)/skin) 588 | } 589 | 590 | public class SkinData 591 | { 592 | public string skin_name; // 表情名 593 | public uint skin_vert_count; // 表情用の頂点数 594 | public byte skin_type; // 表情の種類 // 0:base、1:まゆ、2:目、3:リップ、4:その他 595 | public SkinVertexData[] skin_vert_data; // 表情用の頂点のデータ(16bytes/vert) 596 | } 597 | 598 | public class SkinVertexData 599 | { 600 | // 実際の頂点を参照するには 601 | // int num = vertex_count - skin_vert_count; 602 | // skin_vert[num]みたいな形で参照しないと無理 603 | public uint skin_vert_index; // 表情用の頂点の番号(頂点リストにある番号) 604 | public Point3D skin_vert_pos; // x, y, z // 表情用の頂点の座標(頂点自体の座標) 605 | } 606 | 607 | // 表情用枠名 608 | public class SkinNameList 609 | { 610 | public byte skin_disp_count; 611 | public ushort[] skin_index; // 表情番号 612 | } 613 | 614 | // ボーン用枠名 615 | public class BoneNameList 616 | { 617 | public byte bone_disp_name_count; 618 | public string[] disp_name; // 50byte 619 | } 620 | 621 | // ボーン枠用表示リスト 622 | public class BoneDisplayList 623 | { 624 | public uint bone_disp_count; 625 | public BoneDisplay[] bone_disp; 626 | } 627 | 628 | public class BoneDisplay 629 | { 630 | public ushort bone_index; // 枠用ボーン番号 631 | public byte bone_disp_frame_index; // 表示枠番号 632 | } 633 | 634 | /// 635 | /// 英語表記用ヘッダ 636 | /// 637 | public class EnglishHeader 638 | { 639 | public byte english_name_compatibility; // 01で英名対応 640 | public string model_name_eg; // 20byte 641 | public string comment_eg; // 256byte 642 | } 643 | 644 | /// 645 | /// 英語表記用ボーンの英語名 646 | /// 647 | public class EnglishBoneNameList 648 | { 649 | public string[] bone_name_eg; // 20byte * bone_count 650 | } 651 | 652 | public class EnglishSkinNameList 653 | { 654 | // baseは英名が登録されない 655 | public string[] skin_name_eg; // 20byte * skin_count-1 656 | } 657 | 658 | public class EnglishBoneDisplayList 659 | { 660 | public string[] disp_name_eg; // 50byte * bone_disp_name_count 661 | } 662 | 663 | public class ToonTextureList 664 | { 665 | public string[] toon_texture_file; // 100byte * 10個固定 666 | } 667 | 668 | public class RigidbodyList 669 | { 670 | public uint rigidbody_count; 671 | public PMD.PMDFormat.Rigidbody[] rigidbody; 672 | } 673 | 674 | /// 675 | /// 剛体 676 | /// 677 | public class Rigidbody 678 | { 679 | public string rigidbody_name; // 諸データ:名称 ,20byte 680 | public int rigidbody_rel_bone_index;// 諸データ:関連ボーン番号 681 | public byte rigidbody_group_index; // 諸データ:グループ 682 | public ushort rigidbody_group_target; // 諸データ:グループ:対象 // 0xFFFFとの差 683 | public byte shape_type; // 形状:タイプ(0:球、1:箱、2:カプセル) 684 | public float shape_w; // 形状:半径(幅) 685 | public float shape_h; // 形状:高さ 686 | public float shape_d; // 形状:奥行 687 | public Point3D pos_pos; // 位置:位置(x, y, z) 688 | public Point3D pos_rot; // 位置:回転(rad(x), rad(y), rad(z)) 689 | public float rigidbody_weight; // 諸データ:質量 // 00 00 80 3F // 1.0 690 | public float rigidbody_pos_dim; // 諸データ:移動減 // 00 00 00 00 691 | public float rigidbody_rot_dim; // 諸データ:回転減 // 00 00 00 00 692 | public float rigidbody_recoil; // 諸データ:反発力 // 00 00 00 00 693 | public float rigidbody_friction; // 諸データ:摩擦力 // 00 00 00 00 694 | public byte rigidbody_type; // 諸データ:タイプ(0:Bone追従、1:物理演算、2:物理演算(Bone位置合せ)) // 00 // Bone追従 695 | } 696 | 697 | public class RigidbodyJointList 698 | { 699 | public uint joint_count; 700 | public Joint[] joint; 701 | } 702 | 703 | public class Joint 704 | { 705 | public string joint_name; // 20byte 706 | public uint joint_rigidbody_a; // 諸データ:剛体A 707 | public uint joint_rigidbody_b; // 諸データ:剛体B 708 | public Point3D joint_pos; // 諸データ:位置(x, y, z) // 諸データ:位置合せでも設定可 709 | public Point3D joint_rot; // 諸データ:回転(rad(x), rad(y), rad(z)) 710 | public Point3D constrain_pos_1; // 制限:移動1(x, y, z) 711 | public Point3D constrain_pos_2; // 制限:移動2(x, y, z) 712 | public Point3D constrain_rot_1; // 制限:回転1(rad(x), rad(y), rad(z)) 713 | public Point3D constrain_rot_2; // 制限:回転2(rad(x), rad(y), rad(z)) 714 | public Point3D spring_pos; // ばね:移動(x, y, z) 715 | public Point3D spring_rot; // ばね:回転(rad(x), rad(y), rad(z)) 716 | } 717 | } 718 | } 719 | namespace VMD 720 | { 721 | // VMDのフォーマットクラス 722 | public class VMDFormat 723 | { 724 | public string name; 725 | public string path; 726 | public string folder; 727 | 728 | public Header header; 729 | public MotionList motion_list; 730 | public SkinList skin_list; 731 | public LightList light_list; 732 | public CameraList camera_list; 733 | public SelfShadowList self_shadow_list; 734 | 735 | public class Header 736 | { 737 | public string vmd_header; // 30byte, "Vocaloid Motion Data 0002" 738 | public string vmd_model_name; // 20byte 739 | } 740 | 741 | public class MotionList 742 | { 743 | public uint motion_count; 744 | public Dictionary> motion; 745 | } 746 | 747 | public class Motion 748 | { 749 | public string bone_name; // 15byte 750 | public uint flame_no; 751 | public Point3D location; 752 | public Quaternion rotation; 753 | public byte[] interpolation; // [4][4][4], 64byte 754 | 755 | // なんか不便になりそうな気がして 756 | public byte GetInterpolation(int i, int j, int k) 757 | { 758 | return this.interpolation[i*16+j*4+k]; 759 | } 760 | 761 | public void SetInterpolation(byte val, int i, int j, int k) 762 | { 763 | this.interpolation[i*16+j*4+k] = val; 764 | } 765 | } 766 | 767 | /// 768 | /// 表情リスト 769 | /// 770 | public class SkinList 771 | { 772 | public uint skin_count; 773 | public Dictionary> skin; 774 | } 775 | 776 | public class SkinData 777 | { 778 | public string skin_name; // 15byte 779 | public uint flame_no; 780 | public float weight; 781 | } 782 | 783 | public class CameraList 784 | { 785 | public uint camera_count; 786 | public CameraData[] camera; 787 | } 788 | 789 | public class CameraData 790 | { 791 | public uint flame_no; 792 | public float length; 793 | public Point3D location; 794 | public Point3D rotation; // オイラー角, X軸は符号が反転している 795 | public byte[] interpolation; // [6][4], 24byte(未検証) 796 | public uint viewing_angle; 797 | public byte perspective; // 0:on 1:off 798 | 799 | public byte GetInterpolation(int i, int j) 800 | { 801 | return this.interpolation[i*6+j]; 802 | } 803 | 804 | public void SetInterpolation(byte val, int i, int j) 805 | { 806 | this.interpolation[i*6+j] = val; 807 | } 808 | } 809 | 810 | public class LightList 811 | { 812 | public uint light_count; 813 | public LightData[] light; 814 | } 815 | 816 | public class LightData 817 | { 818 | public uint flame_no; 819 | public Color rgb; // αなし, 256 820 | public Point3D location; 821 | } 822 | 823 | public class SelfShadowList 824 | { 825 | public uint self_shadow_count; 826 | public SelfShadowData[] self_shadow; 827 | } 828 | 829 | public class SelfShadowData 830 | { 831 | public uint flame_no; 832 | public byte mode; //00-02 833 | public float distance; // 0.1 - (dist * 0.00001) 834 | } 835 | } 836 | } -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/PMXLoaderScript.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Media3D; 7 | 8 | namespace GenshinModelViewer.Core 9 | { 10 | public partial class PMXLoaderScript 11 | { 12 | public static PMXFormat.Header GetHeader(string file_path) 13 | { 14 | PMXLoaderScript loader = new(); 15 | return loader.GetHeader_(file_path); 16 | } 17 | 18 | public static PMXFormat.Header GetHeader(Stream stream) 19 | { 20 | PMXLoaderScript loader = new(); 21 | return loader.GetHeader_(stream); 22 | } 23 | 24 | public static PMXFormat Import(string file_path) 25 | { 26 | PMXLoaderScript loader = new(); 27 | return loader.Import_(file_path); 28 | } 29 | 30 | public static PMXFormat Import(Stream stream) 31 | { 32 | PMXLoaderScript loader = new(); 33 | return loader.Import_(stream); 34 | } 35 | 36 | private PMXLoaderScript() 37 | { 38 | } 39 | 40 | private PMXFormat.Header GetHeader_(string file_path) 41 | { 42 | using FileStream stream = new(file_path, FileMode.Open, FileAccess.Read); 43 | return GetHeader_(stream); 44 | } 45 | 46 | private PMXFormat.Header GetHeader_(Stream stream) 47 | { 48 | PMXFormat.Header result; 49 | using BinaryReader bin = new(stream); 50 | bin.BaseStream.Seek(0, SeekOrigin.Begin); 51 | 52 | filePath = null; 53 | binaryReader = bin; 54 | result = ReadHeader(); 55 | return result; 56 | } 57 | 58 | private PMXFormat Import_(string file_path) 59 | { 60 | using FileStream stream = new(file_path, FileMode.Open, FileAccess.Read); 61 | 62 | filePath = file_path; 63 | return Import_(stream); 64 | } 65 | 66 | private PMXFormat Import_(Stream stream) 67 | { 68 | using BinaryReader bin = new(stream); 69 | bin.BaseStream.Seek(0, SeekOrigin.Begin); 70 | 71 | binaryReader = bin; 72 | Read(); 73 | return format; 74 | } 75 | 76 | private PMXFormat Read() 77 | { 78 | format = new(); 79 | format.meta_header = CreateMetaHeader(); 80 | format.header = ReadHeader(); 81 | format.vertex_list = ReadVertexList(); 82 | format.face_vertex_list = ReadFaceVertexList(); 83 | format.texture_list = ReadTextureList(); 84 | format.material_list = ReadMaterialList(); 85 | format.bone_list = ReadBoneList(); 86 | format.morph_list = ReadMorphList(); 87 | format.display_frame_list = ReadDisplayFrameList(); 88 | format.rigidbody_list = ReadRigidbodyList(); 89 | format.rigidbody_joint_list = ReadRigidbodyJointList(); 90 | return format; 91 | } 92 | 93 | private PMXFormat.MetaHeader CreateMetaHeader() 94 | { 95 | PMXFormat.MetaHeader result = new(); 96 | result.path = filePath; 97 | result.name = Path.GetFileNameWithoutExtension(filePath); // .pmdを抜かす 98 | result.folder = Path.GetDirectoryName(filePath); // PMDが格納されているフォルダ 99 | return result; 100 | } 101 | 102 | private PMXFormat.Header ReadHeader() 103 | { 104 | PMXFormat.Header result = new(); 105 | result.magic = binaryReader.ReadBytes(4); 106 | if (Encoding.ASCII.GetString(result.magic) != "PMX ") 107 | { 108 | throw new FormatException(); 109 | } 110 | result.version = binaryReader.ReadSingle(); 111 | binaryReader.ReadByte(); 112 | result.encodeMethod = (PMXFormat.Header.StringCode)binaryReader.ReadByte(); 113 | result.additionalUV = binaryReader.ReadByte(); 114 | result.vertexIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 115 | result.textureIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 116 | result.materialIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 117 | result.boneIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 118 | result.morphIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 119 | result.rigidbodyIndexSize = (PMXFormat.Header.IndexSize)binaryReader.ReadByte(); 120 | 121 | stringCode = result.encodeMethod; 122 | result.model_name = ReadString(); 123 | result.model_english_name = ReadString(); 124 | result.comment = ReadString(); 125 | result.english_comment = ReadString(); 126 | 127 | return result; 128 | } 129 | 130 | private PMXFormat.VertexList ReadVertexList() 131 | { 132 | PMXFormat.VertexList result = new(); 133 | uint vert_count = binaryReader.ReadUInt32(); 134 | result.vertex = new PMXFormat.Vertex[vert_count]; 135 | for (uint i = 0, i_max = (uint)result.vertex.Length; i < i_max; ++i) 136 | { 137 | result.vertex[i] = ReadVertex(); 138 | } 139 | return result; 140 | } 141 | 142 | private PMXFormat.Vertex ReadVertex() 143 | { 144 | PMXFormat.Vertex result = new(); 145 | result.pos = ReadSinglesToPoint3D(binaryReader); 146 | result.normal_vec = ReadSinglesToPoint3D(binaryReader); 147 | result.uv = ReadSinglesToPoint(binaryReader); 148 | result.add_uv = new Point4D[format.header.additionalUV]; 149 | for (int i = 0; i < format.header.additionalUV; i++) 150 | { 151 | result.add_uv[i] = ReadSinglesToPoint4D(binaryReader); 152 | } 153 | PMXFormat.Vertex.WeightMethod weight_method = (PMXFormat.Vertex.WeightMethod)binaryReader.ReadByte(); 154 | switch (weight_method) 155 | { 156 | case PMXFormat.Vertex.WeightMethod.BDEF1: 157 | result.bone_weight = ReadBoneWeightBDEF1(); 158 | break; 159 | case PMXFormat.Vertex.WeightMethod.BDEF2: 160 | result.bone_weight = ReadBoneWeightBDEF2(); 161 | break; 162 | case PMXFormat.Vertex.WeightMethod.BDEF4: 163 | result.bone_weight = ReadBoneWeightBDEF4(); 164 | break; 165 | case PMXFormat.Vertex.WeightMethod.SDEF: 166 | result.bone_weight = ReadBoneWeightSDEF(); 167 | break; 168 | case PMXFormat.Vertex.WeightMethod.QDEF: 169 | result.bone_weight = ReadBoneWeightQDEF(); 170 | break; 171 | default: 172 | result.bone_weight = null; 173 | throw new FormatException(); 174 | } 175 | result.edge_magnification = binaryReader.ReadSingle(); 176 | return result; 177 | } 178 | 179 | private PMXFormat.BoneWeight ReadBoneWeightBDEF1() 180 | { 181 | PMXFormat.BDEF1 result = new(); 182 | result.bone1_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 183 | return result; 184 | } 185 | 186 | private PMXFormat.BoneWeight ReadBoneWeightBDEF2() 187 | { 188 | PMXFormat.BDEF2 result = new(); 189 | result.bone1_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 190 | result.bone2_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 191 | result.bone1_weight = binaryReader.ReadSingle(); 192 | return result; 193 | } 194 | 195 | private PMXFormat.BoneWeight ReadBoneWeightBDEF4() 196 | { 197 | PMXFormat.BDEF4 result = new(); 198 | result.bone1_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 199 | result.bone2_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 200 | result.bone3_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 201 | result.bone4_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 202 | result.bone1_weight = binaryReader.ReadSingle(); 203 | result.bone2_weight = binaryReader.ReadSingle(); 204 | result.bone3_weight = binaryReader.ReadSingle(); 205 | result.bone4_weight = binaryReader.ReadSingle(); 206 | return result; 207 | } 208 | 209 | private PMXFormat.BoneWeight ReadBoneWeightSDEF() 210 | { 211 | PMXFormat.SDEF result = new(); 212 | result.bone1_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 213 | result.bone2_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 214 | result.bone1_weight = binaryReader.ReadSingle(); 215 | result.c_value = ReadSinglesToPoint3D(binaryReader); 216 | result.r0_value = ReadSinglesToPoint3D(binaryReader); 217 | result.r1_value = ReadSinglesToPoint3D(binaryReader); 218 | return result; 219 | } 220 | 221 | private PMXFormat.BoneWeight ReadBoneWeightQDEF() 222 | { 223 | PMXFormat.BDEF4 result = new(); 224 | result.bone1_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 225 | result.bone2_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 226 | result.bone3_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 227 | result.bone4_ref = CastIntRead(binaryReader, format.header.boneIndexSize); 228 | result.bone1_weight = binaryReader.ReadSingle(); 229 | result.bone2_weight = binaryReader.ReadSingle(); 230 | result.bone3_weight = binaryReader.ReadSingle(); 231 | result.bone4_weight = binaryReader.ReadSingle(); 232 | return result; 233 | } 234 | 235 | private PMXFormat.FaceVertexList ReadFaceVertexList() 236 | { 237 | PMXFormat.FaceVertexList result = new(); 238 | uint face_vert_count = binaryReader.ReadUInt32(); 239 | result.face_vert_index = new uint[face_vert_count]; 240 | for (uint i = 0, i_max = (uint)result.face_vert_index.Length; i < i_max; ++i) 241 | { 242 | result.face_vert_index[i] = CastIntRead(binaryReader, format.header.vertexIndexSize); 243 | } 244 | return result; 245 | } 246 | 247 | private PMXFormat.TextureList ReadTextureList() 248 | { 249 | PMXFormat.TextureList result = new(); 250 | uint texture_file_count = binaryReader.ReadUInt32(); 251 | result.texture_file = new string[texture_file_count]; 252 | for (uint i = 0, i_max = (uint)result.texture_file.Length; i < i_max; ++i) 253 | { 254 | result.texture_file[i] = ReadString(); 255 | //"./"開始なら削除する 256 | if (('.' == result.texture_file[i][0]) && (1 == result.texture_file[i].IndexOfAny(new[] { '/', '\\' }, 1, 1))) 257 | { 258 | result.texture_file[i] = result.texture_file[i].Substring(2); 259 | } 260 | } 261 | return result; 262 | } 263 | 264 | private PMXFormat.MaterialList ReadMaterialList() 265 | { 266 | PMXFormat.MaterialList result = new(); 267 | uint material_count = binaryReader.ReadUInt32(); 268 | result.material = new PMXFormat.Material[material_count]; 269 | for (uint i = 0, i_max = (uint)result.material.Length; i < i_max; ++i) 270 | { 271 | result.material[i] = ReadMaterial(); 272 | } 273 | return result; 274 | } 275 | 276 | private PMXFormat.Material ReadMaterial() 277 | { 278 | PMXFormat.Material result = new() 279 | { 280 | name = ReadString(), 281 | english_name = ReadString(), 282 | diffuse_color = ReadSinglesToColor(binaryReader), // dr, dg, db, da // 減衰色 283 | specular_color = ReadSinglesToColor(binaryReader, 1), // sr, sg, sb // 光沢色 284 | specularity = binaryReader.ReadSingle(), 285 | ambient_color = ReadSinglesToColor(binaryReader, 1), // mr, mg, mb // 環境色(ambient) 286 | flag = (PMXFormat.Material.Flag)binaryReader.ReadByte(), 287 | edge_color = ReadSinglesToColor(binaryReader), // r, g, b, a 288 | edge_size = binaryReader.ReadSingle(), 289 | usually_texture_index = CastIntRead(binaryReader, format.header.textureIndexSize), 290 | sphere_texture_index = CastIntRead(binaryReader, format.header.textureIndexSize), 291 | sphere_mode = (PMXFormat.Material.SphereMode)binaryReader.ReadByte(), 292 | common_toon = binaryReader.ReadByte() 293 | }; 294 | PMXFormat.Header.IndexSize texture_index_size = ((result.common_toon == 0) ? format.header.textureIndexSize : PMXFormat.Header.IndexSize.Byte1); 295 | result.toon_texture_index = CastIntRead(binaryReader, texture_index_size); 296 | result.memo = ReadString(); 297 | result.face_vert_count = binaryReader.ReadUInt32(); // 面頂点数 // インデックスに変換する場合は、材質0から順に加算 298 | return result; 299 | } 300 | 301 | private PMXFormat.BoneList ReadBoneList() 302 | { 303 | PMXFormat.BoneList result = new(); 304 | uint bone_count = binaryReader.ReadUInt32(); 305 | result.bone = new PMXFormat.Bone[bone_count]; 306 | for (uint i = 0, i_max = (uint)result.bone.Length; i < i_max; ++i) 307 | { 308 | result.bone[i] = ReadBone(); 309 | } 310 | return result; 311 | } 312 | 313 | private PMXFormat.Bone ReadBone() 314 | { 315 | PMXFormat.Bone result = new(); 316 | result.bone_name = ReadString(); 317 | result.bone_english_name = ReadString(); 318 | result.bone_position = ReadSinglesToPoint3D(binaryReader); 319 | result.parent_bone_index = CastIntRead(binaryReader, format.header.boneIndexSize); 320 | result.transform_level = binaryReader.ReadInt32(); 321 | result.bone_flag = (PMXFormat.Bone.Flag)binaryReader.ReadUInt16(); 322 | 323 | if ((result.bone_flag & PMXFormat.Bone.Flag.Connection) == 0) 324 | { 325 | // 座標オフセットで指定 326 | result.position_offset = ReadSinglesToPoint3D(binaryReader); 327 | } 328 | else 329 | { 330 | // ボーンで指定 331 | result.connection_index = CastIntRead(binaryReader, format.header.boneIndexSize); 332 | } 333 | if ((result.bone_flag & (PMXFormat.Bone.Flag.AddRotation | PMXFormat.Bone.Flag.AddMove)) != 0) 334 | { 335 | result.additional_parent_index = CastIntRead(binaryReader, format.header.boneIndexSize); 336 | result.additional_rate = binaryReader.ReadSingle(); 337 | } 338 | if ((result.bone_flag & PMXFormat.Bone.Flag.FixedAxis) != 0) 339 | { 340 | result.axis_vector = ReadSinglesToPoint3D(binaryReader); 341 | } 342 | if ((result.bone_flag & PMXFormat.Bone.Flag.LocalAxis) != 0) 343 | { 344 | result.x_axis_vector = ReadSinglesToPoint3D(binaryReader); 345 | result.z_axis_vector = ReadSinglesToPoint3D(binaryReader); 346 | } 347 | if ((result.bone_flag & PMXFormat.Bone.Flag.ExternalParentTransform) != 0) 348 | { 349 | result.key_value = binaryReader.ReadUInt32(); 350 | } 351 | if ((result.bone_flag & PMXFormat.Bone.Flag.IkFlag) != 0) 352 | { 353 | result.ik_data = ReadIkData(); 354 | } 355 | return result; 356 | } 357 | 358 | private PMXFormat.IK_Data ReadIkData() 359 | { 360 | PMXFormat.IK_Data result = new(); 361 | result.ik_bone_index = CastIntRead(binaryReader, format.header.boneIndexSize); 362 | result.iterations = binaryReader.ReadUInt32(); 363 | result.limit_angle = binaryReader.ReadSingle(); 364 | uint ik_link_count = binaryReader.ReadUInt32(); 365 | result.ik_link = new PMXFormat.IK_Link[ik_link_count]; 366 | for (uint i = 0, i_max = (uint)result.ik_link.Length; i < i_max; ++i) 367 | { 368 | result.ik_link[i] = ReadIkLink(); 369 | } 370 | return result; 371 | } 372 | 373 | private PMXFormat.IK_Link ReadIkLink() 374 | { 375 | PMXFormat.IK_Link result = new(); 376 | result.target_bone_index = CastIntRead(binaryReader, format.header.boneIndexSize); 377 | result.angle_limit = binaryReader.ReadByte(); 378 | if (result.angle_limit == 1) 379 | { 380 | result.lower_limit = ReadSinglesToPoint3D(binaryReader); 381 | result.upper_limit = ReadSinglesToPoint3D(binaryReader); 382 | } 383 | return result; 384 | } 385 | 386 | private PMXFormat.MorphList ReadMorphList() 387 | { 388 | PMXFormat.MorphList result = new(); 389 | uint morph_count = binaryReader.ReadUInt32(); 390 | result.morph_data = new PMXFormat.MorphData[morph_count]; 391 | for (uint i = 0, i_max = (uint)result.morph_data.Length; i < i_max; ++i) 392 | { 393 | result.morph_data[i] = ReadMorphData(); 394 | } 395 | return result; 396 | } 397 | 398 | private PMXFormat.MorphData ReadMorphData() 399 | { 400 | PMXFormat.MorphData result = new(); 401 | result.morph_name = ReadString(); 402 | result.morph_english_name = ReadString(); 403 | result.handle_panel = (PMXFormat.MorphData.Panel)binaryReader.ReadByte(); 404 | result.morph_type = (PMXFormat.MorphData.MorphType)binaryReader.ReadByte(); 405 | uint morph_offset_count = binaryReader.ReadUInt32(); 406 | result.morph_offset = new PMXFormat.MorphOffset[morph_offset_count]; 407 | for (uint i = 0, i_max = (uint)result.morph_offset.Length; i < i_max; ++i) 408 | { 409 | switch (result.morph_type) 410 | { 411 | case PMXFormat.MorphData.MorphType.Group: 412 | case PMXFormat.MorphData.MorphType.Flip: 413 | result.morph_offset[i] = ReadGroupMorphOffset(); 414 | break; 415 | case PMXFormat.MorphData.MorphType.Vertex: 416 | result.morph_offset[i] = ReadVertexMorphOffset(); 417 | break; 418 | case PMXFormat.MorphData.MorphType.Bone: 419 | result.morph_offset[i] = ReadBoneMorphOffset(); 420 | break; 421 | case PMXFormat.MorphData.MorphType.Uv: 422 | case PMXFormat.MorphData.MorphType.Adduv1: 423 | case PMXFormat.MorphData.MorphType.Adduv2: 424 | case PMXFormat.MorphData.MorphType.Adduv3: 425 | case PMXFormat.MorphData.MorphType.Adduv4: 426 | result.morph_offset[i] = ReadUVMorphOffset(); 427 | break; 428 | case PMXFormat.MorphData.MorphType.Material: 429 | result.morph_offset[i] = ReadMaterialMorphOffset(); 430 | break; 431 | case PMXFormat.MorphData.MorphType.Impulse: 432 | result.morph_offset[i] = ReadImpulseMorphOffset(); 433 | break; 434 | default: 435 | throw new System.FormatException(); 436 | } 437 | } 438 | return result; 439 | } 440 | private PMXFormat.MorphOffset ReadGroupMorphOffset() 441 | { 442 | PMXFormat.GroupMorphOffset result = new(); 443 | result.morph_index = CastIntRead(binaryReader, format.header.morphIndexSize); 444 | result.morph_rate = binaryReader.ReadSingle(); 445 | return result; 446 | } 447 | private PMXFormat.MorphOffset ReadVertexMorphOffset() 448 | { 449 | PMXFormat.VertexMorphOffset result = new(); 450 | result.vertex_index = CastIntRead(binaryReader, format.header.vertexIndexSize); 451 | result.position_offset = ReadSinglesToPoint3D(binaryReader); 452 | return result; 453 | } 454 | private PMXFormat.MorphOffset ReadBoneMorphOffset() 455 | { 456 | PMXFormat.BoneMorphOffset result = new(); 457 | result.bone_index = CastIntRead(binaryReader, format.header.boneIndexSize); 458 | result.move_value = ReadSinglesToPoint3D(binaryReader); 459 | result.rotate_value = ReadSinglesToQuaternion(binaryReader); 460 | return result; 461 | } 462 | private PMXFormat.MorphOffset ReadUVMorphOffset() 463 | { 464 | PMXFormat.UVMorphOffset result = new(); 465 | result.vertex_index = CastIntRead(binaryReader, format.header.vertexIndexSize); 466 | result.uv_offset = ReadSinglesToPoint4D(binaryReader); 467 | return result; 468 | } 469 | private PMXFormat.MorphOffset ReadMaterialMorphOffset() 470 | { 471 | PMXFormat.MaterialMorphOffset result = new(); 472 | result.material_index = CastIntRead(binaryReader, format.header.materialIndexSize); 473 | result.offset_method = (PMXFormat.MaterialMorphOffset.OffsetMethod)binaryReader.ReadByte(); 474 | result.diffuse = ReadSinglesToColor(binaryReader); 475 | result.specular = ReadSinglesToColor(binaryReader, 1); 476 | result.specularity = binaryReader.ReadSingle(); 477 | result.ambient = ReadSinglesToColor(binaryReader, 1); 478 | result.edge_color = ReadSinglesToColor(binaryReader); 479 | result.edge_size = binaryReader.ReadSingle(); 480 | result.texture_coefficient = ReadSinglesToColor(binaryReader); 481 | result.sphere_texture_coefficient = ReadSinglesToColor(binaryReader); 482 | result.toon_texture_coefficient = ReadSinglesToColor(binaryReader); 483 | return result; 484 | } 485 | private PMXFormat.MorphOffset ReadImpulseMorphOffset() 486 | { 487 | PMXFormat.ImpulseMorphOffset result = new(); 488 | result.rigidbody_index = CastIntRead(binaryReader, format.header.morphIndexSize); 489 | result.local_flag = binaryReader.ReadByte(); 490 | result.move_velocity = ReadSinglesToPoint3D(binaryReader); 491 | result.rotation_torque = ReadSinglesToPoint3D(binaryReader); 492 | return result; 493 | } 494 | 495 | private PMXFormat.DisplayFrameList ReadDisplayFrameList() 496 | { 497 | PMXFormat.DisplayFrameList result = new(); 498 | uint display_frame_count = binaryReader.ReadUInt32(); 499 | result.display_frame = new PMXFormat.DisplayFrame[display_frame_count]; 500 | for (uint i = 0, i_max = (uint)result.display_frame.Length; i < i_max; ++i) 501 | { 502 | result.display_frame[i] = ReadDisplayFrame(); 503 | } 504 | return result; 505 | } 506 | 507 | 508 | private PMXFormat.DisplayFrame ReadDisplayFrame() 509 | { 510 | PMXFormat.DisplayFrame result = new(); 511 | result.display_name = ReadString(); 512 | result.display_english_name = ReadString(); 513 | result.special_frame_flag = binaryReader.ReadByte(); 514 | uint display_element_count = binaryReader.ReadUInt32(); 515 | result.display_element = new PMXFormat.DisplayElement[display_element_count]; 516 | for (uint i = 0, i_max = (uint)result.display_element.Length; i < i_max; ++i) 517 | { 518 | result.display_element[i] = ReadDisplayElement(); 519 | } 520 | return result; 521 | } 522 | 523 | private PMXFormat.DisplayElement ReadDisplayElement() 524 | { 525 | PMXFormat.DisplayElement result = new(); 526 | result.element_target = binaryReader.ReadByte(); 527 | PMXFormat.Header.IndexSize element_target_index_size = (result.element_target == 0) ? format.header.boneIndexSize : format.header.morphIndexSize; 528 | result.element_target_index = CastIntRead(binaryReader, element_target_index_size); 529 | return result; 530 | } 531 | 532 | private PMXFormat.RigidbodyList ReadRigidbodyList() 533 | { 534 | PMXFormat.RigidbodyList result = new(); 535 | uint rigidbody_count = binaryReader.ReadUInt32(); 536 | result.rigidbody = new PMXFormat.Rigidbody[rigidbody_count]; 537 | for (uint i = 0, i_max = (uint)result.rigidbody.Length; i < i_max; ++i) 538 | { 539 | result.rigidbody[i] = ReadRigidbody(); 540 | } 541 | return result; 542 | } 543 | 544 | private PMXFormat.Rigidbody ReadRigidbody() 545 | { 546 | PMXFormat.Rigidbody result = new(); 547 | result.name = ReadString(); 548 | result.english_name = ReadString(); 549 | result.rel_bone_index = CastIntRead(binaryReader, format.header.boneIndexSize); 550 | result.group_index = binaryReader.ReadByte(); 551 | result.ignore_collision_group = binaryReader.ReadUInt16(); 552 | result.shape_type = (PMXFormat.Rigidbody.ShapeType)binaryReader.ReadByte(); 553 | result.shape_size = ReadSinglesToPoint3D(binaryReader); 554 | result.collider_position = ReadSinglesToPoint3D(binaryReader); 555 | result.collider_rotation = ReadSinglesToPoint3D(binaryReader); 556 | result.weight = binaryReader.ReadSingle(); 557 | result.position_dim = binaryReader.ReadSingle(); 558 | result.rotation_dim = binaryReader.ReadSingle(); 559 | result.recoil = binaryReader.ReadSingle(); 560 | result.friction = binaryReader.ReadSingle(); 561 | result.operation_type = (PMXFormat.Rigidbody.OperationType)binaryReader.ReadByte(); 562 | return result; 563 | } 564 | 565 | private PMXFormat.RigidbodyJointList ReadRigidbodyJointList() 566 | { 567 | PMXFormat.RigidbodyJointList result = new(); 568 | uint joint_count = binaryReader.ReadUInt32(); 569 | result.joint = new PMXFormat.Joint[joint_count]; 570 | for (uint i = 0, i_max = (uint)result.joint.Length; i < i_max; ++i) 571 | { 572 | result.joint[i] = ReadJoint(); 573 | } 574 | return result; 575 | } 576 | 577 | private PMXFormat.Joint ReadJoint() 578 | { 579 | PMXFormat.Joint result = new(); 580 | result.name = ReadString(); 581 | result.english_name = ReadString(); 582 | result.operation_type = (PMXFormat.Joint.OperationType)binaryReader.ReadByte(); 583 | switch (result.operation_type) 584 | { 585 | case PMXFormat.Joint.OperationType.Spring6DOF: 586 | result.rigidbody_a = CastIntRead(binaryReader, format.header.rigidbodyIndexSize); 587 | result.rigidbody_b = CastIntRead(binaryReader, format.header.rigidbodyIndexSize); 588 | result.position = ReadSinglesToPoint3D(binaryReader); 589 | result.rotation = ReadSinglesToPoint3D(binaryReader); 590 | result.constrain_pos_lower = ReadSinglesToPoint3D(binaryReader); 591 | result.constrain_pos_upper = ReadSinglesToPoint3D(binaryReader); 592 | result.constrain_rot_lower = ReadSinglesToPoint3D(binaryReader); 593 | result.constrain_rot_upper = ReadSinglesToPoint3D(binaryReader); 594 | result.spring_position = ReadSinglesToPoint3D(binaryReader); 595 | result.spring_rotation = ReadSinglesToPoint3D(binaryReader); 596 | break; 597 | default: 598 | //empty. 599 | break; 600 | } 601 | return result; 602 | } 603 | 604 | private string ReadString() 605 | { 606 | string result; 607 | int stringLength = binaryReader.ReadInt32(); 608 | byte[] buf = binaryReader.ReadBytes(stringLength); 609 | switch (stringCode) 610 | { 611 | case PMXFormat.Header.StringCode.Utf16le: 612 | result = Encoding.Unicode.GetString(buf); 613 | break; 614 | case PMXFormat.Header.StringCode.Utf8: 615 | result = Encoding.UTF8.GetString(buf); 616 | break; 617 | default: 618 | throw new InvalidOperationException(); 619 | } 620 | return result; 621 | } 622 | 623 | private uint CastIntRead(BinaryReader _, PMXFormat.Header.IndexSize index_size) 624 | { 625 | uint result; 626 | switch (index_size) 627 | { 628 | case PMXFormat.Header.IndexSize.Byte1: 629 | result = binaryReader.ReadByte(); 630 | if (byte.MaxValue == result) 631 | { 632 | result = uint.MaxValue; 633 | } 634 | break; 635 | case PMXFormat.Header.IndexSize.Byte2: 636 | result = binaryReader.ReadUInt16(); 637 | if (ushort.MaxValue == result) 638 | { 639 | result = uint.MaxValue; 640 | } 641 | break; 642 | case PMXFormat.Header.IndexSize.Byte4: 643 | result = binaryReader.ReadUInt32(); 644 | break; 645 | default: 646 | throw new ArgumentOutOfRangeException(); 647 | } 648 | return result; 649 | } 650 | 651 | private static Point4D ReadSinglesToPoint4D(BinaryReader binaryReader) 652 | { 653 | const int count = 4; 654 | float[] result = new float[count]; 655 | for (int i = 0; i < count; i++) 656 | { 657 | result[i] = binaryReader.ReadSingle(); 658 | if (float.IsNaN(result[i])) result[i] = 0.0f; // 非数値なら回避 659 | } 660 | return new Point4D(result[0], result[1], result[2], result[3]); 661 | } 662 | 663 | private static Point3D ReadSinglesToPoint3D(BinaryReader binaryReader) 664 | { 665 | const int count = 3; 666 | float[] result = new float[count]; 667 | for (int i = 0; i < count; i++) 668 | { 669 | result[i] = binaryReader.ReadSingle(); 670 | if (float.IsNaN(result[i])) result[i] = 0f; // 非数値なら回避 671 | } 672 | return new Point3D(result[0], result[1], result[2]); 673 | } 674 | 675 | private static Point ReadSinglesToPoint(BinaryReader binary_reader_) 676 | { 677 | const int count = 2; 678 | float[] result = new float[count]; 679 | for (int i = 0; i < count; i++) 680 | { 681 | result[i] = binary_reader_.ReadSingle(); 682 | if (float.IsNaN(result[i])) result[i] = 0f; // 非数値なら回避 683 | } 684 | return new Point(result[0], result[1]); 685 | } 686 | 687 | private static Color ReadSinglesToColor(BinaryReader binary_reader_) 688 | { 689 | const int count = 4; 690 | float[] result = new float[count]; 691 | for (int i = 0; i < count; i++) 692 | { 693 | result[i] = binary_reader_.ReadSingle(); 694 | } 695 | return RGBA2ARGB(result[0], result[1], result[2], result[3]); 696 | } 697 | 698 | private static Color ReadSinglesToColor(BinaryReader binary_reader_, float fix_alpha) 699 | { 700 | const int count = 3; 701 | float[] result = new float[count]; 702 | for (int i = 0; i < count; i++) 703 | { 704 | result[i] = binary_reader_.ReadSingle(); 705 | } 706 | return RGBA2ARGB(result[0], result[1], result[2], fix_alpha); 707 | } 708 | 709 | private Quaternion ReadSinglesToQuaternion(BinaryReader binary_reader_) 710 | { 711 | const int count = 4; 712 | float[] result = new float[count]; 713 | for (int i = 0; i < count; i++) 714 | { 715 | result[i] = binary_reader_.ReadSingle(); 716 | if (float.IsNaN(result[i])) result[i] = 0.0f; // 非数値なら回避 717 | } 718 | return new Quaternion(result[0], result[1], result[2], result[3]); 719 | } 720 | 721 | private static Color RGBA2ARGB(float r, float g, float b, float a) 722 | { 723 | var rr = Float2Byte(r); 724 | var gg = Float2Byte(g); 725 | var bb = Float2Byte(b); 726 | var aa = Float2Byte(a); 727 | 728 | return Color.FromArgb(aa, rr, gg, bb); 729 | } 730 | 731 | private static byte Float2Byte(float f) 732 | { 733 | return (byte)(f * 255); 734 | } 735 | 736 | string filePath; 737 | BinaryReader binaryReader; 738 | PMXFormat format; 739 | PMXFormat.Header.StringCode stringCode; 740 | } 741 | } 742 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/PMXProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Windows.Media; 7 | using System.Windows.Media.Imaging; 8 | 9 | namespace GenshinModelViewer.Core 10 | { 11 | public class PMXProvider : IDisposable 12 | { 13 | private SevenZipStock stock; 14 | private string path = null; 15 | 16 | public void Dispose() 17 | { 18 | path = null; 19 | stock?.Dispose(); 20 | stock = null; 21 | } 22 | 23 | private static bool IsArchive(string path) 24 | { 25 | FileInfo fi = new(path); 26 | 27 | if (fi.Extension.ToLower() == ".zip" || fi.Extension.ToLower() == ".7z") 28 | { 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | public void Load(string path) 35 | { 36 | Dispose(); 37 | 38 | if (IsArchive(path)) 39 | { 40 | stock = new(path); 41 | } 42 | } 43 | 44 | public async Task GetPMX(Func> selector) 45 | { 46 | string[] pmxs = stock.ContentDict?.Keys.Where(k => k.ToLower().EndsWith(".pmx")).ToArray(); 47 | 48 | if (pmxs == null || pmxs.Length <= 0) 49 | { 50 | return null; 51 | } 52 | 53 | if (pmxs.Length == 1 || selector == null) 54 | { 55 | path = pmxs.First(); 56 | } 57 | else 58 | { 59 | path = await selector.Invoke(pmxs); 60 | } 61 | if (path == null) 62 | { 63 | return null; 64 | } 65 | return stock.ContentDict[path]; 66 | } 67 | 68 | public async Task GetPMXFormat(string path, Func> selector = null) 69 | { 70 | if (IsArchive(path)) 71 | { 72 | Stream pmxStream = await GetPMX(selector); 73 | 74 | if (pmxStream == null) 75 | { 76 | return null; 77 | } 78 | return PMXLoaderScript.Import(pmxStream); 79 | } 80 | return PMXLoaderScript.Import(path); 81 | } 82 | 83 | public ImageSource GetTexture(string folder, string texturePath) 84 | { 85 | string ext = new FileInfo(texturePath).Extension.ToLower(); 86 | 87 | if (stock == null) 88 | { 89 | string path = Path.Combine(folder, texturePath); 90 | 91 | if (PfimxUtils.IsPfimx(ext)) 92 | { 93 | return PfimxUtils.ToBitmapImage(path); 94 | } 95 | return new BitmapImage(new Uri(path, UriKind.Relative)); 96 | } 97 | else 98 | { 99 | static string GetRelativePath(string relativeFrom, string path) 100 | { 101 | string[] pathSplitted = path.Replace('/', '\\').ToLower().Split('\\'); 102 | 103 | if (pathSplitted.Length >= 2) 104 | { 105 | string[] pathNew = new string[pathSplitted.Length - 1]; 106 | 107 | Array.Copy(pathSplitted, pathNew, pathSplitted.Length - 1); 108 | 109 | return $"{string.Join("\\", pathNew)}\\{relativeFrom.Replace('/', '\\').ToLower()}"; 110 | } 111 | else 112 | { 113 | return relativeFrom.Replace('/', '\\').ToLower(); 114 | } 115 | } 116 | 117 | string key = GetRelativePath(texturePath, path); 118 | 119 | Debug.WriteLine($"[LoadKey] {key}"); 120 | Stream image = stock.ContentDict[key]; 121 | 122 | if (PfimxUtils.IsPfimx(ext)) 123 | { 124 | return PfimxUtils.ToBitmapImage(image); 125 | } 126 | return BitmapUtils.ToBitmapImage(image); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/PfimxUtils.cs: -------------------------------------------------------------------------------- 1 | using Pfim; 2 | using System; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Imaging; 7 | using PfimImageFormat = Pfim.ImageFormat; 8 | using PixelFormat = System.Windows.Media.PixelFormat; 9 | 10 | namespace GenshinModelViewer.Core 11 | { 12 | internal static class PfimxUtils 13 | { 14 | public static bool IsPfimx(string ext) => ext == ".dds" || ext == ".tga"; 15 | 16 | public static ImageSource ToBitmapImage(Stream stream) 17 | { 18 | IImage image = Pfimage.FromStream(stream); 19 | return image.ToBitmapImage(); 20 | } 21 | 22 | public static ImageSource ToBitmapImage(string path) 23 | { 24 | IImage image = Pfimage.FromFile(path); 25 | return image.ToBitmapImage(); 26 | } 27 | 28 | public static ImageSource ToBitmapImage(this IImage image) 29 | { 30 | PixelFormat pixelFormat = image.Format switch 31 | { 32 | PfimImageFormat.Rgb24 => PixelFormats.Bgr24, 33 | PfimImageFormat.Rgba32 => PixelFormats.Bgra32, 34 | PfimImageFormat.Rgb8 => PixelFormats.Gray8, 35 | PfimImageFormat.R5g5b5a1 => PixelFormats.Bgr555, 36 | PfimImageFormat.R5g5b5 => PixelFormats.Bgr555, 37 | PfimImageFormat.R5g6b5 => PixelFormats.Bgr565, 38 | _ => throw new Exception($"Unable to convert {image.Format} to PixelFormat"), 39 | }; 40 | var pinnedArray = GCHandle.Alloc(image.Data, GCHandleType.Pinned); 41 | var addr = pinnedArray.AddrOfPinnedObject(); 42 | var bsource = BitmapSource.Create(image.Width, image.Height, 96.0, 96.0, pixelFormat, null, addr, image.DataLen, image.Stride); 43 | 44 | return bsource; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/SevenZipHelper.cs: -------------------------------------------------------------------------------- 1 | using SevenZip; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Windows; 7 | using System.Windows.Resources; 8 | 9 | namespace GenshinModelViewer.Core 10 | { 11 | public class SevenZipHelper 12 | { 13 | public static Func ModifyArchiveKey = (key) => key.Replace('/', '\\').ToLower(); 14 | 15 | static SevenZipHelper() 16 | { 17 | string lib7zPath = $@"{Environment.CurrentDirectory}\7z.dll"; 18 | 19 | if (!File.Exists(lib7zPath)) 20 | { 21 | byte[] lib7z = GetBytes("pack://application:,,,/GenshinModelViewer;component/Resources/7z.dll"); 22 | 23 | try 24 | { 25 | File.WriteAllBytes(lib7zPath, lib7z); 26 | } 27 | catch (Exception e) 28 | { 29 | Logger.Warn(e.ToString()); 30 | } 31 | } 32 | SetLibraryPath(lib7zPath); 33 | } 34 | 35 | public static byte[] GetBytes(string uriString) 36 | { 37 | Uri uri = new(uriString); 38 | StreamResourceInfo info = Application.GetResourceStream(uri); 39 | using BinaryReader stream = new(info.Stream); 40 | return stream.ReadBytes((int)info.Stream.Length); 41 | } 42 | 43 | public static Dictionary ArchiveStream(string archiveFullName, string? password = null) 44 | { 45 | Dictionary archiveDict = new(); 46 | using SevenZipExtractor sevenZipExtrator = string.IsNullOrEmpty(password) ? new(archiveFullName) : new(archiveFullName, password); 47 | 48 | void ReadFile(string fileName) 49 | { 50 | MemoryStream stream = new(); 51 | 52 | sevenZipExtrator.ExtractFile(fileName, stream); 53 | stream.Seek(0, SeekOrigin.Begin); 54 | 55 | archiveDict.Add(ModifyArchiveKey(fileName), stream); 56 | } 57 | 58 | foreach (ArchiveFileInfo info in sevenZipExtrator?.ArchiveFileData!) 59 | { 60 | if (!info.IsDirectory) 61 | { 62 | ReadFile(info.FileName); 63 | } 64 | } 65 | return archiveDict; 66 | } 67 | 68 | public static void SetLibraryPath(string libraryPath) 69 | { 70 | SevenZipBase.SetLibraryPath(libraryPath); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/SevenZipStock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Windows; 5 | 6 | namespace GenshinModelViewer.Core 7 | { 8 | public class SevenZipStock : IDisposable 9 | { 10 | public Dictionary ContentDict = new(); 11 | 12 | public SevenZipStock(string path) 13 | { 14 | try 15 | { 16 | Dispose(); 17 | ContentDict = SevenZipHelper.ArchiveStream(path, string.Empty); 18 | } 19 | catch (Exception e) 20 | { 21 | MessageBox.Show(e.ToString()); 22 | } 23 | } 24 | 25 | public void Dispose() 26 | { 27 | foreach (var pair in ContentDict) 28 | { 29 | try 30 | { 31 | pair.Value.Dispose(); 32 | } 33 | catch (Exception e) 34 | { 35 | MessageBox.Show(e.ToString()); 36 | } 37 | } 38 | ContentDict.Clear(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Core/StoryboardUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | using System.Windows.Media.Animation; 6 | 7 | namespace GenshinModelViewer.Core 8 | { 9 | public static class StoryboardUtils 10 | { 11 | public static void BeginBrushStoryboard(DependencyObject dependencyObj, IDictionary toDictionary, double durationSeconds = 0.2d) 12 | { 13 | var storyboard = new Storyboard(); 14 | foreach (var keyValue in toDictionary) 15 | { 16 | var anima = new BrushAnimation() 17 | { 18 | To = keyValue.Value, 19 | Duration = TimeSpan.FromSeconds(durationSeconds), 20 | }; 21 | Storyboard.SetTarget(anima, dependencyObj); 22 | Storyboard.SetTargetProperty(anima, new PropertyPath(keyValue.Key)); 23 | storyboard.Children.Add(anima); 24 | } 25 | storyboard.Begin(); 26 | } 27 | 28 | public static void BeginBrushStoryboard(DependencyObject dependencyObj, IList dpList) 29 | { 30 | var storyboard = new Storyboard(); 31 | foreach (var dp in dpList) 32 | { 33 | var anima = new BrushAnimation() 34 | { 35 | Duration = TimeSpan.FromSeconds(0.2), 36 | }; 37 | Storyboard.SetTarget(anima, dependencyObj); 38 | Storyboard.SetTargetProperty(anima, new PropertyPath(dp)); 39 | storyboard.Children.Add(anima); 40 | } 41 | storyboard.Begin(); 42 | } 43 | 44 | public static void BeginDoubleStoryboard(DependencyObject dependencyObj, DependencyProperty dp, double from, double to, Duration? duration = null, Action completed = null) 45 | { 46 | var storyboard = new Storyboard(); 47 | var anima = new DoubleAnimation() 48 | { 49 | From = from, 50 | To = to, 51 | Duration = duration ?? TimeSpan.FromSeconds(0.2), 52 | }; 53 | anima.EasingFunction = new ExponentialEase() 54 | { 55 | Exponent = 8, 56 | EasingMode = EasingMode.EaseOut, 57 | }; 58 | Storyboard.SetTarget(anima, dependencyObj); 59 | Storyboard.SetTargetProperty(anima, new PropertyPath(dp)); 60 | if (completed != null) 61 | { 62 | storyboard.Completed += (s, e) => completed?.Invoke(); 63 | } 64 | storyboard.Children.Add(anima); 65 | storyboard.Begin(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/GenshinModelViewer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows10.0.18362.0 6 | true 7 | true 8 | 10.0 9 | Resources\UI_AvatarIcon_Side_Yunjin.ico 10 | GenshinModelViewer 11 | x64 12 | True 13 | disable 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | $(DefaultXamlRuntime) 47 | Designer 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/GenshinModelViewer.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | <_LastSelectedProfileId>D:\GitHub\genshin-model-viewer\src\GenshinModelViewer\Properties\PublishProfiles\FolderProfile.pubxml 5 | 6 | 7 | 8 | Designer 9 | 10 | 11 | Designer 12 | 13 | 14 | Designer 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using GenshinModelViewer.Core; 2 | global using GenshinModelViewer.Logging; 3 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using GenshinModelViewer.Models; 2 | using Microsoft.Win32; 3 | using Model.Viewer.Plugin; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Input; 10 | using System.Windows.Media; 11 | 12 | namespace GenshinModelViewer 13 | { 14 | public partial class MainWindow : Window 15 | { 16 | public string ModelPath 17 | { 18 | get => (string)GetValue(ModelPathProperty); 19 | set => SetValue(ModelPathProperty, value); 20 | } 21 | public static readonly DependencyProperty ModelPathProperty = DependencyProperty.Register("ModelPath", typeof(string), typeof(MainWindow), new PropertyMetadata(null, (d, e) => 22 | { 23 | if (d is MainWindow self) 24 | self.LoadModel(e.NewValue as string); 25 | })); 26 | 27 | public MainWindow() 28 | { 29 | InitializeComponent(); 30 | 31 | Drop += (s, e) => 32 | { 33 | try 34 | { 35 | string fileName = ((Array)e.Data.GetData(DataFormats.FileDrop)).GetValue(0).ToString(); 36 | 37 | LoadModel(fileName); 38 | } 39 | catch 40 | { 41 | } 42 | }; 43 | 44 | gridOpen.MouseEnter += (s, e) => 45 | { 46 | Brush brush = new SolidColorBrush(Color.FromArgb(255, 107, 107, 107)); 47 | Dictionary dic = new() 48 | { 49 | [TextBlock.ForegroundProperty] = brush, 50 | [Border.BorderBrushProperty] = brush, 51 | }; 52 | gridOpen.Children.ForEachDeep(textBlock => 53 | { 54 | StoryboardUtils.BeginBrushStoryboard(textBlock, dic); 55 | }); 56 | StoryboardUtils.BeginBrushStoryboard(borderOpen, dic); 57 | }; 58 | 59 | gridOpen.MouseLeave += (s, e) => 60 | { 61 | Brush brush = new SolidColorBrush(Color.FromArgb(170, 170, 170, 170)); 62 | Dictionary dic = new() 63 | { 64 | [TextBlock.ForegroundProperty] = brush, 65 | [Border.BorderBrushProperty] = brush, 66 | }; 67 | borderOpen.BorderBrush = brush; 68 | 69 | gridOpen.Children.ForEachDeep(textBlock => 70 | { 71 | StoryboardUtils.BeginBrushStoryboard(textBlock, dic); 72 | }); 73 | StoryboardUtils.BeginBrushStoryboard(borderOpen, dic); 74 | }; 75 | 76 | gridOpen.MouseLeftButtonUp += (s, e) => 77 | { 78 | OpenFromDialog(); 79 | }; 80 | 81 | KeyDown += (s, e) => 82 | { 83 | switch (e.Key) 84 | { 85 | case Key.F1: 86 | OpenFromDialog(); 87 | break; 88 | case Key.F11: 89 | this.SetFullScreen(); 90 | break; 91 | case Key.Escape: 92 | this.SetFullScreen(true); 93 | break; 94 | case Key.R: 95 | ResetCamera(); 96 | break; 97 | } 98 | }; 99 | 100 | viewer.Selector = async ss => 101 | { 102 | string selected = await new ModelSelectionDialog(ss).GetSelectedAsync(); 103 | 104 | if (string.IsNullOrEmpty(selected)) 105 | { 106 | CancelModel(); 107 | return null; 108 | } 109 | return selected; 110 | }; 111 | } 112 | 113 | public void LoadModel(string path) 114 | { 115 | if (!string.IsNullOrEmpty(path)) 116 | { 117 | viewer.ModelPath = path; 118 | viewer.Visibility = Visibility.Visible; 119 | gridOpen.Visibility = Visibility.Collapsed; 120 | } 121 | } 122 | 123 | public void CancelModel() 124 | { 125 | viewer.CancelLoadModel(); 126 | viewer.Visibility = Visibility.Collapsed; 127 | gridOpen.Visibility = Visibility.Visible; 128 | } 129 | 130 | private void OpenFromDialog() 131 | { 132 | OpenFileDialog dialog = new() 133 | { 134 | Title = "Select Model", 135 | Filter = "DMM(*.pmx,*.zip,*.7z,*.rar)|*.pmx;*.zip;*.7z;*.rar", 136 | RestoreDirectory = true, 137 | DefaultExt = "pmx", 138 | InitialDirectory = Directory.Exists(ForDispatcher.ApplicationModelPath) ? ForDispatcher.ApplicationModelPath : null, 139 | }; 140 | if (dialog.ShowDialog() ?? false) 141 | { 142 | LoadModel(dialog.FileName); 143 | } 144 | } 145 | 146 | private void ResetCamera() 147 | { 148 | viewer.ResetCamera(); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Models/ForDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace GenshinModelViewer.Models 5 | { 6 | public class ForDispatcher 7 | { 8 | public static string ApplicationModelPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"GenshinModelViewer\models"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\x64\Release\net6.0-windows\publish\win-x64\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net6.0-windows 13 | win-x64 14 | false 15 | true 16 | false 17 | 18 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Properties/PublishProfiles/FolderProfile.pubxml.user: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | True|2022-09-03T22:40:41.7533480Z;True|2022-09-04T06:39:25.2113449+08:00;True|2022-09-04T03:27:31.2089502+08:00;False|2022-09-04T03:26:57.3380610+08:00;False|2022-09-04T03:25:59.8768989+08:00;False|2022-09-04T03:25:38.2885156+08:00;True|2022-09-04T03:25:13.5652414+08:00;True|2022-09-04T03:24:40.0539847+08:00;True|2022-09-04T03:23:22.5982047+08:00;True|2022-09-04T03:22:41.3387677+08:00;True|2022-09-04T03:18:47.1935821+08:00; 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/7z.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/7z.dll -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/Resources.xaml: -------------------------------------------------------------------------------- 1 |  3 | /GenshinModelViewer;component/Resources/fonts/GenshinIcons/fonts/GenshinIcons.ttf#GenshinIcons 4 | /GenshinModelViewer;component/Resources/fonts/KhaenriahNeue-Chasm-2.000.otf#Khaenriah Neue Chasm 5 | /GenshinModelViewer;component/Resources/fonts/Segoe MDL2 Assets.ttf#Segoe MDL2 Assets 6 | /GenshinModelViewer;component/Resources/fonts/Segoe Fluent Icons.ttf#Segoe Fluent Icons 7 | 8 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/UI_AvatarIcon_Side_Yunjin.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/UI_AvatarIcon_Side_Yunjin.ico -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/UI_AvatarIcon_Side_Yunjin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/UI_AvatarIcon_Side_Yunjin.png -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=JavaScript 2 | *.css linguist-language=JavaScript 3 | *.js linguist-detectable=true 4 | demo-files/*.js linguist-vendored 5 | demo-files/*.css linguist-vendored 6 | demo.html linguist-vendored 7 | style.css linguist-vendored 8 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/.gitignore: -------------------------------------------------------------------------------- 1 | Read Me.txt 2 | selection.json 3 | fonts/icomoon.svg 4 | fonts/icomoon.eot 5 | fonts/icomoon.woff 6 | GenshinIcons-v*.zip 7 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/GenshinIcons.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "GenshinIcons", 4 | "lastOpened": 0, 5 | "created": 1662227419050 6 | }, 7 | "iconSets": [ 8 | { 9 | "selection": [ 10 | { 11 | "order": 8, 12 | "id": 6, 13 | "name": "anemo", 14 | "prevSize": 32, 15 | "code": 59648, 16 | "tempChar": "" 17 | }, 18 | { 19 | "order": 7, 20 | "id": 5, 21 | "name": "cryo", 22 | "prevSize": 32, 23 | "code": 59649, 24 | "tempChar": "" 25 | }, 26 | { 27 | "order": 6, 28 | "id": 4, 29 | "name": "dendro", 30 | "prevSize": 32, 31 | "code": 59650, 32 | "tempChar": "" 33 | }, 34 | { 35 | "order": 5, 36 | "id": 3, 37 | "name": "electro", 38 | "prevSize": 32, 39 | "code": 59651, 40 | "tempChar": "" 41 | }, 42 | { 43 | "order": 4, 44 | "id": 2, 45 | "name": "geo", 46 | "prevSize": 32, 47 | "code": 59652, 48 | "tempChar": "" 49 | }, 50 | { 51 | "order": 3, 52 | "id": 1, 53 | "name": "hydro", 54 | "prevSize": 32, 55 | "code": 59653, 56 | "tempChar": "" 57 | }, 58 | { 59 | "order": 2, 60 | "id": 0, 61 | "name": "pyro", 62 | "prevSize": 32, 63 | "code": 59654, 64 | "tempChar": "" 65 | } 66 | ], 67 | "id": 0, 68 | "metadata": { 69 | "name": "Untitled Set", 70 | "importSize": { 71 | "width": 256, 72 | "height": 256 73 | } 74 | }, 75 | "height": 1024, 76 | "prevSize": 32, 77 | "icons": [ 78 | { 79 | "id": 6, 80 | "paths": [ 81 | "M436.667 611.334c27.604 60.544-60.667 62-60.667 62 1.333 33.333 38.667 34.667 38.667 34.667 56-8 50.667-66.667 50.667-66.667-2-93.334-142-90.667-142-90.667-115.333 10.667-196-132-196-132-31.333 109.998 22 167.334 22 167.334 81.132 92 208 13.333 208 13.333 59.999-34.667 79.333 12 79.333 12zM471 512c-205-186-91-361-91-361-7-9-145.5 57-122 190 24.384 138.002 213 171 213 171z", 82 | "M615.965 718.018l-21.334 13.334s-80-15.346-76.334-79.346c0 0-3.332-106.669 184.334-125.336 0 0 218.334-28.67 100.334-264.67 0 0-1 185.523-213 259.262 0 0-71.965 37.738-84.483 88.738-12.518-51-84.482-88.738-84.482-88.738-212-73.739-213-259.262-213-259.262-118 236 100.333 264.67 100.333 264.67 187.665 18.667 184.333 125.336 184.333 125.336 3.667 64-76.333 79.346-76.333 79.346l-21.333-13.334c-21-3-31.333 5.329-31.333 5.329 108 95.343 141.815 196.334 141.815 196.334s33.815-100.991 141.815-196.334c0 0-10.332-8.329-31.332-5.329zM505.482 848.343c-3.518-12.666-34.148-56.676-34.148-56.676 19.667-7.666 34.148-23 34.148-23s14.481 15.334 34.149 23c0 0-30.631 44.010-34.149 56.676zM505.482 741.333c-23.851 31-61.148 16.667-61.148 16.667 43.333-7.667 61.148-51.667 61.148-51.667s17.815 44 61.149 51.667c0 0-37.297 14.333-61.149-16.667z", 83 | "M752.965 341c23.5-133-115-199-122-190 0 0 114 175-91 361 0 0 188.615-32.998 213-171zM883.631 418.667s-80.666 142.667-196 132c0 0-140-2.667-142 90.667 0 0-5.334 58.667 50.666 66.667 0 0 37.334-1.334 38.668-34.667 0 0-88.271-1.456-60.668-62 0 0 19.334-46.667 79.334-12 0 0 126.867 78.667 208-13.333 0 0 53.334-57.337 22-167.334z" 84 | ], 85 | "attrs": [ 86 | {}, 87 | {}, 88 | {} 89 | ], 90 | "isMulticolor": false, 91 | "isMulticolor2": false, 92 | "grid": 0, 93 | "tags": [ 94 | "anemo" 95 | ] 96 | }, 97 | { 98 | "id": 5, 99 | "paths": [ 100 | "M475 257s35.999-28 37-39v-121s-72 148-108 192l34-10s49.999 43 74 184v-91s-20-101-37-115zM512 97v121c1.001 11 37 39 37 39-17 14-37 115-37 115v91c24.001-141 74-184 74-184l34 10c-36-44-108-192-108-192z", 101 | "M492.592 409.814l-42.596-18.116-5.33 45.969s59.007 56.596 67.194 60.545l0.064 0.111 0.004-0.077 0.070 0.035-0.065-0.113c0.624-9.067-19.341-88.354-19.341-88.354zM702.324 345.598s42.248 17.176 52.274 12.542l104.789-60.5s-164.171 11.646-220.276 2.47l25.66 24.445s-12.239 64.8-122.349 156.085l78.809-45.5c0 0.001 77.469-67.819 81.093-89.542zM859.389 297.641l-104.789 60.5c-9.026 6.367-15.275 51.543-15.275 51.543-20.624-7.723-118.093 25.457-118.093 25.457l-78.809 45.5c134.11-49.714 196.349-27.914 196.349-27.914l8.34 34.445c20.105-53.177 112.277-189.531 112.277-189.531z", 102 | "M578.779 437.241l-5.592-45.938 0.012-0.021-0.014 0.006-0.002-0.016-0.013 0.022-42.476 18.369s-19.51 79.399-18.836 88.464l-0.063 0.111c0.021-0.009 0.047-0.024 0.068-0.035 0.002 0.024 0.002 0.055 0.005 0.078l0.065-0.113c8.165-3.995 66.846-60.927 66.846-60.927z", 103 | "M739.258 586.766s6.249 45.176 15.275 51.542l104.789 60.5s-92.172-136.353-112.277-189.53l-8.34 34.445s-62.238 21.801-196.348-27.914l78.808 45.5s97.469 33.179 118.093 25.457z", 104 | "M859.322 698.809l-104.789-60.499c-10.026-4.634-52.275 12.542-52.275 12.542-3.624-21.722-81.093-89.543-81.093-89.543l-78.809-45.5c110.109 91.286 122.349 156.086 122.349 156.086l-25.66 24.445c56.105-9.177 220.277 2.469 220.277 2.469zM598.121 525.594l36.987-27.831-37.146-27.6s-78.517 22.804-86.030 27.919l-0.128 0.001 0.064 0.042-0.065 0.043h0.131c7.541 5.073 86.187 27.426 86.187 27.426z", 105 | "M548.868 739.335s-35.999 28-37 39v121s71.999-148 108-192l-34 10s-50-43-74-183.999v90.999s20 101 37 115zM511.868 899.335l-0.001-120.999c-1-11-37-39.001-37-39.001 17-13.999 37-115 37-115v-91c-24.001 141-74 184-74 184l-34-10c36 44.001 108.001 192 108.001 192z", 106 | "M531.275 586.521l42.596 18.117 5.329-45.97s-59.007-56.596-67.193-60.544l-0.065-0.111c-0.002 0.022-0.002 0.053-0.003 0.077l-0.070-0.035 0.066 0.113c-0.624 9.067 19.34 88.353 19.34 88.353z", 107 | "M321.544 650.737s-42.248-17.176-52.275-12.542l-104.789 60.5s164.171-11.647 220.277-2.47l-25.66-24.445s12.239-64.801 122.348-156.085l-78.808 45.499s-77.469 67.821-81.093 89.543z", 108 | "M164.48 698.694l104.788-60.5c9.026-6.367 15.276-51.543 15.276-51.543 20.623 7.723 118.093-25.457 118.093-25.457l78.808-45.5c-134.11 49.714-196.349 27.914-196.349 27.914l-8.34-34.446c-20.106 53.178-112.276 189.532-112.276 189.532zM445.088 559.095l5.592 45.938-0.012 0.021 0.014-0.006 0.001 0.017 0.013-0.022 42.476-18.37s19.51-79.399 18.836-88.463l0.063-0.112c-0.021 0.009-0.048 0.024-0.068 0.035l-0.004-0.078-0.065 0.114c-8.164 3.993-66.846 60.926-66.846 60.926z", 109 | "M284.61 409.57s-6.25-45.176-15.276-51.542l-104.789-60.5s92.173 136.353 112.277 189.53l8.34-34.445s62.238-21.801 196.347 27.914l-78.807-45.5c0.001 0-97.468-33.179-118.092-25.457zM164.546 297.527l104.788 60.499c10.027 4.633 52.276-12.542 52.276-12.542 3.624 21.722 81.093 89.543 81.093 89.543l78.808 45.5c-110.108-91.286-122.349-156.086-122.349-156.086l25.661-24.445c-56.106 9.176-220.277-2.469-220.277-2.469z", 110 | "M425.747 470.741l-36.987 27.831 37.147 27.6s78.517-22.803 86.029-27.918l0.129-0.001-0.065-0.042c0.020-0.014 0.046-0.029 0.066-0.042h-0.132c-7.541-5.074-86.187-27.428-86.187-27.428z" 111 | ], 112 | "attrs": [ 113 | {}, 114 | {}, 115 | {}, 116 | {}, 117 | {}, 118 | {}, 119 | {}, 120 | {}, 121 | {}, 122 | {}, 123 | {} 124 | ], 125 | "isMulticolor": false, 126 | "isMulticolor2": false, 127 | "grid": 0, 128 | "tags": [ 129 | "cryo" 130 | ] 131 | }, 132 | { 133 | "id": 4, 134 | "paths": [ 135 | "M511.572 316.734v0.266c0.073-0.044 0.142-0.089 0.214-0.133l0.214 0.133v-0.266c127.394-77.83 8.078-196.516 0-204.324v-0.41l-0.214 0.204-0.214-0.204v0.41c-8.078 7.809-127.394 126.494 0 204.324zM287.5 580s21-21.75 59.75-10c0 0 36.25 11 58.5 29.25 0 0 52.5 31 64 35 0 0-81-66.5-92.5-133.5 0 0-9.75-77.75 81.75-71.75 0 0-10-44.5-78.5-41 0 0-73-2.5-82 82.5 0 0-2.25 31 35.75 71.75 0 0-40-5.25-46.75 37.75z", 136 | "M824.572 530c11-113.5-45.5-169-45.5-169 98 31 105.5-91.5 105.5-91.5-130-21.5-116.5 79.5-116.5 79.5-145-126-243-5-243-5-13 24 27.5 48 27.5 48 18.5-56 80.5-51.5 80.5-51.5 145 1.5 142.5 142 142.5 142 1.5 140-122.5 184-122.5 184-112.15 41.643-138.757 94.765-141.286 100.34-2.53-5.575-29.136-58.697-141.286-100.34 0 0-124-44-122.5-184 0 0-2.5-140.5 142.5-142 0 0 62-4.5 80.5 51.5 0 0 40.5-24 27.5-48 0 0-98-121-243 5 0 0 13.5-101-116.5-79.5 0 0 7.5 122.5 105.5 91.5 0 0-56.5 55.5-45.5 169 0 0-36.5-48-87 21 0 0 62.5 51 89.5-8 0 0 13.5 99 114.5 159 0 0 54.25 38.75 96.5 54.25 0 0 57.766 21.61 99.072 128.65v1.099c0.071-0.186 0.143-0.364 0.214-0.549 0.071 0.185 0.143 0.363 0.214 0.549v-1.099c41.305-107.040 99.072-128.65 99.072-128.65 42.25-15.5 96.5-54.25 96.5-54.25 101-60 114.5-159 114.5-159 27 59 89.5 8 89.5 8-50.5-69-87-21-87-21z", 137 | "M553.822 634.25c11.5-4 64-35 64-35 22.25-18.25 58.5-29.25 58.5-29.25 38.75-11.75 59.75 10 59.75 10-6.75-43-46.75-37.75-46.75-37.75 38-40.75 35.75-71.75 35.75-71.75-9-85-82-82.5-82-82.5-68.5-3.5-78.5 41-78.5 41 91.5-6 81.75 71.75 81.75 71.75-11.5 67-92.5 133.5-92.5 133.5z" 138 | ], 139 | "attrs": [ 140 | {}, 141 | {}, 142 | {} 143 | ], 144 | "isMulticolor": false, 145 | "isMulticolor2": false, 146 | "grid": 0, 147 | "tags": [ 148 | "dendro" 149 | ] 150 | }, 151 | { 152 | "id": 3, 153 | "paths": [ 154 | "M506.482 707.444s-60.077-91.746 14.887-164.088l0.082 0.273s61.361-56.949 118.21-28.747c56.849 28.203 39.907 81.545 28.884 105.973 0 0 106.062-30.37 129.396-145.455 0 0 13.834-61.294-12.812-149.81 0 0 49.431 22.38 105.318 136.246 0 0-17.828-155.785-146.725-252.528 0 0-105.854-81.322-231.335-76.981 0 0 260.969 111.988 171.63 286.729 0 0-49.416 97.901-149.548 69.152l0.196-0.208s-80-24.666-84-87.999 50.667-75.333 77.333-78c0 0-79.333-76.667-190.667-39.333 0 0-60 18.667-123.333 86 0 0-5.333-53.998 65.333-159.332 0 0-126 93.332-145.333 253.332 0 0-17.5 132.332 49 238.832 0 0-33.5-282 162.5-292 0 0 109.491-6.154 134.66 94.936l-0.277-0.066s18.639 81.615-34.209 116.746c-52.849 35.131-90.574-6.212-106.217-27.972 0 0-26.729 107.037 61.27 184.788 0 0 46.166 42.628 136.145 63.81 0 0-44.098 31.618-170.652 23.085 0 0 143.828 62.453 292.059-0.803 0 0 123.354-51.011 182.335-161.852 0 0.001-227.469 170.013-334.13 5.272z" 155 | ], 156 | "attrs": [ 157 | {} 158 | ], 159 | "isMulticolor": false, 160 | "isMulticolor2": false, 161 | "grid": 0, 162 | "tags": [ 163 | "electro" 164 | ] 165 | }, 166 | { 167 | "id": 2, 168 | "paths": [ 169 | "M512 123s-148 105-257 239c0 0 31 109 65 164 0 0 126.999-138 192-153 0 0 65 52 79 103 0 0-83 77-91 84 0 0 14 21 31 38 0 0 109-77 135-114 0 0-56.001-111-154-205 0 0-137 113-165 140 0 0 6.999-92 165-253 0 0 196 207 284 457 0 0 52-62.334 80-111 0 0-123-224-364-389z", 170 | "M512 651s-65-52-79-103c0 0 83-77 91-84 0 0-14-21-31-38 0 0-109 77-135 114 0 0 56.001 111 154 205 0 0 137-113 165-140 0 0-6.999 92-165 253 0 0-196-207-284-457 0 0-52 62.334-80 111 0 0 123 224 364 389 0 0 148-105 257-239 0 0-31-109-65-164 0 0-126.999 138-192 153z" 171 | ], 172 | "attrs": [ 173 | {}, 174 | {} 175 | ], 176 | "isMulticolor": false, 177 | "isMulticolor2": false, 178 | "grid": 0, 179 | "tags": [ 180 | "geo" 181 | ] 182 | }, 183 | { 184 | "id": 1, 185 | "paths": [ 186 | "M889.071 478.092c-3-118.494-116.404-325.425-347.071-340.758s-360.5 162.666-367 203.166 23.5 53.597 23.5 53.597c30 20.597 62.103 0 62.103 0 117.269-159.239 271.372-135.428 271.372-135.428 294.641 37.333 284.692 307.998 284.692 307.998-1.333 185.333-164.167 263.333-164.167 263.333 156.5-99.5 119.5-262 119.5-262-45-230-260-221.333-260-221.333-78.667 2-108.333 25-108.333 25-29.667 22.333-17.667 55.333-17.667 55.333 11 45 58 36 58 36 49-19 87.975-10 87.975-10 193.974 33 182.025 211.667 182.025 211.667-22.667 188.001-202 189.333-202 189.333-95.333 9.333-219.898-59.424-219.898-59.424 37.498 92.995 239.473 94.495 239.473 94.495 392.464-53.997 357.496-410.979 357.496-410.979z", 187 | "M498 644s36.5-4 45.5 49.5c0 0 16 45.5 65.5 29.5 0 0 37-9.5 28-71.5 0 0-19-87.5-125-92 0 0-114.5-5.961-136.5 124.77 0 0-6 133.23 136.5 144.73 0 0 69.5-1 108-55.5 0 0 8.833-10.334 7.5-14 0 0 0.5-3-6.5 2.5s-25.5 48.5-109 47c0 0-71-3-88.5-81 0 0-9.5-80 74.5-84z", 188 | "M136 518.735c0 29.271 23.729 53 53 53s53-23.729 53-53v0c0-29.271-23.729-53-53-53s-53 23.729-53 53v0z", 189 | "M220 684c0 20.435 16.565 37 37 37s37-16.565 37-37v0c0-20.435-16.565-37-37-37s-37 16.565-37 37v0z" 190 | ], 191 | "attrs": [ 192 | {}, 193 | {}, 194 | {}, 195 | {} 196 | ], 197 | "isMulticolor": false, 198 | "isMulticolor2": false, 199 | "grid": 0, 200 | "tags": [ 201 | "hydro" 202 | ] 203 | }, 204 | { 205 | "id": 0, 206 | "paths": [ 207 | "M724 458.67s-55.333-42.667-76-133.333c0 0-35.333 26-41.333 79.333 0 0-4.001 44.667 28.666 72.667 0 0 16 23.334 18 41.334 0 0-62-51.334-77.333-115.334 0 0-14.001-32.667 13.333-116.667 0 0-77.333 48-89.333 103.333 0 0-7.69 35.452 27.333 69.333 0 0-46.667-26.666-45.333-73.333 0 0-1.334-44 71.333-101.333 0 0 34-25.333 6.667-80.667 0 0-42-73.342-64-88.004 0 0 10 56-77 155 0 0-27 39-28 82 0 0-6 77 121 120 0 0 153 46 160 139 0 0 27 84-69 132 0 0-91 46-159-9 0 0-43.666-60.666 27.667-108.333 0 0-82-25.666-101 48.667 0 0-7.667 52.667 64.667 85 0 0 84 42.034 185 1.684s124.664-96.35 128.997-141.017c0 0 9-44-22-109 0 0 75 39.334 74 105.667 0 0 6.334 56.994-93.333 112.661s-63.332 26.339-195.998 118.339c0 0-85-57.917-222-126.167 0 0-66.667-34.166-71.667-105.166 0 0-7-66.333 81.333-118.667 0 0-52.667 57.667-43.667 113 0 0 3.667 75.167 118.5 126.333 0 0-58.5-50.001-39.75-128.501 0 0 25.53-116.166 175.057-122.166 0 0-162.141-48.661-158.807-154.995 0 0-17 44.323-96 138.323 0 0-54 62.004-71 128.004 0 0-16 59.333 9 88.333 0 0 20 35.5 122.5 89 0 0 73 37 196.5 134.5 0 0 43.333-53.166 197.333-137.166 0 0 101.333-46 120-128.667 0.001 0.002 21.335-91.328-105.332-185.995z", 208 | "M623.5 634c-3.5-75-66.5-97-66.5-97-118.5-36-150.5 61.5-150.5 61.5 100.5-45 138.5 27.5 138.5 27.5 28 42-5 86-5 86 82-26 83.5-78 83.5-78z" 209 | ], 210 | "attrs": [ 211 | {}, 212 | {} 213 | ], 214 | "isMulticolor": false, 215 | "isMulticolor2": false, 216 | "grid": 0, 217 | "tags": [ 218 | "pyro" 219 | ] 220 | } 221 | ], 222 | "invisible": false, 223 | "colorThemes": [] 224 | } 225 | ], 226 | "uid": -1, 227 | "preferences": { 228 | "showGlyphs": true, 229 | "showCodes": true, 230 | "showQuickUse": true, 231 | "showQuickUse2": true, 232 | "showSVGs": true, 233 | "fontPref": { 234 | "prefix": "icon-", 235 | "metadata": { 236 | "fontFamily": "GenshinIcons", 237 | "majorVersion": 1, 238 | "minorVersion": 0 239 | }, 240 | "metrics": { 241 | "emSize": 1024, 242 | "baseline": 6.25, 243 | "whitespace": 50 244 | }, 245 | "embed": false, 246 | "showVersion": true, 247 | "showMetadata": false, 248 | "showMetrics": false, 249 | "showSelector": false 250 | }, 251 | "imagePref": { 252 | "prefix": "icon-", 253 | "png": true, 254 | "useClassSelector": true, 255 | "color": 0, 256 | "bgColor": 16777215 257 | }, 258 | "historySize": 50 259 | }, 260 | "time": 1662227483590 261 | } -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/demo-files/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: sans-serif; 5 | font-size: 1em; 6 | line-height: 1.5; 7 | color: #555; 8 | background: #fff; 9 | } 10 | h1 { 11 | font-size: 1.5em; 12 | font-weight: normal; 13 | } 14 | small { 15 | font-size: .66666667em; 16 | } 17 | a { 18 | color: #e74c3c; 19 | text-decoration: none; 20 | } 21 | a:hover, a:focus { 22 | box-shadow: 0 1px #e74c3c; 23 | } 24 | .bshadow0, input { 25 | box-shadow: inset 0 -2px #e7e7e7; 26 | } 27 | input:hover { 28 | box-shadow: inset 0 -2px #ccc; 29 | } 30 | input, fieldset { 31 | font-family: sans-serif; 32 | font-size: 1em; 33 | margin: 0; 34 | padding: 0; 35 | border: 0; 36 | } 37 | input { 38 | color: inherit; 39 | line-height: 1.5; 40 | height: 1.5em; 41 | padding: .25em 0; 42 | } 43 | input:focus { 44 | outline: none; 45 | box-shadow: inset 0 -2px #449fdb; 46 | } 47 | .glyph { 48 | font-size: 16px; 49 | width: 15em; 50 | padding-bottom: 1em; 51 | margin-right: 4em; 52 | margin-bottom: 1em; 53 | float: left; 54 | overflow: hidden; 55 | } 56 | .liga { 57 | width: 80%; 58 | width: calc(100% - 2.5em); 59 | } 60 | .talign-right { 61 | text-align: right; 62 | } 63 | .talign-center { 64 | text-align: center; 65 | } 66 | .bgc1 { 67 | background: #f1f1f1; 68 | } 69 | .fgc1 { 70 | color: #999; 71 | } 72 | .fgc0 { 73 | color: #000; 74 | } 75 | p { 76 | margin-top: 1em; 77 | margin-bottom: 1em; 78 | } 79 | .mvm { 80 | margin-top: .75em; 81 | margin-bottom: .75em; 82 | } 83 | .mtn { 84 | margin-top: 0; 85 | } 86 | .mtl, .mal { 87 | margin-top: 1.5em; 88 | } 89 | .mbl, .mal { 90 | margin-bottom: 1.5em; 91 | } 92 | .mal, .mhl { 93 | margin-left: 1.5em; 94 | margin-right: 1.5em; 95 | } 96 | .mhmm { 97 | margin-left: 1em; 98 | margin-right: 1em; 99 | } 100 | .mls { 101 | margin-left: .25em; 102 | } 103 | .ptl { 104 | padding-top: 1.5em; 105 | } 106 | .pbs, .pvs { 107 | padding-bottom: .25em; 108 | } 109 | .pvs, .pts { 110 | padding-top: .25em; 111 | } 112 | .unit { 113 | float: left; 114 | } 115 | .unitRight { 116 | float: right; 117 | } 118 | .size1of2 { 119 | width: 50%; 120 | } 121 | .size1of1 { 122 | width: 100%; 123 | } 124 | .clearfix:before, .clearfix:after { 125 | content: " "; 126 | display: table; 127 | } 128 | .clearfix:after { 129 | clear: both; 130 | } 131 | .hidden-true { 132 | display: none; 133 | } 134 | .textbox0 { 135 | width: 3em; 136 | background: #f1f1f1; 137 | padding: .25em .5em; 138 | line-height: 1.5; 139 | height: 1.5em; 140 | } 141 | #testDrive { 142 | display: block; 143 | padding-top: 24px; 144 | line-height: 1.5; 145 | } 146 | .fs0 { 147 | font-size: 16px; 148 | } 149 | .fs1 { 150 | font-size: 32px; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/demo-files/demo.js: -------------------------------------------------------------------------------- 1 | if (!('boxShadow' in document.body.style)) { 2 | document.body.setAttribute('class', 'noBoxShadow'); 3 | } 4 | 5 | document.body.addEventListener("click", function(e) { 6 | var target = e.target; 7 | if (target.tagName === "INPUT" && 8 | target.getAttribute('class').indexOf('liga') === -1) { 9 | target.select(); 10 | } 11 | }); 12 | 13 | (function() { 14 | var fontSize = document.getElementById('fontSize'), 15 | testDrive = document.getElementById('testDrive'), 16 | testText = document.getElementById('testText'); 17 | function updateTest() { 18 | testDrive.innerHTML = testText.value || String.fromCharCode(160); 19 | if (window.icomoonLiga) { 20 | window.icomoonLiga(testDrive); 21 | } 22 | } 23 | function updateSize() { 24 | testDrive.style.fontSize = fontSize.value + 'px'; 25 | } 26 | fontSize.addEventListener('change', updateSize, false); 27 | testText.addEventListener('input', updateTest, false); 28 | testText.addEventListener('change', updateTest, false); 29 | updateSize(); 30 | }()); 31 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IcoMoon Demo 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Font Name: GenshinIcons (Glyphs: 7)

13 |
14 |
15 |

Grid Size: Unknown

16 |
17 |
18 | 19 | icon-anemo 20 |
21 |
22 | 23 | 24 |
25 |
26 | liga: 27 | 28 |
29 |
30 |
31 |
32 | 33 | icon-cryo 34 |
35 |
36 | 37 | 38 |
39 |
40 | liga: 41 | 42 |
43 |
44 |
45 |
46 | 47 | icon-dendro 48 |
49 |
50 | 51 | 52 |
53 |
54 | liga: 55 | 56 |
57 |
58 |
59 |
60 | 61 | icon-electro 62 |
63 |
64 | 65 | 66 |
67 |
68 | liga: 69 | 70 |
71 |
72 |
73 |
74 | 75 | icon-geo 76 |
77 |
78 | 79 | 80 |
81 |
82 | liga: 83 | 84 |
85 |
86 |
87 |
88 | 89 | icon-hydro 90 |
91 |
92 | 93 | 94 |
95 |
96 | liga: 97 | 98 |
99 |
100 |
101 |
102 | 103 | icon-pyro 104 |
105 |
106 | 107 | 108 |
109 |
110 | liga: 111 | 112 |
113 |
114 |
115 | 116 | 117 |
118 |

Font Test Drive

119 | 124 | 126 |
  127 |
128 |
129 | 130 |
131 |

Generated by IcoMoon

132 |
133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.eot -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.ttf -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/GenshinIcons/fonts/GenshinIcons.woff -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/GenshinIcons/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'GenshinIcons'; 3 | src: url('fonts/GenshinIcons.eot?wk6p9g'); 4 | src: url('fonts/GenshinIcons.eot?wk6p9g#iefix') format('embedded-opentype'), 5 | url('fonts/GenshinIcons.ttf?wk6p9g') format('truetype'), 6 | url('fonts/GenshinIcons.woff?wk6p9g') format('woff'), 7 | url('fonts/GenshinIcons.svg?wk6p9g#GenshinIcons') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: block; 11 | } 12 | 13 | [class^="icon-"], [class*=" icon-"] { 14 | /* use !important to prevent issues with browser extensions that change fonts */ 15 | font-family: 'GenshinIcons' !important; 16 | speak: never; 17 | font-style: normal; 18 | font-weight: normal; 19 | font-variant: normal; 20 | text-transform: none; 21 | line-height: 1; 22 | 23 | /* Better Font Rendering =========== */ 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .icon-anemo:before { 29 | content: "\e900"; 30 | } 31 | .icon-cryo:before { 32 | content: "\e901"; 33 | } 34 | .icon-dendro:before { 35 | content: "\e902"; 36 | } 37 | .icon-electro:before { 38 | content: "\e903"; 39 | } 40 | .icon-geo:before { 41 | content: "\e904"; 42 | } 43 | .icon-hydro:before { 44 | content: "\e905"; 45 | } 46 | .icon-pyro:before { 47 | content: "\e906"; 48 | } 49 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/KhaenriahNeue-Chasm-2.000.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/KhaenriahNeue-Chasm-2.000.otf -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/README.md: -------------------------------------------------------------------------------- 1 | # Fonts 2 | 3 | https://docs.microsoft.com/en-us/windows/apps/design/downloads/#fonts -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/Segoe Fluent Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/Segoe Fluent Icons.ttf -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/fonts/Segoe MDL2 Assets.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GenshinMatrix/genshin-model-viewer/087d68ccf623a62afba06b0430212cecf94db00f/src/GenshinModelViewer/Resources/fonts/Segoe MDL2 Assets.ttf -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/anemo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/cryo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/dendro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/electro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/geo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/hydro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Resources/svgs/pyro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/HelixViewer.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/HelixViewer.xaml.cs: -------------------------------------------------------------------------------- 1 | using HelixToolkit.Wpf; 2 | using Model.Viewer.Plugin; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Media; 11 | using System.Windows.Media.Media3D; 12 | 13 | namespace GenshinModelViewer.Views 14 | { 15 | public partial class HelixViewer : ObservableUserControl 16 | { 17 | public string ModelPath 18 | { 19 | get => (string)GetValue(ModelPathProperty); 20 | set => SetValue(ModelPathProperty, value); 21 | } 22 | public static readonly DependencyProperty ModelPathProperty = DependencyProperty.Register("ModelPath", typeof(string), typeof(HelixViewer), new PropertyMetadata(string.Empty, OnPathChanged)); 23 | 24 | public Model3DGroup Models 25 | { 26 | get => (Model3DGroup)GetValue(ModelsProperty); 27 | set => SetValue(ModelsProperty, value); 28 | } 29 | public static readonly DependencyProperty ModelsProperty = DependencyProperty.Register("Models", typeof(Model3DGroup), typeof(HelixViewer), new PropertyMetadata(null)); 30 | 31 | //protected Model3DGroup models = new(); 32 | //public Model3DGroup Models 33 | //{ 34 | // get => models; 35 | // set => Set(ref models, value); 36 | //} 37 | 38 | public Func> Selector = null; 39 | 40 | private readonly PMXProvider loader = new(); 41 | private PMXFormat format; 42 | 43 | public HelixViewer() 44 | { 45 | DataContext = this; 46 | InitializeComponent(); 47 | } 48 | 49 | private static void OnPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 50 | { 51 | if (d is HelixViewer self) 52 | { 53 | self.LoadModel((e.NewValue as string)!, self.Selector); 54 | } 55 | } 56 | 57 | public async void LoadModel(string path, Func> selector = null) 58 | { 59 | if (!File.Exists(path)) 60 | { 61 | return; 62 | } 63 | 64 | try 65 | { 66 | loader.Load(path); 67 | 68 | format = (await loader.GetPMXFormat(path, selector)) ?? throw new FormatException("Unsupported format"); 69 | 70 | MeshCreationInfo creation_info = CreateMeshCreationInfoSingle(); 71 | 72 | int mats = format.material_list.material.Length; 73 | 74 | Model3DGroup models = new(); 75 | 76 | for (int i = 0, i_max = creation_info.value.Length; i < i_max; ++i) 77 | { 78 | try 79 | { 80 | // format_.face_vertex_list.face_vert_indexを[start](含む)から[start+count](含まず)迄取り出し 81 | // 頂点リアサインインデックス変換 82 | int[] indices = creation_info.value[i].plane_indices.Select(x => (int)creation_info.reassign_dictionary[x]).ToArray(); 83 | MeshGeometry3D mesh = new() 84 | { 85 | Positions = new Point3DCollection(format.vertex_list.vertex.Select(x => x.pos)), 86 | TextureCoordinates = new PointCollection(format.vertex_list.vertex.Select(x => x.uv)), 87 | }; 88 | 89 | indices.ToList().ForEach(x => mesh.TriangleIndices.Add(x)); 90 | 91 | uint textureIndex = format.material_list.material[i].usually_texture_index; 92 | 93 | Material material; 94 | 95 | if (textureIndex == uint.MaxValue) 96 | { 97 | material = new DiffuseMaterial(new SolidColorBrush(Color.FromRgb(160, 160, 160))); 98 | } 99 | else 100 | { 101 | // Texture 102 | ImageSource bitmapImage = loader.GetTexture(format.meta_header.folder, format.texture_list.texture_file[textureIndex]); 103 | 104 | #if DEBUG && TEXTURE 105 | bitmapImage.Save(format.meta_header.folder + @"\.debug\" + format.texture_list.texture_file[textureIndex] + ".png"); 106 | #endif 107 | ImageBrush colors_brush = new() 108 | { 109 | ImageSource = bitmapImage, 110 | }; 111 | material = new DiffuseMaterial(colors_brush); 112 | } 113 | 114 | GeometryModel3D model = new(mesh, material) 115 | { 116 | BackMaterial = material, 117 | }; 118 | models.Children.Add(model); 119 | } 120 | catch (Exception e) 121 | { 122 | Logger.Warn(e.ToString()); 123 | } 124 | } 125 | Models?.Children.Clear(); 126 | Models = models; 127 | } 128 | catch (Exception e) 129 | { 130 | Logger.Error(e.ToString()); 131 | await new MessageDialog("Failed", e.ToString()).ShowAsync(); 132 | } 133 | } 134 | 135 | public void CancelLoadModel() 136 | { 137 | ModelPath = string.Empty; 138 | Models = null; 139 | loader?.Dispose(); 140 | } 141 | 142 | private MeshCreationInfo CreateMeshCreationInfoSingle() 143 | { 144 | MeshCreationInfo result = new() 145 | { 146 | // 全マテリアルを設定 147 | value = CreateMeshCreationInfoPacks(), 148 | // 全頂点を設定 149 | all_vertices = Enumerable.Range(0, format.vertex_list.vertex.Length).Select(x => (uint)x).ToArray() 150 | }; 151 | // 頂点リアサインインデックス用辞書作成 152 | result.reassign_dictionary = new Dictionary(result.all_vertices.Length); 153 | for (uint i = 0, i_max = (uint)result.all_vertices.Length; i < i_max; ++i) 154 | { 155 | result.reassign_dictionary[i] = i; 156 | } 157 | return result; 158 | } 159 | 160 | private MeshCreationInfo.Pack[] CreateMeshCreationInfoPacks() 161 | { 162 | uint plane_start = 0; 163 | // マテリアル単位のMeshCreationInfo.Packを作成する 164 | return Enumerable.Range(0, format.material_list.material.Length).Select(x => 165 | { 166 | MeshCreationInfo.Pack pack = new() 167 | { 168 | material_index = (uint)x, 169 | }; 170 | uint plane_count = format.material_list.material[x].face_vert_count; 171 | pack.plane_indices = format.face_vertex_list.face_vert_index.Skip((int)plane_start).Take((int)plane_count).ToArray(); 172 | pack.vertices = pack.plane_indices.Distinct().ToArray(); 173 | plane_start += plane_count; 174 | return pack; 175 | }).ToArray(); 176 | } 177 | 178 | public void ResetCamera() 179 | { 180 | camera.FieldOfView = 45; 181 | camera.FarPlaneDistance = 30000; 182 | camera.LookDirection = new(0, 0, 49.867532564294045); 183 | camera.NearPlaneDistance = 0.1; 184 | camera.Position = new(-2.384185791015625E-07, 9.745553016662598, -49.222887469422524); 185 | camera.UpDirection = new(0, 1, 0); 186 | } 187 | } 188 | 189 | public class MeshCreationInfo 190 | { 191 | public class Pack 192 | { 193 | public uint material_index; // マテリアル 194 | public uint[] plane_indices; // 面 195 | public uint[] vertices; // 頂点 196 | } 197 | 198 | public Pack[] value; 199 | public uint[] all_vertices; // 総頂点 200 | public Dictionary reassign_dictionary; // 頂点リアサインインデックス用辞書 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/MessageBox/MessageDialog.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/MessageBox/MessageDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using ModernWpf.Controls; 2 | using System.Windows; 3 | 4 | namespace Model.Viewer.Plugin 5 | { 6 | public partial class MessageDialog : ContentDialog 7 | { 8 | public string Message 9 | { 10 | get => (string)GetValue(MessageProperty); 11 | set => SetValue(MessageProperty, value); 12 | } 13 | public static readonly DependencyProperty MessageProperty = DependencyProperty.Register("Message", typeof(string), typeof(ModelSelectionDialog), new PropertyMetadata(null!)); 14 | 15 | public MessageDialog(string title, string message) 16 | { 17 | Message = message; 18 | DataContext = this; 19 | InitializeComponent(); 20 | Title = title; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/ModelSelectionDialog.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/ModelSelectionDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using ModernWpf.Controls; 2 | using System.Windows; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Model.Viewer.Plugin 7 | { 8 | public partial class ModelSelectionDialog : ContentDialog 9 | { 10 | public class ModelSelection 11 | { 12 | public string Text { get; set; } 13 | public override string ToString() => Text; 14 | } 15 | 16 | public ModelSelection[] Texts 17 | { 18 | get => (ModelSelection[])GetValue(TextsProperty); 19 | set => SetValue(TextsProperty, value); 20 | } 21 | public static readonly DependencyProperty TextsProperty = DependencyProperty.Register("Texts", typeof(ModelSelection[]), typeof(ModelSelectionDialog), new PropertyMetadata(null!)); 22 | 23 | public int SelectedIndex 24 | { 25 | get => (int)GetValue(SelectedIndexProperty); 26 | set => SetValue(SelectedIndexProperty, value); 27 | } 28 | public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register("SelectedIndex", typeof(int), typeof(ModelSelectionDialog), new PropertyMetadata(0)); 29 | 30 | public ModelSelectionDialog(string[] texts) 31 | { 32 | Texts = Array.ConvertAll(texts, m => new ModelSelection() { Text = m }); 33 | DataContext = this; 34 | InitializeComponent(); 35 | } 36 | 37 | public async Task GetSelectedAsync() 38 | { 39 | await ShowAsync(); 40 | return Texts[SelectedIndex].ToString(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/GenshinModelViewer/Views/UserControl/ObservableUserControl.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using System.Windows.Controls; 4 | 5 | namespace GenshinModelViewer.Views 6 | { 7 | public class ObservableUserControl : UserControl, INotifyPropertyChanged 8 | { 9 | public event PropertyChangedEventHandler PropertyChanged; 10 | 11 | protected virtual void Set(ref T field, T newValue, [CallerMemberName] string propertyName = null) 12 | { 13 | field = newValue; 14 | RaisePropertyChanged(propertyName); 15 | } 16 | 17 | protected virtual void Set2(ref T field, T newValue, [CallerMemberName] string propertyName = null) 18 | { 19 | if (!Equals(field, newValue)) 20 | { 21 | field = newValue; 22 | RaisePropertyChanged(propertyName); 23 | } 24 | } 25 | 26 | protected virtual void RaisePropertyChanged(string propertyName) 27 | => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 28 | } 29 | } 30 | --------------------------------------------------------------------------------