mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
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:
parent
69ef9eb184
commit
b210339786
@ -16,5 +16,5 @@ package ebiten
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
CopyImage = copyImage
|
CopyImage = copyImage
|
||||||
MipmapLevel = mipmapLevel
|
MipmapLevelForDownscale = mipmapLevelForDownscale
|
||||||
)
|
)
|
||||||
|
34
image.go
34
image.go
@ -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()
|
||||||
|
@ -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
148
mipmap.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user