Learn to Code via Tutorials on Repl.it!

← Back to all posts
3D graphics, a beginners mind.

# Preface

In this tutorial I would like to show how 3d graphics is done today, why it's important, and how it will change the way you see 3d graphics applications.

To better understand, we'll end up creating a 3d engine with Python.

# Requirements

I expect you to be familiar with Python, if you understand what `class` means you're probably qualified in this department.

I also expect you to understand what the terms fov (field of view), vertex, mesh..etc. mean.

# But 3d graphics is hard!

No, it's not. This is an awesome area of programming you'll be able to show to your friends, there will be math(s) involved, so strap in, but it'll all be explained. If there are aspects you do not understand, simply copy my solution.

# Fundamentals

To start, let's go through the basic building blocks.

Imagine we have a simple object, a cube. There's more going on under the hood, this cube is made up of two things, vertexes and triangles.

Vertexes are essentially points in 3d space. Look around your room, and imagine a speck of dust, a single point in 3d space.

Triangles are, well just triangles, normal 2d flat triangles. However their three points are connected to specific vertexes.

Let's look at the vertexes. On the above image of a cube, you can see there are eight points, these are the points which make up the cube.

In memory, these points each have a 3d coordinate: X, Y, Z axis. however when we go to render the cube, we map each 3d coordinate to 2d screen space. And the way we do that is surprisingly simple.

Next, let's look at the triangles. As you can see, a triangle is now connected to three of the points. Do this 12(*) times and you'll get a cube.

*: A cube is made up of 6 faces, however to make a face with a triangle, you must use two triangles, so it ends up being 12 triangles.

# Enough "fundamentals", more coding!

Alright, now that we understand the basic structure for rendering 3d shapes. Let's get more technical. We'll be doing this in `Python (with Turtle)`.

First, we import Turtle, I will assume you already know how to use Turtle and are familiar with it's functionality. In short, it's just a graphics library aimed at kids learning how to code with graphics, and making flowers and all sorts of things... Except we'll be going much further than flowers.

``import turtle``

Next we need to store our object data. We need to store all our vertexes and triangles.

``````VERTEXES = [(-1, -1, -1), ( 1, -1, -1), ( 1,  1, -1), (-1,  1, -1),
(-1, -1,  1), ( 1, -1,  1), ( 1,  1,  1), (-1,  1,  1)]

TRIANGLES = [(0, 1, 2)]``````

For now, we only have one triangle connected to the first three points.

# Our basic main loop

We want to simulate a normal graphics library with turtle.

Our program will follow this structure:

``````# Create turtle,
pointer = turtle.Turtle()

# Turn off move time, makes drawing instant,
turtle.tracer(0, 0)
pointer.up()

while True:
# Clear screen,
pointer.clear()

# Draw,
# ...

# Update,
turtle.update()``````

# Rendering

Alright, now we need to somehow map these 3d vertex coordinates to 2d screen coordinates.

To do this, let's use the Perspective Formula. Before we dive into the details of what exactly this formula does, let's start with an observation.

Place an object in front of you, for instance a cup. As you move away, the cup shrinks; now this is all very obvious, but it is an essential property of 3d space we must consider.

When we're creating a 3d engine, what we're doing is simulating this observation. When we move away from our objects, that is - the Z axis, we're essentially converging the X and Y axis toward zero.

Look at this front-view of a cube, you can see the back vertexes are closer to the center (zero). # So what is this "formula"?

``````f = field_of_view / z
screen_x = x * f
screen_y = y * f``````

Where x, y, z are vertex coordinates.

We can simplify this to:

``````f = fov / z
sx, sy = x * f, y * f``````

Easy right?

So let's add `FOV` at the top of the file:

``FOV = 100``

# Drawing the points

Let's iterate through each vertex:

``````# Draw,
for vertex in VERTEXES:
# Get the X, Y, Z coords out of the vertex iterator,
x, y, z = vertex

# Perspective formula,
f = FOV / z
sx, sy = x * f, y * f

# Move to and draw point,
pointer.goto(sx, sy)
pointer.dot(3)``````

But where are our four other points from before? The ones behind?

The issue is we're inside the cube, we need to move the camera out.

# The camera

Alright, I won't go into the camera in this tutorial, you can look at my repl at the bottom to see how to properly implement a 3d engine, but we're taking baby steps here.

When we think of moving the camera, we think of the camera object moving, simple right? Well that's not easy to implement in a rasterized renderer. However what's easier is to move the world around it. Think about it, either you can move the camera, or move the world; it's the same effect.

As it turns out, it's a lot easier to offset the vertex positions than somehow change the perspective formula to equate the position; it would be a whole lot more complex.

So quickly solve this, let's move the camera out:

``````# Perspective formula,
z += 5
f = FOV / z
sx, sy = x * f, y * f`````` And adjust the `FOV` to say, `400`. Nice!

# Drawing triangles

To draw triangles, consider this code. By this point you should be able to understand it:

``````# Draw,
for triangle in TRIANGLES:
points = []
for vertex in triangle:
# Get the X, Y, Z coords out of the vertex iterator,
x, y, z = VERTEXES[vertex]
print(x, y, z)

# Perspective formula,
z += 5
f = FOV / z
sx, sy = x * f, y * f

points.append((sx, sy))

# Draw triangle,
pointer.goto(points, points)
pointer.down()

pointer.goto(points, points)
pointer.goto(points, points)
pointer.goto(points, points)
pointer.up()``````

# Rotation

To rotate our object, we'll be using the Rotation Matrix. It sounds scary, right?

If you're familiar with linear algebra, you should already know this, but the rotation matrix is commonly defined as:

``````[x'] = [cos(0), -sin(0)]
[y'] = [sin(0),  cos(0)]``````

using `0` as theta

I won't go into detail of the matrix. If you're unfamiliar, feel free to either research or copy & paste.

To implement this, we'll first need the `math` library:

``from math import sin, cos``

Let's make a function to rotate:

``````def rotate(x, y, r):
s, c = sin(r), cos(r)
return x * c - y * s,
x * s + y * c``````

Then let's place this before we do our perspective formula calculations:

``````# Rotate,
x, z = rotate(x, z, 1)``````

As you can see the triangle is now rotated: Let's make the rest of the triangles:

``````TRIANGLES = [
(0, 1, 2), (2, 3, 0),
(0, 4, 5), (5, 1, 0),
(0, 4, 3), (4, 7, 3),
(5, 4, 7), (7, 6, 5),
(7, 6, 3), (6, 2, 3),
(5, 1, 2), (2, 6, 5)
]`````` Awesome!

Let's initialize a counter at the start of the file:

``counter = 0``

and increment this at the end of every loop:

``````# Update,
turtle.update()

counter += 0.025``````

And replace our rotation function:

``x, z = rotate(x, z, counter)``

It's rotating, awesome!

To rotate on the X, Y and Z axis:

``````x, z = rotate(x, z, counter)
y, z = rotate(y, z, counter)
x, y = rotate(x, y, counter)``````

We're done!

# Complete code

Before you read, I recommend you do read through the above, I know it's easier to just skip down to the bottom for the solutions.

However, if you're here after reading through the above, feel free to post `Full read` in the comments as a token of my respect, and feel free to copy this code =)

``````from math import sin, cos
import turtle

VERTEXES = [(-1, -1, -1), ( 1, -1, -1), ( 1,  1, -1), (-1,  1, -1),
(-1, -1,  1), ( 1, -1,  1), ( 1,  1,  1), (-1,  1,  1)]

TRIANGLES = [
(0, 1, 2), (2, 3, 0),
(0, 4, 5), (5, 1, 0),
(0, 4, 3), (4, 7, 3),
(5, 4, 7), (7, 6, 5),
(7, 6, 3), (6, 2, 3),
(5, 1, 2), (2, 6, 5)
]

FOV = 400

# Create turtle,
pointer = turtle.Turtle()

# Turn off move time, makes drawing instant,
turtle.tracer(0, 0)
pointer.up()

def rotate(x, y, r):
s, c = sin(r), cos(r)
return x * c - y * s, x * s + y * c

counter = 0
while True:
# Clear screen,
pointer.clear()

# Draw,
for triangle in TRIANGLES:
points = []
for vertex in triangle:
# Get the X, Y, Z coords out of the vertex iterator,
x, y, z = VERTEXES[vertex]

# Rotate,
x, z = rotate(x, z, counter)
y, z = rotate(y, z, counter)
x, y = rotate(x, y, counter)

# Perspective formula,
z += 5
f = FOV / z
sx, sy = x * f, y * f

points.append((sx, sy))

# Draw triangle,
pointer.goto(points, points)
pointer.down()

pointer.goto(points, points)
pointer.goto(points, points)
pointer.goto(points, points)
pointer.up()

# Update,
turtle.update()

counter += 0.025``````

# Conclusion

If you want to see an expanded and better written version: https://repl.it/@CoolqB/3D-Engine

If there's demand I will perhaps dive into shading, lighting, culling, clipping and even texturing.

If you've got any questions, fire away in the comments.

Good luck!

PrestonW21 (2)

btw, there's a typo in one of the code's comments. It says Trangle instead of triangle. Not a big deal but thought i'd let you know. Great tutorial as well thanks a bunch.

CoolqB (137)

@PrestonW21 Nice catch! Thanks for reading :)

DanielHerpDerp (1)

dark5am (2)

Great tutorial, thanks man!

CoolqB (137)

@dark5am You're very welcome =)

MirkoTorrisi (2)

How could i fill the triangles?

rshane (2)

@MirkoTorrisi I wrote a quick function to do this:

`def fill_triangle(self, p1, p2, p3):

``````self.pointer.begin_fill()
self.pointer.goto(p1)

self.pointer.goto(p2)

self.pointer.goto(p3)

self.pointer.end_fill()```````

replace the old

`self.line(POINTS, POINTS)

self.line(POINTS, POINTS)

self.line(POINTS, POINTS)`

with this