Mandelbrot-Set Demo Using GLSL
Fractal art is an art form in which the images are generated only with algorithmic calculations without any preset texture or image used. Mandelbrot and julia sets can be regarded as the most famous ones. In this tutorial i will be explaning you how to implement a real-time mandelbrot explorer demo program with opengl shader language. I divided the tutorial into 6 different parts:
1) Mathemathical meaning of the mandelbrot sets
2) How we use it to display an image
3) A Quick Pass Through the Algorithm:
4) A Detailed Interpretation of the Code:
5) Room for improvement
6) Some Screenshots from the Demo
Mathematical Meaning:
Lets imagine a sequence of complex numbers. It always start with the number 0 and the mathematical formula to generate one is:
zn+1 = zn2 + c
You just initialize c with some arbitrary complex number and iteratively generate the sequence. What makes this sequence so special is it's convergent and divergent properties. Proven by Benoît Mandelbrot, if any member of this particular sequence has a magnitude higher than 2 then this sequence is divergent(going to infinity) and the c value is not in the mandelbrot set. Lets see an example to give a physical feeling to the formal definition.
-For c = 1 + i: the sequence goes like = { 0 , (1 + i) , (1 + 3i) , (-7 + 9i) }
-Since 1 + 3i has a magnidute greater than 2, the point 1 + i is not in the mandelbrot set
As you can see, proving that a complex number is not an element of the mandelbrot set is easy but how can we prove the opposite ? Well the only way is to generate the sequence until a pre-defined number of elements is reached and if they all have magnitudes less then 2, we just assume that considered complex number is in the mandelbrot set.
How We Use It To Display an Image:
Complex numbers are composed of two parts, namely real and imaginary values. Thus they can be mapped to a 2-dimensional plane, like the 2d-images. When we map a region of complex numbers to a 2d-image, some of the points will be part of the mandelbrot set and some will not. Then we just paint the ones in the mandelbrot set with one arbitrary color. The beauty of the image comes from the points that are not in the mandelbrot set. We colorcode them with respect to the number of iterations needed to determine that they are not part of the mandelbrot set.
As you can see the exotic black shaded shape in the middle is the points in the mandelbrot set. The white borders are the ones which needed the highest numbers of iterations to be proven outside the set. As we go to the borders of the image, points are bigger thus are easier to eject from the mandelbrot set with less iterations so they have darker colors.
These images have a remarkable in-depth detail which consists of mini-mandelbrots and some other famous locations. To see them we must zoom in to the image but also have to increase the max_iteration number. When zoomed enough, it can be seen that every mini-mandelbrot also have the same locations and details recursively as its ancestors. A quick pass through the whole c++ and shaders code:
A Quick Pass Through the Algorithm:
In brief, what we do per frame is:
A Detailed Interpretation of the Code:
The global variables used throughout the code:
Firstly, we initialize the glut functions to have a window set-up.
Then we set up the callback functions which does the real work:
Since we won't be needing any transformation, ChangeSize function only sets the viewport and load identity matrixes to matrix stacks.
We set the Keyboard function to take the inputs for moving around or zooming the image. WASD moves the center of the image by some distance relative to zoom_ratio and space key increases the zoom value. All of these keys also invoke the display function to render the new image based upon the new variables.
The RenderScene callback function first clears the screen, then sets the shader program that we will be using. Uniform values are passed to the shader and a quad is rendered. The reason for the quad's dimensions is that it is rendered to the canonical form and there won't be any transformation in vertex shader.
The vertex shader code is simple and small because the only thing to do is pass the vertex position to be interpolated for fragment shader.
Finally the fragment shader. In fragment shader, firstly the position information from the vertex shader is mapped to the zoomed portiton of the whole image with respect to the center. Then the sequence initialized with the corresponding point is generated to determine if the point is in the mandelbrot set or not. In the end, the points that are in the set are shaded black. Points that are not in set are split into two group with respect to their iterative calculation count. Then the two group is shaded with different colors using a basic interpolation technique.
Room for Improvement:
Firstly, performance improvements. Well, I couldn't see any way of increasing the performance by using the last frames information. Since everytime you zoom, the points that you get will be totaly different , only with a transformation algorithm (if exists) which determines the mandelbrot-set membership of a point from another point, the older results can be used without generating the whole sequence again. In this case, one can use transform-feedback system to record the last frame's results.
Secondly, visual improvements. To be honest my knowledge about color interpolation techniques are just elementary. One can easily increase the visual delight that mandelbrot sets offer us by using more eleborate shading algorithms. Increasing the number of region groups may also enchance the visuality. From this point it all ends up in your imagination. Farewell...
In brief, what we do per frame is:
- update and pass in the max_iterations, zoom_ratio and center values to shaders,
- render a screen-sized quad diretly to the canonical space without any transformations,
- in vertex shader only interpolate the vertex positions to pass to the fragment shader
- in fragment shader calculate the new position value with respect to the zoom and center values, then do the iterative operation to determine if the point is inside the mandelbrot set or not and shade it with respect to that.
A Detailed Interpretation of the Code:
The global variables used throughout the code:
//shader variables
GLuint mandelbrot_shader,ite_L,zoom_L,cent_L,fib_Texture;
//main variables
int iteration_count;
float zoom_ratio;
Vector3d center;
Firstly, we initialize the glut functions to have a window set-up.
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutCreateWindow("MandelBrot");
glutFullScreen();
glewInit();
Secondly, we call the LoadScene function to load the shaders, store the shader-uniform locations. LoadShader function just goes through the simple steps of loading the shader codes, compiling them, attaching them to a program, linking the program and finally saving the generated program value to the "prog" variable passed to the function. Afterwards, we store the location info to the global variables and finally initializing the shader-passed variables.
void LoadScene()
{
LoadShader("shaders/mandelbrot_V.txt","shaders/mandelbrot_F.txt", mandelbrot_ shader);
ite_L = glGetUniformLocation(mandelbrot_shader,"ite_V");
zoom_L = glGetUniformLocation(mandelbrot_shader,"zoom_V");
cent_L = glGetUniformLocation(mandelbrot_shader,"center_V");
zoom_ratio = 3;
iteration_count = 100;
center = Vector3d(0,0,0);
}
Then we set up the callback functions which does the real work:
Since we won't be needing any transformation, ChangeSize function only sets the viewport and load identity matrixes to matrix stacks.
void ChangeSize(GLsizei w, GLsizei h)
{
glViewport(0,0,w,h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
We set the Keyboard function to take the inputs for moving around or zooming the image. WASD moves the center of the image by some distance relative to zoom_ratio and space key increases the zoom value. All of these keys also invoke the display function to render the new image based upon the new variables.
void Keyboard(unsigned char key, int x, int y)
{
if(key == 'w')
{
center = center + Vector3d(0,0.03,0) * zoom_ratio;
glutPostRedisplay();
}
if(key == 's')
{
center = center + Vector3d(0,-0.03,0) * zoom_ratio;
glutPostRedisplay();
}
if(key == 'a')
{
center = center + Vector3d(-0.03,0,0) * zoom_ratio;
glutPostRedisplay();
}
if(key == 'd')
{
center = center + Vector3d(0.03,0,0) * zoom_ratio;
glutPostRedisplay();
}
if(key == 32)
{
iteration_count += iteration_count * 0.25 ;
zoom_ratio *= 0.75;
glutPostRedisplay();
}
}
The RenderScene callback function first clears the screen, then sets the shader program that we will be using. Uniform values are passed to the shader and a quad is rendered. The reason for the quad's dimensions is that it is rendered to the canonical form and there won't be any transformation in vertex shader.
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(mandelbrot_shader);
glUniform1f(zoom_L,zoom_ratio);
glUniform1i(ite_L,iteration_count);
glUniform2f(cent_L,center.x,center.y);
glBegin(GL_QUADS);
glVertex2f(-1,-1);
glVertex2f(1,-1);
glVertex2f(1,1);
glVertex2f(-1,1);
glEnd();
glUseProgram(0);
glutSwapBuffers();
}
The vertex shader code is simple and small because the only thing to do is pass the vertex position to be interpolated for fragment shader.
varying vec2 pos_F;
void main()
{
pos_F = gl_Vertex.xy;
gl_Position = gl_Vertex;
}
Finally the fragment shader. In fragment shader, firstly the position information from the vertex shader is mapped to the zoomed portiton of the whole image with respect to the center. Then the sequence initialized with the corresponding point is generated to determine if the point is in the mandelbrot set or not. In the end, the points that are in the set are shaded black. Points that are not in set are split into two group with respect to their iterative calculation count. Then the two group is shaded with different colors using a basic interpolation technique.
uniform float zoom_V;
uniform int ite_V;
uniform vec2 center_V;
varying vec2 pos_F;
void main()
{
vec2 pos = pos_F * zoom_V + center_V;
vec2 c = pos;
bool flag = false;
float i;
vec2 cur = pos;
for(i = 0; i < ite_V; i++)
{
float dis;
vec2 result;
result.x = cur.x * cur.x - (cur.y * cur.y) + c.x;
result.y = 2.0 * cur.x * cur.y + c.y;
dis = result.x * result.x + result.y * result.y;
cur = result;
if(dis >= 4)
break;
}
vec4 color;
if(i == ite_V)
color = vec4(0,0,0,0);
else
{
float delta = 1.0 / (float)ite_V;
float border = sqrt((float)ite_V);
if(i < border)
color = vec4(0,0,1,0) * (i * delta) * 16.0 ;
else
color = vec4(1,1,1,0) * (1.0 - i * delta);
}
gl_FragColor = color;
}
Room for Improvement:
Firstly, performance improvements. Well, I couldn't see any way of increasing the performance by using the last frames information. Since everytime you zoom, the points that you get will be totaly different , only with a transformation algorithm (if exists) which determines the mandelbrot-set membership of a point from another point, the older results can be used without generating the whole sequence again. In this case, one can use transform-feedback system to record the last frame's results.
Secondly, visual improvements. To be honest my knowledge about color interpolation techniques are just elementary. One can easily increase the visual delight that mandelbrot sets offer us by using more eleborate shading algorithms. Increasing the number of region groups may also enchance the visuality. From this point it all ends up in your imagination. Farewell...
Some Screenshots From the Demo:
Hiç yorum yok:
Yorum Gönder