diff --git a/image.go b/image.go index d6d6f3da8..467b14e49 100644 --- a/image.go +++ b/image.go @@ -274,26 +274,23 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error { vs := graphics.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, 1, 1, 1, 1, filter == driver.FilterScreen) is := graphics.QuadIndices() - var sr driver.Region - // Pass the source region only when the shader is used, since this affects the condition of merging graphics - // commands (#1293). - if options.Shader != nil { - sr = driver.Region{ - X: float32(bounds.Min.X), - Y: float32(bounds.Min.Y), - Width: float32(bounds.Dx()), - Height: float32(bounds.Dy()), - } - } - srcs := [graphics.ShaderImageNum]*mipmap.Mipmap{img.mipmap} if options.Shader == nil { - i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.AddressUnsafe, sr, nil, nil, canSkipMipmap(options.GeoM, filter)) + i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, canSkipMipmap(options.GeoM, filter)) return nil } + // Pass the source region only when the shader is used, since this affects the condition of merging graphics + // commands (#1293). + sr := driver.Region{ + X: float32(bounds.Min.X), + Y: float32(bounds.Min.Y), + Width: float32(bounds.Dx()), + Height: float32(bounds.Dy()), + } + us := options.Shader.convertUniforms(options.Uniforms) - i.mipmap.DrawTriangles(srcs, vs, is, nil, mode, filter, driver.AddressUnsafe, sr, options.Shader.shader, us, canSkipMipmap(options.GeoM, filter)) + i.mipmap.DrawTriangles(srcs, vs, is, nil, mode, filter, driver.AddressUnsafe, sr, [graphics.ShaderImageNum - 1][2]float32{}, options.Shader.shader, us, canSkipMipmap(options.GeoM, filter)) return nil } @@ -489,12 +486,12 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o } if options.Shader == nil { - i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.Address(options.Address), sr, nil, nil, false) + i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.impl, mode, filter, driver.Address(options.Address), sr, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) return } us := options.Shader.convertUniforms(options.Uniforms) - i.mipmap.DrawTriangles(srcs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, sr, options.Shader.shader, us, false) + i.mipmap.DrawTriangles(srcs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, sr, [graphics.ShaderImageNum - 1][2]float32{}, options.Shader.shader, us, false) } // DrawRectShaderOptions represents options for DrawRectShader @@ -557,18 +554,21 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR if img.isDisposed() { panic("ebiten: the given image to DrawRectShader must not be disposed") } - if img.isSubImage() { - // TODO: Implement this. - panic("ebiten: rendering a sub-image is not implemented (DrawRectShader)") - } if w, h := img.Size(); width != w || height != h { panic("ebiten: all the source images must be the same size with the rectangle") } imgs[i] = img.mipmap } + sx, sy := float32(0), float32(0) + if options.Images[0] != nil { + b := options.Images[0].Bounds() + sx = float32(b.Min.X) + sy = float32(b.Min.Y) + } + a, b, c, d, tx, ty := options.GeoM.elements32() - vs := graphics.QuadVertices(0, 0, float32(width), float32(height), a, b, c, d, tx, ty, 1, 1, 1, 1, false) + vs := graphics.QuadVertices(sx, sy, sx+float32(width), sy+float32(height), a, b, c, d, tx, ty, 1, 1, 1, 1, false) is := graphics.QuadIndices() var sr driver.Region @@ -582,8 +582,18 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR } } + var offsets [graphics.ShaderImageNum - 1][2]float32 + for i, img := range options.Images[1:] { + if img == nil { + continue + } + b := img.Bounds() + offsets[i][0] = -sx + float32(b.Min.X) + offsets[i][1] = -sy + float32(b.Min.Y) + } + us := shader.convertUniforms(options.Uniforms) - i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, sr, shader.shader, us, canSkipMipmap(options.GeoM, driver.FilterNearest)) + i.mipmap.DrawTriangles(imgs, vs, is, nil, mode, driver.FilterNearest, driver.AddressUnsafe, sr, offsets, shader.shader, us, canSkipMipmap(options.GeoM, driver.FilterNearest)) } // SubImage returns an image representing the portion of the image p visible through r. diff --git a/internal/buffered/image.go b/internal/buffered/image.go index cdc8c141c..8afe9d410 100644 --- a/internal/buffered/image.go +++ b/internal/buffered/image.go @@ -248,7 +248,7 @@ func (i *Image) replacePendingPixels(pix []byte, x, y, width, height int) { // DrawTriangles draws the src image with the given vertices. // // Copying vertices and indices is the caller's responsibility. -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}) { for _, src := range srcs { if i == src { panic("buffered: Image.DrawTriangles: source images must be different from the receiver") @@ -258,7 +258,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f if maybeCanAddDelayedCommand() { if tryAddDelayedCommand(func() error { // Arguments are not copied. Copying is the caller's responsibility. - i.DrawTriangles(srcs, vertices, indices, colorm, mode, filter, address, sourceRegion, shader, uniforms) + i.DrawTriangles(srcs, vertices, indices, colorm, mode, filter, address, sourceRegion, subimageOffsets, shader, uniforms) return nil }) { return @@ -286,7 +286,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f imgs[i] = img.img } - i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, sourceRegion, s, uniforms) + i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, sourceRegion, subimageOffsets, s, uniforms) i.invalidatePendingPixels() } diff --git a/internal/mipmap/mipmap.go b/internal/mipmap/mipmap.go index ec0eb413d..5388dfa7d 100644 --- a/internal/mipmap/mipmap.go +++ b/internal/mipmap/mipmap.go @@ -97,7 +97,7 @@ func (m *Mipmap) Pixels(x, y, width, height int) ([]byte, error) { return m.orig.Pixels(x, y, width, height) } -func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, shader *Shader, uniforms []interface{}, canSkipMipmap bool) { +func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}, canSkipMipmap bool) { if len(indices) == 0 { return } @@ -176,7 +176,7 @@ func (m *Mipmap) DrawTriangles(srcs [graphics.ShaderImageNum]*Mipmap, vertices [ imgs[i] = src.orig } - m.orig.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, sourceRegion, s, uniforms) + m.orig.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, sourceRegion, subimageOffsets, s, uniforms) m.disposeMipmaps() } @@ -238,7 +238,7 @@ func (m *Mipmap) level(level int) *buffered.Image { } s := buffered.NewImage(w2, h2) s.SetVolatile(m.volatile) - s.DrawTriangles([graphics.ShaderImageNum]*buffered.Image{src}, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressUnsafe, driver.Region{}, nil, nil) + s.DrawTriangles([graphics.ShaderImageNum]*buffered.Image{src}, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) m.imgs[level] = s return m.imgs[level] diff --git a/internal/shareable/image.go b/internal/shareable/image.go index 360a62a91..9d143a9a4 100644 --- a/internal/shareable/image.go +++ b/internal/shareable/image.go @@ -306,7 +306,7 @@ func (i *Image) processSrc(src *Image) { // 5: Color G // 6: Color B // 7: Color Y -func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, shader *Shader, uniforms []interface{}) { +func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, sourceRegion driver.Region, subimageOffsets [graphics.ShaderImageNum - 1][2]float32, shader *Shader, uniforms []interface{}) { backendsM.Lock() // Do not use defer for performance. @@ -362,8 +362,8 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f ox, oy, _, _ := src.regionWithPadding() ox += paddingSize oy += paddingSize - offsets[i][0] = float32(ox) - oxf - offsets[i][1] = float32(oy) - oyf + offsets[i][0] = float32(ox) - oxf + subimageOffsets[i][0] + offsets[i][1] = float32(oy) - oyf + subimageOffsets[i][0] } var s *restorable.Shader diff --git a/internal/shareable/image_test.go b/internal/shareable/image_test.go index 040beb287..54fca6543 100644 --- a/internal/shareable/image_test.go +++ b/internal/shareable/image_test.go @@ -96,7 +96,7 @@ func TestEnsureNotShared(t *testing.T) { // img4.ensureNotShared() should be called. vs := quadVertices(size/2, size/2, size/4, size/4, 1) is := graphics.QuadIndices() - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) want := false if got := img4.IsSharedForTesting(); got != want { t.Errorf("got: %v, want: %v", got, want) @@ -126,7 +126,7 @@ func TestEnsureNotShared(t *testing.T) { // Check further drawing doesn't cause panic. // This bug was fixed by 03dcd948. - img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img4.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) } func TestReshared(t *testing.T) { @@ -167,7 +167,7 @@ func TestReshared(t *testing.T) { // Use img1 as a render target. vs := quadVertices(size, size, 0, 0, 1) is := graphics.QuadIndices() - img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img1.DrawTriangles([graphics.ShaderImageNum]*Image{img2}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) if got, want := img1.IsSharedForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -177,7 +177,7 @@ func TestReshared(t *testing.T) { if err := MakeImagesSharedForTesting(); err != nil { t.Fatal(err) } - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) if got, want := img1.IsSharedForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -204,7 +204,7 @@ func TestReshared(t *testing.T) { } } - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img1}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) if got, want := img1.IsSharedForTesting(), true; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -232,7 +232,7 @@ func TestReshared(t *testing.T) { if err := MakeImagesSharedForTesting(); err != nil { t.Fatal(err) } - img0.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + img0.DrawTriangles([graphics.ShaderImageNum]*Image{img3}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) if got, want := img3.IsSharedForTesting(), false; got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -327,7 +327,7 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) { vs := quadVertices(w, h, 0, 0, 1) is := graphics.QuadIndices() - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) dst.ReplacePixels(pix) pix, err := dst.Pixels(0, 0, w, h) @@ -369,7 +369,7 @@ func TestSmallImages(t *testing.T) { vs := quadVertices(w, h, 0, 0, 1) is := graphics.QuadIndices() - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) pix, err := dst.Pixels(0, 0, w, h) if err != nil { @@ -411,7 +411,7 @@ func TestLongImages(t *testing.T) { const scale = 120 vs := quadVertices(w, h, 0, 0, scale) is := graphics.QuadIndices() - dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil) + dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil) pix, err := dst.Pixels(0, 0, dstW, dstH) if err != nil { diff --git a/shader_test.go b/shader_test.go index 9e8dcdb80..e528cc6a4 100644 --- a/shader_test.go +++ b/shader_test.go @@ -15,6 +15,7 @@ package ebiten_test import ( + "image" "image/color" "testing" @@ -798,3 +799,66 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } } } + +func TestShaderSubImage(t *testing.T) { + const w, h = 16, 16 + + dst, _ := NewImage(w, h, FilterDefault) + s, err := NewShader([]byte(`package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + r := imageSrc0At(texCoord).r + g := imageSrc1At(texCoord).g + return vec4(r, g, 0, 1) +} +`)) + if err != nil { + t.Fatal(err) + } + + src0, _ := NewImage(w, h, FilterDefault) + pix0 := make([]byte, 4*w*h) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + if 2 <= i && i < 10 && 2 <= j && j < 10 { + pix0[4*(j*w+i)] = 0xff + pix0[4*(j*w+i)+1] = 0 + pix0[4*(j*w+i)+2] = 0 + pix0[4*(j*w+i)+3] = 0xff + } + } + } + src0.ReplacePixels(pix0) + + src1, _ := NewImage(w, h, FilterDefault) + pix1 := make([]byte, 4*w*h) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + if 6 <= i && i < 14 && 6 <= j && j < 14 { + pix1[4*(j*w+i)] = 0 + pix1[4*(j*w+i)+1] = 0xff + pix1[4*(j*w+i)+2] = 0 + pix1[4*(j*w+i)+3] = 0xff + } + } + } + src1.ReplacePixels(pix1) + + op := &DrawRectShaderOptions{} + op.Images[0] = src0.SubImage(image.Rect(2, 2, 10, 10)).(*Image) + op.Images[1] = src1.SubImage(image.Rect(6, 6, 14, 14)).(*Image) + dst.DrawRectShader(w/2, h/2, s, op) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + var want color.RGBA + if i < w/2 && j < h/2 { + want = color.RGBA{0xff, 0xff, 0, 0xff} + } + if got != want { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}