diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index 2d10d62..b2b3ff6 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -1,46 +1,80 @@ name: Build & Release on: - push: - tags: - - 'v*.*.*' + pull_request: + branches: + - main + types: + - closed jobs: build-release: + if: ${{ github.event.pull_request.merged == true }} runs-on: windows-latest env: PROJECT_NAME: SpineViewer - VERSION: ${{ github.ref_name }} + steps: - name: Checkout code uses: actions/checkout@v3 - + with: + fetch-tags: true + - name: Setup .NET SDK uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.0.x' + dotnet-version: "8.0.x" + + - name: Extract version from csproj + shell: pwsh + run: | + [xml]$proj = Get-Content "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" + $VERSION_NUM = $proj.Project.PropertyGroup.Version + $VERSION_TAG = "v$VERSION_NUM" + "VERSION=$VERSION_TAG" >> $env:GITHUB_ENV + + - name: Check Version Tag + shell: pwsh + run: | + if (-not $env:VERSION) { + Write-Error "Version tag not found in csproj file." + exit 1 + } + Write-Host "Version tag found: $env:VERSION" + + - name: Tag merge commit + shell: pwsh + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag $env:VERSION + git push --tags - name: Publish FrameworkDependent version + shell: pwsh run: | - dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc false -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }} - + dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION" + - name: Publish SelfContained version + shell: pwsh run: | - dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc true -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained - + dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained" + - name: Create release directory - run: mkdir release - + shell: pwsh + run: | + New-Item -ItemType Directory -Path release -Force | Out-Null + - name: Compress FrameworkDependent version shell: pwsh run: | - Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}.zip" -Force - + Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION.zip" -Force + - name: Compress SelfContained version shell: pwsh run: | - Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}-SelfContained" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}-SelfContained.zip" -Force - + Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION-SelfContained.zip" -Force + - name: Create GitHub Release id: create_release uses: actions/create-release@v1 @@ -51,7 +85,7 @@ jobs: release_name: Release ${{ env.VERSION }} draft: false prerelease: false - + - name: Upload FrameworkDependent zip uses: actions/upload-release-asset@v1 env: @@ -61,7 +95,7 @@ jobs: asset_path: release/${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip asset_name: ${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip asset_content_type: application/zip - + - name: Upload SelfContained zip uses: actions/upload-release-asset@v1 env: diff --git a/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj index ccab8e1..3767372 100644 --- a/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj +++ b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj @@ -7,7 +7,7 @@ net8.0-windows $(SolutionDir)out false - 0.15.0 + 0.15.1 true diff --git a/README.en.md b/README.en.md index 4fd2b57..ee465a2 100644 --- a/README.en.md +++ b/README.en.md @@ -1,111 +1,133 @@ # [SpineViewer](https://github.com/ww-rm/SpineViewer) -[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml) -[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github&logoColor=959da5&label=Release&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases) -[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github&logoColor=959da5&label=Downloads&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases) +[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml) +[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github\&logoColor=959da5\&label=Release\&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases) +[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github\&logoColor=959da5\&label=Downloads\&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases) [中文](README.md) | [English](README.en.md) -A *WYSIWYG* Spine file viewer & exporter. +A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese). ![previewer](img/preview.webp) ## Features -- Supports multiple Spine file versions -- Drag & drop or copy/paste to open files in batch -- List-based skeleton view with render layer management -- Multi-select list to batch-adjust skeleton parameters -- Multi-track animation support -- Skin / custom slot attachment configuration -- Debug rendering mode -- Fullscreen preview -- Export to single-frame image, animated GIF/WebP/AVIF, video formats -- Batch export at multiple resolutions -- Custom FFmpeg export parameters -- …and more +* Supports multiple versions of Spine files. +* Batch open files via drag-and-drop or copy-paste. +* Batch preview functionality. +* List-based multi-skeleton viewing and render order management. +* Batch adjustment of skeleton parameters using multi-selection. +* Multi-track animation settings. +* Skin and custom slot attachment settings. +* Debug rendering support. +* Fullscreen preview mode. +* Export to single frame/image sequence/animated GIF/video formats. +* Automatic resolution batch export. +* FFmpeg custom export support. +* Program parameter saving. +* ... -### Spine Version Support +### Supported Spine Versions -| Version | View & Export | Format Conversion | Version Conversion | -| :------: | :-----------: | :---------------: | :----------------: | -| `2.1.x` | :white_check_mark: | | | -| `3.6.x` | :white_check_mark: | | | -| `3.7.x` | :white_check_mark: | | | -| `3.8.x` | :white_check_mark: | :white_check_mark: | | -| `4.0.x` | :white_check_mark: | | | -| `4.1.x` | :white_check_mark: | | | -| `4.2.x` | :white_check_mark: | :white_check_mark: | | -| `4.3.x` | | | | +| Version | View & Export | +| :-----: | :------------------: | +| `2.1.x` | :white\_check\_mark: | +| `3.6.x` | :white\_check\_mark: | +| `3.7.x` | :white\_check\_mark: | +| `3.8.x` | :white\_check\_mark: | +| `4.0.x` | :white\_check\_mark: | +| `4.1.x` | :white\_check\_mark: | +| `4.2.x` | :white\_check\_mark: | +| `4.3.x` | | -More versions coming soon 🚀🚀🚀 +More versions under development \:rocket: \:rocket: \:rocket: ### Supported Export Formats -| Export Format | Use Case | -| --------------------- | ----------------------------------------------------------------------------------------- | -| Single Frame | Generate high‑resolution still images; pick any frame manually. | -| Frame Sequence (PNG) | Lossless PNG sequences with alpha channel preserved. | -| GIF / WebP / AVIF | Perfect for quick animated previews. | -| MP4 | The most widely compatible video format. | -| WebM | Browser‑friendly streaming with optional transparency. | -| MKV / MOV | For those who like to tinker. | -| Custom FFmpeg Command | Use any FFmpeg arguments for complex, tailored export workflows. | +| Format | Use Case | +| -------------- | ----------------------------------------------------------------------------- | +| Single Frame | Generate high-resolution images of models; manually adjust the desired frame. | +| Frame Sequence | Supports PNG format with transparency and lossless compression. | +| GIF/Video | Export preview animations or common video formats. | +| Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. | ## Installation -1. Go to the [Releases](https://github.com/ww-rm/SpineViewer/releases) page and download the ZIP. -2. Make sure you have the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) installed. -3. Alternatively, download the `SelfContained` ZIP, which runs standalone without any .NET prerequisites. -4. To export GIF or other video formats, install the `ffmpeg` CLI and add it to your PATH. - - Windows builds: see the [FFmpeg download page](https://ffmpeg.org/download.html#build-windows) - - Direct download: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z) +Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page. + +The software requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to run. + +Alternatively, download the package with the `SelfContained` suffix for standalone execution. + +For exporting GIF/MP4 and other animation/video formats, FFmpeg must be installed and added to the system environment variables. Visit the [FFmpeg Windows download page](https://ffmpeg.org/download.html#build-windows) or download the latest version directly: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z). ## Usage -### Importing Skeletons +### How to Change the Display Language -You can import Spine skeletons in three ways: +In the menu, go to "File" -> "Preferences..." -> "Language," select your desired language, and confirm the change. -- Drag & drop or paste skeleton files or folders onto the model list. -- Use **File > Open** to batch‑open multiple skeleton files. -- Use **File > Open Single Model** to open one at a time. +### Basic Overview -### Adjusting Content +The program is organized into a left-right layout: -- Right‑click menu and keyboard shortcuts are available in the model list. You can multi‑select to adjust parameters in batch. -- In the preview pane, you can also use mouse controls: - - **Left‑click & drag** to move a model; hold **Ctrl** to multi‑select (synced with the list). - - **Right‑click & drag** to pan the entire scene. - - **Mouse wheel** to zoom; hold **Ctrl** to zoom all selected models proportionally. - - **“Render Selected Only”** mode shows only the selected models in preview; use the list to change selection. +* **Left Panel:** Functionality panel. +* **Right Panel:** Preview display. -Below the preview, playback controls let you scrub through the timeline like a basic player. +The left panel includes three sub-panels: -### Exporting Content +* **Browse:** Preview the content of a specified folder without importing files into the program. This panel allows generating `.webp` previews for models or importing selected models. +* **Model:** Lists imported models for rendering. Parameters and rendering order can be adjusted here, along with other model-related functionalities. +* **Display:** Adjust parameters for the right-side preview display. -Exports follow the “what you see is what you get” principle—your real‑time preview is exactly what gets exported. +Hover your mouse over buttons, labels, or input fields to see help text for most UI elements. -Key export options: +### Skeleton Import -- **Render Selected Only**: includes only the selected models in both preview and export. -- **Output Folder**: if unspecified, exports go into each model’s source folder; otherwise, everything exports to the chosen folder. -- **Export Single**: by default, each model is exported separately; enable this to render all selected models together into a single output. -- **Auto Resolution**: ignores preview resolution and viewport size—exports at the content’s actual bounds; for animations, matches the full animation area. +Drag-and-drop or paste skeleton files/directories into the Model panel. -## More +Alternatively, use the right-click menu in the Browse panel to import selected items. -Detailed usage and advanced tips are in the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). -Encounter a bug or have a feature request? Open an [Issue](https://github.com/ww-rm/SpineViewer/issues). +### Content Adjustment + +The Model panel supports right-click menus, some shortcuts, and batch adjustments of model parameters through multi-selection. + +For preview display adjustments: + +* **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list. +* **Right-click:** Drag the entire display. +* **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models. +* **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list. + +The buttons below the preview display allow time adjustments, serving as a simple playback control. + +### Content Export + +Export follows the **WYSIWYG (What You See Is What You Get)** principle, meaning the preview display reflects the exported output. + +Use the right-click menu in the Model panel to export selected items. + +Key export parameters include: + +* **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder. +* **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output. +* **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility. + +### More Information + +For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). For usage questions or bug reports, submit an [Issue](https://github.com/ww-rm/SpineViewer/issues). ## Acknowledgements -- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes) -- [SFML.Net](https://github.com/SFML/SFML.Net) -- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) +* [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes) +* [SFML.Net](https://github.com/SFML/SFML.Net) +* [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) +* [HandyControl](https://github.com/HandyOrg/HandyControl) +* [NLog](https://github.com/NLog/NLog) +* [SkiaSharp](https://github.com/mono/SkiaSharp) --- -If you find this project useful, please give it a ⭐ and share it with others! +*If you find this project helpful, please give it a \:star: and share it with others! :)* [![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer) diff --git a/README.md b/README.md index b292d38..7c068f3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [中文](README.md) | [English](README.en.md) -*所见即所得* 的 Spine 文件查看&导出程序. +一个简单好用的 Spine 文件查看&导出程序, 支持中/英/日多语言界面. ![previewer](img/preview.webp) @@ -14,6 +14,7 @@ - 支持多版本 spine 文件 - 支持拖拽/复制粘贴批量打开文件 +- 支持批量预览 - 支持列表式多骨骼查看和渲染层级管理 - 支持列表多选批量设置骨骼参数 - 支持多轨道动画设置 @@ -23,20 +24,21 @@ - 支持单帧/动图/视频文件导出 - 支持自动分辨率批量导出 - 支持 FFmpeg 自定义导出 +- 支持程序参数保存 - ... ### Spine 版本支持 -| 版本 | 查看&导出 | 格式转换 | 版本转换 | -| :---: | :---: | :---: | :---: | -| `2.1.x` | :white_check_mark: | | | -| `3.6.x` | :white_check_mark: | | | -| `3.7.x` | :white_check_mark: | | | -| `3.8.x` | :white_check_mark: | :white_check_mark: | | -| `4.0.x` | :white_check_mark: | | | -| `4.1.x` | :white_check_mark: | | | -| `4.2.x` | :white_check_mark: | :white_check_mark: | | -| `4.3.x` | | | | +| 版本 | 查看&导出 | +| :---: | :---: | +| `2.1.x` | :white_check_mark: | +| `3.6.x` | :white_check_mark: | +| `3.7.x` | :white_check_mark: | +| `3.8.x` | :white_check_mark: | +| `4.0.x` | :white_check_mark: | +| `4.1.x` | :white_check_mark: | +| `4.2.x` | :white_check_mark: | +| `4.3.x` | | 更多版本正在施工 :rocket: :rocket: :rocket: @@ -46,10 +48,7 @@ | --- | --- | | 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. | | 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. | -| GIF/WebP/AVIF | 适合生成预览动图. | -| MP4 | 最常见的视频格式, 兼容性最好. | -| WebM | 适合浏览器在线播放格式, 支持透明背景. | -| MKV/MOV | 适合折腾. | +| 动图/视频 | 可以生成预览动图或者常见格式视频. | | 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. | ## 安装 @@ -60,21 +59,35 @@ 也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行. -导出 GIF 等视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z). +导出 GIF/MP4 等动图/视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z). ## 使用方法 +### 如何修改显示语言 + +窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改. + +### 基本介绍 + +程序大致是左右布局, 左侧是功能面板, 右侧是画面. + +左侧有三个子面板, 分别是: + +- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型. +- **模型**. 该面板记录导入并进行渲染的模型列表, 可以在这个面板设置与模型渲染相关的参数和渲染顺序, 以及一些与模型有关的功能. +- **画面**. 该面板用于设置右侧预览画面的参数. + +绝大部分按钮或者标签或者输入框都可以通过鼠标指针悬停来获取帮助文本. + ### 骨骼导入 -有 3 种方式导入骨骼文件: +可以直接拖放/粘贴需要导入的骨骼文件/目录到模型面板. -- 拖放/粘贴需要导入的骨骼文件/目录到模型列表 -- 从文件菜单里批量打开骨骼文件 -- 从文件菜单选择单个模型打开 +或者在浏览面板内右键菜单导入选中项. ### 内容调整 -模型列表支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整. +模型面板支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整. 预览画面除了使用面板进行参数设置外, 支持部分鼠标动作: @@ -89,9 +102,10 @@ 导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面. +在模型面板里, 右键菜单可以对选中项进行导出操作. + 导出有以下几个关键参数: -- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型. - 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹. - 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份. - 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致. @@ -105,6 +119,9 @@ - [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes) - [SFML.Net](https://github.com/SFML/SFML.Net) - [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) +- [HandyControl](https://github.com/HandyOrg/HandyControl) +- [NLog](https://github.com/NLog/NLog) +- [SkiaSharp](https://github.com/mono/SkiaSharp) --- diff --git a/SFMLRenderer/SFMLRenderer.csproj b/SFMLRenderer/SFMLRenderer.csproj index 4a770d5..2cbf67e 100644 --- a/SFMLRenderer/SFMLRenderer.csproj +++ b/SFMLRenderer/SFMLRenderer.csproj @@ -7,7 +7,7 @@ net8.0-windows $(SolutionDir)out false - 0.15.0 + 0.15.1 true diff --git a/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs b/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs index abe247d..4a7c9a1 100644 --- a/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs +++ b/Spine/Implementations/SpineWrappers/V21/SpineObjectData21.cs @@ -26,18 +26,37 @@ namespace Spine.Implementations.SpineWrappers.V21 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData21(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData21(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs b/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs index 159063c..a389679 100644 --- a/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs +++ b/Spine/Implementations/SpineWrappers/V36/SpineObjectData36.cs @@ -26,18 +26,37 @@ namespace Spine.Implementations.SpineWrappers.V36 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData36(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData36(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs b/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs index 4fdfee2..470eec7 100644 --- a/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs +++ b/Spine/Implementations/SpineWrappers/V37/SpineObjectData37.cs @@ -26,18 +26,37 @@ namespace Spine.Implementations.SpineWrappers.V37 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData37(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData37(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs b/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs index b46076a..0204041 100644 --- a/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs +++ b/Spine/Implementations/SpineWrappers/V38/SpineObjectData38.cs @@ -27,18 +27,37 @@ namespace Spine.Implementations.SpineWrappers.V38 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData38(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData38(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs b/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs index 3c8370f..bc035d7 100644 --- a/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs +++ b/Spine/Implementations/SpineWrappers/V40/SpineObjectData40.cs @@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V40 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData40(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData40(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } // 加载 skel try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs b/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs index 49ed2b7..18ed63b 100644 --- a/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs +++ b/Spine/Implementations/SpineWrappers/V41/SpineObjectData41.cs @@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V41 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData41(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData41(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } // 加载 skel try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Implementations/SpineWrappers/V42/SpineObjectData42.cs b/Spine/Implementations/SpineWrappers/V42/SpineObjectData42.cs index e826704..83a7365 100644 --- a/Spine/Implementations/SpineWrappers/V42/SpineObjectData42.cs +++ b/Spine/Implementations/SpineWrappers/V42/SpineObjectData42.cs @@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V42 private readonly ImmutableArray _animations; private readonly FrozenDictionary _animationsByName; - public SpineObjectData42(string skelPath, string atlasPath) : base(skelPath, atlasPath) + public SpineObjectData42(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) { // 加载 atlas - try { _atlas = new Atlas(atlasPath, _textureLoader); } + try { _atlas = new Atlas(atlasPath, textureLoader); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } // 加载 skel try { if (Utf8Validator.IsUtf8(skelPath)) - _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } else - _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } } catch (Exception ex) { diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj index a82dfe9..7c0d958 100644 --- a/Spine/Spine.csproj +++ b/Spine/Spine.csproj @@ -7,7 +7,7 @@ net8.0-windows $(SolutionDir)out false - 0.15.0 + 0.15.1 diff --git a/Spine/SpineObject.cs b/Spine/SpineObject.cs index d5735d7..bf71753 100644 --- a/Spine/SpineObject.cs +++ b/Spine/SpineObject.cs @@ -21,7 +21,6 @@ namespace Spine { [".skel"] = ".atlas", [".json"] = ".atlas", - [".skel.bytes"] = ".atlas.bytes", }.ToFrozenDictionary(); /// @@ -45,10 +44,12 @@ namespace Spine /// skel 文件路径 /// atlas 文件路径, 为空时会根据 进行自动检测 /// 要使用的运行时版本, 为空时会自动检测 - public SpineObject(string skelPath, string? atlasPath = null, SpineVersion? version = null) + public SpineObject(string skelPath, string? atlasPath = null, SpineVersion? version = null, TextureLoader? textureLoader = null) { if (string.IsNullOrWhiteSpace(skelPath)) throw new ArgumentException(skelPath, nameof(skelPath)); if (!File.Exists(skelPath)) throw new FileNotFoundException($"{nameof(skelPath)} not found", skelPath); + textureLoader ??= TextureLoader.DefaultLoader; + SkelPath = Path.GetFullPath(skelPath); AssetsDir = Directory.GetParent(skelPath).FullName; Name = Path.GetFileNameWithoutExtension(skelPath); @@ -91,7 +92,7 @@ namespace Spine { try { - _data = SpineObjectData.New(v, skelPath, atlasPath); + _data = SpineObjectData.New(v, skelPath, atlasPath, textureLoader); Version = v; break; } @@ -109,7 +110,7 @@ namespace Spine { // 根据版本实例化对象 Version = version; - _data = SpineObjectData.New(Version, skelPath, atlasPath); + _data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader); } // 创建状态实例 @@ -177,7 +178,6 @@ namespace Spine // 拷贝调试属性 EnableDebug = other.EnableDebug; DebugTexture = other.DebugTexture; - DebugNonTexture = other.DebugNonTexture; DebugBounds = other.DebugBounds; DebugBones = other.DebugBones; DebugRegions = other.DebugRegions; @@ -236,7 +236,7 @@ namespace Spine /// /// 是否使用预乘 Alpha /// - public bool UsePma { get; set; } = false; + public bool UsePma { get; set; } /// /// 物理约束更新方式 @@ -246,62 +246,57 @@ namespace Spine /// /// 启用渲染调试, 将会使所有 DebugXXX 属性生效 /// - public bool EnableDebug { get; set; } = false; + public bool EnableDebug { get; set; } /// /// 显示纹理 /// public bool DebugTexture { get; set; } = true; - /// - /// 是否显示非纹理内容, 一个总开关 - /// - public bool DebugNonTexture { get; set; } = true; - /// /// 显示包围盒 /// - public bool DebugBounds { get; set; } = true; + public bool DebugBounds { get; set; } /// /// 显示骨骼 /// - public bool DebugBones { get; set; } = false; + public bool DebugBones { get; set; } /// /// 显示区域附件边框 /// - public bool DebugRegions { get; set; } = false; + public bool DebugRegions { get; set; } /// /// 显示网格附件边框线 /// - public bool DebugMeshHulls { get; set; } = false; + public bool DebugMeshHulls { get; set; } /// /// 显示网格附件网格线 /// - public bool DebugMeshes { get; set; } = false; + public bool DebugMeshes { get; set; } /// /// 显示碰撞盒附件边框线 /// - public bool DebugBoundingBoxes { get; set; } = false; + public bool DebugBoundingBoxes { get; set; } /// /// 显示路径附件网格线 /// - public bool DebugPaths { get; set; } = false; + public bool DebugPaths { get; set; } /// /// 显示点附件 /// - public bool DebugPoints { get; set; } = false; + public bool DebugPoints { get; set; } /// /// 显示剪裁附件网格线 /// - public bool DebugClippings { get; set; } = false; + public bool DebugClippings { get; set; } /// /// 获取某个插槽上的附件名, 插槽不存在或者无附件均返回 null @@ -864,7 +859,7 @@ namespace Spine else { if (DebugTexture) DrawTexture(target, states); - if (DebugNonTexture) DrawNonTexture(target); + DrawNonTexture(target); } } diff --git a/Spine/SpineWrappers/SpineObjectData.cs b/Spine/SpineWrappers/SpineObjectData.cs index a6c8e2a..0e8f0dc 100644 --- a/Spine/SpineWrappers/SpineObjectData.cs +++ b/Spine/SpineWrappers/SpineObjectData.cs @@ -20,18 +20,13 @@ namespace Spine.SpineWrappers /// /// 构建版本对象 /// - public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath) => CreateInstance(version.Tag, skelPath, atlasPath); - - /// - /// 纹理加载器, 可以设置一些预置参数 - /// - public static TextureLoader TextureLoader => _textureLoader; - protected static readonly TextureLoader _textureLoader = new(); + public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath, TextureLoader textureLoader) + => CreateInstance(version.Tag, skelPath, atlasPath, textureLoader); /// /// 构造函数, 继承的子类应当实现一个相同签名的构造函数 /// - public SpineObjectData(string skelPath, string atlasPath) { } + public SpineObjectData(string skelPath, string atlasPath, TextureLoader textureLoader) { } public abstract string SkeletonVersion { get; } diff --git a/Spine/SpineWrappers/TextureLoader.cs b/Spine/SpineWrappers/TextureLoader.cs index 9aebcff..34c23c7 100644 --- a/Spine/SpineWrappers/TextureLoader.cs +++ b/Spine/SpineWrappers/TextureLoader.cs @@ -19,23 +19,62 @@ namespace Spine.SpineWrappers SpineRuntime42.TextureLoader { /// - /// 强制启用 Smooth + /// 默认的全局纹理加载器 /// - public bool ForceSmooth { get; set; } = false; + public static TextureLoader DefaultLoader { get; } = new(); /// - /// 强制启用 Repeated + /// 在读取纹理时强制进行通道预乘操作 /// - public bool ForceRepeated { get; set; } = false; + public bool ForcePremul { get; set; } + + /// + /// 强制使用 Nearest + /// + public bool ForceNearest { get; set; } /// /// 强制启用 Mipmap /// - public bool ForceMipmap { get; set; } = false; + public bool ForceMipmap { get; set; } - public void Load(SpineRuntime21.AtlasPage page, string path) + private SFML.Graphics.Texture ReadTexture(string path) { - var texture = new SFML.Graphics.Texture(path); + if (ForcePremul) + { + using var image = new SFML.Graphics.Image(path); + var width = image.Size.X; + var height = image.Size.Y; + var pixels = image.Pixels; + var size = width * height * 4; + for (int i = 0; i < size; i += 4) + { + byte a = pixels[i + 3]; + if (a == 0) + { + pixels[i + 0] = 0; + pixels[i + 1] = 0; + pixels[i + 2] = 0; + } + else if (a != 255) + { + float f = a / 255f; + pixels[i + 0] = (byte)(pixels[i + 0] * f); + pixels[i + 1] = (byte)(pixels[i + 1] * f); + pixels[i + 2] = (byte)(pixels[i + 2] * f); + } + } + var tex = new SFML.Graphics.Texture(width, height); + tex.Update(pixels); + return tex; + } + return new(path); + } + + public virtual void Load(SpineRuntime21.AtlasPage page, string path) + { + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime21.TextureFilter.Linear) { texture.Smooth = true; @@ -61,16 +100,16 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; } - public void Load(SpineRuntime36.AtlasPage page, string path) + public virtual void Load(SpineRuntime36.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime36.TextureFilter.Linear) { texture.Smooth = true; @@ -96,16 +135,16 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; } - public void Load(SpineRuntime37.AtlasPage page, string path) + public virtual void Load(SpineRuntime37.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime37.TextureFilter.Linear) { texture.Smooth = true; @@ -131,16 +170,16 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; } - public void Load(SpineRuntime38.AtlasPage page, string path) + public virtual void Load(SpineRuntime38.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime38.TextureFilter.Linear) { texture.Smooth = true; @@ -166,16 +205,20 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; + + // 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常 + // page.width = (int)texture.Size.X; + // page.height = (int)texture.Size.Y; } - public void Load(SpineRuntime40.AtlasPage page, string path) + public virtual void Load(SpineRuntime40.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime40.TextureFilter.Linear) { texture.Smooth = true; @@ -201,16 +244,16 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; } - public void Load(SpineRuntime41.AtlasPage page, string path) + public virtual void Load(SpineRuntime41.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime41.TextureFilter.Linear) { texture.Smooth = true; @@ -236,16 +279,16 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; } - public void Load(SpineRuntime42.AtlasPage page, string path) + public virtual void Load(SpineRuntime42.AtlasPage page, string path) { - var texture = new SFML.Graphics.Texture(path); + var texture = ReadTexture(path); + if (page.magFilter == SpineRuntime42.TextureFilter.Linear) { texture.Smooth = true; @@ -271,18 +314,13 @@ namespace Spine.SpineWrappers break; } - if (ForceSmooth) texture.Smooth = true; - if (ForceRepeated) texture.Repeated = true; + if (ForceNearest) texture.Smooth = false; if (ForceMipmap) texture.GenerateMipmap(); page.rendererObject = texture; - - // 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常 - // page.width = (int)texture.Size.X; - // page.height = (int)texture.Size.Y; } - public void Unload(object texture) + public virtual void Unload(object texture) { ((SFML.Graphics.Texture)texture).Dispose(); } diff --git a/SpineViewer/App.xaml b/SpineViewer/App.xaml index 10e4d3d..1eca518 100644 --- a/SpineViewer/App.xaml +++ b/SpineViewer/App.xaml @@ -11,7 +11,7 @@ - + - + @@ -56,9 +56,10 @@ - + + - + @@ -244,11 +245,11 @@ - + + - @@ -256,12 +257,18 @@ + + - - - - - + + - @@ -726,17 +731,13 @@