更换为wpf
This commit is contained in:
198
Spine/Exporters/BaseExporter.cs
Normal file
198
Spine/Exporters/BaseExporter.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using NLog;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出类基类, 提供基本的帧渲染功能
|
||||
/// </summary>
|
||||
public abstract class BaseExporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 用于渲染的画布
|
||||
/// </summary>
|
||||
protected RenderTexture _renderTexture;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化导出器
|
||||
/// </summary>
|
||||
/// <param name="width">画布宽像素值</param>
|
||||
/// <param name="height">画布高像素值</param>
|
||||
public BaseExporter(uint width , uint height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
throw new ArgumentException($"Invalid resolution: {width}, {height}");
|
||||
_renderTexture = new(width, height);
|
||||
_renderTexture.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化导出器
|
||||
/// </summary>
|
||||
public BaseExporter(Vector2u resolution)
|
||||
{
|
||||
if (resolution.X <= 0 || resolution.Y <= 0)
|
||||
throw new ArgumentException($"Invalid resolution: {resolution}");
|
||||
_renderTexture = new(resolution.X, resolution.Y);
|
||||
_renderTexture.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可选的进度回调函数
|
||||
/// <list type="number">
|
||||
/// <item><c>total</c>: 任务总量</item>
|
||||
/// <item><c>done</c>: 已完成量</item>
|
||||
/// <item><c>progressText</c>: 需要设置的进度提示文本</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public Action<float, float, string>? ProgressReporter { get => _progressReporter; set => _progressReporter = value; }
|
||||
protected Action<float, float, string>? _progressReporter;
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => _backgroundColor;
|
||||
set
|
||||
{
|
||||
_backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
_backgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
protected Color _backgroundColor = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
protected Color _backgroundColorPma = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// <inheritdoc cref="RenderTexture.Size"/>
|
||||
/// </summary>
|
||||
public Vector2u Resolution
|
||||
{
|
||||
get => _renderTexture.Size;
|
||||
set
|
||||
{
|
||||
if (value.X <= 0 || value.Y <= 0)
|
||||
{
|
||||
_logger.Warn("Omit invalid exporter resolution: {0}", value);
|
||||
return;
|
||||
}
|
||||
if (_renderTexture.Size != value)
|
||||
{
|
||||
using var old = _renderTexture;
|
||||
using var view = old.GetView();
|
||||
var renderTexture = new RenderTexture(value.X, value.Y);
|
||||
renderTexture.SetActive(false);
|
||||
renderTexture.SetView(view);
|
||||
_renderTexture = renderTexture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="View.Viewport"/>
|
||||
/// </summary>
|
||||
public FloatRect Viewport
|
||||
{
|
||||
get { using var view = _renderTexture.GetView(); return view.Viewport; }
|
||||
set { using var view = _renderTexture.GetView(); view.Viewport = value; _renderTexture.SetView(view); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="View.Center"/>
|
||||
/// </summary>
|
||||
public Vector2f Center
|
||||
{
|
||||
get { using var view = _renderTexture.GetView(); return view.Center; }
|
||||
set { using var view = _renderTexture.GetView(); view.Center = value; _renderTexture.SetView(view); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="View.Size"/>
|
||||
/// </summary>
|
||||
public Vector2f Size
|
||||
{
|
||||
get { using var view = _renderTexture.GetView(); return view.Size; }
|
||||
set { using var view = _renderTexture.GetView(); view.Size = value; _renderTexture.SetView(view); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="View.Rotation"/>
|
||||
/// </summary>
|
||||
public float Rotation
|
||||
{
|
||||
get { using var view = _renderTexture.GetView(); return view.Rotation; }
|
||||
set { using var view = _renderTexture.GetView(); view.Rotation = value; _renderTexture.SetView(view); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取的一帧, 结果是预乘的
|
||||
/// </summary>
|
||||
protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||
{
|
||||
_renderTexture.SetActive(true);
|
||||
_renderTexture.Clear(_backgroundColorPma);
|
||||
foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp);
|
||||
_renderTexture.Display();
|
||||
_renderTexture.SetActive(false);
|
||||
return new(_renderTexture.Texture.CopyToImage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
|
||||
/// </summary>
|
||||
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
|
||||
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
|
||||
public abstract void Export(string output, params SpineObject[] spines);
|
||||
|
||||
#region IDisposable 接口实现
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (disposing)
|
||||
{
|
||||
_renderTexture.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
~BaseExporter()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
if (_disposed)
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
82
Spine/Exporters/CustomFFmpegExporter.cs
Normal file
82
Spine/Exporters/CustomFFmpegExporter.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using FFMpegCore;
|
||||
using FFMpegCore.Pipes;
|
||||
using SFML.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 自定义参数的 FFmpeg 导出类
|
||||
/// </summary>
|
||||
public class CustomFFmpegExporter : VideoExporter
|
||||
{
|
||||
public CustomFFmpegExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||
public CustomFFmpegExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
/// <summary>
|
||||
/// <c>-f</c>
|
||||
/// </summary>
|
||||
public string? Format { get => _format; set => _format = value; }
|
||||
private string? _format;
|
||||
|
||||
/// <summary>
|
||||
/// <c>-c:v</c>
|
||||
/// </summary>
|
||||
public string? Codec { get => _codec; set => _codec = value; }
|
||||
private string? _codec;
|
||||
|
||||
/// <summary>
|
||||
/// <c>-pix_fmt</c>
|
||||
/// </summary>
|
||||
public string? PixelFormat { get => _pixelFormat; set => _pixelFormat = value; }
|
||||
private string? _pixelFormat;
|
||||
|
||||
/// <summary>
|
||||
/// <c>-b:v</c>
|
||||
/// </summary>
|
||||
public string? Bitrate { get => _bitrate; set => _bitrate = value; }
|
||||
private string? _bitrate;
|
||||
|
||||
/// <summary>
|
||||
/// <c>-vf</c>
|
||||
/// </summary>
|
||||
public string? Filter { get => _filter; set => _filter = value; }
|
||||
private string? _filter;
|
||||
|
||||
/// <summary>
|
||||
/// 其他自定义参数
|
||||
/// </summary>
|
||||
public string? CustomArgs { get => _customArgs; set => _customArgs = value; }
|
||||
private string? _customArgs;
|
||||
|
||||
private void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_format)) options.ForceFormat(_format);
|
||||
if (!string.IsNullOrEmpty(_codec)) options.WithVideoCodec(_codec);
|
||||
if (!string.IsNullOrEmpty(_pixelFormat)) options.ForcePixelFormat(_pixelFormat);
|
||||
if (!string.IsNullOrEmpty(_bitrate)) options.WithCustomArgument($"-b:v {_bitrate}");
|
||||
if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf unpremultiply=inplace=1, {_customArgs}");
|
||||
else options.WithCustomArgument("-vf unpremultiply=inplace=1");
|
||||
}
|
||||
|
||||
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||
{
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, SetOutputOptions);
|
||||
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
Spine/Exporters/FFmpegVideoExporter.cs
Normal file
146
Spine/Exporters/FFmpegVideoExporter.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using FFMpegCore;
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Pipes;
|
||||
using NLog;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 FFmpeg 命令行的导出类, 可以导出动图及视频格式
|
||||
/// </summary>
|
||||
public class FFmpegVideoExporter : VideoExporter
|
||||
{
|
||||
public FFmpegVideoExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||
public FFmpegVideoExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
/// <summary>
|
||||
/// FFmpeg 导出格式
|
||||
/// </summary>
|
||||
public enum VideoFormat
|
||||
{
|
||||
Gif,
|
||||
Webp,
|
||||
Mp4,
|
||||
Webm,
|
||||
Mkv,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频格式
|
||||
/// </summary>
|
||||
public VideoFormat Format { get => _format; set => _format = value; }
|
||||
private VideoFormat _format = VideoFormat.Mp4;
|
||||
|
||||
/// <summary>
|
||||
/// 动图是否循环
|
||||
/// </summary>
|
||||
public bool Loop { get => _loop; set => _loop = value; }
|
||||
private bool _loop = true;
|
||||
|
||||
/// <summary>
|
||||
/// 质量
|
||||
/// </summary>
|
||||
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||
private int _quality = 75;
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
|
||||
private int _crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 获取的一帧, 结果是预乘的
|
||||
/// </summary>
|
||||
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
|
||||
{
|
||||
// XXX: 不知道为什么用 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 };
|
||||
Action<FFMpegArgumentOptions> setOutputOptions = _format switch
|
||||
{
|
||||
VideoFormat.Gif => SetGifOptions,
|
||||
VideoFormat.Webp => SetWebpOptions,
|
||||
VideoFormat.Mp4 => SetMp4Options,
|
||||
VideoFormat.Webm => SetWebmOptions,
|
||||
VideoFormat.Mkv => SetMkvOptions,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, setOutputOptions);
|
||||
|
||||
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetGifOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
// Gif 固定使用 256 调色板和 128 透明度阈值
|
||||
var v = "split [s0][s1]";
|
||||
var s0 = "[s0] palettegen=reserve_transparent=1:max_colors=256 [p]";
|
||||
var s1 = "[s1][p] paletteuse=dither=bayer:alpha_threshold=128";
|
||||
var customArgs = $"-vf \"unpremultiply=inplace=1, {v};{s0};{s1}\" -loop {(_loop ? 0 : -1)}";
|
||||
options.ForceFormat("gif")
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetWebpOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = $"-vf unpremultiply=inplace=1 -quality {_quality} -loop {(_loop ? 0 : 1)}";
|
||||
options.ForceFormat("webp").WithVideoCodec("libwebp_anim").ForcePixelFormat("yuva420p")
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetMp4Options(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = "-vf unpremultiply=inplace=1";
|
||||
options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv444p")
|
||||
.WithFastStart()
|
||||
.WithConstantRateFactor(_crf)
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetWebmOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = "-vf unpremultiply=inplace=1";
|
||||
options.ForceFormat("webm").WithVideoCodec("libvpx-vp9").ForcePixelFormat("yuva420p")
|
||||
.WithConstantRateFactor(_crf)
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetMkvOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = "-vf unpremultiply=inplace=1";
|
||||
options.ForceFormat("matroska").WithVideoCodec("libx265").ForcePixelFormat("yuv444p")
|
||||
.WithConstantRateFactor(_crf)
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
37
Spine/Exporters/FrameExporter.cs
Normal file
37
Spine/Exporters/FrameExporter.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using SFML.System;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出类
|
||||
/// </summary>
|
||||
public class FrameExporter : BaseExporter
|
||||
{
|
||||
public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||
public FrameExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
public SKEncodedImageFormat Format { get => _format; set => _format = value; }
|
||||
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
|
||||
|
||||
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||
protected int _quality = 80;
|
||||
|
||||
public override void Export(string output, params SpineObject[] spines)
|
||||
{
|
||||
using var frame = GetFrame(spines);
|
||||
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||
using var data = skImage.Encode(_format, _quality);
|
||||
using var stream = File.OpenWrite(output);
|
||||
data.SaveTo(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Spine/Exporters/FrameSequenceExporter.cs
Normal file
61
Spine/Exporters/FrameSequenceExporter.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using NLog;
|
||||
using SFML.System;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出器, 导出 png 帧序列
|
||||
/// </summary>
|
||||
public class FrameSequenceExporter : VideoExporter
|
||||
{
|
||||
public FrameSequenceExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||
public FrameSequenceExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
|
||||
int frameCount = GetFrameCount();
|
||||
int frameIdx = 0;
|
||||
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||
foreach (var frame in GetFrames(spines))
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
_logger.Info("Export cancelled");
|
||||
frame.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png");
|
||||
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||
|
||||
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}");
|
||||
try
|
||||
{
|
||||
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||
using var data = skImage.Encode(SKEncodedImageFormat.Png, 100);
|
||||
using var stream = File.OpenWrite(savePath);
|
||||
data.SaveTo(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to save frame {0}, {1}", savePath, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.Dispose();
|
||||
}
|
||||
frameIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Spine/Exporters/SFMLImageVideoFrame.cs
Normal file
52
Spine/Exporters/SFMLImageVideoFrame.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using SFML.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="SFML.Graphics.Image"/> 帧对象包装类, 将接管给定对象生命周期
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
private readonly Image _image = image;
|
||||
|
||||
/// <summary>
|
||||
/// 接管的 <see cref="SFML.Graphics.Image"/> 内部对象
|
||||
/// </summary>
|
||||
public Image Image => _image;
|
||||
|
||||
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);
|
||||
|
||||
#region IDisposable 接口实现
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_image.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
142
Spine/Exporters/VideoExporter.cs
Normal file
142
Spine/Exporters/VideoExporter.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using NLog;
|
||||
using SFML.System;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
/// <summary>
|
||||
/// 多帧画面导出基类, 可以获取连续的帧序列
|
||||
/// </summary>
|
||||
public abstract class VideoExporter : BaseExporter
|
||||
{
|
||||
public VideoExporter(uint width, uint height) : base(width, height) { }
|
||||
public VideoExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
public float Duration
|
||||
{
|
||||
get => _duration;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
_logger.Warn("Omit invalid duration: {0}", value);
|
||||
return;
|
||||
}
|
||||
_duration = value;
|
||||
}
|
||||
}
|
||||
protected float _duration = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
public float Fps
|
||||
{
|
||||
get => _fps;
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
_logger.Warn("Omit invalid fps: {0}", value);
|
||||
return;
|
||||
}
|
||||
_fps = value;
|
||||
}
|
||||
}
|
||||
protected float _fps = 24;
|
||||
|
||||
/// <summary>
|
||||
/// 是否保留最后一帧
|
||||
/// </summary>
|
||||
public bool KeepLast { get => _keepLast; set => _keepLast = value; }
|
||||
protected bool _keepLast = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取总帧数
|
||||
/// </summary>
|
||||
public int GetFrameCount()
|
||||
{
|
||||
var delta = 1f / _fps;
|
||||
var total = (int)(_duration * _fps); // 完整帧的数量
|
||||
|
||||
var deltaFinal = _duration - delta * total; // 最后一帧时长
|
||||
var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0;
|
||||
|
||||
var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
|
||||
return frameCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
|
||||
{
|
||||
float delta = 1f / _fps;
|
||||
int total = (int)(_duration * _fps); // 完整帧的数量
|
||||
bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3;
|
||||
|
||||
// 导出首帧
|
||||
var firstFrame = GetFrame(spines);
|
||||
yield return firstFrame;
|
||||
|
||||
// 导出完整帧
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
foreach (var spine in spines) spine.Update(delta);
|
||||
yield return GetFrame(spines);
|
||||
}
|
||||
|
||||
// 导出最后一帧
|
||||
if (hasFinal)
|
||||
{
|
||||
// XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
|
||||
foreach (var spine in spines) spine.Update(delta);
|
||||
yield return GetFrame(spines);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成帧序列, 支持中途取消和进度输出
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, string output, CancellationToken ct)
|
||||
{
|
||||
int frameCount = GetFrameCount();
|
||||
int frameIdx = 0;
|
||||
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||
foreach (var frame in GetFrames(spines))
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
_logger.Info("Export cancelled");
|
||||
frame.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
|
||||
yield return frame;
|
||||
frameIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines);
|
||||
|
||||
/// <summary>
|
||||
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
|
||||
/// </summary>
|
||||
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
|
||||
public abstract void Export(string output, CancellationToken ct, params SpineObject[] spines);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user