统一导出类结构
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using SpineViewer.Spine;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Drawing.Design;
|
using System.Drawing.Design;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -14,6 +16,46 @@ namespace SpineViewer.Exporter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ExportArgs
|
public abstract class ExportArgs
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实现类缓存
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||||
|
|
||||||
|
static ExportArgs()
|
||||||
|
{
|
||||||
|
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(ExportArgs).IsAssignableFrom(t) && !t.IsAbstract);
|
||||||
|
foreach (var type in impTypes)
|
||||||
|
{
|
||||||
|
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||||
|
if (attr is not null)
|
||||||
|
{
|
||||||
|
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||||
|
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||||
|
ImplementationTypes[attr.ExportType] = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Program.Logger.Debug("Find export args implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建指定类型导出参数
|
||||||
|
/// </summary>
|
||||||
|
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||||
|
{
|
||||||
|
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||||
|
{
|
||||||
|
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||||
|
}
|
||||||
|
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||||
|
{
|
||||||
|
Resolution = resolution;
|
||||||
|
View = view;
|
||||||
|
RenderSelectedOnly = renderSelectedOnly;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 输出文件夹
|
/// 输出文件夹
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,24 +72,21 @@ namespace SpineViewer.Exporter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 画面分辨率
|
/// 画面分辨率
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ReadOnly(true)]
|
|
||||||
[TypeConverter(typeof(SizeConverter))]
|
[TypeConverter(typeof(SizeConverter))]
|
||||||
[Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
[Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||||
public required Size Resolution { get; init; }
|
public Size Resolution { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 渲染视窗
|
/// 渲染视窗
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ReadOnly(true)]
|
|
||||||
[Category("导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
[Category("导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||||
public required SFML.Graphics.View View { get; init; }
|
public SFML.Graphics.View View { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否仅渲染选中
|
/// 是否仅渲染选中
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ReadOnly(true)]
|
|
||||||
[Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
[Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||||
public required bool RenderSelectedOnly { get; init; }
|
public bool RenderSelectedOnly { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||||
@@ -65,109 +104,4 @@ namespace SpineViewer.Exporter
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 单帧画面导出参数
|
|
||||||
/// </summary>
|
|
||||||
public class FrameExportArgs : ExportArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 单帧画面格式
|
|
||||||
/// </summary>
|
|
||||||
[TypeConverter(typeof(ImageFormatConverter))]
|
|
||||||
[Category("单帧画面"), DisplayName("图像格式")]
|
|
||||||
public ImageFormat ImageFormat
|
|
||||||
{
|
|
||||||
get => imageFormat;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
|
||||||
imageFormat = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private ImageFormat imageFormat = ImageFormat.Png;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件名后缀
|
|
||||||
/// </summary>
|
|
||||||
[Category("单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
|
||||||
public string FileSuffix { get => imageFormat.GetSuffix(); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 四周填充像素值
|
|
||||||
/// </summary>
|
|
||||||
[TypeConverter(typeof(PaddingConverter))]
|
|
||||||
[Category("单帧画面"), DisplayName("四周填充像素值"), Description("在图内四周留出来的透明像素区域, 画面内容的可用范围是分辨率裁去填充区域")]
|
|
||||||
public Padding Padding
|
|
||||||
{
|
|
||||||
get => padding;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value.Left < 0) value.Left = 0;
|
|
||||||
if (value.Right < 0) value.Right = 0;
|
|
||||||
if (value.Top < 0) value.Top = 0;
|
|
||||||
if (value.Bottom < 0) value.Bottom = 0;
|
|
||||||
padding = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private Padding padding = new(1);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DPI
|
|
||||||
/// </summary>
|
|
||||||
[TypeConverter(typeof(SizeFConverter))]
|
|
||||||
[Category("单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
|
||||||
public SizeF DPI
|
|
||||||
{
|
|
||||||
get => dpi;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value.Width <= 0) value.Width = 144;
|
|
||||||
if (value.Height <= 0) value.Height = 144;
|
|
||||||
dpi = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private SizeF dpi = new(144, 144);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 视频导出参数基类
|
|
||||||
/// </summary>
|
|
||||||
public abstract class VideoExportArgs : ExportArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 导出时长
|
|
||||||
/// </summary>
|
|
||||||
[Category("视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长")]
|
|
||||||
public float Duration { get => duration; set => duration = Math.Max(0, value); }
|
|
||||||
private float duration = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 帧率
|
|
||||||
/// </summary>
|
|
||||||
[Category("视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
|
||||||
public float FPS { get; set; } = 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 帧序列导出参数
|
|
||||||
/// </summary>
|
|
||||||
public class FrameSequenceExportArgs : VideoExportArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 文件名后缀
|
|
||||||
/// </summary>
|
|
||||||
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
|
|
||||||
[Category("帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
|
||||||
public string FileSuffix { get; set; } = ".png";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// GIF 导出参数
|
|
||||||
/// </summary>
|
|
||||||
public class GifExportArgs : VideoExportArgs
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,37 +8,91 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace SpineViewer.Exporter
|
namespace SpineViewer.Exporter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导出类型
|
||||||
|
/// </summary>
|
||||||
|
public enum ExportType
|
||||||
|
{
|
||||||
|
Frame,
|
||||||
|
FrameSequence,
|
||||||
|
GIF,
|
||||||
|
MKV,
|
||||||
|
MP4,
|
||||||
|
MOV,
|
||||||
|
WebM
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出实现类标记
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||||
|
public class ExportImplementationAttribute : Attribute
|
||||||
|
{
|
||||||
|
public ExportType ExportType { get; }
|
||||||
|
|
||||||
|
public ExportImplementationAttribute(ExportType exportType)
|
||||||
|
{
|
||||||
|
ExportType = exportType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SFML.Graphics.Image 帧对象包装类
|
||||||
|
/// </summary>
|
||||||
|
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||||
|
{
|
||||||
|
public int Width => (int)image.Size.X;
|
||||||
|
public int Height => (int)image.Size.Y;
|
||||||
|
public string Format => "rgba";
|
||||||
|
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
||||||
|
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
||||||
|
public void Dispose() => image.Dispose();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the contents of the image to a file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
||||||
|
/// <returns>True if saving was successful</returns>
|
||||||
|
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the image to a buffer in memory The format of the image must be specified.
|
||||||
|
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
||||||
|
/// the image is empty, or if the format was invalid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="output">Byte array filled with encoded data</param>
|
||||||
|
/// <param name="format">Encoding format to use</param>
|
||||||
|
/// <returns>True if saving was successful</returns>
|
||||||
|
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Winforms Bitmap 对象
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap CopyToBitmap()
|
||||||
|
{
|
||||||
|
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||||
|
using var stream = new MemoryStream(imgBuffer);
|
||||||
|
return new(new Bitmap(stream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为帧导出创建的辅助类
|
/// 为帧导出创建的辅助类
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExportHelper
|
public static class ExportHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从纹理对象获取 Winforms Bitmap 对象
|
/// 根据 Bitmap 文件格式获取合适的文件后缀
|
||||||
/// </summary>
|
|
||||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture tex)
|
|
||||||
{
|
|
||||||
using var img = tex.CopyToImage();
|
|
||||||
img.SaveToMemory(out var imgBuffer, "bmp");
|
|
||||||
using var stream = new MemoryStream(imgBuffer);
|
|
||||||
return new Bitmap(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从纹理获取适合 FFMpegCore 的帧对象
|
|
||||||
/// </summary>
|
|
||||||
public static SFMLImageVideoFrame CopyToFrame(this SFML.Graphics.Texture tex) => new(tex.CopyToImage());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 根据文件格式获取合适的文件后缀
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetSuffix(this ImageFormat imageFormat)
|
public static string GetSuffix(this ImageFormat imageFormat)
|
||||||
{
|
{
|
||||||
if (imageFormat == ImageFormat.Icon) return ".ico";
|
if (imageFormat == ImageFormat.Icon) return ".ico";
|
||||||
else if (imageFormat == ImageFormat.Exif) return ".jpg";
|
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
|
||||||
else return $".{imageFormat.ToString().ToLower()}";
|
else return $".{imageFormat.ToString().ToLower()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region 包围盒辅助函数
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取某个包围盒下合适的视图
|
/// 获取某个包围盒下合适的视图
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -80,5 +134,7 @@ namespace SpineViewer.Exporter
|
|||||||
|
|
||||||
return new(new(x, y), new(viewX, -viewY));
|
return new(new(x, y), new(viewX, -viewY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
|
||||||
|
|
||||||
namespace SpineViewer.Exporter
|
namespace SpineViewer.Exporter
|
||||||
{
|
{
|
||||||
@@ -13,273 +15,110 @@ namespace SpineViewer.Exporter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Exporter
|
public abstract class Exporter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实现类缓存
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||||
|
|
||||||
|
static Exporter()
|
||||||
|
{
|
||||||
|
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Exporter).IsAssignableFrom(t) && !t.IsAbstract);
|
||||||
|
foreach (var type in impTypes)
|
||||||
|
{
|
||||||
|
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||||
|
if (attr is not null)
|
||||||
|
{
|
||||||
|
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||||
|
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||||
|
ImplementationTypes[attr.ExportType] = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Program.Logger.Debug("Find exporter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建指定类型导出参数
|
||||||
|
/// </summary>
|
||||||
|
public static Exporter New(ExportType exportType, ExportArgs exportArgs)
|
||||||
|
{
|
||||||
|
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||||
|
{
|
||||||
|
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||||
|
}
|
||||||
|
return (Exporter)Activator.CreateInstance(type, exportArgs);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 导出参数
|
/// 导出参数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required ExportArgs ExportArgs { get; init; }
|
public ExportArgs ExportArgs { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据参数获取渲染目标
|
/// 渲染目标
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected SFML.Graphics.RenderTexture GetRenderTexture()
|
private SFML.Graphics.RenderTexture tex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可用于文件名的时间戳字符串
|
||||||
|
/// </summary>
|
||||||
|
protected readonly string timestamp;
|
||||||
|
|
||||||
|
public Exporter(ExportArgs exportArgs)
|
||||||
|
{
|
||||||
|
ExportArgs = exportArgs;
|
||||||
|
timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取单个模型的单帧画面
|
||||||
|
/// </summary>
|
||||||
|
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine)
|
||||||
{
|
{
|
||||||
var tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height);
|
|
||||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||||
tex.SetView(ExportArgs.View);
|
tex.Draw(spine);
|
||||||
return tex;
|
tex.Display();
|
||||||
|
return new(tex.Texture.CopyToImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 得到需要渲染的模型数组,并按渲染顺序排列
|
/// 获取模型列表的单帧画面
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Spine.Spine[] GetSpinesToRender(IEnumerable<Spine.Spine> spines)
|
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
|
||||||
{
|
{
|
||||||
return spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||||
|
foreach (var spine in spinesToRender) tex.Draw(spine);
|
||||||
|
tex.Display();
|
||||||
|
return new(tex.Texture.CopyToImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每个模型在同一个画面进行导出
|
||||||
|
/// </summary>
|
||||||
|
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每个模型独立导出
|
||||||
|
/// </summary>
|
||||||
|
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行导出
|
/// 执行导出
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||||
/// <param name="worker">用来执行该函数的 worker</param>
|
/// <param name="worker">用来执行该函数的 worker</param>
|
||||||
public abstract void Export(IEnumerable<Spine.Spine> spines, BackgroundWorker? worker = null);
|
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 单帧画面导出器
|
|
||||||
/// </summary>
|
|
||||||
public class FrameExporter : Exporter
|
|
||||||
{
|
|
||||||
public override void Export(IEnumerable<Spine.Spine> spines, BackgroundWorker? worker = null)
|
|
||||||
{
|
{
|
||||||
var args = (FrameExportArgs)ExportArgs;
|
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
||||||
using var tex = GetRenderTexture();
|
|
||||||
var spinesToRender = GetSpinesToRender(spines);
|
|
||||||
var timestamp = DateTime.Now;
|
|
||||||
|
|
||||||
int total = spinesToRender.Length;
|
// tex 必须临时创建, 防止出现跨线程的情况
|
||||||
int success = 0;
|
using (tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height))
|
||||||
int error = 0;
|
|
||||||
|
|
||||||
worker?.ReportProgress(0, $"已处理 0/{total}");
|
|
||||||
for (int i = 0; i < total; i++)
|
|
||||||
{
|
{
|
||||||
if (worker?.CancellationPending == true)
|
|
||||||
{
|
|
||||||
Program.Logger.Info("Export cancelled");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var spine = spinesToRender[i];
|
|
||||||
tex.Draw(spine);
|
|
||||||
|
|
||||||
if (args.ExportSingle)
|
|
||||||
{
|
|
||||||
// 导出单个则直接算成功, 在最后一次将整体导出
|
|
||||||
success++;
|
|
||||||
if (i >= total - 1)
|
|
||||||
{
|
|
||||||
tex.Display();
|
|
||||||
|
|
||||||
// 导出单个时必定提供输出文件夹
|
|
||||||
var filename = $"frame_{timestamp:yyMMddHHmmss}{args.FileSuffix}";
|
|
||||||
var savePath = Path.Combine(args.OutputDir, filename);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var img = new Bitmap(tex.Texture.CopyToBitmap()))
|
|
||||||
{
|
|
||||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
|
||||||
img.Save(savePath, args.ImageFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Program.Logger.Error(ex.ToString());
|
|
||||||
Program.Logger.Error("Failed to save single frame");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 逐个导出则立即渲染, 并且保存完之后需要清除画面
|
|
||||||
tex.Display();
|
|
||||||
|
|
||||||
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
|
|
||||||
var filename = $"{spine.Name}_{timestamp:yyMMddHHmmss}{args.FileSuffix}";
|
|
||||||
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var img = new Bitmap(tex.Texture.CopyToBitmap()))
|
|
||||||
{
|
|
||||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
|
||||||
img.Save(savePath, args.ImageFormat);
|
|
||||||
}
|
|
||||||
success++;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Program.Logger.Error(ex.ToString());
|
|
||||||
Program.Logger.Error("Failed to save frame {}", spine.SkelPath);
|
|
||||||
error++;
|
|
||||||
}
|
|
||||||
|
|
||||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输出逐个导出的统计信息
|
|
||||||
if (!args.ExportSingle)
|
|
||||||
{
|
|
||||||
if (error > 0)
|
|
||||||
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error);
|
|
||||||
else
|
|
||||||
Program.Logger.Info("{} frames saved successfully", success);
|
|
||||||
}
|
|
||||||
|
|
||||||
Program.LogCurrentMemoryUsage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 视频导出基类
|
|
||||||
/// </summary>
|
|
||||||
public abstract class VideoExporter : Exporter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 生成单个模型的帧序列
|
|
||||||
/// </summary>
|
|
||||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SFML.Graphics.RenderTexture tex, Spine.Spine spine, BackgroundWorker? worker = null)
|
|
||||||
{
|
|
||||||
var args = (VideoExportArgs)ExportArgs;
|
|
||||||
float delta = 1f / args.FPS;
|
|
||||||
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
|
|
||||||
|
|
||||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
|
||||||
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
|
|
||||||
for (int i = 0; i < total; i++)
|
|
||||||
{
|
|
||||||
if (worker?.CancellationPending == true)
|
|
||||||
{
|
|
||||||
Program.Logger.Info("Export cancelled");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||||
tex.Draw(spine);
|
tex.SetView(ExportArgs.View);
|
||||||
spine.Update(delta);
|
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
|
||||||
tex.Display();
|
else ExportIndividual(spinesToRender, worker);
|
||||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
|
|
||||||
yield return tex.Texture.CopyToFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 生成多个模型的帧序列
|
|
||||||
/// </summary>
|
|
||||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SFML.Graphics.RenderTexture tex, Spine.Spine[] spines, BackgroundWorker? worker = null)
|
|
||||||
{
|
|
||||||
var args = (VideoExportArgs)ExportArgs;
|
|
||||||
float delta = 1f / args.FPS;
|
|
||||||
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
|
|
||||||
|
|
||||||
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
|
|
||||||
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
|
|
||||||
for (int i = 0; i < total; i++)
|
|
||||||
{
|
|
||||||
if (worker?.CancellationPending == true)
|
|
||||||
{
|
|
||||||
Program.Logger.Info("Export cancelled");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
|
||||||
foreach (var spine in spines)
|
|
||||||
{
|
|
||||||
tex.Draw(spine);
|
|
||||||
spine.Update(delta);
|
|
||||||
}
|
|
||||||
tex.Display();
|
|
||||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
|
|
||||||
yield return tex.Texture.CopyToFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 帧序列导出器
|
|
||||||
/// </summary>
|
|
||||||
public class FrameSequenceExporter : VideoExporter
|
|
||||||
{
|
|
||||||
public override void Export(IEnumerable<Spine.Spine> spines, BackgroundWorker? worker = null)
|
|
||||||
{
|
|
||||||
var args = (FrameSequenceExportArgs)ExportArgs;
|
|
||||||
using var tex = GetRenderTexture();
|
|
||||||
var spinesToRender = GetSpinesToRender(spines);
|
|
||||||
var timestamp = DateTime.Now;
|
|
||||||
|
|
||||||
if (args.ExportSingle)
|
|
||||||
{
|
|
||||||
int frameIdx = 0;
|
|
||||||
foreach (var frame in GetFrames(tex, spinesToRender, worker))
|
|
||||||
{
|
|
||||||
// 导出单个时必定提供输出文件夹
|
|
||||||
var filename = $"frames_{timestamp:yyMMddHHmmss}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
|
||||||
var savePath = Path.Combine(args.OutputDir, filename);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
frame.SaveToFile(savePath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Program.Logger.Error(ex.ToString());
|
|
||||||
Program.Logger.Error("Failed to save frame {}", savePath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
frame.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
frameIdx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var spine in spinesToRender)
|
|
||||||
{
|
|
||||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
|
||||||
|
|
||||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
|
||||||
var subDir = $"{spine.Name}_{timestamp:yyMMddHHmmss}_{args.FPS:f0}";
|
|
||||||
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
|
|
||||||
Directory.CreateDirectory(saveDir);
|
|
||||||
|
|
||||||
int frameIdx = 0;
|
|
||||||
foreach (var frame in GetFrames(tex, spine, worker))
|
|
||||||
{
|
|
||||||
var filename = $"{spine.Name}_{timestamp:yyMMddHHmmss}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
|
||||||
var savePath = Path.Combine(saveDir, filename);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
frame.SaveToFile(savePath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Program.Logger.Error(ex.ToString());
|
|
||||||
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
frame.Dispose();
|
|
||||||
}
|
|
||||||
frameIdx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
tex = null;
|
||||||
|
|
||||||
Program.LogCurrentMemoryUsage();
|
Program.LogCurrentMemoryUsage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧画面导出参数
|
||||||
|
/// </summary>
|
||||||
|
[ExportImplementation(ExportType.Frame)]
|
||||||
|
public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
|
||||||
|
{
|
||||||
|
public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧画面格式
|
||||||
|
/// </summary>
|
||||||
|
[TypeConverter(typeof(ImageFormatConverter))]
|
||||||
|
[Category("单帧画面"), DisplayName("图像格式")]
|
||||||
|
public ImageFormat ImageFormat
|
||||||
|
{
|
||||||
|
get => imageFormat;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||||
|
imageFormat = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private ImageFormat imageFormat = ImageFormat.Png;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名后缀
|
||||||
|
/// </summary>
|
||||||
|
[Category("单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||||
|
public string FileSuffix { get => imageFormat.GetSuffix(); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DPI
|
||||||
|
/// </summary>
|
||||||
|
[TypeConverter(typeof(SizeFConverter))]
|
||||||
|
[Category("单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||||
|
public SizeF DPI
|
||||||
|
{
|
||||||
|
get => dpi;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.Width <= 0) value.Width = 144;
|
||||||
|
if (value.Height <= 0) value.Height = 144;
|
||||||
|
dpi = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private SizeF dpi = new(144, 144);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 帧序列导出参数
|
||||||
|
/// </summary>
|
||||||
|
[ExportImplementation(ExportType.FrameSequence)]
|
||||||
|
public class FrameSequenceExportArgs : VideoExportArgs
|
||||||
|
{
|
||||||
|
public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名后缀
|
||||||
|
/// </summary>
|
||||||
|
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
|
||||||
|
[Category("帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||||
|
public string FileSuffix { get; set; } = ".png";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 视频导出参数基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
|
||||||
|
{
|
||||||
|
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出时长
|
||||||
|
/// </summary>
|
||||||
|
[Category("视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长")]
|
||||||
|
public float Duration { get => duration; set => duration = Math.Max(0, value); }
|
||||||
|
private float duration = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 帧率
|
||||||
|
/// </summary>
|
||||||
|
[Category("视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||||
|
public float FPS { get; set; } = 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||||
|
using SpineViewer.Spine;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧画面导出器
|
||||||
|
/// </summary>
|
||||||
|
[ExportImplementation(ExportType.Frame)]
|
||||||
|
public class FrameExporter : SpineViewer.Exporter.Exporter
|
||||||
|
{
|
||||||
|
public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
|
||||||
|
|
||||||
|
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (FrameExportArgs)ExportArgs;
|
||||||
|
|
||||||
|
// 导出单个时必定提供输出文件夹
|
||||||
|
var filename = $"frame_{timestamp}{args.FileSuffix}";
|
||||||
|
var savePath = Path.Combine(args.OutputDir, filename);
|
||||||
|
|
||||||
|
worker?.ReportProgress(0, $"已处理 0/1");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var frame = GetFrame(spinesToRender);
|
||||||
|
using var img = frame.CopyToBitmap();
|
||||||
|
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||||
|
img.Save(savePath, args.ImageFormat);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Logger.Error(ex.ToString());
|
||||||
|
Program.Logger.Error("Failed to save single frame");
|
||||||
|
}
|
||||||
|
worker?.ReportProgress(100, $"已处理 1/1");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (FrameExportArgs)ExportArgs;
|
||||||
|
|
||||||
|
int total = spinesToRender.Length;
|
||||||
|
int success = 0;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
worker?.ReportProgress(0, $"已处理 0/{total}");
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
var spine = spinesToRender[i];
|
||||||
|
|
||||||
|
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
|
||||||
|
var filename = $"{spine.Name}_{timestamp}{args.FileSuffix}";
|
||||||
|
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var frame = GetFrame(spine);
|
||||||
|
using var img = frame.CopyToBitmap();
|
||||||
|
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||||
|
img.Save(savePath, args.ImageFormat);
|
||||||
|
success++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Logger.Error(ex.ToString());
|
||||||
|
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||||
|
error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error > 0)
|
||||||
|
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error);
|
||||||
|
else
|
||||||
|
Program.Logger.Info("{} frames saved successfully", success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 帧序列导出器
|
||||||
|
/// </summary>
|
||||||
|
[ExportImplementation(ExportType.FrameSequence)]
|
||||||
|
public class FrameSequenceExporter : VideoExporter
|
||||||
|
{
|
||||||
|
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
|
||||||
|
|
||||||
|
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||||
|
int frameIdx = 0;
|
||||||
|
foreach (var frame in GetFrames(spinesToRender, worker))
|
||||||
|
{
|
||||||
|
// 导出单个时必定提供输出文件夹
|
||||||
|
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||||
|
var savePath = Path.Combine(args.OutputDir, filename);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
frame.SaveToFile(savePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Logger.Error(ex.ToString());
|
||||||
|
Program.Logger.Error("Failed to save frame {}", savePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
frame.Dispose();
|
||||||
|
}
|
||||||
|
frameIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||||
|
foreach (var spine in spinesToRender)
|
||||||
|
{
|
||||||
|
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||||
|
|
||||||
|
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||||
|
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
|
||||||
|
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
|
||||||
|
Directory.CreateDirectory(saveDir);
|
||||||
|
|
||||||
|
int frameIdx = 0;
|
||||||
|
foreach (var frame in GetFrames(spine, worker))
|
||||||
|
{
|
||||||
|
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||||
|
var savePath = Path.Combine(saveDir, filename);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
frame.SaveToFile(savePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Logger.Error(ex.ToString());
|
||||||
|
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
frame.Dispose();
|
||||||
|
}
|
||||||
|
frameIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||||
|
using SpineViewer.Spine;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 视频导出基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class VideoExporter : SpineViewer.Exporter.Exporter
|
||||||
|
{
|
||||||
|
public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成单个模型的帧序列
|
||||||
|
/// </summary>
|
||||||
|
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (VideoExportArgs)ExportArgs;
|
||||||
|
float delta = 1f / args.FPS;
|
||||||
|
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
|
||||||
|
|
||||||
|
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
if (worker?.CancellationPending == true)
|
||||||
|
{
|
||||||
|
Program.Logger.Info("Export cancelled");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = GetFrame(spine);
|
||||||
|
spine.Update(delta);
|
||||||
|
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
|
||||||
|
yield return frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成多个模型的帧序列
|
||||||
|
/// </summary>
|
||||||
|
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
var args = (VideoExportArgs)ExportArgs;
|
||||||
|
float delta = 1f / args.FPS;
|
||||||
|
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
|
||||||
|
|
||||||
|
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
if (worker?.CancellationPending == true)
|
||||||
|
{
|
||||||
|
Program.Logger.Info("Export cancelled");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = GetFrame(spinesToRender);
|
||||||
|
foreach (var spine in spinesToRender) spine.Update(delta);
|
||||||
|
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
|
||||||
|
yield return frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||||
|
{
|
||||||
|
// 导出视频格式需要把模型时间都重置到 0
|
||||||
|
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
|
||||||
|
base.Export(spines, worker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using FFMpegCore.Pipes;
|
|
||||||
|
|
||||||
namespace SpineViewer.Exporter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// SFML.Graphics.Image 帧对象包装类
|
|
||||||
/// </summary>
|
|
||||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
|
||||||
{
|
|
||||||
public int Width => (int)image.Size.X;
|
|
||||||
public int Height => (int)image.Size.Y;
|
|
||||||
public string Format => "rgba";
|
|
||||||
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
|
||||||
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
|
||||||
public void Dispose() => image.Dispose();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save the contents of the image to a file
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
|
||||||
/// <returns>True if saving was successful</returns>
|
|
||||||
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save the image to a buffer in memory The format of the image must be specified.
|
|
||||||
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
|
||||||
/// the image is empty, or if the format was invalid.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="output">Byte array filled with encoded data</param>
|
|
||||||
/// <param name="format">Encoding format to use</param>
|
|
||||||
/// <returns>True if saving was successful</returns>
|
|
||||||
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
SpineViewer/MainForm.Designer.cs
generated
8
SpineViewer/MainForm.Designer.cs
generated
@@ -142,14 +142,14 @@
|
|||||||
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
|
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
|
||||||
toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
|
toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
|
||||||
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
|
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
|
||||||
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
|
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
|
||||||
//
|
//
|
||||||
// toolStripMenuItem_ExportFrames
|
// toolStripMenuItem_ExportFrameSequence
|
||||||
//
|
//
|
||||||
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrames";
|
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
|
||||||
toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
|
toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
|
||||||
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
||||||
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
|
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
|
||||||
//
|
//
|
||||||
// toolStripMenuItem_ExportGif
|
// toolStripMenuItem_ExportGif
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ namespace SpineViewer
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
InitializeLogConfiguration();
|
InitializeLogConfiguration();
|
||||||
|
|
||||||
|
// 在此处将导出菜单需要的类绑定起来
|
||||||
|
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
|
||||||
|
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -68,43 +72,16 @@ namespace SpineViewer
|
|||||||
spineListView.BatchAdd();
|
spineListView.BatchAdd();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
|
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
lock (spineListView.Spines)
|
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
|
||||||
{
|
|
||||||
if (spineListView.Spines.Count <= 0)
|
|
||||||
{
|
|
||||||
MessageBox.Info("请至少打开一个骨骼文件");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spinePreviewer.IsUpdating)
|
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
|
||||||
{
|
{
|
||||||
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var exportDialog = new Dialogs.ExportDialog()
|
|
||||||
{
|
|
||||||
ExportArgs = new FrameExportArgs()
|
|
||||||
{
|
|
||||||
Resolution = spinePreviewer.Resolution,
|
|
||||||
View = spinePreviewer.GetView(),
|
|
||||||
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var progressDialog = new Dialogs.ProgressDialog();
|
|
||||||
progressDialog.DoWork += ExportFrame_Work;
|
|
||||||
progressDialog.RunWorkerAsync(exportDialog.ExportArgs);
|
|
||||||
progressDialog.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
lock (spineListView.Spines)
|
lock (spineListView.Spines)
|
||||||
{
|
{
|
||||||
if (spineListView.Spines.Count <= 0)
|
if (spineListView.Spines.Count <= 0)
|
||||||
@@ -114,21 +91,16 @@ namespace SpineViewer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var exportDialog = new Dialogs.ExportDialog()
|
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
|
||||||
{
|
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
|
||||||
ExportArgs = new FrameSequenceExportArgs()
|
|
||||||
{
|
|
||||||
Resolution = spinePreviewer.Resolution,
|
|
||||||
View = spinePreviewer.GetView(),
|
|
||||||
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var exporter = Exporter.Exporter.New(type, exportArgs);
|
||||||
|
|
||||||
var progressDialog = new Dialogs.ProgressDialog();
|
var progressDialog = new Dialogs.ProgressDialog();
|
||||||
progressDialog.DoWork += ExportFrameSequence_Work;
|
progressDialog.DoWork += Export_Work;
|
||||||
progressDialog.RunWorkerAsync(exportDialog.ExportArgs);
|
progressDialog.RunWorkerAsync(exporter);
|
||||||
progressDialog.ShowDialog();
|
progressDialog.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,22 +208,12 @@ namespace SpineViewer
|
|||||||
propertyGrid_Spine.Refresh();
|
propertyGrid_Spine.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExportFrame_Work(object? sender, DoWorkEventArgs e)
|
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||||
{
|
{
|
||||||
var worker = (BackgroundWorker)sender;
|
var worker = (BackgroundWorker)sender;
|
||||||
var exporter = new FrameExporter() { ExportArgs = (ExportArgs)e.Argument };
|
var exporter = (Exporter.Exporter)e.Argument;
|
||||||
spinePreviewer.StopRender();
|
spinePreviewer.StopRender();
|
||||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines, (BackgroundWorker)sender); }
|
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.ToArray(), (BackgroundWorker)sender); }
|
||||||
e.Cancel = worker.CancellationPending;
|
|
||||||
spinePreviewer.StartRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExportFrameSequence_Work(object? sender, DoWorkEventArgs e)
|
|
||||||
{
|
|
||||||
var worker = (BackgroundWorker)sender;
|
|
||||||
var exporter = new FrameSequenceExporter() { ExportArgs = (ExportArgs)e.Argument };
|
|
||||||
spinePreviewer.StopRender();
|
|
||||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines, (BackgroundWorker)sender); }
|
|
||||||
e.Cancel = worker.CancellationPending;
|
e.Cancel = worker.CancellationPending;
|
||||||
spinePreviewer.StartRender();
|
spinePreviewer.StartRender();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user