From 8b82667df1b4e3acb41c35b7b2f5ac73840f03f8 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 26 Oct 2018 00:48:49 +0900 Subject: [PATCH] graphics: Bug fix: Violating edge pixels with linear filter Fixes #724 --- examples/minify/main.go | 14 +++++++- image.go | 77 +++++++++++++++++++++++++---------------- image_test.go | 73 +++++++++++++++++++++----------------- 3 files changed, 103 insertions(+), 61 deletions(-) diff --git a/examples/minify/main.go b/examples/minify/main.go index 6ef147db4..b3770a27a 100644 --- a/examples/minify/main.go +++ b/examples/minify/main.go @@ -41,6 +41,7 @@ const ( var ( gophersImage *ebiten.Image rotate = false + clip = false counter = 0 ) @@ -53,13 +54,20 @@ func update(screen *ebiten.Image) error { if inpututil.IsKeyJustPressed(ebiten.KeyR) { rotate = !rotate } + if inpututil.IsKeyJustPressed(ebiten.KeyC) { + clip = !clip + } if ebiten.IsDrawingSkipped() { return nil } s := 1.5 / math.Pow(1.01, float64(counter)) - ebitenutil.DebugPrint(screen, fmt.Sprintf("Minifying images (Nearest filter vs Linear filter): Press R to rotate the images.\nScale: %0.2f", s)) + msg := fmt.Sprintf(`Minifying images (Nearest filter vs Linear filter): +Press R to rotate the images. +Press C to clip the images. +Scale: %0.2f`, s) + ebitenutil.DebugPrint(screen, msg) for i, f := range []ebiten.Filter{ebiten.FilterNearest, ebiten.FilterLinear} { w, h := gophersImage.Size() @@ -73,6 +81,10 @@ func update(screen *ebiten.Image) error { op.GeoM.Scale(s, s) op.GeoM.Translate(32+float64(i*w)*s+float64(i*4), 64) op.Filter = f + if clip { + r := image.Rect(10, 10, 100, 100) + op.SourceRect = &r + } screen.DrawImage(gophersImage, op) } diff --git a/image.go b/image.go index 9c107bd50..0291a7d17 100644 --- a/image.go +++ b/image.go @@ -37,12 +37,13 @@ func init() { type mipmap struct { orig *shareable.Image - imgs []*shareable.Image + imgs map[image.Rectangle][]*shareable.Image } func newMipmap(s *shareable.Image) *mipmap { return &mipmap{ orig: s, + imgs: map[image.Rectangle][]*shareable.Image{}, } } @@ -50,18 +51,26 @@ func (m *mipmap) original() *shareable.Image { return m.orig } -func (m *mipmap) level(level int) *shareable.Image { +func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image { if level == 0 { return m.orig } - idx := level - 1 - w, h := m.orig.Size() - if len(m.imgs) > 0 { - w, h = m.imgs[len(m.imgs)-1].Size() + imgs, ok := m.imgs[r] + if !ok { + imgs = []*shareable.Image{} + m.imgs[r] = imgs } - for len(m.imgs) < idx+1 { - src := m.level(len(m.imgs)) + idx := level - 1 + + size := r.Size() + w, h := size.X, size.Y + if len(imgs) > 0 { + w, h = imgs[len(imgs)-1].Size() + } + + for len(imgs) < idx+1 { + src := m.level(r, len(imgs)) w2 := w / 2 h2 := h / 2 if w2 == 0 || h2 == 0 { @@ -73,17 +82,24 @@ func (m *mipmap) level(level int) *shareable.Image { } else { s = shareable.NewImage(w2, h2) } - vs := src.QuadVertices(0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1) + var vs []float32 + if len(imgs) == 0 { + vs = src.QuadVertices(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1) + } else { + vs = src.QuadVertices(0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1) + } is := graphicsutil.QuadIndices() s.DrawImage(src, vs, is, nil, opengl.CompositeModeCopy, graphics.FilterLinear) - m.imgs = append(m.imgs, s) + imgs = append(imgs, s) w = w2 h = h2 } - if len(m.imgs) <= idx { + m.imgs[r] = imgs + + if len(imgs) <= idx { return nil } - return m.imgs[idx] + return imgs[idx] } func (m *mipmap) isDisposed() bool { @@ -97,10 +113,12 @@ func (m *mipmap) dispose() { } func (m *mipmap) disposeMipmaps() { - for _, img := range m.imgs { - img.Dispose() + for _, a := range m.imgs { + for _, img := range a { + img.Dispose() + } } - m.imgs = nil + m.imgs = map[image.Rectangle][]*shareable.Image{} } // Image represents a rectangle set of pixels. @@ -346,19 +364,8 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) { level = 6 } - if level > 0 { - s := 1 << uint(level) - a *= float32(s) - b *= float32(s) - c *= float32(s) - d *= float32(s) - sx0 = sx0 / s - sy0 = sy0 / s - sx1 = sx1 / s - sy1 = sy1 / s - } - - if src := img.mipmap.level(level); src != nil { + // TODO: Move this logic to mipmap? + if src := img.mipmap.level(image.Rect(sx0, sy0, sx1, sy1), level); src != nil { colorm := options.ColorM.impl cr, cg, cb, ca := float32(1), float32(1), float32(1), float32(1) if colorm.ScaleOnly() { @@ -368,12 +375,24 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) { cb = body[10] ca = body[15] } - vs := src.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, cr, cg, cb, ca) + var vs []float32 + if level == 0 { + vs = src.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, cr, cg, cb, ca) + } else { + w, h := src.Size() + s := 1 << uint(level) + a *= float32(s) + b *= float32(s) + c *= float32(s) + d *= float32(s) + vs = src.QuadVertices(0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca) + } is := graphicsutil.QuadIndices() if colorm.ScaleOnly() { colorm = nil } + i.mipmap.original().DrawImage(src, vs, is, colorm, mode, filter) } i.disposeMipmaps() diff --git a/image_test.go b/image_test.go index da7737b9d..2b13c2214 100644 --- a/image_test.go +++ b/image_test.go @@ -508,11 +508,16 @@ func TestImageFill(t *testing.T) { } } -// Issue #317, #558 +// Issue #317, #558, #724 func TestImageEdge(t *testing.T) { const ( - img0Width = 16 - img0Height = 16 + img0Width = 16 + img0Height = 16 + img0InnerWidth = 6 + img0InnerHeight = 6 + img0OffsetWidth = (img0Width - img0InnerWidth) / 2 + img0OffsetHeight = (img0Height - img0InnerHeight) / 2 + img1Width = 32 img1Height = 32 ) @@ -522,7 +527,8 @@ func TestImageEdge(t *testing.T) { for i := 0; i < img0Width; i++ { idx := 4 * (i + j*img0Width) switch { - case j < img0Height/2: + case img0OffsetWidth <= i && i < img0Width-img0OffsetWidth && + img0InnerHeight <= j && j < img0Height-img0InnerHeight: pixels[idx] = 0xff pixels[idx+1] = 0 pixels[idx+2] = 0 @@ -544,40 +550,45 @@ func TestImageEdge(t *testing.T) { for a := 0; a < 1440; a++ { angles = append(angles, float64(a)/1440*2*math.Pi) } - for a := 0; a < 4096; a++ { + for a := 0; a < 4096; a += 3 { + // a++ should be fine, but it takes long to test. angles = append(angles, float64(a)/4096*2*math.Pi) } - for _, f := range []Filter{FilterNearest, FilterLinear} { - for _, a := range angles { - img1.Clear() - op := &DrawImageOptions{} - w, h := img0.Size() - r := image.Rect(0, 0, w, h/2) - op.SourceRect = &r - op.GeoM.Translate(-float64(img0Width)/2, -float64(img0Height)/2) - op.GeoM.Rotate(a) - op.GeoM.Translate(img1Width/2, img1Height/2) - op.Filter = f - img1.DrawImage(img0, op) - for j := 0; j < img1Height; j++ { - for i := 0; i < img1Width; i++ { - c := img1.At(i, j) - if c == transparent { - continue - } - switch f { - case FilterNearest: - if c == red { + for _, s := range []float64{1, 0.5, 0.25} { + for _, f := range []Filter{FilterNearest, FilterLinear} { + for _, a := range angles { + img1.Clear() + op := &DrawImageOptions{} + r := image.Rect(img0OffsetWidth, img0InnerHeight, img0Width-img0OffsetWidth, img0Height-img0InnerHeight) + op.SourceRect = &r + + w, h := img0.Size() + op.GeoM.Translate(-float64(w)/2, -float64(h)/2) + op.GeoM.Scale(s, s) + op.GeoM.Rotate(a) + op.GeoM.Translate(img1Width/2, img1Height/2) + op.Filter = f + img1.DrawImage(img0, op) + for j := 0; j < img1Height; j++ { + for i := 0; i < img1Width; i++ { + c := img1.At(i, j) + if c == transparent { continue } - case FilterLinear: - _, g, b, _ := c.RGBA() - if g == 0 && b == 0 { - continue + switch f { + case FilterNearest: + if c == red { + continue + } + case FilterLinear: + _, g, b, _ := c.RGBA() + if g == 0 && b == 0 { + continue + } } + t.Errorf("img1.At(%d, %d) (filter: %d, scale: %f, angle: %f) want: red or transparent, got: %v", i, j, f, s, a, c) } - t.Errorf("img1.At(%d, %d) (filter: %d, angle: %f) want: red or transparent, got: %v", i, j, f, a, c) } } }