graphics: Implement (*Image).Set

This commit is contained in:
Hajime Hoshi 2019-01-14 00:39:37 +09:00
parent ddecc1c602
commit 128e99b6af
2 changed files with 175 additions and 2 deletions

View File

@ -116,7 +116,7 @@ func (m *mipmap) disposeMipmaps() {
// Image represents a rectangle set of pixels.
// The pixel format is alpha-premultiplied RGBA.
// Image implements image.Image.
// Image implements image.Image and draw.Image.
//
// Functions of Image never returns error as of 1.5.0-alpha, and error values are always nil.
type Image struct {
@ -131,6 +131,8 @@ type Image struct {
bounds *image.Rectangle
original *Image
pixelsToSet map[int]color.RGBA
filter Filter
}
@ -180,6 +182,8 @@ func (i *Image) Fill(clr color.Color) error {
panic("render to a subimage is not implemented")
}
i.resolvePixelsToSet(false)
r16, g16, b16, a16 := clr.RGBA()
r, g, b, a := uint8(r16>>8), uint8(g16>>8), uint8(b16>>8), uint8(a16>>8)
i.mipmap.original().Fill(r, g, b, a)
@ -243,6 +247,9 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
panic("render to a subimage is not implemented")
}
img.resolvePixelsToSet(true)
i.resolvePixelsToSet(true)
// Calculate vertices before locking because the user can do anything in
// options.ImageParts interface without deadlock (e.g. Call Image functions).
if options == nil {
@ -428,6 +435,9 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
panic("render to a subimage is not implemented")
}
img.resolvePixelsToSet(true)
i.resolvePixelsToSet(true)
if len(indices)%3 != 0 {
panic("ebiten: len(indices) % 3 must be 0")
}
@ -531,9 +541,76 @@ func (i *Image) At(x, y int) color.Color {
if i.bounds != nil && !image.Pt(x, y).In(*i.bounds) {
return color.RGBA{}
}
i.resolvePixelsToSet(true)
return i.mipmap.original().At(x, y)
}
// Set sets the color at (x, y).
//
// Set loads pixels from GPU to system memory if necessary, which means that Set can be slow.
//
// Set can't be called before the main loop (ebiten.Run) starts.
//
// If the image is disposed, Set does nothing.
func (i *Image) Set(x, y int, clr color.Color) {
i.copyCheck()
if i.isDisposed() {
return
}
if i.bounds != nil && !image.Pt(x, y).In(*i.bounds) {
return
}
if i.isSubimage() {
i = i.original
}
if i.pixelsToSet == nil {
i.pixelsToSet = map[int]color.RGBA{}
}
r, g, b, a := clr.RGBA()
w, _ := i.Size()
i.pixelsToSet[x+y*w] = color.RGBA{
byte(r >> 8),
byte(g >> 8),
byte(b >> 8),
byte(a >> 8),
}
}
func (img *Image) resolvePixelsToSet(draw bool) {
if img.isSubimage() {
img = img.original
}
if img.pixelsToSet == nil {
return
}
if !draw {
img.pixelsToSet = nil
return
}
w, h := img.Size()
pix := make([]byte, 4*w*h)
idx := 0
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
c, ok := img.pixelsToSet[idx]
if !ok {
c = img.mipmap.original().At(i, j)
}
pix[4*idx] = c.R
pix[4*idx+1] = c.G
pix[4*idx+2] = c.B
pix[4*idx+3] = c.A
idx++
}
}
img.ReplacePixels(pix)
img.pixelsToSet = nil
}
// Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values.
//
// Dispose is useful to save memory.
@ -549,6 +626,7 @@ func (i *Image) Dispose() error {
if !i.isSubimage() {
i.mipmap.dispose()
}
i.resolvePixelsToSet(false)
runtime.SetFinalizer(i, nil)
return nil
}
@ -573,6 +651,7 @@ func (i *Image) ReplacePixels(p []byte) error {
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
i.resolvePixelsToSet(false)
s := i.Bounds().Size()
if l := 4 * s.X * s.Y; len(p) != l {
panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l))

View File

@ -1340,7 +1340,7 @@ func TestImageAddressRepeat(t *testing.T) {
}
}
func TestReplacePixelsAfterClear(t *testing.T) {
func TestImageReplacePixelsAfterClear(t *testing.T) {
const w, h = 256, 256
img, _ := NewImage(w, h, FilterDefault)
img.ReplacePixels(make([]byte, 4*w*h))
@ -1353,3 +1353,97 @@ func TestReplacePixelsAfterClear(t *testing.T) {
// The test passes if this doesn't crash.
}
func TestImageSet(t *testing.T) {
type Pt struct {
X, Y int
}
const w, h = 16, 16
img, _ := NewImage(w, h, FilterDefault)
colors := map[Pt]color.RGBA{
{1, 2}: {3, 4, 5, 6},
{7, 8}: {9, 10, 11, 12},
{13, 14}: {15, 16, 17, 18},
}
for p, c := range colors {
img.Set(p.X, p.Y, c)
}
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := img.At(i, j).(color.RGBA)
var want color.RGBA
if c, ok := colors[Pt{i, j}]; ok {
want = c
}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}
func TestImageSetAndDraw(t *testing.T) {
type Pt struct {
X, Y int
}
const w, h = 16, 16
src, _ := NewImage(w, h, FilterDefault)
dst, _ := NewImage(w, h, FilterDefault)
colors := map[Pt]color.RGBA{
{1, 2}: {3, 4, 5, 6},
{7, 8}: {9, 10, 11, 12},
{13, 14}: {15, 16, 17, 18},
}
for p, c := range colors {
src.Set(p.X, p.Y, c)
dst.Set(p.X+1, p.Y+1, c)
}
dst.DrawImage(src, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
var want color.RGBA
if c, ok := colors[Pt{i, j}]; ok {
want = c
}
if c, ok := colors[Pt{i - 1, j - 1}]; ok {
want = c
}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
src.Clear()
dst.Clear()
for p, c := range colors {
src.Set(p.X, p.Y, c)
dst.Set(p.X+1, p.Y+1, c)
}
op := &DrawImageOptions{}
op.GeoM.Translate(2, 2)
dst.DrawImage(src.SubImage(image.Rect(2, 2, w-2, h-2)).(*Image), 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 2 <= i && 2 <= j && i < w-2 && j < h-2 {
if c, ok := colors[Pt{i, j}]; ok {
want = c
}
}
if c, ok := colors[Pt{i - 1, j - 1}]; ok {
want = c
}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}