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

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

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

    Но сегодня мы попробуем реализовать такой эмулятор при помощи видеокарты, так как алгоритм самой игры хорошо реализовывается при помощи параллельных вычислений.
    Используем 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
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 21
    • +1
      Чего только не вытворяют с «Жизнью». Но такими темпами скоро можно будет и более сложные вычисления графики переносить на железо, освобождая проц.
      • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          просто мозг вынес :)
          • +4
            Наверное, многие хоть раз в жизни писали эмулятор игры «жизнь».
            Я вот не писал, но частота появления на Хабре топиков про жизнь так и подмывает когда-нибудь, наконец, это сделать. Подозреваю и не только меня.
            • 0
              да, чуть не запустил уже Eclipse, тока лень стало, позже, может, замучу.
              А в универе я делал на c++ такую штуку. Не вспомнил, правда, каким способом, но когда читал условие, сразу мысль про двумерный массив двумерных массивов пришла, — видимо, этим.
              • 0
                достаточно одного трёхмерного — точнее двух двухмерных: первый слой = ситуация, второй слой = счётчик соседей. затем со счётчика соседей обновляем слой ситуации.
              • 0
                на фортране побеждал, на искре-1256, в 1989 примерно.
              • –4
                на винде нету sdl.h
                  • 0
                    оч просторное понимание «винды».

                    ненавижу третьи компоненты, которые позже причиняют гемор сопровождению. особенно rxlib.

                    может вы и правы, но моя правда такова.
                    • +1
                      Автор сразу сказал, что используется библиотека SDL. Да, это «третьи компоненты». Хоть под виндой, хоть под линукс.
                      • 0
                        ну извыняйте бывшего программера. отошёл от курса дел.
                • 0
                  запустилось на Win7 x64, VS 2008.
                  goo.gl/NpTqi
                  • 0
                    а весь проект можно? а то что-то матерится насчет SxS и не запускается. а самому что куда и как настраивать чтоб шейдеры компилить, я не знаю :(
                    • +3
                      Шейдеры компилятся драйвером видеокарты, нужны только библиотеки для работы с OpenGL,SDL, ну и видеокарта, поддерживающая шейдеры, с установленными драйверами
                      • 0
                    • +1
                      Действительно замечательное упражнение для изучения языка. Хотя сам алгоритм игры жизнь вы сделали не оптимально. Любые ветвления в коде шейдера — потеря производительности. Можно заменить все ветвления хеш-таблицей. Всего существует 16 вариантов состояния клетки и её окружения. Это совсем немного. Это ещё и неплохое упражнение и в новом языке, и вообще в программинге.
                      • 0
                        Тут уже писали свой вариант на шейдерах не так давно: "Conway’s Game Of Life in Pixel Bender". И про вариант с HLSL кто-то отозвался в комментариях.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.