diff --git a/blend.go b/blend.go index 5942bd2bd..f76a90b9e 100644 --- a/blend.go +++ b/blend.go @@ -200,6 +200,20 @@ const ( // c_out = (BlendFactorDestinationRGB) × c_dst - (BlendFactorSourceRGB) × c_src // α_out = (BlendFactorDestinationAlpha) × α_dst - (BlendFactorSourceAlpha) × α_src BlendOperationReverseSubtract + + // BlendOperationMin represents a minimum function for the source and destination color. + // If BlendOperationMin is specified, blend factors are not used. + // + // c_out = min(c_dst, c_src) + // α_out = min(α_dst, α_src) + BlendOperationMin + + // BlendOperationMax represents a maximum function for the source and destination color. + // If BlendOperationMax is specified, blend factors are not used. + // + // c_out = max(c_dst, c_src) + // α_out = max(α_dst, α_src) + BlendOperationMax ) func (b BlendOperation) internalBlendOperation() graphicsdriver.BlendOperation { @@ -210,6 +224,10 @@ func (b BlendOperation) internalBlendOperation() graphicsdriver.BlendOperation { return graphicsdriver.BlendOperationSubtract case BlendOperationReverseSubtract: return graphicsdriver.BlendOperationReverseSubtract + case BlendOperationMin: + return graphicsdriver.BlendOperationMin + case BlendOperationMax: + return graphicsdriver.BlendOperationMax default: panic(fmt.Sprintf("ebiten: invalid blend operation: %d", b)) } diff --git a/image_test.go b/image_test.go index 52ab46539..d62b78d9e 100644 --- a/image_test.go +++ b/image_test.go @@ -335,13 +335,26 @@ func TestImageDispose(t *testing.T) { } } -func min(a, b int) int { +type ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string +} + +// TODO: Use the built-in function min from Go 1.21. +func min[T ordered](a, b T) T { if a < b { return a } return b } +// TODO: Use the built-in function max from Go 1.21. +func max[T ordered](a, b T) T { + if a < b { + return b + } + return a +} + func TestImageBlendLighter(t *testing.T) { img0, _, err := openEbitenImage() if err != nil { @@ -3687,6 +3700,87 @@ func TestImageBlendOperation(t *testing.T) { } } +func TestImageBlendOperationMinAndMax(t *testing.T) { + const w, h = 16, 1 + dst := ebiten.NewImage(w, h) + src := ebiten.NewImage(w, h) + + dstColor := func(i int) (byte, byte, byte, byte) { + return byte(4 * i * 17), byte(4*i*17 + 1), byte(4*i*17 + 2), byte(4*i*17 + 3) + } + srcColor := func(i int) (byte, byte, byte, byte) { + return byte(4 * i * 13), byte(4*i*13 + 1), byte(4*i*13 + 2), byte(4*i*13 + 3) + } + + dstPix := make([]byte, 4*w*h) + for i := 0; i < w; i++ { + r, g, b, a := dstColor(i) + dstPix[4*i] = r + dstPix[4*i+1] = g + dstPix[4*i+2] = b + dstPix[4*i+3] = a + } + srcPix := make([]byte, 4*w*h) + for i := 0; i < w; i++ { + r, g, b, a := srcColor(i) + srcPix[4*i] = r + srcPix[4*i+1] = g + srcPix[4*i+2] = b + srcPix[4*i+3] = a + } + src.WritePixels(srcPix) + + operations := []ebiten.BlendOperation{ + ebiten.BlendOperationMin, + ebiten.BlendOperationMax, + } + for _, rgbOp := range operations { + for _, alphaOp := range operations { + // Reset the destination state. + dst.WritePixels(dstPix) + op := &ebiten.DrawImageOptions{} + // Use the default blend factors, and confirm that the factors are ignored. + op.Blend = ebiten.Blend{ + BlendFactorSourceRGB: ebiten.BlendFactorDefault, + BlendFactorSourceAlpha: ebiten.BlendFactorDefault, + BlendFactorDestinationRGB: ebiten.BlendFactorDefault, + BlendFactorDestinationAlpha: ebiten.BlendFactorDefault, + BlendOperationRGB: rgbOp, + BlendOperationAlpha: alphaOp, + } + dst.DrawImage(src, op) + for i := 0; i < w; i++ { + got := dst.At(i, 0).(color.RGBA) + + sr, sg, sb, sa := srcColor(i) + dr, dg, db, da := dstColor(i) + + var want color.RGBA + switch rgbOp { + case ebiten.BlendOperationMin: + want.R = min(sr, dr) + want.G = min(sg, dg) + want.B = min(sb, db) + case ebiten.BlendOperationMax: + want.R = max(sr, dr) + want.G = max(sg, dg) + want.B = max(sb, db) + } + switch alphaOp { + case ebiten.BlendOperationMin: + want.A = min(sa, da) + case ebiten.BlendOperationMax: + want.A = max(sa, da) + } + + if !sameColors(got, want, 1) { + t.Errorf("dst.At(%d, 0): operations: %d, %d: got: %v, want: %v", i, rgbOp, alphaOp, got, want) + } + } + } + } +} + func TestImageBlendFactor(t *testing.T) { if skipTooSlowTests(t) { return diff --git a/internal/graphicsdriver/blend.go b/internal/graphicsdriver/blend.go index 39516e11b..a5681ce31 100644 --- a/internal/graphicsdriver/blend.go +++ b/internal/graphicsdriver/blend.go @@ -45,6 +45,8 @@ const ( BlendOperationAdd BlendOperation = iota BlendOperationSubtract BlendOperationReverseSubtract + BlendOperationMin + BlendOperationMax ) var BlendSourceOver = Blend{ diff --git a/internal/graphicsdriver/directx/graphics11_windows.go b/internal/graphicsdriver/directx/graphics11_windows.go index 8f301f7f8..e2002da30 100644 --- a/internal/graphicsdriver/directx/graphics11_windows.go +++ b/internal/graphicsdriver/directx/graphics11_windows.go @@ -106,6 +106,10 @@ func blendOperationToBlendOp11(o graphicsdriver.BlendOperation) _D3D11_BLEND_OP return _D3D11_BLEND_OP_SUBTRACT case graphicsdriver.BlendOperationReverseSubtract: return _D3D11_BLEND_OP_REV_SUBTRACT + case graphicsdriver.BlendOperationMin: + return _D3D11_BLEND_OP_MIN + case graphicsdriver.BlendOperationMax: + return _D3D11_BLEND_OP_MAX default: panic(fmt.Sprintf("directx: invalid blend operation: %d", o)) } diff --git a/internal/graphicsdriver/directx/pipeline12_windows.go b/internal/graphicsdriver/directx/pipeline12_windows.go index b180d824a..35d2fbd54 100644 --- a/internal/graphicsdriver/directx/pipeline12_windows.go +++ b/internal/graphicsdriver/directx/pipeline12_windows.go @@ -107,6 +107,10 @@ func blendOperationToBlendOp12(o graphicsdriver.BlendOperation) _D3D12_BLEND_OP return _D3D12_BLEND_OP_SUBTRACT case graphicsdriver.BlendOperationReverseSubtract: return _D3D12_BLEND_OP_REV_SUBTRACT + case graphicsdriver.BlendOperationMin: + return _D3D12_BLEND_OP_MIN + case graphicsdriver.BlendOperationMax: + return _D3D12_BLEND_OP_MAX default: panic(fmt.Sprintf("directx: invalid blend operation: %d", o)) } diff --git a/internal/graphicsdriver/metal/graphics_darwin.go b/internal/graphicsdriver/metal/graphics_darwin.go index cb65f0a35..fd8c034d4 100644 --- a/internal/graphicsdriver/metal/graphics_darwin.go +++ b/internal/graphicsdriver/metal/graphics_darwin.go @@ -381,6 +381,10 @@ func blendOperationToMetalBlendOperation(o graphicsdriver.BlendOperation) mtl.Bl return mtl.BlendOperationSubtract case graphicsdriver.BlendOperationReverseSubtract: return mtl.BlendOperationReverseSubtract + case graphicsdriver.BlendOperationMin: + return mtl.BlendOperationMin + case graphicsdriver.BlendOperationMax: + return mtl.BlendOperationMax default: panic(fmt.Sprintf("metal: invalid blend operation: %d", o)) } diff --git a/internal/graphicsdriver/opengl/context.go b/internal/graphicsdriver/opengl/context.go index 16d0f5660..947b6a781 100644 --- a/internal/graphicsdriver/opengl/context.go +++ b/internal/graphicsdriver/opengl/context.go @@ -67,6 +67,10 @@ func convertBlendOperation(o graphicsdriver.BlendOperation) blendOperation { return gl.FUNC_SUBTRACT case graphicsdriver.BlendOperationReverseSubtract: return gl.FUNC_REVERSE_SUBTRACT + case graphicsdriver.BlendOperationMin: + return gl.MIN + case graphicsdriver.BlendOperationMax: + return gl.MAX default: panic(fmt.Sprintf("opengl: invalid blend operation %d", o)) } diff --git a/internal/graphicsdriver/opengl/gl/const.go b/internal/graphicsdriver/opengl/gl/const.go index 342c282ca..d455309ff 100644 --- a/internal/graphicsdriver/opengl/gl/const.go +++ b/internal/graphicsdriver/opengl/gl/const.go @@ -40,7 +40,9 @@ const ( INVERT = 0x150A KEEP = 0x1E00 LINK_STATUS = 0x8B82 + MAX = 0x8008 MAX_TEXTURE_SIZE = 0x0D33 + MIN = 0x8007 NEAREST = 0x2600 NO_ERROR = 0 NOTEQUAL = 0x0205