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
import (
"image"
"image/color"
"math"
@ -23,7 +24,7 @@ import (
)
var (
emptyImage = ebiten.NewImage(1, 1)
emptyImage = ebiten.NewImage(3, 3)
)
func init() {
@ -36,17 +37,16 @@ func init() {
//
// DrawLine is not concurrent-safe.
func DrawLine(dst *ebiten.Image, x1, y1, x2, y2 float64, clr color.Color) {
ew, eh := emptyImage.Size()
length := math.Hypot(x2-x1, y2-y1)
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.Translate(x1, y1)
op.ColorM = colormcache.ColorToColorM(clr)
// Filter must be 'nearest' filter (default).
// 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.
@ -55,13 +55,11 @@ func DrawLine(dst *ebiten.Image, x1, y1, x2, y2 float64, clr color.Color) {
//
// DrawRect is not concurrent-safe.
func DrawRect(dst *ebiten.Image, x, y, width, height float64, clr color.Color) {
ew, eh := emptyImage.Size()
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(width/float64(ew), height/float64(eh))
op.GeoM.Scale(width, height)
op.GeoM.Translate(x, y)
op.ColorM = colormcache.ColorToColorM(clr)
// Filter must be 'nearest' filter (default).
// 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 (
"fmt"
"image"
"image/color"
"log"
"math"
@ -33,7 +34,7 @@ const (
)
var (
emptyImage = ebiten.NewImage(16, 16)
emptyImage = ebiten.NewImage(3, 3)
)
func init() {
@ -126,7 +127,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
for i := 0; i < g.ngon; i++ {
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)
ebitenutil.DebugPrint(screen, msg)

View File

@ -18,6 +18,7 @@ package main
import (
"fmt"
"image"
"image/color"
"log"
"math"
@ -32,7 +33,7 @@ const (
)
var (
emptyImage = ebiten.NewImage(16, 16)
emptyImage = ebiten.NewImage(3, 3)
)
func init() {
@ -161,18 +162,20 @@ func (g *Game) Update() error {
}
func (g *Game) Draw(screen *ebiten.Image) {
src := emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
cf := float64(g.count)
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})
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})
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})
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})
screen.DrawTriangles(v, i, emptyImage, nil)
screen.DrawTriangles(v, i, src, nil)
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)
loop:
for h := 1; h <= 32; h++ {
src := NewImage(w, h)
src := NewImage(w+2, h+2)
pix := make([]byte, 4*w*h)
for i := 0; i < w*h; i++ {
pix := make([]byte, 4*(w+2)*(h+2))
for i := 0; i < (w+2)*(h+2); i++ {
pix[4*i] = 0xff
pix[4*i+3] = 0xff
}
@ -812,7 +812,7 @@ loop:
dst.Clear()
op := &DrawImageOptions{}
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++ {
if i+j < 0 {
continue

View File

@ -205,20 +205,6 @@ func (m *Mipmap) level(level int) *buffered.Image {
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)
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:
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 {
if level > 0 {
for i := 0; i < level; i++ {
x /= 2
if x == 0 {
return 0
}
}
} else {
for i := 0; i < -level; i++ {
x *= 2
}
}
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).
if math.IsInf(float64(scale), 0) {
if filter == driver.FilterNearest {
return -maxLevel
}
return 0
}
@ -306,42 +283,6 @@ func mipmapLevelFromDistance(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1 float32, fil
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 {
return 0
}
@ -378,18 +319,10 @@ func mipmapLevelFromDistance(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1 float32, fil
}
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
}
type Shader struct {
shader *buffered.Shader
}

View File

@ -113,6 +113,7 @@ var emptyImage *Image
func init() {
// Use a big-enough image as an rendering source. By enlarging with x128, this can reach to 16384.
// 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
emptyImage = &Image{
image: graphicscommand.NewImage(w, h),

View File

@ -18,6 +18,7 @@
package vector
import (
"image"
"image/color"
"math"
@ -25,7 +26,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector/internal/triangulate"
)
var emptyImage = ebiten.NewImage(1, 1)
var emptyImage = ebiten.NewImage(3, 3)
func init() {
emptyImage.Fill(color.White)
@ -138,5 +139,5 @@ func (p *Path) Fill(dst *ebiten.Image, op *FillOptions) {
}
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)
}