
Mandelbrot rendering
In this blog, I’ll show how to improve the rendering speed of the mandelbrot fractal using the GPU.
Improving rendering speed using GPU
You can do several tricks to improve the renderings speed, such as decreasing the size of the Mandelbrot image, minimizing the maximum number of iterations or use parallel programming to run multiple calculations on multiple CPU cores in parallel.
I have chosen to speed up the calculations using a GPU. Highend GPU have up to 512 floating point cores (such as GeForce GTX 480), where highend computers only have 8 cores available (such as in the Mac Pro). This means that calculating the Mandelbrot on the GPU gives an enormous performance boost.
To unleash to power of the GPU, I have chosen to used OpenGL’s shader (GLSL) using the Java binding for OpenGL (JOGL). The GLSL language is very close to C and is quite easy to learn. The fragment shader for Mandelbrot (the shader that calculates the color of each pixel) can be seen here:
uniform float mandel_x; uniform float mandel_y; uniform float mandel_width; uniform float mandel_height; uniform float mandel_iterations; float calculateMandelbrotIterations(float x, float y) { float xx = 0.0; float yy = 0.0; float iter = 0.0; while (xx * xx + yy * yy <= 4.0 && iter<mandel_iterations) { float temp = xx*xx - yy*yy + x; yy = 2.0*xx*yy + y; xx = temp; iter ++; } return iter; } const vec3 blue = vec3(0.0,0.0,1.0); const vec3 white = vec3(1.0,1.0,1.0); const vec3 yellow = vec3(1.0,1.0,0.0); const vec3 red = vec3(1.0,0.0,0.0); const float colorResolution = 16.0; // how many iterations the first color band should use (2nd use the double amount) vec3 getColorByIndex(float index){ float i = mod(index,4.0); if (i<0.5){ return blue; } if (i<1.5){ return white; } if (i<2.5){ return yellow; } return red; } vec4 getColor(float iterations) { if (iterations==mandel_iterations){ return vec4(0.0,0.0,0.0,1.0); } float colorIndex = 0.0; float iterationsFloat = iterations; float colorRes = colorResolution; while (iterationsFloat>colorRes){ iterationsFloat -= colorRes; colorRes = colorRes*2.0; colorIndex ++; } float fraction = iterationsFloat/colorRes; vec3 from = getColorByIndex(colorIndex); vec3 to = getColorByIndex(colorIndex+1.0); vec3 res = mix(from,to,fraction); return vec4(res.x,res.y,res.z,1.0); } void main() { float x = mandel_x+gl_TexCoord[0].x*mandel_width; float y = mandel_y+gl_TexCoord[0].y*mandel_height; float iterations = calculateMandelbrotIterations(x,y); gl_FragColor = getColor(iterations); }
You might wonder – isn’t OpenGL meant for rendering 3D stuff only? The answer is yes. My Mandelbrot is actually in 3D, to make it look like the Java2D rendering I use an orthogonal camera (a 3D camera without perspective), and only draw a single Quad that fills the full view volume. Each vertex in the quad is assigned texture coordinates (0.0 – 1.0), these coordinates are interpolated by the graphics pipeline, so when the rendering a pixel (aka fragment) the fragment shader knows the right coordinate. The mandelbrot settings (current view and iterations) are send to the shader when updated. A simplified version of the display function looks like this:
// get memory address of uniform shader variables int mandel_x = gl.glGetUniformLocation(shaderprogram, "mandel_x"); int mandel_y = gl.glGetUniformLocation(shaderprogram, "mandel_y"); int mandel_width = gl.glGetUniformLocation(shaderprogram, "mandel_width"); int mandel_height = gl.glGetUniformLocation(shaderprogram, "mandel_height"); int mandel_iterations = gl.glGetUniformLocation(shaderprogram, "mandel_iterations"); // set uniform shader variables gl.glUniform1f(mandel_x, settings.getX()); gl.glUniform1f(mandel_y, settings.getY()); gl.glUniform1f(mandel_width, settings.getWidth()); gl.glUniform1f(mandel_height, settings.getHeight()); gl.glUniform1f(mandel_iterations, settings.getIterations()); // Reset the current matrix to the "identity" gl.glLoadIdentity(); // Draw A Quad gl.glBegin(GL.GL_QUADS); { gl.glTexCoord2f(0.0f, 0.0f); gl.glVertex3f(0.0f, 1.0f, 1.0f); gl.glTexCoord2f(1.0f, 0.0f); gl.glVertex3f(1.0f, 1.0f, 1.0f); gl.glTexCoord2f(1.0f, 1.0f); gl.glVertex3f(1.0f, 0.0f, 1.0f); gl.glTexCoord2f(0.0f, 1.0f); gl.glVertex3f(0.0f, 0.0f, 1.0f); } // Done Drawing The Quad gl.glEnd();
Running the program
Simply launch the webstart application by clicking on the launch button below, and when the application is running, click the JOGL radiobutton. If your graphics card has GLSL shader and OpenGL support, you should see mandelbrot-fractal rendered blazing fast. On my machine it is around 10 times faster that the Java2D rendering!
Edit 23 okt. 2011: Since JOGL is no longer maintained at the java.net, there is currently no webstart demo available.
Full source code is available here:
http://www.nobel-joergensen.com/java/projects/mandelbrot_jogl/javamandelbrot_src.zip
Useful links:
JOGL (Java Bindings for OpenGL)
OpenGL Shading Language (3rd Edition)
OpenGL is able to render pure 2D aswell :), and if you want to you can even just output raw pixel data (you want to do this), instead of what your doing, outputting raw pixel data to the frame buffer will speed things up, maybe you even what to just set it all up, and let a pixelshader handle it all? – and just render nothing, but one full plane, with a light source on, that happens to calculate mandelbrot using a shader 🙂
By: Emil Madsen on October 7, 2010
at 17:08
Hi Emil
Thanks for your suggesting. However I’m not totally convinced that your idea gives a major performance boost. Currently I’m only rendering one quad – so it is really not the 3D rendering that takes time. Most of the workload in the current program happens in the fragment shader (also known as the pixel shader), that calculates the mandelbrot pixel (once per pixel on the scene) – and this computation needs to be done no matter what method used.
If I’m wrong and you manage to create a faster mandelbrot program, please let me know 🙂
By: Morten Nobel-Jørgensen on October 19, 2010
at 16:33
Emil is wrong, using raw input data would be noticeably slower. Thank you for this nice example.
By: Julien on December 16, 2010
at 09:51
Great example thank you very much.
By: martin on February 5, 2012
at 10:37
Do you have a version suitable for OpenGLES2?
I tried updating the code and got it to compile but then got run-time errors.
I was running on a Raspberry Pi. ~0.7fps in Java2D
By: Howard on October 6, 2012
at 04:09
I don’t have a version for OpenGL ES 2. If you need help with OpenGL ES 2, I suggest you post a question on StackOverflow and put a link in here (these blog comments are not good at finding problems).
By: Morten Nobel-Jørgensen on October 6, 2012
at 17:55
Great example, I did some code changes this evening to get it to run on Jogamp 2.3.2 and use a microsecond accuracy timing. Running the demo on my desktop (which uses Win 10 and a GeForce GTX 1050 card), I was staggered to notice the difference that Java2D was around 20FPS, and using shaders it went up to 20,000FPS (1,000 times the speed), using 128 iterations and movement speed 10.
I’m a bit of a newbie to OpenGL, got the books and going through them, but wanted an example to help me along (as I had already previous made a mandelbrot renderer in Java2D, so was familiar with the concept). Thanks for this example !! Shaders are obviously the way to go if you need to get some serious grunt out of your machine when coding in Java and OpenGL.
By: Chris Clark on October 13, 2017
at 22:41