diff --git a/image.go b/image.go index 7c4d63721..374a63e6e 100644 --- a/image.go +++ b/image.go @@ -587,6 +587,9 @@ var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]stru // // If a specified uniform variable's length or type doesn't match with an expected one, DrawTrianglesShader panics. // +// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values, +// the value is kept and is not clamped. +// // When the image i is disposed, DrawTrianglesShader does nothing. func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) { i.copyCheck() @@ -742,6 +745,9 @@ var _ [len(DrawRectShaderOptions{}.Images)]struct{} = [graphics.ShaderImageCount // If no source images are specified, imageSrc0Size returns a valid size only when the unit is pixels, // but always returns 0 when the unit is texels (default). // +// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values, +// the value is kept and is not clamped. +// // When the image i is disposed, DrawRectShader does nothing. func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions) { i.copyCheck() @@ -954,6 +960,9 @@ func (i *Image) at(x, y int) (r, g, b, a byte) { // // Set implements the standard draw.Image's Set. // +// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values, +// the value is kept and is not clamped. +// // If the image is disposed, Set does nothing. func (i *Image) Set(x, y int, clr color.Color) { i.copyCheck() @@ -1028,6 +1037,9 @@ func (i *Image) Deallocate() { // // WritePixels also works on a sub-image. // +// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values, +// the value is kept and is not clamped. +// // When the image is disposed, WritePixels does nothing. func (i *Image) WritePixels(pixels []byte) { i.copyCheck() diff --git a/image_test.go b/image_test.go index 91c3a0626..660418a48 100644 --- a/image_test.go +++ b/image_test.go @@ -4582,3 +4582,44 @@ func TestImageDrawImageAfterDeallocation(t *testing.T) { } } } + +// Issue #2798 +func TestImageInvalidPremultipliedAlphaColor(t *testing.T) { + // This test checks the rendering result when Set and WritePixels use an invalid premultiplied alpha color. + // The result values are kept and not clamped. + + const ( + w = 16 + h = 16 + ) + + dst := ebiten.NewImage(w, h) + dst.Set(0, 0, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}) + dst.Set(0, 1, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00}) + if got, want := dst.At(0, 0).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}); got != want { + t.Errorf("got: %v, want: %v", got, want) + } + if got, want := dst.At(0, 1).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00}); got != want { + t.Errorf("got: %v, want: %v", got, want) + } + + pix := make([]byte, 4*w*h) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + pix[4*(j*16+i)] = byte(i) + pix[4*(j*16+i)+1] = byte(j) + pix[4*(j*16+i)+2] = 0x80 + pix[4*(j*16+i)+3] = byte(i - j) + } + } + dst.WritePixels(pix) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j) + want := color.RGBA{R: byte(i), G: byte(j), B: 0x80, A: byte(i - j)} + if got != want { + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +} diff --git a/shader_test.go b/shader_test.go index 6af4ebd6e..d2ca7aafd 100644 --- a/shader_test.go +++ b/shader_test.go @@ -2429,3 +2429,61 @@ func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { } } } + +// Issue #2798 +func TestShaderInvalidPremultipliedAlphaColor(t *testing.T) { + // This test checks the rendering result when the shader returns an invalid premultiplied alpha color. + // The result values are kept and not clamped. + + const w, h = 16, 16 + + dst := ebiten.NewImage(w, h) + s, err := ebiten.NewShader([]byte(`//kage:unit pixels + +package main + +func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { + return vec4(1, 0.75, 0.5, 0.25) +} +`)) + if err != nil { + t.Fatal(err) + } + + dst.DrawRectShader(w, h, s, nil) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40} + if !sameColors(got, want, 2) { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + + dst.Clear() + s, err = ebiten.NewShader([]byte(`//kage:unit pixels + +package main + +func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { + return vec4(1, 0.75, 0.5, 0) +} +`)) + if err != nil { + t.Fatal(err) + } + + dst.DrawRectShader(w, h, s, nil) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0} + if !sameColors(got, want, 2) { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}