Skip to content

Moving, scaling and rotating with Matrix

Michal Štrba edited this page Apr 2, 2017 · 23 revisions

In this part, we'll learn how to move, rotate and scale using Matrix and how to manipulate geometry primitives.

Geometry

First, we'll learn how to manipulate geometry primitives as it's very important for pretty much everything. If you're not familiar with the mathematical notion of a vector, I strongly suggest you study that first.

Every solid game library comes with a set of types that represent geometry primitives, such as vectors, rectangles, and so on. Pixel is no exception. Pixel comes with 3 major geometry primitives:

  • Vector pixel.Vec. Positions, movements (translations), velocities, accelerations, and so on.
  • Rectangle pixel.Rect. Mainly picture frames (portions for sprites) and bounds.
  • Matrix pixel.Matrix. All kinds of linear transformations: movements, rotations, scaling.

Each one of these primitives is implemented to be very flexible as well as easy to use. Let's take a look at each one of them!

Vector

Pixel being a 2D game library, vector in Pixel is a 2D vector with two coordinates: X and Y. If you take a look at the documentation of pixel.Vec, you'll notice that it's implemented quite unusually as a complex number (a number with real and imaginary components).

type Vec complex128

If you don't know anything about complex numbers, don't worry. This implementation comes with major benefits. Since complex numbers are first-class citizens in Go with support of addition and subtraction using standard operators, the same is now possible with vectors in Pixel. To create a vector, use pixel.V constructor.

u := pixel.V(2.7, 5)
v := pixel.V(10, 3.14)
w := u + v
fmt.Println(w.X()) // 12.7

First two lines construct two 2D vectors with X and Y coordinates (2.7, 5) and (10, 3.14) respectively. The third line is interesting. Go does not support operator overloading, but since pixel.Vec is implemented as complex128, addition can be done naturally through the + operator. That's neat. We can subtract the vectors too.

fmt.Println(w - v) // prints Vec(2.7, 5)

Multiplying the vectors is trickier though. Doing u * v produces a complex number product of the two vectors, which is almost never what we want. Multiplying by a float64 like u * c, where c is a float64 variable is also not possible, because Go is a strictly typed language. For these tasks, Pixel provides two methods: Vec.Scaled and Vec.ScaledXY.

u := pixel.V(2, 1)
v := u.Scaled(10)  // (20, 10)
w := u.ScaledXY(v) // (40, 10)

Scaled multiplies by a float64 scalar, ScaledXY multiplies by another vector, component-wise (X with X, Y with Y).

Constructing a zero vector (0, 0) is surprisingly easy due to Go's constant semantics. It can be expressed as a simple 0.

u := pixel.V(0, 0)
u = 0 // u is unchanged, 0 is equivalent to pixel.V(0, 0)

Pixel also provides several methods for dealing with the individual X and Y components of a vector. Check out the documentation, here are some of them.

u := pixel.V(0, 0)
u += pixel.X(10)
u -= pixel.Y(10)
fmt.Println(u) // prints Vec(10, -10)
u = u.WithX(5)
fmt.Println(u) // prints Vec(5, -10)

Rotating, uniting, doting, crossing and so on is all available too.

Rectangle

Rectangles are very simple. The pixel.Rect type is defined like this.

type Rect struct {
	Min, Max Vec
}

It has a Min and a Max component. Min is the position of the lower-left corner of the rectangle and Max is the position of the upper-right corner of the rectangle. The sides of the rectangle are always parallel with the X and Y axes.

To create a rectangle, use pixel.R constructor.

rect := pixel.R(1, 1, 7, 7)

Variable rect now contains a rectangle, which is a square with the lower-left corner at (1, 1) and the upper-right corner at (7, 7). The width and height of rect are both 6.

fmt.Println(rect.W(), rect.H()) // 6 6
fmt.Println(rect.Size())        // Vec(6, 6)

To get the center of a rectangle, use rect.Center method. To move a rectangle by a vector, use rect.Moved method.

fmt.Println(rect.Center())             // Vec(4, 4)
fmt.Println(rect.Moved(pixel.V(4, 10)) // Rect(5, 11, 11, 17)

Rectangles also support resizing (implemented kinda cool) and a few other methods. Check out the docs.

Matrix

Probably the most interesting out of all of the geometry primitives is the matrix. The pixel.Matrix type is defined like this.

type Matrix [9]float64

That definition hints the truth, it really is a 3x3 algebraic matrix. But don't worry, no algebra here. Working with matrices is very easy and convenient in Pixel. But, why learn it all by listing a bunch of methods and describing them? Let's code and see for ourselves!

Back to the code

We'll continue where we left off in the previous part.

package main

import (
	"image"
	"os"

	_ "image/png"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	"golang.org/x/image/colornames"
)

func loadPicture(path string) (pixel.Picture, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	img, _, err := image.Decode(file)
	if err != nil {
		return nil, err
	}
	return pixel.PictureDataFromImage(img), nil
}

func run() {
	cfg := pixelgl.WindowConfig{
		Title:  "Pixel Rocks!",
		Bounds: pixel.R(0, 0, 1024, 768),
		VSync:  true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	pic, err := loadPicture("hiking.png")
	if err != nil {
		panic(err)
	}

	sprite := pixel.NewSprite(pic, pic.Bounds())

	win.Clear(colornames.Greenyellow)

	sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))
	sprite.Draw(win)

	for !win.Closed() {
		win.Update()
	}
}

func main() {
	pixelgl.Run(run)
}

Now, let's take a deep look at this line.

	sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))

We already know what it does, but let's break it down once again.

pixel.IM

As we've already discovered, pixel.IM is the identity matrix. It does nothing, no transformations. When we construct a matrix, we always start off from here, the identity matrix. Then we construct our desired matrix using successive applications of the matrix methods.

win.Bounds().Center()

As you could guess, win.Bounds() returns a rectangle, the bounds of the window. Getting it's center is no surprise.

pixel.IM.Moved(win.Bounds().Center())

Here's the first interestinig matrix method, Moved. It only takes one parameter, a delta vector. The matrix will be moved (translated) by this vector.

Rotation

Now, let's take a look at another useful matrix method, Rotated. It takes two arguments. The first argument is a vector that we'll be rotating everything around. The second argument is an angle in radians. Simple, right?

First, let's split our matrix line into a few lines for clarity. Change this

	sprite.SetMatrix(pixel.IM.Moved(win.Bounds().Center()))

to this

	mat := pixel.IM
	mat = mat.Moved(win.Bounds().Center())
	sprite.SetMatrix(mat)

Now, let's go ahead and add a rotation.

	mat := pixel.IM
	mat = mat.Moved(win.Bounds().Center())
	mat = mat.Rotated(win.Bounds().Center(), math.Pi/4)
	sprite.SetMatrix(mat)
	sprite.Draw(win)

So, we first moved the sprite to the center of the window, then we rotated it around the center of the window by 45 degrees. Run the code and see for yourself!

Ugh, what's those pixely artifacts? The picture is no longer as smooth as it was before. That's no good. To fix this, we need to tell the window, that we want our pictures be drawn smoothly and not pixely, this is no pixel art. To do that, add this line.

	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	win.SetSmooth(true)

When we run the program now, our picture is perfectly smooth, crisp, beautiful!

Scaling

Scaling is very similar to rotating. There are two methods for scaling, Scaled and ScaledXY. The first one scales everything around a certain position by a float64 scalar. The second one scales independently in each axis.

Let's add some crazy scaling!

	mat := pixel.IM
	mat = mat.Moved(win.Bounds().Center())
	mat = mat.Rotated(win.Bounds().Center(), math.Pi/4)
	mat = mat.ScaledXY(win.Bounds().Center(), pixel.V(0.5, 2))
	sprite.SetMatrix(mat)
	sprite.Draw(win)

Well, that looks weird. Let's swap the rotation and scaling.

	mat := pixel.IM
	mat = mat.Moved(win.Bounds().Center())
	mat = mat.ScaledXY(win.Bounds().Center(), pixel.V(0.5, 2))
	mat = mat.Rotated(win.Bounds().Center(), math.Pi/4)
	sprite.SetMatrix(mat)
	sprite.Draw(win)

Notice the difference. The order of transformations matters a lot.

In fact, if we didn't call Moved before anything else, we could simplify our code. Remember, without Moved the sprite is located at the position (0, 0). So, if we scale and rotate the sprite around (0, 0) and only then move it, we get the same result.

	mat := pixel.IM
	mat = mat.ScaledXY(pixel.V(0, 0), pixel.V(0.5, 2))
	mat = mat.Rotated(pixel.V(0, 0), math.Pi/4)
	mat = mat.Moved(win.Bounds().Center())
	sprite.SetMatrix(mat)
	sprite.Draw(win)

We can simplify this code even further. Remember that the (0, 0) vector can be represented as a simple 0?

	mat := pixel.IM
	mat = mat.ScaledXY(0, pixel.V(0.5, 2))
	mat = mat.Rotated(0, math.Pi/4)
	mat = mat.Moved(win.Bounds().Center())
	sprite.SetMatrix(mat)
	sprite.Draw(win)

Nice, isn't it? Representing the (0, 0) vector this way is both nice and idiomatic.

Dynamic

TODO