2

I'm trying to model a 3d array of LEDs on a raspberry pi (model 2B) in a 10x10x10 grid. I simply want them to turn on and off based on a pattern generation algorithm.

I've written some basic code in pi3d to model 1000 spheres, held in an array. It cycles through the array and switched each led on or off by changing the sphere's colour to blue or black.

The core part of the code is as follows:

spheres = [[[pi3d.Sphere(x=x-5,y=y-5,z=z-5,radius=0.1) for x in range(dim)] for y in range(dim)] for z in range(dim)]
i = 0

while DISPLAY.loop_running():
    k = mykeys.read()
    if k == 27:
        mykeys.close()
        ISPLAY.destroy()
        break

    CAM.update(mymouse)
    for x in range (dim):
        for y in range(dim):
            for z in range(dim):
                colour=0.1
                if(((x-dim/2.0) * (x-dim/2.0)) + ((y-dim/2.0) * (y-dim/2.0)) + ((z-dim/2.0) * (z-dim/2.0)) <= i * dim):
                    colour = 1.0
                spheres[x][y][z].set_material((0.0,0.0,colour))
                spheres[x][y][z].draw()
    i=i+0.1
    if i > 4:
        i=0

This works fine, but gives me about 5 fps. Changing the spheres to cubes improved this very slightly, but I'd really like an order of magnitude performance improvement, at the least. I know there are a few efficiency gains I could make in the maths, but I experienced similar performance turning them on and off randomly, so I'm not focusing on that for now.

I though perhaps that this was just asking too much of a raspberry pi, but then played the minecraft game that comes bundled with it and found it to have greater complexity whilst rendering smoothly.

I'm wondering if there is another approach, or perhaps even another language, that I could use to give me the kind of performance I'm looking for.

I know very little of 3d programming so any suggestions or tutorials anyone can point me at would potentially be useful.

Aaron
  • 10,133
  • 1
  • 24
  • 40
user1111284
  • 886
  • 1
  • 6
  • 23

2 Answers2

1

Before you do anything, profile your code to see where it's running slowly. It's worth noting that pi3D is not necessarily going to run as fast as Minecraft's tuned 3D engine.

A sphere requires a lot of polygons to draw smooth edges. Even a conservative estimate of just 32 polygons per sphere, your total polygon count winds up being:

10 * 10 * 10 * 32 = 32000

An easy optimization is to replace the spheres with cubes:

10 * 10 * 10 * 6 = 6000

If you want the appearance of spheres, you can further reduce the polygon count by rendering 1 polygon planes that face the camera (aka: billboards) with a texture of a sphere on them.

10 * 10 * 10 * 1 = 1000

Try multiplying instead of dividing 10 / 2 is the same as 10 * 0.5 and don't do the same work twice:

x_dim = x - dim * 0.5
y_dim = y - dim * 0.5
z_dim = z - dim * 0.5

if((x_dim * x_dim) + (y_dim * y_dim) + (z_dim * z_dim) <= i * dim):

Finally, try only calling draw() once on the entire scene, rather than on each sphere.

Soviut
  • 88,194
  • 49
  • 192
  • 260
  • also sometimes referred to a billboards or [sprites](https://en.wikipedia.org/wiki/Sprite_(computer_graphics)) – Aaron May 01 '17 at 17:32
  • *“Changing the spheres to cubes improved this very slightly”* – from the question. – Ry- May 01 '17 at 17:36
  • Re-drawing the entire scene in one go sounds like it would be a big improvement. I can't see a way to do that in pi3d though, it doesn't seem to have a scene graph or canvas that I can put things in to redraw all at once. I saw one canvas object, but it seemed to only be for 2d drawing – user1111284 May 01 '17 at 21:25
  • Added answer showing how to implement these ideas (sprites and draw() once) in pi3d – paddyg May 14 '17 at 16:46
1

The problem is that there is python code doing the matrix multiplication for each pi3d.Shape one at a time. Although this is done using numpy and is as fast as possible it's still slow.

You can make all of your spheres into one pi3d.MergeShape which will then require only one draw() per frame and will be very fast... But

  1. Your Sphere objects use the default values of 12 sides x 12 slices giving 288 faces and 864 vertices so your MergeShape will have 864,000 vertices which is probably starting to slow the GPU down.

  2. the bundled shaders just use one material RGB value for the whole Shape, you want to specify a different colour for each sphere which would need a hacked shader (easy to do if you're used to hacking shaders) where you specify the RGB value in the texture coordinate fields of the buffer array.

Your code doesn't show what shader you are using, the default will be mat_light which will give a smooth 3D effect for each sphere but if you could manage with points (see the demo SpriteBalls) then you could have thousands of spheres running fast... But you would still need to modify the shader to vary the diffuse colour for each vertex.

Or you could make a texture half blue, half black and tweak the texture coordinates of the various spheres each frame. Assuming you've merged all the spheres into one shape this will be very quick (though will involve an ungainly numpy formula to reproduce the effect of your x,y,z nested loops)

Over the next few days I will try to devise a demo showing how to do these options and add it to https://github.com/pi3d/pi3d_demos

EDIT I just remembered the Starfield.py demo which uses variable colour 'billboard' points. This can render many thousand points every frame but it has all kinds of complications that obscure the relatively simple structure, as I mention above I will make a simpler version to demo your 10x10x10 array with colour change using Euclidean distance from the centre.

2nd EDIT Here is a billboard or sprite version using pi3d_demos/shaders/star_point

import pi3d
import numpy as np

DIM = 10
half_d = DIM/2.0
arr_len = DIM ** 3

disp = pi3d.Display.create()
shader = pi3d.Shader('shaders/star_point')
cam = pi3d.Camera()
spheres = pi3d.Points(camera=cam, point_size=400.0, z=15.0,
          vertices=[[x - half_d, y - half_d, z - half_d] for x in range(DIM) for y in range(DIM) for z in range(DIM)],
          normals=np.zeros((arr_len, 3)), tex_coords=np.full((arr_len, 2), 1.0))
spheres.set_shader(shader)
arr_buf = spheres.buf[0].array_buffer # shortcut to numpy array shape (1000,8) [[vx,vy,vz,nx,ny,nz,u,v]]
# the star_point shader uses nx,ny,nz as RGB values, only the B value is being
# changed here i.e. arr_buff[:,5]
i = 0
while disp.loop_running():
  spheres.draw()
  ix = np.where(np.sum((arr_buf[:,:3] - [half_d, half_d, half_d]) ** 2, axis=1) <= i * DIM)[0]
  arr_buf[:,5] = 0.1 # set all to midnight blue first
  arr_buf[ix,5] = 1.0 # set ones within (i * DIM) ** 0.5 to blue
  spheres.re_init() # have to update buffer
  i += 0.1
  if i > 4.0:
    i = 0.0

and here is a version using MergeShape then tweaking the uv coordinates

import pi3d
import numpy as np

DIM = 10
half_d = DIM/2.0
arr_len = DIM ** 3

disp = pi3d.Display.create()
shader = pi3d.Shader('uv_light')
cam = pi3d.Camera()
tex_array = np.zeros((16,16,3), dtype=np.uint8)
tex_array[:8,:8] = [0, 0, 25] # top left midnight blue
tex_array[8:, 8:] = [0, 0, 255] # bottom right bright blue
tex = pi3d.Texture(tex_array, mipmap=False)
spheres = pi3d.MergeShape(camera=cam, z=15.0)
spheres.merge([[pi3d.Sphere(radius=0.1, sides=6, slices=6), x - half_d, y - half_d, z - half_d, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0] 
                                      for x in range(DIM) for y in range(DIM) for z in range(DIM)])
spheres.set_draw_details(shader, [tex])
arr_buf = spheres.buf[0].array_buffer # shortcut to numpy array shape (1000,8) [[vx,vy,vz,nx,ny,nz,u,v]]
arr_buf[:,6:8] *= 0.5 # scale uv to just use top left part of texture
base_tex_c = arr_buf[:,6:8].copy()
i = 0
while disp.loop_running():
  spheres.draw()
  ix = np.where(np.sum((arr_buf[:,:3] - [half_d, half_d, half_d]) ** 2, axis=1) <= i * DIM)[0]
  arr_buf[:,6:8] = base_tex_c # set uv to base (top left)
  arr_buf[ix,6:8] += 0.5 # set index ix to bottome right
  spheres.re_init() # have to update buffer
  i += 0.1
  if i > 4.0:
    i = 0.0

I found that the size of the array buffer became too large with the default Sphere so reduced it to a 6x6 version. Hope this helps someone at some stage.

paddyg
  • 2,153
  • 20
  • 24
  • I had tried MergeShape but found, as you said, that then I couldn't change things inside it. It looks like your other suggestions should be enough to get the performance I'll need though, thanks! – user1111284 May 15 '17 at 09:05
  • the examples are a bit sparse I'm afraid but you might be able to figure what's going on. With the uv tweaking version you could generate a Texture with alpha by using shape (16,16,4) – paddyg May 16 '17 at 11:58
  • also forgot to mention that you can do the line calculating an array of ditances before the loop then just ix=np.where(A>=i) inside the loop for a bit more speed. So long as the centre point doesn't move. – paddyg May 19 '17 at 12:03