The normal result of drawing objects with pi3d is for them to appear on the display. However there can be situations where it might be useful to capture the output and do other processing on it before posting it to the screen. Reasons include: blurring, distorting, edge detection and any number of artistic post-processing effect but also collision detection, shadow casting and stereo imaging.
In pi3d there is a class OffScreenTexture that inherits from Texture. It isn’t really intended to be used directly but other classes inherit from it: Clashtest, Defocus, PostProcess, ShadowCaster and StereoCam. Each of these has a similar outline work flow each frame.
- Start the off-screen capture
- Draw the objects in the scene, possibly with a special shader as with Clashtest or ShadowCaster but otherwise just normally i.e. PostProcess or StereoCam
- Stop the off-screen capture
- Process the image. Sometimes using a special shader, sometimes drawing the off-screen texture to a Sprite with a normal uv_flat shader.
Because offscreen textures can be used for a wide range of reasons the details of each one will vary greatly. However the Post-processing application is probably the most general so this is a good one to look at.
Open pi3d_demos/Post.py and, after running it to see what it does, try commenting out the lines 67, 73 and 78. This will basically cut out the capturing to off-screen texture and subsequent post processing so you can see what is being captured. For the moment ignore the fancy swirling texture on the Spheres, this is a separate complication that I will explain later. If you look inside the pi3d/util/PostProcess.py file you will see that although it can be called with all its arguments defaulting to values, in this instance we are passing a Camera instance and setting the divide argument. The reason for this is explained in the docstrings: there is a facility to only capture part of the screen to generate a lower resolution off screen texture which speeds up the whole rendering process quite a bit. In order to effect this low resolution capture the camera has to be defined with a wider field of view which is done on line 28 of pi3d_demos/Post.py (scale is a tidier alternative to defining a whole lens spec with larger fov) And the camera is passed to the PostProcess constructor in line 35 along with the same scaling factor, however you will notice that the camera instance is set to self.viewcam in line 51 which is then not used! The same camera is specified for myshape and mysprite on lines 42 and 48. The reason for all this camera specification is the default instance behaviour of pi3d - which will make the default camera from the first one to be created and, as a 2D camera is created in the __init__() function of PostProcess, care has to be taken to ensure that this doesn’t become the default instance by accident.
In PostProcess line 72 you can see the OffScreenTexture._start() method call and some code to just render part of the screen using the glScissor function. On line 85 OffScreenTexture._end() stops the screen capture and draw() renders a simple subdivided quad self.sprite using self.shader, self.tex_list and self.camera. On line 63 you will see that self.tex_list[0] points to the PostProcess instance itself which inherits the behaviour of pi3d.Texture via pi3d.OffScreenTexture. There are a couple of things that make this even harder to follow: 1. on line 64 and 65 there is a facility to add additional textures (such as bump and relfection) for use by the shader, 2. on lines 99 to 101 there is a facility to modify the unif array of self.sprite. The pi3d_demos/Post.py example doesn’t use any additional textures (although some of the shaders in pi3d_demos/FilterDemo.py do) but on line 78 of pi3d_demos/Post.py you will see that post.draw() is passed a value for unif[48] that very slowly increases from 2.0 to 6.999, after which it resets to 2.0.
Now if you look in pi3d/shader/post_base.fs - the fragment shader - you will see on line 27 that use seems to be made of unif[16][0] (remember that the “flat” c_types.float(60) array in python becomes vec3[20] in GLSL so unif[48] in python is unif[16][0] in the shader.) But what exactly is it doing? Well the vertex shader is very simple, essentially just setting the vertex location in gl_Position and flipping the image top to bottom as it sets the uniform variable texcoordout. In the fragment shader, lines 26 to 29 loop nine times to increment the eventual pixel RGBA value texc. Each loop looks up the value from the PostProcess texture using Texture2D with a slightly offset coordinate dx[] and dy[] and a weighting factor f[]. unif[16][0] is used as a multiplier for the dx[] and dy[] values in order to sample the “convolution” [1] over a wider area. If you watch the demo for long enough you will see the edges gradually get wider then suddenly jump back when the value in unif[48] wraps back to 2.0.
The “star” shader used to texture the Spheres in pi3d_demos/Post.py above is another example of how you can use the GPU to do all kinds of fancy things. Look at the source code, which was contributed by Peter Hess based on www.iquilezles.org shadertoy demos. The shader works by converting the texture coordinates to a polar basis in lines 25 and 26, then applying factors that depend on an incrementing value “time” and trigonometric transformations, then using the values to lookup and modify the RGBA values from the texture sampler.
It’s quite fun to experiment with different formulas and values in shaders but, if you do, you will probably have to put your shaders in a subdirectory of your working directory (as with pi3d_demos/shaders) and you will probably have to “expand” the #include ... syntax used in the main pi3d shaders as the process of figuring out the path to import from might defeat the Shader loader! Shaders are difficult to debug as the only info is graphical output to the screen but a general rule is to start from something that works and change a very small part before testing. That way you will stand more chance of figuring out what broke it!
By using pi3d.Texture.update_ndarray() to update the Texture with a numpy array it’s possible to change the image relatively quickly. Obviously this depends on the size of the image and the power of the cpu but even on the Raspberry Pi it can give a reasonable frame rate using ffmpeg as the video decoder. Have a look at the pi3d_demos/VideoWalk.py
On line 39 image is defined as a numpy ndarray with dimensions the same as each video frame (N.B. C type arrays are rows (height) then cols (width) then RGB bytes). This array is then filled in a Thread running in the function pipe_thread() defined on line 41 and started just after that. In pipe_thread ffmpeg is run as a subprocess and the output piped into the image array (line 48). There is a slightly messy variable length sleep on line 58 to keep the video frame rate regular, and a flag is set so that the main Thread which has the pi3d frame loop can refresh the Texture after each video frame has been copied into the numpy array see line 168.
Hopefully you’ve arrived here, at the end of the book, with a better understanding of the way that pi3d uses the enormous processing power of the GPU through the OpenGL ES 2.0 standard. More importantly I hope you have a grasp of the architecture and terminology to help you search for and understand the answers to any problems you (inevitably) encounter as you start to make your own programs.
If you started reading this book because you had some specific ideas you wanted to implement then you will be tempted to launch straight into an ambitious project. I have to say that is an excellent idea. However, before you do any coding draw up a plan of action that identifies the smallest, simplest elements first then write short programs to help you get to grips with the problems one at a time at a manageable scale. This approach has the advantage of giving you encouraging feedback early on, it forces you to break the problem down into its functional elements and you build up a set of test programs to help you verify later changes to your project code.
Finally, don’t give up too quickly when you run into trouble, but don’t struggle on alone for too long either. There is always help available on-line. Try www.raspberrypi.org/forums/, groups.google.com/forum/ or stackoverflow.com to name but three.
[1] | https://en.wikipedia.org/wiki/Kernel_(image_processing) |