hajimehoshi / ebiten

Ebitengine - A dead simple 2D game engine for Go
https://ebitengine.org
Apache License 2.0
11.01k stars 660 forks source link

ebiten.Image.DrawImage: DestinationOut blend mode is behaving the same as SourceOut. #2560

Closed sedyh closed 1 year ago

sedyh commented 1 year ago

Ebitengine Version

v2.5.0-alpha.12

Operating System

Go Version (go version)

1.20

What steps will reproduce the problem?

When rendering images via ebiten.Blend, DestinationOut behaves the same as SourceOut.

Images for test

Source: source Destination: dest

Code example

package main

import (
    "bytes"
    _ "embed"
    "fmt"
    "image"
    _ "image/png"
    "log"
    "math"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/text"
    "github.com/hajimehoshi/ebiten/v2/vector"
    "golang.org/x/image/colornames"
    "golang.org/x/image/font/inconsolata"
)

var (
    //go:embed source.png
    Source []byte
    //go:embed dest.png
    Dest []byte
)

func main() {
    ebiten.SetWindowSize(980, 600)
    ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
    if err := ebiten.RunGame(NewGame()); err != nil {
        log.Fatal(err)
    }
}

type Mode struct {
    Blend ebiten.Blend
    Name  string
}

type Game struct {
    Source       *ebiten.Image
    Dest         *ebiten.Image
    Offscreen    *ebiten.Image
    TileSize     float64
    GridW, GridH float64
    Padding      float64
    TextSpacing  float64
    Modes        []Mode
}

func NewGame() *Game {
    g := &Game{
        Source: LoadImage(Source),
        Dest:   LoadImage(Dest),
        Modes: []Mode{
            {Blend: ebiten.BlendCopy, Name: "BlendCopy"},
            {Blend: ebiten.BlendSourceAtop, Name: "BlendSourceAtop"},
            {Blend: ebiten.BlendSourceOver, Name: "BlendSourceOver"},
            {Blend: ebiten.BlendSourceIn, Name: "BlendSourceIn"},
            {Blend: ebiten.BlendSourceOut, Name: "BlendSourceOut"},
            {Blend: ebiten.BlendDestination, Name: "BlendDestination"},
            {Blend: ebiten.BlendDestinationAtop, Name: "BlendDestinationAtop"},
            {Blend: ebiten.BlendDestinationOver, Name: "BlendDestinationOver"},
            {Blend: ebiten.BlendDestinationIn, Name: "BlendDestinationIn"},
            {Blend: ebiten.BlendDestinationOut, Name: "BlendDestinationOut"},
            {Blend: ebiten.BlendClear, Name: "BlendClear"},
            {Blend: ebiten.BlendXor, Name: "BlendXor"},
            {Blend: ebiten.BlendLighter, Name: "BlendLighter"},
        },
        GridW: 5, GridH: 3,
        Padding: 64, TextSpacing: 16,
    }
    g.TileSize = MaxSide(g.Source, g.Dest)
    g.Offscreen = ebiten.NewImage(int(g.TileSize), int(g.TileSize))
    return g
}

func (g *Game) Update() error {
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    screen.Fill(colornames.White)
    sw, sh := float64(screen.Bounds().Dx()), float64(screen.Bounds().Dy())
    ebiten.SetWindowTitle(fmt.Sprintf("%.0fx%.0f", sw, sh))
    gw, gh := g.GridW*(g.TileSize+g.Padding)-g.Padding, g.GridH*(g.TileSize+g.Padding)
    cx, cy := (sw-gw)/2, (sh-gh)/2
    for y := 0.; y < g.GridH; y++ {
        for x := 0.; x < g.GridW; x++ {
            px, py := x*(g.TileSize+g.Padding), y*(g.TileSize+g.Padding)
            if y > 0 {
                py += g.TextSpacing * y
            }
            mode := Get(g.Modes, int(x), int(y), int(g.GridW), int(g.GridH))
            g.DrawBlendMode(screen, px+cx, py+cy, mode.Blend)
            DrawCenteredText(screen, px+cx+g.TileSize/2, py+cy+g.TileSize+g.TextSpacing, mode.Name)
        }
    }
}

func (g *Game) Layout(w, h int) (int, int) {
    return w, h
}

func (g *Game) DrawBlendMode(screen *ebiten.Image, x, y float64, mode ebiten.Blend) {
    g.Offscreen.Clear()
    g.Offscreen.DrawImage(g.Dest, nil)

    op := &ebiten.DrawImageOptions{}
    op.Blend = mode
    g.Offscreen.DrawImage(g.Source, op)

    op = &ebiten.DrawImageOptions{}
    op.GeoM.Translate(x, y)
    screen.DrawImage(g.Offscreen, op)

    vector.StrokeRect(
        screen,
        float32(x), float32(y),
        float32(g.TileSize), float32(g.TileSize),
        2, colornames.Blue,
    )
}

func LoadImage(data []byte) *ebiten.Image {
    m, _, err := image.Decode(bytes.NewReader(data))
    if err != nil {
        log.Fatal(err)
    }
    return ebiten.NewImageFromImage(m)
}

func MaxSide(a, b *ebiten.Image) float64 {
    xMax := math.Max(float64(a.Bounds().Dx()), float64(a.Bounds().Dx()))
    yMax := math.Max(float64(b.Bounds().Dx()), float64(b.Bounds().Dx()))
    return math.Max(xMax, yMax)
}

func Index(x, y, w int) int {
    return y*w + x
}

func InRange(x, y, w, h int) bool {
    xOut := x < 0 || x >= w
    yOut := y < 0 || y >= h
    if xOut || yOut {
        return false
    }
    return true
}

func Get(modes []Mode, x, y, w, h int) Mode {
    index := Index(x, y, w)

    if !InRange(x, y, w, h) || index > len(modes)-1 {
        return Mode{Blend: ebiten.BlendClear, Name: "Empty"}
    }

    return modes[index]
}

func DrawCenteredText(screen *ebiten.Image, cx, cy float64, s string, args ...any) {
    interpolated := fmt.Sprintf(s, args...)
    bounds := text.BoundString(inconsolata.Bold8x16, interpolated)
    x, y := int(cx)-bounds.Min.X-bounds.Dx()/2, int(cy)-bounds.Min.Y-bounds.Dy()/2
    text.Draw(screen, interpolated, inconsolata.Bold8x16, x, y, colornames.Black)
}

What is the expected result?

image

What happens instead?

image

Anything else you feel useful to add?

I've not tested it with ebitengine 2.4 but add the result for it later.

hajimehoshi commented 1 year ago

Thanks! I'll take a look later.

sedyh commented 1 year ago

Fixed it, adding PR...

hajimehoshi commented 1 year ago

Oh I could fixed this now, but you can send a PR