There are two significant changes that have been made to Functy recently. The first is improved per-pixel colouring and the second is improved per-pixel lighting. I’m going to leave the lighting to another time and just concentrate on the colouring for now.
Functy is basically a tool for rendering surfaces. Luckily the 3D graphics hardware in modern computers is ideally set up for rendering 3D surfaces; it’s what they were built for. However, those surfaces are almost universally defined by vertices – the positions of points on the surface – meaning that every surface is actually made up of a collection of flat triangles. The result is that graphics cards are great at rendering objects with edges, but less great at rendering curves. Modern cards are so phenomenally fast they can render enough triangles – along with a number of other clever tricks – to make this unnoticeable (nonetheless, it would be great if they actually had the ability to render curved surfaces as first-class objects).
Functy is no different in that it also renders surfaces using triangles. However, it has the advantage that in addition to the vertices, it actually knows the function that defines the surface coordinates and the colours of the points on the surface. Strictly speaking, this means it knows the surface and surface colours to an infinite level of detail.
With the recent changes made to Functy for using vertex and pixel shaders, Functy is now able to properly take advantage of this fact.
Previous versions coloured the surface by specifying the colour of each vertex that makes up a triangle on the rendered surface. The colours in between these points were interpolated, resulting in a smooth transition between different colours at adjacent vertices. This is standard practice and the graphics card can be told to do this – using for example Phong shading – automatically.
Using a fragment shader however, we can improve on this. A fragment shader is a small piece of code that’s executed for every pixel rendered on the screen. By compiling the colour function into a fragment shader, we can therefore calculate the colour accurately for ever pixel rendered, rather than having to approximate between vertices.
The following diagram illustrates this. Suppose we’re rendering a triangle which we want to colour with blue and mauve stripes. The correct result according to the function definitions is a triangle split into two colours. However, if we just colour the vertices and interpolate between them, the resulting output is like that shown on the top right. By contrast, calculating the colour correctly for every pixel gives us the result shown on the bottom right, which is much closer to the correct result we were after.
It sounds like this should be a lot of work, but in fact the result is that (in most cases) it also improves performance. The reason is that previously the colours were calculated for each vertex on the main CPU, taking this processing time away from other tasks that could otherwise be making use of it. Now the calculations are performed entirely on the graphics card’s GPU instead, through the shader processing streams. My (fairly old) graphics card has over 700 processing streams that can run in parallel, allowing the calculations to be performed easily, and taking away the need to do this on the CPU. Everyone’s a winner!
The benefits can be seen most on surfaces with lower accuracy (fewer triangles), but which still have intricate or discrete colouring detail. As you can see in the top right screenshot below, previously the detail would have become blurred or lost (especially if it appeared entirely within one triangle), but can now be rendered in full, with discrete edges as shown in the bottom right image. What’s more, using fragment shaders the colour detail is now entirely independent of the number of triangles rendered, so previous surfaces that needed high accuracy (top left) can now potentially be rendered using as few as two triangles (bottom left).
Although this is a surprisingly simple change to the program, in my view the improvements are impressive, and demonstrate the latent processing power available on graphics cards that often are left unused. Functy has also benefited from similar improvements to the method used for calculating the light reflections from surfaces, and I’ll be looking at this in a future post.