Understanding Two-Point Perspective Drawing using Python

Have you ever struggled to truly understand a topic? Perhaps one related to your work, or maybe linked to a hobby? Writing a computer program to investigate the subject can often help you get that extra level of understanding you’re looking for. I’ve often used this method to understand fairly abstract physics concepts in my past science work. But in this article, I’ll write about a very different topic: understanding two-point perspective drawing using Python.

In this article, I’ll walk you through the code I wrote. You can use this program to create two-point perspective drawings.

First things first: what’s two-point perspective drawing? You may know already, but when my son asked me about it in the 2020 lockdown, when we had to keep ourselves occupied at home, I only had a vague idea of what it meant.

Here’s what a two-point perspective drawing looks like, drawn using the code I’ll explain in this article:

Two-point perspective drawing of a building, drawn using Python code

As I barely knew what two-point perspective meant, I could hardly explain it to my son. Instead, he learnt about something more important: learning how to learn something new.

First Stop: YouTube

Our first instinct was to go on YouTube. And we did find some helpful videos that guided us through the technique to draw a building in 3D using two-point perspective.

We got a few sheets of paper, a couple of rulers and pencils, and we made many mistakes. The buildings we drew weren’t great. And that’s an understatement.

I felt I understood the general idea of what was going on. But I didn’t have a good grasp of it.

Time for learning two-point perspective properly.

Next Stop: Python

As I’ve done time and time again when I needed to understand something, I turned to coding and decided to try to understand two-point perspective drawing using Python.

Why is writing a computer program so effective at helping you understand a topic? The answer is that your computer doesn’t understand the topic. You have to code every detail, every aspect of what you’re trying to simulate or replicate through your program. This process forces you to drill down to the detail. You won’t be able to code it unless you can understand it.

Two Point Perspective Drawing Using Python

You’ll need to start with the two vanishing points needed to create a drawing. It’s easier to understand the vanishing point using one-point perspective drawing first. The classic example is that of railway tracks:

Image of railway tracks going into the distance to show the one-point perspective vanishing point

The lines showing the parallel tracks and the bridge’s railings are not parallel in the picture. Instead, they’re converging towards a single point in the distance. You can’t see these lines meeting at this point, but they’re going towards it.

Therefore, lines that are parallel in the real world appear to converge to a vanishing point in the distance in a one-point perspective 2D drawing.

In the two-point perspective method, there are two vanishing points, one at either end of the image:

Two-point perspective image of a building drawn using Python code

The lines defining the horizontal edges or the building and the roads all converge to one of two points located outside of the image edges.

Look at the left half of the image first. The lines representing the top and bottom of the building and those showing the road all converge to a vanishing point to the left of the image. You cannot see this vanishing point, but you can track the direction of all these lines to a common point.

The lines on the right half of the image converge to a vanishing point outside the right-hand side edge of the picture.

The vertical lines in the real world are shown as vertical in the image, too.

What Does The Program Do?

When you run the code you’ll write as you follow this article, you’ll be able to:

  • Choose the two vanishing points by clicking on the locations you want.
  • Choose the vertical reference line also by clicking on the screen.

And through keypresses, you’ll be able to:

  • Turn the drawing pen to face either of the two vanishing points.
  • Turn the drawing pen to face upwards to draw the vertical lines.
  • Move the pen forward and backwards to draw a line.
  • Toggle between having the pen up and down so you can move the pen without drawing a line when required.
  • Change the thickness of the line you draw.
  • Enter into erase mode to make corrections to your drawing.
  • Add guides to the drawing canvas at locations and orientations you desire, and remove those guides when you no longer need them.

Vanishing Points and Vertical Reference Line

Let’s start writing the code. The first things we’ll need the user to define are the locations of the two vanishing points and the location of the vertical reference line. The reference line represents the foremost part of the building.

In this article, you’ll use the turtle module. This module is part of Python’s standard library and provides a relatively straightforward way of drawing using Python.

You can start setting things up in a Python script:

import turtle

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

turtle.done()

You created the screen in which you’ll be able to draw. The call to window.setup(1.0, 1.0) sets the size of the window to the full width and the full height of your display. The use of floats as arguments indicates that you’re setting the fraction of your display width and height. You can use integers instead, and setup() interprets these as the number of pixels representing the width and height of the window you want.

You also created a Turtle() object named reference. The Turtle object is the drawing pen in the turtle module. You’ll be able to move this “turtle” across the screen and use it to draw lines. You’ve set its colour to red, and penup() lifts the “pen” from the “paper”. The turtle will not draw any lines when you move it as the pen is lifted. You’ll change this later when you’re ready for the pen to draw lines.

By default, the turtle module will display each step as the turtle moves across the screen or rotates. This setting can slow things down as the display will need to refresh the drawing repeatedly as the turtle moves. The window.tracer(0) call turns this off so that the display is refreshed only when you call window.update().

When you run the code above, you’ll note that you can see the window which fills your screen, but you cannot see the Turtle object you have created. If you add window.update(), you’ll be able to see the turtle, too, which you’ll see as a small red arrow:

import turtle

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

window.update()
turtle.done()

You’ll need to add window.update() each time you want to refresh the display.

You also need turtle.done() at the end of your code to keep the window open. Without this final line, your program will terminate immediately and close the window.

Choosing the vanishing points

You can store the vanishing points as pairs of coordinates in a list and define a function to set the vanishing points:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    window.update()

window.onclick(set_vanishing_points)

window.update()
turtle.done()

You define set_vanishing_points(), which takes two arguments: the x- and y-coordinates of the point you want to set. Next, you move the Turtle object you created earlier to those coordinates using setposition(), one of the Turtle class methods. You also draw a dot of size 10 pixels using another of the Turtle methods.

Finally, you append a tuple containing the coordinates to the list vanishing_points.

You’ve also used the function name set_vanishing_points as an argument for window.onclick(). The onclick() method calls the function set_vanishing_points() whenever you click on the drawing canvas and passes the coordinates of where you click to the function. For this reason, the function you use as an argument in onclick() must always have two parameters.

Choosing the vertical reference line

When you run this code, you’ll be able to add vanishing points by clicking on the screen. But there’s a problem. What if you keep clicking on the screen even after selecting two points?

Choosing the vanishing points

You need two and only two perspective points. You can modify set_vanishing_points() so that once you’ve selected the two points, the next click calls another function and therefore, you won’t be able to call set_vanishing_points() again:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.update()

window.update()
turtle.done()

In the definition of set_vanishing_points(), you’ve added an if statement. Once the required number of points is reached, the following click will now call a different function: set_vertical_reference_line().

By reassigning which function is bound to the click, you’re making sure set_vanishing_points() is only called twice.

You also sort the vanishing_points list. The list contains tuples, and the sort() method uses the first item in each tuple to sort the list. Therefore, the vanishing point on the left will be first in the list.

You also define set_vertical_reference_line(). This function only needs the value of the x- coordinate as it will draw a vertical line at that position. However, any function used as an argument for onclick() needs to accept two arguments. To satisfy this condition, you include the underscore _ as the second parameter in the function definition.

In set_vertical_position(), you place the turtle at the top of the screen at the x-coordinate corresponding to the click location. The turtle module places the (0, 0) coordinate at the centre of the screen. Therefore the top of the screen has a y-coordinate equal to half the window height.

Next, you draw a vertical line. Its length is the entire window height. And you shouldn’t forget the call to window.update() so that the vertical line is displayed.

When you click three times on the canvas, you’ll get the two vanishing points and the reference line showing the middle of your image. This line doesn’t have to be in the middle of the drawing canvas:

VAnishing points and vertical reference line

However, you still have the same problem you encountered earlier. If you click a fourth time, you’ll call set_vertical_reference_line() again. But you don’t want this!

Getting ready to start drawing

You can reassign what function a click calls inside set_vertical_reference_line(). This function only needs to run once. Therefore, you can change the behaviour of a click inside this function. You’re now also ready to start drawing, so the function you’ll call when you next click should set you up to start drawing:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

window.update()
turtle.done()

You’ve added another Turtle object called pen which will be your main drawing object. You’ll use pen for your actual drawing while reference is used for the guides and vanishing points.

From this point onwards, clicking on the screen will call set_pen_position(), placing the pen at the required location. The arrow representing the pen has a white centre. Arrow is the default shape in the turtle module.

Main Functions for Drawing

You’ve set the canvas with the vanishing points, the vertical reference line, and the pen you’ll use for two-point perspective drawing using Python.

Next, you need to bind functions to specific keys, which you can use to start drawing lines on the canvas. You can start with the main ones:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(put_pen_down, "space")
window.listen()

turtle.done()

You defined four additional functions to control the drawing pen:

  • move_forward() and move_backward() do as it says on the tin. You’re using the forward method of the Turtle class to move pen. They’re bound to the up and down arrow keys using window.onkeypress().
  • put_pen_down() calls the pendown() method of the Turtle object and changes the inside of the arrow to black. This shows you that the pen is down and that it will draw a line on the canvas when you move it. Note that in the key binding section in the main scope of the program, you bind the space bar key to put_pen_down(). However, once you call put_pen_down() by pressing the space bar, you also change the key binding so that the space bar will now call lift_pen_up().
  • lift_pen_up() does the opposite of put_pen_down(), including changing the colour of the arrow’s centre to white to indicate that you’ve lifted the pen from the drawing canvas. The key binding for the space bar is swapped again.

By including the calls to window.onkeypress() inside the definitions of put_pen_down() and lift_pen_up(), you’re toggling the behaviours of the space bar between the two. However, you also need an initial key binding that will take effect at the start of the program. You add this with the other key bindings outside the function definitions in the main program scope.

When using key bindings in the turtle module, you also need to call window.listen() to enable the program to listen out for keypresses while it’s running.

Note that I’ve also removed the call to window.update() at the end of the program, just before turtle.done(). You added this call at the beginning to show what it does. However, you no longer need this as each function calls window.update() when called.

The program so far behaves as follows:

  • The first two clicks place the vanishing points on the canvas.
  • The third click draws the vertical reference line.
  • The fourth click places the drawing pen on the canvas.
  • The up and down arrow keys move the pen.
  • The space bar toggles whether the pen draws a line or not.

Changing the orientation of the drawing pen

When drawing using two-point perspective, all lines should either be vertical or directed towards one of the two vanishing points.

Therefore, the next step is to include functions that allow you to change the orientation of the drawing pen to one of these three options:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

def point_vertical():
    pen.setheading(90)
    window.update()

def point_towards_left_point():
    pen.setheading(
        pen.towards(vanishing_points[0])
    )
    window.update()

def point_towards_right_point():
    pen.setheading(
        pen.towards(vanishing_points[1])
    )
    window.update()

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(put_pen_down, "space")
window.onkeypress(point_vertical, "v")
window.onkeypress(point_towards_left_point, "Left")
window.onkeypress(point_towards_right_point, "Right")
window.listen()

turtle.done()

You bound the “v” key to point_vertical() which points the pen upwards. The left and right arrow keys are bound to point_towards_left_point() and point_towards_right_point().

These functions change the heading of the Turtle object using its setheading() method. You calculate the angle required for setheading() using pen.towards(), which returns the angle of the line connecting pen to the coordinates you include as an argument for towards().

Earlier, you sorted vanishing_points so that the point furthest left is the first one in the list. Therefore, you use the index 0 when you want the point on the left and 1 when you want the point on the right.

You now have a program to draw using two-point perspective in Python:

You can draw the lines you need using a combination of keys to point the pen towards the correct position.

However, as you can see from the video above, and you probably encountered the same problem when you tried the program yourself, it’s not easy to get the lines to meet up when you complete an outline.

You’ll need to add a few more features to your program to assist you with this.

Adding Guides To Your Drawing Canvas

The most helpful addition you can make to your two-point perspective drawing Python program is the ability to add guides. You’ll need to be able to draw a line going through a specific point on your canvas which has a specific orientation that you can use to guide you as you draw.

You’ll also need to be able to delete these guides at the end once you’ve completed your drawing.

Let’s see how we can add this to the code:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

def point_vertical():
    pen.setheading(90)
    window.update()

def point_towards_left_point():
    pen.setheading(
        pen.towards(vanishing_points[0])
    )
    window.update()

def point_towards_right_point():
    pen.setheading(
        pen.towards(vanishing_points[1])
    )
    window.update()

def draw_guide():
    reference.penup()
    reference.setposition(pen.position())
    reference.setheading(pen.heading())
    reference.pendown()

    max_guide_length = (
        window.window_width() ** 2
        + window.window_height() ** 2
    ) ** 0.5
    reference.forward(max_guide_length)
    reference.forward(-2 * max_guide_length)
    window.update()

def delete_guides():
    reference.clear()
    window.update()

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(put_pen_down, "space")
window.onkeypress(point_vertical, "v")
window.onkeypress(point_towards_left_point, "Left")
window.onkeypress(point_towards_right_point, "Right")
window.onkeypress(draw_guide, "Return")
window.onkeypress(delete_guides, "Escape")

window.listen()

turtle.done()

You bind the return key to draw_guide(). The function placed the reference turtle at the same location as the pen. It also changes the orientation of reference to match that of pen.

The longest possible length for a guide line is the diagonal of the canvas, so you set this value as max_guide_length. I’m using exponent 0.5 to calculate the square root to avoid importing the math module since this would be the only time it’s needed.

You bind the escape key to delete_guides(), which clears everything that reference has drawn.

Now, you can include some well-placed guides to help you make ends meet when you draw:

You’re now ready to create your two-point perspective drawing masterpieces in Python. However, there are a few more finishing touches you can add to your code.

Finishing Touches

A useful addition to the program is to have the ability to change the thickness of the lines you draw. You can do so by adding two more functions: increase_pensize() and decrease_pensize():

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

def point_vertical():
    pen.setheading(90)
    window.update()

def point_towards_left_point():
    pen.setheading(
        pen.towards(vanishing_points[0])
    )
    window.update()

def point_towards_right_point():
    pen.setheading(
        pen.towards(vanishing_points[1])
    )
    window.update()

def draw_guide():
    reference.penup()
    reference.setposition(pen.position())
    reference.setheading(pen.heading())
    reference.pendown()

    max_guide_length = (
        window.window_width() ** 2
        + window.window_height() ** 2
    ) ** 0.5
    reference.forward(max_guide_length)
    reference.forward(-2 * max_guide_length)
    window.update()

def delete_guides():
    reference.clear()
    window.update()

def increase_pensize():
    pen.pensize(pen.pensize() + 1)
    window.title(f"pen size: {pen.pensize()}")


def decrease_pensize():
    if pen.pensize() > 1:
        pen.pensize(pen.pensize() - 1)
    window.title(f"pen size: {pen.pensize()}")

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(put_pen_down, "space")
window.onkeypress(point_vertical, "v")
window.onkeypress(point_towards_left_point, "Left")
window.onkeypress(point_towards_right_point, "Right")
window.onkeypress(draw_guide, "Return")
window.onkeypress(delete_guides, "Escape")
window.onkeypress(increase_pensize, "=")
window.onkeypress(decrease_pensize, "-")

window.listen()

turtle.done()

The two new functions take the current pen size and increase it or decrease it by 1. In the case of decrease_pensize(), you include an extra condition to ensure that the pen size does not go to 0 or negative values.

You use the keys for = and – for these functions. You bind increase_pensize() to = and not to + to avoid having to press the shift key each time you want to increase the pen size!

Erase function

When creating a two-point perspective drawing using this Python code, you’re likely to make a mistake at some point. You don’t want to have to start from scratch. You can add a couple of functions to switch to erase mode and back:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

def point_vertical():
    pen.setheading(90)
    window.update()

def point_towards_left_point():
    pen.setheading(
        pen.towards(vanishing_points[0])
    )
    window.update()

def point_towards_right_point():
    pen.setheading(
        pen.towards(vanishing_points[1])
    )
    window.update()

def draw_guide():
    reference.penup()
    reference.setposition(pen.position())
    reference.setheading(pen.heading())
    reference.pendown()

    max_guide_length = (
        window.window_width() ** 2
        + window.window_height() ** 2
    ) ** 0.5
    reference.forward(max_guide_length)
    reference.forward(-2 * max_guide_length)
    window.update()

def delete_guides():
    reference.clear()
    window.update()

def increase_pensize():
    pen.pensize(pen.pensize() + 1)
    window.title(f"pen size: {pen.pensize()}")


def decrease_pensize():
    if pen.pensize() > 1:
        pen.pensize(pen.pensize() - 1)
    window.title(f"pen size: {pen.pensize()}")

def erase():
    pen.pencolor("white")
    pen.pensize(pen.pensize() + 2)
    window.onkeypress(stop_erase, "q")
    pen.fillcolor("light blue")
    window.update()

def stop_erase():
    pen.pencolor("black")
    pen.fillcolor("black")
    pen.pensize(pen.pensize() - 2)
    window.onkeypress(erase, "q")
    window.update()

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(put_pen_down, "space")
window.onkeypress(point_vertical, "v")
window.onkeypress(point_towards_left_point, "Left")
window.onkeypress(point_towards_right_point, "Right")
window.onkeypress(draw_guide, "Return")
window.onkeypress(delete_guides, "Escape")
window.onkeypress(increase_pensize, "=")
window.onkeypress(decrease_pensize, "-")
window.onkeypress(erase, "q")

window.listen()

turtle.done()

The erase() function changes the colour of the line you draw to white, which is the same as the background colour. You also increase the pen size to make sure you can cover your errors. This function is the equivalent of using a corrector pen when writing! You also change the arrow’s colour to light blue to show that you’re in erase mode.

And stop_erase() reverses these steps so that you can return to the normal drawing mode. As you’ve seen earlier with put_pen_down() and lift_pen_up(), you’re calling window.onkeypress() inside the function definitions to toggle between erase and normal mode. You also create a key binding outside the function definition in the main scope of the code. This call to window.onclick() ensures that the “q” key is bound to erase() at the start of the program, ready to be used for the first time it’s needed.

Rapid forward and backwards movement

If you’ve already tried to draw using this code, you would have noticed that the drawing speed is rather slow. You can, if you want, increase the number used as an argument for pen.forward() in move_forward() and move_backward(). However, you want to have that fine precision to go around corners and make sure lines meet in many instances.

Instead, you can create two separate functions to enable you to move forward and backwards quicker whenever you need to:

import turtle

n_perspective_points = 2

window = turtle.Screen()
window.tracer(0)
window.setup(1.0, 1.0)

reference = turtle.Turtle()
reference.color("red")
reference.penup()

pen = turtle.Turtle()

# Setting up the vanishing points and vertical reference line
vanishing_points = []

def set_vanishing_points(x, y):
    reference.setposition(x, y)
    reference.dot(10)
    vanishing_points.append((x, y))

    if len(vanishing_points) == n_perspective_points:
        window.onclick(set_vertical_reference_line)
        # set vanishing points so that first one
        # is the one on the left (smaller x first)
        vanishing_points.sort()

    window.update()

window.onclick(set_vanishing_points)

def set_vertical_reference_line(x, _):
    reference.setposition(x, window.window_height() / 2)
    reference.setheading(-90)
    reference.pendown()
    reference.pensize(1)
    reference.forward(window.window_height())

    window.onclick(set_pen_position)
    window.update()

# Controlling the drawing pen
def set_pen_position(x, y):
    pen.penup()
    pen.fillcolor("white")
    pen.setposition(x, y)
    window.update()

# Following functions are all linked to a key
def move_forward():
    pen.forward(2)
    window.update()

def move_backward():
    pen.forward(-2)
    window.update()

def move_forward_rapidly():
    pen.forward(10)
    window.update()

def move_backward_rapidly():
    pen.forward(-10)
    window.update()

def put_pen_down():
    pen.pendown()
    pen.fillcolor("black")
    window.onkeypress(lift_pen_up, "space")
    window.update()

def lift_pen_up():
    pen.penup()
    pen.fillcolor("white")
    window.onkeypress(put_pen_down, "space")
    window.update()

def point_vertical():
    pen.setheading(90)
    window.update()

def point_towards_left_point():
    pen.setheading(
        pen.towards(vanishing_points[0])
    )
    window.update()

def point_towards_right_point():
    pen.setheading(
        pen.towards(vanishing_points[1])
    )
    window.update()

def draw_guide():
    reference.penup()
    reference.setposition(pen.position())
    reference.setheading(pen.heading())
    reference.pendown()

    max_guide_length = (
        window.window_width() ** 2
        + window.window_height() ** 2
    ) ** 0.5
    reference.forward(max_guide_length)
    reference.forward(-2 * max_guide_length)
    window.update()

def delete_guides():
    reference.clear()
    window.update()

def increase_pensize():
    pen.pensize(pen.pensize() + 1)
    window.title(f"pen size: {pen.pensize()}")


def decrease_pensize():
    if pen.pensize() > 1:
        pen.pensize(pen.pensize() - 1)
    window.title(f"pen size: {pen.pensize()}")

def erase():
    pen.pencolor("white")
    pen.pensize(pen.pensize() + 2)
    window.onkeypress(stop_erase, "q")
    pen.fillcolor("light blue")
    window.update()

def stop_erase():
    pen.pencolor("black")
    pen.fillcolor("black")
    pen.pensize(pen.pensize() - 2)
    window.onkeypress(erase, "q")
    window.update()

# Key bindings
window.onkeypress(move_forward, "Up")
window.onkeypress(move_backward, "Down")
window.onkeypress(move_forward_rapidly, "]")
window.onkeypress(move_backward_rapidly, "[")
window.onkeypress(put_pen_down, "space")
window.onkeypress(point_vertical, "v")
window.onkeypress(point_towards_left_point, "Left")
window.onkeypress(point_towards_right_point, "Right")
window.onkeypress(draw_guide, "Return")
window.onkeypress(delete_guides, "Escape")
window.onkeypress(increase_pensize, "=")
window.onkeypress(decrease_pensize, "-")
window.onkeypress(erase, "q")

window.listen()

turtle.done()

These new functions are similar to move_forward() and move_backward() but the step size is larger.

If you wish, there are other additions you can make, such as changing the colour of the lines you draw. I’ll leave this feature and others you may think are useful as an exercise for you to try out.

Creating a Two-Point Perspective Drawing Using Python

Let’s summarise the features of the two-point perspective drawing Python program by creating a brief user guide for the software:

  • When you run the program, click on the two locations where you want to place the vanishing points. Usually, these are roughly at the same height.
  • Next, click on the screen position where you want to place the vertical reference line. This line represents the foremost edge in the drawing, such as the corner of a building.
  • You can now click anywhere on the screen to place the drawing pen. You can move the pen to a new position by clicking again.
  • Press the left arrow key to turn the pen to point towards the left vanishing point, and the right arrow key to turn the pen to point towards the right vanishing point.
  • Press “v” to turn the pen to face vertically upwards.
  • Press the up arrow key to move the pen forward and the down arrow key to move the pen backwards.
  • Press “]” to move the pen forward rapidly and “[“ to move the pen backwards rapidly.
  • Press space bar to toggle between whether the pen will draw a line or not when moved. The arrow showing the location and orientation of the pen will have a white centre when the pen is lifted up.
  • Press Enter/Return to draw a guide that goes through the current location of the pen in the direction the pen is facing.
  • Press “q” to toggle between erase mode and normal mode. The centre of the arrow will be light blue when in erase mode.
  • Press “=” to increase the thickness of the lines you draw, and “-“ to decrease the line thickness.

Here’s my two-point perspective drawing using Python artistic masterpiece (ahem), shown sped up quite a bit:

You can also see a longer video which goes through the process of how to create a drawing using this code in more detail.

Final Words

Let me get back to when my son asked me about two-point perspective drawing. Writing the code above was fun (coding always is, for me!) but also very informative. It forced me to think about how each line needs to be drawn either vertically or towards one of the vanishing points. Writing the code to do this has made sure I understood this and other requirements for two-point perspective drawing.

However, once the code was complete, I also learned a lot from using it. Of course, the code became a program that I was using as a user at this point. But having been the programmer as well gave me a much better insight into what’s happening in two-point perspective drawing.

Don’t expect to see any of my work at an art gallery close to you any time soon, though.


Further Reading

image credit for railway tracks picture: https://pixabay.com/images/id-2439189/


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


Leave a Reply