修复导出过程中的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))]
[TypeConverter(typeof(SFMLColorConverter))]
[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>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因

View File

@@ -1,5 +1,4 @@
using NLog;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -52,28 +51,55 @@ namespace SpineViewer.Exporter
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine)
{
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture();
tex.Clear(ExportArgs.BackgroundColor);
tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
}
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) => GetFrame([spine]);
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
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();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(ExportArgs.BackgroundColor);
foreach (var spine in spinesToRender) tex.Draw(spine);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
}
else
{
return new(texPma.Texture.CopyToImage());
}
}
/// <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)
{
// 给一个纯白的背景
BackgroundColor = new(255, 255, 255, 0);
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
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)
{
BackgroundColor = new(0, 255, 0, 0);
BackgroundColor = new(0, 255, 0);
}
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)
{
BackgroundColor = new(0, 255, 0, 0);
BackgroundColor = new(0, 255, 0);
}
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)
{
BackgroundColor = new(0, 255, 0, 0);
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mp4";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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