From 09c8e4f779f3308d6be26d32a8236e5a6513d220 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Fri, 4 Apr 2025 17:21:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=87=BA=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E4=B8=AD=E7=9A=84PMA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/Exporter/ExportArgs.cs | 22 ++++++- SpineViewer/Exporter/Exporter.cs | 58 ++++++++++++++----- .../ExportArgs/GifExportArgs.cs | 3 - .../ExportArgs/MkvExportArgs.cs | 2 +- .../ExportArgs/MovExportArgs.cs | 2 +- .../ExportArgs/Mp4ExportArgs.cs | 2 +- SpineViewer/MainForm.cs | 2 +- SpineViewer/{Spine => }/Shader.cs | 28 +++++---- .../Spine/Implementations/Spine/Spine21.cs | 2 +- .../Spine/Implementations/Spine/Spine36.cs | 2 +- .../Spine/Implementations/Spine/Spine37.cs | 2 +- .../Spine/Implementations/Spine/Spine38.cs | 2 +- .../Spine/Implementations/Spine/Spine40.cs | 2 +- .../Spine/Implementations/Spine/Spine41.cs | 2 +- .../Spine/Implementations/Spine/Spine42.cs | 2 +- SpineViewer/Spine/Spine.cs | 9 ++- 16 files changed, 99 insertions(+), 43 deletions(-) rename SpineViewer/{Spine => }/Shader.cs (75%) diff --git a/SpineViewer/Exporter/ExportArgs.cs b/SpineViewer/Exporter/ExportArgs.cs index a40b51f..6abfa1f 100644 --- a/SpineViewer/Exporter/ExportArgs.cs +++ b/SpineViewer/Exporter/ExportArgs.cs @@ -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; + + /// + /// 预乘后的背景颜色 + /// + [Browsable(false)] + public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent; /// /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因 diff --git a/SpineViewer/Exporter/Exporter.cs b/SpineViewer/Exporter/Exporter.cs index 2541937..6fe6a2e 100644 --- a/SpineViewer/Exporter/Exporter.cs +++ b/SpineViewer/Exporter/Exporter.cs @@ -1,5 +1,4 @@ using NLog; -using SpineViewer.Spine; using System; using System.Collections.Generic; using System.ComponentModel; @@ -52,27 +51,54 @@ namespace SpineViewer.Exporter /// /// 获取单个模型的单帧画面 /// - 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]); /// /// 获取模型列表的单帧画面 /// protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender) { - // tex 必须临时创建, 随用随取, 防止出现跨线程的情况 - using var tex = GetRenderTexture(); - tex.Clear(ExportArgs.BackgroundColor); - foreach (var spine in spinesToRender) tex.Draw(spine); - tex.Display(); - return new(tex.Texture.CopyToImage()); + // 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); + tex.Draw(sp, st); + tex.Display(); + return new(tex.Texture.CopyToImage()); + } + else + { + return new(texPma.Texture.CopyToImage()); + } } /// diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs index ebe6b53..3919f6b 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs @@ -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; } diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs index b8145d6..43bef4e 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs @@ -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"; diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs index 172c274..9af32a1 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs @@ -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"; diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs index c6d3ec3..2e928a2 100644 --- a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs +++ b/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs @@ -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"; diff --git a/SpineViewer/MainForm.cs b/SpineViewer/MainForm.cs index dc22e50..5ac910d 100644 --- a/SpineViewer/MainForm.cs +++ b/SpineViewer/MainForm.cs @@ -28,7 +28,7 @@ namespace SpineViewer // 执行一些初始化工作 try { - Spine.Shader.Init(); + Shader.Init(); } catch (Exception ex) { diff --git a/SpineViewer/Spine/Shader.cs b/SpineViewer/Shader.cs similarity index 75% rename from SpineViewer/Spine/Shader.cs rename to SpineViewer/Shader.cs index a2b2ded..f62001c 100644 --- a/SpineViewer/Spine/Shader.cs +++ b/SpineViewer/Shader.cs @@ -4,39 +4,39 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace SpineViewer.Spine +namespace SpineViewer { public static class Shader { /// /// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值 /// - 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; }" - ); + ; /// /// 用于预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘) /// - 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; }" - ); + ; /// /// 预乘转非预乘 fragment shader /// - 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; }" + ; /// /// 考虑了顶点透明度变化的着色器, 输入是非预乘纹理像素, 输出是预乘像素 @@ -48,6 +48,11 @@ namespace SpineViewer.Spine /// private static SFML.Graphics.Shader? VertexAlphaPma = null; + /// + /// 反预乘着色器, 用于得到正确透明度的非预乘画面 + /// + public static SFML.Graphics.Shader? InversePma { get; private set; } + /// /// 加载 Shader, 可能会存在异常导致着色器加载失败 /// @@ -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); } /// - /// 获取合适的着色器 + /// 获取绘制 Spine 的着色器 /// /// 纹理是否是预乘的 /// 是否是双色着色的(TODO) - 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; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine21.cs b/SpineViewer/Spine/Implementations/Spine/Spine21.cs index f91c6d4..a4adf4a 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine21.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine21.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine36.cs b/SpineViewer/Spine/Implementations/Spine/Spine36.cs index ddc365a..abadf04 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine36.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine36.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine37.cs b/SpineViewer/Spine/Implementations/Spine/Spine37.cs index 61d66ce..9b17147 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine37.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine37.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine38.cs b/SpineViewer/Spine/Implementations/Spine/Spine38.cs index b8f2073..09e1d8f 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine38.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine38.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine40.cs b/SpineViewer/Spine/Implementations/Spine/Spine40.cs index b6ec861..e6f3765 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine40.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine40.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine41.cs b/SpineViewer/Spine/Implementations/Spine/Spine41.cs index 3471db1..52a5f68 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine41.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine41.cs @@ -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) diff --git a/SpineViewer/Spine/Implementations/Spine/Spine42.cs b/SpineViewer/Spine/Implementations/Spine/Spine42.cs index 1697385..46394e7 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine42.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine42.cs @@ -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) diff --git a/SpineViewer/Spine/Spine.cs b/SpineViewer/Spine/Spine.cs index 80f1829..ac2ccaf 100644 --- a/SpineViewer/Spine/Spine.cs +++ b/SpineViewer/Spine/Spine.cs @@ -370,7 +370,7 @@ namespace SpineViewer.Spine protected abstract RectangleF bounds { get; } /// - /// 骨骼预览图 + /// 骨骼预览图, 并没有去除预乘, 画面可能偏暗 /// [Browsable(false)] public Image Preview { get; private set; } @@ -405,8 +405,15 @@ namespace SpineViewer.Spine /// /// SFML.Graphics.Drawable 接口实现 + /// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗 + /// 可以用于 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的 /// public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); } + + /// + /// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗 + /// 可以用于 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的 + /// protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states); #endregion