graphics: Bug fix: Violating edge pixels with linear filter

Fixes #724
This commit is contained in:
Hajime Hoshi 2018-10-26 00:48:49 +09:00
parent 58f4feda8d
commit 8b82667df1
3 changed files with 103 additions and 61 deletions

View File

@ -41,6 +41,7 @@ const (
var (
gophersImage *ebiten.Image
rotate = false
clip = false
counter = 0
)
@ -53,13 +54,20 @@ func update(screen *ebiten.Image) error {
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
rotate = !rotate
}
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
clip = !clip
}
if ebiten.IsDrawingSkipped() {
return nil
}
s := 1.5 / math.Pow(1.01, float64(counter))
ebitenutil.DebugPrint(screen, fmt.Sprintf("Minifying images (Nearest filter vs Linear filter): Press R to rotate the images.\nScale: %0.2f", s))
msg := fmt.Sprintf(`Minifying images (Nearest filter vs Linear filter):
Press R to rotate the images.
Press C to clip the images.
Scale: %0.2f`, s)
ebitenutil.DebugPrint(screen, msg)
for i, f := range []ebiten.Filter{ebiten.FilterNearest, ebiten.FilterLinear} {
w, h := gophersImage.Size()
@ -73,6 +81,10 @@ func update(screen *ebiten.Image) error {
op.GeoM.Scale(s, s)
op.GeoM.Translate(32+float64(i*w)*s+float64(i*4), 64)
op.Filter = f
if clip {
r := image.Rect(10, 10, 100, 100)
op.SourceRect = &r
}
screen.DrawImage(gophersImage, op)
}

View File

@ -37,12 +37,13 @@ func init() {
type mipmap struct {
orig *shareable.Image
imgs []*shareable.Image
imgs map[image.Rectangle][]*shareable.Image
}
func newMipmap(s *shareable.Image) *mipmap {
return &mipmap{
orig: s,
imgs: map[image.Rectangle][]*shareable.Image{},
}
}
@ -50,18 +51,26 @@ func (m *mipmap) original() *shareable.Image {
return m.orig
}
func (m *mipmap) level(level int) *shareable.Image {
func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
if level == 0 {
return m.orig
}
idx := level - 1
w, h := m.orig.Size()
if len(m.imgs) > 0 {
w, h = m.imgs[len(m.imgs)-1].Size()
imgs, ok := m.imgs[r]
if !ok {
imgs = []*shareable.Image{}
m.imgs[r] = imgs
}
for len(m.imgs) < idx+1 {
src := m.level(len(m.imgs))
idx := level - 1
size := r.Size()
w, h := size.X, size.Y
if len(imgs) > 0 {
w, h = imgs[len(imgs)-1].Size()
}
for len(imgs) < idx+1 {
src := m.level(r, len(imgs))
w2 := w / 2
h2 := h / 2
if w2 == 0 || h2 == 0 {
@ -73,17 +82,24 @@ func (m *mipmap) level(level int) *shareable.Image {
} else {
s = shareable.NewImage(w2, h2)
}
vs := src.QuadVertices(0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1)
var vs []float32
if len(imgs) == 0 {
vs = src.QuadVertices(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 {
vs = src.QuadVertices(0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1)
}
is := graphicsutil.QuadIndices()
s.DrawImage(src, vs, is, nil, opengl.CompositeModeCopy, graphics.FilterLinear)
m.imgs = append(m.imgs, s)
imgs = append(imgs, s)
w = w2
h = h2
}
if len(m.imgs) <= idx {
m.imgs[r] = imgs
if len(imgs) <= idx {
return nil
}
return m.imgs[idx]
return imgs[idx]
}
func (m *mipmap) isDisposed() bool {
@ -97,10 +113,12 @@ func (m *mipmap) dispose() {
}
func (m *mipmap) disposeMipmaps() {
for _, img := range m.imgs {
img.Dispose()
for _, a := range m.imgs {
for _, img := range a {
img.Dispose()
}
}
m.imgs = nil
m.imgs = map[image.Rectangle][]*shareable.Image{}
}
// Image represents a rectangle set of pixels.
@ -346,19 +364,8 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
level = 6
}
if level > 0 {
s := 1 << uint(level)
a *= float32(s)
b *= float32(s)
c *= float32(s)
d *= float32(s)
sx0 = sx0 / s
sy0 = sy0 / s
sx1 = sx1 / s
sy1 = sy1 / s
}
if src := img.mipmap.level(level); src != nil {
// TODO: Move this logic to mipmap?
if src := img.mipmap.level(image.Rect(sx0, sy0, sx1, sy1), level); src != nil {
colorm := options.ColorM.impl
cr, cg, cb, ca := float32(1), float32(1), float32(1), float32(1)
if colorm.ScaleOnly() {
@ -368,12 +375,24 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
cb = body[10]
ca = body[15]
}
vs := src.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, cr, cg, cb, ca)
var vs []float32
if level == 0 {
vs = src.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, cr, cg, cb, ca)
} else {
w, h := src.Size()
s := 1 << uint(level)
a *= float32(s)
b *= float32(s)
c *= float32(s)
d *= float32(s)
vs = src.QuadVertices(0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca)
}
is := graphicsutil.QuadIndices()
if colorm.ScaleOnly() {
colorm = nil
}
i.mipmap.original().DrawImage(src, vs, is, colorm, mode, filter)
}
i.disposeMipmaps()

View File

@ -508,11 +508,16 @@ func TestImageFill(t *testing.T) {
}
}
// Issue #317, #558
// Issue #317, #558, #724
func TestImageEdge(t *testing.T) {
const (
img0Width = 16
img0Height = 16
img0Width = 16
img0Height = 16
img0InnerWidth = 6
img0InnerHeight = 6
img0OffsetWidth = (img0Width - img0InnerWidth) / 2
img0OffsetHeight = (img0Height - img0InnerHeight) / 2
img1Width = 32
img1Height = 32
)
@ -522,7 +527,8 @@ func TestImageEdge(t *testing.T) {
for i := 0; i < img0Width; i++ {
idx := 4 * (i + j*img0Width)
switch {
case j < img0Height/2:
case img0OffsetWidth <= i && i < img0Width-img0OffsetWidth &&
img0InnerHeight <= j && j < img0Height-img0InnerHeight:
pixels[idx] = 0xff
pixels[idx+1] = 0
pixels[idx+2] = 0
@ -544,40 +550,45 @@ func TestImageEdge(t *testing.T) {
for a := 0; a < 1440; a++ {
angles = append(angles, float64(a)/1440*2*math.Pi)
}
for a := 0; a < 4096; a++ {
for a := 0; a < 4096; a += 3 {
// a++ should be fine, but it takes long to test.
angles = append(angles, float64(a)/4096*2*math.Pi)
}
for _, f := range []Filter{FilterNearest, FilterLinear} {
for _, a := range angles {
img1.Clear()
op := &DrawImageOptions{}
w, h := img0.Size()
r := image.Rect(0, 0, w, h/2)
op.SourceRect = &r
op.GeoM.Translate(-float64(img0Width)/2, -float64(img0Height)/2)
op.GeoM.Rotate(a)
op.GeoM.Translate(img1Width/2, img1Height/2)
op.Filter = f
img1.DrawImage(img0, op)
for j := 0; j < img1Height; j++ {
for i := 0; i < img1Width; i++ {
c := img1.At(i, j)
if c == transparent {
continue
}
switch f {
case FilterNearest:
if c == red {
for _, s := range []float64{1, 0.5, 0.25} {
for _, f := range []Filter{FilterNearest, FilterLinear} {
for _, a := range angles {
img1.Clear()
op := &DrawImageOptions{}
r := image.Rect(img0OffsetWidth, img0InnerHeight, img0Width-img0OffsetWidth, img0Height-img0InnerHeight)
op.SourceRect = &r
w, h := img0.Size()
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
op.GeoM.Scale(s, s)
op.GeoM.Rotate(a)
op.GeoM.Translate(img1Width/2, img1Height/2)
op.Filter = f
img1.DrawImage(img0, op)
for j := 0; j < img1Height; j++ {
for i := 0; i < img1Width; i++ {
c := img1.At(i, j)
if c == transparent {
continue
}
case FilterLinear:
_, g, b, _ := c.RGBA()
if g == 0 && b == 0 {
continue
switch f {
case FilterNearest:
if c == red {
continue
}
case FilterLinear:
_, g, b, _ := c.RGBA()
if g == 0 && b == 0 {
continue
}
}
t.Errorf("img1.At(%d, %d) (filter: %d, scale: %f, angle: %f) want: red or transparent, got: %v", i, j, f, s, a, c)
}
t.Errorf("img1.At(%d, %d) (filter: %d, angle: %f) want: red or transparent, got: %v", i, j, f, a, c)
}
}
}