From b466a0cbd7133721b1d463d79ef276b1245b6980 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 2 Jul 2021 19:26:09 +0900 Subject: [PATCH] ebiten: Add EvenOdd to DrawTrianglesOptions and DrawShaderTrianglesOptions Updates #844 Closes #1684 --- examples/vector/main.go | 52 ++- image.go | 34 +- internal/atlas/image.go | 12 +- internal/atlas/image_test.go | 32 +- internal/buffered/image.go | 6 +- internal/driver/graphics.go | 2 +- internal/graphicscommand/command.go | 29 +- internal/graphicscommand/image.go | 4 +- internal/graphicscommand/image_test.go | 10 +- internal/graphicsdriver/metal/graphics.go | 228 +++++++++--- internal/graphicsdriver/metal/shader.go | 33 +- internal/graphicsdriver/opengl/context.go | 9 + .../graphicsdriver/opengl/context_desktop.go | 78 ++++- internal/graphicsdriver/opengl/context_js.go | 85 ++++- .../graphicsdriver/opengl/context_mobile.go | 74 +++- internal/graphicsdriver/opengl/graphics.go | 15 +- internal/graphicsdriver/opengl/image.go | 25 ++ internal/mipmap/mipmap.go | 6 +- internal/restorable/image.go | 16 +- internal/restorable/images_test.go | 52 +-- internal/restorable/shader_test.go | 12 +- vector/internal/triangulate/math.go | 26 -- vector/internal/triangulate/triangulate.go | 154 -------- .../internal/triangulate/triangulate_test.go | 328 ------------------ vector/path.go | 101 +++--- 25 files changed, 678 insertions(+), 745 deletions(-) delete mode 100644 vector/internal/triangulate/math.go delete mode 100644 vector/internal/triangulate/triangulate.go delete mode 100644 vector/internal/triangulate/triangulate_test.go diff --git a/examples/vector/main.go b/examples/vector/main.go index d3c7e50f6..6a4805517 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -19,6 +19,7 @@ package main import ( "fmt" + "image" "image/color" "log" "math" @@ -28,6 +29,15 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +var ( + emptyImage = ebiten.NewImage(3, 3) + emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) +) + +func init() { + emptyImage.Fill(color.White) +} + const ( screenWidth = 640 screenHeight = 480 @@ -99,10 +109,18 @@ func drawEbitenText(screen *ebiten.Image) { path.LineTo(320, 55) path.LineTo(290, 20) - op := &vector.FillOptions{ - Color: color.RGBA{0xdb, 0x56, 0x20, 0xff}, + op := &ebiten.DrawTrianglesOptions{ + EvenOdd: true, } - path.Fill(screen, op) + vs, is := path.AppendVerticesAndIndices(nil, nil) + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = 0xdb / float32(0xff) + vs[i].ColorG = 0x56 / float32(0xff) + vs[i].ColorB = 0x20 / float32(0xff) + } + screen.DrawTriangles(vs, is, emptySubImage, op) } func drawEbitenLogo(screen *ebiten.Image, x, y int) { @@ -131,10 +149,18 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int) { path.LineTo(xf+unit, yf+3*unit) path.LineTo(xf+unit, yf+4*unit) - op := &vector.FillOptions{ - Color: color.RGBA{0xdb, 0x56, 0x20, 0xff}, + op := &ebiten.DrawTrianglesOptions{ + EvenOdd: true, } - path.Fill(screen, op) + vs, is := path.AppendVerticesAndIndices(nil, nil) + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = 0xdb / float32(0xff) + vs[i].ColorG = 0x56 / float32(0xff) + vs[i].ColorB = 0x20 / float32(0xff) + } + screen.DrawTriangles(vs, is, emptySubImage, op) } func maxCounter(index int) int { @@ -166,10 +192,18 @@ func drawWave(screen *ebiten.Image, counter int) { path.LineTo(screenWidth, screenHeight) path.LineTo(0, screenHeight) - op := &vector.FillOptions{ - Color: color.RGBA{0x33, 0x66, 0xff, 0xff}, + op := &ebiten.DrawTrianglesOptions{ + EvenOdd: true, } - path.Fill(screen, op) + vs, is := path.AppendVerticesAndIndices(nil, nil) + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = 0x33 / float32(0xff) + vs[i].ColorG = 0x66 / float32(0xff) + vs[i].ColorB = 0xff / float32(0xff) + } + screen.DrawTriangles(vs, is, emptySubImage, op) } type Game struct { diff --git a/image.go b/image.go index 625148315..382ba4470 100644 --- a/image.go +++ b/image.go @@ -213,7 +213,7 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) { is := graphics.QuadIndices() srcs := [graphics.ShaderImageNum]*mipmap.Mipmap{img.mipmap} - i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.AddressUnsafe, dstRegion, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, canSkipMipmap(options.GeoM, filter)) + i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.AddressUnsafe, dstRegion, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, canSkipMipmap(options.GeoM, filter)) } // Vertex represents a vertex passed to DrawTriangles. @@ -271,6 +271,19 @@ type DrawTrianglesOptions struct { // Address is a sampler address mode. // The default (zero) value is AddressUnsafe. Address Address + + // EvenOdd represents whether the even-odd rule is applied or not. + // + // If EvenOdd is true, triangles are rendered based on the even-odd rule. If false, triangles are rendered without condition. + // Whether overlapped regions by multiple triangles is rendered or not depends on the number of the overlapping: + // if and only if the number is odd, the region is rendered. + // + // EvenOdd is useful when you want to render a complex polygon. + // A complex polygon is a non-convex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon. + // See examples/vector for actual usages. + // + // The default value is false. + EvenOdd bool } // MaxIndicesNum is the maximum number of indices for DrawTriangles. @@ -349,7 +362,7 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o srcs := [graphics.ShaderImageNum]*mipmap.Mipmap{img.mipmap} - i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, address, dstRegion, sr, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) + i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, address, dstRegion, sr, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, options.EvenOdd, false) } // DrawTrianglesShaderOptions represents options for DrawTrianglesShader. @@ -371,6 +384,19 @@ type DrawTrianglesShaderOptions struct { // Images is a set of the source images. // All the image must be the same size. Images [4]*Image + + // EvenOdd represents whether the even-odd rule is applied or not. + // + // If EvenOdd is true, triangles are rendered based on the even-odd rule. If false, triangles are rendered without condition. + // Whether overlapped regions by multiple triangles is rendered or not depends on the number of the overlapping: + // if and only if the number is odd, the region is rendered. + // + // EvenOdd is useful when you want to render a complex polygon. + // A complex polygon is a non-convex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon. + // See examples/vector for actual usages. + // + // The default value is false. + EvenOdd bool } func init() { @@ -485,7 +511,7 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader } us := shader.convertUniforms(options.Uniforms) - i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, us, false) + i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, us, options.EvenOdd, false) } // DrawRectShaderOptions represents options for DrawRectShader. @@ -597,7 +623,7 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR } us := shader.convertUniforms(options.Uniforms) - i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, us, canSkipMipmap(options.GeoM, driver.FilterNearest)) + i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, us, false, canSkipMipmap(options.GeoM, driver.FilterNearest)) } // SubImage returns an image representing the portion of the image p visible through r. diff --git a/internal/atlas/image.go b/internal/atlas/image.go index 4fb11d3d0..9d4376a33 100644 --- a/internal/atlas/image.go +++ b/internal/atlas/image.go @@ -298,7 +298,7 @@ func (i *Image) ensureIsolated() { Width: float32(w - 2*paddingSize), Height: float32(h - 2*paddingSize), } - newImg.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dstRegion, driver.Region{}, nil, nil) + newImg.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dstRegion, driver.Region{}, nil, nil, false) i.dispose(false) i.backend = &backend{ @@ -354,7 +354,7 @@ func (i *Image) putOnAtlas() error { Width: w, Height: h, } - newI.drawTriangles([graphics.ShaderImageNum]*Image{i}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, true) + newI.drawTriangles([graphics.ShaderImageNum]*Image{i}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, true) } newI.moveTo(i) @@ -402,13 +402,13 @@ func (i *Image) processSrc(src *Image) { // 5: Color G // 6: Color B // 7: Color Y -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, evenOdd bool) { backendsM.Lock() defer backendsM.Unlock() - i.drawTriangles(srcs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, shader, uniforms, false) + i.drawTriangles(srcs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, shader, uniforms, evenOdd, false) } -func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, keepOnAtlas bool) { +func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, evenOdd bool, keepOnAtlas bool) { if i.disposed { panic("atlas: the drawing target image must not be disposed (DrawTriangles)") } @@ -487,7 +487,7 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f } } - i.backend.restorable.DrawTriangles(imgs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, s, uniforms) + i.backend.restorable.DrawTriangles(imgs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, s, uniforms, evenOdd) for _, src := range srcs { if src == nil { diff --git a/internal/atlas/image_test.go b/internal/atlas/image_test.go index d85e4dee3..b02b25721 100644 --- a/internal/atlas/image_test.go +++ b/internal/atlas/image_test.go @@ -102,7 +102,7 @@ func TestEnsureIsolated(t *testing.T) { Width: size, Height: size, } - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) want := false if got := img4.IsOnAtlasForTesting(); got != want { t.Errorf("got: %v, want: %v", got, want) @@ -132,7 +132,7 @@ func TestEnsureIsolated(t *testing.T) { // Check further drawing doesn't cause panic. // This bug was fixed by 03dcd948. - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) } func TestReputOnAtlas(t *testing.T) { @@ -179,7 +179,7 @@ func TestReputOnAtlas(t *testing.T) { Width: size, Height: size, } - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -191,7 +191,7 @@ func TestReputOnAtlas(t *testing.T) { if err := PutImagesOnAtlasForTesting(); err != nil { t.Fatal(err) } - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -219,7 +219,7 @@ func TestReputOnAtlas(t *testing.T) { } // img1 is on an atlas again. - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), true; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -243,7 +243,7 @@ func TestReputOnAtlas(t *testing.T) { } // Use img1 as a render target again. - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -255,7 +255,7 @@ func TestReputOnAtlas(t *testing.T) { t.Fatal(err) } img1.ReplacePixels(make([]byte, 4*size*size)) - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -265,7 +265,7 @@ func TestReputOnAtlas(t *testing.T) { } // img1 is not on an atlas due to ReplacePixels. - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img1.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -275,7 +275,7 @@ func TestReputOnAtlas(t *testing.T) { if err := PutImagesOnAtlasForTesting(); err != nil { t.Fatal(err) } - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := img3.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -375,7 +375,7 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) dst.ReplacePixels(pix) pix, err := dst.Pixels(0, 0, w, h) @@ -423,7 +423,7 @@ func TestSmallImages(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) pix, err := dst.Pixels(0, 0, w, h) if err != nil { @@ -471,7 +471,7 @@ func TestLongImages(t *testing.T) { Width: dstW, Height: dstH, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) pix, err := dst.Pixels(0, 0, dstW, dstH) if err != nil { @@ -559,7 +559,7 @@ func TestDisposedAndReputOnAtlas(t *testing.T) { Width: size, Height: size, } - src.DrawTriangles([graphics.ShaderImageNum]*Image{src2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + src.DrawTriangles([graphics.ShaderImageNum]*Image{src2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := src.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -569,7 +569,7 @@ func TestDisposedAndReputOnAtlas(t *testing.T) { if err := PutImagesOnAtlasForTesting(); err != nil { t.Fatal(err) } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := src.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -609,7 +609,7 @@ func TestImageIsNotReputOnAtlasWithoutUsingAsSource(t *testing.T) { } // Use src2 as a rendering target, and make src2 an independent image. - src2.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + src2.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := src2.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -630,7 +630,7 @@ func TestImageIsNotReputOnAtlasWithoutUsingAsSource(t *testing.T) { if err := PutImagesOnAtlasForTesting(); err != nil { t.Fatal(err) } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) if got, want := src2.IsOnAtlasForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } diff --git a/internal/buffered/image.go b/internal/buffered/image.go index 234ae1ec2..c3e3fb653 100644 --- a/internal/buffered/image.go +++ b/internal/buffered/image.go @@ -202,7 +202,7 @@ func (i *Image) replacePendingPixels(pix []byte, x, y, width, height int) { // DrawTriangles draws the src image with the given vertices. // // Copying vertices and indices is the caller's responsibility. -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, evenOdd bool) { for _, src := range srcs { if i == src { panic("buffered: Image.DrawTriangles: source images must be different from the receiver") @@ -212,7 +212,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f if maybeCanAddDelayedCommand() { if tryAddDelayedCommand(func() error { // Arguments are not copied. Copying is the caller's responsibility. - i.DrawTriangles(srcs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, shader, uniforms) + i.DrawTriangles(srcs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, shader, uniforms, evenOdd) return nil }) { return @@ -238,7 +238,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f } i.resolvePendingPixels(false) - i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms) + i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms, evenOdd) i.invalidatePendingPixels() } diff --git a/internal/driver/graphics.go b/internal/driver/graphics.go index bc85f9785..67ef8225f 100644 --- a/internal/driver/graphics.go +++ b/internal/driver/graphics.go @@ -57,7 +57,7 @@ type Graphics interface { // // * float32 // * []float32 - DrawTriangles(dst ImageID, srcs [graphics.ShaderImageNum]ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shader ShaderID, indexLen int, indexOffset int, mode CompositeMode, colorM *affine.ColorM, filter Filter, address Address, dstRegion, srcRegion Region, uniforms []interface{}) error + DrawTriangles(dst ImageID, srcs [graphics.ShaderImageNum]ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shader ShaderID, indexLen int, indexOffset int, mode CompositeMode, colorM *affine.ColorM, filter Filter, address Address, dstRegion, srcRegion Region, uniforms []interface{}, evenOdd bool) error } // GraphicsNotReady represents that the graphics driver is not ready for recovering from the context lost. diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 239cec479..b9d9cbd45 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -55,7 +55,7 @@ type command interface { NumIndices() int AddNumVertices(n int) AddNumIndices(n int) - CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool + CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool } type size struct { @@ -133,7 +133,7 @@ func (q *commandQueue) appendIndices(indices []uint16, offset uint16) { } // EnqueueDrawTrianglesCommand enqueues a drawing-image command. -func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}, evenOdd bool) { if len(indices) > graphics.IndicesNum { panic(fmt.Sprintf("graphicscommand: len(indices) must be <= graphics.IndicesNum but not at EnqueueDrawTrianglesCommand: len(indices): %d, graphics.IndicesNum: %d", len(indices), graphics.IndicesNum)) } @@ -167,7 +167,7 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.Sh // TODO: If dst is the screen, reorder the command to be the last. if !split && 0 < len(q.commands) { // TODO: Pass offsets and uniforms when merging considers the shader. - if last := q.commands[len(q.commands)-1]; last.CanMergeWithDrawTrianglesCommand(dst, srcs, color, mode, filter, address, dstRegion, srcRegion, shader) { + if last := q.commands[len(q.commands)-1]; last.CanMergeWithDrawTrianglesCommand(dst, srcs, color, mode, filter, address, dstRegion, srcRegion, shader, evenOdd) { last.AddNumVertices(len(vertices)) last.AddNumIndices(len(indices)) return @@ -188,6 +188,7 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.Sh srcRegion: srcRegion, shader: shader, uniforms: uniforms, + evenOdd: evenOdd, } q.commands = append(q.commands, c) } @@ -319,6 +320,7 @@ type drawTrianglesCommand struct { srcRegion driver.Region shader *Shader uniforms []interface{} + evenOdd bool } func (c *drawTrianglesCommand) String() string { @@ -428,7 +430,7 @@ func (c *drawTrianglesCommand) Exec(indexOffset int) error { imgs[0] = c.srcs[0].image.ID() } - return theGraphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.offsets, shaderID, c.nindices, indexOffset, c.mode, c.color, c.filter, c.address, c.dstRegion, c.srcRegion, c.uniforms) + return theGraphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.offsets, shaderID, c.nindices, indexOffset, c.mode, c.color, c.filter, c.address, c.dstRegion, c.srcRegion, c.uniforms, c.evenOdd) } func (c *drawTrianglesCommand) NumVertices() int { @@ -449,7 +451,7 @@ func (c *drawTrianglesCommand) AddNumIndices(n int) { // CanMergeWithDrawTrianglesCommand returns a boolean value indicating whether the other drawTrianglesCommand can be merged // with the drawTrianglesCommand c. -func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { // If a shader is used, commands are not merged. // // TODO: Merge shader commands considering uniform variables. @@ -480,6 +482,9 @@ func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs if c.srcRegion != srcRegion { return false } + if c.evenOdd != evenOdd { + return false + } return true } @@ -513,7 +518,7 @@ func (c *replacePixelsCommand) AddNumVertices(n int) { func (c *replacePixelsCommand) AddNumIndices(n int) { } -func (c *replacePixelsCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *replacePixelsCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } @@ -550,7 +555,7 @@ func (c *pixelsCommand) AddNumVertices(n int) { func (c *pixelsCommand) AddNumIndices(n int) { } -func (c *pixelsCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *pixelsCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } @@ -583,7 +588,7 @@ func (c *disposeImageCommand) AddNumVertices(n int) { func (c *disposeImageCommand) AddNumIndices(n int) { } -func (c *disposeImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *disposeImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } @@ -616,7 +621,7 @@ func (c *disposeShaderCommand) AddNumVertices(n int) { func (c *disposeShaderCommand) AddNumIndices(n int) { } -func (c *disposeShaderCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *disposeShaderCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } @@ -655,7 +660,7 @@ func (c *newImageCommand) AddNumVertices(n int) { func (c *newImageCommand) AddNumIndices(n int) { } -func (c *newImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *newImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } @@ -691,7 +696,7 @@ func (c *newScreenFramebufferImageCommand) AddNumVertices(n int) { func (c *newScreenFramebufferImageCommand) AddNumIndices(n int) { } -func (c *newScreenFramebufferImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *newScreenFramebufferImageCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, copmlex bool) bool { return false } @@ -726,7 +731,7 @@ func (c *newShaderCommand) AddNumVertices(n int) { func (c *newShaderCommand) AddNumIndices(n int) { } -func (c *newShaderCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader) bool { +func (c *newShaderCommand) CanMergeWithDrawTrianglesCommand(dst *Image, src [graphics.ShaderImageNum]*Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, evenOdd bool) bool { return false } diff --git a/internal/graphicscommand/image.go b/internal/graphicscommand/image.go index d39661fdf..e29d2e167 100644 --- a/internal/graphicscommand/image.go +++ b/internal/graphicscommand/image.go @@ -139,7 +139,7 @@ func (i *Image) InternalSize() (int, int) { // // If the source image is not specified, i.e., src is nil and there is no image in the uniform variables, the // elements for the source image are not used. -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, clr *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, clr *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}, evenOdd bool) { if shader == nil { // Fast path for rendering without a shader (#1355). img := srcs[0] @@ -160,7 +160,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [gra } i.resolveBufferedReplacePixels() - theCommandQueue.EnqueueDrawTrianglesCommand(i, srcs, offsets, vertices, indices, clr, mode, filter, address, dstRegion, srcRegion, shader, uniforms) + theCommandQueue.EnqueueDrawTrianglesCommand(i, srcs, offsets, vertices, indices, clr, mode, filter, address, dstRegion, srcRegion, shader, uniforms, evenOdd) } // Pixels returns the image's pixels. diff --git a/internal/graphicscommand/image_test.go b/internal/graphicscommand/image_test.go index df408f064..c41868890 100644 --- a/internal/graphicscommand/image_test.go +++ b/internal/graphicscommand/image_test.go @@ -50,7 +50,7 @@ func TestClear(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) pix, err := dst.Pixels() if err != nil { @@ -81,8 +81,8 @@ func TestReplacePixelsPartAfterDrawTriangles(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) dst.ReplacePixels(make([]byte, 4), 0, 0, 1, 1) // TODO: Check the result. @@ -100,11 +100,11 @@ func TestShader(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) ir := etesting.ShaderProgramFill(0xff, 0, 0, 0xff) s := NewShader(&ir) - dst.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) pix, err := dst.Pixels() if err != nil { diff --git a/internal/graphicsdriver/metal/graphics.go b/internal/graphicsdriver/metal/graphics.go index e3ab0c847..d0357e326 100644 --- a/internal/graphicsdriver/metal/graphics.go +++ b/internal/graphicsdriver/metal/graphics.go @@ -296,11 +296,12 @@ FragmentShaderFunc(0, FILTER_SCREEN, ADDRESS_UNSAFE) ` type rpsKey struct { - useColorM bool - filter driver.Filter - address driver.Address - compositeMode driver.CompositeMode - screen bool + useColorM bool + filter driver.Filter + address driver.Address + compositeMode driver.CompositeMode + colorWriteMask bool + screen bool } type Graphics struct { @@ -313,7 +314,9 @@ type Graphics struct { rce mtl.RenderCommandEncoder screenDrawable ca.MetalDrawable + lastDstTexture mtl.Texture + lastStencilUse bool vb mtl.Buffer ib mtl.Buffer @@ -334,6 +337,18 @@ type Graphics struct { pool unsafe.Pointer } +type stencilMode int + +const ( + prepareStencil stencilMode = iota + drawWithStencil + noStencil +) + +func (s stencilMode) useStencil() bool { + return s != noStencil +} + var theGraphics Graphics func Get() *Graphics { @@ -539,8 +554,9 @@ func (g *Graphics) Reset() error { return err } rpld := mtl.RenderPipelineDescriptor{ - VertexFunction: vs, - FragmentFunction: fs, + VertexFunction: vs, + FragmentFunction: fs, + StencilAttachmentPixelFormat: mtl.PixelFormatStencil8, } rpld.ColorAttachments[0].PixelFormat = g.view.colorPixelFormat() rpld.ColorAttachments[0].BlendingEnabled = true @@ -548,6 +564,7 @@ func (g *Graphics) Reset() error { rpld.ColorAttachments[0].DestinationRGBBlendFactor = mtl.BlendFactorZero rpld.ColorAttachments[0].SourceAlphaBlendFactor = mtl.BlendFactorOne rpld.ColorAttachments[0].SourceRGBBlendFactor = mtl.BlendFactorOne + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll rps, err := g.view.getMTLDevice().MakeRenderPipelineState(rpld) if err != nil { return err @@ -566,42 +583,51 @@ func (g *Graphics) Reset() error { driver.FilterLinear, } { for c := driver.CompositeModeSourceOver; c <= driver.CompositeModeMax; c++ { - cmi := 0 - if cm { - cmi = 1 - } - fs, err := lib.MakeFunction(fmt.Sprintf("FragmentShader_%d_%d_%d", cmi, f, a)) - if err != nil { - return err - } - rpld := mtl.RenderPipelineDescriptor{ - VertexFunction: vs, - FragmentFunction: fs, - } + for _, cwm := range []bool{false, true} { + cmi := 0 + if cm { + cmi = 1 + } + fs, err := lib.MakeFunction(fmt.Sprintf("FragmentShader_%d_%d_%d", cmi, f, a)) + if err != nil { + return err + } + rpld := mtl.RenderPipelineDescriptor{ + VertexFunction: vs, + FragmentFunction: fs, + StencilAttachmentPixelFormat: mtl.PixelFormatStencil8, + } - pix := mtl.PixelFormatRGBA8UNorm - if screen { - pix = g.view.colorPixelFormat() - } - rpld.ColorAttachments[0].PixelFormat = pix - rpld.ColorAttachments[0].BlendingEnabled = true + pix := mtl.PixelFormatRGBA8UNorm + if screen { + pix = g.view.colorPixelFormat() + } + rpld.ColorAttachments[0].PixelFormat = pix + rpld.ColorAttachments[0].BlendingEnabled = true - src, dst := c.Operations() - rpld.ColorAttachments[0].DestinationAlphaBlendFactor = operationToBlendFactor(dst) - rpld.ColorAttachments[0].DestinationRGBBlendFactor = operationToBlendFactor(dst) - rpld.ColorAttachments[0].SourceAlphaBlendFactor = operationToBlendFactor(src) - rpld.ColorAttachments[0].SourceRGBBlendFactor = operationToBlendFactor(src) - rps, err := g.view.getMTLDevice().MakeRenderPipelineState(rpld) - if err != nil { - return err + src, dst := c.Operations() + rpld.ColorAttachments[0].DestinationAlphaBlendFactor = operationToBlendFactor(dst) + rpld.ColorAttachments[0].DestinationRGBBlendFactor = operationToBlendFactor(dst) + rpld.ColorAttachments[0].SourceAlphaBlendFactor = operationToBlendFactor(src) + rpld.ColorAttachments[0].SourceRGBBlendFactor = operationToBlendFactor(src) + if cwm { + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll + } else { + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskNone + } + rps, err := g.view.getMTLDevice().MakeRenderPipelineState(rpld) + if err != nil { + return err + } + g.rpss[rpsKey{ + screen: screen, + useColorM: cm, + filter: f, + address: a, + compositeMode: c, + colorWriteMask: cwm, + }] = rps } - g.rpss[rpsKey{ - screen: screen, - useColorM: cm, - filter: f, - address: a, - compositeMode: c, - }] = rps } } } @@ -619,12 +645,14 @@ func (g *Graphics) flushRenderCommandEncoderIfNeeded() { g.rce.EndEncoding() g.rce = mtl.RenderCommandEncoder{} g.lastDstTexture = mtl.Texture{} + g.lastStencilUse = false } -func (g *Graphics) draw(rps mtl.RenderPipelineState, dst *Image, dstRegion driver.Region, srcs [graphics.ShaderImageNum]*Image, indexLen int, indexOffset int, uniforms []interface{}) error { - if g.lastDstTexture != dst.mtlTexture() { +func (g *Graphics) draw(rps mtl.RenderPipelineState, dst *Image, dstRegion driver.Region, srcs [graphics.ShaderImageNum]*Image, indexLen int, indexOffset int, uniforms []interface{}, stencilMode stencilMode) error { + if g.lastDstTexture != dst.mtlTexture() || g.lastStencilUse != stencilMode.useStencil() { g.flushRenderCommandEncoderIfNeeded() } + if g.rce == (mtl.RenderCommandEncoder{}) { rpd := mtl.RenderPassDescriptor{} // Even though the destination pixels are not used, mtl.LoadActionDontCare might cause glitches @@ -640,11 +668,19 @@ func (g *Graphics) draw(rps mtl.RenderPipelineState, dst *Image, dstRegion drive rpd.ColorAttachments[0].Texture = t rpd.ColorAttachments[0].ClearColor = mtl.ClearColor{} + if stencilMode.useStencil() { + dst.ensureStencil() + rpd.StencilAttachment.LoadAction = mtl.LoadActionClear + rpd.StencilAttachment.StoreAction = mtl.StoreActionDontCare + rpd.StencilAttachment.Texture = dst.stencil + } + if g.cb == (mtl.CommandBuffer{}) { g.cb = g.cq.MakeCommandBuffer() } g.rce = g.cb.MakeRenderCommandEncoder(rpd) } + g.lastStencilUse = stencilMode.useStencil() g.rce.SetRenderPipelineState(rps) @@ -687,11 +723,51 @@ func (g *Graphics) draw(rps mtl.RenderPipelineState, dst *Image, dstRegion drive g.rce.SetFragmentTexture(mtl.Texture{}, i) } } + + // The stencil reference value is always 0 (default). + switch stencilMode { + case prepareStencil: + desc := mtl.DepthStencilDescriptor{ + BackFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationInvert, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + FrontFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationInvert, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + } + g.rce.SetDepthStencilState(g.view.getMTLDevice().MakeDepthStencilState(desc)) + case drawWithStencil: + desc := mtl.DepthStencilDescriptor{ + BackFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationZero, + DepthFailureOperation: mtl.StencilOperationZero, + DepthStencilPassOperation: mtl.StencilOperationZero, + StencilCompareFunction: mtl.CompareFunctionNotEqual, + }, + FrontFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationZero, + DepthFailureOperation: mtl.StencilOperationZero, + DepthStencilPassOperation: mtl.StencilOperationZero, + StencilCompareFunction: mtl.CompareFunctionNotEqual, + }, + } + g.rce.SetDepthStencilState(g.view.getMTLDevice().MakeDepthStencilState(desc)) + case noStencil: + g.rce.SetDepthStencilState(mtl.DepthStencilState{}) + } + g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, indexLen, mtl.IndexTypeUInt16, g.ib, indexOffset*2) + return nil } -func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderImageNum]driver.ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shaderID driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, colorM *affine.ColorM, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, uniforms []interface{}) error { +func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderImageNum]driver.ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shaderID driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, colorM *affine.ColorM, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, uniforms []interface{}, evenOdd bool) error { dst := g.images[dstID] if dst.screen { @@ -703,18 +779,28 @@ func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderIm srcs[i] = g.images[srcID] } - var rps mtl.RenderPipelineState + var rpss [2]mtl.RenderPipelineState var uniformVars []interface{} if shaderID == driver.InvalidShaderID { if dst.screen && filter == driver.FilterScreen { - rps = g.screenRPS + rpss[1] = g.screenRPS } else { - rps = g.rpss[rpsKey{ - screen: dst.screen, - useColorM: colorM != nil, - filter: filter, - address: address, - compositeMode: mode, + useColorM := colorM != nil + rpss[0] = g.rpss[rpsKey{ + screen: dst.screen, + useColorM: useColorM, + filter: filter, + address: address, + compositeMode: mode, + colorWriteMask: false, + }] + rpss[1] = g.rpss[rpsKey{ + screen: dst.screen, + useColorM: useColorM, + filter: filter, + address: address, + compositeMode: mode, + colorWriteMask: true, }] } @@ -745,7 +831,11 @@ func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderIm } } else { var err error - rps, err = g.shaders[shaderID].RenderPipelineState(g.view.getMTLDevice(), mode) + rpss[0], err = g.shaders[shaderID].RenderPipelineState(g.view.getMTLDevice(), mode, false) + if err != nil { + return err + } + rpss[1], err = g.shaders[shaderID].RenderPipelineState(g.view.getMTLDevice(), mode, true) if err != nil { return err } @@ -798,8 +888,17 @@ func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderIm } } - if err := g.draw(rps, dst, dstRegion, srcs, indexLen, indexOffset, uniformVars); err != nil { - return err + if evenOdd { + if err := g.draw(rpss[0], dst, dstRegion, srcs, indexLen, indexOffset, uniformVars, prepareStencil); err != nil { + return err + } + if err := g.draw(rpss[1], dst, dstRegion, srcs, indexLen, indexOffset, uniformVars, drawWithStencil); err != nil { + return err + } + } else { + if err := g.draw(rpss[1], dst, dstRegion, srcs, indexLen, indexOffset, uniformVars, noStencil); err != nil { + return err + } } return nil @@ -889,6 +988,7 @@ type Image struct { height int screen bool texture mtl.Texture + stencil mtl.Texture } func (i *Image) ID() driver.ImageID { @@ -903,6 +1003,10 @@ func (i *Image) internalSize() (int, int) { } func (i *Image) Dispose() { + if i.stencil != (mtl.Texture{}) { + i.stencil.Release() + i.stencil = mtl.Texture{} + } if i.texture != (mtl.Texture{}) { i.texture.Release() i.texture = mtl.Texture{} @@ -1022,3 +1126,19 @@ func (i *Image) mtlTexture() mtl.Texture { } return i.texture } + +func (i *Image) ensureStencil() { + if i.stencil != (mtl.Texture{}) { + return + } + + td := mtl.TextureDescriptor{ + TextureType: mtl.TextureType2D, + PixelFormat: mtl.PixelFormatStencil8, + Width: graphics.InternalImageSize(i.width), + Height: graphics.InternalImageSize(i.height), + StorageMode: mtl.StorageModePrivate, + Usage: mtl.TextureUsageRenderTarget, + } + i.stencil = i.graphics.view.getMTLDevice().MakeTexture(td) +} diff --git a/internal/graphicsdriver/metal/shader.go b/internal/graphicsdriver/metal/shader.go index 732b637f1..147a13ae5 100644 --- a/internal/graphicsdriver/metal/shader.go +++ b/internal/graphicsdriver/metal/shader.go @@ -26,20 +26,25 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/shaderir/metal" ) +type shaderRpsKey struct { + compositeMode driver.CompositeMode + colorWriteMask bool +} + type Shader struct { id driver.ShaderID ir *shaderir.Program fs mtl.Function vs mtl.Function - rpss map[driver.CompositeMode]mtl.RenderPipelineState + rpss map[shaderRpsKey]mtl.RenderPipelineState } func newShader(device mtl.Device, id driver.ShaderID, program *shaderir.Program) (*Shader, error) { s := &Shader{ id: id, ir: program, - rpss: map[driver.CompositeMode]mtl.RenderPipelineState{}, + rpss: map[shaderRpsKey]mtl.RenderPipelineState{}, } if err := s.init(device); err != nil { return nil, err @@ -83,31 +88,43 @@ func (s *Shader) init(device mtl.Device) error { return nil } -func (s *Shader) RenderPipelineState(device mtl.Device, c driver.CompositeMode) (mtl.RenderPipelineState, error) { - if rps, ok := s.rpss[c]; ok { +func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode driver.CompositeMode, colorWriteMask bool) (mtl.RenderPipelineState, error) { + if rps, ok := s.rpss[shaderRpsKey{ + compositeMode: compositeMode, + colorWriteMask: colorWriteMask, + }]; ok { return rps, nil } rpld := mtl.RenderPipelineDescriptor{ - VertexFunction: s.vs, - FragmentFunction: s.fs, + VertexFunction: s.vs, + FragmentFunction: s.fs, + StencilAttachmentPixelFormat: mtl.PixelFormatStencil8, } // TODO: For the precise pixel format, whether the render target is the screen or not must be considered. rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm rpld.ColorAttachments[0].BlendingEnabled = true - src, dst := c.Operations() + src, dst := compositeMode.Operations() rpld.ColorAttachments[0].DestinationAlphaBlendFactor = operationToBlendFactor(dst) rpld.ColorAttachments[0].DestinationRGBBlendFactor = operationToBlendFactor(dst) rpld.ColorAttachments[0].SourceAlphaBlendFactor = operationToBlendFactor(src) rpld.ColorAttachments[0].SourceRGBBlendFactor = operationToBlendFactor(src) + if colorWriteMask { + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll + } else { + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskNone + } rps, err := device.MakeRenderPipelineState(rpld) if err != nil { return mtl.RenderPipelineState{}, err } - s.rpss[c] = rps + s.rpss[shaderRpsKey{ + compositeMode: compositeMode, + colorWriteMask: colorWriteMask, + }] = rps return rps, nil } diff --git a/internal/graphicsdriver/opengl/context.go b/internal/graphicsdriver/opengl/context.go index a69136df3..641fdb274 100644 --- a/internal/graphicsdriver/opengl/context.go +++ b/internal/graphicsdriver/opengl/context.go @@ -49,6 +49,7 @@ type context struct { screenFramebuffer framebufferNative // This might not be the default frame buffer '0' (e.g. iOS). lastFramebuffer framebufferNative lastTexture textureNative + lastRenderbuffer renderbufferNative lastViewportWidth int lastViewportHeight int lastCompositeMode driver.CompositeMode @@ -68,6 +69,14 @@ func (c *context) bindTexture(t textureNative) { c.lastTexture = t } +func (c *context) bindRenderbuffer(r renderbufferNative) { + if c.lastRenderbuffer.equal(r) { + return + } + c.bindRenderbufferImpl(r) + c.lastRenderbuffer = r +} + func (c *context) bindFramebuffer(f framebufferNative) { if c.lastFramebuffer.equal(f) { return diff --git a/internal/graphicsdriver/opengl/context_desktop.go b/internal/graphicsdriver/opengl/context_desktop.go index 4397efa26..c700b5710 100644 --- a/internal/graphicsdriver/opengl/context_desktop.go +++ b/internal/graphicsdriver/opengl/context_desktop.go @@ -29,17 +29,22 @@ import ( ) type ( - textureNative uint32 - framebufferNative uint32 - shader uint32 - program uint32 - buffer uint32 + textureNative uint32 + renderbufferNative uint32 + framebufferNative uint32 + shader uint32 + program uint32 + buffer uint32 ) func (t textureNative) equal(rhs textureNative) bool { return t == rhs } +func (r renderbufferNative) equal(rhs renderbufferNative) bool { + return r == rhs +} + func (f framebufferNative) equal(rhs framebufferNative) bool { return f == rhs } @@ -198,6 +203,38 @@ func (c *context) isTexture(t textureNative) bool { panic("opengl: isTexture is not implemented") } +func (c *context) newRenderbuffer(width, height int) (renderbufferNative, error) { + var r uint32 + gl.GenRenderbuffersEXT(1, &r) + if r <= 0 { + return 0, errors.New("opengl: creating renderbuffer failed") + } + + renderbuffer := renderbufferNative(r) + c.bindRenderbuffer(renderbuffer) + + // GL_STENCIL_INDEX8 might not be available with OpenGL 2.1. + // https://www.khronos.org/opengl/wiki/Image_Format + gl.RenderbufferStorageEXT(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, int32(width), int32(height)) + + return renderbuffer, nil +} + +func (c *context) bindRenderbufferImpl(r renderbufferNative) { + gl.BindRenderbufferEXT(gl.RENDERBUFFER, uint32(r)) +} + +func (c *context) deleteRenderbuffer(r renderbufferNative) { + rr := uint32(r) + if !gl.IsRenderbufferEXT(rr) { + return + } + if c.lastRenderbuffer.equal(r) { + c.lastRenderbuffer = 0 + } + gl.DeleteRenderbuffersEXT(1, &rr) +} + func (c *context) newFramebuffer(texture textureNative) (framebufferNative, error) { var f uint32 gl.GenFramebuffersEXT(1, &f) @@ -220,6 +257,17 @@ func (c *context) newFramebuffer(texture textureNative) (framebufferNative, erro return framebufferNative(f), nil } +func (c *context) bindStencilBuffer(f framebufferNative, r renderbufferNative) error { + c.bindFramebuffer(f) + + gl.FramebufferRenderbufferEXT(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, uint32(r)) + if s := gl.CheckFramebufferStatusEXT(gl.FRAMEBUFFER); s != gl.FRAMEBUFFER_COMPLETE { + return errors.New(fmt.Sprintf("opengl: glFramebufferRenderbuffer failed: %d", s)) + } + gl.Clear(gl.STENCIL_BUFFER_BIT) + return nil +} + func (c *context) setViewportImpl(width, height int) { gl.Viewport(0, 0, int32(width), int32(height)) } @@ -492,3 +540,23 @@ func (c *context) getBufferSubData(buffer buffer, width, height int) []byte { gl.BindBuffer(gl.PIXEL_UNPACK_BUFFER, 0) return pixels } + +func (c *context) enableStencilTest() { + gl.Enable(gl.STENCIL_TEST) +} + +func (c *context) disableStencilTest() { + gl.Disable(gl.STENCIL_TEST) +} + +func (c *context) beginStencilWithEvenOddRule() { + gl.StencilFunc(gl.ALWAYS, 0x00, 0xff) + gl.StencilOp(gl.KEEP, gl.KEEP, gl.INVERT) + gl.ColorMask(false, false, false, false) +} + +func (c *context) endStencilWithEvenOddRule() { + gl.StencilFunc(gl.NOTEQUAL, 0x00, 0xff) + gl.StencilOp(gl.ZERO, gl.ZERO, gl.ZERO) + gl.ColorMask(true, true, true, true) +} diff --git a/internal/graphicsdriver/opengl/context_js.go b/internal/graphicsdriver/opengl/context_js.go index 23f51f925..6ec65c33d 100644 --- a/internal/graphicsdriver/opengl/context_js.go +++ b/internal/graphicsdriver/opengl/context_js.go @@ -26,11 +26,12 @@ import ( ) type ( - textureNative js.Value - framebufferNative js.Value - shader js.Value - buffer js.Value - uniformLocation js.Value + textureNative js.Value + renderbufferNative js.Value + framebufferNative js.Value + shader js.Value + buffer js.Value + uniformLocation js.Value attribLocation int programID int @@ -44,6 +45,10 @@ func (t textureNative) equal(rhs textureNative) bool { return js.Value(t).Equal(js.Value(rhs)) } +func (r renderbufferNative) equal(rhs renderbufferNative) bool { + return js.Value(r).Equal(js.Value(rhs)) +} + func (f framebufferNative) equal(rhs framebufferNative) bool { return js.Value(f).Equal(js.Value(rhs)) } @@ -100,6 +105,7 @@ func (c *context) initGL() { attr := js.Global().Get("Object").New() attr.Set("alpha", true) attr.Set("premultipliedAlpha", true) + attr.Set("stencil", true) if isWebGL2Available { gl = canvas.Call("getContext", "webgl2", attr) @@ -165,7 +171,7 @@ func (c *context) newTexture(width, height int) (textureNative, error) { gl := c.gl t := gl.createTexture.Invoke() if !t.Truthy() { - return textureNative(js.Null()), errors.New("opengl: glGenTexture failed") + return textureNative(js.Null()), errors.New("opengl: createTexture failed") } c.bindTexture(textureNative(t)) @@ -240,6 +246,37 @@ func (c *context) isTexture(t textureNative) bool { panic("opengl: isTexture is not implemented") } +func (c *context) newRenderbuffer(width, height int) (renderbufferNative, error) { + gl := c.gl + r := gl.createRenderbuffer.Invoke() + if !r.Truthy() { + return renderbufferNative(js.Null()), errors.New("opengl: createRenderbuffer failed") + } + + c.bindRenderbuffer(renderbufferNative(r)) + // TODO: Is STENCIL_INDEX8 portable? + // https://stackoverflow.com/questions/11084961/binding-a-stencil-render-buffer-to-a-frame-buffer-in-opengl + gl.renderbufferStorage.Invoke(gles.RENDERBUFFER, gles.STENCIL_INDEX8, width, height) + + return renderbufferNative(r), nil +} + +func (c *context) bindRenderbufferImpl(r renderbufferNative) { + gl := c.gl + gl.bindRenderbuffer.Invoke(gles.RENDERBUFFER, js.Value(r)) +} + +func (c *context) deleteRenderbuffer(r renderbufferNative) { + gl := c.gl + if !gl.isRenderbuffer.Invoke(js.Value(r)).Bool() { + return + } + if c.lastRenderbuffer.equal(r) { + c.lastRenderbuffer = renderbufferNative(js.Null()) + } + gl.deleteRenderbuffer.Invoke(js.Value(r)) +} + func (c *context) newFramebuffer(t textureNative) (framebufferNative, error) { gl := c.gl f := gl.createFramebuffer.Invoke() @@ -253,6 +290,18 @@ func (c *context) newFramebuffer(t textureNative) (framebufferNative, error) { return framebufferNative(f), nil } +func (c *context) bindStencilBuffer(f framebufferNative, r renderbufferNative) error { + gl := c.gl + c.bindFramebuffer(f) + + gl.framebufferRenderbuffer.Invoke(gles.FRAMEBUFFER, gles.STENCIL_ATTACHMENT, gles.RENDERBUFFER, js.Value(r)) + if s := gl.checkFramebufferStatus.Invoke(gles.FRAMEBUFFER); s.Int() != gles.FRAMEBUFFER_COMPLETE { + return errors.New(fmt.Sprintf("opengl: framebufferRenderbuffer failed: %d", s.Int())) + } + gl.clear.Invoke(gles.STENCIL_BUFFER_BIT) + return nil +} + func (c *context) setViewportImpl(width, height int) { gl := c.gl gl.viewport.Invoke(0, 0, width, height) @@ -598,3 +647,27 @@ func (c *context) getBufferSubData(buffer buffer, width, height int) []byte { gl.bindBuffer.Invoke(gles.PIXEL_UNPACK_BUFFER, nil) return jsutil.Uint8ArrayToSlice(arr, l) } + +func (c *context) enableStencilTest() { + gl := c.gl + gl.enable.Invoke(gles.STENCIL_TEST) +} + +func (c *context) disableStencilTest() { + gl := c.gl + gl.disable.Invoke(gles.STENCIL_TEST) +} + +func (c *context) beginStencilWithEvenOddRule() { + gl := c.gl + gl.stencilFunc.Invoke(gles.ALWAYS, 0x00, 0xff) + gl.stencilOp.Invoke(gles.KEEP, gles.KEEP, gles.INVERT) + gl.colorMask.Invoke(false, false, false, false) +} + +func (c *context) endStencilWithEvenOddRule() { + gl := c.gl + gl.stencilFunc.Invoke(gles.NOTEQUAL, 0x00, 0xff) + gl.stencilOp.Invoke(gles.ZERO, gles.ZERO, gles.ZERO) + gl.colorMask.Invoke(true, true, true, true) +} diff --git a/internal/graphicsdriver/opengl/context_mobile.go b/internal/graphicsdriver/opengl/context_mobile.go index 2640e3f8b..740994f17 100644 --- a/internal/graphicsdriver/opengl/context_mobile.go +++ b/internal/graphicsdriver/opengl/context_mobile.go @@ -27,17 +27,22 @@ import ( ) type ( - textureNative uint32 - framebufferNative uint32 - shader uint32 - program uint32 - buffer uint32 + textureNative uint32 + renderbufferNative uint32 + framebufferNative uint32 + shader uint32 + program uint32 + buffer uint32 ) func (t textureNative) equal(rhs textureNative) bool { return t == rhs } +func (r renderbufferNative) equal(rhs renderbufferNative) bool { + return r == rhs +} + func (f framebufferNative) equal(rhs framebufferNative) bool { return f == rhs } @@ -185,6 +190,34 @@ func (c *context) isTexture(t textureNative) bool { return c.ctx.IsTexture(uint32(t)) } +func (c *context) newRenderbuffer(width, height int) (renderbufferNative, error) { + r := c.ctx.GenRenderbuffers(1)[0] + if r <= 0 { + return 0, errors.New("opengl: creating renderbuffer failed") + } + + renderbuffer := renderbufferNative(r) + c.bindRenderbuffer(renderbuffer) + + c.ctx.RenderbufferStorage(gles.RENDERBUFFER, gles.STENCIL_INDEX8, int32(width), int32(height)) + + return renderbuffer, nil +} + +func (c *context) bindRenderbufferImpl(r renderbufferNative) { + c.ctx.BindRenderbuffer(gles.RENDERBUFFER, uint32(r)) +} + +func (c *context) deleteRenderbuffer(r renderbufferNative) { + if !c.ctx.IsRenderbuffer(uint32(r)) { + return + } + if c.lastRenderbuffer.equal(r) { + c.lastRenderbuffer = 0 + } + c.ctx.DeleteRenderbuffers([]uint32{uint32(r)}) +} + func (c *context) newFramebuffer(texture textureNative) (framebufferNative, error) { f := c.ctx.GenFramebuffers(1)[0] if f <= 0 { @@ -206,6 +239,17 @@ func (c *context) newFramebuffer(texture textureNative) (framebufferNative, erro return framebufferNative(f), nil } +func (c *context) bindStencilBuffer(f framebufferNative, r renderbufferNative) error { + c.bindFramebuffer(f) + + c.ctx.FramebufferRenderbuffer(gles.FRAMEBUFFER, gles.STENCIL_ATTACHMENT, gles.RENDERBUFFER, uint32(r)) + if s := c.ctx.CheckFramebufferStatus(gles.FRAMEBUFFER); s != gles.FRAMEBUFFER_COMPLETE { + return errors.New(fmt.Sprintf("opengl: glFramebufferRenderbuffer failed: %d", s)) + } + c.ctx.Clear(gles.STENCIL_BUFFER_BIT) + return nil +} + func (c *context) setViewportImpl(width, height int) { c.ctx.Viewport(0, 0, int32(width), int32(height)) } @@ -457,3 +501,23 @@ func (c *context) getBufferSubData(buffer buffer, width, height int) []byte { // As PBO is not used in mobiles, leave this unimplemented so far. panic("opengl: getBufferSubData is not implemented for mobiles") } + +func (c *context) enableStencilTest() { + c.ctx.Enable(gles.STENCIL_TEST) +} + +func (c *context) disableStencilTest() { + c.ctx.Disable(gles.STENCIL_TEST) +} + +func (c *context) beginStencilWithEvenOddRule() { + c.ctx.StencilFunc(gles.ALWAYS, 0x00, 0xff) + c.ctx.StencilOp(gles.KEEP, gles.KEEP, gles.INVERT) + c.ctx.ColorMask(false, false, false, false) +} + +func (c *context) endStencilWithEvenOddRule() { + c.ctx.StencilFunc(gles.NOTEQUAL, 0x00, 0xff) + c.ctx.StencilOp(gles.ZERO, gles.ZERO, gles.ZERO) + c.ctx.ColorMask(true, true, true, true) +} diff --git a/internal/graphicsdriver/opengl/graphics.go b/internal/graphicsdriver/opengl/graphics.go index bef16f05a..3d514bd03 100644 --- a/internal/graphicsdriver/opengl/graphics.go +++ b/internal/graphicsdriver/opengl/graphics.go @@ -142,7 +142,7 @@ func (g *Graphics) SetVertices(vertices []float32, indices []uint16) { g.context.elementArrayBufferSubData(indices) } -func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderImageNum]driver.ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shaderID driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, colorM *affine.ColorM, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, uniforms []interface{}) error { +func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderImageNum]driver.ImageID, offsets [graphics.ShaderImageNum - 1][2]float32, shaderID driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, colorM *affine.ColorM, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, uniforms []interface{}, evenOdd bool) error { destination := g.images[dstID] if !destination.pbo.equal(*new(buffer)) { @@ -308,7 +308,20 @@ func (g *Graphics) DrawTriangles(dstID driver.ImageID, srcIDs [graphics.ShaderIm return err } + if evenOdd { + if err := destination.ensureStencilBuffer(); err != nil { + return err + } + g.context.enableStencilTest() + g.context.beginStencilWithEvenOddRule() + g.context.drawElements(indexLen, indexOffset*2) + g.context.endStencilWithEvenOddRule() + } g.context.drawElements(indexLen, indexOffset*2) // 2 is uint16 size in bytes + if evenOdd { + g.context.disableStencilTest() + } + return nil } diff --git a/internal/graphicsdriver/opengl/image.go b/internal/graphicsdriver/opengl/image.go index 49e07527a..6889a1efb 100644 --- a/internal/graphicsdriver/opengl/image.go +++ b/internal/graphicsdriver/opengl/image.go @@ -23,6 +23,7 @@ type Image struct { id driver.ImageID graphics *Graphics texture textureNative + stencil renderbufferNative framebuffer *framebuffer pbo buffer width int @@ -48,6 +49,9 @@ func (i *Image) Dispose() { if !i.texture.equal(*new(textureNative)) { i.graphics.context.deleteTexture(i.texture) } + if !i.stencil.equal(*new(renderbufferNative)) { + i.graphics.context.deleteRenderbuffer(i.stencil) + } i.graphics.removeImage(i) } @@ -105,6 +109,27 @@ func (i *Image) ensureFramebuffer() error { return nil } +func (i *Image) ensureStencilBuffer() error { + if !i.stencil.equal(*new(renderbufferNative)) { + return nil + } + + if err := i.ensureFramebuffer(); err != nil { + return err + } + + r, err := i.graphics.context.newRenderbuffer(i.framebufferSize()) + if err != nil { + return err + } + i.stencil = r + + if err := i.graphics.context.bindStencilBuffer(i.framebuffer.native, i.stencil); err != nil { + return err + } + return nil +} + func (i *Image) ReplacePixels(args []*driver.ReplacePixelsArgs) { if i.screen { panic("opengl: ReplacePixels cannot be called on the screen, that doesn't have a texture") diff --git a/internal/mipmap/mipmap.go b/internal/mipmap/mipmap.go index d96a39aca..2fa150a8a 100644 --- a/internal/mipmap/mipmap.go +++ b/internal/mipmap/mipmap.go @@ -85,7 +85,7 @@ func (m *Mipmap) Pixels(x, y, width, height int) ([]byte, error) { return m.orig.Pixels(x, y, width, height) } -func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, canSkipMipmap bool) { +func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, evenOdd bool, canSkipMipmap bool) { if len(indices) == 0 { return } @@ -164,7 +164,7 @@ func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices [ imgs[i] = src.orig } - m.orig.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms) + m.orig.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms, evenOdd) m.disposeMipmaps() } @@ -226,7 +226,7 @@ func (m *Mipmap) level(level int) *buffered.Image { Width: float32(w2), Height: float32(h2), } - s.DrawTriangles([graphics.ShaderImageNum]*buffered.Image{src}, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressUnsafe, dstRegion, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) + s.DrawTriangles([graphics.ShaderImageNum]*buffered.Image{src}, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressUnsafe, dstRegion, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) m.imgs[level] = s return m.imgs[level] diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 1e7ca231d..04a613f65 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -76,6 +76,7 @@ type drawTrianglesHistoryItem struct { srcRegion driver.Region shader *Shader uniforms []interface{} + evenOdd bool } // Image represents an image that can be restored when GL context is lost. @@ -186,7 +187,7 @@ func (i *Image) Extend(width, height int) *Image { Width: float32(sw), Height: float32(sh), } - newImg.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + newImg.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) // Overwrite the history as if the image newImg is created only by ReplacePixels. Now drawTrianglesHistory // and basePixels cannot be mixed. @@ -247,7 +248,7 @@ func clearImage(i *graphicscommand.Image) { Width: float32(dw), Height: float32(dh), } - i.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dstRegion, driver.Region{}, nil, nil) + i.DrawTriangles(srcs, offsets, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dstRegion, driver.Region{}, nil, nil, false) } // BasePixelsForTesting returns the image's basePixels for testing. @@ -350,7 +351,7 @@ func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) { // 5: Color G // 6: Color B // 7: Color Y -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}, evenOdd bool) { if i.priority { panic("restorable: DrawTriangles cannot be called on a priority image") } @@ -374,7 +375,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [gra if srcstale || i.screen || !NeedsRestoring() || i.volatile { i.makeStale() } else { - i.appendDrawTrianglesHistory(srcs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, shader, uniforms) + i.appendDrawTrianglesHistory(srcs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, shader, uniforms, evenOdd) } var s *graphicscommand.Shader @@ -391,11 +392,11 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, offsets [gra } s = shader.shader } - i.image.DrawTriangles(imgs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, s, uniforms) + i.image.DrawTriangles(imgs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, s, uniforms, evenOdd) } // appendDrawTrianglesHistory appends a draw-image history item to the image. -func (i *Image) appendDrawTrianglesHistory(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (i *Image) appendDrawTrianglesHistory(srcs [graphics.ShaderImageNum]*Image, offsets [graphics.ShaderImageNum - 1][2]float32, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, dstRegion, srcRegion driver.Region, shader *Shader, uniforms []interface{}, evenOdd bool) { if i.stale || i.volatile || i.screen { return } @@ -427,6 +428,7 @@ func (i *Image) appendDrawTrianglesHistory(srcs [graphics.ShaderImageNum]*Image, srcRegion: srcRegion, shader: shader, uniforms: uniforms, + evenOdd: evenOdd, } i.drawTrianglesHistory = append(i.drawTrianglesHistory, item) } @@ -605,7 +607,7 @@ func (i *Image) restore() error { } imgs[i] = img.image } - gimg.DrawTriangles(imgs, c.offsets, c.vertices, c.indices, c.colorm, c.mode, c.filter, c.address, c.dstRegion, c.srcRegion, s, c.uniforms) + gimg.DrawTriangles(imgs, c.offsets, c.vertices, c.indices, c.colorm, c.mode, c.filter, c.address, c.dstRegion, c.srcRegion, s, c.uniforms, c.evenOdd) } if len(i.drawTrianglesHistory) > 0 { diff --git a/internal/restorable/images_test.go b/internal/restorable/images_test.go index bcc8acc80..e7e8e5cf5 100644 --- a/internal/restorable/images_test.go +++ b/internal/restorable/images_test.go @@ -137,7 +137,7 @@ func TestRestoreChain(t *testing.T) { Width: 1, Height: 1, } - imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) } if err := ResolveStaleImages(); err != nil { t.Fatal(err) @@ -185,10 +185,10 @@ func TestRestoreChain2(t *testing.T) { Width: w, Height: h, } - imgs[8].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[7]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) - imgs[9].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[8]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + imgs[8].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[7]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) + imgs[9].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[8]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) for i := 0; i < 7; i++ { - imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) } if err := ResolveStaleImages(); err != nil { @@ -234,10 +234,10 @@ func TestRestoreOverrideSource(t *testing.T) { Width: w, Height: h, } - img2.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) - img3.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img2.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) + img3.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) img0.ReplacePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, 0, 0, w, h) - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) if err := ResolveStaleImages(); err != nil { t.Fatal(err) } @@ -323,23 +323,23 @@ func TestRestoreComplexGraph(t *testing.T) { Height: h, } var offsets [graphics.ShaderImageNum - 1][2]float32 - img3.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img3.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 1, 0) - img3.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img3.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 1, 0) - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 2, 0) - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 0, 0) - img5.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img5.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 0, 0) - img6.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img6.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 1, 0) - img6.DrawTriangles([graphics.ShaderImageNum]*Image{img4}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img6.DrawTriangles([graphics.ShaderImageNum]*Image{img4}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 0, 0) - img7.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img7.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) vs = quadVertices(w, h, 2, 0) - img7.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img7.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, offsets, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) if err := ResolveStaleImages(); err != nil { t.Fatal(err) } @@ -437,8 +437,8 @@ func TestRestoreRecursive(t *testing.T) { Width: w, Height: h, } - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) if err := ResolveStaleImages(); err != nil { t.Fatal(err) } @@ -538,7 +538,7 @@ func TestDrawTrianglesAndReplacePixels(t *testing.T) { Width: 2, Height: 1, } - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) img1.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 0, 0, 2, 1) if err := ResolveStaleImages(); err != nil { @@ -581,8 +581,8 @@ func TestDispose(t *testing.T) { Width: 1, Height: 1, } - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) img1.Dispose() if err := ResolveStaleImages(); err != nil { @@ -696,7 +696,7 @@ func TestReplacePixelsOnly(t *testing.T) { Width: 1, Height: 1, } - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) img0.ReplacePixels([]byte{5, 6, 7, 8}, 0, 0, 1, 1) // BasePixelsForTesting is available without GPU accessing. @@ -756,7 +756,7 @@ func TestReadPixelsFromVolatileImage(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) // Read the pixels. If the implementation is correct, dst tries to read its pixels from GPU due to being // stale. @@ -783,7 +783,7 @@ func TestAllowReplacePixelsAfterDrawTriangles(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) dst.ReplacePixels(make([]byte, 4*w*h), 0, 0, w, h) // ReplacePixels for a whole image doesn't panic. } @@ -807,7 +807,7 @@ func TestDisallowReplacePixelsForPartAfterDrawTriangles(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) dst.ReplacePixels(make([]byte, 4), 0, 0, 1, 1) } @@ -884,7 +884,7 @@ func TestMutateSlices(t *testing.T) { Width: w, Height: h, } - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) for i := range vs { vs[i] = 0 } diff --git a/internal/restorable/shader_test.go b/internal/restorable/shader_test.go index aabf8553c..28baab82c 100644 --- a/internal/restorable/shader_test.go +++ b/internal/restorable/shader_test.go @@ -48,7 +48,7 @@ func clearImage(img *Image, w, h int) { Width: float32(w), Height: float32(h), } - img.DrawTriangles([graphics.ShaderImageNum]*Image{emptyImage}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil) + img.DrawTriangles([graphics.ShaderImageNum]*Image{emptyImage}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, nil, nil, false) } func TestShader(t *testing.T) { @@ -63,7 +63,7 @@ func TestShader(t *testing.T) { Width: 1, Height: 1, } - img.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + img.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) if err := ResolveStaleImages(); err != nil { t.Fatal(err) @@ -99,7 +99,7 @@ func TestShaderChain(t *testing.T) { Width: 1, Height: 1, } - imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + imgs[i+1].DrawTriangles([graphics.ShaderImageNum]*Image{imgs[i]}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) } if err := ResolveStaleImages(); err != nil { @@ -138,7 +138,7 @@ func TestShaderMultipleSources(t *testing.T) { Width: 1, Height: 1, } - dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) // Clear one of the sources after DrawTriangles. dst should not be affected. clearImage(srcs[0], 1, 1) @@ -180,7 +180,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) { Width: 1, Height: 1, } - dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) // Clear one of the sources after DrawTriangles. dst should not be affected. clearImage(srcs[0], 3, 1) @@ -211,7 +211,7 @@ func TestShaderDispose(t *testing.T) { Width: 1, Height: 1, } - img.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil) + img.DrawTriangles([graphics.ShaderImageNum]*Image{}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, dr, driver.Region{}, s, nil, false) // Dispose the shader. This should invalidates all the images using this shader i.e., all the images become // stale. diff --git a/vector/internal/triangulate/math.go b/vector/internal/triangulate/math.go deleted file mode 100644 index b5287df39..000000000 --- a/vector/internal/triangulate/math.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2019 The Ebiten Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package triangulate - -import ( - "math" -) - -var nan32 = float32(math.NaN()) - -type Point struct { - X float32 - Y float32 -} diff --git a/vector/internal/triangulate/triangulate.go b/vector/internal/triangulate/triangulate.go deleted file mode 100644 index 5c98d9c29..000000000 --- a/vector/internal/triangulate/triangulate.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2019 The Ebiten Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package triangulate - -import ( - "fmt" -) - -func cross(v0x, v0y, v1x, v1y float32) float32 { - return v0x*v1y - v0y*v1x -} - -func triangleCross(pt0, pt1, pt2 Point) float32 { - return cross(pt1.X-pt0.X, pt1.Y-pt0.Y, pt2.X-pt1.X, pt2.Y-pt1.Y) -} - -func adjacentIndices(indices []uint16, idx int) (uint16, uint16, uint16) { - return indices[(idx+len(indices)-1)%len(indices)], indices[idx], indices[(idx+1)%len(indices)] -} - -func InTriangle(pt, pt0, pt1, pt2 Point) bool { - if pt.X <= pt0.X && pt.X <= pt1.X && pt.X <= pt2.X { - return false - } - if pt.X >= pt0.X && pt.X >= pt1.X && pt.X >= pt2.X { - return false - } - if pt.Y <= pt0.Y && pt.Y <= pt1.Y && pt.Y <= pt2.Y { - return false - } - if pt.Y >= pt0.Y && pt.Y >= pt1.Y && pt.Y >= pt2.Y { - return false - } - c0 := cross(pt.X-pt0.X, pt.Y-pt0.Y, pt1.X-pt0.X, pt1.Y-pt0.Y) - c1 := cross(pt.X-pt1.X, pt.Y-pt1.Y, pt2.X-pt1.X, pt2.Y-pt1.Y) - c2 := cross(pt.X-pt2.X, pt.Y-pt2.Y, pt0.X-pt2.X, pt0.Y-pt2.Y) - return (c0 <= 0 && c1 <= 0 && c2 <= 0) || (c0 >= 0 && c1 >= 0 && c2 >= 0) -} - -// Triangulate triangulates the region surrounded by the points pts and returnes the point indices. -func Triangulate(pts []Point) []uint16 { - if len(pts) < 3 { - return nil - } - - var currentIndices []uint16 - - // Split pts into the two point groups if there are the same points. - for i := range pts { - for j := 0; j < i; j++ { - if pts[i] == pts[j] { - is0 := Triangulate(pts[j:i]) - for idx := range is0 { - is0[idx] += uint16(j) - } - is1 := Triangulate(append(pts[i:], pts[:j]...)) - for idx := range is1 { - is1[idx] = uint16((int(is1[idx]) + i) % len(pts)) - } - return append(is0, is1...) - } - } - currentIndices = append(currentIndices, uint16(i)) - } - - var indices []uint16 - - // Triangulation by Ear Clipping. - // https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf - // TODO: Adopt a more efficient algorithm. - for len(currentIndices) >= 3 { - // Calculate cross-products and remove unneeded vertices. - cs := make([]float32, len(currentIndices)) - idxToRemove := -1 - - // Determine the direction of the polygon from the upper-left point. - var upperLeft int - for i := range currentIndices { - i0, i1, i2 := adjacentIndices(currentIndices, i) - pt0 := pts[i0] - pt1 := pts[i1] - pt2 := pts[i2] - c := triangleCross(pt0, pt1, pt2) - if c == 0 { - idxToRemove = i - break - } - cs[i] = c - - if pts[currentIndices[upperLeft]].X > pts[currentIndices[i]].X { - upperLeft = i - } else if pts[currentIndices[upperLeft]].X == pts[currentIndices[i]].X && - pts[currentIndices[upperLeft]].Y > pts[currentIndices[i]].Y { - upperLeft = i - } - } - if idxToRemove != -1 { - currentIndices = append(currentIndices[:idxToRemove], currentIndices[idxToRemove+1:]...) - continue - } - - clockwise := cs[upperLeft] < 0 - - idx := -1 - index: - for i := range currentIndices { - c := cs[i] - if c == 0 { - panic("math: cross value must not be 0") - } - if c < 0 && !clockwise || c > 0 && clockwise { - // The angle is more than 180 degrees. This is not an ear. - continue - } - - i0, i1, i2 := adjacentIndices(currentIndices, i) - pt0 := pts[i0] - pt1 := pts[i1] - pt2 := pts[i2] - for _, j := range currentIndices { - if j == i0 || j == i1 || j == i2 { - continue - } - if InTriangle(pts[j], pt0, pt1, pt2) { - // If the triangle includes another point, the triangle is not an ear. - continue index - } - } - // The angle is less than 180 degrees. This is an ear. - idx = i - break - } - if idx < 0 { - // TODO: This happens when there is self-crossing. - panic(fmt.Sprintf("math: there is no ear in the polygon: %v", pts)) - } - i0, i1, i2 := adjacentIndices(currentIndices, idx) - indices = append(indices, i0, i1, i2) - currentIndices = append(currentIndices[:idx], currentIndices[idx+1:]...) - } - return indices -} diff --git a/vector/internal/triangulate/triangulate_test.go b/vector/internal/triangulate/triangulate_test.go deleted file mode 100644 index b139efacf..000000000 --- a/vector/internal/triangulate/triangulate_test.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2019 The Ebiten Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package triangulate_test - -import ( - "math" - "reflect" - "testing" - - . "github.com/hajimehoshi/ebiten/v2/vector/internal/triangulate" -) - -func TestIsInTriangle(t *testing.T) { - tests := []struct { - Tri []Point - Pt Point - Out bool - }{ - { - Tri: []Point{ - {0, 0}, - {0, 10}, - {10, 10}, - }, - Pt: Point{1, 9}, - Out: true, - }, - { - Tri: []Point{ - {0, 0}, - {0, 10}, - {10, 10}, - }, - Pt: Point{8, 9}, - Out: true, - }, - { - Tri: []Point{ - {0, 0}, - {0, 10}, - {10, 10}, - }, - Pt: Point{10, 9}, - Out: false, - }, - { - Tri: []Point{ - {3, 5}, - {2, 7}, - {7, 7}, - }, - Pt: Point{3, 6}, - Out: true, - }, - { - Tri: []Point{ - {3, 5}, - {2, 7}, - {7, 7}, - }, - Pt: Point{7, 6}, - Out: false, - }, - } - for _, tc := range tests { - got := InTriangle(tc.Pt, tc.Tri[0], tc.Tri[1], tc.Tri[2]) - want := tc.Out - if got != want { - t.Errorf("InTriangle(%v, %v, %v, %v): got: %t, want: %t", tc.Pt, tc.Tri[0], tc.Tri[1], tc.Tri[2], got, want) - } - } -} - -func TestTriangulate(t *testing.T) { - tests := []struct { - In []Point - Out []uint16 - }{ - { - In: []Point{}, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - }, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - {0, 1}, - }, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - {0, 0}, - {1, 1}, - }, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - {0.5, 0.5}, - {1, 1}, - }, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - {0.5, 0.5}, - {1.5, 1.5}, - {1, 1}, - }, - Out: nil, - }, - { - In: []Point{ - {0, 0}, - {0, 1}, - {1, 1}, - }, - Out: []uint16{2, 0, 1}, - }, - { - In: []Point{ - {0, 0}, - {1, 1}, - {0, 1}, - }, - Out: []uint16{2, 0, 1}, - }, - { - In: []Point{ - {0, 0}, - {1, 1}, - {0, 1}, - {0, 0.5}, - }, - Out: []uint16{2, 0, 1}, - }, - { - In: []Point{ - {0, 0}, - {0, 1}, - {1, 1}, - {1, 0}, - }, - Out: []uint16{3, 0, 1, 3, 1, 2}, - }, - { - In: []Point{ - {2, 2}, - {2, 7}, - {7, 7}, - {7, 6}, - {3, 6}, - {3, 5}, - }, - Out: []uint16{5, 0, 1, 1, 2, 3, 1, 3, 4, 5, 1, 4}, - }, - { - In: []Point{ - {2, 2}, - {2, 7}, - {7, 7}, - {7, 6}, - {3, 6}, - {3, 5}, - {7, 5}, - {7, 4}, - {3, 4}, - {3, 3}, - }, - Out: []uint16{9, 0, 1, 1, 2, 3, 1, 3, 4, 9, 1, 4, 8, 5, 6, 8, 6, 7}, - }, - { - In: []Point{ - {0, 0}, - {0, 5}, - {2, 5}, - {3, 3}, - {2, 2}, - {3, 1}, - {2, 0}, - }, - Out: []uint16{6, 0, 1, 6, 1, 2, 6, 2, 3, 4, 5, 6, 6, 3, 4}, - }, - { - In: []Point{ - {0, 0}, - {0, 5}, - {2, 5}, - {2, 5}, - {3, 3}, - {2, 2}, - {2, 2}, - {3, 1}, - {2, 0}, - }, - Out: []uint16{6, 7, 8, 6, 8, 0, 4, 0, 1, 4, 1, 3}, - }, - { - In: []Point{ - {0, 0}, - {0, 1}, - {1, 1}, - {1, 0}, - {2, 0}, - }, - Out: []uint16{3, 0, 1, 3, 1, 2}, - }, - { - In: []Point{ - {2, 0}, - {0, 0}, - {1, 0}, - {1, 1}, - {2, 1}, - }, - Out: []uint16{4, 0, 2, 4, 2, 3}, - }, - { - In: []Point{ - {2, 0}, - {2, 1}, - {1, 1}, - {1, 0}, - {0, 0}, - }, - Out: []uint16{3, 0, 1, 3, 1, 2}, - }, - { - // Butterfly - In: []Point{ - {0, 2}, - {1, 1}, - {2, 2}, - {2, 0}, - {1, 1}, - {0, 0}, - }, - Out: []uint16{3, 1, 2, 0, 4, 5}, - }, - { - In: []Point{ - {0, 6}, - {0, 9}, - {6, 6}, - {6, 3}, - {9, 3}, - {8, 0}, - {6, 0}, - {6, 3}, - }, - Out: []uint16{6, 3, 4, 6, 4, 5, 2, 7, 0, 2, 0, 1}, - }, - { - In: []Point{ - {0, 4}, - {0, 6}, - {2, 6}, - {2, 5}, - {3, 5}, - {3, 4}, - {4, 4}, - {4, 2}, - {6, 2}, - {6, 1}, - {5, 1}, - {5, 0}, - {4, 0}, - {4, 2}, - {2, 2}, - {2, 3}, - {1, 3}, - {1, 4}, - }, - Out: []uint16{7, 8, 9, 7, 9, 10, 12, 7, 10, 12, 10, 11, 6, 13, 14, 6, 14, 15, 6, 15, 16, 6, 16, 17, 5, 0, 1, 1, 2, 3, 5, 1, 3, 5, 3, 4}, - }, - } - for _, tc := range tests { - got := Triangulate(tc.In) - want := tc.Out - if !reflect.DeepEqual(got, want) { - t.Errorf("Triangulate(%v): got: %v, want: %v", tc.In, got, want) - } - } -} - -var benchmarkPath []Point - -func init() { - const ( - w = 640 - h = 480 - ) - var p []Point - for i := 0; i < w; i++ { - x := float32(i) - y := h/2 + 80*float32(math.Sin(2*math.Pi*float64(i)/40)) - p = append(p, Point{X: x, Y: y}) - } - p = append(p, Point{w, h}, Point{0, h}) - benchmarkPath = p -} - -func BenchmarkTriangulate(b *testing.B) { - for n := 0; n < b.N; n++ { - Triangulate(benchmarkPath) - } -} diff --git a/vector/path.go b/vector/path.go index ef8fe05fb..dfd4024e7 100644 --- a/vector/path.go +++ b/vector/path.go @@ -18,33 +18,26 @@ package vector import ( - "image" - "image/color" "math" "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/vector/internal/triangulate" ) -var ( - emptyImage = ebiten.NewImage(3, 3) - emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) -) - -func init() { - emptyImage.Fill(color.White) +type point struct { + x float32 + y float32 } // Path represents a collection of path segments. type Path struct { - segs [][]triangulate.Point - cur triangulate.Point + segs [][]point + cur point } // MoveTo skips the current position of the path to the given position (x, y) without adding any strokes. func (p *Path) MoveTo(x, y float32) { - p.cur = triangulate.Point{X: x, Y: y} - p.segs = append(p.segs, []triangulate.Point{p.cur}) + p.cur = point{x: x, y: y} + p.segs = append(p.segs, []point{p.cur}) } // LineTo adds a line segument to the path, which starts from the current position and ends to the given position (x, y). @@ -52,10 +45,10 @@ func (p *Path) MoveTo(x, y float32) { // LineTo updates the current position to (x, y). func (p *Path) LineTo(x, y float32) { if len(p.segs) == 0 { - p.segs = append(p.segs, []triangulate.Point{p.cur}) + p.segs = append(p.segs, []point{p.cur}) } - p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], triangulate.Point{X: x, Y: y}) - p.cur = triangulate.Point{X: x, Y: y} + p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], point{x: x, y: y}) + p.cur = point{x: x, y: y} } // nseg returns a number of segments based on the given two points (x0, y0) and (x1, y1). @@ -78,69 +71,61 @@ func nseg(x0, y0, x1, y1 float32) int { // QuadTo adds a quadratic Bézier curve to the path. func (p *Path) QuadTo(cpx, cpy, x, y float32) { + // TODO: Split more appropriate number of segments. c := p.cur - num := nseg(c.X, c.Y, x, y) + num := nseg(c.x, c.y, x, y) for t := float32(0.0); t <= 1; t += 1.0 / float32(num) { - xf := (1-t)*(1-t)*c.X + 2*t*(1-t)*cpx + t*t*x - yf := (1-t)*(1-t)*c.Y + 2*t*(1-t)*cpy + t*t*y + xf := (1-t)*(1-t)*c.x + 2*t*(1-t)*cpx + t*t*x + yf := (1-t)*(1-t)*c.y + 2*t*(1-t)*cpy + t*t*y p.LineTo(xf, yf) } } // CubicTo adds a cubic Bézier curve to the path. func (p *Path) CubicTo(cp0x, cp0y, cp1x, cp1y, x, y float32) { + // TODO: Split more appropriate number of segments. c := p.cur - num := nseg(c.X, c.Y, x, y) + num := nseg(c.x, c.y, x, y) for t := float32(0.0); t <= 1; t += 1.0 / float32(num) { - xf := (1-t)*(1-t)*(1-t)*c.X + 3*(1-t)*(1-t)*t*cp0x + 3*(1-t)*t*t*cp1x + t*t*t*x - yf := (1-t)*(1-t)*(1-t)*c.Y + 3*(1-t)*(1-t)*t*cp0y + 3*(1-t)*t*t*cp1y + t*t*t*y + xf := (1-t)*(1-t)*(1-t)*c.x + 3*(1-t)*(1-t)*t*cp0x + 3*(1-t)*t*t*cp1x + t*t*t*x + yf := (1-t)*(1-t)*(1-t)*c.y + 3*(1-t)*(1-t)*t*cp0y + 3*(1-t)*t*t*cp1y + t*t*t*y p.LineTo(xf, yf) } } -// FillOptions represents options to fill a path. -type FillOptions struct { - // Color is a color to fill with. - Color color.Color -} - -// Fill fills the region of the path with the given options op. -func (p *Path) Fill(dst *ebiten.Image, op *FillOptions) { - var vertices []ebiten.Vertex - var indices []uint16 - - if op.Color == nil { - return - } - - r, g, b, a := op.Color.RGBA() - var rf, gf, bf, af float32 - if a == 0 { - return - } - rf = float32(r) / float32(a) - gf = float32(g) / float32(a) - bf = float32(b) / float32(a) - af = float32(a) / 0xffff +// AppendVerticesAndIndices appends vertices and indices for this path and returns them. +// AppendVerticesAndIndices works in a similar way to the built-in append function. +// If the arguments are nils, AppendVerticesAndIndices returns new slices. +// +// The returned vertice's SrcX and SrcY are 0, and ColorR, ColorG, ColorB, and ColorA are 1. +// +// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with EvenOdd option +// in order to render a complex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon. +func (p *Path) AppendVerticesAndIndices(vertices []ebiten.Vertex, indices []uint16) ([]ebiten.Vertex, []uint16) { + // TODO: Add tests. var base uint16 for _, seg := range p.segs { - for _, pt := range seg { + if len(seg) < 3 { + continue + } + for i, pt := range seg { vertices = append(vertices, ebiten.Vertex{ - DstX: pt.X, - DstY: pt.Y, + DstX: pt.x, + DstY: pt.y, SrcX: 0, SrcY: 0, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, }) - } - for _, idx := range triangulate.Triangulate(seg) { - indices = append(indices, idx+base) + if i < 2 { + continue + } + indices = append(indices, base, base+uint16(i-1), base+uint16(i)) } base += uint16(len(seg)) } - dst.DrawTriangles(vertices, indices, emptySubImage, nil) + return vertices, indices }