ebiten/mipmap.go
Hajime Hoshi b210339786 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
2019-07-30 23:03:55 +09:00

269 lines
5.6 KiB
Go

// Copyright 2018 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ebiten
import (
"fmt"
"image"
"math"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/shareable"
)
type levelToImage map[int]*shareable.Image
type mipmap struct {
orig *shareable.Image
imgs map[image.Rectangle]levelToImage
}
func newMipmap(s *shareable.Image) *mipmap {
return &mipmap{
orig: s,
imgs: map[image.Rectangle]levelToImage{},
}
}
func (m *mipmap) original() *shareable.Image {
return m.orig
}
func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
if level == 0 {
panic("ebiten: level must be non-zero at level")
}
if m.orig.IsVolatile() {
panic("ebiten: mipmap images for a volatile image is not implemented yet")
}
if _, ok := m.imgs[r]; !ok {
m.imgs[r] = levelToImage{}
}
imgs := m.imgs[r]
if img, ok := imgs[level]; ok {
return img
}
size := r.Size()
w, h := size.X, size.Y
w2, h2 := w, h
if level > 0 {
for i := 0; i < level; i++ {
w2 /= 2
h2 /= 2
if w == 0 || h == 0 {
imgs[level] = nil
return nil
}
}
} else {
for i := 0; i < -level; i++ {
w2 *= 2
h2 *= 2
}
}
var src *shareable.Image
vs := vertexSlice(4)
var filter driver.Filter
switch {
case level == 1:
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)
filter = driver.FilterLinear
case level > 1:
src = m.level(r, level-1)
if src == nil {
imgs[level] = nil
return nil
}
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()
s := shareable.NewImage(w2, h2)
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero)
imgs[level] = s
return imgs[level]
}
func (m *mipmap) isDisposed() bool {
return m.orig == nil
}
func (m *mipmap) dispose() {
m.disposeMipmaps()
m.orig.Dispose()
m.orig = nil
}
func (m *mipmap) disposeMipmaps() {
for _, a := range m.imgs {
for _, img := range a {
img.Dispose()
}
}
for k := range m.imgs {
delete(m.imgs, k)
}
}
func (m *mipmap) clearFramebuffer() {
m.orig.ClearFramebuffer()
}
func (m *mipmap) resetRestoringState() {
m.orig.ResetRestoringState()
}
// mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix.
//
// mipmapLevel panics if det is NaN or 0.
func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int {
det := geom.det()
if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevel")
}
if det == 0 {
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))
level := 0
for d < 0.25 {
level++
d *= 4
}
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
}