Газовый шейдер в Cocos2d

    Добрый день.
    Хотела поделиться своим небольшим опытом оптимизации шейдеров на IOS, и по возможности услышать дельные советы на этот счет. Вроде бы есть прекрасный инструмент OpenGl ES 2.0, и можно сделать неплохие эффекты, но при этом получить более-менее вменяемый fps не всегда получается.



    Начала с простого. Сделана небольшая программка построения бесконечного газопровода, обходя препятствия и подключая объекты. Задача – отрисовать газ, заполняющий трубу. Программирую я на Cocos2d, материалов по шейдерам там не очень-то много, но по большому счету, можно легко пристраивать любые примеры доступные для ios и андроида. Небольшое отличие, которое вносит непосредственно Cocos2d, — это передача координат вершинных шейдеров в экранных размерах, а не в диапазоне [-1.0, 1.0]. Это весьма удобно.
    Для конвертации диапазонов кокос сам формирует матрицу CC_MVPMatrix, и добавляет ее в вертексный шейдер.
    За основу газа взят шейдер, который комбинирует цвета на основе входящей текстуры шума Перлина.

    precision mediump float;
    varying vec4 Position;
    varying vec2 v_texCoord;
    uniform float Offset;
    uniform sampler2D uTextNoise;
    
    void main (void)
    {
        vec4 noisevec;
        vec3 color;
        float intensity;
        
        vec3 FireColor1 = vec3(0.5, 0.7, 0.8);
        vec3 FireColor2 = vec3(0.1, 0.3, 0.8);
        
        noisevec = texture2D(uTextNoise, Position.xy);
        noisevec = texture2D(uTextNoise, vec2 (Position.x+noisevec[1]+Offset, Position.y-noisevec[2]+Offset));
        
        intensity = 1.5 * (noisevec[0] + noisevec[1] + noisevec[2] + noisevec[3]);
        intensity = 1.95 * abs(intensity - 0.35);
        intensity = clamp(intensity, 0.0, 1.0);
        color = mix(FireColor1, FireColor2, intensity) * 1.8;
        
        gl_FragColor = vec4(color,1.0);
    } 


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

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

    -(void) createFBO
    {
        GLint	_oldFBO;
        glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_oldFBO);
        
        ccGLBindTexture2DN(0, texOut.name); 
        glGenFramebuffers(1, &_noiseFBO);
        glBindFramebuffer(GL_FRAMEBUFFER, _noiseFBO);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texOut.name, 0);
        
        glBindFramebuffer(GL_FRAMEBUFFER, _oldFBO);
        glActiveTexture(GL_TEXTURE0);
    }
    


    Отрисовка будет выглядеть примерно так:

    -(void)draw
    {  
        ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        
        GLint	_oldFBO;
        glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_oldFBO);
        glBindFramebuffer(GL_FRAMEBUFFER, _noiseFBO);
        
        [shaderProgramNoise use];
        [shaderProgramNoise setUniformsForBuiltins];
        
        glUniform1f(quOffset0, offset);
        ccGLBindTexture2DN(0, texNoise.name); 
        
        ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position|kCCVertexAttribFlag_TexCoords);
        glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexArr);
        glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, vertexTex);
        
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glBindFramebuffer(GL_FRAMEBUFFER, _oldFBO);
        
        [shaderProgramAlpha use];
        [shaderProgramAlpha setUniformsForBuiltins];
        ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position|kCCVertexAttribFlag_TexCoords);
        ccGLBindTexture2DN(0, texOut.name); 
        ccGLBindTexture2DN(1, texTemplate.name);
    
        glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexArrOut);
        glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, vertexTexOut);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glActiveTexture(GL_TEXTURE0);
    } 


    В отрисовке мы сначала подключаем фрейм буффер, в котором shaderProgramNoise формирует текстуру texNoise маленького размера.
    Далее в рендер буффере shaderProgramAlpha отрисовывает обработку двух текстур: газа и трафарета. Текстуру трафарета первоначально я отрисовывала в другой фрейм буфер, используя картинки полных и частичных труб.

    Fps значительно улучшился. Тем не менее, полученный вариант был вовсе не оптимальным. Как выяснилось, увеличение количества frambuffer’ов (! хотя их всего было 2!) нагружает систему, и fps сразу падает на десяток. Понятное дело, что такая драматическая ситуация наблюдалась только на iphone4, тем не менее это надо учитывать при разработке и не плодить дополнительные frambuffer’ы без необходимости.
    Хорошим решением было бы разместить трафаретную текстуру в stencil буффер frameBuffera c газом. Там где значение стенсила 0, не происходит вызов пиксельного шейдера. Однако, неприятным моментом в использовании stencil- буфферов является невозможность запихать туда текстуру. Т.е. рисовать туда придется треугольниками – а это полное ФУ (для данной задачи). Но раз уж я упоминула о stencil-буффере, скажу, что все-таки запихать туда текстуру мне удалось. Поскольку вызов glDrawArrays для stencil’а тоже использует шейдерную программу, то используя в этой программе discard для прозрачных областей, мы все-таки можем получить там трафарет текстуры. Если кому-то интересно смотрите такой способ:

    -(void)draw
    {
        glDisable(GL_DEPTH_TEST);
        ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        
        glClearColor(0.0/255.0, 200.0/255.0, 245.0/255.0, 1.0);
        glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 
        
        glClearStencil(0);
        glEnable(GL_STENCIL_TEST);
        
        glStencilFunc(GL_NEVER, 1, 0);
        glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);
        
        ccGLBindTexture2DN(0, stencilTexture.name);
        [shaderProgramStencil use];
        [shaderProgramStencil setUniformsForBuiltins];
    
        ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position|kCCVertexAttribFlag_Color|kCCVertexAttribFlag_TexCoords);
        glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexArr);
        glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, vertexTex);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        
        // теперь устанавливаем stencil buffer как маску
        glStencilFunc(GL_EQUAL, 1, 255);
        
        [self.shaderProgram use];
        [self.shaderProgram setUniformsForBuiltins];
        
        ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        ccGLBindTexture2DN(0, tex0.name);
    
        ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position|kCCVertexAttribFlag_Color|kCCVertexAttribFlag_TexCoords);
        glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertexArr);
        glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, vertexTex);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        
        glActiveTexture(GL_TEXTURE0);
    
    }


    Шейдер для отрисовки трафарета текстуры, будет выглядеть так:

    precision lowp float;
    varying vec2 v_texCoord;
    uniform sampler2D CC_Texture0;  // по умолчанию в кокосе
    
    void main()
    {
        vec4 color = texture2D(CC_Texture0, v_texCoord);
        if (color.a == 0.0)
        {
            discard;
        }
        gl_FragColor = color; 
    }


    Разумеется использование функции discard (а также функции if и непоследовательного доступа к текстурам) в пиксельном шейдере – это зло и низкий fps. Поэтому никакого смысла в таком надругательстве над stencil буффером нет, вот собственно поэтому его мало используют в 2d.

    В итоге, я решила избавится от отдельного “запекания” текстуры трафарета в фреймбуфер, а также от загрузки ее в stencil буффер. И пришла к такому решению: делаю один фреймбуфер c маленькой текстурой газа, строю VBO трафарета, в который складирую координаты текстур трубопровода, координаты текстур газа, и координаты экрана и рендерю на экран. Вижу отличный fps.

    Видео результата можно посмотреть здесь.
    Мне эти “упражнения” очень помогли разобраться в пучине “open gl”. Возможно, кому-то тоже будут интересны и полезны эти знания.
    • +17
    • 10.7k
    • 6
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      +13
      Как приятно видеть «хотела» и «шейдер» в одном предложении.
      Помню сразу же после курса аналитической геометрии засели с другом писать первые простейшие шейдеры, тот же bump mapping. Сколько было удовольствия!

      Подкину ка я мотивационное видео заняться шейдерами. На видео minecraft с очень простым вертексным шейдером.
      www.youtube.com/watch?v=79IhZnXSWbw
        0
        Сколько было удовольствия!

        Завидую.
        На чем учились, если не секрет?
          0
          Курс аналитической геометрии Беклемишева, имеется в свободном доступе на просторах интернетов. После того, как определения и связи в голове уложились (и экзамен был спихнут =D ) Я взял какой-то туториал, который показывал, как с нуля запилить на opengl сценку с вращающимся радужным кубиком, потом туториал накладывающий на кубик текстуру. Этого было достаточно, чтобы рискнуть попробовать сделать плоскость с фотографией, картой нормалей и попробовать написать бампмеппинг. Ну а дальше Остапа понесло.
        0
        Результат симпатичный получился.
        А вот это определяется непосредственно тем, кто пишет шейдер.
        «Небольшое отличие, которое вносит непосредственно Cocos2d, — это передача координат вершинных шейдеров в экранных размерах, а не в диапазоне [-1.0, 1.0]. Это весьма удобно. „
        Например я для простого GUI тоже выставляю ортогональную проекцию по размерам окна.
          0
          Добротная статейка!

          Разумеется использование функции discard (а также функции if и непоследовательного доступа к текстурам) в пиксельном шейдере – это зло и низкий fps.

          Ничего страшного в discard нет, if у Вас всё равно сильно не меняет ход событий, а непоследовательного доступа к текстурам я в этом шейдере не вижу. Проблема плохого FPS скорее всего просто в том, что Вы рисуете весь экран лишний раз.

          делаю один фреймбуфер c маленькой текстурой газа, строю VBO трафарета, в который складирую координаты текстур трубопровода, координаты текстур газа, и координаты экрана и рендерю на экран

          Это зачёт :)
            0
            Спасибо:)
            Под непоследовательным доступом к текстуре я имела ввиду ситуацию, когда для вычисления значения пикселя в получаемой текстуре (в пиксельном шейдере), используется один или несколько пикселей во входящей текстуре. Возможно более привычно звучит «dependent texture read». Например:
            noisevec = texture2D(uTextNoise, Position.xy);
            noisevec = texture2D(uTextNoise, vec2 (Position.x+noisevec[1]+Offset, Position.y-noisevec[2]+Offset));
            Такое часто используется для блюров и размытий.

          Only users with full accounts can post comments. Log in, please.