Skip to content

Pseudo 3D Road Derivation Part 1: Boundary Conditions

DISCLAIMER:

This, for me, is an exercise in curiosity. I am not a mathematician, a professional game developer, or anyone with anything more than a rudimentary knowledge of art theory (pertaining to the theory of perspective). I probably do a lot of things in inefficient ways. For me, the fun is in using what I know to derive things, and arrive at fun conclusions. With that….

The Journey Begins

My pseudo3D journey started about a year ago while working on a game for a local annual game jam, TOJam. At the time my fifth TOJam, I was responsible for graphics programming on an endless runner idea in a custom HTML5 framework, and wanted to implement a parallax scrolling background with many layers, to give the impression of a ground moving fast beneath your feet.

Screen Shot 2015-05-10 at 12.13.17 PM
The ground and scenery is made up of a finite number of layers that “shrink” as they recede into the distance.

The game didn’t quite pan out as clearly as I’d hoped, but the endless parallax stuck with me, and I worked on it in my head a bit until I was convinced I could do a full pseudo3D racing engine, a la Outrun or Road Rash.

A few months later, while home sick from work, I decided to hammer it out. Now, what I love about game development is ruminating on a problem, and solving it myself using mathematical principles. I’m well aware that pseudo3D is a problem that has been solved, but I wanted to see how far I could get on it from scratch.

Concept

I started with the premise that in a canvas of size 320 x 220 (roughly that of CRT-era games), I would draw a series of horizontal lines — unit-height slices of the road — one on top of the other, about 100 of them total, taking up half the screen. The horizontal line is the road at some distance away from my viewing perspective. From bottom to top, the lines will shrink in width according to some formula, such that they’re widest at the bottom of the screen (closest to viewing perspective), and narrowest when they stop near the middle of the screen (furthest from viewing perspective).

perspective

Boundary Conditions: Is There Anything They Can’t Do?

So the question is, what do we use as the formula to determine the width of the lines? On surface-level analysis, this is nothing more than a linear scaling. The lines shrink by some constant value with each iteration from bottom to top (horizon). But there’s a very important complication: If we’re going to be adding things like lines on the road, and eventually scenery objects like trees and buildings (and we are going to add these things), then we don’t just need to shrink the road, we also need to know how far away is this line from our viewing perspective?

The problem we wish to solve, then, I summarize thus:

Given a line’s “number” from bottom to top (zero at the bottom, 100 at the top), what is that line’s distance from the viewing perspective (which is a rudimentary camera, so let’s call it that)?

Presumably, with the distance value calculated, we’ll be able to calculate the other needed properties of that line, such as its width at that distance, and several other properties that we’ll get to later.

So what’s our formula, f(x) = d, that takes a line number x and returns a distance d? For this, my go-to method of deriving a formula is to look at our known boundary conditions! We don’t really have solid distance units in mind (maybe you did? I sure didn’t), so we can set some arbitrary plug-in values.

Let’s say we can see a maximum of 100 units away — that’s our horizon, so the distance value we want for our line number 100 is 100.

f(100) = 100

Since we’re setting that as our horizon line, there’s no distance we can see beyond that. If something were infinitely far away, we wouldn’t it want it any higher than that line on our horizon. So:

f(101) → ∞

Now the distance d at line 0 is not technically zero units away from our camera. Presumably our camera is some height above the road, and has some viewing angle, so our sightline actually makes contact with the road at some unknown distance d˚. This is not a useful boundary condition.

camera
The yellow line represents where our camera’s viewing angle hits the ground – that’s our zeroth line! Note that it’s not at distance 0 from the camera’s base.

Here’s where a little inference from the real world must be made, to get us our last boundary condition. From basic rules of perspective, if some horizontal slice of height h on our canvas covers a receding distance d, then directly above it, to cover distance d a second time will take a horizontal slice of height h / 2.

perspective2
Ack! Ignore the top statement; it’s not accurate. Everything after the OR is true!

[Author’s note: Since posting this yesterday, and looking over the numbers, I’m a bit insecure about my description of the relationship between height on a canvas and implied distance in an image with perspective. I’ll mull on it some more, but would be happy to accept a better description from someone more up and up on the theory! Ultimately, my results worked out, so I think I’ve nailed it in practice – just not necessarily theory.]

Each of our lines is the same height: 1 pixel. So if the distance covered by each one-pixel slice doubles from bottom to top, and line 100 is 100 units of distance away, how far away is line 99? (In other words, what is the distance covered between lines 99 and 100?)

This is a simple doubling problem. At line 99, we’ll be at exactly half of where we are at line 100. Line 99 is 50 units away. Line 98 is 25 units. Line 97 is 12.5, etc.

Have you ever heard the problem of traversing some short distance, each step being exactly half the distance between your current position and your destination? You’ll never get there. Every step will only get you half way, and there are infinite halves between you and your destination.
Similarly, each of our horizontal lines covers half the distance of the line before it. So if we could extend our lines beyond line zero, toward negative infinity, we’d find that we approached, but never reach, a distance d of 0. This is our last boundary condition!

f(-∞) → 0

So what we need is the simplest formula with those boundary conditions:

f(101) → ∞

f(100) = 100

f(-∞) → 0

No problem! What are you even worried about? It looks something like this:

distance = 100 / (101 – x)

Breaking this down, x is the line number, where 0 is the closest line to us, and 100 is the furthest line away. 100 represents the horizon distance (this is arbitrary! It could be anything, but I like 100). 101 is one higher than our highest line number (maxlines + 1), representing the fact that we can never see beyond our horizon. From those values we can get the distance d of some arbitrary line. LET’S PUT THIS IN CODE!

(All that preamble for six lines of code!?)

Now what we have is an array, lines, that contains objects with a distance property, and can be retrieved by their lineNo from 0 to 100. Nice! But what we originally wanted was each line’s rendered width! We still don’t have that!

Here’s where I introduce what is probably the most important value for each horizontal line in a pseudo3D engine (at least in my own implementation): The scale factor! (scaleFactor)

By the inverse square law, as a thing gets further away, its dimensions appear to scale inversely proportional to its distance away. (It’s the inverse square law because the total visible area of some 2-dimensional surface scales down by a factor of 1/d for each of its 2 dimensions, for a net effect of 1/d^2 (one over distance d-squared).) We’ve already seen this concept with our diminishing distance values.

Basically, the scaleFactor, the value by which we’re going to scale each dimension of each drawn surface, is simply 1/d.

We can arbitrarily set the “standard” width of our road (the unscaled width). I found a value of 40 worked well for my screen dimensions. (We can get into real units later if we want to – this is all just to hammer out the basic concepts.)

A handful of other values we need in order to render our lines include our canvas width and canvas height (the total width and height of the canvas we’re rendering to). With that, we can now draw the most basic version of our road, ready to be built upon later:

That’s clearly not production code. I cheated by assuming we have a drawLine function, but I hope this conveys the gist of what we’re doing. Also note that I iterate not from zero to the end of our array, but rather starting from the highest value, lines.length and working down to zero. This doesn’t make much difference in our current iteration, but it’ll prove useful once we’re drawing various altitudes, and objects with vertical height. We want to draw things in the foreground on top of things in the background, so it makes sense to iterate from back to front, drawing all foreground objects on top as we go.

We also need to put that drawing into our game loop. The above code only draws it once.

That’s about it for our basic basics. I’ll update later with how angles and hills were handled, and follow it up with scenery objects. In the meantime, here’s the little demo I ended up making by the end of that sick day:

Screen Shot 2015-05-10 at 12.17.23 PM
Click through to view road demo on itch.io

 

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *