修复导出过程中的PMA问题

This commit is contained in:
ww-rm
2025-04-04 17:21:30 +08:00
parent 6994fa6be8
commit 09c8e4f779
16 changed files with 99 additions and 43 deletions

View File

@@ -72,7 +72,27 @@ namespace SpineViewer.Exporter
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))] [Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))] [TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")] [Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent; public SFML.Graphics.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;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
[Browsable(false)]
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
/// <summary> /// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因 /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因

View File

@@ -1,5 +1,4 @@
using NLog; using NLog;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -52,28 +51,55 @@ namespace SpineViewer.Exporter
/// <summary> /// <summary>
/// 获取单个模型的单帧画面 /// 获取单个模型的单帧画面
/// </summary> /// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) => GetFrame([spine]);
{
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture();
tex.Clear(ExportArgs.BackgroundColor);
tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
}
/// <summary> /// <summary>
/// 获取模型列表的单帧画面 /// 获取模型列表的单帧画面
/// </summary> /// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender) protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
{ {
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况 // RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(ExportArgs.BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (ExportArgs.BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
using var img = texPma.Texture.CopyToImage();
using var texSprite = new SFML.Graphics.Texture(img);
using var sp = new SFML.Graphics.Sprite(texSprite)
{
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
Position = new(view.Center.X, view.Center.Y),
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
Rotation = view.Rotation
};
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero); // 用源的颜色和透明度直接覆盖
st.Shader = Shader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture(); using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(ExportArgs.BackgroundColor); tex.Clear(ExportArgs.BackgroundColor);
foreach (var spine in spinesToRender) tex.Draw(spine); tex.Draw(sp, st);
tex.Display(); tex.Display();
return new(tex.Texture.CopyToImage()); return new(tex.Texture.CopyToImage());
} }
else
{
return new(texPma.Texture.CopyToImage());
}
}
/// <summary> /// <summary>
/// 每个模型在同一个画面进行导出 /// 每个模型在同一个画面进行导出

View File

@@ -16,9 +16,6 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
{ {
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{ {
// 给一个纯白的背景
BackgroundColor = new(255, 255, 255, 0);
// GIF 的帧率不能太高, 超过 50 帧反而会变慢 // GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 12; FPS = 12;
} }

View File

@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
{ {
public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{ {
BackgroundColor = new(0, 255, 0, 0); BackgroundColor = new(0, 255, 0);
} }
public override string Format => "matroska"; public override string Format => "matroska";

View File

@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
{ {
public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{ {
BackgroundColor = new(0, 255, 0, 0); BackgroundColor = new(0, 255, 0);
} }
public override string Format => "mov"; public override string Format => "mov";

View File

@@ -16,7 +16,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
{ {
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{ {
BackgroundColor = new(0, 255, 0, 0); BackgroundColor = new(0, 255, 0);
} }
public override string Format => "mp4"; public override string Format => "mp4";

View File

@@ -28,7 +28,7 @@ namespace SpineViewer
// 执行一些初始化工作 // 执行一些初始化工作
try try
{ {
Spine.Shader.Init(); Shader.Init();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -4,39 +4,39 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer
{ {
public static class Shader public static class Shader
{ {
/// <summary> /// <summary>
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值 /// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值
/// </summary> /// </summary>
private const string FRAGMENT_VertexAlpha = ( private const string FRAGMENT_VertexAlpha =
"uniform sampler2D t;" + "uniform sampler2D t;" +
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" + "void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
"p.rgb *= p.a * gl_Color.a;" + "p.rgb *= p.a * gl_Color.a;" +
"gl_FragColor = gl_Color * p; }" "gl_FragColor = gl_Color * p; }"
); ;
/// <summary> /// <summary>
/// 用于预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘) /// 用于预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘)
/// </summary> /// </summary>
private const string FRAGMENT_VertexAlphaPma = ( private const string FRAGMENT_VertexAlphaPma =
"uniform sampler2D t;" + "uniform sampler2D t;" +
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" + "void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
"p.rgb *= gl_Color.a;" + "p.rgb *= gl_Color.a;" +
"gl_FragColor = gl_Color * p; }" "gl_FragColor = gl_Color * p; }"
); ;
/// <summary> /// <summary>
/// 预乘转非预乘 fragment shader /// 预乘转非预乘 fragment shader
/// </summary> /// </summary>
private const string FRAGMENT_PmaInv = ( private const string FRAGMENT_InvPma =
"uniform sampler2D t;" + "uniform sampler2D t;" +
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" + "void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
"p.rgb *= gl_Color.a;" + "if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
"gl_FragColor = gl_Color * p; }" "gl_FragColor = p; }"
); ;
/// <summary> /// <summary>
/// 考虑了顶点透明度变化的着色器, 输入是非预乘纹理像素, 输出是预乘像素 /// 考虑了顶点透明度变化的着色器, 输入是非预乘纹理像素, 输出是预乘像素
@@ -48,6 +48,11 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
private static SFML.Graphics.Shader? VertexAlphaPma = null; private static SFML.Graphics.Shader? VertexAlphaPma = null;
/// <summary>
/// 反预乘着色器, 用于得到正确透明度的非预乘画面
/// </summary>
public static SFML.Graphics.Shader? InversePma { get; private set; }
/// <summary> /// <summary>
/// 加载 Shader, 可能会存在异常导致着色器加载失败 /// 加载 Shader, 可能会存在异常导致着色器加载失败
/// </summary> /// </summary>
@@ -56,14 +61,15 @@ namespace SpineViewer.Spine
{ {
VertexAlpha = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlpha); VertexAlpha = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlpha);
VertexAlphaPma = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlphaPma); VertexAlphaPma = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_VertexAlphaPma);
InversePma = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_InvPma);
} }
/// <summary> /// <summary>
/// 获取合适的着色器 /// 获取绘制 Spine 的着色器
/// </summary> /// </summary>
/// <param name="pma">纹理是否是预乘的</param> /// <param name="pma">纹理是否是预乘的</param>
/// <param name="twoColor">是否是双色着色的(TODO)</param> /// <param name="twoColor">是否是双色着色的(TODO)</param>
public static SFML.Graphics.Shader? GetShader(bool pma, bool twoColor = false) public static SFML.Graphics.Shader? GetSpineShader(bool pma, bool twoColor = false)
{ {
if (pma) if (pma)
return VertexAlphaPma; return VertexAlphaPma;

View File

@@ -261,7 +261,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -220,7 +220,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -190,7 +190,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -196,7 +196,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -192,7 +192,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -192,7 +192,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -192,7 +192,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetShader(usePremultipliedAlpha); states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -370,7 +370,7 @@ namespace SpineViewer.Spine
protected abstract RectangleF bounds { get; } protected abstract RectangleF bounds { get; }
/// <summary> /// <summary>
/// 骨骼预览图 /// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
/// </summary> /// </summary>
[Browsable(false)] [Browsable(false)]
public Image Preview { get; private set; } public Image Preview { get; private set; }
@@ -405,8 +405,15 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// SFML.Graphics.Drawable 接口实现 /// SFML.Graphics.Drawable 接口实现
/// <para>这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗</para>
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
/// </summary> /// </summary>
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); } public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); }
/// <summary>
/// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
/// </summary>
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states); protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion #endregion