three-dimensional graphics in py5#

These tutorials have revolved around the use of two-dimensional graphics in a two-dimensional space. By now, you’ll likely be familiar with the way that coordinates work in py5: 0,0 is the top-left corner of the sketch window, and increasing those numbers can move something along the X axis (left to right) or the Y axis (top to bottom).

The two-dimensional graphics you might draw in this space (like rectangles, circles and complex polyhedrons) are rendered using the built-in py5 renderer. However, this is not the only type of renderer available.

You can actually define the renderer py5 will use in an optional argument when you size your sketch with size(). Although there are quite a few interesting options to choose from (P2D, P3D, FX2D, PDF and SVG), in this tutorial, we’ll be looking at the P3D renderer, which can be used to draw in a three-dimensional space.

The P3D renderer (Processing 3D) is a three-dimensional graphics renderer that makes use of OpenGL-compatible graphics hardware. This means that its ability to render quickly and efficiently will depend on your computer’s graphics card, but it’s a worthy trade-off to have access to three-dimensional shapes (and for most devices, it will still be quite speedy).

Before we draw anything, or even change the renderer to P3D, we need to talk about how we position objects in 3D space. First, we’ll have to look beyond the x, y positioning system, since it only accounts for two dimensions. In a 3D space, we’ll be using x, y, z positioning to place objects. The Z axis is our missing third dimension. A negative Z value will move a 3D object “farther away” from the viewer, and a positive Z value will move it “closer” to the viewer.

It’s important to note that this system is not universal! The way that the Z axis works in py5 is not, for example, the way that it works in the interface OpenGL. py5 is a “left-handed” coordinate system, and OpenGL is “right-handed”. What does this mean? If you use your right hand, then point your index finger along the Y axis (upwards) and your thumb along the X axis, then bend your middle finger, it will be pointing along the Z axis, towards a “positive” value for a right-handed system. For a left-handed system, you can do the same thing with your left hand. The right-hand rule is even depicted on one of the banknotes for the Swiss franc:

Front of the Swiss 200-franc banknote, ninth series (issued in 2018), from Wikimedia Commons

However, we also need to address the expectations you may have on how you can position these objects in 3D space. When drawing a 2D object, like a rect(), you typically pass it arguments to position it, as well as to size it.

rect(x,y,w,h) # X and Y position, then width and height

When we’re drawing a 3D shape, like a box(), you might expect we should give it X, Y and Z positions. However, rendering the positions of objects works a little differently in 3D. The positions of any shape in this 3D space will be adjusted a bit to provide the 3D illusion – so we can’t anticipate that the corners of a box(), for example, will be exactly where we would like it to be. However, the P3D renderer will do its best to position any shape we give it according to any translate() functions we’ve used.

Before we start drawing 3D shapes, we can draw 2D shapes in a 3D space to get used to the concept. The code below shows how a three-dimensional translate() function works. This is how we’ll be positioning our three-dimensional objects, too.

# Creating some global variables for positioning
x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) # Using the P3D renderer! 
    
def draw():
    global x, y, z # Let's make sure we can adjust those positioning variables
    translate(x,y,z) # We can translate using the Z value, too!
    rect_mode(CENTER) # Drawing our rectangle with the center point aligned to the screen center
    rect(0,0,100,100) 
    
    z += 1 # As Z increases, the rectangle moves closer!
    
run_sketch()

If you’ve read the other tutorials, you may recall some other transformation functions, aside from translate(), that you can use in a two-dimensional space. rotate(), for example, can spin a shape. In fact, you can use these transformations in a three-dimensional space, too. To get the old rotate() behavior that you’re used to, you’ll want to use rotate_z() with a single value.

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    rect_mode(CENTER) 
    
    rotate_z( z / 10 ) # Rotating the shape by a fraction of Z
    
    rect(0,0,100,100) 
    
    z += 1 # As Z increases, the rectangle spins and moves closer!
    
run_sketch()

In addition to rotate_z(), we have access to rotate_x() and rotate_y() in this three-dimensional space, too. Experiment with the following snippets to see how they work on 2D shapes.

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    rect_mode(CENTER) 
    
    rotate_x( z / 10 ) # Rotating the shape by a fraction of Z
    
    rect(0,0,100,100) 
    
    z += 1 # As Z increases, the rectangle spins and moves closer!
    
run_sketch()
x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    rect_mode(CENTER) 
    
    rotate_y( z / 10 ) # Rotating the shape by a fraction of Z
    
    rect(0,0,100,100) 
    
    z += 1 # As Z increases, the rectangle spins and moves closer!
    
run_sketch()

3d primitives in py5#

Let’s swap out our rect() for a three-dimensional primitive shape, box().

The box() function can take a few different arguments. If you only give it a single argument, like box(100), it will be a perfect cube, with an equal height, weight and depth of (in this case) 100 pixels. If you’d like to create an uneven, rectangular box, you can pass it three different arguments for height, weight and depth, like box(100, 70, 50). Remember, we use the translate() function to position three-dimensional objects, so box() doesn’t take any sort of coordinates.

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    
    # You can do multiple rotations at once!
    rotate_y( z / 100 ) 
    rotate_x( z / 100 ) 
    
    box(100) # A box with 100 pixel length on all sides
    
    z += 1 # As Z increases, the box spins and moves closer!
    
run_sketch()

The sphere() function is almost exactly the same – but spheres are always equilateral, so it only takes one argument.

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    
    rotate_y( z / 100 ) 
    rotate_x( z / 100 ) 
    
    sphere(100) # A sphere with 100 pixel length on all sides
    
    z += 1 # As Z increases, the sphere spins and moves closer!
    
run_sketch()

custom shapes in 3d#

You might recall that in two dimensions, you can use the begin_shape() and end_shape() functions to draw complex polygons, vertex-by-vertex. This works in 3D, too. You’ll be giving each vertex an X, Y and Z position, so things can get complex quite quickly. Here’s an example of a triangular prism drawn with vertices.

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477') # Adding a background to clear the previous frame
    
    translate(x,y,z)
    
    # a slow rotation based on our frame count
    rotate_x( frame_count / 200 ) 
    rotate_y( frame_count / 300 )
    
    begin_shape()
    vertex(-100, -100, -100)
    vertex( 100, -100, -100)
    vertex(   0,    0,  100)

    vertex( 100, -100, -100)
    vertex( 100,  100, -100)
    vertex(   0,    0,  100)

    vertex( 100, 100, -100)
    vertex(-100, 100, -100)
    vertex(   0,   0,  100)

    vertex(-100,  100, -100)
    vertex(-100, -100, -100)
    vertex(   0,    0,  100)
    end_shape()
    
    
run_sketch()

When working in a 3D space like this, it’s much easier to work with some kind of standard unit (in this case, all vertex positions are 0, 100 or -100) and then rotate or reposition the resulting shape using translations and rotations. If you wanted to draw this same prism a little to the left, you wouldn’t want to do all that math yourself!

storing 3d positions#

Since you’re using translate() to position your boxes, spheres and other shapes, accessing their position can sometimes feel a bit tricky. If I wanted to move two shapes, but at very different points in my code, I would likely be using push_matrix() and pop_matrix() to reset or clear out my translate() function. This means that if I moved a box somewhere, moving a rectangle to that same somewhere might require carefully tracking those positioning changes. After all, when a series of rotations and translations has been done, straightforward coordinates like 0,0,0 or 34, 50, 100 are no longer inherently meaningful.

Luckily, a trio of functions makes this a little easier. Using model_x(), model_y() and model_z() can allow us to find the current translated location of any coordinates. In the below code, X, Y and Z are changing each frame, and then being used for our translation function, all wrapped inside of a matrix so that they wouldn’t change the location of any subsequent shapes. However, we can easily find out where 0,0,0 (or any other location) would be in this matrix using model_x(), model_y() and model_z().

x = 250
y = 250
z = 0

def setup():
    size(500,500, P3D) 
    
def draw():
    global x, y, z 
    
    background('#004477')
    
    # randomize translation values
    x = random(width)
    y = random(height)
    z = random(-500,0)
    
    # beginning our matrix
    push_matrix()
    translate(x,y,z)
    
    sphere(100) # A sphere with 100 pixel length on all sides
    
    # Using model_x, model_y and model_z to see where translate has put our 0 positions
    print("Current X,Y,Z: " 
          + str( model_x(0,0,0) ) + ", " 
          + str( model_y(0,0,0) ) + ", " 
          + str( model_z(0,0,0) ) 
         )
    
    
    # ending our matrix
    pop_matrix()
    
    
run_sketch()
Current X,Y,Z: 483.15289306640625, 433.76934814453125, -174.49578857421875
Current X,Y,Z: 183.30369567871094, 437.2563781738281, -58.02716064453125
Current X,Y,Z: 253.91717529296875, 141.59722900390625, -386.01629638671875
Current X,Y,Z: 485.52227783203125, 66.2340087890625, -69.869140625
Current X,Y,Z: 48.67796325683594, 308.223388671875, -302.30218505859375
Current X,Y,Z: 21.77362060546875, 406.84857177734375, -87.76483154296875
Current X,Y,Z: 408.6650085449219, 435.8484802246094, -371.71636962890625
Current X,Y,Z: 135.9403533935547, 14.053298950195312, -105.60595703125
Current X,Y,Z: 75.52163696289062, 161.6375732421875, -486.1219482421875
Current X,Y,Z: 284.39569091796875, 440.91168212890625, -116.3714599609375
Current X,Y,Z: 294.7420349121094, 325.0190124511719, -459.27081298828125
Current X,Y,Z: 336.8973693847656, 23.029556274414062, -33.976959228515625
Current X,Y,Z: 322.2358703613281, 277.70611572265625, -314.938232421875
Current X,Y,Z: 498.5119323730469, 409.46624755859375, -272.6939697265625
Current X,Y,Z: 492.7420654296875, 318.40740966796875, -212.4383544921875
Current X,Y,Z: 216.26535034179688, 138.9805145263672, -63.711273193359375
Current X,Y,Z: 76.20742797851562, 284.0785217285156, -185.63836669921875
Current X,Y,Z: 231.35116577148438, 65.9593505859375, -336.12542724609375
Current X,Y,Z: 228.14149475097656, 437.0134582519531, -442.154296875
Current X,Y,Z: 374.0193176269531, 142.76568603515625, -211.9166259765625
Current X,Y,Z: 347.3322448730469, 0.8894500732421875, -313.66986083984375
Current X,Y,Z: 390.86956787109375, 410.7087707519531, -21.86407470703125
Current X,Y,Z: 394.79766845703125, 445.2142028808594, -30.317840576171875
Current X,Y,Z: 243.07574462890625, 308.0960388183594, -288.1002197265625
Current X,Y,Z: 12.190750122070312, 196.68643188476562, -200.10626220703125
Current X,Y,Z: 368.5630798339844, 494.468505859375, -41.858917236328125
Current X,Y,Z: 111.06826782226562, 246.1043701171875, -207.32757568359375
Current X,Y,Z: 187.33334350585938, 75.9483642578125, -386.9564208984375
Current X,Y,Z: 362.2627868652344, 338.3837585449219, -154.8077392578125
Current X,Y,Z: 471.271240234375, 241.6849365234375, -286.46533203125
Current X,Y,Z: 229.0855712890625, 291.9169921875, -338.4881591796875
Current X,Y,Z: 105.34735107421875, 174.68179321289062, -237.353759765625
Current X,Y,Z: 482.89801025390625, 178.05343627929688, -247.0880126953125
Current X,Y,Z: 388.8479309082031, 437.32342529296875, -156.17138671875
Current X,Y,Z: 50.57048034667969, 297.5235900878906, -192.05023193359375
Current X,Y,Z: 468.628662109375, 317.7287292480469, -69.21823120117188
Current X,Y,Z: 62.095062255859375, 335.8416748046875, -13.356781005859375
Current X,Y,Z: 30.069671630859375, 194.26197814941406, -153.7088623046875
Current X,Y,Z: 177.64910888671875, 61.38255310058594, -387.67889404296875
Current X,Y,Z: 115.47225952148438, 50.86643981933594, -356.080078125
Current X,Y,Z: 43.13954162597656, 431.33465576171875, -466.7279052734375
Current X,Y,Z: 468.96160888671875, 277.18341064453125, -230.94000244140625
Current X,Y,Z: 385.0951232910156, 310.1051025390625, -424.7071533203125
Current X,Y,Z: 429.5288391113281, 485.466552734375, -48.71649169921875
Current X,Y,Z: 148.249755859375, 5.4719696044921875, -484.684814453125
Current X,Y,Z: 62.24188232421875, 433.2490539550781, -128.25579833984375
Current X,Y,Z: 146.90023803710938, 332.3992919921875, -66.3685302734375
Current X,Y,Z: 211.32801818847656, 491.0352783203125, -402.7911376953125
Current X,Y,Z: 323.29266357421875, 104.64387512207031, -405.56689453125
Current X,Y,Z: 240.10675048828125, 449.2503356933594, -92.4447021484375
Current X,Y,Z: 112.85755920410156, 253.28306579589844, -273.1614990234375
Current X,Y,Z: 218.71932983398438, 240.55792236328125, -408.79840087890625
Current X,Y,Z: 88.44757080078125, 17.13702392578125, -273.585205078125
Current X,Y,Z: 92.58662414550781, 389.50299072265625, -177.6766357421875
Current X,Y,Z: 43.43208312988281, 38.71470642089844, -489.50726318359375
Current X,Y,Z: 404.6117248535156, 350.74212646484375, -450.5462646484375
Current X,Y,Z: 272.4700012207031, 20.4091796875, -323.7421875
Current X,Y,Z: 267.54736328125, 365.7922058105469, -65.333740234375
Current X,Y,Z: 202.3271942138672, 159.4085693359375, -352.20849609375
Current X,Y,Z: 367.1572570800781, 381.1259460449219, -431.113525390625
Current X,Y,Z: 448.5921325683594, 339.79840087890625, -492.7349853515625
Current X,Y,Z: 471.9732360839844, 291.6261901855469, -71.66595458984375
Current X,Y,Z: 206.70858764648438, 301.5210266113281, -29.87396240234375
Current X,Y,Z: 24.968658447265625, 292.6414794921875, -421.8729248046875
Current X,Y,Z: 284.1300048828125, 215.40638732910156, -14.9720458984375
Current X,Y,Z: 221.42543029785156, 262.7158508300781, -75.9271240234375
Current X,Y,Z: 271.0750732421875, 58.673004150390625, -357.4178466796875
Current X,Y,Z: 204.0425262451172, 56.85591125488281, -260.529296875
Current X,Y,Z: 494.8199462890625, 138.99073791503906, -27.49139404296875
Current X,Y,Z: 13.0177001953125, 484.0523681640625, -267.8104248046875
Current X,Y,Z: 371.3304748535156, 176.6786346435547, -42.40753173828125
Current X,Y,Z: 357.0896301269531, 137.9918670654297, -433.3892822265625
Current X,Y,Z: 190.04421997070312, 151.9492645263672, -165.3443603515625
Current X,Y,Z: 196.99635314941406, 77.11895751953125, -122.33551025390625
Current X,Y,Z: 403.20263671875, 289.69586181640625, -240.51025390625
Current X,Y,Z: 336.5689697265625, 134.74554443359375, -41.6630859375
Current X,Y,Z: 281.09686279296875, 397.7801513671875, -281.8985595703125
Current X,Y,Z: 206.77078247070312, 383.2416687011719, -143.7562255859375
Current X,Y,Z: 102.07785034179688, 436.05950927734375, -468.9937744140625
Current X,Y,Z: 174.13661193847656, 316.9664001464844, -490.3955078125
Current X,Y,Z: 319.52288818359375, 406.5914001464844, -329.5443115234375
Current X,Y,Z: 261.14666748046875, 371.805908203125, -382.25048828125
Current X,Y,Z: 364.8212585449219, 91.59640502929688, -161.8642578125
Current X,Y,Z: 129.99559020996094, 251.53091430664062, -311.77392578125
Current X,Y,Z: 53.72126770019531, 260.8821716308594, -149.058837890625
Current X,Y,Z: 310.4067077636719, 411.06243896484375, -231.42864990234375
Current X,Y,Z: 309.8070373535156, 427.0944519042969, -387.07073974609375
Current X,Y,Z: 220.1288299560547, 241.64039611816406, -42.721954345703125
Current X,Y,Z: 381.4869079589844, 435.4007568359375, -437.05364990234375
Current X,Y,Z: 451.2829895019531, 462.5299072265625, -209.2315673828125
Current X,Y,Z: 455.0464172363281, 434.4371643066406, -43.008392333984375
Current X,Y,Z: 15.826614379882812, 385.28802490234375, -272.14117431640625
Current X,Y,Z: 105.3167724609375, 410.8910827636719, -333.5653076171875
Current X,Y,Z: 204.6310272216797, 106.32391357421875, -460.2696533203125
Current X,Y,Z: 406.3569030761719, 299.3294982910156, -331.903564453125
Current X,Y,Z: 18.290115356445312, 399.8677062988281, -423.630615234375
Current X,Y,Z: 220.65655517578125, 151.26670837402344, -419.39385986328125
Current X,Y,Z: 271.57232666015625, 477.93670654296875, -218.4925537109375
Current X,Y,Z: 194.5200958251953, 298.3354797363281, -5.803985595703125
Current X,Y,Z: 108.74038696289062, 283.89190673828125, -278.0836181640625
Current X,Y,Z: 397.3038635253906, 258.8103942871094, -265.19281005859375
Current X,Y,Z: 44.37898254394531, 70.0809326171875, -306.810302734375
Current X,Y,Z: 210.89126586914062, 452.1929931640625, -383.8603515625
Current X,Y,Z: 319.4904479980469, 1.887451171875, -170.61798095703125
Current X,Y,Z: 71.29034423828125, 264.1891174316406, -160.318359375
Current X,Y,Z: 300.8430480957031, 412.8188781738281, -338.21868896484375
Current X,Y,Z: 412.89501953125, 219.93080139160156, -231.60687255859375
Current X,Y,Z: 441.58209228515625, 396.3789978027344, -7.52838134765625
Current X,Y,Z: 28.141494750976562, 208.96112060546875, -85.53851318359375
Current X,Y,Z: 417.90130615234375, 408.7789306640625, -478.350341796875
Current X,Y,Z: 300.8284606933594, 362.3199157714844, -223.42352294921875
Current X,Y,Z: 390.9452819824219, 159.16600036621094, -103.39593505859375
Current X,Y,Z: 101.95382690429688, 327.54638671875, -275.16455078125
Current X,Y,Z: 134.34814453125, 256.9185791015625, -115.50958251953125
Current X,Y,Z: 130.17510986328125, 202.5076141357422, -433.0777587890625
Current X,Y,Z: 59.781158447265625, 327.6095886230469, -336.43603515625
Current X,Y,Z: 398.2267150878906, 138.10423278808594, -119.88226318359375
Current X,Y,Z: 406.1331481933594, 129.64208984375, -171.233642578125
Current X,Y,Z: 98.86932373046875, 497.3245849609375, -322.86041259765625
Current X,Y,Z: 39.88299560546875, 110.09957885742188, -161.10711669921875
Current X,Y,Z: 118.90277099609375, 447.3978576660156, -68.759521484375
Current X,Y,Z: 240.4796905517578, 370.4513244628906, -331.20989990234375
Current X,Y,Z: 291.50408935546875, 41.29685974121094, -46.183502197265625
Current X,Y,Z: 465.74017333984375, 246.6051483154297, -476.1126708984375
Current X,Y,Z: 450.109375, 49.423370361328125, -60.49603271484375
Current X,Y,Z: 386.8868408203125, 198.9925079345703, -318.49725341796875
Current X,Y,Z: 186.0069580078125, 228.87710571289062, -302.75579833984375
Current X,Y,Z: 71.01451110839844, 169.62234497070312, -71.50314331054688
Current X,Y,Z: 406.3643493652344, 312.61016845703125, -221.7183837890625
Current X,Y,Z: 62.098052978515625, 16.565841674804688, -365.30718994140625
Current X,Y,Z: 52.154388427734375, 328.310791015625, -476.38720703125
Current X,Y,Z: 413.3092041015625, 182.3605194091797, -143.05517578125
Current X,Y,Z: 199.27081298828125, 495.3159484863281, -103.5133056640625
Current X,Y,Z: 66.17457580566406, 426.46380615234375, -216.691162109375
Current X,Y,Z: 119.97314453125, 189.11935424804688, -420.90789794921875
Current X,Y,Z: 206.37945556640625, 121.86766052246094, -442.36419677734375
Current X,Y,Z: 69.71218872070312, 67.2935791015625, -299.97454833984375
Current X,Y,Z: 43.58978271484375, 301.5831298828125, -396.16119384765625
Current X,Y,Z: 247.68902587890625, 199.521728515625, -420.5830078125
Current X,Y,Z: 169.48489379882812, 302.9715270996094, -89.78253173828125
Current X,Y,Z: 311.0644226074219, 86.85505676269531, -323.6341552734375
Current X,Y,Z: 485.7181396484375, 113.7252197265625, -345.878173828125
Current X,Y,Z: 297.9479675292969, 110.15478515625, -496.646240234375
Current X,Y,Z: 265.8119812011719, 12.036422729492188, -369.10498046875
Current X,Y,Z: 377.60009765625, 387.6795654296875, -262.88037109375

texturing 3D objects#

Using the texture() function inside of a custom shape, before you start drawing vertices, you can apply an image to the surface of a shape. In addition to loading the image (with load_image()) and applying it (with texture()), you’ll have to pass some extra arguments to each vertex. By default, these extra arguments relate to the full dimensions of the image in pixels. You can use texture_mode(NORMAL) to switch to a mode where these arguments are “normalized” to a range from 0 to 1, or texture_mode(DEFAULT) to switch back.

Wrapping an image around 3D shapes is no trivial task. The example below uses a simple 2D triangle, though in a 3D space. I’ve elected to use the grid.png image from previous tutorials here, but you could use any image of a sufficient size.

x = 250
y = 250
z = 0

img = None

def setup():
    size(500,500, P3D) 
    global img
    img = load_image("images/3d/grid.png")
    
def draw():
    global x, y, z 
    
    background('#000000')
    
    translate(x,y,z)
    
    # a slow rotation based on our frame count
    rotate_x( frame_count / 200 ) 
    rotate_y( frame_count / 300 )
    
    begin_shape()
    texture(img)
    vertex(-100, -100, -100, 0, 0)
    vertex(100, -100, -100, 300, 120)
    vertex(0, 0, 100, 200, 400)
    end_shape()
    
    
run_sketch()

importing 3d models into py5#

In addition to the (admittedly tedious) method of building 3D shapes using vertices, you can actually load 3D models into py5 with the load_shape() function! This function can take file formats of .svg or .obj, with the latter being used for 3D models. For the purposes of this demo, we’re using Suzanne, the unofficial mascot of the 3D modeling program Blender. You can download suzanne.obj yourself and put her in the same folder as your sketch, or use any other 3D model in the right format.

First, we load our .obj in the setup() block. You can use load_shape() (or load_image(), for that matter) inside of draw(), but it’s slow and expensive in terms of processing power, so it’s much better to load things into a variable ahead of time and just use them when needed.

Inside of draw(), we use shape() with our new variable (and a position of 0,0) to actually display Suzanne. In addition to applying a translate() and a few rotations, we’re using scale() to scale the model up – the default scale provided by Blender is really small!

x = 250
y = 250
z = 0

suzanne = None

def setup():
    size(500,500, P3D) 
    global suzanne
    suzanne = load_shape("images/3d/suzanne.obj")
    
def draw():
    global x, y, z 
    
    background('#004477')
    
    translate(x,y,z)
    
    # a slow rotation based on our frame count
    rotate_x( frame_count / 200 ) 
    rotate_y( frame_count / 300 )
    
    scale(100)
    shape(suzanne,0,0)
    
    
run_sketch()

Compared to other ways you may have viewed 3D models before, Suzanne is looking pretty flat without any textures or materials. The details are difficult to make out because there’s currently no lighting in our sketch – but we can fix that.

3d lighting simulation in py5#

When using the P3D or P2D renderers, py5 gives you access to a few different types of lighting. The simplest way to use this is with the lights() function, which sets up some default lighting for subsequently loaded objects. In the example below, holding the mouse button down will run lights() and add some real definition to Suzanne’s face.

x = 250
y = 250
z = 0

suzanne = None

def setup():
    size(500,500, P3D) 
    global suzanne
    suzanne = load_shape("images/3d/suzanne.obj")
    
def draw():
    global x, y, z 
    
    background('#004477')
    
    translate(x,y,z)
    rotate_z(9.45) # Putting Suzanne's head upright
    
    scale(100)
    
    if (is_mouse_pressed):
        lights()
    
    shape(suzanne,0,0)
    
    
run_sketch()
py5 encountered an error in your code:File "Timer.java", line 516, in java.util.TimerThread.run

File "Timer.java", line 566, in java.util.TimerThread.mainLoop

File "FPSAnimator.java", line 178, in com.jogamp.opengl.util.FPSAnimator$MainTask.run

File "AnimatorBase.java", line 453, in com.jogamp.opengl.util.AnimatorBase.display

File "AWTAnimatorImpl.java", line 81, in com.jogamp.opengl.util.AWTAnimatorImpl.display

File "GLWindow.java", line 782, in com.jogamp.newt.opengl.GLWindow.display

File "GLDrawableHelper.java", line 1147, in jogamp.opengl.GLDrawableHelper.invokeGL

File "GLDrawableHelper.java", line 1293, in jogamp.opengl.GLDrawableHelper.invokeGLImpl

File "GLAutoDrawableBase.java", line 443, in jogamp.opengl.GLAutoDrawableBase$2.run

File "GLDrawableHelper.java", line 674, in jogamp.opengl.GLDrawableHelper.display

File "GLDrawableHelper.java", line 692, in jogamp.opengl.GLDrawableHelper.displayImpl

File "PSurfaceJOGL.java", line 825, in processing.opengl.PSurfaceJOGL$DrawListener.display

File "PApplet.java", line 2185, in processing.core.PApplet.handleDraw

File "Sketch.java", line 198, in py5.core.Sketch.draw

File "jdk.proxy2.$Proxy8.java", line -1, in jdk.proxy2.$Proxy8.run_method

File "org.jpype.proxy.JPypeProxy.java", line -1, in org.jpype.proxy.JPypeProxy.invoke

File "org.jpype.proxy.JPypeProxy.java", line -2, in org.jpype.proxy.JPypeProxy.hostInvoke

File "PApplet.java", line 12587, in processing.core.PApplet.shape

File "PGraphics.java", line 4167, in processing.core.PGraphics.shape

Exception: Java Exception

The above exception was the direct cause of the following exception:

File "C:\Users\PC\AppData\Local\Temp\ipykernel_10984\53904551.py", line 25, in draw
    12   def draw():
 (...)
    21       
    22       if (is_mouse_pressed):
    23           lights()
    24       
--> 25       shape(suzanne,0,0)
    ..................................................
     suzanne = None
    ..................................................

java.lang.NullPointerException: java.lang.NullPointerException: Cannot invoke "processing.core.PShape.isVisible()" because "shape" is null

Using lights() is a quick way of using a few different lighting functions made available in py5: ambient_light(), directional_light(), light_falloff() and light_specular(). You can adjust these qualities manually and also create another type of lighting, point_light().

The functions ambient_light(), directional_light() and point_light() all create new lighting sources, but with slightly different properties.

ambient_light() has no particular direction, and spreads throughout the space. It takes three arguments for the color of the light (R, G and B values) and two or three arguments for its position in the sketch (X, Y and Z coordinates).

directional_light() is aimed in a direction, and will be stronger on some surfaces depending on the angle at which it hits them. It takes three arguments for the color of the light (R, G and B values) and two or three arguments for its direction (along the X, Y and Z axis).

point_light() is a positioned light that does not light the space equally – a happy medium between an ambient and a directional light, good for lighting specific areas. It takes three arguments for the color of the light (R, G and B values) and two or three arguments for its position in the sketch (X, Y and Z coordinates).

Meanwhile, light_falloff() and light_specular() adjust the properties of any subsequent lighting in the sketch to give you more fine-tuned control.

It can be difficult to understand light_falloff() without further studying digital lighting, but experimenting with the three arguments it takes (constant, linear and quadratic values) can yield interesting results.

The light_specular() function adjusts the specular color of the lights, which refers to the color of the highlight or “shine” on lit objects. It takes three arguments for R, G and B values.

The below sketch sets the specular values to a teal color, and when the mouse is pressed, produces a dark blue point light at the current mouse position. Try holding the mouse down and dragging it around your sketch to watch how the location of the point light changes the shadows and highlights.

x = 250
y = 250
z = 0

suzanne = None

def setup():
    size(500,500, P3D) 
    global suzanne
    suzanne = load_shape("images/3d/suzanne.obj")
    
def draw():
    global x, y, z 
    
    background('#004477')
    
    translate(x,y,z)
    rotate_z(9.45) # Putting Suzanne's head upright
    
    scale(100)
    
    if (is_mouse_pressed):
        light_specular(0,180,180) # Teal highlights/shine
        point_light(0,0,255,mouse_x,mouse_y,0) # A dark blue point light
        
    
    shape(suzanne,0,0)
    
    
run_sketch()

In addition to adjusting the properties of the lights, you can adjust the properties of the objects in the scene and how they react to light. specular() works similarly to light_specular(), but for the models themselves instead of the lights. shininess() takes a single value from 0 to 255 to determine the glossiness or shininess of objects. You can also adjust ambient reflective color values with ambient() and emissive color values with emissive().

There’s a lot more experimentation you could do with lighting in py5, but that’s enough for a quick taste. Let’s move on to adjusting our view of the 3D environment.

camera adjustment in py5#

Using camera(), you can set a few different values. The first three (which refer to an X, Y and Z position, of course) are the position of the camera itself. The next three coordinates refer to the center of the screen, towards which the camera is pointing. Finally, you can define custom “up” directions for the X, Y and Z axis in your scene. Below, we use camera() with the mouse position to pan around Suzanne’s front and sides, while always pointing directly at her.

x = 250
y = 250
z = 0

suzanne = None

def setup():
    size(500,500, P3D) 
    global suzanne
    suzanne = load_shape("images/3d/suzanne.obj")

    
def draw():
    global x, y, z 
    
    background('#004477')
    
    
    camera(mouse_x, mouse_y, width/2.0, # Adjusting the camera based on mouse position
           x, y, z, # ... but always pointing towards Suzanne
           0, 1, 0)
    
    translate(x,y,z)
    rotate_z(9.45) # Putting Suzanne's head upright
    
    scale(100)
    
    if (is_mouse_pressed):
        light_specular(0,180,180) # Teal highlights/shine
        point_light(0,0,255,mouse_x,mouse_y,0) # A dark blue point light
        
    
    shape(suzanne,0,0)
    
    
run_sketch()

Of course, this is a simple way of experimenting with camera positioning. If you wanted to create a scene where the camera could be moved with the arrow keys, it might be a little more complex, but you’d also have much greater control over these values.

where next?#

One potential area worth exploring is the world of shaders. If you happen to have any lying around, you can load shader files (probably in a .GLSL format) into py5 using the shader() function with a filename. Shaders are complex and fascinating, and completely beyond the scope of this tutorial.

However, it’s worth checking out The Book of Shaders for an introduction to this world, and parts of the book which apply to Processing are broadly applicable to what we’ve done here in py5.

Have fun!