Газовый шейдер в 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». Возможно, кому-то тоже будут интересны и полезны эти знания.