From 3ca618429429c3a2bac0a25996098a3904de4f94 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Nov 2023 09:18:08 +0900 Subject: [PATCH] ebiten: add a new FillRule: NonZero Closes #2782 --- colorm/draw.go | 2 +- examples/vector/main.go | 11 +- image.go | 8 +- image_test.go | 183 ++++++++++++++++++ internal/graphicscommand/command.go | 2 +- .../directx/graphics11_windows.go | 38 +++- .../directx/graphics12_windows.go | 2 +- .../directx/graphics_windows.go | 5 +- .../directx/pipeline12_windows.go | 31 ++- internal/graphicsdriver/graphics.go | 3 + .../graphicsdriver/metal/graphics_darwin.go | 90 ++++++--- .../graphicsdriver/metal/shader_darwin.go | 6 +- internal/graphicsdriver/opengl/gl/const.go | 5 + internal/graphicsdriver/opengl/gl/debug.go | 8 +- .../graphicsdriver/opengl/gl/default_cgo.go | 14 +- .../graphicsdriver/opengl/gl/default_js.go | 8 +- .../opengl/gl/default_purego.go | 8 +- internal/graphicsdriver/opengl/gl/gomobile.go | 4 +- .../graphicsdriver/opengl/gl/interface.go | 2 +- internal/graphicsdriver/opengl/graphics.go | 21 +- vector/path.go | 4 +- 21 files changed, 363 insertions(+), 92 deletions(-) diff --git a/colorm/draw.go b/colorm/draw.go index e51b7052c..56408fbb6 100644 --- a/colorm/draw.go +++ b/colorm/draw.go @@ -72,7 +72,7 @@ type DrawTrianglesOptions struct { // FillRule indicates the rule how an overlapped region is rendered. // - // The rule EvenOdd is useful when you want to render a complex polygon. + // The rules NonZero and EvenOdd are 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. // diff --git a/examples/vector/main.go b/examples/vector/main.go index f8236ea64..017a666fc 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -141,7 +141,10 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { op := &ebiten.DrawTrianglesOptions{} op.AntiAlias = aa if !line { - op.FillRule = ebiten.EvenOdd + // ebiten.EvenOdd is also fine here. + // NonZero and EvenOdd differ when rendering a complex polygons with self-intersections and/or holes. + // See https://en.wikipedia.org/wiki/Nonzero-rule and https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule . + op.FillRule = ebiten.NonZero } screen.DrawTriangles(vs, is, whiteSubImage, op) } @@ -197,7 +200,7 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, aa bool, line bool) { op := &ebiten.DrawTrianglesOptions{} op.AntiAlias = aa if !line { - op.FillRule = ebiten.EvenOdd + op.FillRule = ebiten.NonZero } screen.DrawTriangles(vs, is, whiteSubImage, op) } @@ -241,7 +244,7 @@ func drawArc(screen *ebiten.Image, count int, aa bool, line bool) { op := &ebiten.DrawTrianglesOptions{} op.AntiAlias = aa if !line { - op.FillRule = ebiten.EvenOdd + op.FillRule = ebiten.NonZero } screen.DrawTriangles(vs, is, whiteSubImage, op) } @@ -298,7 +301,7 @@ func drawWave(screen *ebiten.Image, counter int, aa bool, line bool) { op := &ebiten.DrawTrianglesOptions{} op.AntiAlias = aa if !line { - op.FillRule = ebiten.EvenOdd + op.FillRule = ebiten.NonZero } screen.DrawTriangles(vs, is, whiteSubImage, op) } diff --git a/image.go b/image.go index 1d83c7e9c..7c4d63721 100644 --- a/image.go +++ b/image.go @@ -314,6 +314,10 @@ const ( // FillAll indicates all the triangles are rendered regardless of overlaps. FillAll FillRule = FillRule(graphicsdriver.FillAll) + // NonZero means that triangles are rendered based on the non-zero rule. + // If and only if the number of overlaps is not 0, the region is rendered. + NonZero FillRule = FillRule(graphicsdriver.NonZero) + // EvenOdd means that triangles are rendered based on the even-odd rule. // If and only if the number of overlaps is odd, the region is rendered. EvenOdd FillRule = FillRule(graphicsdriver.EvenOdd) @@ -367,7 +371,7 @@ type DrawTrianglesOptions struct { // FillRule indicates the rule how an overlapped region is rendered. // - // The rule EvenOdd is useful when you want to render a complex polygon. + // The rules NonZero and EvenOdd are 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. // @@ -543,7 +547,7 @@ type DrawTrianglesShaderOptions struct { // FillRule indicates the rule how an overlapped region is rendered. // - // The rule EvenOdd is useful when you want to render a complex polygon. + // The rules NonZero and EvenOdd are 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. // diff --git a/image_test.go b/image_test.go index c8e2493fc..b5be81819 100644 --- a/image_test.go +++ b/image_test.go @@ -2793,6 +2793,189 @@ func TestImageEvenOdd(t *testing.T) { } } +func TestImageFillRule(t *testing.T) { + for _, fillRule := range []ebiten.FillRule{ebiten.FillAll, ebiten.NonZero, ebiten.EvenOdd} { + fillRule := fillRule + var name string + switch fillRule { + case ebiten.FillAll: + name = "FillAll" + case ebiten.NonZero: + name = "NonZero" + case ebiten.EvenOdd: + name = "EvenOdd" + } + t.Run(name, func(t *testing.T) { + whiteImage := ebiten.NewImage(3, 3) + whiteImage.Fill(color.White) + emptySubImage := whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) + + // The outside rectangle (clockwise) + vs0 := []ebiten.Vertex{ + { + DstX: 1, DstY: 1, SrcX: 1, SrcY: 1, + ColorR: 1, ColorG: 0, ColorB: 0, ColorA: 1, + }, + { + DstX: 15, DstY: 1, SrcX: 1, SrcY: 1, + ColorR: 1, ColorG: 0, ColorB: 0, ColorA: 1, + }, + { + DstX: 15, DstY: 15, SrcX: 1, SrcY: 1, + ColorR: 1, ColorG: 0, ColorB: 0, ColorA: 1, + }, + { + DstX: 1, DstY: 15, SrcX: 1, SrcY: 1, + ColorR: 1, ColorG: 0, ColorB: 0, ColorA: 1, + }, + } + is0 := []uint16{0, 1, 2, 2, 3, 0} + + // An inside rectangle (clockwise) + vs1 := []ebiten.Vertex{ + { + DstX: 2, DstY: 2, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 1, ColorB: 0, ColorA: 1, + }, + { + DstX: 7, DstY: 2, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 1, ColorB: 0, ColorA: 1, + }, + { + DstX: 7, DstY: 7, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 1, ColorB: 0, ColorA: 1, + }, + { + DstX: 2, DstY: 7, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 1, ColorB: 0, ColorA: 1, + }, + } + is1 := []uint16{4, 5, 6, 6, 7, 4} + + // An inside rectangle (counter-clockwise) + vs2 := []ebiten.Vertex{ + { + DstX: 9, DstY: 9, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 0, ColorB: 1, ColorA: 1, + }, + { + DstX: 14, DstY: 9, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 0, ColorB: 1, ColorA: 1, + }, + { + DstX: 14, DstY: 14, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 0, ColorB: 1, ColorA: 1, + }, + { + DstX: 9, DstY: 14, SrcX: 1, SrcY: 1, + ColorR: 0, ColorG: 0, ColorB: 1, ColorA: 1, + }, + } + is2 := []uint16{8, 11, 10, 10, 9, 8} + + // Draw all the vertices once. The even-odd rule is applied for all the vertices once. + dst := ebiten.NewImage(16, 16) + op := &ebiten.DrawTrianglesOptions{ + FillRule: fillRule, + } + dst.DrawTriangles(append(append(vs0, vs1...), vs2...), append(append(is0, is1...), is2...), emptySubImage, op) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + got := dst.At(i, j) + var want color.RGBA + switch { + case 2 <= i && i < 7 && 2 <= j && j < 7: + if fillRule != ebiten.EvenOdd { + want = color.RGBA{G: 0xff, A: 0xff} + } + case 9 <= i && i < 14 && 9 <= j && j < 14: + if fillRule == ebiten.FillAll { + want = color.RGBA{B: 0xff, A: 0xff} + } + case 1 <= i && i < 15 && 1 <= j && j < 15: + want = color.RGBA{R: 0xff, A: 0xff} + } + if got != want { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + + // Do the same thing but with a little shift. This confirms that the underlying stencil buffer is cleared correctly. + for i := range vs0 { + vs0[i].DstX++ + vs0[i].DstY++ + } + for i := range vs1 { + vs1[i].DstX++ + vs1[i].DstY++ + } + for i := range vs2 { + vs2[i].DstX++ + vs2[i].DstY++ + } + dst.Clear() + dst.DrawTriangles(append(append(vs0, vs1...), vs2...), append(append(is0, is1...), is2...), emptySubImage, op) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + got := dst.At(i, j) + var want color.RGBA + switch { + case 3 <= i && i < 8 && 3 <= j && j < 8: + if fillRule != ebiten.EvenOdd { + want = color.RGBA{G: 0xff, A: 0xff} + } + case 10 <= i && i < 15 && 10 <= j && j < 15: + if fillRule == ebiten.FillAll { + want = color.RGBA{B: 0xff, A: 0xff} + } + case 2 <= i && i < 16 && 2 <= j && j < 16: + want = color.RGBA{R: 0xff, A: 0xff} + } + if got != want { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + + // Do the same thing but with split DrawTriangle calls. This confirms that fill rules are applied for one call. + for i := range vs0 { + vs0[i].DstX-- + vs0[i].DstY-- + } + for i := range vs1 { + vs1[i].DstX-- + vs1[i].DstY-- + } + for i := range vs2 { + vs2[i].DstX-- + vs2[i].DstY-- + } + dst.Clear() + dst.DrawTriangles(vs0, []uint16{0, 1, 2, 2, 3, 0}, emptySubImage, op) + dst.DrawTriangles(vs1, []uint16{0, 1, 2, 2, 3, 0}, emptySubImage, op) + dst.DrawTriangles(vs2, []uint16{0, 3, 2, 2, 1, 0}, emptySubImage, op) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + got := dst.At(i, j) + var want color.RGBA + switch { + case 2 <= i && i < 7 && 2 <= j && j < 7: + want = color.RGBA{G: 0xff, A: 0xff} + case 9 <= i && i < 14 && 9 <= j && j < 14: + want = color.RGBA{B: 0xff, A: 0xff} + case 1 <= i && i < 15 && 1 <= j && j < 15: + want = color.RGBA{R: 0xff, A: 0xff} + } + if got != want { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + }) + } +} + // #1658 func BenchmarkColorMScale(b *testing.B) { r := rand.Float64 diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index d9a948d1e..46b077e14 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -166,7 +166,7 @@ func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs if c.fillRule != fillRule { return false } - if c.fillRule == graphicsdriver.EvenOdd && mightOverlapDstRegions(c.vertices, vertices) { + if c.fillRule != graphicsdriver.FillAll && mightOverlapDstRegions(c.vertices, vertices) { return false } return true diff --git a/internal/graphicsdriver/directx/graphics11_windows.go b/internal/graphicsdriver/directx/graphics11_windows.go index 92d85fee2..e1fbfe065 100644 --- a/internal/graphicsdriver/directx/graphics11_windows.go +++ b/internal/graphicsdriver/directx/graphics11_windows.go @@ -584,25 +584,39 @@ func (g *graphics11) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphic switch fillRule { case graphicsdriver.FillAll: g.deviceContext.DrawIndexed(uint32(dstRegion.IndexCount), uint32(indexOffset), 0) - case graphicsdriver.EvenOdd: - bs, err := g.blendState(blend, prepareStencil) + case graphicsdriver.NonZero: + bs, err := g.blendState(blend, incrementStencil) if err != nil { return err } g.deviceContext.OMSetBlendState(bs, nil, 0xffffffff) - dss, err := g.depthStencilState(prepareStencil) + dss, err := g.depthStencilState(incrementStencil) if err != nil { return err } g.deviceContext.OMSetDepthStencilState(dss, 0) g.deviceContext.DrawIndexed(uint32(dstRegion.IndexCount), uint32(indexOffset), 0) - - bs, err = g.blendState(blend, drawWithStencil) + case graphicsdriver.EvenOdd: + bs, err := g.blendState(blend, invertStencil) if err != nil { return err } g.deviceContext.OMSetBlendState(bs, nil, 0xffffffff) - dss, err = g.depthStencilState(drawWithStencil) + dss, err := g.depthStencilState(invertStencil) + if err != nil { + return err + } + g.deviceContext.OMSetDepthStencilState(dss, 0) + g.deviceContext.DrawIndexed(uint32(dstRegion.IndexCount), uint32(indexOffset), 0) + } + + if fillRule != graphicsdriver.FillAll { + bs, err := g.blendState(blend, drawWithStencil) + if err != nil { + return err + } + g.deviceContext.OMSetBlendState(bs, nil, 0xffffffff) + dss, err := g.depthStencilState(drawWithStencil) if err != nil { return err } @@ -627,9 +641,9 @@ func (g *graphics11) genNextShaderID() graphicsdriver.ShaderID { } func (g *graphics11) blendState(blend graphicsdriver.Blend, stencilMode stencilMode) (*_ID3D11BlendState, error) { - writeMask := uint8(_D3D11_COLOR_WRITE_ENABLE_ALL) - if stencilMode == prepareStencil { - writeMask = 0 + var writeMask uint8 + if stencilMode == noStencil || stencilMode == drawWithStencil { + writeMask = uint8(_D3D11_COLOR_WRITE_ENABLE_ALL) } key := blendStateKey{ @@ -693,7 +707,11 @@ func (g *graphics11) depthStencilState(mode stencilMode) (*_ID3D11DepthStencilSt }, } switch mode { - case prepareStencil: + case incrementStencil: + desc.StencilEnable = 1 + desc.FrontFace.StencilPassOp = _D3D11_STENCIL_OP_INCR + desc.BackFace.StencilPassOp = _D3D11_STENCIL_OP_DECR + case invertStencil: desc.StencilEnable = 1 desc.FrontFace.StencilPassOp = _D3D11_STENCIL_OP_INVERT desc.BackFace.StencilPassOp = _D3D11_STENCIL_OP_INVERT diff --git a/internal/graphicsdriver/directx/graphics12_windows.go b/internal/graphicsdriver/directx/graphics12_windows.go index f03f9571b..50f27cd15 100644 --- a/internal/graphicsdriver/directx/graphics12_windows.go +++ b/internal/graphicsdriver/directx/graphics12_windows.go @@ -1097,7 +1097,7 @@ func (g *graphics12) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics. // Release constant buffers when too many ones will be created. numPipelines := 1 - if fillRule == graphicsdriver.EvenOdd { + if fillRule != graphicsdriver.FillAll { numPipelines = 2 } if len(g.pipelineStates.constantBuffers[g.frameIndex])+numPipelines > numDescriptorsPerFrame { diff --git a/internal/graphicsdriver/directx/graphics_windows.go b/internal/graphicsdriver/directx/graphics_windows.go index 20347ed79..87f122c92 100644 --- a/internal/graphicsdriver/directx/graphics_windows.go +++ b/internal/graphicsdriver/directx/graphics_windows.go @@ -35,9 +35,10 @@ import ( type stencilMode int const ( - prepareStencil stencilMode = iota + noStencil stencilMode = iota + incrementStencil + invertStencil drawWithStencil - noStencil ) const frameCount = 2 diff --git a/internal/graphicsdriver/directx/pipeline12_windows.go b/internal/graphicsdriver/directx/pipeline12_windows.go index 49a012af7..c06b4d9d4 100644 --- a/internal/graphicsdriver/directx/pipeline12_windows.go +++ b/internal/graphicsdriver/directx/pipeline12_windows.go @@ -309,21 +309,31 @@ func (p *pipelineStates) drawTriangles(device *_ID3D12Device, commandList *_ID3D switch fillRule { case graphicsdriver.FillAll: commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0) - case graphicsdriver.EvenOdd: - s, err := shader.pipelineState(blend, prepareStencil, screen) + case graphicsdriver.NonZero: + s, err := shader.pipelineState(blend, incrementStencil, screen) if err != nil { return err } commandList.SetPipelineState(s) commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0) - - s, err = shader.pipelineState(blend, drawWithStencil, screen) + case graphicsdriver.EvenOdd: + s, err := shader.pipelineState(blend, invertStencil, screen) if err != nil { return err } commandList.SetPipelineState(s) commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0) } + + if fillRule != graphicsdriver.FillAll { + s, err := shader.pipelineState(blend, drawWithStencil, screen) + if err != nil { + return err + } + commandList.SetPipelineState(s) + commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0) + } + indexOffset += dstRegion.IndexCount } @@ -443,14 +453,21 @@ func (p *pipelineStates) newPipelineState(device *_ID3D12Device, vsh, psh *_ID3D StencilFunc: _D3D12_COMPARISON_FUNC_ALWAYS, }, } - writeMask := uint8(_D3D12_COLOR_WRITE_ENABLE_ALL) + + var writeMask uint8 + if stencilMode == noStencil || stencilMode == drawWithStencil { + writeMask = uint8(_D3D12_COLOR_WRITE_ENABLE_ALL) + } switch stencilMode { - case prepareStencil: + case incrementStencil: + depthStencilDesc.StencilEnable = 1 + depthStencilDesc.FrontFace.StencilPassOp = _D3D12_STENCIL_OP_INCR + depthStencilDesc.BackFace.StencilPassOp = _D3D12_STENCIL_OP_DECR + case invertStencil: depthStencilDesc.StencilEnable = 1 depthStencilDesc.FrontFace.StencilPassOp = _D3D12_STENCIL_OP_INVERT depthStencilDesc.BackFace.StencilPassOp = _D3D12_STENCIL_OP_INVERT - writeMask = 0 case drawWithStencil: depthStencilDesc.StencilEnable = 1 depthStencilDesc.FrontFace.StencilFunc = _D3D12_COMPARISON_FUNC_NOT_EQUAL diff --git a/internal/graphicsdriver/graphics.go b/internal/graphicsdriver/graphics.go index 7ece13b86..d78642d9f 100644 --- a/internal/graphicsdriver/graphics.go +++ b/internal/graphicsdriver/graphics.go @@ -31,6 +31,7 @@ type FillRule int const ( FillAll FillRule = iota + NonZero EvenOdd ) @@ -38,6 +39,8 @@ func (f FillRule) String() string { switch f { case FillAll: return "FillAll" + case NonZero: + return "NonZero" case EvenOdd: return "EvenOdd" default: diff --git a/internal/graphicsdriver/metal/graphics_darwin.go b/internal/graphicsdriver/metal/graphics_darwin.go index c5ea60f4b..6b0031102 100644 --- a/internal/graphicsdriver/metal/graphics_darwin.go +++ b/internal/graphicsdriver/metal/graphics_darwin.go @@ -70,9 +70,10 @@ type Graphics struct { type stencilMode int const ( - prepareStencil stencilMode = iota + noStencil stencilMode = iota + incrementStencil + invertStencil drawWithStencil - noStencil ) var ( @@ -412,7 +413,35 @@ func (g *Graphics) Initialize() error { } // The stencil reference value is always 0 (default). - g.dsss[prepareStencil] = g.view.getMTLDevice().MakeDepthStencilState(mtl.DepthStencilDescriptor{ + g.dsss[noStencil] = g.view.getMTLDevice().MakeDepthStencilState(mtl.DepthStencilDescriptor{ + BackFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationKeep, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + FrontFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationKeep, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + }) + g.dsss[incrementStencil] = g.view.getMTLDevice().MakeDepthStencilState(mtl.DepthStencilDescriptor{ + BackFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationDecrementWrap, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + FrontFaceStencil: mtl.StencilDescriptor{ + StencilFailureOperation: mtl.StencilOperationKeep, + DepthFailureOperation: mtl.StencilOperationKeep, + DepthStencilPassOperation: mtl.StencilOperationIncrementWrap, + StencilCompareFunction: mtl.CompareFunctionAlways, + }, + }) + g.dsss[invertStencil] = g.view.getMTLDevice().MakeDepthStencilState(mtl.DepthStencilDescriptor{ BackFaceStencil: mtl.StencilDescriptor{ StencilFailureOperation: mtl.StencilOperationKeep, DepthFailureOperation: mtl.StencilOperationKeep, @@ -440,20 +469,6 @@ func (g *Graphics) Initialize() error { StencilCompareFunction: mtl.CompareFunctionNotEqual, }, }) - g.dsss[noStencil] = g.view.getMTLDevice().MakeDepthStencilState(mtl.DepthStencilDescriptor{ - BackFaceStencil: mtl.StencilDescriptor{ - StencilFailureOperation: mtl.StencilOperationKeep, - DepthFailureOperation: mtl.StencilOperationKeep, - DepthStencilPassOperation: mtl.StencilOperationKeep, - StencilCompareFunction: mtl.CompareFunctionAlways, - }, - FrontFaceStencil: mtl.StencilDescriptor{ - StencilFailureOperation: mtl.StencilOperationKeep, - DepthFailureOperation: mtl.StencilOperationKeep, - DepthStencilPassOperation: mtl.StencilOperationKeep, - StencilCompareFunction: mtl.CompareFunctionAlways, - }, - }) g.cq = g.view.getMTLDevice().MakeCommandQueue() return nil @@ -472,7 +487,7 @@ func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs // When preparing a stencil buffer, flush the current render command encoder // to make sure the stencil buffer is cleared when loading. // TODO: What about clearing the stencil buffer by vertices? - if g.lastDst != dst || g.lastFillRule != fillRule || fillRule == graphicsdriver.EvenOdd { + if g.lastDst != dst || g.lastFillRule != fillRule || fillRule != graphicsdriver.FillAll { g.flushRenderCommandEncoderIfNeeded() } g.lastDst = dst @@ -498,7 +513,7 @@ func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs rpd.ColorAttachments[0].Texture = t rpd.ColorAttachments[0].ClearColor = mtl.ClearColor{} - if fillRule == graphicsdriver.EvenOdd { + if fillRule != graphicsdriver.FillAll { dst.ensureStencil() rpd.StencilAttachment.LoadAction = mtl.LoadActionClear rpd.StencilAttachment.StoreAction = mtl.StoreActionDontCare @@ -539,9 +554,10 @@ func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs } var ( - prepareStencilRpss mtl.RenderPipelineState - drawWithStencilRpss mtl.RenderPipelineState - noStencilRpss mtl.RenderPipelineState + noStencilRpss mtl.RenderPipelineState + incrementStencilRpss mtl.RenderPipelineState + invertStencilRpss mtl.RenderPipelineState + drawWithStencilRpss mtl.RenderPipelineState ) switch fillRule { case graphicsdriver.FillAll: @@ -550,14 +566,21 @@ func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs return err } noStencilRpss = s - case graphicsdriver.EvenOdd: - s, err := shader.RenderPipelineState(&g.view, blend, prepareStencil, dst.screen) + case graphicsdriver.NonZero: + s, err := shader.RenderPipelineState(&g.view, blend, incrementStencil, dst.screen) if err != nil { return err } - prepareStencilRpss = s - - s, err = shader.RenderPipelineState(&g.view, blend, drawWithStencil, dst.screen) + incrementStencilRpss = s + case graphicsdriver.EvenOdd: + s, err := shader.RenderPipelineState(&g.view, blend, invertStencil, dst.screen) + if err != nil { + return err + } + invertStencilRpss = s + } + if fillRule != graphicsdriver.FillAll { + s, err := shader.RenderPipelineState(&g.view, blend, drawWithStencil, dst.screen) if err != nil { return err } @@ -577,11 +600,16 @@ func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs g.rce.SetDepthStencilState(g.dsss[noStencil]) g.rce.SetRenderPipelineState(noStencilRpss) g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt32, g.ib, indexOffset*int(unsafe.Sizeof(uint32(0)))) - case graphicsdriver.EvenOdd: - g.rce.SetDepthStencilState(g.dsss[prepareStencil]) - g.rce.SetRenderPipelineState(prepareStencilRpss) + case graphicsdriver.NonZero: + g.rce.SetDepthStencilState(g.dsss[incrementStencil]) + g.rce.SetRenderPipelineState(incrementStencilRpss) g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt32, g.ib, indexOffset*int(unsafe.Sizeof(uint32(0)))) - + case graphicsdriver.EvenOdd: + g.rce.SetDepthStencilState(g.dsss[invertStencil]) + g.rce.SetRenderPipelineState(invertStencilRpss) + g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt32, g.ib, indexOffset*int(unsafe.Sizeof(uint32(0)))) + } + if fillRule != graphicsdriver.FillAll { g.rce.SetDepthStencilState(g.dsss[drawWithStencil]) g.rce.SetRenderPipelineState(drawWithStencilRpss) g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt32, g.ib, indexOffset*int(unsafe.Sizeof(uint32(0)))) diff --git a/internal/graphicsdriver/metal/shader_darwin.go b/internal/graphicsdriver/metal/shader_darwin.go index 1c7c95d3c..8bedb0784 100644 --- a/internal/graphicsdriver/metal/shader_darwin.go +++ b/internal/graphicsdriver/metal/shader_darwin.go @@ -119,10 +119,10 @@ func (s *Shader) RenderPipelineState(view *view, blend graphicsdriver.Blend, ste rpld.ColorAttachments[0].AlphaBlendOperation = blendOperationToMetalBlendOperation(blend.BlendOperationAlpha) rpld.ColorAttachments[0].RGBBlendOperation = blendOperationToMetalBlendOperation(blend.BlendOperationRGB) - if stencilMode == prepareStencil { - rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskNone - } else { + if stencilMode == noStencil || stencilMode == drawWithStencil { rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll + } else { + rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskNone } rps, err := view.getMTLDevice().MakeRenderPipelineState(rpld) diff --git a/internal/graphicsdriver/opengl/gl/const.go b/internal/graphicsdriver/opengl/gl/const.go index 01a37c11b..97d416ec1 100644 --- a/internal/graphicsdriver/opengl/gl/const.go +++ b/internal/graphicsdriver/opengl/gl/const.go @@ -17,10 +17,12 @@ package gl const ( ALWAYS = 0x0207 ARRAY_BUFFER = 0x8892 + BACK = 0x0405 BLEND = 0x0BE2 CLAMP_TO_EDGE = 0x812F COLOR_ATTACHMENT0 = 0x8CE0 COMPILE_STATUS = 0x8B81 + DECR_WRAP = 0x8508 DEPTH24_STENCIL8 = 0x88F0 DST_ALPHA = 0x0304 DST_COLOR = 0x0306 @@ -32,10 +34,13 @@ const ( FRAMEBUFFER = 0x8D40 FRAMEBUFFER_BINDING = 0x8CA6 FRAMEBUFFER_COMPLETE = 0x8CD5 + FRONT = 0x0404 + FRONT_AND_BACK = 0x0408 FUNC_ADD = 0x8006 FUNC_REVERSE_SUBTRACT = 0x800b FUNC_SUBTRACT = 0x800a HIGH_FLOAT = 0x8DF2 + INCR_WRAP = 0x8507 INFO_LOG_LENGTH = 0x8B84 INVERT = 0x150A KEEP = 0x1E00 diff --git a/internal/graphicsdriver/opengl/gl/debug.go b/internal/graphicsdriver/opengl/gl/debug.go index 29ea323a5..a978f8ee2 100644 --- a/internal/graphicsdriver/opengl/gl/debug.go +++ b/internal/graphicsdriver/opengl/gl/debug.go @@ -501,11 +501,11 @@ func (d *DebugContext) StencilFunc(arg0 uint32, arg1 int32, arg2 uint32) { } } -func (d *DebugContext) StencilOp(arg0 uint32, arg1 uint32, arg2 uint32) { - d.Context.StencilOp(arg0, arg1, arg2) - fmt.Fprintln(os.Stderr, "StencilOp") +func (d *DebugContext) StencilOpSeparate(arg0 uint32, arg1 uint32, arg2 uint32, arg3 uint32) { + d.Context.StencilOpSeparate(arg0, arg1, arg2, arg3) + fmt.Fprintln(os.Stderr, "StencilOpSeparate") if e := d.Context.GetError(); e != NO_ERROR { - panic(fmt.Sprintf("gl: GetError() returned %d at StencilOp", e)) + panic(fmt.Sprintf("gl: GetError() returned %d at StencilOpSeparate", e)) } } diff --git a/internal/graphicsdriver/opengl/gl/default_cgo.go b/internal/graphicsdriver/opengl/gl/default_cgo.go index b19ce7c7b..d0484e7bf 100644 --- a/internal/graphicsdriver/opengl/gl/default_cgo.go +++ b/internal/graphicsdriver/opengl/gl/default_cgo.go @@ -244,9 +244,9 @@ package gl // typedef void (*fn)(GLenum func, GLint ref, GLuint mask); // ((fn)(fnptr))(func, ref, mask); // } -// static void glowStencilOp(uintptr_t fnptr, GLenum fail, GLenum zfail, GLenum zpass) { -// typedef void (*fn)(GLenum fail, GLenum zfail, GLenum zpass); -// ((fn)(fnptr))(fail, zfail, zpass); +// static void glowStencilOpSeparate(uintptr_t fnptr, GLenum face, GLenum fail, GLenum zfail, GLenum zpass) { +// typedef void (*fn)(GLenum face, GLenum fail, GLenum zfail, GLenum zpass); +// ((fn)(fnptr))(face, fail, zfail, zpass); // } // static void glowTexImage2D(uintptr_t fnptr, GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels) { // typedef void (*fn)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels); @@ -384,7 +384,7 @@ type defaultContext struct { gpScissor C.uintptr_t gpShaderSource C.uintptr_t gpStencilFunc C.uintptr_t - gpStencilOp C.uintptr_t + gpStencilOpSeparate C.uintptr_t gpTexImage2D C.uintptr_t gpTexParameteri C.uintptr_t gpTexSubImage2D C.uintptr_t @@ -688,8 +688,8 @@ func (c *defaultContext) StencilFunc(xfunc uint32, ref int32, mask uint32) { C.glowStencilFunc(c.gpStencilFunc, C.GLenum(xfunc), C.GLint(ref), C.GLuint(mask)) } -func (c *defaultContext) StencilOp(fail uint32, zfail uint32, zpass uint32) { - C.glowStencilOp(c.gpStencilOp, C.GLenum(fail), C.GLenum(zfail), C.GLenum(zpass)) +func (c *defaultContext) StencilOpSeparate(face uint32, fail uint32, zfail uint32, zpass uint32) { + C.glowStencilOpSeparate(c.gpStencilOpSeparate, C.GLenum(face), C.GLenum(fail), C.GLenum(zfail), C.GLenum(zpass)) } func (c *defaultContext) TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) { @@ -840,7 +840,7 @@ func (c *defaultContext) LoadFunctions() error { c.gpScissor = C.uintptr_t(g.get("glScissor")) c.gpShaderSource = C.uintptr_t(g.get("glShaderSource")) c.gpStencilFunc = C.uintptr_t(g.get("glStencilFunc")) - c.gpStencilOp = C.uintptr_t(g.get("glStencilOp")) + c.gpStencilOpSeparate = C.uintptr_t(g.get("glStencilOpSeparate")) c.gpTexImage2D = C.uintptr_t(g.get("glTexImage2D")) c.gpTexParameteri = C.uintptr_t(g.get("glTexParameteri")) c.gpTexSubImage2D = C.uintptr_t(g.get("glTexSubImage2D")) diff --git a/internal/graphicsdriver/opengl/gl/default_js.go b/internal/graphicsdriver/opengl/gl/default_js.go index e5728265a..2665847ae 100644 --- a/internal/graphicsdriver/opengl/gl/default_js.go +++ b/internal/graphicsdriver/opengl/gl/default_js.go @@ -79,7 +79,7 @@ type defaultContext struct { fnShaderSource js.Value fnStencilFunc js.Value fnStencilMask js.Value - fnStencilOp js.Value + fnStencilOpSeparate js.Value fnTexImage2D js.Value fnTexSubImage2D js.Value fnTexParameteri js.Value @@ -210,7 +210,7 @@ func NewDefaultContext(v js.Value) (Context, error) { fnShaderSource: v.Get("shaderSource").Call("bind", v), fnStencilFunc: v.Get("stencilFunc").Call("bind", v), fnStencilMask: v.Get("stencilMask").Call("bind", v), - fnStencilOp: v.Get("stencilOp").Call("bind", v), + fnStencilOpSeparate: v.Get("stencilOpSeparate").Call("bind", v), fnTexImage2D: v.Get("texImage2D").Call("bind", v), fnTexSubImage2D: v.Get("texSubImage2D").Call("bind", v), fnTexParameteri: v.Get("texParameteri").Call("bind", v), @@ -533,8 +533,8 @@ func (c *defaultContext) StencilFunc(func_ uint32, ref int32, mask uint32) { c.fnStencilFunc.Invoke(func_, ref, mask) } -func (c *defaultContext) StencilOp(sfail, dpfail, dppass uint32) { - c.fnStencilOp.Invoke(sfail, dpfail, dppass) +func (c *defaultContext) StencilOpSeparate(face, sfail, dpfail, dppass uint32) { + c.fnStencilOpSeparate.Invoke(face, sfail, dpfail, dppass) } func (c *defaultContext) TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) { diff --git a/internal/graphicsdriver/opengl/gl/default_purego.go b/internal/graphicsdriver/opengl/gl/default_purego.go index a084b6499..b6a22daca 100644 --- a/internal/graphicsdriver/opengl/gl/default_purego.go +++ b/internal/graphicsdriver/opengl/gl/default_purego.go @@ -80,7 +80,7 @@ type defaultContext struct { gpScissor uintptr gpShaderSource uintptr gpStencilFunc uintptr - gpStencilOp uintptr + gpStencilOpSeparate uintptr gpTexImage2D uintptr gpTexParameteri uintptr gpTexSubImage2D uintptr @@ -384,8 +384,8 @@ func (c *defaultContext) StencilFunc(xfunc uint32, ref int32, mask uint32) { purego.SyscallN(c.gpStencilFunc, uintptr(xfunc), uintptr(ref), uintptr(mask)) } -func (c *defaultContext) StencilOp(fail uint32, zfail uint32, zpass uint32) { - purego.SyscallN(c.gpStencilOp, uintptr(fail), uintptr(zfail), uintptr(zpass)) +func (c *defaultContext) StencilOpSeparate(face uint32, fail uint32, zfail uint32, zpass uint32) { + purego.SyscallN(c.gpStencilOpSeparate, uintptr(face), uintptr(fail), uintptr(zfail), uintptr(zpass)) } func (c *defaultContext) TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) { @@ -536,7 +536,7 @@ func (c *defaultContext) LoadFunctions() error { c.gpScissor = g.get("glScissor") c.gpShaderSource = g.get("glShaderSource") c.gpStencilFunc = g.get("glStencilFunc") - c.gpStencilOp = g.get("glStencilOp") + c.gpStencilOpSeparate = g.get("glStencilOpSeparate") c.gpTexImage2D = g.get("glTexImage2D") c.gpTexParameteri = g.get("glTexParameteri") c.gpTexSubImage2D = g.get("glTexSubImage2D") diff --git a/internal/graphicsdriver/opengl/gl/gomobile.go b/internal/graphicsdriver/opengl/gl/gomobile.go index 87dd0c79f..6fd3f13b6 100644 --- a/internal/graphicsdriver/opengl/gl/gomobile.go +++ b/internal/graphicsdriver/opengl/gl/gomobile.go @@ -267,8 +267,8 @@ func (g *gomobileContext) StencilFunc(func_ uint32, ref int32, mask uint32) { g.ctx.StencilFunc(gl.Enum(func_), int(ref), mask) } -func (g *gomobileContext) StencilOp(sfail, dpfail, dppass uint32) { - g.ctx.StencilOp(gl.Enum(sfail), gl.Enum(dpfail), gl.Enum(dppass)) +func (g *gomobileContext) StencilOpSeparate(face, sfail, dpfail, dppass uint32) { + g.ctx.StencilOpSeparate(gl.Enum(face), gl.Enum(sfail), gl.Enum(dpfail), gl.Enum(dppass)) } func (g *gomobileContext) TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) { diff --git a/internal/graphicsdriver/opengl/gl/interface.go b/internal/graphicsdriver/opengl/gl/interface.go index 52bb8f224..471908b16 100644 --- a/internal/graphicsdriver/opengl/gl/interface.go +++ b/internal/graphicsdriver/opengl/gl/interface.go @@ -81,7 +81,7 @@ type Context interface { Scissor(x, y, width, height int32) ShaderSource(shader uint32, xstring string) StencilFunc(func_ uint32, ref int32, mask uint32) - StencilOp(sfail, dpfail, dppass uint32) + StencilOpSeparate(face, sfail, dpfail, dppass uint32) TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) TexParameteri(target uint32, pname uint32, param int32) TexSubImage2D(target uint32, level int32, xoffset int32, yoffset int32, width int32, height int32, format uint32, xtype uint32, pixels []byte) diff --git a/internal/graphicsdriver/opengl/graphics.go b/internal/graphicsdriver/opengl/graphics.go index 63971eb64..609146f56 100644 --- a/internal/graphicsdriver/opengl/graphics.go +++ b/internal/graphicsdriver/opengl/graphics.go @@ -243,7 +243,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics. } g.uniformVars = g.uniformVars[:0] - if fillRule == graphicsdriver.EvenOdd { + if fillRule != graphicsdriver.FillAll { if err := destination.ensureStencilBuffer(); err != nil { return err } @@ -257,23 +257,32 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics. int32(dstRegion.Region.Dx()), int32(dstRegion.Region.Dy()), ) - if fillRule == graphicsdriver.EvenOdd { + switch fillRule { + case graphicsdriver.NonZero: g.context.ctx.Clear(gl.STENCIL_BUFFER_BIT) g.context.ctx.StencilFunc(gl.ALWAYS, 0x00, 0xff) - g.context.ctx.StencilOp(gl.KEEP, gl.KEEP, gl.INVERT) + g.context.ctx.StencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.INCR_WRAP) + g.context.ctx.StencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.DECR_WRAP) + g.context.ctx.ColorMask(false, false, false, false) + g.context.ctx.DrawElements(gl.TRIANGLES, int32(dstRegion.IndexCount), gl.UNSIGNED_INT, indexOffset*int(unsafe.Sizeof(uint32(0)))) + case graphicsdriver.EvenOdd: + g.context.ctx.Clear(gl.STENCIL_BUFFER_BIT) + g.context.ctx.StencilFunc(gl.ALWAYS, 0x00, 0xff) + g.context.ctx.StencilOpSeparate(gl.FRONT_AND_BACK, gl.KEEP, gl.KEEP, gl.INVERT) g.context.ctx.ColorMask(false, false, false, false) g.context.ctx.DrawElements(gl.TRIANGLES, int32(dstRegion.IndexCount), gl.UNSIGNED_INT, indexOffset*int(unsafe.Sizeof(uint32(0)))) - + } + if fillRule != graphicsdriver.FillAll { g.context.ctx.StencilFunc(gl.NOTEQUAL, 0x00, 0xff) - g.context.ctx.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP) + g.context.ctx.StencilOpSeparate(gl.FRONT_AND_BACK, gl.KEEP, gl.KEEP, gl.KEEP) g.context.ctx.ColorMask(true, true, true, true) } g.context.ctx.DrawElements(gl.TRIANGLES, int32(dstRegion.IndexCount), gl.UNSIGNED_INT, indexOffset*int(unsafe.Sizeof(uint32(0)))) indexOffset += dstRegion.IndexCount } - if fillRule == graphicsdriver.EvenOdd { + if fillRule != graphicsdriver.FillAll { g.context.ctx.Disable(gl.STENCIL_TEST) } diff --git a/vector/path.go b/vector/path.go index 94e41c32e..0274349c8 100644 --- a/vector/path.go +++ b/vector/path.go @@ -396,7 +396,7 @@ func (p *Path) Close() { // // 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 the EvenOdd fill rule +// The returned values are intended to be passed to DrawTriangles or DrawTrianglesShader with the fill rule NonZero or EvenOdd // in order to render a complex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon. // // The returned vertices and indices should be rendered with a solid (non-transparent) color with the default Blend (source-over). @@ -480,7 +480,7 @@ type StrokeOptions struct { // 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 a solid (non-transparent) color -// with the FillAll fill rule (not the EvenOdd fill rule). +// with the FillAll fill rule (not NonZero or EvenOdd fill rule). func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indices []uint16, op *StrokeOptions) ([]ebiten.Vertex, []uint16) { if op == nil { return vertices, indices