mipmap: Stop using negative mipmaps

Negative mipmaps tend to allocate extremely big images.

Instead, encourage to use images with explicit padding when enlarging
the image.

Fixes #1400
This commit is contained in:
Hajime Hoshi 2020-10-31 02:52:38 +09:00
parent 19d6f8d20a
commit fa53160e18
7 changed files with 32 additions and 95 deletions

View File

@ -15,6 +15,7 @@
package ebitenutil package ebitenutil
import ( import (
"image"
"image/color" "image/color"
"math" "math"
@ -23,7 +24,7 @@ import (
) )
var ( var (
emptyImage = ebiten.NewImage(1, 1) emptyImage = ebiten.NewImage(3, 3)
) )
func init() { func init() {
@ -36,17 +37,16 @@ func init() {
// //
// DrawLine is not concurrent-safe. // DrawLine is not concurrent-safe.
func DrawLine(dst *ebiten.Image, x1, y1, x2, y2 float64, clr color.Color) { func DrawLine(dst *ebiten.Image, x1, y1, x2, y2 float64, clr color.Color) {
ew, eh := emptyImage.Size()
length := math.Hypot(x2-x1, y2-y1) length := math.Hypot(x2-x1, y2-y1)
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(length/float64(ew), 1/float64(eh)) op.GeoM.Scale(length, 1)
op.GeoM.Rotate(math.Atan2(y2-y1, x2-x1)) op.GeoM.Rotate(math.Atan2(y2-y1, x2-x1))
op.GeoM.Translate(x1, y1) op.GeoM.Translate(x1, y1)
op.ColorM = colormcache.ColorToColorM(clr) op.ColorM = colormcache.ColorToColorM(clr)
// Filter must be 'nearest' filter (default). // Filter must be 'nearest' filter (default).
// Linear filtering would make edges blurred. // Linear filtering would make edges blurred.
dst.DrawImage(emptyImage, op) dst.DrawImage(emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image), op)
} }
// DrawRect draws a rectangle on the given destination dst. // DrawRect draws a rectangle on the given destination dst.
@ -55,13 +55,11 @@ func DrawLine(dst *ebiten.Image, x1, y1, x2, y2 float64, clr color.Color) {
// //
// DrawRect is not concurrent-safe. // DrawRect is not concurrent-safe.
func DrawRect(dst *ebiten.Image, x, y, width, height float64, clr color.Color) { func DrawRect(dst *ebiten.Image, x, y, width, height float64, clr color.Color) {
ew, eh := emptyImage.Size()
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(width/float64(ew), height/float64(eh)) op.GeoM.Scale(width, height)
op.GeoM.Translate(x, y) op.GeoM.Translate(x, y)
op.ColorM = colormcache.ColorToColorM(clr) op.ColorM = colormcache.ColorToColorM(clr)
// Filter must be 'nearest' filter (default). // Filter must be 'nearest' filter (default).
// Linear filtering would make edges blurred. // Linear filtering would make edges blurred.
dst.DrawImage(emptyImage, op) dst.DrawImage(emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image), op)
} }

View File

@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"image"
"image/color" "image/color"
"log" "log"
"math" "math"
@ -33,7 +34,7 @@ const (
) )
var ( var (
emptyImage = ebiten.NewImage(16, 16) emptyImage = ebiten.NewImage(3, 3)
) )
func init() { func init() {
@ -126,7 +127,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
for i := 0; i < g.ngon; i++ { for i := 0; i < g.ngon; i++ {
indices = append(indices, uint16(i), uint16(i+1)%uint16(g.ngon), uint16(g.ngon)) indices = append(indices, uint16(i), uint16(i+1)%uint16(g.ngon), uint16(g.ngon))
} }
screen.DrawTriangles(g.vertices, indices, emptyImage, op) screen.DrawTriangles(g.vertices, indices, emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image), op)
msg := fmt.Sprintf("TPS: %0.2f\n%d-gon\nPress <- or -> to change the number of the vertices", ebiten.CurrentTPS(), g.ngon) msg := fmt.Sprintf("TPS: %0.2f\n%d-gon\nPress <- or -> to change the number of the vertices", ebiten.CurrentTPS(), g.ngon)
ebitenutil.DebugPrint(screen, msg) ebitenutil.DebugPrint(screen, msg)

View File

@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"image"
"image/color" "image/color"
"log" "log"
"math" "math"
@ -32,7 +33,7 @@ const (
) )
var ( var (
emptyImage = ebiten.NewImage(16, 16) emptyImage = ebiten.NewImage(3, 3)
) )
func init() { func init() {
@ -161,18 +162,20 @@ func (g *Game) Update() error {
} }
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
src := emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
cf := float64(g.count) cf := float64(g.count)
v, i := line(100, 100, 300, 100, color.RGBA{0xff, 0xff, 0xff, 0xff}) v, i := line(100, 100, 300, 100, color.RGBA{0xff, 0xff, 0xff, 0xff})
screen.DrawTriangles(v, i, emptyImage, nil) screen.DrawTriangles(v, i, src, nil)
v, i = line(50, 150, 50, 350, color.RGBA{0xff, 0xff, 0x00, 0xff}) v, i = line(50, 150, 50, 350, color.RGBA{0xff, 0xff, 0x00, 0xff})
screen.DrawTriangles(v, i, emptyImage, nil) screen.DrawTriangles(v, i, src, nil)
v, i = line(50, 100+float32(cf), 200+float32(cf), 250, color.RGBA{0x00, 0xff, 0xff, 0xff}) v, i = line(50, 100+float32(cf), 200+float32(cf), 250, color.RGBA{0x00, 0xff, 0xff, 0xff})
screen.DrawTriangles(v, i, emptyImage, nil) screen.DrawTriangles(v, i, src, nil)
v, i = rect(50+float32(cf), 50+float32(cf), 100+float32(cf), 100+float32(cf), color.RGBA{0x80, 0x80, 0x80, 0x80}) v, i = rect(50+float32(cf), 50+float32(cf), 100+float32(cf), 100+float32(cf), color.RGBA{0x80, 0x80, 0x80, 0x80})
screen.DrawTriangles(v, i, emptyImage, nil) screen.DrawTriangles(v, i, src, nil)
v, i = rect(300-float32(cf), 50, 120, 120, color.RGBA{0x00, 0x80, 0x00, 0x80}) v, i = rect(300-float32(cf), 50, 120, 120, color.RGBA{0x00, 0x80, 0x00, 0x80})
screen.DrawTriangles(v, i, emptyImage, nil) screen.DrawTriangles(v, i, src, nil)
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS())) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS()))
} }

View File

@ -798,10 +798,10 @@ func TestImageStretch(t *testing.T) {
dst := NewImage(w, 4096) dst := NewImage(w, 4096)
loop: loop:
for h := 1; h <= 32; h++ { for h := 1; h <= 32; h++ {
src := NewImage(w, h) src := NewImage(w+2, h+2)
pix := make([]byte, 4*w*h) pix := make([]byte, 4*(w+2)*(h+2))
for i := 0; i < w*h; i++ { for i := 0; i < (w+2)*(h+2); i++ {
pix[4*i] = 0xff pix[4*i] = 0xff
pix[4*i+3] = 0xff pix[4*i+3] = 0xff
} }
@ -812,7 +812,7 @@ loop:
dst.Clear() dst.Clear()
op := &DrawImageOptions{} op := &DrawImageOptions{}
op.GeoM.Scale(1, float64(i)/float64(h)) op.GeoM.Scale(1, float64(i)/float64(h))
dst.DrawImage(src, op) dst.DrawImage(src.SubImage(image.Rect(1, 1, w+1, h+1)).(*Image), op)
for j := -1; j <= 1; j++ { for j := -1; j <= 1; j++ {
if i+j < 0 { if i+j < 0 {
continue continue

View File

@ -205,20 +205,6 @@ func (m *Mipmap) level(level int) *buffered.Image {
h := sizeForLevel(m.height, level-1) h := sizeForLevel(m.height, level-1)
vs = graphics.QuadVertices(0, 0, float32(w), float32(h), 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1, false) vs = graphics.QuadVertices(0, 0, float32(w), float32(h), 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterLinear filter = driver.FilterLinear
case level == -1:
src = m.orig
vs = graphics.QuadVertices(0, 0, float32(m.width), float32(m.height), 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterNearest
case level < -1:
src = m.level(level + 1)
if src == nil {
m.imgs[level] = nil
return nil
}
w := sizeForLevel(m.width, level-1)
h := sizeForLevel(m.height, level-1)
vs = graphics.QuadVertices(0, 0, float32(w), float32(h), 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterNearest
default: default:
panic(fmt.Sprintf("ebiten: invalid level: %d", level)) panic(fmt.Sprintf("ebiten: invalid level: %d", level))
} }
@ -246,18 +232,12 @@ func (m *Mipmap) level(level int) *buffered.Image {
} }
func sizeForLevel(x int, level int) int { func sizeForLevel(x int, level int) int {
if level > 0 {
for i := 0; i < level; i++ { for i := 0; i < level; i++ {
x /= 2 x /= 2
if x == 0 { if x == 0 {
return 0 return 0
} }
} }
} else {
for i := 0; i < -level; i++ {
x *= 2
}
}
return x return x
} }
@ -295,9 +275,6 @@ func mipmapLevelFromDistance(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1 float32, fil
// Scale can be infinite when the specified scale is extremely big (#1398). // Scale can be infinite when the specified scale is extremely big (#1398).
if math.IsInf(float64(scale), 0) { if math.IsInf(float64(scale), 0) {
if filter == driver.FilterNearest {
return -maxLevel
}
return 0 return 0
} }
@ -306,42 +283,6 @@ func mipmapLevelFromDistance(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1 float32, fil
return 0 return 0
} }
// Use 'negative' mipmap to render edges correctly (#611, #907).
// It looks like 128 is the enlargement factor that causes edge missings to pass the test TestImageStretch,
// but we use 32 here for environments where the float precision is low (#1044, #1270).
var tooBigScale float32 = 32
if scale >= tooBigScale*tooBigScale {
// If the filter is not nearest, the target needs to be rendered with graduation. Don't use mipmaps.
if filter != driver.FilterNearest {
return 0
}
const mipmapMaxSize = 1024
w, h := sx1-sx0, sy1-sy0
if w >= mipmapMaxSize || h >= mipmapMaxSize {
return 0
}
level := 0
for scale >= tooBigScale*tooBigScale {
level--
scale /= 4
w *= 2
h *= 2
if w >= mipmapMaxSize || h >= mipmapMaxSize {
break
}
}
// If tooBigScale is 32, level -6 means that the maximum scale is 32 * 2^6 = 2048. This should be
// enough.
if level < -maxLevel {
level = -maxLevel
}
return level
}
if filter != driver.FilterLinear { if filter != driver.FilterLinear {
return 0 return 0
} }
@ -378,18 +319,10 @@ func mipmapLevelFromDistance(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1 float32, fil
} }
func pow2(power int) float32 { func pow2(power int) float32 {
if power >= 0 {
x := 1 x := 1
return float32(x << uint(power)) return float32(x << uint(power))
} }
x := float32(1)
for i := 0; i < -power; i++ {
x /= 2
}
return x
}
type Shader struct { type Shader struct {
shader *buffered.Shader shader *buffered.Shader
} }

View File

@ -113,6 +113,7 @@ var emptyImage *Image
func init() { func init() {
// Use a big-enough image as an rendering source. By enlarging with x128, this can reach to 16384. // Use a big-enough image as an rendering source. By enlarging with x128, this can reach to 16384.
// See #907 for details. // See #907 for details.
// TODO: This doesn't have to be 128 due to the 1px padding. 3x3 should be enough.
const w, h = 128, 128 const w, h = 128, 128
emptyImage = &Image{ emptyImage = &Image{
image: graphicscommand.NewImage(w, h), image: graphicscommand.NewImage(w, h),

View File

@ -18,6 +18,7 @@
package vector package vector
import ( import (
"image"
"image/color" "image/color"
"math" "math"
@ -25,7 +26,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector/internal/triangulate" "github.com/hajimehoshi/ebiten/v2/vector/internal/triangulate"
) )
var emptyImage = ebiten.NewImage(1, 1) var emptyImage = ebiten.NewImage(3, 3)
func init() { func init() {
emptyImage.Fill(color.White) emptyImage.Fill(color.White)
@ -138,5 +139,5 @@ func (p *Path) Fill(dst *ebiten.Image, op *FillOptions) {
} }
base += uint16(len(seg)) base += uint16(len(seg))
} }
dst.DrawTriangles(vertices, indices, emptyImage, nil) dst.DrawTriangles(vertices, indices, emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image), nil)
} }