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

в 6:47, , рубрики: cocos2d, game development, objective-c, разработка под iOS, метки: ,

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

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

Начала с простого. Сделана небольшая программка построения бесконечного газопровода, обходя препятствия и подключая объекты. Задача – отрисовать газ, заполняющий трубу. Программирую я на 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”. Возможно, кому-то тоже будут интересны и полезны эти знания.

Автор: IrixV

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js