graphics: Appropriate rendering of edges on linear filter (Reland)

Fixes #456
This commit is contained in:
Hajime Hoshi 2017-12-03 21:13:21 +09:00
parent 8091aa5190
commit 1152439e65
15 changed files with 176 additions and 86 deletions

View File

@ -15,6 +15,7 @@
package ebiten
import (
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/opengl"
)
@ -23,22 +24,12 @@ type Filter int
const (
// FilterNearest represents nearest (crisp-edged) filter
FilterNearest Filter = iota
FilterNearest Filter = Filter(graphics.FilterNearest)
// FilterLinear represents linear filter
FilterLinear
FilterLinear Filter = Filter(graphics.FilterLinear)
)
func glFilter(filter Filter) opengl.Filter {
switch filter {
case FilterNearest:
return opengl.Nearest
case FilterLinear:
return opengl.Linear
}
panic("not reach")
}
// CompositeMode represents Porter-Duff composition mode.
type CompositeMode int

View File

@ -20,6 +20,7 @@ import (
"image/color"
"runtime"
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/math"
"github.com/hajimehoshi/ebiten/internal/opengl"
"github.com/hajimehoshi/ebiten/internal/restorable"
@ -244,7 +245,7 @@ type DrawImageOptions struct {
// Error returned by NewImage is always nil as of 1.5.0-alpha.
func NewImage(width, height int, filter Filter) (*Image, error) {
checkSize(width, height)
r := restorable.NewImage(width, height, glFilter(filter), false)
r := restorable.NewImage(width, height, graphics.Filter(filter), false)
r.Fill(0, 0, 0, 0)
i := &Image{r}
runtime.SetFinalizer(i, (*Image).Dispose)
@ -268,7 +269,7 @@ func NewImage(width, height int, filter Filter) (*Image, error) {
// Error returned by newVolatileImage is always nil as of 1.5.0-alpha.
func newVolatileImage(width, height int, filter Filter) *Image {
checkSize(width, height)
r := restorable.NewImage(width, height, glFilter(filter), true)
r := restorable.NewImage(width, height, graphics.Filter(filter), true)
r.Fill(0, 0, 0, 0)
i := &Image{r}
runtime.SetFinalizer(i, (*Image).Dispose)
@ -283,7 +284,7 @@ func newVolatileImage(width, height int, filter Filter) *Image {
func NewImageFromImage(source image.Image, filter Filter) (*Image, error) {
size := source.Bounds().Size()
checkSize(size.X, size.Y)
r := restorable.NewImageFromImage(source, glFilter(filter))
r := restorable.NewImageFromImage(source, graphics.Filter(filter))
i := &Image{r}
runtime.SetFinalizer(i, (*Image).Dispose)
return i, nil

View File

@ -25,6 +25,7 @@ import (
"testing"
. "github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
emath "github.com/hajimehoshi/ebiten/internal/math"
)
@ -595,3 +596,28 @@ func BenchmarkDrawImage(b *testing.B) {
img0.DrawImage(img1, op)
}
}
func TestImageLinear(t *testing.T) {
src, _ := NewImage(32, 32, FilterLinear)
dst, _ := NewImage(64, 64, FilterNearest)
src.Fill(color.RGBA{0, 0xff, 0, 0xff})
ebitenutil.DrawRect(src, 8, 8, 16, 16, color.RGBA{0xff, 0, 0, 0xff})
op := &DrawImageOptions{}
op.GeoM.Translate(8, 8)
op.GeoM.Scale(2, 2)
r := image.Rect(8, 8, 24, 24)
op.SourceRect = &r
dst.DrawImage(src, op)
for j := 0; j < 64; j++ {
for i := 0; i < 64; i++ {
c := color.RGBAModel.Convert(dst.At(i, j)).(color.RGBA)
got := c.G
want := uint8(0)
if got != want {
t.Errorf("dst At(%d, %d).G: got %#v, want: %#v", i, j, got, want)
}
}
}
}

View File

@ -242,12 +242,18 @@ func (c *drawImageCommand) Exec(indexOffsetInBytes int) error {
if n == 0 {
return nil
}
_, h := c.dst.Size()
proj := f.projectionMatrix(h)
theOpenGLState.useProgram(proj, c.src.texture.native, c.color)
sw, sh := c.src.Size()
sw = emath.NextPowerOf2Int(sw)
sh = emath.NextPowerOf2Int(sh)
_, dh := c.dst.Size()
proj := f.projectionMatrix(dh)
theOpenGLState.useProgram(proj, c.src.texture.native, sw, sh, c.color, c.src.texture.filter)
// TODO: We should call glBindBuffer here?
// The buffer is already bound at begin() but it is counterintuitive.
opengl.GetContext().DrawElements(opengl.Triangles, 6*n, indexOffsetInBytes)
// This is necessary at least on MacBook Pro (a smilar problem at #419)
opengl.GetContext().Flush()
return nil
}
@ -338,7 +344,7 @@ func (c *disposeCommand) Exec(indexOffsetInBytes int) error {
type newImageFromImageCommand struct {
result *Image
img *image.RGBA
filter opengl.Filter
filter Filter
}
// Exec executes the newImageFromImageCommand.
@ -354,12 +360,13 @@ func (c *newImageFromImageCommand) Exec(indexOffsetInBytes int) error {
if c.img.Bounds() != image.Rect(0, 0, emath.NextPowerOf2Int(w), emath.NextPowerOf2Int(h)) {
panic(fmt.Sprintf("graphics: invalid image bounds: %v", c.img.Bounds()))
}
native, err := opengl.GetContext().NewTexture(w, h, c.img.Pix, c.filter)
native, err := opengl.GetContext().NewTexture(w, h, c.img.Pix)
if err != nil {
return err
}
c.result.texture = &texture{
native: native,
filter: c.filter,
}
return nil
}
@ -369,7 +376,7 @@ type newImageCommand struct {
result *Image
width int
height int
filter opengl.Filter
filter Filter
}
// Exec executes a newImageCommand.
@ -382,12 +389,13 @@ func (c *newImageCommand) Exec(indexOffsetInBytes int) error {
if h < 1 {
return errors.New("graphics: height must be equal or more than 1.")
}
native, err := opengl.GetContext().NewTexture(w, h, nil, c.filter)
native, err := opengl.GetContext().NewTexture(w, h, nil)
if err != nil {
return err
}
c.result.texture = &texture{
native: native,
filter: c.filter,
}
return nil
}

View File

@ -33,7 +33,7 @@ type Image struct {
// MaxImageSize is the maximum of width/height of an image.
const MaxImageSize = defaultViewportSize
func NewImage(width, height int, filter opengl.Filter) *Image {
func NewImage(width, height int, filter Filter) *Image {
i := &Image{
width: width,
height: height,
@ -48,7 +48,7 @@ func NewImage(width, height int, filter opengl.Filter) *Image {
return i
}
func NewImageFromImage(img *image.RGBA, width, height int, filter opengl.Filter) *Image {
func NewImageFromImage(img *image.RGBA, width, height int, filter Filter) *Image {
i := &Image{
width: width,
height: height,

View File

@ -121,6 +121,9 @@ type openGLState struct {
lastProjectionMatrix []float32
lastColorMatrix []float32
lastColorMatrixTranslation []float32
lastFilterType Filter
lastSourceWidth int
lastSourceHeight int
}
var (
@ -150,6 +153,9 @@ func (s *openGLState) reset() error {
s.lastProjectionMatrix = nil
s.lastColorMatrix = nil
s.lastColorMatrixTranslation = nil
s.lastFilterType = FilterNone
s.lastSourceWidth = 0
s.lastSourceHeight = 0
// When context lost happens, deleting programs or buffers is not necessary.
// However, it is not assumed that reset is called only when context lost happens.
@ -214,7 +220,7 @@ func areSameFloat32Array(a, b []float32) bool {
}
// useProgram uses the program (programTexture).
func (s *openGLState) useProgram(proj []float32, texture opengl.Texture, colorM affine.ColorM) {
func (s *openGLState) useProgram(proj []float32, texture opengl.Texture, sourceWidth, sourceHeight int, colorM affine.ColorM, filter Filter) {
c := opengl.GetContext()
program := s.programTexture
@ -273,6 +279,18 @@ func (s *openGLState) useProgram(proj []float32, texture opengl.Texture, colorM
copy(s.lastColorMatrixTranslation, colorMatrixTranslation)
}
if s.lastFilterType != filter {
c.UniformInt(program, "filter_type", int(filter))
s.lastFilterType = filter
}
if s.lastSourceWidth != sourceWidth || s.lastSourceHeight != sourceHeight {
c.UniformFloats(program, "source_size",
[]float32{float32(sourceWidth), float32(sourceHeight)})
s.lastSourceWidth = sourceWidth
s.lastSourceHeight = sourceHeight
}
// We don't have to call gl.ActiveTexture here: GL_TEXTURE0 is the default active texture
// See also: https://www.opengl.org/sdk/docs/man2/xhtml/glActiveTexture.xml
c.BindTexture(texture)

View File

@ -63,17 +63,50 @@ precision mediump float;
uniform sampler2D texture;
uniform mat4 color_matrix;
uniform vec4 color_matrix_translation;
uniform int filter_type;
uniform vec2 source_size;
varying vec2 varying_tex_coord;
varying vec2 varying_tex_coord_min;
varying vec2 varying_tex_coord_max;
vec2 roundTexel(vec2 p) {
// Many devices can't use highp in fragment shaders, so use only mediump here.
// According to the spec, mediump value range is -2**14 to 2**14 (16384).
// Some machines have higher precisions, but a very slight value difference causes
// a different result of getColorAt. This function avoids such flakines by rounding values.
float max_mediump = 16384.0;
return floor(p * max_mediump + 0.5) / max_mediump;
}
vec4 getColorAt(vec2 pos) {
if (pos.x < varying_tex_coord_min.x ||
pos.y < varying_tex_coord_min.y ||
varying_tex_coord_max.x <= pos.x ||
varying_tex_coord_max.y <= pos.y) {
return vec4(0, 0, 0, 0);
}
return texture2D(texture, pos);
}
void main(void) {
vec4 color = vec4(0, 0, 0, 0);
if (varying_tex_coord_min.x <= varying_tex_coord.x &&
varying_tex_coord_min.y <= varying_tex_coord.y &&
varying_tex_coord.x < varying_tex_coord_max.x &&
varying_tex_coord.y < varying_tex_coord_max.y) {
color = texture2D(texture, varying_tex_coord);
vec2 pos = roundTexel(varying_tex_coord);
if (filter_type == 1) {
// Nearest neighbor
color = getColorAt(pos);
} else if (filter_type == 2) {
// Bi-linear
vec2 texel_size = 1.0 / source_size;
pos -= texel_size * 0.5;
vec4 c0 = getColorAt(pos);
vec4 c1 = getColorAt(pos + vec2(texel_size.x, 0));
vec4 c2 = getColorAt(pos + vec2(0, texel_size.y));
vec4 c3 = getColorAt(pos + texel_size);
float rateX = fract(pos.x * source_size.x);
float rateY = fract(pos.y * source_size.y);
color = mix(mix(c0, c1, rateX), mix(c2, c3, rateX), rateY);
}
// Un-premultiply alpha

View File

@ -18,7 +18,16 @@ import (
"github.com/hajimehoshi/ebiten/internal/opengl"
)
type Filter int
const (
FilterNone Filter = iota
FilterNearest
FilterLinear
)
// texture represents OpenGL's texture.
type texture struct {
native opengl.Texture
filter Filter
}

View File

@ -38,8 +38,6 @@ func adjustForClearColor(x float32) float32 {
}
var (
Nearest Filter
Linear Filter
VertexShader ShaderType
FragmentShader ShaderType
ArrayBuffer BufferType

View File

@ -26,11 +26,13 @@ import (
"github.com/go-gl/gl/v2.1/gl"
)
type Texture uint32
type Framebuffer uint32
type Shader uint32
type Program uint32
type Buffer uint32
type (
Texture uint32
Framebuffer uint32
Shader uint32
Program uint32
Buffer uint32
)
func (t Texture) equals(other Texture) bool {
return t == other
@ -40,8 +42,10 @@ func (f Framebuffer) equals(other Framebuffer) bool {
return f == other
}
type uniformLocation int32
type attribLocation int32
type (
uniformLocation int32
attribLocation int32
)
type programID uint32
@ -55,8 +59,6 @@ func (p Program) id() programID {
}
func init() {
Nearest = gl.NEAREST
Linear = gl.LINEAR
VertexShader = gl.VERTEX_SHADER
FragmentShader = gl.FRAGMENT_SHADER
ArrayBuffer = gl.ARRAY_BUFFER
@ -137,7 +139,7 @@ func (c *Context) BlendFunc(mode CompositeMode) {
})
}
func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (Texture, error) {
func (c *Context) NewTexture(width, height int, pixels []uint8) (Texture, error) {
var texture Texture
if err := c.runOnContextThread(func() error {
var t uint32
@ -154,10 +156,8 @@ func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (
}
c.BindTexture(texture)
_ = c.runOnContextThread(func() error {
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, int32(filter))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, int32(filter))
//gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP)
//gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
var p interface{}
if pixels != nil {
@ -408,12 +408,14 @@ func (c *Context) UniformFloats(p Program, location string, v []float32) {
_ = c.runOnContextThread(func() error {
l := int32(c.locationCache.GetUniformLocation(c, p, location))
switch len(v) {
case 2:
gl.Uniform2fv(l, 1, (*float32)(gl.Ptr(v)))
case 4:
gl.Uniform4fv(l, 1, (*float32)(gl.Ptr(v)))
case 16:
gl.UniformMatrix4fv(l, 1, false, (*float32)(gl.Ptr(v)))
default:
panic("not reach")
panic("not reached")
}
return nil
})

View File

@ -77,8 +77,6 @@ func (p Program) id() programID {
func init() {
// Accessing the prototype is rquired on Safari.
c := js.Global.Get("WebGLRenderingContext").Get("prototype")
Nearest = Filter(c.Get("NEAREST").Int())
Linear = Filter(c.Get("LINEAR").Int())
VertexShader = ShaderType(c.Get("VERTEX_SHADER").Int())
FragmentShader = ShaderType(c.Get("FRAGMENT_SHADER").Int())
ArrayBuffer = BufferType(c.Get("ARRAY_BUFFER").Int())
@ -159,7 +157,7 @@ func (c *Context) BlendFunc(mode CompositeMode) {
gl.BlendFunc(int(s), int(d))
}
func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (Texture, error) {
func (c *Context) NewTexture(width, height int, pixels []uint8) (Texture, error) {
gl := c.gl
t := gl.CreateTexture()
if t == nil {
@ -168,8 +166,8 @@ func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 4)
c.BindTexture(Texture{t})
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, int(filter))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, int(filter))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
// TODO: Can we use glTexSubImage2D with linear filtering?
@ -348,12 +346,14 @@ func (c *Context) UniformFloats(p Program, location string, v []float32) {
gl := c.gl
l := c.locationCache.GetUniformLocation(c, p, location)
switch len(v) {
case 2:
gl.Call("uniform2fv", l.Object, v)
case 4:
gl.Call("uniform4fv", l.Object, v)
case 16:
gl.UniformMatrix4fv(l.Object, false, v)
default:
panic("not reach")
panic("not reached")
}
}

View File

@ -25,11 +25,13 @@ import (
mgl "golang.org/x/mobile/gl"
)
type Texture mgl.Texture
type Framebuffer mgl.Framebuffer
type Shader mgl.Shader
type Program mgl.Program
type Buffer mgl.Buffer
type (
Texture mgl.Texture
Framebuffer mgl.Framebuffer
Shader mgl.Shader
Program mgl.Program
Buffer mgl.Buffer
)
func (t Texture) equals(other Texture) bool {
return t == other
@ -39,8 +41,10 @@ func (f Framebuffer) equals(other Framebuffer) bool {
return f == other
}
type uniformLocation mgl.Uniform
type attribLocation mgl.Attrib
type (
uniformLocation mgl.Uniform
attribLocation mgl.Attrib
)
type programID uint32
@ -54,8 +58,6 @@ func (p Program) id() programID {
}
func init() {
Nearest = mgl.NEAREST
Linear = mgl.LINEAR
VertexShader = mgl.VERTEX_SHADER
FragmentShader = mgl.FRAGMENT_SHADER
ArrayBuffer = mgl.ARRAY_BUFFER
@ -127,7 +129,7 @@ func (c *Context) BlendFunc(mode CompositeMode) {
gl.BlendFunc(mgl.Enum(s), mgl.Enum(d))
}
func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (Texture, error) {
func (c *Context) NewTexture(width, height int, pixels []uint8) (Texture, error) {
gl := c.gl
t := gl.CreateTexture()
if t.Value <= 0 {
@ -136,8 +138,8 @@ func (c *Context) NewTexture(width, height int, pixels []uint8, filter Filter) (
gl.PixelStorei(mgl.UNPACK_ALIGNMENT, 4)
c.BindTexture(Texture(t))
gl.TexParameteri(mgl.TEXTURE_2D, mgl.TEXTURE_MAG_FILTER, int(filter))
gl.TexParameteri(mgl.TEXTURE_2D, mgl.TEXTURE_MIN_FILTER, int(filter))
gl.TexParameteri(mgl.TEXTURE_2D, mgl.TEXTURE_MAG_FILTER, mgl.NEAREST)
gl.TexParameteri(mgl.TEXTURE_2D, mgl.TEXTURE_MIN_FILTER, mgl.NEAREST)
var p []uint8
if pixels != nil {
@ -317,12 +319,14 @@ func (c *Context) UniformFloats(p Program, location string, v []float32) {
gl := c.gl
l := mgl.Uniform(c.locationCache.GetUniformLocation(c, p, location))
switch len(v) {
case 2:
gl.Uniform2fv(l, v)
case 4:
gl.Uniform4fv(l, v)
case 16:
gl.UniformMatrix4fv(l, v)
default:
panic("not reach")
panic("not reached")
}
}

View File

@ -15,7 +15,6 @@
package opengl
type (
Filter int
ShaderType int
BufferType int
BufferUsage int

View File

@ -60,7 +60,7 @@ func (d *drawImageHistoryItem) canMerge(image *Image, colorm *affine.ColorM, mod
// Image represents an image that can be restored when GL context is lost.
type Image struct {
image *graphics.Image
filter opengl.Filter
filter graphics.Filter
// baseImage and baseColor are exclusive.
basePixels []uint8
@ -84,7 +84,7 @@ type Image struct {
}
// NewImage creates an empty image with the given size and filter.
func NewImage(width, height int, filter opengl.Filter, volatile bool) *Image {
func NewImage(width, height int, filter graphics.Filter, volatile bool) *Image {
i := &Image{
image: graphics.NewImage(width, height, filter),
filter: filter,
@ -96,7 +96,7 @@ func NewImage(width, height int, filter opengl.Filter, volatile bool) *Image {
}
// NewImageFromImage creates an image with source image.
func NewImageFromImage(source image.Image, filter opengl.Filter) *Image {
func NewImageFromImage(source image.Image, filter graphics.Filter) *Image {
size := source.Bounds().Size()
width, height := size.X, size.Y
rgbaImg := CopyImage(source)

View File

@ -23,6 +23,7 @@ import (
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/opengl"
. "github.com/hajimehoshi/ebiten/internal/restorable"
)
@ -47,7 +48,7 @@ func uint8SliceToColor(b []uint8, index int) color.RGBA {
}
func TestRestore(t *testing.T) {
img0 := NewImage(1, 1, opengl.Nearest, false)
img0 := NewImage(1, 1, graphics.FilterNearest, false)
// Clear images explicitly.
// In this 'restorable' layer, reused texture might not be cleared.
img0.Fill(0, 0, 0, 0)
@ -89,7 +90,7 @@ func TestRestoreChain(t *testing.T) {
const num = 10
imgs := []*Image{}
for i := 0; i < num; i++ {
img := NewImage(1, 1, opengl.Nearest, false)
img := NewImage(1, 1, graphics.FilterNearest, false)
img.Fill(0, 0, 0, 0)
imgs = append(imgs, img)
}
@ -119,13 +120,13 @@ func TestRestoreChain(t *testing.T) {
}
func TestRestoreOverrideSource(t *testing.T) {
img0 := NewImage(1, 1, opengl.Nearest, false)
img0 := NewImage(1, 1, graphics.FilterNearest, false)
img0.Fill(0, 0, 0, 0)
img1 := NewImage(1, 1, opengl.Nearest, false)
img1 := NewImage(1, 1, graphics.FilterNearest, false)
img1.Fill(0, 0, 0, 0)
img2 := NewImage(1, 1, opengl.Nearest, false)
img2 := NewImage(1, 1, graphics.FilterNearest, false)
img2.Fill(0, 0, 0, 0)
img3 := NewImage(1, 1, opengl.Nearest, false)
img3 := NewImage(1, 1, graphics.FilterNearest, false)
img3.Fill(0, 0, 0, 0)
defer func() {
img3.Dispose()
@ -194,18 +195,18 @@ func TestRestoreComplexGraph(t *testing.T) {
base.Pix[1] = 0xff
base.Pix[2] = 0xff
base.Pix[3] = 0xff
img0 := NewImageFromImage(base, opengl.Nearest)
img1 := NewImageFromImage(base, opengl.Nearest)
img2 := NewImageFromImage(base, opengl.Nearest)
img3 := NewImage(4, 1, opengl.Nearest, false)
img0 := NewImageFromImage(base, graphics.FilterNearest)
img1 := NewImageFromImage(base, graphics.FilterNearest)
img2 := NewImageFromImage(base, graphics.FilterNearest)
img3 := NewImage(4, 1, graphics.FilterNearest, false)
img3.Fill(0, 0, 0, 0)
img4 := NewImage(4, 1, opengl.Nearest, false)
img4 := NewImage(4, 1, graphics.FilterNearest, false)
img4.Fill(0, 0, 0, 0)
img5 := NewImage(4, 1, opengl.Nearest, false)
img5 := NewImage(4, 1, graphics.FilterNearest, false)
img5.Fill(0, 0, 0, 0)
img6 := NewImage(4, 1, opengl.Nearest, false)
img6 := NewImage(4, 1, graphics.FilterNearest, false)
img6.Fill(0, 0, 0, 0)
img7 := NewImage(4, 1, opengl.Nearest, false)
img7 := NewImage(4, 1, graphics.FilterNearest, false)
img7.Fill(0, 0, 0, 0)
defer func() {
img7.Dispose()
@ -298,8 +299,8 @@ func TestRestoreRecursive(t *testing.T) {
base.Pix[1] = 0xff
base.Pix[2] = 0xff
base.Pix[3] = 0xff
img0 := NewImageFromImage(base, opengl.Nearest)
img1 := NewImage(4, 1, opengl.Nearest, false)
img0 := NewImageFromImage(base, graphics.FilterNearest)
img1 := NewImage(4, 1, graphics.FilterNearest, false)
img1.Fill(0, 0, 0, 0)
defer func() {
img1.Dispose()