Sonic Battle (GBA) Renderer Series - Walls

Posted on January 23, 2019

Table of Contents


Welcome to the seventh article of the Sonic Battle (GBA) Renderer series.


Coordinate system

Quick reminder (just in case): here’s a link to the coordinate system.

Walls

Directions

All walls are axis-aligned. (Except for the 45-degree walls but they will be discussed in a later article.)

It means that walls only face 4 directions which I call:

  • Y_PLUS
  • X_MINUS
  • Y_MINUS
  • X_PLUS

Visibility

Our walls represent boxes so it’s safe to assume that if a given side is visible, then the opposite is invisible. (Ex: X_MINUS and X_PLUS visibility is exclusive)

This fact can be used for an optimization: once a side is determined to be visible, the processing of the opposite one can be skipped. Note that there’s a special case where two opposite sides can be both not visible, it happens when the dot product for both is 0.

The usual algorithm for determining if a wall is facing away from a view is to check if the dot product of its normal against (the opposite of) the camera’s direction is smaller than 0.

In short: visible = dot(wallNormal, -cam.directionIn2D) > 0.

Our wall directions are the +/-X axis and +/-Y axis.

Then the dot product for the X_PLUS direction is dot(vec(0, 1), -cam.directionIn2D). It simplifies to -cam.directionIn2D.y. The dot product for X_MINUS is cam.directionIn2D.y.

Similarly, for Y_PLUS it’s -cam.directionIn2D.x and for Y_MINUS it’s cam.directionIn2D.x.

This translates to:

WallVisible[WallDirection::Y_PLUS] = camDirXY.x < 0.0f;
WallVisible[WallDirection::Y_MINUS] = camDirXY.x > 0.0f;
WallVisible[WallDirection::X_PLUS] = camDirXY.y > 0.0f;
WallVisible[WallDirection::X_MINUS] = camDirXY.y < 0.0f;

Another way to look at it is to visualize the camera angle from the top, where the relation between wall directions, camera direction, and XY axes is clear:

Re-using affine matrices (optimization)

Once the visible wall directions are determined, the corresponding affine matrices are computed.

We re-use these affine matrices for every wall of the same facing direction because an individual wall’s position doesn’t affect its projection (only wall normal and camera direction matter - as will be described below).

So all visible X_PLUS walls share the same X_PLUS affine matrix for example.

This is a significant optimization: each frame we only compute 1 or 2 affine matrices instead of 1 per wall.

Affine matrix

My initial approach was very awkward, I’m glad that I replaced it by a much simpler one recently.

In a nutshell we need to find the matrix that will transform the wall texture’s local coordinate space to the coordinate space of the texture as it would appear on screen.

Here’s an illustration of the full process:

We can pre-compute the world-space texture X axis (wall tangents). The texture Y axis points straight down (-Z in world space). So we create a matrix which uses those two as basis vectors.

Next we transform that matrix into camera space (inCamSpace = mat3(camTransformInverse) * inWorldSpace).

Then we perform the orthographic projection (by simply discarding any depth-axis information). Finally we flip the vertical axis to account for screen/canvas coordinates.

I’m fairly confident that this method would support camera roll and arbitrary wall orientations. I’m tempted to revisit the approach I used earlier for tilemaps so that they could support these things too. Not that it matters for Sonic Battle of course, since it doesn’t use those bells and whistles.

Note: on the real hardware we wouldn’t be done just yet. There’s a few gotchas with affine matrices.

View-space position

The view space positions of individual walls are computed the same as for tilemaps: camera.transformMatrixInverse * worldSpacePosition.

Note - Tilemap and wall base scale

All dimensions of the game world (save for the characters) have been doubled! Tilemaps and most wall sprites are exactly x2 larger than their source bitmaps. That really tripped me up until I figured it out with mGBA’s inspection tools.

This wall is rendered ~64 pixels wide at native screen resolution

But its bitmap is only 32 pixels wide (the "Double Size" flag is unrelated by the way)

The wall behind it though has the same width as its bitmap (it's only scaled in height) - so there's some flexibility here

This could have been done for performance reasons, to use ~4 times fewer hardware sprites to cover the same area (which reduces load on the entire renderer).

Or for artistic reasons, to reduce the visual noise.

Note - Wall height scale

For gameplay reasons (to prevent the action from vertically overflowing the screen) all walls are additionally scaled in height to make them shorter. It also looks better that way.

Just the basic x2 scale - that's really tall

x2 scale and additional height scale (I eyeballed the value, it's ~1.4)



The next article will discuss the depth sorting of the walls.

You can subscribe to the mailing list to be notified when a new article appears.