📜 ⬆️ ⬇️

The emulator of the game "life" in the GLSL language

For a start, a small educational program: one , two , three .

Probably many at least once in their life wrote an emulator of the game "life".
Maybe for learning programming, maybe for interest, experiments ...
In any case, implementation in many popular programming languages ​​is a simple exercise for learning this language.

But today we will try to implement such an emulator using a video card, since the algorithm of the game itself is well implemented using parallel computing.
We use OpenGL, respectively, the language of shaders - GLSL. The main program will be written in C ++

Introduction


So let's get started.
To begin with, let us remember how the “change of generations” takes place in this game.
For each cell, we look at the number of its living neighbors. If it is 3 and the original cell is empty, then the cell comes to life. If the source cell is alive, but the number of neighbors is not equal to 2 or 3, then it dies.
Obviously, for each cell, you can check these conditions separately; you only need to know its state and the state of 8 neighbors in the previous step. In addition, the transition to the next generation occurs “simultaneously”, so in the simplest implementations two two-dimensional arrays are used for this purpose.
')
There is certainly an option with one array - to mark the cells, which will come to life and die, and then make changes to the data in the array. In this implementation, we will use two framebuffers, each with a texture (= on a two-dimensional array).
The variant with one array does not work, for example, because when rendering it is impossible to assign the same texture to read and write. In addition, this method requires two passes through the array.
So, let's not bother, and implement a simple option. Video memory is a lot now :)

Initialization


Creating a framebuffer and texture

First, create two framebuffers and add each one by texture.
Single buffer initialization code:

glGenFramebuffersEXT(1,&FrameBufferID);
glGenTextures(1,?ColorBufferID);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FrameBufferID );
glBindTexture(GL_TEXTURE_2D,ColorBufferID);
glTexImage2D(GL_TEXTURE_2D,0,4,SizeX,SizeY,0,RGBA,GL_UNSIGNED_BYTE,0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT0_EXT,GL_TEXTURE_2D,ColorBufferID,0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glBindTexture(GL_TEXTURE_2D,0);
glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);


Here we created a framebuffer and a texture, then with the help of glTexImage2D we set the size and format of the texture (you could of course create a one-component texture for this purpose, and render it to the shader by rendering the color pixel to the screen, but for simplicity the same texture and output, so immediately set this format).
The following function glFramebufferTexture2DEXT attaches a texture to the framebuffer.
Next, we configure the texture parameters - Nearest-filtering in order to avoid errors in the shader when reading from the neighboring texture pixels.

Closed field

As is well known, very often in computer implementations of the “life” emulator, a toroidal field is used, the left edge closes at the right, and the top edge at the bottom.
Here, this feature of the field gives us GL_REPEAT - when trying to read a value from a texture, its value will be received from the other side, i.e. exactly what you need.

Editing field cells

Using the glTexSubImage2D function, you can easily change the data in the texture. We agree to assume that the cell is “alive”, then all the components of RGBA will be equal to 255, otherwise they will all be equal to 0.
Then the code changes one pixel of the texture:

unsigned char buf[4]={val,val,val,val}; // val is 0 or 255
glBindTexture(GL_TEXTURE_2D,ColorBufferID);
glTexSubImage2D(GL_TEXTURE_2D,0,x,y,1,1,GL_RGBA,GL_UNSIGNED_BYTE,buf);
glBindTexture(GL_TEXTURE_2D,0);


Loading shaders

I will not give here the shader load code, since in this case they are downloaded in the most usual way.
You can read about the use of shaders and framebuffers here.

Main part


Actually, for what everything was written.
Here, for simplicity, there is one rendering pass to the texture.
Accordingly, it is necessary to alternate two buffers in turn - from one we read, to the other we write.

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, FrameBufferID );
glUseProgram ( ProgramObject );
float szx=sizex;
float szy=sizey;
glUniform1f( glGetUniformLocation (ProgramObject, "SizeX" ) , szx);
glUniform1f( glGetUniformLocation (ProgramObject, "SizeY" ) , szy);

glBindTexture(GL_TEXTURE_2D,ColorTextureID_2); //
glUniform1i( glGetUniformLocation (ProgramObject, "SourceTexture" ) , 0);

glViewport(0, 0, szx,szy);

glMatrixMode ( GL_PROJECTION );
glLoadIdentity ();
glOrtho ( 0, width, 0, height, -1, 1 );
glMatrixMode ( GL_MODELVIEW );
glLoadIdentity ();
DrawQuad(0,0,szx,szy,-1.0);

glBindTexture(GL_TEXTURE_2D,0);

glUseProgram ( 0 );
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0 );


DrawQuad function (we use FFP only for simplicity, in a good way, of course, we need to create a vertex buffer, etc.)

void DrawQuad (float x,float y, float w, float h ,float z)
{
glBegin ( GL_QUADS );
glTexCoord2f ( 0, 0 );
glVertex3f ( x, y ,z);

glTexCoord2f ( 1, 0 );
glVertex3f ( x+w, y ,z);

glTexCoord2f ( 1, 1 );
glVertex3f ( x+w, y+h ,z);

glTexCoord2f ( 0, 1 );
glVertex3f ( x, y+h ,z);
glEnd ();
}


In this piece of code, we first assign the framebuffer and shader as current, transfer the field dimensions and texture from the other framebuffer to the shader as the source code. Next, draw a rectangle on the whole screen, which causes the launch of the shader for all points of the test. As a result, the current framebuffer in the texture will contain the state of the field in the next generation of "life."

And of course, the shader code!



In the vertex shader, we don’t do anything special, we don’t need a vertex transformation.
void main(void)
{
gl_Position=ftransform(); //
gl_TexCoord[0] = gl_MultiTexCoord0; //
}


Fragment shader:
uniform sampler2D SourceTexture;

uniform float SizeX;
uniform float SizeY;

void main(void)
{
float deltax = 1.0/SizeX;
float deltay = 1.0/SizeY;
float Sum = texture2D(SourceTexture,gl_TexCoord[0].st+vec2( deltax, deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2(-deltax, deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2(-deltax,-deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2( deltax,-deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2( 0, deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2( 0,-deltay)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2(-deltax, 0)).r +
texture2D(SourceTexture,gl_TexCoord[0].st+vec2( deltax, 0)).r ;
float center =texture2D(SourceTexture,gl_TexCoord[0].st).r;
float koef=0.0;

if(center>0.5) // - , ,
{
if(Sum>3.5 || Sum<1.5)koef=0.0;else koef=1.0; // ,
}
else
{
if(Sum>2.5 && Sum<3.5)koef=1.0; // Sum==3,
}
gl_FragColor=vec4(1.0,1.0,1.0,1.0)*koef;
}


Source


The program was written and launched only under Linux (Ubuntu 10.10).
The SDL library is used to initialize the OpenGL context.
So, compiling in Windows is most likely possible.
If someone collects and puts it somewhere, thank you very much.
In the file with the settings you can change the screen resolution, field size and the initial state of the field.

narod.ru/disk/2930622001/LifeSim.zip.html

Source: https://habr.com/ru/post/111332/


All Articles