vector: Add Fill by triangulation

Updates #845
This commit is contained in:
Hajime Hoshi 2019-12-27 11:26:16 +09:00
parent d6d1cbc01a
commit 18b267fd4f
6 changed files with 373 additions and 294 deletions

View File

@ -33,66 +33,22 @@ func drawEbitenText(screen *ebiten.Image) {
var path vector.Path
// E
path.MoveTo(60, 20)
path.LineTo(20, 20)
path.LineTo(20, 60)
path.LineTo(60, 60)
path.MoveTo(20, 40)
path.LineTo(60, 40)
path.MoveTo(20, 20)
path.LineTo(20, 70)
path.LineTo(70, 70)
path.LineTo(70, 60)
path.LineTo(30, 60)
path.LineTo(30, 50)
path.LineTo(70, 50)
path.LineTo(70, 40)
path.LineTo(30, 40)
path.LineTo(30, 30)
path.LineTo(70, 30)
path.LineTo(70, 20)
// B
path.MoveTo(110, 20)
path.LineTo(80, 20)
path.LineTo(80, 60)
path.LineTo(110, 60)
path.LineTo(120, 50)
path.LineTo(110, 40)
path.LineTo(120, 30)
path.LineTo(110, 20)
path.LineTo(80, 20)
path.MoveTo(80, 40)
path.LineTo(110, 40)
// TODO: Draw other letters like B, I, T, E, N
// I
path.MoveTo(140, 20)
path.LineTo(140, 60)
// T
path.MoveTo(160, 20)
path.LineTo(200, 20)
path.MoveTo(180, 20)
path.LineTo(180, 60)
// E
path.MoveTo(260, 20)
path.LineTo(220, 20)
path.LineTo(220, 60)
path.LineTo(260, 60)
path.MoveTo(220, 40)
path.LineTo(260, 40)
// N
path.MoveTo(280, 60)
path.LineTo(280, 20)
path.LineTo(320, 60)
path.LineTo(320, 20)
op := &vector.DrawPathOptions{}
op.LineWidth = 8
op.StrokeColor = color.RGBA{0xdb, 0x56, 0x20, 0xff}
path.Draw(screen, op)
}
func drawLines(screen *ebiten.Image) {
var path vector.Path
path.MoveTo(20, 80+float32(counter%320)/4)
path.LineTo(60, 120)
path.LineTo(20, 160-float32(counter%320)/4)
op := &vector.DrawPathOptions{}
op.LineWidth = 16
op.StrokeColor = color.White
path.Draw(screen, op)
path.Fill(screen, color.White)
}
var counter = 0
@ -104,7 +60,6 @@ func update(screen *ebiten.Image) error {
}
drawEbitenText(screen)
drawLines(screen)
return nil
}

View File

@ -18,39 +18,14 @@ import (
"math"
)
var nan32 = float32(math.NaN())
type Point struct {
X float32
Y float32
}
type Vec2 struct {
type Vector struct {
X float32
Y float32
}
func (p Vec2) Cross(other Vec2) float32 {
return p.X*other.Y - p.Y*other.X
}
type Segment struct {
P0 Point
P1 Point
}
func (s Segment) Translate(offset float32) Segment {
a := math.Atan2(float64(s.P1.Y-s.P0.Y), float64(s.P1.X-s.P0.X))
si, co := math.Sincos(a + math.Pi/2)
dx, dy := float32(co)*offset, float32(si)*offset
return Segment{
P0: Point{s.P0.X + dx, s.P0.Y + dy},
P1: Point{s.P1.X + dx, s.P1.Y + dy},
}
}
func (s Segment) IntersectionAsLines(other Segment) Point {
v1 := Vec2{other.P1.X - other.P0.X, other.P1.Y - other.P0.Y}
d0 := v1.Cross(Vec2{s.P0.X - other.P0.X, s.P0.Y - other.P0.Y})
d1 := -v1.Cross(Vec2{s.P1.X - other.P1.X, s.P1.Y - other.P1.Y})
t := d0 / (d0 + d1)
return Point{s.P0.X + (s.P1.X-s.P0.X)*t, s.P0.Y + (s.P1.Y-s.P0.Y)*t}
}

View File

@ -1,47 +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 math_test
import (
"testing"
. "github.com/hajimehoshi/ebiten/vector/internal/math"
)
func TestIntersectionAsLine(t *testing.T) {
cases := []struct {
S0 Segment
S1 Segment
Want Point
}{
{
S0: Segment{Point{0.5, 0}, Point{0.5, 0.5}},
S1: Segment{Point{1, 1}, Point{2, 1}},
Want: Point{0.5, 1},
},
{
S0: Segment{Point{0.5, 0}, Point{0.5, 1.5}},
S1: Segment{Point{1, 1}, Point{2, 1}},
Want: Point{0.5, 1},
},
}
for _, c := range cases {
got := c.S0.IntersectionAsLines(c.S1)
want := c.Want
if got != want {
t.Errorf("got: %v, want: %v", got, want)
}
}
}

View File

@ -0,0 +1,140 @@
// 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 math
import (
"fmt"
)
func cross(v0, v1 Vector) float32 {
return v0.X*v1.Y - v0.Y*v1.X
}
func triangleCross(pt0, pt1, pt2 Point) float32 {
return cross(Vector{pt1.X - pt0.X, pt1.Y - pt0.Y}, Vector{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 {
c0 := cross(Vector{pt.X - pt0.X, pt.Y - pt0.Y}, Vector{pt1.X - pt0.X, pt1.Y - pt0.Y})
c1 := cross(Vector{pt.X - pt1.X, pt.Y - pt1.Y}, Vector{pt2.X - pt1.X, pt2.Y - pt1.Y})
c2 := cross(Vector{pt.X - pt2.X, pt.Y - pt2.Y}, Vector{pt0.X - pt2.X, pt0.Y - pt2.Y})
return (c0 < 0 && c1 < 0 && c2 < 0) || (c0 > 0 && c1 > 0 && c2 > 0)
}
func Triangulate(pts []Point) []uint16 {
if len(pts) < 3 {
return nil
}
var currentIndices []uint16
// Remove duplicated points
dup:
for i := range pts {
for j := 0; j < i; j++ {
if pts[i] == pts[j] {
continue dup
}
}
currentIndices = append(currentIndices, uint16(i))
}
if len(currentIndices) < 3 {
return nil
}
// Determine the direction of the polygon from the upper-left point.
var upperLeft int
for _, i := range currentIndices {
if pts[upperLeft].X < pts[i].X {
upperLeft = int(i)
}
if pts[upperLeft].X == pts[i].X && pts[upperLeft].Y < pts[i].Y {
upperLeft = int(i)
}
}
i0, i1, i2 := adjacentIndices(currentIndices, upperLeft)
pt0 := pts[i0]
pt1 := pts[i1]
pt2 := pts[i2]
clockwise := triangleCross(pt0, pt1, pt2) < 0
var indices []uint16
// Triangulation by Ear Clipping.
// https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
for len(currentIndices) >= 3 {
// Calculate cross-products and remove unneeded vertices.
cs := make([]float32, len(currentIndices))
idx := -1
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 {
idx = i
break
}
cs[i] = c
}
if idx != -1 {
currentIndices = append(currentIndices[:idx], currentIndices[idx+1:]...)
continue
}
idx = -1
index:
for i := range currentIndices {
i0, i1, i2 := adjacentIndices(currentIndices, i)
pt0 := pts[i0]
pt1 := pts[i1]
pt2 := pts[i2]
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
}
for j := range currentIndices {
if l := len(currentIndices); j == (i+l-1)%l || j == i || j == (i+1)%l {
continue
}
if InTriangle(pts[currentIndices[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
}

View File

@ -0,0 +1,182 @@
// 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 math_test
import (
"reflect"
"testing"
. "github.com/hajimehoshi/ebiten/vector/internal/math"
)
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, 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},
{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},
},
}
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)
}
}
}

View File

@ -27,190 +27,64 @@ import (
var emptyImage *ebiten.Image
func init() {
const w, h = 16, 16
emptyImage, _ = ebiten.NewImage(w, h, ebiten.FilterDefault)
emptyImage, _ = ebiten.NewImage(1, 1, ebiten.FilterDefault)
emptyImage.Fill(color.White)
}
// Path represents a collection of path segments.
type Path struct {
segs [][]math.Segment
segs [][]math.Point
cur math.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 = math.Point{X: x, Y: y}
if len(p.segs) > 0 && len(p.segs[len(p.segs)-1]) == 0 {
return
}
p.segs = append(p.segs, []math.Segment{})
p.segs = append(p.segs, []math.Point{p.cur})
}
// LineTo adds a math.Segment to the path, which starts from the current position and ends to the given position (x, y).
// LineTo adds a line segument to the path, which starts from the current position and ends to the given position (x, y).
//
// 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, []math.Segment{})
p.segs = append(p.segs, []math.Point{{X: 0, Y: 0}})
}
p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], math.Segment{P0: p.cur, P1: math.Point{X: x, Y: y}})
p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], math.Point{X: x, Y: y})
p.cur = math.Point{X: x, Y: y}
}
func (p *Path) strokeVertices(lineWidth float32, clr color.Color) (vertices []ebiten.Vertex, indices []uint16) {
const miterLimit = 10
if len(p.segs) == 0 {
return nil, nil
}
func (p *Path) Fill(dst *ebiten.Image, clr color.Color) {
var vertices []ebiten.Vertex
var indices []uint16
r, g, b, a := clr.RGBA()
rf, gf, bf, af := float32(r)/0xffff, float32(g)/0xffff, float32(b)/0xffff, float32(a)/0xffff
for _, ss := range p.segs {
idx := uint16(len(vertices))
for i, s := range ss {
s0 := s.Translate(-lineWidth / 2)
s1 := s.Translate(lineWidth / 2)
var rf, gf, bf, af float32
if a > 0 {
rf = float32(r) / float32(a)
gf = float32(g) / float32(a)
bf = float32(b) / float32(a)
af = float32(a) / 0xffff
}
if i == 0 {
v0 := s0.P0
v1 := s1.P0
vertices = append(vertices,
ebiten.Vertex{
DstX: v0.X,
DstY: v0.Y,
SrcX: v0.X,
SrcY: v0.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
},
ebiten.Vertex{
DstX: v1.X,
DstY: v1.Y,
SrcX: v1.X,
SrcY: v1.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
})
}
v2 := s0.P1
v3 := s1.P1
cut := false
if i != len(ss)-1 {
ns := ss[i+1]
nv2 := ns.Translate(-lineWidth / 2).IntersectionAsLines(s0)
nv3 := ns.Translate(lineWidth / 2).IntersectionAsLines(s1)
l := lineWidth / 2 * miterLimit
if (nv2.X-nv3.X)*(nv2.X-nv3.X)+(nv2.Y-nv3.Y)*(nv2.Y-nv3.Y) < l*l {
v2 = nv2
v3 = nv3
} else {
cut = true
}
}
if cut {
ns := ss[i+1]
s2 := ns.Translate(-lineWidth / 2)
s3 := ns.Translate(lineWidth / 2)
vertices = append(vertices,
ebiten.Vertex{
DstX: s0.P1.X,
DstY: s0.P1.Y,
SrcX: s0.P1.X,
SrcY: s0.P1.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
},
ebiten.Vertex{
DstX: s1.P1.X,
DstY: s1.P1.Y,
SrcX: s1.P1.X,
SrcY: s1.P1.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
},
ebiten.Vertex{
DstX: s2.P0.X,
DstY: s2.P0.Y,
SrcX: s2.P0.X,
SrcY: s2.P0.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
},
ebiten.Vertex{
DstX: s3.P0.X,
DstY: s3.P0.Y,
SrcX: s3.P0.X,
SrcY: s3.P0.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
})
indices = append(indices, idx, idx+1, idx+2, idx+1, idx+2, idx+3,
idx+2, idx+3, idx+4, idx+3, idx+4, idx+5)
idx += 4
} else {
vertices = append(vertices,
ebiten.Vertex{
DstX: v2.X,
DstY: v2.Y,
SrcX: v2.X,
SrcY: v2.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
},
ebiten.Vertex{
DstX: v3.X,
DstY: v3.Y,
SrcX: v3.X,
SrcY: v3.Y,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
})
indices = append(indices, idx, idx+1, idx+2, idx+1, idx+2, idx+3)
idx += 2
}
var base uint16
for _, seg := range p.segs {
for _, pt := range seg {
vertices = append(vertices, ebiten.Vertex{
DstX: pt.X,
DstY: pt.Y,
SrcX: 0,
SrcY: 0,
ColorR: rf,
ColorG: gf,
ColorB: bf,
ColorA: af,
})
}
for _, idx := range math.Triangulate(seg) {
indices = append(indices, idx+base)
}
base += uint16(len(seg))
}
return
}
// DrawPathOptions is the options specified at (*Path).Draw.
type DrawPathOptions struct {
LineWidth float32
StrokeColor color.Color
}
// Draw draws the path by rendering its stroke or filling.
func (p *Path) Draw(target *ebiten.Image, op *DrawPathOptions) {
if op == nil {
return
}
// TODO: Implement filling
if op.StrokeColor != nil {
vs, is := p.strokeVertices(op.LineWidth, op.StrokeColor)
op := &ebiten.DrawTrianglesOptions{}
op.Address = ebiten.AddressRepeat
target.DrawTriangles(vs, is, emptyImage, op)
}
dst.DrawTriangles(vertices, indices, emptyImage, nil)
}