Pull to refresh

Эмулятор игры «жизнь» на языке GLSL

Abnormal programming
Для начала небольшой ликбез: раз, два, три.

Наверное, многие хоть раз в жизни писали эмулятор игры «жизнь».
Может быть для обучения программированию, может быть для интереса, экспериментов…
В любом случае, реализация на многих популярных языках программирования — несложное упражнение для обучения этому языку.

Но сегодня мы попробуем реализовать такой эмулятор при помощи видеокарты, так как алгоритм самой игры хорошо реализовывается при помощи параллельных вычислений.
Используем OpenGL, соответственно, язык шейдеров — GLSL. Основная программа будет написана на С++

Введение


Итак, приступим.
Для начала всё же вспомним, как происходит «смена поколений» в данной игре.
Для каждой клетки смотрим количество её живых соседей. Если оно равно 3 и исходная клетка пустая, то клетка оживает. Если исходная клетка жива, но количество соседей не равно 2 или 3, то она умирает.
Очевидно, что для каждой клетки можно проверить эти условия отдельно, достаточно лишь знать её состояние и состояние 8-и соседей на предыдущем шаге. Кроме того, переход в следующее поколение происходит «одномоментно», поэтому в простейших реализациях используется два двумерных массива для этой цели.

Есть конечно вариант с одним массивом — отмечать клетки, которые оживут и умрут, а потом уже делать изменение данных в массиве. В данной реализации мы будем использовать два фреймбуфера, в каждом по текстуре (=по двумерному массиву).
Вариант с одним массивом не проходит, например, потому, что при рендере нельзя назначить одну и ту же текстуру на чтение и на запись. Кроме того, такой метод требует двух проходов по массиву.
Так что, не будем заморачиваться, и реализуем простой вариант. Видеопамяти нынче много :)

Инициализация


Создание фреймбуфера и текстуры

Для начала создадим два фреймбуфера и добавим каждому по текстуре.
Код инициализации одного буфера:

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);


Здесь мы создали фреймбуфер и текстуру, далее с помощью glTexImage2D задали размер и формат текстуры (можно конечно было создать однокомпонентную текстуру для этих целей, а при рендере передавать её в шейдер и по данным из текстуры выводить цветной пиксель на экран, но мы ради простоты будем эту же текстуру и выводить, так что сразу задаём такой формат).
Следующая функция glFramebufferTexture2DEXT прикрепляет текстуру к фреймбуферу.
Далее мы настраиваем параметры текстуры — Nearest-фильтрацию для того, чтобы в шейдере избежать погрешностей при чтении из соседних пикселей текстуры.

Замкнутое поле

Как известно, очень часто в компьютерных реализациях эмулятора «жизни» используется тороидальное поле, т.е. левый край замыкается на правый, а верхний на нижний.
Здесь же такую особенность поля нам дарит GL_REPEAT — при попытке прочитать из текстуры значение за её краем будет получено значение с другой стороны — т.е. ровно то, что и нужно.

Редактирование клеток поля

С помощью функции glTexSubImage2D можно легко изменять данные в текстуре. Условимся считать, что клетка «жива», то все компоненты RGBA будут равны 255, а иначе они все равны 0.
Тогда код изменения одного пикселя текстуры:

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);


Загрузка шейдеров

Не буду приводить здесь код загрузки шейдеров, т.к. они заружаются в данном случае самым обычным образом.
Про использование шейдеров и фреймбуферов можно почитать здесь

Основная часть


Собственно, ради чего всё и писалось.
Здесь для простоты приведен один проход рендеринга в текстуру.
Соответственно, нужно чередовать два буфера по очереди — из одного читаем, в другой пишем.

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 (используем FFP только для простоты, по-хорошему нужно конечно создать вершинный буфер, и т.п.)

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 ();
}


В этом куске кода мы сначала назначаем фреймбуфер и шейдер как текущие, передаем в шейдер размеры поля и текстуру из другого фреймбуфера в качестве исходника. Далее, рисуем прямоугольник на весь экран, что и вызывает запуск шейдера для всех точек тестуры. В итоге текущем фреймбуфере в текстуре будет содержаться состояние поля в следующее поколение «жизни».

И конечно же, код шейдеров!



В вершинном шейдере ничего особенного не делаем, нам не нужна трансформация вершин.
void main(void)
{
gl_Position=ftransform(); // вершина без изменений
gl_TexCoord[0] = gl_MultiTexCoord0; // передаём текстурную координату во фрагментный шейдер
}


Фрагментный шейдер:
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;
}


Исходный код


Программу писал и запускал только под линуксом (Ubuntu 10.10).
Используется библиотека SDL для инициализации OpenGL-контекста.
Так что, скомпилить в Windows скорее всего можно.
Если кто соберет и выложит где-нибудь — большое спасибо.
В файле с настройками можно поменять разрешение экрана, размер поля и начальное состояние поля.

narod.ru/disk/2930622001/LifeSim.zip.html
Tags:GLSLигра жизнь
Hubs: Abnormal programming
Total votes 37: ↑34 and ↓3 +31
Views6.8K

Popular right now

Программист видеоигр С++
from 100,000 to 250,000 ₽Enad Global 7Remote job
Programming Manager (C++)
from 250,000 ₽Game InsightRemote job
Unity developer (удаленно)
from 150,000 ₽IT and DigitalRemote job
Junior Clojure Developer
from 70,000 to 150,000 ₽Health SamuraiСанкт-ПетербургRemote job