internal/atlas: optimization: send premultiplied alpha from vertex to fragment shader. (#1996)

Note that this applies only to the builtin shaders - interface for Kage stays
unchanged for compatibility.

Minor compatibility delta: when interpolating alpha values, previous code has
created nonsense values, such as, when interpolating from
fully-transparent-black (0,0,0,0) to opaque-white (1,1,1,1), something like
half-transparent-grey (0.25,0.25,0.25,0.5) where half-transparent-white
(0.5,0.5,0.5,0.5) is used by the new code.

I assume this is a strict improvement, however this may warrant some testing.

Possible later improvement could be moving the premultiplication from fragment
shader to CPU. Did not do this as it makes the code rather inconsistent of Kage
vs built-in shader usage.

Updates #1772
This commit is contained in:
divVerent 2022-02-23 12:27:50 -05:00 committed by GitHub
parent 476f4e3f9a
commit f2209a0b51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 18 deletions

View File

@ -223,8 +223,13 @@ type Vertex struct {
SrcY float32 SrcY float32
// ColorR/ColorG/ColorB/ColorA represents color scaling values. // ColorR/ColorG/ColorB/ColorA represents color scaling values.
// 1 means the original source image color is used. // Their interpretation depends on the concrete draw call used:
// 0 means a transparent color is used. // - DrawTriangles: straight-alpha encoded color multiplier.
// If ColorA is 0, the vertex is fully transparent and color is ignored.
// If ColorA is 1, the vertex has the color (ColorR, ColorG, ColorB).
// Vertex colors are interpolated linearly respecting alpha.
// - DrawTrianglesShader: arbitrary floating point values sent to the shader.
// These are interpolated linearly and independently from each other.
ColorR float32 ColorR float32
ColorG float32 ColorG float32
ColorB float32 ColorB float32
@ -293,6 +298,8 @@ const MaxIndicesNum = graphics.IndicesNum
// DrawTriangles draws triangles with the specified vertices and their indices. // DrawTriangles draws triangles with the specified vertices and their indices.
// //
// Vertex contains color values, which are interpreted as straight-alpha colors.
//
// If len(indices) is not multiple of 3, DrawTriangles panics. // If len(indices) is not multiple of 3, DrawTriangles panics.
// //
// If len(indices) is more than MaxIndicesNum, DrawTriangles panics. // If len(indices) is more than MaxIndicesNum, DrawTriangles panics.
@ -406,6 +413,8 @@ func init() {
// DrawTrianglesShader draws triangles with the specified vertices and their indices with the specified shader. // DrawTrianglesShader draws triangles with the specified vertices and their indices with the specified shader.
// //
// Vertex contains color values, which can be interpreted for any purpose by the shader.
//
// For the details about the shader, see https://ebiten.org/documents/shader.html. // For the details about the shader, see https://ebiten.org/documents/shader.html.
// //
// If len(indices) is not multiple of 3, DrawTrianglesShader panics. // If len(indices) is not multiple of 3, DrawTrianglesShader panics.

View File

@ -420,16 +420,38 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f
i.processSrc(src) i.processSrc(src)
} }
// If a color matrix is used, but the matrix is merely a scaling matrix,
// and the scaling cannot cause out-of-range colors, do not use a color matrix
// when rendering but instead multiply all vertex colors by the scale.
// This speeds up rendering.
//
// NOTE: this is only safe when not using a custom Kage shader,
// as custom shaders may be using vertex colors for different purposes
// than colorization. However, currently there are no Ebiten APIs that
// support both shaders and color matrices.
cr := float32(1) cr := float32(1)
cg := float32(1) cg := float32(1)
cb := float32(1) cb := float32(1)
ca := float32(1) ca := float32(1)
if !colorm.IsIdentity() && colorm.ScaleOnly() { if !colorm.IsIdentity() && colorm.ScaleOnly() {
cr = colorm.At(0, 0) r := colorm.At(0, 0)
cg = colorm.At(1, 1) g := colorm.At(1, 1)
cb = colorm.At(2, 2) b := colorm.At(2, 2)
ca = colorm.At(3, 3) a := colorm.At(3, 3)
colorm = affine.ColorMIdentity{} if r >= 0 && g >= 0 && b >= 0 && a >= 0 && r <= 1 && g <= 1 && b <= 1 {
// Color matrices work on non-premultiplied colors.
// This color matrix can only make colors darker or equal,
// and thus can never invoke color clamping.
// Thus the simpler vertex color scale based shader can be used.
//
// Negative color values can become positive and out-of-range
// after applying to vertex colors below, which can make the min() in the shader kick in.
//
// Alpha values smaller than 0, combined with negative vertex colors,
// can also make the min() kick in, so that shall be ruled out too.
cr, cg, cb, ca = r, g, b, a
colorm = affine.ColorMIdentity{}
}
} }
var dx, dy float32 var dx, dy float32

View File

@ -83,7 +83,8 @@ vertex VertexOut VertexShader(
VertexOut out = { VertexOut out = {
.position = projectionMatrix * float4(in.position, 0, 1), .position = projectionMatrix * float4(in.position, 0, 1),
.tex = in.tex, .tex = in.tex,
.color = in.color, // Fragment shader wants premultiplied alpha.
.color = float4(in.color.rgb, 1) * in.color.a,
}; };
return out; return out;
@ -230,13 +231,12 @@ struct FragmentShaderImpl {
if (useColorM) { if (useColorM) {
c.rgb /= c.a + (1.0 - sign(c.a)); c.rgb /= c.a + (1.0 - sign(c.a));
c = (color_matrix_body * c) + color_matrix_translation; c = (color_matrix_body * c) + color_matrix_translation;
c *= v.color;
c.rgb *= c.a; c.rgb *= c.a;
c *= v.color;
c.rgb = min(c.rgb, c.a);
} else { } else {
float4 s = v.color; c *= v.color;
c *= float4(s.r, s.g, s.b, 1.0) * s.a;
} }
c = min(c, c.a);
return c; return c;
} }
}; };

View File

@ -117,7 +117,9 @@ varying vec4 varying_color_scale;
void main(void) { void main(void) {
varying_tex = A1; varying_tex = A1;
varying_color_scale = A2;
// Fragment shader wants premultiplied alpha.
varying_color_scale = vec4(A2.rgb, 1) * A2.a;
mat4 projection_matrix = mat4( mat4 projection_matrix = mat4(
vec4(2.0 / viewport_size.x, 0, 0, 0), vec4(2.0 / viewport_size.x, 0, 0, 0),
@ -267,16 +269,18 @@ void main(void) {
color.rgb /= color.a + (1.0 - sign(color.a)); color.rgb /= color.a + (1.0 - sign(color.a));
// Apply the color matrix or scale. // Apply the color matrix or scale.
color = (color_matrix_body * color) + color_matrix_translation; color = (color_matrix_body * color) + color_matrix_translation;
color *= varying_color_scale;
// Premultiply alpha // Premultiply alpha
color.rgb *= color.a; color.rgb *= color.a;
// Apply color scale.
color *= varying_color_scale;
// Clamp the output.
color.rgb = min(color.rgb, color.a);
# else # else
vec4 s = varying_color_scale; // Apply color scale.
color *= vec4(s.r, s.g, s.b, 1.0) * s.a; color *= varying_color_scale;
// No clamping needed as the color matrix shader is used then.
# endif // defined(USE_COLOR_MATRIX) # endif // defined(USE_COLOR_MATRIX)
color = min(color, color.a);
gl_FragColor = color; gl_FragColor = color;
#endif // defined(FILTER_SCREEN) #endif // defined(FILTER_SCREEN)