mipmap: Create mipmap package and bufferd.Image uses it

Mipmap calculation must be executed after the main loop starts
because the graphics driver's HasHighPrecisionFloat is needed.
Then, operations on mipmap images must be called from images in
buffered package.

Updates #1044
This commit is contained in:
Hajime Hoshi 2020-01-08 00:15:46 +09:00
parent 7f4a82ddf0
commit c99fd1a742
7 changed files with 143 additions and 100 deletions

View File

@ -15,6 +15,5 @@
package ebiten package ebiten
var ( var (
CopyImage = copyImage CopyImage = copyImage
MipmapLevelForDownscale = mipmapLevelForDownscale
) )

View File

@ -19,6 +19,7 @@ import (
"image" "image"
"image/color" "image/color"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphics" "github.com/hajimehoshi/ebiten/internal/graphics"
) )
@ -33,9 +34,7 @@ type Image struct {
// See strings.Builder for similar examples. // See strings.Builder for similar examples.
addr *Image addr *Image
// mipmap is a set of shareable.Image sorted by the order of mipmap level. buffered *buffered.Image
// The level 0 image is a regular image and higher-level images are used for mipmap.
mipmap *mipmap
bounds image.Rectangle bounds image.Rectangle
original *Image original *Image
@ -56,7 +55,7 @@ func (i *Image) Size() (width, height int) {
} }
func (i *Image) isDisposed() bool { func (i *Image) isDisposed() bool {
return i.mipmap.isDisposed() return i.buffered == nil
} }
func (i *Image) isSubImage() bool { func (i *Image) isSubImage() bool {
@ -90,7 +89,7 @@ func (i *Image) Fill(clr color.Color) error {
panic("ebiten: render to a subimage is not implemented (Fill)") panic("ebiten: render to a subimage is not implemented (Fill)")
} }
i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA)) i.buffered.Fill(color.RGBAModel.Convert(clr).(color.RGBA))
return nil return nil
} }
@ -199,7 +198,8 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
filter = driver.Filter(img.filter) filter = driver.Filter(img.filter)
} }
i.mipmap.drawImage(img.mipmap, img.Bounds(), geom, options.ColorM.impl, mode, filter) a, b, c, d, tx, ty := geom.elements()
i.buffered.DrawImage(img.buffered, img.Bounds(), a, b, c, d, tx, ty, options.ColorM.impl, mode, filter)
return nil return nil
} }
@ -307,7 +307,32 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
filter = driver.Filter(img.filter) filter = driver.Filter(img.filter)
} }
i.mipmap.drawTriangles(img.mipmap, img.Bounds(), vertices, indices, options.ColorM.impl, mode, filter, driver.Address(options.Address)) b := img.Bounds()
bx0 := float32(b.Min.X)
by0 := float32(b.Min.Y)
bx1 := float32(b.Max.X)
by1 := float32(b.Max.Y)
// TODO: Should we use mipmap.verticesBackend?
vs := make([]float32, len(vertices)*graphics.VertexFloatNum)
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
vs[i*graphics.VertexFloatNum+4] = bx0
vs[i*graphics.VertexFloatNum+5] = by0
vs[i*graphics.VertexFloatNum+6] = bx1
vs[i*graphics.VertexFloatNum+7] = by1
vs[i*graphics.VertexFloatNum+8] = v.ColorR
vs[i*graphics.VertexFloatNum+9] = v.ColorG
vs[i*graphics.VertexFloatNum+10] = v.ColorB
vs[i*graphics.VertexFloatNum+11] = v.ColorA
}
is := make([]uint16, len(indices))
copy(is, indices)
i.buffered.DrawTriangles(img.buffered, vs, is, options.ColorM.impl, mode, filter, driver.Address(options.Address))
} }
// SubImage returns an image representing the portion of the image p visible through r. The returned value shares pixels with the original image. // SubImage returns an image representing the portion of the image p visible through r. The returned value shares pixels with the original image.
@ -324,8 +349,8 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
} }
img := &Image{ img := &Image{
mipmap: i.mipmap, buffered: i.buffered,
filter: i.filter, filter: i.filter,
} }
// Keep the original image's reference not to dispose that by GC. // Keep the original image's reference not to dispose that by GC.
@ -351,10 +376,6 @@ func (i *Image) Bounds() image.Rectangle {
if i.isDisposed() { if i.isDisposed() {
panic("ebiten: the image is already disposed") panic("ebiten: the image is already disposed")
} }
if !i.isSubImage() {
w, h := i.mipmap.size()
return image.Rect(0, 0, w, h)
}
return i.bounds return i.bounds
} }
@ -380,7 +401,7 @@ func (i *Image) At(x, y int) color.Color {
if i.isSubImage() && !image.Pt(x, y).In(i.bounds) { if i.isSubImage() && !image.Pt(x, y).In(i.bounds) {
return color.RGBA{} return color.RGBA{}
} }
r, g, b, a := i.mipmap.at(x, y) r, g, b, a := i.buffered.At(x, y)
return color.RGBA{r, g, b, a} return color.RGBA{r, g, b, a}
} }
@ -404,7 +425,7 @@ func (i *Image) Set(x, y int, clr color.Color) {
} }
r, g, b, a := clr.RGBA() r, g, b, a := clr.RGBA()
i.mipmap.set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8)) i.buffered.Set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8))
} }
// Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values. // Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values.
@ -424,7 +445,8 @@ func (i *Image) Dispose() error {
if i.isSubImage() { if i.isSubImage() {
return nil return nil
} }
i.mipmap.dispose() i.buffered.MarkDisposed()
i.buffered = nil
return nil return nil
} }
@ -454,7 +476,7 @@ func (i *Image) ReplacePixels(p []byte) error {
panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l)) panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l))
} }
i.mipmap.replacePixels(p) i.buffered.ReplacePixels(p)
return nil return nil
} }
@ -508,8 +530,9 @@ func NewImage(width, height int, filter Filter) (*Image, error) {
func newImage(width, height int, filter Filter, volatile bool) *Image { func newImage(width, height int, filter Filter, volatile bool) *Image {
i := &Image{ i := &Image{
mipmap: newMipmap(width, height, volatile), buffered: buffered.NewImage(width, height, volatile),
filter: filter, filter: filter,
bounds: image.Rect(0, 0, width, height),
} }
i.addr = i i.addr = i
return i return i
@ -529,8 +552,9 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) {
width, height := size.X, size.Y width, height := size.X, size.Y
i := &Image{ i := &Image{
mipmap: newMipmap(width, height, false), buffered: buffered.NewImage(width, height, false),
filter: filter, filter: filter,
bounds: image.Rect(0, 0, width, height),
} }
i.addr = i i.addr = i
@ -540,8 +564,9 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) {
func newScreenFramebufferImage(width, height int) *Image { func newScreenFramebufferImage(width, height int) *Image {
i := &Image{ i := &Image{
mipmap: newScreenFramebufferMipmap(width, height), buffered: buffered.NewScreenFramebufferImage(width, height),
filter: FilterDefault, filter: FilterDefault,
bounds: image.Rect(0, 0, width, height),
} }
i.addr = i i.addr = i
return i return i

View File

@ -52,7 +52,7 @@ func takeScreenshot(screen *Image) error {
return err return err
} }
if err := screen.mipmap.dump(newname); err != nil { if err := screen.buffered.Dump(newname); err != nil {
return err return err
} }

View File

@ -15,15 +15,16 @@
package buffered package buffered
import ( import (
"image"
"image/color" "image/color"
"github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/shareable" "github.com/hajimehoshi/ebiten/internal/mipmap"
) )
type Image struct { type Image struct {
img *shareable.Image img *mipmap.Mipmap
width int width int
height int height int
@ -32,7 +33,7 @@ type Image struct {
} }
func BeginFrame() error { func BeginFrame() error {
if err := shareable.BeginFrame(); err != nil { if err := mipmap.BeginFrame(); err != nil {
return err return err
} }
flushDelayedCommands() flushDelayedCommands()
@ -40,7 +41,7 @@ func BeginFrame() error {
} }
func EndFrame() error { func EndFrame() error {
return shareable.EndFrame() return mipmap.EndFrame()
} }
func NewImage(width, height int, volatile bool) *Image { func NewImage(width, height int, volatile bool) *Image {
@ -48,7 +49,7 @@ func NewImage(width, height int, volatile bool) *Image {
delayedCommandsM.Lock() delayedCommandsM.Lock()
if needsToDelayCommands { if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() { delayedCommands = append(delayedCommands, func() {
i.img = shareable.NewImage(width, height, volatile) i.img = mipmap.New(width, height, volatile)
i.width = width i.width = width
i.height = height i.height = height
}) })
@ -57,7 +58,7 @@ func NewImage(width, height int, volatile bool) *Image {
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.img = shareable.NewImage(width, height, volatile) i.img = mipmap.New(width, height, volatile)
i.width = width i.width = width
i.height = height i.height = height
return i return i
@ -68,7 +69,7 @@ func NewScreenFramebufferImage(width, height int) *Image {
delayedCommandsM.Lock() delayedCommandsM.Lock()
if needsToDelayCommands { if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() { delayedCommands = append(delayedCommands, func() {
i.img = shareable.NewScreenFramebufferImage(width, height) i.img = mipmap.NewScreenFramebufferMipmap(width, height)
i.width = width i.width = width
i.height = height i.height = height
}) })
@ -77,7 +78,7 @@ func NewScreenFramebufferImage(width, height int) *Image {
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.img = shareable.NewScreenFramebufferImage(width, height) i.img = mipmap.NewScreenFramebufferMipmap(width, height)
i.width = width i.width = width
i.height = height i.height = height
return i return i
@ -204,6 +205,35 @@ func (i *Image) ReplacePixels(pix []byte) {
i.img.ReplacePixels(pix) i.img.ReplacePixels(pix)
} }
func (i *Image) DrawImage(src *Image, bounds image.Rectangle, a, b, c, d, tx, ty float32, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) {
if i == src {
panic("buffered: Image.DrawImage: src must be different from the receiver")
}
g := &mipmap.GeoM{
A: a,
B: b,
C: c,
D: d,
Tx: tx,
Ty: ty,
}
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
i.img.DrawImage(src.img, bounds, g, colorm, mode, filter)
})
delayedCommandsM.Unlock()
return
}
delayedCommandsM.Unlock()
src.resolvePendingPixels(true)
i.resolvePendingPixels(false)
i.img.DrawImage(src.img, bounds, g, colorm, mode, filter)
}
func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) {
if i == src { if i == src {
panic("buffered: Image.DrawTriangles: src must be different from the receiver") panic("buffered: Image.DrawTriangles: src must be different from the receiver")

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ebiten package mipmap
import ( import (
"fmt" "fmt"
@ -21,67 +21,82 @@ import (
"math" "math"
"github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphics" "github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/shareable"
) )
type levelToImage map[int]*buffered.Image func BeginFrame() error {
return shareable.BeginFrame()
}
type mipmap struct { func EndFrame() error {
return shareable.EndFrame()
}
type GeoM struct {
A float32
B float32
C float32
D float32
Tx float32
Ty float32
}
func (g *GeoM) det() float32 {
return g.A*g.D - g.B*g.C
}
type levelToImage map[int]*shareable.Image
// Mipmap is a set of shareable.Image sorted by the order of mipmap level.
// The level 0 image is a regular image and higher-level images are used for mipmap.
type Mipmap struct {
width int width int
height int height int
volatile bool volatile bool
orig *buffered.Image orig *shareable.Image
imgs map[image.Rectangle]levelToImage imgs map[image.Rectangle]levelToImage
} }
func newMipmap(width, height int, volatile bool) *mipmap { func New(width, height int, volatile bool) *Mipmap {
return &mipmap{ return &Mipmap{
width: width, width: width,
height: height, height: height,
volatile: volatile, volatile: volatile,
orig: buffered.NewImage(width, height, volatile), orig: shareable.NewImage(width, height, volatile),
imgs: map[image.Rectangle]levelToImage{}, imgs: map[image.Rectangle]levelToImage{},
} }
} }
func newScreenFramebufferMipmap(width, height int) *mipmap { func NewScreenFramebufferMipmap(width, height int) *Mipmap {
return &mipmap{ return &Mipmap{
width: width, width: width,
height: height, height: height,
orig: buffered.NewScreenFramebufferImage(width, height), orig: shareable.NewScreenFramebufferImage(width, height),
imgs: map[image.Rectangle]levelToImage{}, imgs: map[image.Rectangle]levelToImage{},
} }
} }
func (m *mipmap) dump(name string) error { func (m *Mipmap) Dump(name string) error {
return m.orig.Dump(name) return m.orig.Dump(name)
} }
func (m *mipmap) fill(clr color.RGBA) { func (m *Mipmap) Fill(clr color.RGBA) {
m.orig.Fill(clr) m.orig.Fill(clr)
m.disposeMipmaps() m.disposeMipmaps()
} }
func (m *mipmap) replacePixels(pix []byte) { func (m *Mipmap) ReplacePixels(pix []byte) {
m.orig.ReplacePixels(pix) m.orig.ReplacePixels(pix)
m.disposeMipmaps() m.disposeMipmaps()
} }
func (m *mipmap) size() (int, int) { func (m *Mipmap) At(x, y int) (r, g, b, a byte) {
return m.width, m.height
}
func (m *mipmap) at(x, y int) (r, g, b, a byte) {
return m.orig.At(x, y) return m.orig.At(x, y)
} }
func (m *mipmap) set(x, y int, r, g, b, a byte) { func (m *Mipmap) DrawImage(src *Mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) {
m.orig.Set(x, y, r, g, b, a)
}
func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) {
if det := geom.det(); det == 0 { if det := geom.det(); det == 0 {
return return
} else if math.IsNaN(float64(det)) { } else if math.IsNaN(float64(det)) {
@ -130,7 +145,7 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo
panic("ebiten: Mipmap must not be used when the filter is FilterScreen") panic("ebiten: Mipmap must not be used when the filter is FilterScreen")
} }
a, b, c, d, tx, ty := geom.elements() a, b, c, d, tx, ty := geom.A, geom.B, geom.C, geom.D, geom.Tx, geom.Ty
if level == 0 { if level == 0 {
vs := quadVertices(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, a, b, c, d, tx, ty, cr, cg, cb, ca, screen) vs := quadVertices(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, a, b, c, d, tx, ty, cr, cg, cb, ca, screen)
is := graphics.QuadIndices() is := graphics.QuadIndices()
@ -149,37 +164,12 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo
m.disposeMipmaps() m.disposeMipmaps()
} }
func (m *mipmap) drawTriangles(src *mipmap, bounds image.Rectangle, vertices []Vertex, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { func (m *Mipmap) DrawTriangles(src *Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) {
bx0 := float32(bounds.Min.X) m.orig.DrawTriangles(src.orig, vertices, indices, colorm, mode, filter, address)
by0 := float32(bounds.Min.Y)
bx1 := float32(bounds.Max.X)
by1 := float32(bounds.Max.Y)
// TODO: Needs boundary check optimization?
// See https://go101.org/article/bounds-check-elimination.html
vs := vertexSlice(len(vertices), false)
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
vs[i*graphics.VertexFloatNum+4] = bx0
vs[i*graphics.VertexFloatNum+5] = by0
vs[i*graphics.VertexFloatNum+6] = bx1
vs[i*graphics.VertexFloatNum+7] = by1
vs[i*graphics.VertexFloatNum+8] = v.ColorR
vs[i*graphics.VertexFloatNum+9] = v.ColorG
vs[i*graphics.VertexFloatNum+10] = v.ColorB
vs[i*graphics.VertexFloatNum+11] = v.ColorA
}
is := make([]uint16, len(indices))
copy(is, indices)
m.orig.DrawTriangles(src.orig, vs, is, colorm, mode, filter, address)
m.disposeMipmaps() m.disposeMipmaps()
} }
func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image { func (m *Mipmap) level(r image.Rectangle, level int) *shareable.Image {
if level == 0 { if level == 0 {
panic("ebiten: level must be non-zero at level") panic("ebiten: level must be non-zero at level")
} }
@ -197,7 +187,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image {
return img return img
} }
var src *buffered.Image var src *shareable.Image
var vs []float32 var vs []float32
var filter driver.Filter var filter driver.Filter
switch { switch {
@ -237,7 +227,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image {
imgs[level] = nil imgs[level] = nil
return nil return nil
} }
s := buffered.NewImage(w2, h2, m.volatile) s := shareable.NewImage(w2, h2, m.volatile)
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero) s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero)
imgs[level] = s imgs[level] = s
@ -264,17 +254,17 @@ func sizeForLevel(origWidth, origHeight int, level int) (width, height int) {
return return
} }
func (m *mipmap) isDisposed() bool { func (m *Mipmap) isDisposed() bool {
return m.orig == nil return m.orig == nil
} }
func (m *mipmap) dispose() { func (m *Mipmap) MarkDisposed() {
m.disposeMipmaps() m.disposeMipmaps()
m.orig.MarkDisposed() m.orig.MarkDisposed()
m.orig = nil m.orig = nil
} }
func (m *mipmap) disposeMipmaps() { func (m *Mipmap) disposeMipmaps() {
for _, a := range m.imgs { for _, a := range m.imgs {
for _, img := range a { for _, img := range a {
img.MarkDisposed() img.MarkDisposed()
@ -288,7 +278,7 @@ func (m *mipmap) disposeMipmaps() {
// mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix. // mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix.
// //
// mipmapLevel panics if det is NaN or 0. // mipmapLevel panics if det is NaN or 0.
func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int { func (m *Mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int {
det := geom.det() det := geom.det()
if math.IsNaN(float64(det)) { if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevel") panic("ebiten: det must be finite at mipmapLevel")
@ -338,10 +328,10 @@ func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter
} }
// This is a separate function for testing. // This is a separate function for testing.
return mipmapLevelForDownscale(det) return MipmapLevelForDownscale(det)
} }
func mipmapLevelForDownscale(det float32) int { func MipmapLevelForDownscale(det float32) int {
if math.IsNaN(float64(det)) { if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevelForDownscale") panic("ebiten: det must be finite at mipmapLevelForDownscale")
} }
@ -401,7 +391,7 @@ func minf32(a, b, c, d float32) float32 {
} }
func geomScaleSize(geom *GeoM) (sx, sy float32) { func geomScaleSize(geom *GeoM) (sx, sy float32) {
a, b, c, d, _, _ := geom.elements() a, b, c, d := geom.A, geom.B, geom.C, geom.D
// (0, 1) // (0, 1)
x0 := 0*a + 1*b x0 := 0*a + 1*b
y0 := 0*c + 1*d y0 := 0*c + 1*d

View File

@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ebiten_test package mipmap_test
import ( import (
"math" "math"
"testing" "testing"
. "github.com/hajimehoshi/ebiten" . "github.com/hajimehoshi/ebiten/internal/mipmap"
) )
func TestMipmapLevelForDownscale(t *testing.T) { func TestMipmapLevelForDownscale(t *testing.T) {

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ebiten package mipmap
import ( import (
"sync" "sync"
@ -70,7 +70,6 @@ func quadVertices(sx0, sy0, sx1, sy1 int, a, b, c, d, tx, ty float32, cr, cg, cb
vs := vertexSlice(4, last) vs := vertexSlice(4, last)
_ = vs[:48] _ = vs[:48]
// For each values, see the comment at shareable.(*Image).DrawTriangles.
vs[0] = tx vs[0] = tx
vs[1] = ty vs[1] = ty
vs[2] = u0 vs[2] = u0