graphics: Use 'negative' mipmap when enlarging a too small image

This is a hack to render edges correctly.

This works only when the filter is nearest.

Fixes #611
This commit is contained in:
Hajime Hoshi 2019-07-30 13:20:41 +09:00
parent 69ef9eb184
commit b210339786
5 changed files with 196 additions and 69 deletions

View File

@ -16,5 +16,5 @@ package ebiten
var ( var (
CopyImage = copyImage CopyImage = copyImage
MipmapLevel = mipmapLevel MipmapLevelForDownscale = mipmapLevelForDownscale
) )

View File

@ -249,22 +249,15 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
filter = driver.Filter(img.filter) filter = driver.Filter(img.filter)
} }
a, b, c, d, tx, ty := geom.elements() if det := geom.det(); det == 0 {
return nil
level := 0 } else if math.IsNaN(float64(det)) {
if filter == driver.FilterLinear && !img.mipmap.original().IsVolatile() {
det := geom.det()
if det == 0 {
return nil return nil
} }
if math.IsNaN(float64(det)) {
return nil
}
level = mipmapLevel(det)
if level < 0 {
panic(fmt.Sprintf("ebiten: level must be >= 0 but %d", level))
}
level := img.mipmap.mipmapLevel(geom, bounds.Dx(), bounds.Dy(), filter)
if level > 0 {
// If the image can be scaled into 0 size, adjust the level. (#839) // If the image can be scaled into 0 size, adjust the level. (#839)
w, h := bounds.Dx(), bounds.Dy() w, h := bounds.Dx(), bounds.Dy()
for level >= 0 { for level >= 0 {
@ -281,9 +274,13 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
return nil return nil
} }
} }
if level > 6 { if level > 6 {
level = 6 level = 6
} }
if level < -6 {
level = -6
}
// TODO: Add (*mipmap).drawImage and move the below code. // TODO: Add (*mipmap).drawImage and move the below code.
colorm := options.ColorM.impl colorm := options.ColorM.impl
@ -297,6 +294,7 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
colorm = nil colorm = nil
} }
a, b, c, d, tx, ty := geom.elements()
if level == 0 { if level == 0 {
src := img.mipmap.original() src := img.mipmap.original()
vs := vertexSlice(4) vs := vertexSlice(4)
@ -305,11 +303,11 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
i.mipmap.original().DrawTriangles(src, vs, is, colorm, mode, filter, driver.AddressClampToZero) i.mipmap.original().DrawTriangles(src, vs, is, colorm, mode, filter, driver.AddressClampToZero)
} else if src := img.mipmap.level(bounds, level); src != nil { } else if src := img.mipmap.level(bounds, level); src != nil {
w, h := src.Size() w, h := src.Size()
s := 1 << uint(level) s := pow2(level)
a *= float32(s) a *= s
b *= float32(s) b *= s
c *= float32(s) c *= s
d *= float32(s) d *= s
vs := vertexSlice(4) vs := vertexSlice(4)
graphics.PutQuadVertices(vs, src, 0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca) graphics.PutQuadVertices(vs, src, 0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca)
is := graphics.QuadIndices() is := graphics.QuadIndices()

View File

@ -940,36 +940,42 @@ func TestImageCopy(t *testing.T) {
img1.Fill(color.Transparent) img1.Fill(color.Transparent)
} }
// Issue #611, #907
func TestImageStretch(t *testing.T) { func TestImageStretch(t *testing.T) {
img0, _ := NewImage(16, 17, FilterDefault) const w = 16
pix := make([]byte, 4*16*17) dst, _ := NewImage(w, 4096, FilterDefault)
for i := 0; i < 16*16; i++ { loop:
for h := 1; h <= 32; h++ {
src, _ := NewImage(w, h+1, FilterDefault)
pix := make([]byte, 4*w*(h+1))
for i := 0; i < w*h; i++ {
pix[4*i] = 0xff pix[4*i] = 0xff
pix[4*i+3] = 0xff pix[4*i+3] = 0xff
} }
for i := 0; i < 16; i++ { for i := 0; i < w; i++ {
pix[4*(16*16+i)+1] = 0xff pix[4*(w*h+i)+1] = 0xff
pix[4*(16*16+i)+3] = 0xff pix[4*(w*h+i)+3] = 0xff
} }
img0.ReplacePixels(pix) src.ReplacePixels(pix)
// TODO: 4096 doesn't pass on MacBook Pro (#611). _, dh := dst.Size()
const h = 4000 for i := 32; i < dh; i += 32 {
img1, _ := NewImage(16, h, FilterDefault) dst.Clear()
for i := 1; i < h; i++ {
img1.Clear()
op := &DrawImageOptions{} op := &DrawImageOptions{}
op.GeoM.Scale(1, float64(i)/16) op.GeoM.Scale(1, float64(i)/float64(h))
img1.DrawImage(img0.SubImage(image.Rect(0, 0, 16, 16)).(*Image), op) dst.DrawImage(src.SubImage(image.Rect(0, 0, w, h)).(*Image), op)
for j := -1; j <= 1; j++ { for j := -2; j <= 2; j++ {
got := img1.At(0, i+j).(color.RGBA) got := dst.At(0, i+j).(color.RGBA)
want := color.RGBA{} want := color.RGBA{}
if j < 0 { if j < 0 {
want = color.RGBA{0xff, 0, 0, 0xff} want = color.RGBA{0xff, 0, 0, 0xff}
} }
if got != want { if got != want {
t.Fatalf("At(%d, %d) (i=%d): got: %#v, want: %#v", 0, i+j, i, got, want) t.Errorf("At(%d, %d) (height=%d, scale=%d/%d): got: %#v, want: %#v", 0, i+j, h, i, h, got, want)
continue loop
}
} }
} }
} }

148
mipmap.go
View File

@ -15,6 +15,7 @@
package ebiten package ebiten
import ( import (
"fmt"
"image" "image"
"math" "math"
@ -42,8 +43,8 @@ func (m *mipmap) original() *shareable.Image {
} }
func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image { func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
if level <= 0 { if level == 0 {
panic("ebiten: level must be positive at level") panic("ebiten: level must be non-zero at level")
} }
if m.orig.IsVolatile() { if m.orig.IsVolatile() {
@ -63,6 +64,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
w, h := size.X, size.Y w, h := size.X, size.Y
w2, h2 := w, h w2, h2 := w, h
if level > 0 {
for i := 0; i < level; i++ { for i := 0; i < level; i++ {
w2 /= 2 w2 /= 2
h2 /= 2 h2 /= 2
@ -71,22 +73,41 @@ func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
return nil return nil
} }
} }
s := shareable.NewImage(w2, h2) } else {
for i := 0; i < -level; i++ {
w2 *= 2
h2 *= 2
}
}
var src *shareable.Image var src *shareable.Image
vs := vertexSlice(4) vs := vertexSlice(4)
if level == 1 { var filter driver.Filter
switch {
case level == 1:
src = m.orig src = m.orig
graphics.PutQuadVertices(vs, src, r.Min.X, r.Min.Y, r.Max.X, r.Max.Y, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1) graphics.PutQuadVertices(vs, src, 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 { filter = driver.FilterLinear
case level > 1:
src = m.level(r, level-1) src = m.level(r, level-1)
if src == nil { if src == nil {
imgs[level] = nil imgs[level] = nil
return nil return nil
} }
graphics.PutQuadVertices(vs, src, 0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1) graphics.PutQuadVertices(vs, src, 0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1)
filter = driver.FilterLinear
case level < 0:
src = m.orig
s := pow2(-level)
graphics.PutQuadVertices(vs, src, r.Min.X, r.Min.Y, r.Max.X, r.Max.Y, s, 0, 0, s, 0, 0, 1, 1, 1, 1)
filter = driver.FilterNearest
default:
panic(fmt.Sprintf("ebiten: invalid level: %d", level))
} }
is := graphics.QuadIndices() is := graphics.QuadIndices()
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, driver.FilterLinear, driver.AddressClampToZero)
s := shareable.NewImage(w2, h2)
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero)
imgs[level] = s imgs[level] = s
return imgs[level] return imgs[level]
@ -123,17 +144,65 @@ func (m *mipmap) resetRestoringState() {
// mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix. // mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix.
// //
// mipmapLevel returns -1 if det is 0. // mipmapLevel panics if det is NaN or 0.
// func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int {
// mipmapLevel panics if det is NaN. det := geom.det()
func mipmapLevel(det float32) int {
if math.IsNaN(float64(det)) { if math.IsNaN(float64(det)) {
panic("graphicsutil: det must be finite") panic("ebiten: det must be finite at mipmapLevel")
} }
if det == 0 { if det == 0 {
return -1 panic("ebiten: dst must be non zero at mipmapLevel")
} }
// Use 'negative' mipmap to render edges correctly (#611, #907).
// It looks like 256 is the enlargement factor that causes edge missings to pass the test TestImageStretch.
const tooBigScale = 256
if sx, sy := geomScaleSize(geom); sx >= tooBigScale || sy >= tooBigScale {
// If the filter is not nearest, the target needs to be rendered with gradiation. Don't use mipmaps.
if filter != driver.FilterNearest {
return 0
}
const mipmapMaxSize = 1024
w, h := width, height
if w >= mipmapMaxSize || h >= mipmapMaxSize {
return 0
}
level := 0
for sx >= tooBigScale || sy >= tooBigScale {
level--
sx /= 2
sy /= 2
w *= 2
h *= 2
if w >= mipmapMaxSize || h >= mipmapMaxSize {
break
}
}
return level
}
if filter != driver.FilterLinear {
return 0
}
if m.original().IsVolatile() {
return 0
}
// This is a separate function for testing.
return mipmapLevelForDownscale(det)
}
func mipmapLevelForDownscale(det float32) int {
if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevelForDownscale")
}
if det == 0 {
panic("ebiten: dst must be non zero at mipmapLevelForDownscale")
}
// TODO: Should this be determined by x/y scales instead of det?
d := math.Abs(float64(det)) d := math.Abs(float64(det))
level := 0 level := 0
for d < 0.25 { for d < 0.25 {
@ -142,3 +211,58 @@ func mipmapLevel(det float32) int {
} }
return level return level
} }
func pow2(power int) float32 {
if power >= 0 {
x := 1
return float32(x << uint(power))
}
x := float32(1)
for i := 0; i < -power; i++ {
x /= 2
}
return x
}
func maxf32(values ...float32) float32 {
max := float32(math.Inf(-1))
for _, v := range values {
if max < v {
max = v
}
}
return max
}
func minf32(values ...float32) float32 {
min := float32(math.Inf(1))
for _, v := range values {
if min > v {
min = v
}
}
return min
}
func geomScaleSize(geom *GeoM) (sx, sy float32) {
a, b, c, d, _, _ := geom.elements()
// (0, 1)
x0 := 0*a + 1*b
y0 := 0*c + 1*d
// (1, 0)
x1 := 1*a + 0*b
y1 := 1*c + 0*d
// (1, 1)
x2 := 1*a + 1*b
y2 := 1*c + 1*d
maxx := maxf32(0, x0, x1, x2)
maxy := maxf32(0, y0, y1, y2)
minx := minf32(0, x0, x1, x2)
miny := minf32(0, y0, y1, y2)
return maxx - minx, maxy - miny
}

View File

@ -21,13 +21,12 @@ import (
. "github.com/hajimehoshi/ebiten" . "github.com/hajimehoshi/ebiten"
) )
func TestMipmapLevel(t *testing.T) { func TestMipmapLevelForDownscale(t *testing.T) {
inf := float32(math.Inf(1)) inf := float32(math.Inf(1))
cases := []struct { cases := []struct {
In float32 In float32
Out int Out int
}{ }{
{0, -1},
{1, 0}, {1, 0},
{-1, 0}, {-1, 0},
{2, 0}, {2, 0},
@ -55,7 +54,7 @@ func TestMipmapLevel(t *testing.T) {
} }
for _, c := range cases { for _, c := range cases {
got := MipmapLevel(c.In) got := MipmapLevelForDownscale(c.In)
want := c.Out want := c.Out
if got != want { if got != want {
t.Errorf("MipmapLevel(%v): got %v, want %v", c.In, got, want) t.Errorf("MipmapLevel(%v): got %v, want %v", c.In, got, want)