Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65508782c6 | ||
|
|
d02ab536b6 | ||
|
|
db3700bda3 | ||
|
|
6dea656e5e | ||
|
|
7bc82ab318 | ||
|
|
3eb9b1d008 | ||
|
|
eca59dc67b | ||
|
|
93b806dccd | ||
|
|
89c31d7c77 | ||
|
|
0a5432bb30 | ||
|
|
2eded25c03 | ||
|
|
fa00f0064e | ||
|
|
ddd3e94698 | ||
|
|
d7ee88f7f6 | ||
|
|
1d7a402749 | ||
|
|
86bcb079b0 | ||
|
|
390416df06 | ||
|
|
1344b34d08 | ||
|
|
497103bdb6 | ||
|
|
04953d13b6 | ||
|
|
b272d9802e | ||
|
|
bd5a537058 | ||
|
|
64a3caf938 | ||
|
|
ca34494483 | ||
|
|
e717eab6df | ||
|
|
068734549c | ||
|
|
3d1fa38eb3 | ||
|
|
bff3b39371 | ||
|
|
a44161053b | ||
|
|
4b64ec74c2 | ||
|
|
1f56e2f03c | ||
|
|
311b09cc63 | ||
|
|
cd7f841e38 |
3
.github/workflows/dotnet-desktop.yml
vendored
3
.github/workflows/dotnet-desktop.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
PROJECT_NAME: SpineViewer
|
||||
PROJ_CLI_NAME: SpineViewerCLI
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -54,11 +55,13 @@ jobs:
|
||||
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:PROJ_CLI_NAME\$env:PROJ_CLI_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:PROJ_CLI_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||
|
||||
- name: Create release directory
|
||||
shell: pwsh
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.15.8
|
||||
|
||||
- 修复渲染纹理过程中可能的 null 错误
|
||||
|
||||
## v0.15.7
|
||||
|
||||
- 合并社区 CLI 功能项目
|
||||
|
||||
## v0.15.6
|
||||
|
||||
- 修复导出单个的时长错误
|
||||
- 修改默认导出背景色为不透明黑色
|
||||
|
||||
## v0.15.5
|
||||
|
||||
- 修复自定义导出时的画面错误
|
||||
- 设置 mp4 像素格式为 yuv420p 避免 windows 默认播放器无法打开
|
||||
- 增加预览画面和导出时的速度参数设置
|
||||
- 修复一些提示文本错误
|
||||
- 导出时自动将分辨率向下调整为 2 的倍数, 避免 yuv420p 格式出错
|
||||
|
||||
## v0.15.4
|
||||
|
||||
- 修复导出时可能的卡死问题
|
||||
|
||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
## 仓库分支
|
||||
|
||||
仓库目前包含 4 个分支:
|
||||
|
||||
- `main`: 默认分支, 也是项目最新版的发布用分支
|
||||
- `dev/wpf`: WPF 版本开发分支
|
||||
- `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复)
|
||||
- `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复)
|
||||
|
||||
仓库的每个发布分支都有对应的开发分支 `dev/*`, **在进行贡献和推送时请在开发分支上进行**, 待开发分支上审核完毕进行必要的确认 (例如版本号的更新) 后, 再从开发分支向对应的发布分支发起 pr, 合并后将会通过 Actions 进行自动生成和发布.
|
||||
|
||||
## 仓库结构
|
||||
|
||||
仓库目前包含两个可执行文件项目, 分别是:
|
||||
|
||||
- `SpineViewer.csproj`
|
||||
- `SpineViewerCLI.csproj`
|
||||
|
||||
前者为仓库主要项目, 提供一个预览操作 Spine 模型文件的 UI 界面, 后者基于社区贡献进行开发, 提供一些便捷的 CLI 功能, 从而可以对模型文件进行一些批量操作.
|
||||
|
||||
除此之外其余项目均为一些基础功能库, 为以上两个项目提供必要的功能支持. 原则上 UI 项目和 CLI 项目二者独立互不引用, 仅引用相同的基础功能库, 以保证整个仓库的层次结构清晰便于维护.
|
||||
|
||||
## 如何贡献
|
||||
|
||||
对于一些小改动, 例如:
|
||||
|
||||
- 某些文件内的 bug 修复 (例如一些逻辑上的错误)
|
||||
- 已有功能的扩展性增强 (例如在已有代码逻辑结构上扩充某些功能字段)
|
||||
- 其他可能的对**已有功能**的修复改进
|
||||
|
||||
可以直接 fork 修改后向开发分支发起 pr, 经 review 无问题后可直接合并.
|
||||
|
||||
对于较大的改动, 例如:
|
||||
|
||||
- 新增某些代码文件 (例如需要添加一些全新的类)
|
||||
- 添加一些全新的逻辑或者功能代码 (例如在自行车上加装发动机)
|
||||
- 其他可能影响项目代码逻辑结构的改动
|
||||
|
||||
这些改动请先提 Issue, 进行必要性讨论, 以及确认新功能的引入方式, 请不要直接将这些可能的破坏性改动发起 pr.
|
||||
@@ -25,7 +25,7 @@
|
||||
- 支持自动分辨率批量导出
|
||||
- 支持 FFmpeg 自定义导出
|
||||
- 支持程序参数保存
|
||||
- ...
|
||||
- ......
|
||||
|
||||
### Spine 版本支持
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Spine.Exporters
|
||||
/// <param name="height">画布高像素值</param>
|
||||
public BaseExporter(uint width , uint height)
|
||||
{
|
||||
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||
width = width >> 1 << 1;
|
||||
height = height >> 1 << 1;
|
||||
if (width <= 0 || height <= 0)
|
||||
throw new ArgumentException($"Invalid resolution: {width}, {height}");
|
||||
_renderTexture = new(width, height);
|
||||
@@ -42,6 +45,9 @@ namespace Spine.Exporters
|
||||
/// </summary>
|
||||
public BaseExporter(Vector2u resolution)
|
||||
{
|
||||
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||
resolution.X = resolution.X >> 1 << 1;
|
||||
resolution.Y = resolution.Y >> 1 << 1;
|
||||
if (resolution.X <= 0 || resolution.Y <= 0)
|
||||
throw new ArgumentException($"Invalid resolution: {resolution}");
|
||||
_renderTexture = new(resolution.X, resolution.Y);
|
||||
@@ -76,12 +82,12 @@ namespace Spine.Exporters
|
||||
_backgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
protected Color _backgroundColor = Color.Transparent;
|
||||
protected Color _backgroundColor = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
protected Color _backgroundColorPma = Color.Transparent;
|
||||
protected Color _backgroundColorPma = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
@@ -92,6 +98,9 @@ namespace Spine.Exporters
|
||||
get => _renderTexture.Size;
|
||||
set
|
||||
{
|
||||
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
|
||||
value.X = value.X >> 1 << 1;
|
||||
value.Y = value.Y >> 1 << 1;
|
||||
if (value.X <= 0 || value.Y <= 0)
|
||||
{
|
||||
_logger.Warn("Omit invalid exporter resolution: {0}", value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FFMpegCore;
|
||||
using FFMpegCore.Pipes;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -63,6 +64,22 @@ namespace Spine.Exporters
|
||||
else options.WithCustomArgument("-vf unpremultiply=inplace=1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取的一帧, 结果是预乘的
|
||||
/// </summary>
|
||||
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||
{
|
||||
// BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
|
||||
// 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
|
||||
using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
|
||||
using var view = _renderTexture.GetView();
|
||||
tex.SetView(view);
|
||||
tex.Clear(_backgroundColorPma);
|
||||
foreach (var sp in spines.Reverse()) tex.Draw(sp);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
|
||||
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||
{
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
|
||||
|
||||
@@ -126,8 +126,10 @@ namespace Spine.Exporters
|
||||
|
||||
private void SetMp4Options(FFMpegArgumentOptions options)
|
||||
{
|
||||
// XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式
|
||||
// 但是如果是 libx265 则没有该限制
|
||||
var customArgs = "-vf unpremultiply=inplace=1";
|
||||
options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv444p")
|
||||
options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv420p")
|
||||
.WithFastStart()
|
||||
.WithConstantRateFactor(_crf)
|
||||
.WithCustomArgument(customArgs);
|
||||
|
||||
@@ -55,6 +55,21 @@ namespace Spine.Exporters
|
||||
}
|
||||
protected float _fps = 24;
|
||||
|
||||
public float Speed
|
||||
{
|
||||
get => _speed;
|
||||
set
|
||||
{
|
||||
if (_speed <= 0)
|
||||
{
|
||||
_logger.Warn("Omit invalid speed: {0}", value);
|
||||
return;
|
||||
}
|
||||
_speed = value;
|
||||
}
|
||||
}
|
||||
protected float _speed = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 是否保留最后一帧
|
||||
/// </summary>
|
||||
@@ -92,7 +107,7 @@ namespace Spine.Exporters
|
||||
// 导出完整帧
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
foreach (var spine in spines) spine.Update(delta);
|
||||
foreach (var spine in spines) spine.Update(delta * _speed);
|
||||
yield return GetFrame(spines);
|
||||
}
|
||||
|
||||
@@ -100,7 +115,7 @@ namespace Spine.Exporters
|
||||
if (hasFinal)
|
||||
{
|
||||
// XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
|
||||
foreach (var spine in spines) spine.Update(delta);
|
||||
foreach (var spine in spines) spine.Update(delta * _speed);
|
||||
yield return GetFrame(spines);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.4</Version>
|
||||
<Version>0.15.8</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -478,7 +478,6 @@ namespace Spine
|
||||
}
|
||||
|
||||
var attachment = slot.Attachment;
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices; // 顶点世界坐标数组, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesLength; // 顶点数组的长度
|
||||
@@ -491,10 +490,11 @@ namespace Spine
|
||||
float tintB = _skeleton.B * slot.B;
|
||||
float tintA = _skeleton.A * slot.A;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
switch (attachment)
|
||||
{
|
||||
case IRegionAttachment regionAttachment:
|
||||
texture = regionAttachment.RendererObject;
|
||||
worldVerticesLength = regionAttachment.ComputeWorldVertices(slot, ref _worldVertices);
|
||||
worldVertices = _worldVertices;
|
||||
triangles = regionAttachment.Triangles;
|
||||
@@ -504,9 +504,11 @@ namespace Spine
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
|
||||
// NOTE: RenderObject 的获取要在 ComputeWorldVertices 发生之后, 否则可能存在某些 Region 尚未被赋值产生 null 引用报错
|
||||
texture = regionAttachment.RendererObject;
|
||||
break;
|
||||
case IMeshAttachment meshAttachment:
|
||||
texture = meshAttachment.RendererObject;
|
||||
worldVerticesLength = meshAttachment.ComputeWorldVertices(slot, ref _worldVertices);
|
||||
worldVertices = _worldVertices;
|
||||
triangles = meshAttachment.Triangles;
|
||||
@@ -516,9 +518,9 @@ namespace Spine
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
texture = meshAttachment.RendererObject;
|
||||
break;
|
||||
case ISkinnedMeshAttachment skinnedMeshAttachment:
|
||||
texture = skinnedMeshAttachment.RendererObject;
|
||||
worldVerticesLength = skinnedMeshAttachment.ComputeWorldVertices(slot, ref _worldVertices);
|
||||
worldVertices = _worldVertices;
|
||||
triangles = skinnedMeshAttachment.Triangles;
|
||||
@@ -528,6 +530,7 @@ namespace Spine
|
||||
tintG *= skinnedMeshAttachment.G;
|
||||
tintB *= skinnedMeshAttachment.B;
|
||||
tintA *= skinnedMeshAttachment.A;
|
||||
texture = skinnedMeshAttachment.RendererObject;
|
||||
break;
|
||||
case IClippingAttachment clippingAttachment:
|
||||
_clipping.ClipStart(slot, clippingAttachment);
|
||||
|
||||
@@ -28,6 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
CHANGELOG.md = CHANGELOG.md
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
README.en.md = README.en.md
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
@@ -36,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SFMLRenderer", "SFMLRendere
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLog.Windows.Wpf", "NLog.Windows.Wpf\NLog.Windows.Wpf.csproj", "{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineViewerCLI", "SpineViewerCLI\SpineViewerCLI.csproj", "{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -86,6 +89,10 @@ Global
|
||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Debug|x64.Build.0 = Debug|x64
|
||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.ActiveCfg = Release|x64
|
||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.Build.0 = Release|x64
|
||||
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Debug|x64.Build.0 = Debug|x64
|
||||
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Release|x64.ActiveCfg = Release|x64
|
||||
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace SpineViewer.Models
|
||||
|
||||
public uint MaxFps { get; set; } = 30;
|
||||
|
||||
public float Speed { get; set; } = 1f;
|
||||
|
||||
public bool ShowAxis { get; set; } = true;
|
||||
|
||||
public Color BackgroundColor { get; set; }
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<s:String x:Key="Str_Zoom">Zoom</s:String>
|
||||
<s:String x:Key="Str_Rotation">Rotation (Degrees)</s:String>
|
||||
<s:String x:Key="Str_MaxFps">Max FPS</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">Playback Speed</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
|
||||
<s:String x:Key="Str_ShowAxis">Show Axis</s:String>
|
||||
<s:String x:Key="Str_BackgroundColor">Background Color</s:String>
|
||||
@@ -166,6 +167,8 @@
|
||||
<s:String x:Key="Str_ExportDurationTooltip">Export duration; if less than 0, the maximum duration of all animations in all models will be used during export.</s:String>
|
||||
|
||||
<s:String x:Key="Str_Fps">FPS</s:String>
|
||||
<s:String x:Key="Str_ExportSpeed">Export Speed</s:String>
|
||||
<s:String x:Key="Str_ExportSpeedTooltip">Export speed factor; only affects the animation speed of the model, not the export duration or frame rate.</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrame">Keep Last Frame</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrameTooltip">When keeping the last frame, animation is smoother but frame count may be one higher</s:String>
|
||||
|
||||
@@ -184,7 +187,7 @@
|
||||
<s:String x:Key="Str_FFmpegCodec">Codec</s:String>
|
||||
<s:String x:Key="Str_FFmpegCodecTooltip">FFmpeg codec (equivalent to "-c:v"), e.g. "libx264", "libx265"</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormat">Pixel Format</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpeg pixel format (equivalent to "-pix_fmt"), e.g. "yuv420", "yuv444"</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpeg pixel format (equivalent to "-pix_fmt"), e.g. "yuv420p", "yuv444p"</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrate">Bitrate</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrateTooltip">FFmpeg bitrate (equivalent to "-b:v"), e.g. "6K", "2M"</s:String>
|
||||
<s:String x:Key="Str_FFmpegFilter">Filter</s:String>
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<s:String x:Key="Str_Zoom">ズーム</s:String>
|
||||
<s:String x:Key="Str_Rotation">回転(度)</s:String>
|
||||
<s:String x:Key="Str_MaxFps">最大FPS</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">再生速度</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
|
||||
<s:String x:Key="Str_ShowAxis">座標軸を表示</s:String>
|
||||
<s:String x:Key="Str_BackgroundColor">背景色</s:String>
|
||||
@@ -166,6 +167,8 @@
|
||||
<s:String x:Key="Str_ExportDurationTooltip">エクスポート時間。0 未満の場合、エクスポート時にすべてのモデルのすべてのアニメーションの最大時間が使用されます。</s:String>
|
||||
|
||||
<s:String x:Key="Str_Fps">FPS</s:String>
|
||||
<s:String x:Key="Str_ExportSpeed">エクスポート速度</s:String>
|
||||
<s:String x:Key="Str_ExportSpeedTooltip">エクスポート速度係数。モデルの動作速度のみに影響し、エクスポート時間やフレームレートなどには影響しません。</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrame">最後のフレームを保持</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrameTooltip">最後のフレームを保持すると、アニメーションはより連続して見えますが、フレーム数が予想より1フレーム多くなる可能性があります</s:String>
|
||||
|
||||
@@ -184,7 +187,7 @@
|
||||
<s:String x:Key="Str_FFmpegCodec">コーデック</s:String>
|
||||
<s:String x:Key="Str_FFmpegCodecTooltip">FFmpegコーデック。パラメーター“-c:v”に相当します。例: “libx264”、“libx265”</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormat">ピクセルフォーマット</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpegピクセルフォーマット。パラメーター“-pix_fmt”に相当します。例: “yuv420”、“yuv444”</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpegピクセルフォーマット。パラメーター“-pix_fmt”に相当します。例: “yuv420p”、“yuv444p”</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrate">ビットレート</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrateTooltip">FFmpegビットレート。パラメーター“-b:v”に相当します。例: “6K”、“2M”</s:String>
|
||||
<s:String x:Key="Str_FFmpegFilter">フィルター</s:String>
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<s:String x:Key="Str_Zoom">缩放</s:String>
|
||||
<s:String x:Key="Str_Rotation">旋转(角度)</s:String>
|
||||
<s:String x:Key="Str_MaxFps">最大帧率</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">播放速度</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
|
||||
<s:String x:Key="Str_ShowAxis">显示坐标轴</s:String>
|
||||
<s:String x:Key="Str_BackgroundColor">背景颜色</s:String>
|
||||
@@ -166,6 +167,8 @@
|
||||
<s:String x:Key="Str_ExportDurationTooltip">导出时长,如果小于 0,则在导出时使用所有模型所有动画的最大时长</s:String>
|
||||
|
||||
<s:String x:Key="Str_Fps">帧率</s:String>
|
||||
<s:String x:Key="Str_ExportSpeed">导出速度</s:String>
|
||||
<s:String x:Key="Str_ExportSpeedTooltip">导出速度因子, 仅影响模型的动作速度, 不影响导出时长和帧率等参数</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrame">保留最后一帧</s:String>
|
||||
<s:String x:Key="Str_KeepLastFrameTooltip">当设置保留最后一帧时,动图会更为连贯,但是帧数可能比预期帧数多 1</s:String>
|
||||
|
||||
@@ -184,7 +187,7 @@
|
||||
<s:String x:Key="Str_FFmpegCodec">编码器</s:String>
|
||||
<s:String x:Key="Str_FFmpegCodecTooltip">FFmpeg 编码器,等价于参数 “-c:v”,例如 “libx264”、“libx265”</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormat">像素格式</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpeg 像素格式,等价于参数 “-pix_fmt”,例如 “yuv420”、“yuv444”</s:String>
|
||||
<s:String x:Key="Str_FFmpegPixelFormatTooltip">FFmpeg 像素格式,等价于参数 “-pix_fmt”,例如 “yuv420p”、“yuv444p”</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrate">比特率</s:String>
|
||||
<s:String x:Key="Str_FFmpegBitrateTooltip">FFmpeg 比特率,等价于参数 “-b:v”,例如 “6K”、“2M”</s:String>
|
||||
<s:String x:Key="Str_FFmpegFilter">滤镜</s:String>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.4</Version>
|
||||
<Version>0.15.8</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
public Color BackgroundColor { get => _backgroundColor; set => SetProperty(ref _backgroundColor, value); }
|
||||
protected Color _backgroundColor = Color.FromArgb(0, 0, 0, 0);
|
||||
protected Color _backgroundColor = Color.FromArgb(255, 0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 四周边缘距离
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Fps = _fps,
|
||||
Speed = _speed,
|
||||
KeepLast = _keepLast,
|
||||
Format = _format,
|
||||
Codec = _codec,
|
||||
@@ -91,7 +92,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, filename);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
@@ -118,12 +121,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
// 统计总帧数
|
||||
int totalFrameCount = 0;
|
||||
if (_duration > 0)
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
else
|
||||
if (_duration < 0)
|
||||
{
|
||||
foreach (var sp in spines)
|
||||
{
|
||||
@@ -131,6 +129,11 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
totalFrameCount += exporter.GetFrameCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
|
||||
pr.Total = totalFrameCount;
|
||||
pr.Done = 0;
|
||||
@@ -153,7 +156,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Fps = _fps,
|
||||
Speed = _speed,
|
||||
KeepLast = _keepLast,
|
||||
Format = _format,
|
||||
Loop = _loop,
|
||||
@@ -82,7 +83,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, filename);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
@@ -109,12 +112,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
// 统计总帧数
|
||||
int totalFrameCount = 0;
|
||||
if (_duration > 0)
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
else
|
||||
if (_duration < 0)
|
||||
{
|
||||
foreach (var sp in spines)
|
||||
{
|
||||
@@ -122,6 +120,11 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
totalFrameCount += exporter.GetFrameCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
|
||||
pr.Total = totalFrameCount;
|
||||
pr.Done = 0;
|
||||
@@ -144,7 +147,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Fps = _fps,
|
||||
Speed = _speed,
|
||||
KeepLast = _keepLast
|
||||
};
|
||||
|
||||
@@ -55,12 +56,14 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, folderName);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
pr.Total = total;
|
||||
pr.Done = done;
|
||||
pr.Total = total;
|
||||
pr.Done = done;
|
||||
pr.ProgressText = text;
|
||||
_vmMain.ProgressValue = pr.Done / pr.Total;
|
||||
};
|
||||
@@ -82,12 +85,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
// 统计总帧数
|
||||
int totalFrameCount = 0;
|
||||
if (_duration > 0)
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
else
|
||||
if (_duration < 0)
|
||||
{
|
||||
foreach (var sp in spines)
|
||||
{
|
||||
@@ -95,6 +93,11 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
totalFrameCount += exporter.GetFrameCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter.Duration = _duration;
|
||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||
}
|
||||
|
||||
pr.Total = totalFrameCount;
|
||||
pr.Done = 0;
|
||||
@@ -117,7 +120,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||
|
||||
var folderName = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}";
|
||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, folderName);
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
public uint Fps { get => _fps; set => SetProperty(ref _fps, Math.Max(1, value)); }
|
||||
protected uint _fps = 30;
|
||||
|
||||
public float Speed { get => _speed; set => SetProperty(ref _speed, Math.Clamp(value, 0.001f, 1000f)); }
|
||||
protected float _speed = 1f;
|
||||
|
||||
public bool KeepLast { get => _keepLast; set => SetProperty(ref _keepLast, value); }
|
||||
protected bool _keepLast = true;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,13 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
set => SetProperty(_renderer.MaxFps, value, v => _renderer.MaxFps = value);
|
||||
}
|
||||
|
||||
public float Speed
|
||||
{
|
||||
get => _speed;
|
||||
set => SetProperty(ref _speed, Math.Clamp(value, 0.01f, 100f));
|
||||
}
|
||||
private float _speed = 1f;
|
||||
|
||||
public bool ShowAxis
|
||||
{
|
||||
get => _showAxis;
|
||||
@@ -193,15 +200,15 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
});
|
||||
private RelayCommand? _cmd_Restart;
|
||||
|
||||
public RelayCommand Cmd_ForwardStep => _cmd_ForwardStep ??= new(() =>
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 1f / _renderer.MaxFps : 0.001f;
|
||||
public RelayCommand Cmd_ForwardStep => _cmd_ForwardStep ??= new(() =>
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 1f / _renderer.MaxFps : 0.001f;
|
||||
});
|
||||
private RelayCommand? _cmd_ForwardStep;
|
||||
|
||||
public RelayCommand Cmd_ForwardFast => _cmd_ForwardFast ??= new(() =>
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 10f / _renderer.MaxFps : 0.01f;
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 10f / _renderer.MaxFps : 0.01f;
|
||||
});
|
||||
private RelayCommand? _cmd_ForwardFast;
|
||||
|
||||
@@ -390,7 +397,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
if (_cancelToken?.IsCancellationRequested ?? true) break; // 提前中止
|
||||
|
||||
sp.Update(0); // 避免物理效果出现问题
|
||||
sp.Update(delta);
|
||||
sp.Update(delta * _speed);
|
||||
|
||||
// 为选中对象绘制一个半透明背景
|
||||
if (sp.IsSelected)
|
||||
@@ -426,7 +433,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
|
||||
public RendererWorkspaceConfigModel WorkspaceConfig
|
||||
{
|
||||
{
|
||||
// TODO: 背景图片
|
||||
get
|
||||
{
|
||||
@@ -441,23 +448,25 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
FlipX = FlipX,
|
||||
FlipY = FlipY,
|
||||
MaxFps = MaxFps,
|
||||
Speed = Speed,
|
||||
ShowAxis = ShowAxis,
|
||||
BackgroundColor = BackgroundColor,
|
||||
};
|
||||
}
|
||||
set
|
||||
{
|
||||
ResolutionX = value.ResolutionX;
|
||||
ResolutionY = value.ResolutionY;
|
||||
CenterX = value.CenterX;
|
||||
CenterY = value.CenterY;
|
||||
Zoom = value.Zoom;
|
||||
Rotation = value.Rotation;
|
||||
FlipX = value.FlipX;
|
||||
FlipY = value.FlipY;
|
||||
MaxFps = value.MaxFps;
|
||||
ShowAxis = value.ShowAxis;
|
||||
BackgroundColor = value.BackgroundColor;
|
||||
ResolutionX = value.ResolutionX;
|
||||
ResolutionY = value.ResolutionY;
|
||||
CenterX = value.CenterX;
|
||||
CenterY = value.CenterY;
|
||||
Zoom = value.Zoom;
|
||||
Rotation = value.Rotation;
|
||||
FlipX = value.FlipX;
|
||||
FlipY = value.FlipY;
|
||||
MaxFps = value.MaxFps;
|
||||
Speed = value.Speed;
|
||||
ShowAxis = value.ShowAxis;
|
||||
BackgroundColor = value.BackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="60"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -122,38 +123,42 @@
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
|
||||
<Separator Grid.Row="12" Grid.ColumnSpan="2" Height="10"/>
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
|
||||
<Separator Grid.Row="13" Grid.ColumnSpan="2" Height="10"/>
|
||||
|
||||
<!-- 导出格式 -->
|
||||
<Label Grid.Row="13" Grid.Column="0" Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<TextBox Grid.Row="13" Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<TextBox Grid.Row="14" Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
|
||||
<!-- 编码器 -->
|
||||
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<TextBox Grid.Row="14" Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
|
||||
<!-- 像素格式 -->
|
||||
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
|
||||
<!-- 比特率 -->
|
||||
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<TextBox Grid.Row="17" Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
|
||||
<!-- 滤镜 -->
|
||||
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<TextBox Grid.Row="17" Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<Label Grid.Row="18" Grid.Column="0" Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<TextBox Grid.Row="18" Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
|
||||
<!-- 自定义参数 -->
|
||||
<Label Grid.Row="18" Grid.Column="0"
|
||||
<Label Grid.Row="19" Grid.Column="0"
|
||||
VerticalAlignment="Top"
|
||||
Content="{DynamicResource Str_FFmpegCustomArgs}"
|
||||
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
|
||||
<TextBox Grid.Row="18" Grid.Column="1"
|
||||
<TextBox Grid.Row="19" Grid.Column="1"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalContentAlignment="Top"
|
||||
TextWrapping="Wrap"
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 水平分辨率 -->
|
||||
@@ -121,31 +122,35 @@
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
|
||||
<Separator Grid.Row="12" Grid.ColumnSpan="2" Height="10"/>
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
|
||||
<Separator Grid.Row="13" Grid.ColumnSpan="2" Height="10"/>
|
||||
|
||||
<!-- 视频格式 -->
|
||||
<Label Grid.Row="13" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
|
||||
<ComboBox Grid.Row="13" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
|
||||
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
|
||||
<ComboBox Grid.Row="14" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
|
||||
|
||||
<!-- 动图是否循环 -->
|
||||
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<ToggleButton Grid.Row="14" Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<ToggleButton Grid.Row="15" Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
|
||||
<!-- 质量参数 -->
|
||||
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
|
||||
<!-- 无损压缩 -->
|
||||
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<ToggleButton Grid.Row="16" Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<ToggleButton Grid.Row="17" Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
|
||||
<!-- CRF 参数 -->
|
||||
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<TextBox Grid.Row="17" Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<Label Grid.Row="18" Grid.Column="0" Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<TextBox Grid.Row="18" Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 水平分辨率 -->
|
||||
@@ -116,9 +117,13 @@
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
@@ -693,6 +693,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 水平分辨率 -->
|
||||
@@ -731,13 +732,17 @@
|
||||
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_MaxFps}"/>
|
||||
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding MaxFps}"/>
|
||||
|
||||
<!-- 播放速度 -->
|
||||
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_PlaySpeed}"/>
|
||||
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Speed}"/>
|
||||
|
||||
<!-- 显示坐标轴 -->
|
||||
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
|
||||
<ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
|
||||
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
|
||||
<!-- 背景图案 -->
|
||||
<!-- 背景图案模式 -->
|
||||
|
||||
240
SpineViewerCLI/SpineViewerCLI.cs
Normal file
240
SpineViewerCLI/SpineViewerCLI.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public class CLI
|
||||
{
|
||||
const string USAGE = @"
|
||||
usage: SpineViewerCLI.exe [--skel PATH] [--atlas PATH] [--output PATH] [--animation STR] [--pma] [--fps INT] [--loop] [--crf INT] [--width INT] [--height INT] [--centerx INT] [--centery INT] [--zoom FLOAT] [--speed FLOAT] [--color HEX] [--quiet]
|
||||
|
||||
options:
|
||||
--skel PATH Path to the .skel file
|
||||
--atlas PATH Path to the .atlas file, default searches in the skel file directory
|
||||
--output PATH Output file path
|
||||
--animation STR Animation name
|
||||
--pma Use premultiplied alpha, default false
|
||||
--fps INT Frames per second, default 24
|
||||
--loop Whether to loop the animation, default false
|
||||
--crf INT Constant Rate Factor i.e. video quality, from 0 (lossless) to 51 (worst), default 23
|
||||
--width INT Output width, default 512
|
||||
--height INT Output height, default 512
|
||||
--centerx INT Center X offset, default automatically finds bounds
|
||||
--centery INT Center Y offset, default automatically finds bounds
|
||||
--zoom FLOAT Zoom level, default 1.0
|
||||
--speed FLOAT Speed of animation, default 1.0
|
||||
--color HEX Background color as a hex RGBA color, default 000000ff (opaque black)
|
||||
--quiet Removes console progress log, default false
|
||||
";
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
string? skelPath = null;
|
||||
string? atlasPath = null;
|
||||
string? output = null;
|
||||
string? animation = null;
|
||||
bool pma = false;
|
||||
uint fps = 24;
|
||||
bool loop = false;
|
||||
int crf = 23;
|
||||
uint? width = null;
|
||||
uint? height = null;
|
||||
int? centerx = null;
|
||||
int? centery = null;
|
||||
float zoom = 1;
|
||||
float speed = 1;
|
||||
Color backgroundColor = Color.Black;
|
||||
bool quiet = false;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--help":
|
||||
Console.Write(USAGE);
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
case "--skel":
|
||||
skelPath = args[++i];
|
||||
break;
|
||||
case "--atlas":
|
||||
atlasPath = args[++i];
|
||||
break;
|
||||
case "--output":
|
||||
output = args[++i];
|
||||
break;
|
||||
case "--animation":
|
||||
animation = args[++i];
|
||||
break;
|
||||
case "--pma":
|
||||
pma = true;
|
||||
break;
|
||||
case "--fps":
|
||||
fps = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--loop":
|
||||
loop = true;
|
||||
break;
|
||||
case "--crf":
|
||||
crf = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--width":
|
||||
width = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--height":
|
||||
height = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--centerx":
|
||||
centerx = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--centery":
|
||||
centery = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--zoom":
|
||||
zoom = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--speed":
|
||||
speed = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--color":
|
||||
backgroundColor = new Color(uint.Parse(args[++i], NumberStyles.HexNumber));
|
||||
break;
|
||||
case "--quiet":
|
||||
quiet = true;
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown argument: {args[i]}");
|
||||
Environment.Exit(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(skelPath))
|
||||
{
|
||||
Console.Error.WriteLine("Missing --skel");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
Console.Error.WriteLine("Missing --output");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
if (!Enum.TryParse<FFmpegVideoExporter.VideoFormat>(Path.GetExtension(output).TrimStart('.'), true, out var videoFormat))
|
||||
{
|
||||
var validExtensions = string.Join(", ", Enum.GetNames(typeof(FFmpegVideoExporter.VideoFormat)));
|
||||
Console.Error.WriteLine($"Invalid output extension. Supported formats are: {validExtensions}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
var sp = new SpineObject(skelPath, atlasPath);
|
||||
sp.UsePma = pma;
|
||||
|
||||
if (string.IsNullOrEmpty(animation))
|
||||
{
|
||||
var availableAnimations = string.Join(", ", sp.Data.Animations);
|
||||
Console.Error.WriteLine($"Missing --animation. Available animations for {sp.Name}: {availableAnimations}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
var trackEntry = sp.AnimationState.SetAnimation(0, animation, loop);
|
||||
sp.Update(0);
|
||||
|
||||
FFmpegVideoExporter exporter;
|
||||
if (width is uint w && height is uint h && centerx is int cx && centery is int cy)
|
||||
{
|
||||
exporter = new FFmpegVideoExporter(w, h)
|
||||
{
|
||||
Center = (cx, cy),
|
||||
Size = (w / zoom, -h / zoom),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var bounds = GetFloatRectCanvasBounds(GetSpineObjectAnimationBounds(sp, fps), new(width ?? 512, height ?? 512));
|
||||
exporter = new FFmpegVideoExporter(width ?? (uint)Math.Ceiling(bounds.Width), height ?? (uint)Math.Ceiling(bounds.Height))
|
||||
{
|
||||
Center = bounds.Position + bounds.Size / 2,
|
||||
Size = (bounds.Width, -bounds.Height),
|
||||
};
|
||||
}
|
||||
exporter.Duration = trackEntry.Animation.Duration;
|
||||
exporter.Fps = fps;
|
||||
exporter.Format = videoFormat;
|
||||
exporter.Loop = loop;
|
||||
exporter.Crf = crf;
|
||||
exporter.Speed = speed;
|
||||
exporter.BackgroundColor = backgroundColor;
|
||||
|
||||
if (!quiet)
|
||||
exporter.ProgressReporter = (total, done, text) => Console.Write($"\r{text}");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
exporter.Export(output, cts.Token, sp);
|
||||
|
||||
if (!quiet)
|
||||
Console.WriteLine();
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public static SpineObject CopySpineObject(SpineObject sp)
|
||||
{
|
||||
var spineObject = new SpineObject(sp, true);
|
||||
foreach (var tr in sp.AnimationState.IterTracks().Where(t => t is not null))
|
||||
{
|
||||
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
|
||||
}
|
||||
spineObject.Update(0);
|
||||
return spineObject;
|
||||
}
|
||||
|
||||
static FloatRect GetSpineObjectBounds(SpineObject sp)
|
||||
{
|
||||
sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
return new(x, y, Math.Max(w, 1e-6f), Math.Max(h, 1e-6f));
|
||||
}
|
||||
static FloatRect FloatRectUnion(FloatRect a, FloatRect b)
|
||||
{
|
||||
float left = Math.Min(a.Left, b.Left);
|
||||
float top = Math.Min(a.Top, b.Top);
|
||||
float right = Math.Max(a.Left + a.Width, b.Left + b.Width);
|
||||
float bottom = Math.Max(a.Top + a.Height, b.Top + b.Height);
|
||||
return new FloatRect(left, top, right - left, bottom - top);
|
||||
}
|
||||
static FloatRect GetSpineObjectAnimationBounds(SpineObject sp, float fps = 10)
|
||||
{
|
||||
sp = CopySpineObject(sp);
|
||||
var bounds = GetSpineObjectBounds(sp);
|
||||
var maxDuration = sp.AnimationState.IterTracks().Select(t => t?.Animation.Duration ?? 0).DefaultIfEmpty(0).Max();
|
||||
sp.Update(0);
|
||||
for (float tick = 0, delta = 1 / fps; tick < maxDuration; tick += delta)
|
||||
{
|
||||
bounds = FloatRectUnion(bounds, GetSpineObjectBounds(sp));
|
||||
sp.Update(delta);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
static FloatRect GetFloatRectCanvasBounds(FloatRect rect, Vector2u resolution)
|
||||
{
|
||||
float sizeW = rect.Width;
|
||||
float sizeH = rect.Height;
|
||||
float innerW = resolution.X;
|
||||
float innerH = resolution.Y;
|
||||
var scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH));
|
||||
var scaleW = scale * Math.Sign(sizeW);
|
||||
var scaleH = scale * Math.Sign(sizeH);
|
||||
|
||||
innerW *= scaleW;
|
||||
innerH *= scaleH;
|
||||
|
||||
var x = rect.Left - (innerW - sizeW) / 2;
|
||||
var y = rect.Top - (innerH - sizeH) / 2;
|
||||
var w = resolution.X * scaleW;
|
||||
var h = resolution.Y * scaleH;
|
||||
return new(x, y, w, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
SpineViewerCLI/SpineViewerCLI.csproj
Normal file
23
SpineViewerCLI/SpineViewerCLI.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.0.1</Version>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SFMLRenderer\SFMLRenderer.csproj" />
|
||||
<ProjectReference Include="..\Spine\Spine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user