понедельник, 18 марта 2013 г.

AndEngine. Frosted Glass шейдер (2-е текстуры).

Предупреждение!: Я перешел на Anchor Center  и уходить не собираюсь ;) Но вы не переживайте, этот код подойдет и для старой версии GLES2, только координаты спрайтов поменяйте.
Всем привет! Наконец-то мне удалось найти отправную точку для создания шейдера использующего 2-е (!) текстуры. Вот эта точка: http://www.andengine.org/forums/tutorials/water-shader-t10103.html (автор Matthias) :) К сожалению, данный пример, хоть и показывает что используются две текстуры, все же выглядит хардкорно. Более того, показанный там пример реализации меня лично отпугнул, так как он больше похож на некий хак, а не на нормальный кодинг. Тем не менее автору поста респект, он открыл для меня строчку texture.bind(pGLState) - недостающее звено в изучении шейдеров. ))) Я советую вам пройти по данной ссылке и изучить пример автора поста, для того чтобы вы лучше поняли смысл моих претензий, а суть в следующем:

  1. @Override
  2. protected void preDraw(final GLState pGLState,
  3.                          final Camera pCamera) {
  4.     setShaderProgram(waterShader);
  5.     super.preDraw(pGLState, pCamera);
  6.                                             
        GLES20.glUniform1f(WaterShaderProgram.sUniformTime, secondsElapsed);
  7.     GLES20.glUniform1f(WaterShaderProgram.sUniformFrequency, Frequency);
  8.     GLES20.glUniform1f(WaterShaderProgram.sUniformAmplitude, Amplitude);                         };
Может мне кто-нибудь объяснить, за каким таким "надом" нам необходимо устанавливать шейдер и инициализировать униформы каждый раз(!!!) когда происходит preDraw(...)! Теперь далее:
  1.  pGLState.pushProjectionGLMatrix();
  2.   {
  3.      GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
  4.      displacementTexture.bind(pGLState, GLES20.GL_TEXTURE1);
  5.      GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  6.      RenderTextureBackgroundSprite.onDraw(pGLState, pCamera);
  7.   }
Эта хрень также вызывается постоянно(!) на onDraw(...) контейнера содержащего спрайт на котором отрисовывается шейдер... (И народ еще удивляется "чо у нас фпс упал с 60-и до 30-и?").  По поводу RenderTexture* у меня есть соображения о которых я напишу позже. Если вы поняли хотя-бы половину кода из draw() - напишите мне.
       Перед тем, как написать это сообщение, я запостил на форум свое решение данного вопроса  (ник Unregistered) и попросил дать замечания по поводу моего кода, но наши забугорные коллеги, то ли не посмотрели, то ли не знают что сказать, то ли болт забили =), поэтому обращаюсь к Вам, братья программеры! 
       Если код приведенный в конце сообщения вызовет у вас приступ тошноты или неадекватное восприятие действительности - пишите и мы вместе все исправим! Кроме того, на базе этого класса мы вместе можем сделать библиотеку шейдеров которую легко использовать в наших будущих приложениях. Есть предложение разместиться на GitHub.
      Класс, который я хочу представить, позволяет довольно быстро создавать(портировать) шейдеры, а применять их - дело двух-трех строк. Примерно за 3 часа мне удалось перенести 6 шейдеров из библиотеки Geeks3D.com и большая часть времени ушла на приведение текста к строке Java =). В общем, вот что у нас на текущий момент в активе: Ripple (Pulse), FrostedGlass(2 текстуры),  Dream Vision, Thermal Vision, GaussianBlur (горизонтальный/вертикальный), Water(из примера на форуме AndEngine 2-текстуры). 
   
     Перед тем как начнем - пара ремарок:
1. Если вы захотите прикрутить шейдер к спрайту который имеет альфу, то при запуске, вместо альфы вы увидите безпонтовую черноту, это не ошибка подхода, а просто так написан шейдер. Если кто-то соображает в GLSL буду рад получить код "исправленного" шейдера, который бы учитывал это дело..
2. Класс реализован в виде синглтона потому, что у меня реально мало времени чтобы рисовать супер-реализацию, я хотел много и быстро так, что уж извините. :) Если кто из вас захочет поучаствовать в создании библиотеки я буду только рад. Для новичков скажу, что минус в данном случае такой: например вы прикрутили шейдер к одному спрайту и ко второму. Теперь, если вы поменяете параметр шейдера, то изменение отразится на обоих спрайтах., что, собственно , мало кому нужно. Выхода два: Переделывание кода под обычный объект, или моя статья с другой (чуток измененной) реализацией. Думаю, вы выберете первое =)

     Итак, обещанный FrostedGlass эффект (не пугайтесь, что он такой длинный, основное место занимает код самого шейдера, ну он очень клсассный).
     Параметры от которых стоит отталкиваться: (mNoiseTexture, 2.0f, 5.0f, 5.0f, 0.215f,  CAMERA_WIDTH, CAMERA_HEIGHT);: 

public class FrostedGlassEffectShader extends Object{
 
 private static final FrostedGlassEffectShader INSTANCE = new FrostedGlassEffectShader();
 
 public  float vx_offset; 
 public  float pixelX; 
 public  float pixelY;
 public  float frequency;
 public  float width;
 public  float height;
 
 public  ITexture maskTexture;


 public FrostedGlassEffectShader() {
 
 }

 public static FrostedGlassEffectShader getInstance(){
  return INSTANCE;
 }
 
 public static void setup(ITexture pMaskTexture, float pvx_offset, float pPixelX, float pPixelY, float pFrequency, float pWidth, float pHeight){
  
  getInstance().vx_offset = pvx_offset;
  getInstance().pixelX = pPixelX;
  getInstance().pixelY = pPixelY;
  getInstance().frequency = pFrequency;
  getInstance().width = pWidth;
  getInstance().height = pHeight;
   getInstance().maskTexture = pMaskTexture;
  
 }
 
 public static FrostedGlassEffectShaderProgram getShaderProgram(){  
  return FrostedGlassEffectShaderProgram.getInstance();
 }

 public static class FrostedGlassEffectShaderProgram extends ShaderProgram {
 private static FrostedGlassEffectShaderProgram instance;

 public static final String FRAGMENTSHADER ="precision lowp float;\n"
  + "varying mediump vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n"
  + "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ";\n" // 0
  + "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_1 + ";\n" // 1
  + "uniform float vx_offset;\n"
  + "uniform float PixelX;\n"
  + "uniform float PixelY;\n"
  + "uniform float Freq;\n"
  + "uniform float rt_w;\n" 
  + "uniform float rt_h;\n" 

  + "vec4 spline(float x, vec4 c1, vec4 c2, vec4 c3, vec4 c4, vec4 c5, vec4 c6, vec4 c7, vec4 c8, vec4 c9)\n"
  + "{\n"
  + " float w1, w2, w3, w4, w5, w6, w7, w8, w9;\n"
  + " w1 = 0.0;\n"
  + " w2 = 0.0;\n"
  + "w3 = 0.0;\n"
  + " w4 = 0.0;\n"
  + " w5 = 0.0;\n"
  + "w6 = 0.0;\n"
  + " w7 = 0.0;\n"
  + "w8 = 0.0;\n"
  + "w9 = 0.0;\n"
  + "float tmp = x * 8.0;\n"
  + "if (tmp<=1.0) {\n"
  + "w1 = 1.0 - tmp;\n"
  + "w2 = tmp;\n"
  + "}\n"
  + "else if (tmp<=2.0) {\n"
  + "tmp = tmp - 1.0;\n"
  + "w2 = 1.0 - tmp;\n"
  + "w3 = tmp;\n"
  + "}\n"
  + "else if (tmp<=3.0) {\n"
  + "tmp = tmp - 2.0;\n"
  + "w3 = 1.0-tmp;\n"
  + "w4 = tmp;\n"
  + "}\n"
  + "else if (tmp<=4.0) {\n"
  + "tmp = tmp - 3.0;\n"
  + "w4 = 1.0-tmp;\n"
  + "w5 = tmp;\n"
  + "}\n"
  + "else if (tmp<=5.0) {\n"
  + "tmp = tmp - 4.0;\n"
  + "w5 = 1.0-tmp;\n"
  + "w6 = tmp;\n"
  + "}\n"
  + "else if (tmp<=6.0) {\n"
  + "tmp = tmp - 5.0;\n"
  + "w6 = 1.0-tmp;\n"
  + "w7 = tmp;\n"
  + "}\n"
  + "else if (tmp<=7.0) {\n"
  + "tmp = tmp - 6.0;\n"
  + "w7 = 1.0 - tmp;\n"
  + " w8 = tmp;\n"
  + "}\n"
  + "else\n" 
  + "{\n"
  + "tmp = clamp(tmp - 7.0, 0.0, 1.0);\n"
  + "w8 = 1.0-tmp;\n"
  + "w9 = tmp;\n"
  + "}\n"
  + "return w1*c1 + w2*c2 + w3*c3 + w4*c4 + w5*c5 + w6*c6 + w7*c7 + w8*c8 + w9*c9;\n"
  + "}\n"

  + "vec3 NOISE2D(vec2 p)\n"
  + "{ return texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_1 + ",p).xyz; }\n"

  + "void main() \n"
  + "{ \n"
  + "vec2 uv = " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".xy;\n"
  + "vec3 tc = vec3(1.0, 0.0, 0.0);\n"
     
  + "if (uv.x < (vx_offset-0.005))\n"
  + "{\n"
  + "float DeltaX = PixelX / rt_w;\n"
  + "float DeltaY = PixelY / rt_h;\n"
  + "vec2 ox = vec2(DeltaX,0.0);\n"
  + "vec2 oy = vec2(0.0,DeltaY);\n"
  + "vec2 PP = uv - oy;\n"
  + "vec4 C00 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP - ox);\n"
  + "vec4 C01 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP);\n"
  + "vec4 C02 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP + ox);\n"
  + "PP = uv;\n"
  + "vec4 C10 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP - ox);\n"
  + "vec4 C11 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP);\n"
  + "vec4 C12 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP + ox);\n"
  + "PP = uv + oy;\n"
  + "vec4 C20 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP - ox);\n"
  + "vec4 C21 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP);\n"
  + "vec4 C22 = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",PP + ox);\n"
     
  + "float n = NOISE2D(Freq*uv).x;\n"
  + "n = mod(n, 0.111111)/0.111111;\n"
  + "vec4 result = spline(n,C00,C01,C02,C10,C11,C12,C20,C21,C22);\n"
  + "tc = result.rgb;  \n"
  + "}\n"
  + "else if (uv.x>=(vx_offset+0.005))\n"
  + "{\n"
  + " tc = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", uv).rgb;\n"
  + "}\n"
   
  + "gl_FragColor = vec4(tc, 1.0);\n"
  + "}\n";

 public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformTexture1Location = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformVx_offsetLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformFrequencyLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformPixelXLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformPixelYLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformRT_WidthLocation = ShaderProgramConstants.LOCATION_INVALID;
 public static int sUniformRT_HeightLocation = ShaderProgramConstants.LOCATION_INVALID;
 
 public static FrostedGlassEffectShaderProgram getInstance() {
  if (instance == null)
   instance = new FrostedGlassEffectShaderProgram();
  return instance;
 }

 private FrostedGlassEffectShaderProgram() {
  super(PositionTextureCoordinatesShaderProgram.VERTEXSHADER,
    FRAGMENTSHADER);
 }

 @Override
 protected void link(final GLState pGLState)
   throws ShaderProgramLinkException {
  GLES20.glBindAttribLocation(this.mProgramID,
    ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION,
    ShaderProgramConstants.ATTRIBUTE_POSITION);
  GLES20.glBindAttribLocation(this.mProgramID,
    ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES_LOCATION,
    ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES);

  super.link(pGLState);

  FrostedGlassEffectShaderProgram.sUniformModelViewPositionMatrixLocation = this
    .getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);
  FrostedGlassEffectShaderProgram.sUniformTexture0Location = this
    .getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_0);
  FrostedGlassEffectShaderProgram.sUniformTexture1Location = this
  .getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_1);
  FrostedGlassEffectShaderProgram.sUniformVx_offsetLocation = this
  .getUniformLocation("vx_offset");
  FrostedGlassEffectShaderProgram.sUniformFrequencyLocation = this
  .getUniformLocation("Freq");
  FrostedGlassEffectShaderProgram.sUniformPixelXLocation = this
  .getUniformLocation("PixelX");
  FrostedGlassEffectShaderProgram.sUniformPixelYLocation = this
  .getUniformLocation("PixelY");
  FrostedGlassEffectShaderProgram.sUniformRT_WidthLocation = this
  .getUniformLocation("rt_w");
  FrostedGlassEffectShaderProgram.sUniformRT_HeightLocation = this
  .getUniformLocation("rt_h");
 }

 @Override
 public void bind(final GLState pGLState,
   final VertexBufferObjectAttributes pVertexBufferObjectAttributes) {
  GLES20.glDisableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);
  
  
  
  super.bind(pGLState, pVertexBufferObjectAttributes);
  
  FrostedGlassEffectShader.getInstance().maskTexture.bind(pGLState, GLES20.GL_TEXTURE1);
  GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  
  GLES20.glUniformMatrix4fv(
    FrostedGlassEffectShaderProgram.sUniformModelViewPositionMatrixLocation, 1,
    false, pGLState.getModelViewProjectionGLMatrix(), 0);
  GLES20.glUniform1i(FrostedGlassEffectShaderProgram.sUniformTexture0Location, 0);
  GLES20.glUniform1i(FrostedGlassEffectShaderProgram.sUniformTexture1Location, 1);
  GLES20.glUniform1f(sUniformVx_offsetLocation, FrostedGlassEffectShader.getInstance().vx_offset);
  GLES20.glUniform1f(sUniformFrequencyLocation, FrostedGlassEffectShader.getInstance().frequency);
  GLES20.glUniform1f(sUniformPixelXLocation, FrostedGlassEffectShader.getInstance().pixelX);
  GLES20.glUniform1f(sUniformPixelYLocation, FrostedGlassEffectShader.getInstance().pixelY);
  GLES20.glUniform1f(sUniformRT_WidthLocation, FrostedGlassEffectShader.getInstance().width);
  GLES20.glUniform1f(sUniformRT_HeightLocation, FrostedGlassEffectShader.getInstance().height);
 }

 @Override
 public void unbind(final GLState pGLState) throws ShaderProgramException {
  GLES20.glEnableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);
  super.unbind(pGLState);
 }
 
 }
}


Напоминаю - этот шейдер использует 2-е текстуры. Первая текстура - автоматически берется со спрайта к которому мы применяем шейдер, а вторую (текстура с шумом) вы должны подгрузить отдельно стандартными средствами AndEngine.

Как использовать этот класс:

Sprite sprite = new Sprite(CAMERA_WIDTH/2, CAMERA_HEIGHT/2, 800,480, mSprite1TextureRegion, getVertexBufferObjectManager()); 
FrostedGlassEffectShader.setup(mNoiseTexture, 0, 5.0f, 5.0f, 0.215f, 800f, 480f);
this.getShaderProgramManager().loadShaderProgram(FrostedGlassEffectShader.getShaderProgram());
sprite.setShaderProgram(FrostedGlassEffectShader.getShaderProgram());

mScene.attachChild(sprite);

mNoiseTexture - это та самая текстура с "шумом". Я использовал вот эту (Geeks3D.com):

Загрузить ее можно например вот так:

mNoiseTexture = new BitmapTexture(
     this.getTextureManager(), new IInputStreamOpener() {

      @Override
      public InputStream open() throws IOException {
       return getAssets().open("gfx/noise.jpg");
      }
     }, TextureOptions.BILINEAR_PREMULTIPLYALPHA);

mNoiseTexture.load();

Параметры шейдера:
В объяснениях, пожалуй, нуждается только pvx_offset - параметр отвечающий за положение границы эффекта, меняется от 0 - 1. Что это за граница такая, вы можете легко выяснить сами задавая разные значения этому параметру.
Остальные параметры влияют на внешний вид. Обратите внимание, что все параметры являются uniform, т.е. мы можем задавать их значения в RT и сразу видеть производимый эффект! Это можно сделать например так:

mScene.registerUpdateHandler(new TimerHandler(0.01f, true, new  ITimerCallback() {
   private boolean dir = false;
   @Override
   public void onTimePassed(TimerHandler pTimerHandler) {
    
    if (FrostedGlassEffectShader.getInstance().vx_offset >= 1 ){
     
     dir = true;
    }
    if (FrostedGlassEffectShader.getInstance().vx_offset <= 0 ){
     
     dir = false;
    }
    if (dir){
     FrostedGlassEffectShader.getInstance().vx_offset -=0.01f;
    }else{
     FrostedGlassEffectShader.getInstance().vx_offset +=0.01f;
    }
    
   }
  }));

    Рассказывать по классу почти нечего, а если все-таки возникнут вопросы или пожелания - пишите сюда.
    Если вы дочитали сообщение до этой строчки, значит вы заинтересованы в дальнейшем развитии библиотеки)) И, специально для вас, далее пойдут пояснения к данному классу.
  • Класс является наследником Object, сделал я это для того, чтобы переменные которые участвуют в настройке и "жизни" шейдера, не болтались где попало, а являлись частью одного целого.
  • Одним из основных методов класса является метод setup(... ... ...); Этот метод был  введен для первоначальной инициализации шейдера. Через этот метод мы передаем доп.текстуры, начальные значения и т.д. Параметры метода будут меняться в зависимости от того какой шейдер вы захотите реализовать. Например: в шейдере FrostedGlass у нас это текстура "шума" и обычные значения потому, что этот шейдер является "стационарным". Т.е. весь его эффект заключается в отображении картинки "через замерзшее стекло" (конечно мы можем прикрутить к нему риалтайм изменения, но его суть все же не в этом), а если, например у нас шейдер изначально подразумевает некое движение (Ripple или Water)? Тогда, через наш setup(), мы можем передать объект Engine, чтобы внутри класса реализовать изменение переменной времени и, как следствие, дать волнам движение не выходя за рамки самого класса, а извне, спокойно управлять, например, интенсивностью, амплитудой, частотой и скоростью. Если коротко, нам нужен этот метод чтобы облегчить себе работу с шейдером в будущем. Кстати, для некоторых шейдеров (для тех которые не имеют uniform), метод setup() можно вообще не указывать или лучше оставить пустышкой чтобы не нарушать общности.
  • Реализация шейдерной программы ничем не отличается от описанных мною в предыдущих статьях. Разница только в том, что если мы используем более одной текстуры, то нам надо ее прибиндить в методе bind() (см. код) и потом обязательно указать активную текстуру строкой 
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
     (активной в нашем случае всегда должна быть TEXTURE_0, т.е. текстура того спрайта к которому мы применяем шейдер). Если не задать активную текстуру, то наблюдается такой эффект - все спрайты размещенные на сцене будут иметь текстуру вашего "отшейдеренного" :) спрайта.
         Вот, наверное, и все ключевые моменты данной реализации. 
      Если вы соберетесь самостоятельно портировать какой-нибудь шейдер запомните пару ключевых моментов которые помогут вам сэкономить время на отладку:
  • если в исходнике найденого вами шейдера униформам сразу задаются значения:
    uniform float param1 = 1.0;
    uniform float param2 = 0.25;
    то вам необходимо убрать эти значения (= 1.0 и =0.25) из кода шейдера и задать их программно (вынести в переменные вашего основного класса), либо задать их внутри шейдера как константы: const ..., но это не наш метод :)).
  • если в шейдере задан массив, например:
    float offset[3] = float[]( 0.0, 1.3846153846, 3.2307692308 );
    то исправьте на float offset[3] = float[3]( 0.0, 1.3846153846, 3.2307692308 ); т.е. явно укажите длину массива в правой части.
Если этого не сделать, то компилятор шейдера будет выдавать ошибку без каких-либо объяснений, учтите это. Вообще, таких несоответствий много и если я с ними буду сталкиваться, то обязательно вас предупрежу.
      И напоследок поразмышляем о  RenderTexture. На мой взгляд, этот класс нужен нам для проецирования изображения на экране на специальную текстуру с которой можно потом что-нибудь сотворить... Ну например, шейдер с эффектом движения горячего воздуха (Heat-эффект). Этот эффект нельзя применить к конкретному спрайту потому, что он теряет всякий смысл (как мы знаем, любое изображение должно искажаться если на него смотреть сквозь жар от костра). Т.е. у нас имеется "жар от костра", а за ним движутся другие шашлыки объекты (спрайты), которые искажаются за жаром, и не искажаются если они не за жаром. Вот тут, я думаю, нам и понадобитя RenderTexture, но проверим мы это в следующем сообщении ибо на сегодня уже достаточно. 
         
        Всем удачи, присылайте свои реализации шейдеров!

* По поводу этого класса я напишу следующую статью. А сейчас скажу только, что этот класс-сказка! :)
https://github.com/unregistered33/ShaderTest.git

1 комментарий:

  1. Casino & Resort - WooriSinos.info
    Casino 스포츠 토토 & Resort in Waterloo, Wisconsin is a five-minute 야동 사이트 순위 drive from the Museum of the 스포츠토토 Gods. At this 텐벳 casino, the table games are fun and 생활 바카라

    ОтветитьУдалить