mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
ebiten: add NewImageWithOptions and NewImageOptions
This change adds NewImageWithOptions, that creates a new image with the given options. NewImageWithOptions takes image.Rectangle instead of a width and a height, then a user can create an image with an arbitrary bounds. A left-upper position can be a negative number. NewImageWithOptions can create an unmanged image, that is no longer on an automatic internal texture atlas. A user can have finer controls over the image. This change also adds tests for this function. Updates #2013 Updates #2017 Updates #2124
This commit is contained in:
parent
7a94140724
commit
bac34a4474
@ -15,6 +15,8 @@
|
||||
package ebiten
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
||||
)
|
||||
@ -45,7 +47,7 @@ func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
|
||||
// A violatile image is also always isolated.
|
||||
imageType = atlas.ImageTypeVolatile
|
||||
}
|
||||
c.offscreen = newImage(width, height, imageType)
|
||||
c.offscreen = newImage(image.Rect(0, 0, width, height), imageType)
|
||||
return c.offscreen.image
|
||||
}
|
||||
|
||||
|
236
image.go
236
image.go
@ -111,6 +111,47 @@ type DrawImageOptions struct {
|
||||
Filter Filter
|
||||
}
|
||||
|
||||
// adjustPosition converts the position in the *ebiten.Image coordinate to the *ui.Image coordinate.
|
||||
func (i *Image) adjustPosition(x, y int) (int, int) {
|
||||
if i.isSubImage() {
|
||||
or := i.original.Bounds()
|
||||
x -= or.Min.X
|
||||
y -= or.Min.Y
|
||||
return x, y
|
||||
}
|
||||
|
||||
r := i.Bounds()
|
||||
x -= r.Min.X
|
||||
y -= r.Min.Y
|
||||
return x, y
|
||||
}
|
||||
|
||||
// adjustPositionF32 converts the position in the *ebiten.Image coordinate to the *ui.Image coordinate.
|
||||
func (i *Image) adjustPositionF32(x, y float32) (float32, float32) {
|
||||
if i.isSubImage() {
|
||||
or := i.original.Bounds()
|
||||
x -= float32(or.Min.X)
|
||||
y -= float32(or.Min.Y)
|
||||
return x, y
|
||||
}
|
||||
|
||||
r := i.Bounds()
|
||||
x -= float32(r.Min.X)
|
||||
y -= float32(r.Min.Y)
|
||||
return x, y
|
||||
}
|
||||
|
||||
func (i *Image) adjustedRegion() graphicsdriver.Region {
|
||||
b := i.Bounds()
|
||||
x, y := i.adjustPosition(b.Min.X, b.Min.Y)
|
||||
return graphicsdriver.Region{
|
||||
X: float32(x),
|
||||
Y: float32(y),
|
||||
Width: float32(b.Dx()),
|
||||
Height: float32(b.Dy()),
|
||||
}
|
||||
}
|
||||
|
||||
// DrawImage draws the given image on the image i.
|
||||
//
|
||||
// DrawImage accepts the options. For details, see the document of
|
||||
@ -156,37 +197,30 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
dstBounds := i.Bounds()
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: float32(dstBounds.Min.X),
|
||||
Y: float32(dstBounds.Min.Y),
|
||||
Width: float32(dstBounds.Dx()),
|
||||
Height: float32(dstBounds.Dy()),
|
||||
}
|
||||
|
||||
// Calculate vertices before locking because the user can do anything in
|
||||
// options.ImageParts interface without deadlock (e.g. Call Image functions).
|
||||
if options == nil {
|
||||
options = &DrawImageOptions{}
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
mode := graphicsdriver.CompositeMode(options.CompositeMode)
|
||||
filter := graphicsdriver.Filter(options.Filter)
|
||||
|
||||
if offsetX, offsetY := i.adjustPosition(0, 0); offsetX != 0 || offsetY != 0 {
|
||||
options.GeoM.Translate(float64(offsetX), float64(offsetY))
|
||||
}
|
||||
a, b, c, d, tx, ty := options.GeoM.elements32()
|
||||
|
||||
sx0 := float32(bounds.Min.X)
|
||||
sy0 := float32(bounds.Min.Y)
|
||||
sx1 := float32(bounds.Max.X)
|
||||
sy1 := float32(bounds.Max.Y)
|
||||
bounds := img.Bounds()
|
||||
sx0, sy0 := img.adjustPosition(bounds.Min.X, bounds.Min.Y)
|
||||
sx1, sy1 := img.adjustPosition(bounds.Max.X, bounds.Max.Y)
|
||||
colorm, cr, cg, cb, ca := colorMToScale(options.ColorM.affineColorM())
|
||||
vs := graphics.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, cr, cg, cb, ca)
|
||||
vs := graphics.QuadVertices(float32(sx0), float32(sy0), float32(sx1), float32(sy1), a, b, c, d, tx, ty, cr, cg, cb, ca)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
srcs := [graphics.ShaderImageNum]*ui.Image{img.image}
|
||||
|
||||
i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, canSkipMipmap(options.GeoM, filter))
|
||||
i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, graphicsdriver.AddressUnsafe, i.adjustedRegion(), graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, canSkipMipmap(options.GeoM, filter))
|
||||
}
|
||||
|
||||
// Vertex represents a vertex passed to DrawTriangles.
|
||||
@ -311,14 +345,6 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
|
||||
}
|
||||
// TODO: Check the maximum value of indices and len(vertices)?
|
||||
|
||||
dstBounds := i.Bounds()
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: float32(dstBounds.Min.X),
|
||||
Y: float32(dstBounds.Min.Y),
|
||||
Width: float32(dstBounds.Dx()),
|
||||
Height: float32(dstBounds.Dy()),
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = &DrawTrianglesOptions{}
|
||||
}
|
||||
@ -328,13 +354,7 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
|
||||
address := graphicsdriver.Address(options.Address)
|
||||
var sr graphicsdriver.Region
|
||||
if address != graphicsdriver.AddressUnsafe {
|
||||
b := img.Bounds()
|
||||
sr = graphicsdriver.Region{
|
||||
X: float32(b.Min.X),
|
||||
Y: float32(b.Min.Y),
|
||||
Width: float32(b.Dx()),
|
||||
Height: float32(b.Dy()),
|
||||
}
|
||||
sr = img.adjustedRegion()
|
||||
}
|
||||
|
||||
filter := graphicsdriver.Filter(options.Filter)
|
||||
@ -342,11 +362,14 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
|
||||
colorm, cr, cg, cb, ca := colorMToScale(options.ColorM.affineColorM())
|
||||
|
||||
vs := graphics.Vertices(len(vertices))
|
||||
dst := i
|
||||
for i, v := range vertices {
|
||||
vs[i*graphics.VertexFloatNum] = v.DstX
|
||||
vs[i*graphics.VertexFloatNum+1] = v.DstY
|
||||
vs[i*graphics.VertexFloatNum+2] = v.SrcX
|
||||
vs[i*graphics.VertexFloatNum+3] = v.SrcY
|
||||
dx, dy := dst.adjustPositionF32(v.DstX, v.DstY)
|
||||
vs[i*graphics.VertexFloatNum] = dx
|
||||
vs[i*graphics.VertexFloatNum+1] = dy
|
||||
sx, sy := img.adjustPositionF32(v.SrcX, v.SrcY)
|
||||
vs[i*graphics.VertexFloatNum+2] = sx
|
||||
vs[i*graphics.VertexFloatNum+3] = sy
|
||||
vs[i*graphics.VertexFloatNum+4] = v.ColorR * cr
|
||||
vs[i*graphics.VertexFloatNum+5] = v.ColorG * cg
|
||||
vs[i*graphics.VertexFloatNum+6] = v.ColorB * cb
|
||||
@ -357,7 +380,7 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
|
||||
|
||||
srcs := [graphics.ShaderImageNum]*ui.Image{img.image}
|
||||
|
||||
i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, address, dstRegion, sr, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, options.FillRule == EvenOdd, false)
|
||||
i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, address, i.adjustedRegion(), sr, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, options.FillRule == EvenOdd, false)
|
||||
}
|
||||
|
||||
// DrawTrianglesShaderOptions represents options for DrawTrianglesShader.
|
||||
@ -377,7 +400,7 @@ type DrawTrianglesShaderOptions struct {
|
||||
Uniforms map[string]interface{}
|
||||
|
||||
// Images is a set of the source images.
|
||||
// All the image must be the same size.
|
||||
// All the image must be the same bounds.
|
||||
Images [4]*Image
|
||||
|
||||
// FillRule indicates the rule how an overlapped region is rendered.
|
||||
@ -427,14 +450,6 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
|
||||
}
|
||||
// TODO: Check the maximum value of indices and len(vertices)?
|
||||
|
||||
dstBounds := i.Bounds()
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: float32(dstBounds.Min.X),
|
||||
Y: float32(dstBounds.Min.Y),
|
||||
Width: float32(dstBounds.Dx()),
|
||||
Height: float32(dstBounds.Dy()),
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = &DrawTrianglesShaderOptions{}
|
||||
}
|
||||
@ -442,11 +457,18 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
|
||||
mode := graphicsdriver.CompositeMode(options.CompositeMode)
|
||||
|
||||
vs := graphics.Vertices(len(vertices))
|
||||
dst := i
|
||||
src := options.Images[0]
|
||||
for i, v := range vertices {
|
||||
vs[i*graphics.VertexFloatNum] = v.DstX
|
||||
vs[i*graphics.VertexFloatNum+1] = v.DstY
|
||||
vs[i*graphics.VertexFloatNum+2] = v.SrcX
|
||||
vs[i*graphics.VertexFloatNum+3] = v.SrcY
|
||||
dx, dy := dst.adjustPositionF32(v.DstX, v.DstY)
|
||||
vs[i*graphics.VertexFloatNum] = dx
|
||||
vs[i*graphics.VertexFloatNum+1] = dy
|
||||
sx, sy := v.SrcX, v.SrcY
|
||||
if src != nil {
|
||||
sx, sy = src.adjustPositionF32(sx, sy)
|
||||
}
|
||||
vs[i*graphics.VertexFloatNum+2] = sx
|
||||
vs[i*graphics.VertexFloatNum+3] = sy
|
||||
vs[i*graphics.VertexFloatNum+4] = v.ColorR
|
||||
vs[i*graphics.VertexFloatNum+5] = v.ColorG
|
||||
vs[i*graphics.VertexFloatNum+6] = v.ColorB
|
||||
@ -475,22 +497,12 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
|
||||
imgs[i] = img.image
|
||||
}
|
||||
|
||||
var sx, sy float32
|
||||
if options.Images[0] != nil {
|
||||
b := options.Images[0].Bounds()
|
||||
sx = float32(b.Min.X)
|
||||
sy = float32(b.Min.Y)
|
||||
}
|
||||
|
||||
var sx, sy int
|
||||
var sr graphicsdriver.Region
|
||||
if img := options.Images[0]; img != nil {
|
||||
b := img.Bounds()
|
||||
sr = graphicsdriver.Region{
|
||||
X: float32(b.Min.X),
|
||||
Y: float32(b.Min.Y),
|
||||
Width: float32(b.Dx()),
|
||||
Height: float32(b.Dy()),
|
||||
}
|
||||
sx, sy = img.adjustPosition(b.Min.X, b.Min.Y)
|
||||
sr = img.adjustedRegion()
|
||||
}
|
||||
|
||||
var offsets [graphics.ShaderImageNum - 1][2]float32
|
||||
@ -499,11 +511,14 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
|
||||
continue
|
||||
}
|
||||
b := img.Bounds()
|
||||
offsets[i][0] = -sx + float32(b.Min.X)
|
||||
offsets[i][1] = -sy + float32(b.Min.Y)
|
||||
x, y := img.adjustPosition(b.Min.X, b.Min.Y)
|
||||
// (sx, sy) is the left-upper position of the first image.
|
||||
// Calculate the direction between the current image's left-upper position and the first one's.
|
||||
offsets[i][0] = float32(x - sx)
|
||||
offsets[i][1] = float32(y - sy)
|
||||
}
|
||||
|
||||
i.image.DrawTriangles(imgs, vs, is, affine.ColorMIdentity{}, mode, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), options.FillRule == EvenOdd, false)
|
||||
i.image.DrawTriangles(imgs, vs, is, affine.ColorMIdentity{}, mode, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), options.FillRule == EvenOdd, false)
|
||||
}
|
||||
|
||||
// DrawRectShaderOptions represents options for DrawRectShader.
|
||||
@ -527,7 +542,7 @@ type DrawRectShaderOptions struct {
|
||||
Uniforms map[string]interface{}
|
||||
|
||||
// Images is a set of the source images.
|
||||
// All the image must be the same size with the rectangle.
|
||||
// All the image must be the same bounds.
|
||||
Images [4]*Image
|
||||
}
|
||||
|
||||
@ -554,14 +569,6 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
|
||||
return
|
||||
}
|
||||
|
||||
dstBounds := i.Bounds()
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: float32(dstBounds.Min.X),
|
||||
Y: float32(dstBounds.Min.Y),
|
||||
Width: float32(dstBounds.Dx()),
|
||||
Height: float32(dstBounds.Dy()),
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = &DrawRectShaderOptions{}
|
||||
}
|
||||
@ -582,39 +589,35 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
|
||||
imgs[i] = img.image
|
||||
}
|
||||
|
||||
var sx, sy float32
|
||||
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(sx, sy, sx+float32(width), sy+float32(height), a, b, c, d, tx, ty, 1, 1, 1, 1)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
var sx, sy int
|
||||
var sr graphicsdriver.Region
|
||||
if img := options.Images[0]; img != nil {
|
||||
b := img.Bounds()
|
||||
sr = graphicsdriver.Region{
|
||||
X: float32(b.Min.X),
|
||||
Y: float32(b.Min.Y),
|
||||
Width: float32(b.Dx()),
|
||||
Height: float32(b.Dy()),
|
||||
}
|
||||
sx, sy = img.adjustPosition(b.Min.X, b.Min.Y)
|
||||
sr = img.adjustedRegion()
|
||||
}
|
||||
|
||||
if offsetX, offsetY := i.adjustPosition(0, 0); offsetX != 0 || offsetY != 0 {
|
||||
options.GeoM.Translate(float64(offsetX), float64(offsetY))
|
||||
}
|
||||
a, b, c, d, tx, ty := options.GeoM.elements32()
|
||||
vs := graphics.QuadVertices(float32(sx), float32(sy), float32(sx+width), float32(sy+height), a, b, c, d, tx, ty, 1, 1, 1, 1)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
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)
|
||||
x, y := img.adjustPosition(b.Min.X, b.Min.Y)
|
||||
// (sx, sy) is the left-upper position of the first image.
|
||||
// Calculate the direction between the current image's left-upper position and the first one's.
|
||||
offsets[i][0] = float32(x - sx)
|
||||
offsets[i][1] = float32(y - sy)
|
||||
}
|
||||
|
||||
i.image.DrawTriangles(imgs, vs, is, affine.ColorMIdentity{}, mode, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dstRegion, sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), false, canSkipMipmap(options.GeoM, graphicsdriver.FilterNearest))
|
||||
i.image.DrawTriangles(imgs, vs, is, affine.ColorMIdentity{}, mode, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, i.adjustedRegion(), sr, offsets, shader.shader, shader.convertUniforms(options.Uniforms), false, canSkipMipmap(options.GeoM, graphicsdriver.FilterNearest))
|
||||
}
|
||||
|
||||
// SubImage returns an image representing the portion of the image p visible through r.
|
||||
@ -706,7 +709,7 @@ func (i *Image) at(x, y int) (r, g, b, a uint8) {
|
||||
if !image.Pt(x, y).In(i.Bounds()) {
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
return i.image.At(x, y)
|
||||
return i.image.At(i.adjustPosition(x, y))
|
||||
}
|
||||
|
||||
// Set sets the color at (x, y).
|
||||
@ -729,6 +732,7 @@ func (i *Image) Set(x, y int, clr color.Color) {
|
||||
}
|
||||
|
||||
r, g, b, a := clr.RGBA()
|
||||
x, y = i.adjustPosition(x, y)
|
||||
i.image.ReplacePixels([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)}, x, y, 1, 1)
|
||||
}
|
||||
|
||||
@ -772,10 +776,11 @@ func (i *Image) ReplacePixels(pixels []byte) {
|
||||
}
|
||||
|
||||
r := i.Bounds()
|
||||
x, y := i.adjustPosition(r.Min.X, r.Min.Y)
|
||||
// Do not need to copy pixels here.
|
||||
// * In internal/mipmap, pixels are copied when necessary.
|
||||
// * In internal/atlas, pixels are copied to make its paddings.
|
||||
i.image.ReplacePixels(pixels, r.Min.X, r.Min.Y, r.Dx(), r.Dy())
|
||||
i.image.ReplacePixels(pixels, x, y, r.Dx(), r.Dy())
|
||||
}
|
||||
|
||||
// NewImage returns an empty image.
|
||||
@ -788,22 +793,57 @@ func (i *Image) ReplacePixels(pixels []byte) {
|
||||
//
|
||||
// NewImage panics if RunGame already finishes.
|
||||
func NewImage(width, height int) *Image {
|
||||
return newImage(width, height, atlas.ImageTypeRegular)
|
||||
return newImage(image.Rect(0, 0, width, height), atlas.ImageTypeRegular)
|
||||
}
|
||||
|
||||
func newImage(width, height int, imageType atlas.ImageType) *Image {
|
||||
// NewImageOptions represents options for NewImage.
|
||||
type NewImageOptions struct {
|
||||
// Unmanaged represents whether the image is unmanaged or not.
|
||||
// The default (zero) value is false, that means the image is managed.
|
||||
//
|
||||
// An unmanged image is never on an internal automatic texture atlas.
|
||||
// A regular image is a part of an internal texture atlas, and locating them is done automatically in Ebitengine.
|
||||
// NewUnmanagedImage is useful when you want finer controls over the image for performance and memory reasons.
|
||||
Unmanaged bool
|
||||
}
|
||||
|
||||
// NewImageWithOptions returns an empty image with the given bounds and the options.
|
||||
//
|
||||
// If width or height is less than 1 or more than device-dependent maximum size, NewImageWithOptions panics.
|
||||
//
|
||||
// The rendering origin position is (0, 0) of the given bounds.
|
||||
// If DrawImage is called on a new image created by NewImageOptions,
|
||||
// for example, the center of scaling and rotating is (0, 0), that might not be a left-upper position.
|
||||
//
|
||||
// NewImageWithOptions should be called only when necessary.
|
||||
// For example, you should avoid to call NewImageWithOptions every Update or Draw call.
|
||||
// Reusing the same image by Clear is much more efficient than creating a new image.
|
||||
//
|
||||
// NewImageWithOptions panics if RunGame already finishes.
|
||||
func NewImageWithOptions(bounds image.Rectangle, options *NewImageOptions) *Image {
|
||||
imageType := atlas.ImageTypeRegular
|
||||
if options != nil && options.Unmanaged {
|
||||
imageType = atlas.ImageTypeUnmanaged
|
||||
}
|
||||
return newImage(bounds, imageType)
|
||||
}
|
||||
|
||||
func newImage(bounds image.Rectangle, imageType atlas.ImageType) *Image {
|
||||
if isRunGameEnded() {
|
||||
panic(fmt.Sprintf("ebiten: NewImage cannot be called after RunGame finishes"))
|
||||
}
|
||||
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
if width <= 0 {
|
||||
panic(fmt.Sprintf("ebiten: width at NewImage must be positive but %d", width))
|
||||
}
|
||||
if height <= 0 {
|
||||
panic(fmt.Sprintf("ebiten: height at NewImage must be positive but %d", height))
|
||||
}
|
||||
|
||||
i := &Image{
|
||||
image: ui.NewImage(width, height, imageType),
|
||||
bounds: image.Rect(0, 0, width, height),
|
||||
bounds: bounds,
|
||||
}
|
||||
i.addr = i
|
||||
return i
|
||||
|
232
image_test.go
232
image_test.go
@ -2889,3 +2889,235 @@ func TestImageNewImageFromEbitenImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageOptionsUnmanaged(t *testing.T) {
|
||||
const (
|
||||
w = 16
|
||||
h = 16
|
||||
)
|
||||
|
||||
pix := make([]byte, 4*w*h)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
idx := 4 * (i + j*w)
|
||||
pix[idx] = byte(i)
|
||||
pix[idx+1] = byte(j)
|
||||
pix[idx+2] = 0
|
||||
pix[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
|
||||
op := &ebiten.NewImageOptions{
|
||||
Unmanaged: true,
|
||||
}
|
||||
img := ebiten.NewImageWithOptions(image.Rect(0, 0, w, h), op)
|
||||
img.ReplacePixels(pix)
|
||||
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
got := img.At(i, j)
|
||||
want := color.RGBA{byte(i), byte(j), 0, 0xff}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageOptionsNegativeBoundsReplacePixels(t *testing.T) {
|
||||
const (
|
||||
w = 16
|
||||
h = 16
|
||||
)
|
||||
|
||||
pix0 := make([]byte, 4*w*h)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
idx := 4 * (i + j*w)
|
||||
pix0[idx] = byte(i)
|
||||
pix0[idx+1] = byte(j)
|
||||
pix0[idx+2] = 0
|
||||
pix0[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
|
||||
const offset = -8
|
||||
img := ebiten.NewImageWithOptions(image.Rect(offset, offset, w+offset, h+offset), nil)
|
||||
img.ReplacePixels(pix0)
|
||||
|
||||
for j := offset; j < h+offset; j++ {
|
||||
for i := offset; i < w+offset; i++ {
|
||||
got := img.At(i, j)
|
||||
want := color.RGBA{byte(i - offset), byte(j - offset), 0, 0xff}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pix1 := make([]byte, 4*(w/2)*(h/2))
|
||||
for j := 0; j < h/2; j++ {
|
||||
for i := 0; i < w/2; i++ {
|
||||
idx := 4 * (i + j*w/2)
|
||||
pix1[idx] = 0
|
||||
pix1[idx+1] = 0
|
||||
pix1[idx+2] = 0xff
|
||||
pix1[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
|
||||
const offset2 = -4
|
||||
sub := image.Rect(offset2, offset2, w/2+offset2, h/2+offset2)
|
||||
img.SubImage(sub).(*ebiten.Image).ReplacePixels(pix1)
|
||||
for j := offset; j < h+offset; j++ {
|
||||
for i := offset; i < w+offset; i++ {
|
||||
got := img.At(i, j)
|
||||
want := color.RGBA{byte(i - offset), byte(j - offset), 0, 0xff}
|
||||
if image.Pt(i, j).In(sub) {
|
||||
want = color.RGBA{0, 0, 0xff, 0xff}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageOptionsNegativeBoundsSet(t *testing.T) {
|
||||
const (
|
||||
w = 16
|
||||
h = 16
|
||||
)
|
||||
|
||||
pix0 := make([]byte, 4*w*h)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
idx := 4 * (i + j*w)
|
||||
pix0[idx] = byte(i)
|
||||
pix0[idx+1] = byte(j)
|
||||
pix0[idx+2] = 0
|
||||
pix0[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
|
||||
const offset = -8
|
||||
img := ebiten.NewImageWithOptions(image.Rect(offset, offset, w+offset, h+offset), nil)
|
||||
img.ReplacePixels(pix0)
|
||||
img.Set(-1, -2, color.RGBA{0, 0, 0, 0})
|
||||
|
||||
for j := offset; j < h+offset; j++ {
|
||||
for i := offset; i < w+offset; i++ {
|
||||
got := img.At(i, j)
|
||||
want := color.RGBA{byte(i - offset), byte(j - offset), 0, 0xff}
|
||||
if i == -1 && j == -2 {
|
||||
want = color.RGBA{0, 0, 0, 0}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageOptionsNegativeBoundsDrawImage(t *testing.T) {
|
||||
const (
|
||||
w = 16
|
||||
h = 16
|
||||
offset = -8
|
||||
)
|
||||
dst := ebiten.NewImageWithOptions(image.Rect(offset, offset, w+offset, h+offset), nil)
|
||||
src := ebiten.NewImageWithOptions(image.Rect(-1, -1, 1, 1), nil)
|
||||
pix := make([]byte, 4*2*2)
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
src.ReplacePixels(pix)
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(-1, -1)
|
||||
op.GeoM.Scale(2, 3)
|
||||
dst.DrawImage(src, op)
|
||||
for j := offset; j < h+offset; j++ {
|
||||
for i := offset; i < w+offset; i++ {
|
||||
got := dst.At(i, j)
|
||||
var want color.RGBA
|
||||
if -2 <= i && i < 2 && -3 <= j && j < 3 {
|
||||
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageOptionsNegativeBoundsDrawTriangles(t *testing.T) {
|
||||
const (
|
||||
w = 16
|
||||
h = 16
|
||||
offset = -8
|
||||
)
|
||||
dst := ebiten.NewImageWithOptions(image.Rect(offset, offset, w+offset, h+offset), nil)
|
||||
src := ebiten.NewImageWithOptions(image.Rect(-1, -1, 1, 1), nil)
|
||||
pix := make([]byte, 4*2*2)
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
src.ReplacePixels(pix)
|
||||
vs := []ebiten.Vertex{
|
||||
{
|
||||
DstX: -2,
|
||||
DstY: -3,
|
||||
SrcX: -1,
|
||||
SrcY: -1,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: 2,
|
||||
DstY: -3,
|
||||
SrcX: 1,
|
||||
SrcY: -1,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: -2,
|
||||
DstY: 3,
|
||||
SrcX: -1,
|
||||
SrcY: 1,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: 2,
|
||||
DstY: 3,
|
||||
SrcX: 1,
|
||||
SrcY: 1,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
}
|
||||
is := []uint16{0, 1, 2, 1, 2, 3}
|
||||
dst.DrawTriangles(vs, is, src, nil)
|
||||
for j := offset; j < h+offset; j++ {
|
||||
for i := offset; i < w+offset; i++ {
|
||||
got := dst.At(i, j)
|
||||
var want color.RGBA
|
||||
if -2 <= i && i < 2 && -3 <= j && j < 3 {
|
||||
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
126
shader_test.go
126
shader_test.go
@ -969,3 +969,129 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShaderOptionsNegativeBounds(t *testing.T) {
|
||||
const w, h = 16, 16
|
||||
|
||||
s, err := ebiten.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)
|
||||
}
|
||||
|
||||
const offset0 = -4
|
||||
src0 := ebiten.NewImageWithOptions(image.Rect(offset0, offset0, w+offset0, h+offset0), nil)
|
||||
pix0 := make([]byte, 4*w*h)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
if 2 <= i && i < 10 && 3 <= j && j < 11 {
|
||||
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)
|
||||
src0 = src0.SubImage(image.Rect(2+offset0, 3+offset0, 10+offset0, 11+offset0)).(*ebiten.Image)
|
||||
|
||||
const offset1 = -6
|
||||
src1 := ebiten.NewImageWithOptions(image.Rect(offset1, offset1, w+offset1, h+offset1), nil)
|
||||
pix1 := make([]byte, 4*w*h)
|
||||
for j := 0; j < h; j++ {
|
||||
for i := 0; i < w; i++ {
|
||||
if 6 <= i && i < 14 && 8 <= j && j < 16 {
|
||||
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)
|
||||
src1 = src1.SubImage(image.Rect(6+offset1, 8+offset1, 14+offset1, 16+offset1)).(*ebiten.Image)
|
||||
|
||||
const offset2 = -2
|
||||
testPixels := func(testname string, dst *ebiten.Image) {
|
||||
for j := offset2; j < h+offset2; j++ {
|
||||
for i := offset2; i < w+offset2; i++ {
|
||||
got := dst.At(i, j).(color.RGBA)
|
||||
var want color.RGBA
|
||||
if 0 <= i && i < w/2 && 0 <= j && j < h/2 {
|
||||
want = color.RGBA{0xff, 0xff, 0, 0xff}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s dst.At(%d, %d): got: %v, want: %v", testname, i, j, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("DrawRectShader", func(t *testing.T) {
|
||||
dst := ebiten.NewImageWithOptions(image.Rect(offset2, offset2, w+offset2, h+offset2), nil)
|
||||
op := &ebiten.DrawRectShaderOptions{}
|
||||
op.Images[0] = src0
|
||||
op.Images[1] = src1
|
||||
dst.DrawRectShader(w/2, h/2, s, op)
|
||||
testPixels("DrawRectShader", dst)
|
||||
})
|
||||
|
||||
t.Run("DrawTrianglesShader", func(t *testing.T) {
|
||||
dst := ebiten.NewImageWithOptions(image.Rect(offset2, offset2, w+offset2, h+offset2), nil)
|
||||
vs := []ebiten.Vertex{
|
||||
{
|
||||
DstX: 0,
|
||||
DstY: 0,
|
||||
SrcX: 2 + offset0,
|
||||
SrcY: 3 + offset0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: w / 2,
|
||||
DstY: 0,
|
||||
SrcX: 10 + offset0,
|
||||
SrcY: 3 + offset0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: 0,
|
||||
DstY: h / 2,
|
||||
SrcX: 2 + offset0,
|
||||
SrcY: 11 + offset0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
{
|
||||
DstX: w / 2,
|
||||
DstY: h / 2,
|
||||
SrcX: 10 + offset0,
|
||||
SrcY: 11 + offset0,
|
||||
ColorR: 1,
|
||||
ColorG: 1,
|
||||
ColorB: 1,
|
||||
ColorA: 1,
|
||||
},
|
||||
}
|
||||
is := []uint16{0, 1, 2, 1, 2, 3}
|
||||
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Images[0] = src0
|
||||
op.Images[1] = src1
|
||||
dst.DrawTrianglesShader(vs, is, s, op)
|
||||
testPixels("DrawTrianglesShader", dst)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user