Газовий шейдер в 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". Можливо, комусь теж будуть цікаві і корисні ці знання.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.