diff --git a/examples/lines/main.go b/examples/lines/main.go index f7f82a458..11553e732 100644 --- a/examples/lines/main.go +++ b/examples/lines/main.go @@ -57,8 +57,6 @@ type Game struct { vertices []ebiten.Vertex indices []uint16 - offscreen *ebiten.Image - aa bool showCenter bool } @@ -76,24 +74,6 @@ func (g *Game) Update() error { func (g *Game) Draw(screen *ebiten.Image) { target := screen - if g.aa { - // Prepare the double-sized offscreen. - // This is for anti-aliasing by a pseudo MSAA (multisample anti-aliasing). - if g.offscreen != nil { - sw, sh := screen.Size() - ow, oh := g.offscreen.Size() - if ow != sw*2 || oh != sh*2 { - g.offscreen.Dispose() - g.offscreen = nil - } - } - if g.offscreen == nil { - sw, sh := screen.Size() - g.offscreen = ebiten.NewImage(sw*2, sh*2) - } - g.offscreen.Clear() - target = g.offscreen - } joins := []vector.LineJoin{ vector.LineJoinMiter, @@ -123,14 +103,6 @@ func (g *Game) Draw(screen *ebiten.Image) { } } - if g.aa { - // Render the offscreen to the screen. - op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(0.5, 0.5) - op.Filter = ebiten.FilterLinear - screen.DrawImage(g.offscreen, op) - } - msg := fmt.Sprintf(`FPS: %0.2f, TPS: %0.2f Press A to switch anti-aliasing. Press C to switch to draw the center lines.`, ebiten.ActualFPS(), ebiten.ActualTPS()) @@ -165,7 +137,9 @@ func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, cap vector vs[i].SrcX = 1 vs[i].SrcY = 1 } - screen.DrawTriangles(vs, is, emptySubImage, nil) + screen.DrawTriangles(vs, is, emptySubImage, &ebiten.DrawTrianglesOptions{ + AntiAlias: g.aa, + }) // Draw the center line in red. if g.showCenter { @@ -180,7 +154,9 @@ func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, cap vector vs[i].ColorG = 0 vs[i].ColorB = 0 } - screen.DrawTriangles(vs, is, emptySubImage, nil) + screen.DrawTriangles(vs, is, emptySubImage, &ebiten.DrawTrianglesOptions{ + AntiAlias: g.aa, + }) } } diff --git a/examples/vector/main.go b/examples/vector/main.go index bdf857de1..57e1d1eac 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -47,7 +47,7 @@ const ( screenHeight = 480 ) -func drawEbitenText(screen *ebiten.Image, x, y int, scale float32, line bool) { +func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { var path vector.Path // E @@ -125,8 +125,8 @@ func drawEbitenText(screen *ebiten.Image, x, y int, scale float32, line bool) { } for i := range vs { - vs[i].DstX = (vs[i].DstX + float32(x)) * scale - vs[i].DstY = (vs[i].DstY + float32(y)) * scale + vs[i].DstX = (vs[i].DstX + float32(x)) + vs[i].DstY = (vs[i].DstY + float32(y)) vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = 0xdb / float32(0xff) @@ -135,13 +135,14 @@ func drawEbitenText(screen *ebiten.Image, x, y int, scale float32, line bool) { } op := &ebiten.DrawTrianglesOptions{} + op.AntiAlias = aa if !line { op.FillRule = ebiten.EvenOdd } screen.DrawTriangles(vs, is, emptySubImage, op) } -func drawEbitenLogo(screen *ebiten.Image, x, y int, scale float32, line bool) { +func drawEbitenLogo(screen *ebiten.Image, x, y int, aa bool, line bool) { const unit = 16 var path vector.Path @@ -178,8 +179,8 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, scale float32, line bool) { } for i := range vs { - vs[i].DstX = (vs[i].DstX + float32(x)) * scale - vs[i].DstY = (vs[i].DstY + float32(y)) * scale + vs[i].DstX = (vs[i].DstX + float32(x)) + vs[i].DstY = (vs[i].DstY + float32(y)) vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = 0xdb / float32(0xff) @@ -188,13 +189,14 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, scale float32, line bool) { } op := &ebiten.DrawTrianglesOptions{} + op.AntiAlias = aa if !line { op.FillRule = ebiten.EvenOdd } screen.DrawTriangles(vs, is, emptySubImage, op) } -func drawArc(screen *ebiten.Image, count int, scale float32, line bool) { +func drawArc(screen *ebiten.Image, count int, aa bool, line bool) { var path vector.Path path.MoveTo(350, 100) @@ -220,8 +222,6 @@ func drawArc(screen *ebiten.Image, count int, scale float32, line bool) { } for i := range vs { - vs[i].DstX *= scale - vs[i].DstY *= scale vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = 0x33 / float32(0xff) @@ -230,6 +230,7 @@ func drawArc(screen *ebiten.Image, count int, scale float32, line bool) { } op := &ebiten.DrawTrianglesOptions{} + op.AntiAlias = aa if !line { op.FillRule = ebiten.EvenOdd } @@ -240,7 +241,7 @@ func maxCounter(index int) int { return 128 + (17*index+32)%64 } -func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) { +func drawWave(screen *ebiten.Image, counter int, aa bool, line bool) { var path vector.Path const npoints = 8 @@ -277,8 +278,6 @@ func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) { } for i := range vs { - vs[i].DstX *= scale - vs[i].DstY *= scale vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = 0x33 / float32(0xff) @@ -287,6 +286,7 @@ func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) { } op := &ebiten.DrawTrianglesOptions{} + op.AntiAlias = aa if !line { op.FillRule = ebiten.EvenOdd } @@ -296,9 +296,8 @@ func drawWave(screen *ebiten.Image, counter int, scale float32, line bool) { type Game struct { counter int - aa bool - line bool - offscreen *ebiten.Image + aa bool + line bool } func (g *Game) Update() error { @@ -318,37 +317,13 @@ func (g *Game) Update() error { } func (g *Game) Draw(screen *ebiten.Image) { - if g.offscreen != nil { - w, h := screen.Size() - if ow, oh := g.offscreen.Size(); ow != w*2 || oh != h*2 { - g.offscreen.Dispose() - g.offscreen = nil - } - } - if g.aa && g.offscreen == nil { - w, h := screen.Size() - g.offscreen = ebiten.NewImage(w*2, h*2) - } - - scale := float32(1) dst := screen - if g.aa { - scale = 2 - dst = g.offscreen - } dst.Fill(color.RGBA{0xe0, 0xe0, 0xe0, 0xff}) - drawEbitenText(dst, 0, 50, scale, g.line) - drawEbitenLogo(dst, 20, 150, scale, g.line) - drawArc(dst, g.counter, scale, g.line) - drawWave(dst, g.counter, scale, g.line) - - if g.aa { - op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(0.5, 0.5) - op.Filter = ebiten.FilterLinear - screen.DrawImage(g.offscreen, op) - } + drawEbitenText(dst, 0, 50, g.aa, g.line) + drawEbitenLogo(dst, 20, 150, g.aa, g.line) + drawArc(dst, g.counter, g.aa, g.line) + drawWave(dst, g.counter, g.aa, g.line) msg := fmt.Sprintf("TPS: %0.2f\nFPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()) msg += "\nPress A to switch anti-alias." diff --git a/image.go b/image.go index 6f32eaf61..a234b636e 100644 --- a/image.go +++ b/image.go @@ -257,7 +257,7 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) { }) } - i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedRegion(), graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, shader.shader, uniforms, false, canSkipMipmap(options.GeoM, filter)) + i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedRegion(), graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, shader.shader, uniforms, false, canSkipMipmap(options.GeoM, filter), false) } // Vertex represents a vertex passed to DrawTriangles. @@ -365,6 +365,15 @@ type DrawTrianglesOptions struct { // // The default (zero) value is FillAll. FillRule FillRule + + // AntiAlias indicates whether the rendering uses anti-alias or not. + // AntiAlias is useful especially when you pass vertices you get from the vector package. + // + // AntiAlias increases internal draw calls and might affect performance. + // Use `ebitenginedebug` to check the number of draw calls if you care. + // + // The default (zero) value is false. + AntiAlias bool } // MaxIndicesCount is the maximum number of indices for DrawTriangles and DrawTrianglesShader. @@ -479,7 +488,7 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o }) } - i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedRegion(), sr, [graphics.ShaderImageCount - 1][2]float32{}, shader.shader, uniforms, options.FillRule == EvenOdd, filter != builtinshader.FilterLinear) + i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedRegion(), sr, [graphics.ShaderImageCount - 1][2]float32{}, shader.shader, uniforms, options.FillRule == EvenOdd, filter != builtinshader.FilterLinear, options.AntiAlias) } // DrawTrianglesShaderOptions represents options for DrawTrianglesShader. @@ -515,6 +524,15 @@ type DrawTrianglesShaderOptions struct { // // The default (zero) value is FillAll. FillRule FillRule + + // AntiAlias indicates whether the rendering uses anti-alias or not. + // AntiAlias is useful especially when you pass vertices you get from the vector package. + // + // AntiAlias increases internal draw calls and might affect performance. + // Use `ebitenginedebug` to check the number of draw calls if you care. + // + // The default (zero) value is false. + AntiAlias bool } func init() { @@ -625,7 +643,7 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader offsets[i][1] = float32(y - sy) } - i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), options.FillRule == EvenOdd, true) + i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), options.FillRule == EvenOdd, true, options.AntiAlias) } // DrawRectShaderOptions represents options for DrawRectShader. @@ -738,7 +756,7 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR offsets[i][1] = float32(y - sy) } - i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), false, true) + i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), false, true, false) } // SubImage returns an image representing the portion of the image p visible through r. diff --git a/image_test.go b/image_test.go index 11eb8a66a..001f9a5c8 100644 --- a/image_test.go +++ b/image_test.go @@ -3864,3 +3864,80 @@ func TestImageBlendFactor(t *testing.T) { } } } + +func TestImageAntiAliasAndBlend(t *testing.T) { + const w, h = 16, 16 + + dst0 := ebiten.NewImage(w, h) + dst1 := ebiten.NewImage(w, h) + src := ebiten.NewImage(w, h) + + for _, blend := range []ebiten.Blend{ + {}, // Default + ebiten.BlendClear, + ebiten.BlendCopy, + ebiten.BlendSourceOver, + } { + dst0.Fill(color.RGBA{0x24, 0x3f, 0x6a, 0x88}) + dst1.Fill(color.RGBA{0x24, 0x3f, 0x6a, 0x88}) + src.Fill(color.RGBA{0x85, 0xa3, 0x08, 0xd3}) + + op0 := &ebiten.DrawTrianglesOptions{} + op0.Blend = blend + op0.AntiAlias = true + vs := []ebiten.Vertex{ + { + DstX: 0, + DstY: 0, + SrcX: 0, + SrcY: 0, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: w, + DstY: 0, + SrcX: w, + SrcY: 0, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: 0, + DstY: h, + SrcX: 0, + SrcY: h, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: w, + DstY: h, + SrcX: w, + SrcY: h, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + } + is := []uint16{0, 1, 2, 1, 2, 3} + dst0.DrawTriangles(vs, is, src, op0) + got := dst0.At(0, 0).(color.RGBA) + + op1 := &ebiten.DrawImageOptions{} + op1.Blend = blend + dst1.DrawImage(src, op1) + want := dst1.At(0, 0).(color.RGBA) + + if got != want { + t.Errorf("blend: %v, got: %v, want: %v", blend, got, want) + } + } +} diff --git a/internal/ui/context.go b/internal/ui/context.go index 721df698b..85e77fc13 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -19,6 +19,7 @@ import ( "sync" "sync/atomic" + "github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/buffered" "github.com/hajimehoshi/ebiten/v2/internal/clock" "github.com/hajimehoshi/ebiten/v2/internal/debug" @@ -153,7 +154,7 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update } func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) error { - if c.offscreen.volatile != theGlobalState.isScreenClearedEveryFrame() { + if (c.offscreen.imageType == atlas.ImageTypeVolatile) != theGlobalState.isScreenClearedEveryFrame() { w, h := c.offscreen.width, c.offscreen.height c.offscreen.MarkDisposed() c.offscreen = c.game.NewOffscreenImage(w, h) diff --git a/internal/ui/image.go b/internal/ui/image.go index 812b88050..92157a80c 100644 --- a/internal/ui/image.go +++ b/internal/ui/image.go @@ -15,6 +15,8 @@ package ui import ( + "fmt" + "github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" @@ -30,20 +32,25 @@ func SetPanicOnErrorOnReadingPixelsForTesting(value bool) { } type Image struct { - mipmap *mipmap.Mipmap - width int - height int - volatile bool + mipmap *mipmap.Mipmap + width int + height int + imageType atlas.ImageType dotsBuffer map[[2]int][4]byte + + // bigOffscreenBuffer is a double-sized offscreen for anti-alias rendering. + bigOffscreenBuffer *Image + bigOffscreenBufferBlend graphicsdriver.Blend + bigOffscreenBufferDirty bool } func NewImage(width, height int, imageType atlas.ImageType) *Image { return &Image{ - mipmap: mipmap.New(width, height, imageType), - width: width, - height: height, - volatile: imageType == atlas.ImageTypeVolatile, + mipmap: mipmap.New(width, height, imageType), + width: width, + height: height, + imageType: imageType, } } @@ -51,12 +58,72 @@ func (i *Image) MarkDisposed() { if i.mipmap == nil { return } + if i.bigOffscreenBuffer != nil { + i.bigOffscreenBuffer.MarkDisposed() + i.bigOffscreenBuffer = nil + i.bigOffscreenBufferDirty = false + } i.mipmap.MarkDisposed() i.mipmap = nil i.dotsBuffer = nil } -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint16, blend graphicsdriver.Blend, dstRegion, srcRegion graphicsdriver.Region, subimageOffsets [graphics.ShaderImageCount - 1][2]float32, shader *Shader, uniforms [][]float32, evenOdd bool, canSkipMipmap bool) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint16, blend graphicsdriver.Blend, dstRegion, srcRegion graphicsdriver.Region, subimageOffsets [graphics.ShaderImageCount - 1][2]float32, shader *Shader, uniforms [][]float32, evenOdd bool, canSkipMipmap bool, antialias bool) { + if antialias { + // Flush the other buffer to make the buffers exclusive. + i.flushDotsBufferIfNeeded() + + if i.bigOffscreenBufferBlend != blend { + i.flushBigOffscreenBufferIfNeeded() + } + + if i.bigOffscreenBuffer == nil { + var imageType atlas.ImageType + switch i.imageType { + case atlas.ImageTypeRegular, atlas.ImageTypeUnmanaged: + imageType = atlas.ImageTypeUnmanaged + case atlas.ImageTypeScreen, atlas.ImageTypeVolatile: + imageType = atlas.ImageTypeVolatile + default: + panic(fmt.Sprintf("ui: unexpected image type: %d", imageType)) + } + i.bigOffscreenBuffer = NewImage(i.width*2, i.height*2, imageType) + } + + i.bigOffscreenBufferBlend = blend + + // Copy the current rendering result to get the correct blending result. + if blend != graphicsdriver.BlendSourceOver && !i.bigOffscreenBufferDirty { + srcs := [graphics.ShaderImageCount]*Image{i} + vs := graphics.QuadVertices( + 0, 0, float32(i.width), float32(i.height), + 2, 0, 0, 2, 0, 0, + 1, 1, 1, 1) + is := graphics.QuadIndices() + dstRegion := graphicsdriver.Region{ + X: 0, + Y: 0, + Width: float32(i.width * 2), + Height: float32(i.height * 2), + } + i.bigOffscreenBuffer.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, NearestFilterShader, nil, false, true, false) + } + + for i := 0; i < len(vertices); i += graphics.VertexFloatCount { + vertices[i] *= 2 + vertices[i+1] *= 2 + } + + dstRegion.X *= 2 + dstRegion.Y *= 2 + dstRegion.Width *= 2 + dstRegion.Height *= 2 + + i.bigOffscreenBuffer.DrawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegion, subimageOffsets, shader, uniforms, evenOdd, canSkipMipmap, false) + i.bigOffscreenBufferDirty = true + return + } + i.flushBufferIfNeeded() var srcMipmaps [graphics.ShaderImageCount]*mipmap.Mipmap @@ -73,6 +140,9 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices [ func (i *Image) WritePixels(pix []byte, x, y, width, height int) { if width == 1 && height == 1 { + // Flush the other buffer to make the buffers exclusive. + i.flushBigOffscreenBufferIfNeeded() + if i.dotsBuffer == nil { i.dotsBuffer = map[[2]int][4]byte{} } @@ -83,7 +153,7 @@ func (i *Image) WritePixels(pix []byte, x, y, width, height int) { // One square requires 6 indices (= 2 triangles). if len(i.dotsBuffer) >= graphics.IndicesCount/6 { - i.flushBufferIfNeeded() + i.flushDotsBufferIfNeeded() } return } @@ -98,15 +168,17 @@ func (i *Image) ReadPixels(pixels []byte, x, y, width, height int) { return } + i.flushBigOffscreenBufferIfNeeded() + if width == 1 && height == 1 { if c, ok := i.dotsBuffer[[2]int{x, y}]; ok { copy(pixels, c[:]) return } - // Do not call flushBufferIfNeeded here. This would slow (image/draw).Draw. + // Do not call flushDotsBufferIfNeeded here. This would slow (image/draw).Draw. // See ebiten.TestImageDrawOver. } else { - i.flushBufferIfNeeded() + i.flushDotsBufferIfNeeded() } if err := theUI.readPixels(i.mipmap, pixels, x, y, width, height); err != nil { @@ -122,6 +194,12 @@ func (i *Image) DumpScreenshot(name string, blackbg bool) (string, error) { } func (i *Image) flushBufferIfNeeded() { + // The buffers are exclusive and the order should not matter. + i.flushDotsBufferIfNeeded() + i.flushBigOffscreenBufferIfNeeded() +} + +func (i *Image) flushDotsBufferIfNeeded() { if len(i.dotsBuffer) == 0 { return } @@ -193,6 +271,36 @@ func (i *Image) flushBufferIfNeeded() { i.mipmap.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, NearestFilterShader.shader, nil, false, true) } +func (i *Image) flushBigOffscreenBufferIfNeeded() { + if !i.bigOffscreenBufferDirty { + return + } + + // Mark the offscreen clearn earlier to avoid recursive calls. + i.bigOffscreenBufferDirty = false + + srcs := [graphics.ShaderImageCount]*Image{i.bigOffscreenBuffer} + vs := graphics.QuadVertices( + 0, 0, float32(i.width*2), float32(i.height*2), + 0.5, 0, 0, 0.5, 0, 0, + 1, 1, 1, 1) + is := graphics.QuadIndices() + dstRegion := graphicsdriver.Region{ + X: 0, + Y: 0, + Width: float32(i.width), + Height: float32(i.height), + } + blend := graphicsdriver.BlendSourceOver + if i.bigOffscreenBufferBlend != graphicsdriver.BlendSourceOver { + blend = graphicsdriver.BlendCopy + } + i.DrawTriangles(srcs, vs, is, blend, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, LinearFilterShader, nil, false, true, false) + + i.bigOffscreenBuffer.clear() + i.bigOffscreenBufferDirty = false +} + func DumpImages(dir string) (string, error) { return theUI.dumpImages(dir) } @@ -230,5 +338,5 @@ func (i *Image) Fill(r, g, b, a float32, x, y, width, height int) { srcs := [graphics.ShaderImageCount]*Image{emptyImage} - i.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, NearestFilterShader, nil, false, true) + i.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, NearestFilterShader, nil, false, true, false) }