ebiten: add DrawTrianglesOptions.AntiAlias and DrawTrianglesShaderOptions.AntiAlias

Closes #2385
This commit is contained in:
Hajime Hoshi 2022-10-20 00:46:44 +09:00
parent 8b16badb83
commit 9ec23ddeb4
6 changed files with 246 additions and 91 deletions

View File

@ -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,
})
}
}

View File

@ -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
}
@ -298,7 +298,6 @@ type Game struct {
aa bool
line bool
offscreen *ebiten.Image
}
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."

View File

@ -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.

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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"
@ -33,9 +35,14 @@ type Image struct {
mipmap *mipmap.Mipmap
width int
height int
volatile bool
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 {
@ -43,7 +50,7 @@ func NewImage(width, height int, imageType atlas.ImageType) *Image {
mipmap: mipmap.New(width, height, imageType),
width: width,
height: height,
volatile: imageType == atlas.ImageTypeVolatile,
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)
}