diff --git a/SpineViewer/Exporter/VideoExporter.cs b/SpineViewer/Exporter/VideoExporter.cs
index 0b9d93d..0c99ae9 100644
--- a/SpineViewer/Exporter/VideoExporter.cs
+++ b/SpineViewer/Exporter/VideoExporter.cs
@@ -24,6 +24,11 @@ namespace SpineViewer.Exporter
///
public float FPS { get; set; } = 60;
+ ///
+ /// 是否保留最后一帧
+ ///
+ public bool KeepLast { get; set; } = true;
+
public override string? Validate()
{
if (base.Validate() is string error)
@@ -43,9 +48,21 @@ namespace SpineViewer.Exporter
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
float delta = 1f / FPS;
- int total = Math.Max(1, (int)(duration * FPS)); // 至少导出 1 帧
+ int total = (int)(duration * FPS); // 完整帧的数量
- worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
+ float deltaFinal = duration - delta * total; // 最后一帧时长
+ int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
+
+ int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
+
+ worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{frameCount} 帧");
+
+ // 导出首帧
+ var firstFrame = GetFrame(spine);
+ worker?.ReportProgress(1 * 100 / frameCount, $"{spine.Name} 已处理 1/{frameCount} 帧");
+ yield return firstFrame;
+
+ // 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
@@ -54,11 +71,20 @@ namespace SpineViewer.Exporter
break;
}
- var frame = GetFrame(spine);
spine.Update(delta);
- worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
+ var frame = GetFrame(spine);
+ worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"{spine.Name} 已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
+
+ // 导出最后一帧
+ if (final > 0)
+ {
+ spine.Update(deltaFinal);
+ var finalFrame = GetFrame(spine);
+ worker?.ReportProgress(100, $"{spine.Name} 已处理 {frameCount}/{frameCount} 帧");
+ yield return finalFrame;
+ }
}
///
@@ -67,10 +93,24 @@ namespace SpineViewer.Exporter
protected IEnumerable GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必须根据 Duration 决定导出时长
- float delta = 1f / FPS;
- int total = Math.Max(1, (int)(Duration * FPS)); // 至少导出 1 帧
+ var duration = Duration;
- worker?.ReportProgress(0, $"已处理 0/{total} 帧");
+ float delta = 1f / FPS;
+ int total = (int)(duration * FPS); // 完整帧的数量
+
+ float deltaFinal = duration - delta * total; // 最后一帧时长
+ int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
+
+ int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
+
+ worker?.ReportProgress(0, $"已处理 0/{frameCount} 帧");
+
+ // 导出首帧
+ var firstFrame = GetFrame(spinesToRender);
+ worker?.ReportProgress(1 * 100 / frameCount, $"已处理 1/{frameCount} 帧");
+ yield return firstFrame;
+
+ // 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
@@ -79,11 +119,20 @@ namespace SpineViewer.Exporter
break;
}
- var frame = GetFrame(spinesToRender);
foreach (var spine in spinesToRender) spine.Update(delta);
- worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
+ var frame = GetFrame(spinesToRender);
+ worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
+
+ // 导出最后一帧
+ if (final > 0)
+ {
+ foreach (var spine in spinesToRender) spine.Update(delta);
+ var finalFrame = GetFrame(spinesToRender);
+ worker?.ReportProgress(100, $"已处理 {frameCount}/{frameCount} 帧");
+ yield return finalFrame;
+ }
}
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs
index e9f14d9..3becc3e 100644
--- a/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs
+++ b/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs
@@ -24,5 +24,11 @@ namespace SpineViewer.PropertyGridWrappers.Exporter
///
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
+
+ ///
+ /// 保留最后一帧
+ ///
+ [Category("[1] 视频参数"), DisplayName("保留最后一帧"), Description("当设置保留最后一帧时, 动图会更为连贯, 但是帧数可能比预期帧数多 1")]
+ public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
}
}