понедельник, 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

воскресенье, 17 февраля 2013 г.

Менеджер ресурсов

Привет! В этом сообщении я освещу некоторые вопросы связанные с загрузкой и освобождением ресурсов игры и на итог мы напишем шаблон класса который будет организовывать работу с этими ресурсами.

Ресурсами игры считается вся используемая графика, музыка, звуки и шрифты.
Многие из вас, только начиная работать с AndEngine, действовали по шаблону взятому из примеров: создавали активити, прописывали в ней при необходимости поля текстур, регионов и спрайтов; заполняли поля в onCreateResources(); собирали сцену в onCreateScene() и радовались жизни =) Проблемы начинались тогда, когда объект игры представлял собой не просто спрайт, а комплексный объект использующий свои дополнительные свойства и методы, содержащий более одного спрайта и т.д. С подходом описанным выше, приходиться либо объявлять поля активити как protected/public или передавать текстуры через конструктор объекта. Как результат:
- грязный код,
- активити заполнено полями которые этой активти не используются,
- множество параметров в конструкторах объектов,
- сложности в отслеживании неиспользуемых ресурсов.

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

Вы уже наверняка догадались, что этим условиям идеально подходит синглтон.
Сразу скажу, что решение вопроса доступа к ресурсам через синглтон целиком и полностью зависит от того, как организованы ваши игровые объекты. Т.е. игровым объектом может быть простое расширение спрайта (Object extended Sprite), а может быть и объект содержащий несколько спрайтов. Здесь я приведу код простого ResourceManager-а просто для того. чтобы обозначить сам подход к реализации.


package com.expedition107.evileye;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import org.andengine.audio.music.Music;
import org.andengine.audio.music.MusicFactory;
import org.andengine.audio.sound.Sound;
import org.andengine.audio.sound.SoundFactory;
import org.andengine.engine.Engine;
import org.andengine.entity.scene.Scene;
import org.andengine.opengl.font.Font;
import org.andengine.opengl.font.FontFactory;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.util.adt.color.Color;
import org.andengine.util.adt.io.in.IInputStreamOpener;

import android.content.Context;
import android.graphics.Typeface;

public class ResourceManager {

 private static ResourceManager Instance;
 
 //Поля для доступа к "движковым" переменным
 private Context currentActivity;
 private Engine engine;

 //текстуры
 public BitmapTexture mBackgroundBitmapTexture;
 public BitmapTexture mCloudsBitmapTexture;
 public BitmapTexture mHeroBitmapTexture;
 //регионы
 public ITextureRegion bgrTextureRegion;
    public ITextureRegion heroTextureRegion;
    public ITextureRegion cloudTextureRegion;
    public ITextureRegion arrowTextureRegion;
    
 //музыка и звуки
    public Music bgrMusic;
 public ArrayList<Sound> sounds = new ArrayList<Sound>(); 
 
 //шрифт
 public Font font;


 public synchronized static ResourceManager getInstance(){
  if(Instance == null){
   Instance = new ResourceManager();
  }
  return Instance;
 }
 
 //для того чтобы наш менеджер заработал мы должны его проинициализировать
 public synchronized void init(Context pContext, Engine pEngine){
  currentActivity = pContext;
  engine = pEngine;
 }
 
 public synchronized void loadGameSceneTextures(){
  
  try {
   mBackgroundBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/background.png");
    }
   }, TextureOptions.BILINEAR);
   mBackgroundBitmapTexture.load();
   bgrTextureRegion = TextureRegionFactory.extractFromTexture(mBackgroundBitmapTexture);
   
   mCloudsBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/clouds.png");
    }
   }, TextureOptions.BILINEAR);
   mCloudsBitmapTexture.load();
   cloudTextureRegion = TextureRegionFactory.extractFromTexture(mCloudsBitmapTexture);
   
   mHeroBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/hero.png");
    }
   }, TextureOptions.BILINEAR);
   mHeroBitmapTexture.load();
   heroTextureRegion = TextureRegionFactory.extractFromTexture(mHeroBitmapTexture, 0, 0, 100, 100);
   arrowTextureRegion = TextureRegionFactory.extractFromTexture(mHeroBitmapTexture, 110, 0, 50, 5);
   
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
  
  
  
  
 }
 
 public synchronized void unloadGameSceneTextures(){
  mBackgroundBitmapTexture.unload();
  mCloudsBitmapTexture.unload();
  mHeroBitmapTexture.unload();
  System.gc();
 }
 
 public synchronized void loadSounds(){
  SoundFactory.setAssetBasePath("sounds/");
   try {
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit1sound.mp3"));    
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit2sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit3sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit4sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hero_die_sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "shootsound.mp3"));
   } catch (final IOException e) {
             
   }
 } 
 
 public synchronized void unloadSounds(){
  while (getInstance().sounds.size() > 0){
   if (!getInstance().sounds.get(0).isReleased()){
    getInstance().sounds.get(0).release();
   }
   getInstance().sounds.remove(0);
  }
 }
 
 public synchronized void loadMusic(){
  MusicFactory.setAssetBasePath("music/");
   try {
    bgrMusic = MusicFactory.createMusicFromAsset(getInstance().engine.getMusicManager(), getInstance().currentActivity, "mfx/bgrmusic1.mp3");
   } catch (final IOException e) {
             
   }
 } 
 public synchronized void unloadMusic(){
  if (!bgrMusic.isReleased())
   bgrMusic.release();
 }
 
 public synchronized void loadFont(){
  FontFactory.setAssetBasePath("fonts/");
  font = FontFactory.create(getInstance().engine.getFontManager(), getInstance().engine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL),  32f, true);
  font.load();
 }
 
 public synchronized void unloadFont(){
  font.unload();
 }
}


Как им пользоваться:
В onCreateResources() нашей активити c начала инициализируем ResourceManager, а затем грузим всю нашу графику, звуки, шрифты и т.д.:

@Override
 public void onCreateResources() {
  ResourceManager.getInstance().init(this, mEngine);
  ResourceManager.getInstance().loadGameSceneTextures();
  ResourceManager.getInstance().loadSounds();
  ResourceManager.getInstance().loadMusic();
  ResourceManager.getInstance().loadFonts();
 }

Где-то в коде..
...
ResourceManager rm = ResourceManager.getInstance();
mArrowSprite = new Sprite(pX, pY, rm.arrowTextureRegion,  rm.engine.getVertexBufferObjectManager());
...

Вы можете спросить - и чо же такого в нем менеджерского, чтобы выделять этот код в отдельный класс? Ответ на этот вопрос кроется в организации структуры игры. Если ваша игра состоит только из одной сцены (а такого не бывает ;-)), то менеджер не нужен. Но в самом простом случае мы имеем как минимум две сцены: 1. Главное меню 2. Сцена с самой игрой. Нам не нужны ресурсы игры когда мы находимся в главном меню и наоборот - в игре нам не нужны ресурсы главного меню. Так вот, чтобы загружать/выгружать нужные/ненужные ресурсы и поможет наш менеджер.
PS. Продумывайте какие ресурсы будут задействованы во время главного геймплея. Все ресурсы необходимо грузить до того как игра началась. Если в процессе игры приложение начнет что-то загружать - это немедленно отразится на производительности.


четверг, 14 февраля 2013 г.

AndEngine GLES2-AnchorCenter vs AndEngine GLES2. Стоит ли овчинка выделки?

Всем привет!

В этом сообщении хочу немного прояснить ситуацию с новой версией AndEngine GLES2-AnchorCenter (сокращенно будем писать AC).

NOTE: Здесь я не буду подробно расписывать разницу между GLES2 и AC потому, что для этого нужно досконально знать оба движка. Данное сообщение скорее призвано  сэкономить ваше время на установку рабочей сборки AC, дать небольшое представление о целесообразности миграции с GLES2 на AC, а также немного осветить особенности нового двига.

Многие из вас, кто уже работал с версиями AndEngine GLES1 и GLES2, встречали на официальном форуме движка упоминание об AndEngine AnchorCenter (AC), а так же о том, что, это, на текущий момент, самая передовая версия AndEngine. Многие старожилы форума уже давно на нем работают, и активно участвуют в развитии данной версии. Тем не менее, от создателя движка, нет никаких официальных сообщений (релизов) этой версии в которых бы говорилось что-то вроде "ребята, велком в новую версию. юзаем, сообщаем о багах... + список отличий от GLES2". Но ведь люди ее используют, причем весьма успешно решают задачи, о которых в GLES2 даже не заикались! Даже книжку написали по нему. Ситуация усугубляется тем, что многие баги движка, почему-то в первую очередь исправляются именно в ветке AC, а не в официальном GLES2. Примером тому может служить баг в Engine.java в функциях

protected void onUpdateScene(final float pSecondsElapsed)
protected void onUpdateUpdateHandlers(final float pSecondsElapsed)

проявляющийся в лагах графики (jitter effect) при движении камеры отслеживающей какой-нибудь объект.

А так же баг в расширении AndEngineLiveWallpaper, проявляющийся в том, что обои после перезагрузки девайса или при переходе с активити на хоумскрин замораживались.И вообще, глядя на историю изменений веток GLES2 и AC, на момент написания этого сообщения, видно, что правки по GLES2 последний раз были два месяца назад, а по AC два дня назад.

На днях, в очередной раз просматривая форум на предмет интересных статей или вопросов, я опять столкнулся с Anchor Center и моя чаша терпения переполнилась...

Итак, настало время разобраться, что это за AnchorCenter и стоит ли на него переходить.
Начал я с того, что для экспериментов создал отдельный workspace. Дальше по порядку:
1. С помощью Git импортируем версию движка из ветки GLES2-AnchorCenter (здесь и далее только из нее)
https://github.com/nicolasgramlich/AndEngine.git (Почему именно c помощью Git а не просто скачать архивом, поясню: раз уж движок активно развивается и дорабатывается, то каждый раз (через месяц, неделю, день) искать отличия и выкачивать зип обновленной версии, а потом повторять процедуру импорта и линковки будет сильно накладно по времени и нервам. Git в этом случае просто спасение утопающих. Кто не умеет им пользоваться, тому самое время научиться).
2. Импортируем расширение для физики AndEngineBox2DExtension.
https://github.com/nicolasgramlich/AndEnginePhysicsBox2DExtension.git
А теперь... АХТУНГ! НЕ КАЧАЙТЕ ЭТУ ВЕРСИЮ! По крайней мере пока. На момент написания сообщения (15 февраля 2013 года) это глючный порт*. Я потратил кучу времени, чтобы удостовериться, что он НЕ юзабельный (но не обижаюсь, потому, что мне никто и не обещал, что он рабочий=)), Так что не тратьте на него свое время. Рабочая и наиболее полная (RopeJoint, GearJoint, WheelJoint, EdgeShape и т.д.) версия порта Box2D лежит тут:
https://github.com/RealFictionFactory/AndEnginePhysicsBox2DExtension.git
3. Также под AC есть расширение AndEngineTMXTiledMapExtension, но я его не проверял. Лежит здесь:
https://github.com/nicolasgramlich/AndEngineTMXTiledMapExtension.git
4. Расширение для живых обоев:
https://github.com/nicolasgramlich/AndEngineLiveWallpaperExtension.git
Должно быть рабочее. В принципе, там особо ничего не поменялось кроме исправления бага про который я написал выше.
5. Настоятельно рекомендую расширение AndEngineDebugDrawExtension:
https://github.com/nazgee/AndEngineDebugDrawExtension.git Честь и слава людям, которые делают подобные вещи! Расширение позволяет без всякой графики (да и с графикой тоже) дебажить физический мир который вы построили в своем приложении.
6. Примеры AndEngineExamples:
https://github.com/nicolasgramlich/AndEngineExamples.git
Когда я их качал пару месяцев назад, то запустить не хватило терпения. Все было в полном хаосе. Сейчас посмотрел в ветку и там, напротив src, написано следующее:
Updated all examples to the latest and greatest of AndEngine@GLES2-AnchorCenter!

Последнее изменение было 9 месяцев назад... Имеет смысл их качать или нет предлагаю удостовериться самим. Сам сделаю это попозже.

Ок. Это вроде и все, что нам нужно для создания игры на новом движке. А как же расширение для  мультитача, текстурпакера? А они уже в самом движке, что не может не радовать =).
По расширениям AndEngineMODPlayerExtension, AndEngineMultiplayerExtension, AndEngineSVGTextureRegionExtension сказать пока ничего не могу, судя по репозиторию, эти расширения присутствуют только для GLES2, но с одной стороны, в них нет ничего такого что не работало бы и в AC, а с другой стороны - за все время пользования движком, я подключал их только ради ознакомления, в реальных задачах они не участвовали (хотя это, возможно, мой минус). И все бы хорошо, но это, как ни странно - мелочи. Приятные, удобные но... мелочи.

Основное отличие AC от предыдущей версии состоит в том, что поменялась ось координат. Теперь точка (0,0) находится не в левом верхнем углу, а в левом нижнем. Такое изменение создатель движка мотивирует тем, что привел координаты сцены к родной системе OpenGL, в результате, освободились ресурсы процессора которые тратились на перевод из одних координат в другие. Ну чтож, это нововведение можно считать целесообразным, а нам не так уж и долго перестроиться. Кроме инверсии оси ординат,  координаты спрайта относительно сцены теперь также не левый верхний угол, а... центр этого спрайта! Т.е. создали спрайт:

Sprite sprite = new Sprite(100,200, mSpriteTextureRegion, VBO);

и поместили на сцену

mScene.attachChild(sprite);

Так вот центр спрайта будет аккурат в координате (100,200) . И sprite.getX(), sprite.getY() вернет нам те же 100 и 200. (Ага! Вот почему он AnchorCenter!) Немного мозголомно правда? Здесь привыкать будет чуть сложнее, но мы привыкнем, я точно знаю =) Хорошо, ну а это-то извращение для чего понадобилось? Думаю, опять же для экономии ресурсов. От центра спрайта, проще высчитывать вершины и делать с ними различные преобразования. Кстати центр этот можно задавать. Для этого в Entity предусмотрены функции setAnchorCenter.... Функции getWidthScaled и getHeightScaled теперь deprecated. Тем кому приходилось сталкиваться со скайлом и дальнейшей привязкой координат это оценят.
Итак, смена системы координат - основное отличие. Имхо, за этим изменением кроются гораздо более серьезные возможности движка о которых пока умалчивается.
Вот что пишет Nicolas Gramlich в своем блоге (цитата):

"...The coordinate system in the GLES2-AnchorCenter branch has its origin in the lower left. This was changed for multiple reasons:
  • It is the native OpenGL coordinate system. (I can save a few +- calculations here and there.)
  • It is the same coordinate system as cocos2d-iphone and cocos2d-x. (This eases porting efforts in both directions by a whole bunch!)
  • It allowed me to easily/efficiently write the AndEngineCocosBuilderExtension, which allows reading a format exported by CocosBuilder.
  • It just feels more natural for any side-scrolling game.
Another thing that changed in this branch is that the anchorpoints (rotatincenter, scalecenter, etc…) are now relative, from 0.0 to 1.0, instead of being absolute values. So in general, AndEngine got a little more cocos2d-like on this branch, which is definitely not a bad thing...."  

Про AndEngineCocosBuilderExtension заметили? Я тоже заметил. =) Судя по всему отличный инструмент, надо будет его попробовать.**

Для эксперимента взял одну из наших недоработанных игр включающую в себя физику и попробовал перенести ее на новый двиг. С тем набором расширений который перечислен выше (а этих наборов пришлось перебрать несколько )) эксперимент завершился удачно, небольшие заминки возникали именно из-за новой системы координат. Кстати, замечено, что с новой системой, прикручивать графику к физтелам стало удобнее. Чтобы понять почему удобнее,  нужно пройти путь от GLES1 до AnchorCenter поэтому объяснить это я не берусь, да оно и не к чему ).

Резюмируя данное сообщение немного по-философствуем на тему переходить или нет на AnchorCenter =):

"ЗА"
1. Исправления багов, рефакторинг и новые плюшки делаются уже для AC, в то время как столкнувшись с багом в GLES2 вам обеспечены скитания по исходникам или форуму в поисках нужной заплатки. Плюшек и рефакторинга в GLES2 ожидать вообще не приходится.
2. Наиболее необходимые расширения движка за исключением Box2D уже интегрированы в сам движок.
3. Отличия GLES2 и AC значительны по внутреннему содержанию, но с точки зрения программиста переходить не так уж и проблемно (вспомним GLES1 vs GLES2 и выдохнем =)).

"ПРОТИВ"
1. Если сам движок вполне юзабельный, то расширение Box2D в официальной ветке вызывает грусть. Не смотря на то, что есть добрые люди как Andrzej J. Debicki (RealFictionFactory) и Michal Stawinski (nazgee), все-таки хочется, чтобы рабочая версия лежала и на официальной ветке.
2. Программистская  агорафобия  =)

В целом, советую дописать то, что не дописано, на той версии на которой вы работаете сейчас, ну а следующий шедевр начинать уже в AnchorCenter! 

Спасибо за внимание.

* - глючность порта у меня проявилась в использовании PrismaticJoint. Тело, которое должно было двигаться по оси X почему-то "тонуло" под действием силы тяжести. Что я только с ним не вытворял - ничего не помогало, хотя по определению PrismaticJoint никак не могло дать такую степень свободы. В итоге оказалось, что такое поведение тела случается если зафиксировать его вращение body.setFixedRotation(true); Ну кто бы мог подумать, что эта опция напрочь убивает джоинт, и какие еще подобные сюрпризы нам уготованы?
** - попробовать это расширение не получилось =(, потому как не смог найти этот билдер под Windows. Мало того, говорят, что его и не будет. А с другой стороны - если у программиста есть Мак, то он наверное пишет под iOS ага? Короче тут облом (.

четверг, 24 января 2013 г.

AndEngine. Прикручиваем шейдер к конкретному спрайту. Blur-эффект.

Всем привет! В этом сообщении расскажу как прикрутить шейдер к конкретному спрайту. Мои предыдущие сообщения по теме шейдеров могли ввести в заблуждение тем, что процесс отрисовки результатов шейдера производился через специальную текстуру и UncoloredSprite. Все эти изыски изрядно сбивали с толку и меня,но время разобраться с этим вопросом появилось только сейчас (Maksim respect!)  =)
В этом примере мы напишем активити которая отображает спрайт с примененным к нему шейдером размытия (Blur-эффект). Все тривиально:

package com.expedition107.shader.test;

import java.io.IOException;
import java.io.InputStream;

import org.andengine.engine.Engine;
import org.andengine.engine.LimitedFPSEngine;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.*;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.sprite.Sprite;
import org.andengine.entity.sprite.UncoloredSprite;
import org.andengine.entity.util.FPSLogger;
import org.andengine.opengl.shader.PositionTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.exception.ShaderProgramException;
import org.andengine.opengl.shader.exception.ShaderProgramLinkException;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.opengl.texture.render.RenderTexture;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;
import org.andengine.ui.activity.BaseGameActivity;
import org.andengine.util.adt.io.in.IInputStreamOpener;
import org.andengine.util.color.Color;

import android.opengl.GLES20;

public class ShaderTestActivitySprite extends BaseGameActivity {
 
 public static final int WIDTH = 720;
 public static final int HEIGHT = 480;
 
 private Camera mCamera;
 private Sprite mSprite;
    private ITexture mTexture;
    private ITextureRegion mTextureRegion;
 
 @Override
 public EngineOptions onCreateEngineOptions() {
  this.mCamera = new Camera(0, 0, WIDTH, HEIGHT);
  
  final EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new RatioResolutionPolicy(WIDTH, HEIGHT), this.mCamera);

  return engineOptions;
 }
 
 @Override
    public Engine onCreateEngine(final EngineOptions pEngineOptions) {
        return new LimitedFPSEngine(pEngineOptions, 60);
 }

 @Override
 public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) throws Exception {
  
        try {
            this.mTexture = new BitmapTexture(this.getTextureManager(), new IInputStreamOpener() {
    
    @Override
    public InputStream open() throws IOException {
     return getAssets().open("gfx/sw_12_23.jpg");
    }
   });
            
            this.mTextureRegion = TextureRegionFactory.extractFromTexture(mTexture);
            this.mTexture.load();
        } catch (IOException e) {
        }
        
        this.getShaderProgramManager().loadShaderProgram(GaussianBlurPass1ShaderProgram.getInstance());

  pOnCreateResourcesCallback.onCreateResourcesFinished();
 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) throws Exception {        
   this.mEngine.registerUpdateHandler(new FPSLogger());

         final Scene scene = new Scene();
         
         scene.setBackground(new Background(0.09804f, 0.6274f, 0.8784f));
         
         getEngine().registerUpdateHandler(new FPSLogger());
         
         pOnCreateSceneCallback.onCreateSceneFinished(scene);
 }

 @Override
 public void onPopulateScene(Scene pScene,
   OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {
  
  this.mSprite = new Sprite(234f, 40f, this.mTextureRegion, getVertexBufferObjectManager()) {   
   @Override
   protected void onManagedUpdate(float pSecondsElapsed) {        
    this.setRotation(this.getRotation()+1);
    super.onManagedUpdate(pSecondsElapsed);
   }
  };
  
  Sprite sprite = new Sprite(0, 0, 200,200, this.mTextureRegion, getVertexBufferObjectManager());
    
  mSprite.setShaderProgram(GaussianBlurPass1ShaderProgram.getInstance());
  
  pScene.attachChild(mSprite);
  pScene.attachChild(sprite);
  
  pOnPopulateSceneCallback.onPopulateSceneFinished();
 }
 
 public static class GaussianBlurPass1ShaderProgram extends ShaderProgram {
  
  private static GaussianBlurPass1ShaderProgram instance;
  
  public static GaussianBlurPass1ShaderProgram getInstance() {
   if (instance == null) instance = new GaussianBlurPass1ShaderProgram();
   return instance;
  }
    
  public static final String FRAGMENTSHADER = 
    "precision lowp float;\n" +

             "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ";\n" +
             "varying mediump vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n" +

    "const float blurSize = 2.0/" + (WIDTH-1) + ".0; \n" +
 
    "void main() \n" +
    "{    \n" +
    " vec4 sum = vec4(0.0); \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x - 4.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.05; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x - 3.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.09; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x - 2.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.12; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x - blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.15; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.16; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x + blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.15; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x + 2.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.12; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x + 3.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.09; \n" +
    " sum += texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", vec2(" + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".x + 4.0*blurSize, " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ".y)) * 0.05; \n" +
    " gl_FragColor = sum; \n" +
    "}      \n";

  
  private GaussianBlurPass1ShaderProgram() {
   super(PositionTextureCoordinatesShaderProgram.VERTEXSHADER, FRAGMENTSHADER);
  }
  
  
  
        public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
        
        @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);

            GaussianBlurPass1ShaderProgram.sUniformModelViewPositionMatrixLocation = this.getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);
            GaussianBlurPass1ShaderProgram.sUniformTexture0Location = this.getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_0);

        }
        
        @Override
        public void bind(final GLState pGLState, final VertexBufferObjectAttributes pVertexBufferObjectAttributes) {
            GLES20.glDisableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);

            super.bind(pGLState, pVertexBufferObjectAttributes);
            
            GLES20.glUniformMatrix4fv(GaussianBlurPass1ShaderProgram.sUniformModelViewPositionMatrixLocation, 1, false, pGLState.getModelViewProjectionGLMatrix(), 0);
            GLES20.glUniform1i(GaussianBlurPass1ShaderProgram.sUniformTexture0Location, 0);
        }

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

            super.unbind(pGLState);
        }
 }
}

Ну вот, собственно и все. Подводя итог, можно сказать следующее: убрали лишнее и применили шейдер к конкретному спрайту методом: Sprite.setShaderProgram(...) в который и предаем нашу программу шейдера. Для наглядности, я добавил еще один спрайт на сцену с той же текстурой и задал вращение спрайту к которому применен шейдер. Надеюсь кому-то данная информация пригодится. Спасибо!


Продвинутый пример смотрите в статье "Немного о Render-to-Texture..."
Проект на GitHub

воскресенье, 9 декабря 2012 г.

ClientSocket под Android. Клиент для взаимодействия приложений через интернет.

Доброго всем! Решил поделиться своими наработками в области создания сетевых приложений под Андроид. Данное сообщение не относится непосредственно к AndEngine, но имеет прямое отношение к геймдеву, ведь каждому игроделу когда-нибудь приходит в голову идея организовать взаимодействие игроков через интернет (настольные игры, общение, пошаговые стратегии и прочее).
Сразу оговорюсь, что приведенный здесь класс не обеспечивает "прямого" взаимодействия пользователей. Между пользователями необходимо наличие веб-сервера на котором должна быть реализована логика аутентификации, игровая логика и который будет координировать команды и события от клиентов.
В процессе портирования одной популярной настольной игры под Android, мы решили прикрутить к ней возможность игры через интернет.
Задача стояла следующая: получить инструмент, позволяющий легко реализовывать пошаговый протокол игры, имеющий события подключения, отключения, перехват ошибок,  ну и конечно отправки/приема данных. Я намеренно пишу "получить", потому что не являюсь хардкорным программером и если нахожу хорошее опенсурс-решение со свободной лицензией, то ничуть не стесняюсь его использовать. Такого же мнения придерживаюсь и для своего кода в этом блоге: если кому-то что-то глянется - берите не стесняйтесь. =)
Итак, начали мы с активных поисков готового кода. Через пару дней скитаний по интернету стало понятно, что устойчивого и законченного клиента для WEB для нас никто не выложил =). Конечно, нашлось немало статей по созданию таких клиентов, но все они были настолько печальны, что их юзабельность сводилась к нулю.
Я не могу привести здесь все источники из которых мы брали информацию, во-первых их очень много, а во-вторых ни один из них не стал для нас отправной точкой, поэтому, без лишних слов привожу здесь код который у нас получился из собранной инфы и собственных умозаключений.

package com.expedition107.mytreasures;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

import android.os.Handler;
import android.util.Log;

public class ClientSocket {

 private Handler handler = new Handler();

 private static String mHostIpAddress;
 private static int mPort;

 private Thread mServiceThread;
 private Runnable mServiceRunnable;

 private Socket mSocket;

 private volatile boolean isConnected = false;
 private int mSocketStatus = 0;// 0-disconnected, 1- connection, 2-
         // io_pending, 3 - write out, 4 - read in, -1 - unknown state
 private volatile String mOutString = "";
 
 private volatile boolean mIsNewData;

 public ClientSocket(String pHostIp, int pPort) {
  mHostIpAddress = pHostIp;
  mPort = pPort;

 }

 public void connect(final int pTimeoutMillisecs) {
  if (mSocketStatus != 0) {
   return;
  }
  mServiceRunnable = new Runnable() {
   @Override
   public void run() {
    mSocketStatus = 1; //соединяемся
    // connect
    try {
     mSocket = new Socket();
     mSocket.connect(new InetSocketAddress(mHostIpAddress,mPort), pTimeoutMillisecs);
     mSocket.setKeepAlive(true);
     setConnected(true);
     onConnect();
     mSocketStatus = 2;//уходим в прослушку сокета
     char[] inBuff = new char[1024];
     while (isConnected()) {
      String tmpSendData = getSendData(); 
      if (!tmpSendData.equals("")) {
       mSocketStatus = 3;//пишем в сокет
       PrintWriter out = new PrintWriter(
         new BufferedWriter(new OutputStreamWriter(
           mSocket.getOutputStream())), true);
       
       out.println(tmpSendData);
       handler.post(new Runnable() {
        @Override
        public void run() {
         onSendData();
        }
       });
       
       clearSendData(); 
      }
      
      mSocketStatus = 4;//читаем из сокета
      BufferedReader in = new BufferedReader(
        new InputStreamReader(mSocket.getInputStream()), 1024);
      String line = "";
      if (in.ready()) {      
       int length = in.read(inBuff);
       while ((length) != -1) {
        for (int i = 0; i < length; i++) {
         line = line + inBuff[i];
        }
        length = -1;
        if (in.ready()){
         length = in.read(inBuff);
        }
       } 
       final String inStr = line;
       handler.post(new Runnable() {
        @Override
        public void run() {
         onReadData(inStr);
        }
       });
       
      }
     }
     mSocketStatus = 0;//отсоединяемся
     mSocket.close();
     handler.post(new Runnable() {
      @Override
      public void run() {
       onDisconnect();
      }
     });
     

    } catch (UnknownHostException e) {
     final UnknownHostException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       mSocketStatus = -1;
       onSocketError(mSocketStatus,_e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    } catch (SocketException e) {
     final SocketException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       onSocketError(mSocketStatus, _e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    } catch (IOException e) {
     final IOException _e = e;
     handler.post(new Runnable() {
      @Override
      public void run() {
       onSocketError(mSocketStatus, _e);
       mSocketStatus = 0;
      }
     });
     e.printStackTrace();
    }
   }
  };
  mServiceThread = new Thread(mServiceRunnable);
  mServiceThread.start();
 }
 
 //эти методы вызываются из UI-потока
 public void disconnect() {
  setConnected(false);
 }


 public void onConnect() {
  
 }

 public void onDisconnect() {
  
  setConnected(false);
 }

 public void onSocketError(int pSocketStatus, Exception e) {
  
 }

 public void onReadData(String pDataString) {
  
 }

 public void onSendData() {
  
 }

 //синхронизированные методы чтения/записи
 protected synchronized boolean isConnected(){
  return this.isConnected;
 }
 
 private synchronized boolean setConnected(boolean pIsConnected){
  this.isConnected = pIsConnected;
  return this.isConnected;
 }
 //этот метод вызывается из UI-потока
 protected synchronized void send(String pOutString){
  mOutString = pOutString;
  mIsNewData = true;
 }
 
 private synchronized String getSendData(){
  mIsNewData = false;
  return mOutString;
 }
 
 private synchronized void clearSendData(){
  if (!mIsNewData)
   mOutString = "";
 }
}

И, как обычно, немного пояснений.
Работа сокета по чтению и записи данных должна происходить в отдельном потоке и ни в коем случае не в потоке UI., именно поэтому, чтобы получить доступ из UI к данным (а ведь это нам и нужно) которые приняты/переданы мы создаем Handler и работаем через него.
Синхронизированные методы нужны для синхронизации доступа к volatile переменным, т.е. переменным которые могут использоваться и потоком UI и потоком сокета.
В коде фигурирует переменная mSocketStatus использование которой обоснованно только нашими способами отладки =).

Использование класса заключается в перекрытии его методов прямо в UI потоке:

Чтение данных и обработка ошибок:
final ClientSocket cs = new ClientSocket(serverIpAddress, 7777){
         @Override
         public void onConnect(){
             //Например показать Toast уведомление
           
         }
         @Override
         public void onDisconnect(){
             //обязательно вызвать метод супера
             //иначе разрыва связи не произойдет
                    super.onDisconnect();
             //тут ваш код
           
         }
         @Override
         public void onReadData(String pStr){
             //В pStr хранятся принятые данные
                    textView.setText(textView.getText()+pStr+"\n");
           
         }
         
         @Override
         public void onSocketError(int pSocketStatus, Exception e) {
          Log.v("game", e.getMessage());
         }
        };

Подключение к серверу:
...
cs.connect(5000);
...  

Отправка данных:
...
cs.send("СТРОКА ДАННЫХ НА ОТПРАВКУ");
...

Отключение от сервера:
...
cs.disconnect();
...  

Отсюда видно, что с этим классом не надо заморачиваться с работой сокетов, а сразу приступить к реализации своего протокола, а при наличии хорошего стабильного соединения - не только пошагового. Здесь имеется возможность контролировать момент подключения/отключения к серверу, ошибки сокета, отправку и прием данных.
Протокол для своей игрушки мы реализовали на базе XML. Советую использовать вот этот класс для парсинга, очень помогает не запутаться в ветках.
PS Не воспринимайте данный класс как панацею, хотя он и вполне работоспособен, небольшие сомнения все-таки вызывает отправка/очистка данных и если у кого-то появится желание по усовершенствованию данного класса мы будем рады совету или подсказке. Изменения я постараюсь сразу же внести.
Спасибо за внимание!

воскресенье, 28 октября 2012 г.

Moonlight Spirits Pro. Кое-что новенькое в мире LWP.

Всем привет!
Чтобы как-то себя расшевелить на написание новых статей по программированию, решил написать сообщение не содержащее ни кусочка кода. А все потому, что наша команда, наконец-то закончила еще один проект живых обоев под Android с названием Moonlight Spirits Pro.  =)

Скриншоты (видео смотрите внизу сообщения):

Проект был реализован примерно за один месяц и над проектом работали два человека, так что, отбросив скромность могу сказать,что работа была проделана не малая и, я бы даже сказал - очень большая. :) Движок для обоев все тотже: AndeEngine GLES2.

Что же из себя представляют наши обои, оправдано ли в название слово "Pro" и стоит ли о них вообще писать? Давайте посмотрим.

Темы.
Итак, обои выполнены в жанре ужасов, мистики и прочей чертовщины. В отличии от других обоев предлагаемых на маркете, Moonlight Spirits не является просто несколькими настраиваемыми картинками, ведь они включают в себя 7 разных тематических наборов, каждый из которых может быть отдельными приложениями. Тут вам и замок дракулы, и заснеженные горы, и Стоунхендж, и еще 4-е основных темы кардинально различающиеся друг от друга.
Небесные объекты.
Особое внимание следует уделить небесным объектам: луна, мерцающие звезды, плывущие облака и туман. На луне проявляются картины подобранные для каждой темы отдельно, например картины из темы Haunted House невозможно увидеть в теме  Dust of  Time. Кроме того, луна умеет подниматься и садиться за горизонт, при этом цветовая гамма меняется соответствующим образом и все выглядит очень естественно и главное красиво. Всего сделано 4-е режима восхода и захода луны: всегда ночь, всегда день, день/ночь автоматически и день/ночь по тапу. Ну и конечно, все небесные объекты имеют опции включения/отключения.
Лунные образы.
Лунные образы это те самые картины которые нам показывает луна. Повторюсь, что наборы образов тематические и индивидуальны для каждой темы. Сменяются они случайным образом.  Из можно отключить или изменить скорость показа.
Облака.
На переднем плане плывут облака. Это даже скорее не облака, а больше похоже на туман который добавляет еще больше мистичности. Всего в обоях предусмотрено два слоя тумана которыми можно манипулировать настраивая скорость движения и включая/отключая слои.
Цветовые схемы.
К каждой теме может быть применена цветовая схема. На текущий момент 11 схем и каждая из них индивидуальна. Цветовая схема по сути представляет собой набор цветов для дневного и ночного режима для каждого элемента текущей темы. Использование цветовых схем - это один из значительных шагов в сторону реальной персонализации смартфона.
Создания.
Moonlight Spirits живут своей жизнью. Для каждой из тем предусмотрены свои наборы созданий которые периодически появляются на экране (летучие мыши, совы, вороны и даже есть орлы с химерами). Кроме того присутствует настройка по выбору созданий при взаимодействии пользователя с экраном смартфона и их количества. Опция Background Life включает/отключает случайно появляющихся созданий.
Интерактивность.
Ну какие обои на сегодняшний день не обладают возможностью взаимодействия с пользователем? Вот и Moonlight Spirits предоставляет такую опцию. Взаимодействие с пользователем проявляется в двух событиях: на тап по экрану и на сдвиг экрана. В первом случае обои порождают созданий в зависимости от выбранной настройки,а во втором случае выполняется параллакс-эффект:  основные элементы композиции сдвигаются относительно друг друга и выглядит это действительно здорово.
Звук.
При касании экрана раздаются звуки специально подобранные в жанре ужасов. Можно выбрать конкретный звук на нажатие или поставить случайный звук.
Psycho.
В попытке еще больше расширить возможность персонализации, мы придумали цветовой фильтр который собственно и назвали Psycho. При установке этого фильтра, вся текущая цветовая схема просто выворачивается наизнанку и как будут выглядеть обои после применения этого фильтра фактически неизвестно, НО цвета получаются настолько неправдоподобными и сумасшедшими, что просто не могут не понравиться  =) .
Quake at touch.
Интересная опция которая позволяет шевелиться всей основной композиции при прикосновению к экрану. Получается что-то вроде волнения местнности и мне особенно нравиться этот эффект на готических развалинах  и на на теме Стоунхендж.
Прочее.
Обои поддерживают портретный и ландшафтный режимы. Переносятся на SD, и имеют вес менее 4 Мб! Это действительно здорово, при том что уровень графики высок и очень разнообразен.

Вот такие вот обои у нас получились, а вам судить на сколько хорошо у нас это вышло. Спасибо! Маркет: https://play.google.com/store/apps/details?id=com.expedition107.wp.halloween.hd


понедельник, 30 июля 2012 г.

Пример шейдера на Android. Metablob.

Здесь я выложу код модифицированного шейдера под названием Metablob (автор Adrian Boeing). Исходник взят от сюда Shader Toy.
Как, что и почему я тут расписывать не буду. Более детальное прикручивание шейдера к Android можно посмотреть в моем сообщении "Пример шейдера на Android. Ripple Effect.". Здесь будет интересен уже результат шейдера.

Выглядит он *примерно* так, плюс оно шевелиться всячески =):

Изменению подверглись количество и радиусы сгустков (свой! уникальный! аррргххх....=( ).
А вот и код активити:

package com.expedition107.shader.test;
import java.io.IOException;
import java.io.InputStream;

import org.andengine.engine.Engine;
import org.andengine.engine.LimitedFPSEngine;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.scene.IOnAreaTouchListener;
import org.andengine.entity.scene.IOnSceneTouchListener;
import org.andengine.entity.scene.ITouchArea;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.sprite.Sprite;
import org.andengine.entity.sprite.UncoloredSprite;
import org.andengine.entity.util.FPSLogger;
import org.andengine.input.touch.TouchEvent;
import org.andengine.input.touch.detector.ClickDetector;
import org.andengine.input.touch.detector.ClickDetector.IClickDetectorListener;
import org.andengine.opengl.shader.PositionTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.shader.exception.ShaderProgramException;
import org.andengine.opengl.shader.exception.ShaderProgramLinkException;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.opengl.texture.render.RenderTexture;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;
import org.andengine.ui.activity.BaseGameActivity;
import org.andengine.util.adt.io.in.IInputStreamOpener;
import org.andengine.util.color.Color;

import com.expedition107.shader.test.ShockwaveTest.ShockwaveShaderProgram;

import android.opengl.GLES20;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;

public class MetablobTest extends BaseGameActivity implements IOnSceneTouchListener {
 
 public static int WIDTH = 800;
 public static int HEIGHT = 480;
 
 protected static MetablobTest Instance;
 private Camera mCamera;
 private Sprite mSprite;
        private ITexture mTexture;
        private ITextureRegion mTextureRegion;
    
 private boolean mRenderTextureInitialized = false;
 private RenderTexture mRenderTexture;
 private Sprite mRenderTextureSprite;
 
 private float mShockwaveTime = 0f;
 private float mCenterX = 0.5f;
 private float mCenterY= 0.5f;
 
 @Override
 public EngineOptions onCreateEngineOptions() {
  
  DisplayMetrics displayMetrics = new DisplayMetrics();
  WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
  wm.getDefaultDisplay().getMetrics(displayMetrics);
  wm.getDefaultDisplay().getRotation();
  WIDTH = displayMetrics.widthPixels;
  HEIGHT = displayMetrics.heightPixels;
  
  mCamera = new Camera(0, 0, WIDTH, HEIGHT);
  
  final EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new RatioResolutionPolicy(WIDTH, HEIGHT), this.mCamera);

  return engineOptions;
 }
 
 @Override
    public Engine onCreateEngine(final EngineOptions pEngineOptions) {
        return new LimitedFPSEngine(pEngineOptions, 120) {
         
         
         @Override
         public void onDrawFrame(GLState pGLState)
           throws InterruptedException {
    
     if (!mRenderTextureInitialized) {
      initRenderTexture(pGLState);
      mRenderTextureInitialized = true;
     }
     
     mRenderTexture.begin(pGLState, false, true, Color.TRANSPARENT);
     {  
      super.onDrawFrame(pGLState);
     }
     mRenderTexture.end(pGLState);
         
     pGLState.pushProjectionGLMatrix();
     pGLState.orthoProjectionGLMatrixf(0, mCamera.getSurfaceWidth(), 0, mCamera.getSurfaceHeight(), -1, 1);
     {
      mRenderTextureSprite.onDraw(pGLState, mCamera);
     }
     pGLState.popProjectionGLMatrix(); 

         }
         
   private void initRenderTexture(GLState pGLState) {
    mRenderTexture = new RenderTexture(this.getTextureManager(),mCamera.getSurfaceWidth(), mCamera.getSurfaceHeight(), PixelFormat.RGBA_8888);
    mRenderTexture.init(pGLState);
    mRenderTextureSprite = new UncoloredSprite(0f, 0f, TextureRegionFactory.extractFromTexture(mRenderTexture), getVertexBufferObjectManager());
    
    mRenderTextureSprite.setShaderProgram(ShockwaveShaderProgram.getInstance());    
   }
        };
 }

 @Override
 public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) throws Exception {

 Instance = this;

        try {
            this.mTexture = new BitmapTexture(this.getTextureManager(), new IInputStreamOpener() {
    
    @Override
    public InputStream open() throws IOException {
     // TODO Auto-generated method stub
     return getAssets().open("gfx/sw_12_23.jpg");
    }
   });
            
            
            this.mTextureRegion = TextureRegionFactory.extractFromTexture(mTexture);
            this.mTexture.load();
        } catch (IOException e) {
        }
        
        this.getShaderProgramManager().loadShaderProgram(ShockwaveShaderProgram.getInstance());

  pOnCreateResourcesCallback.onCreateResourcesFinished();
 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) throws Exception {        
   this.mEngine.registerUpdateHandler(new FPSLogger());

         final Scene scene = new Scene();
         
         scene.setBackground(new Background(0.09804f, 0.6274f, 0.8784f));
         
         this.mSprite = new Sprite(0f, 0f,40,40, this.mTextureRegion, getVertexBufferObjectManager());
   scene.attachChild(mSprite);
   
   getEngine().registerUpdateHandler(new IUpdateHandler() {
    @Override
    public void reset() {
    }   
    @Override
    public void onUpdate(float pSecondsElapsed) {
     mShockwaveTime += pSecondsElapsed*0.5;
    }
   });
         scene.setOnSceneTouchListener(this);         
         pOnCreateSceneCallback.onCreateSceneFinished(scene);
 }

 @Override
 public void onPopulateScene(Scene pScene,
   OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {
  pOnPopulateSceneCallback.onPopulateSceneFinished();
 }
 
 public static class ShockwaveShaderProgram extends ShaderProgram {
  
  private static ShockwaveShaderProgram instance;
  
  public static ShockwaveShaderProgram getInstance() {
   if (instance == null) instance = new ShockwaveShaderProgram();
   return instance;
  }
    
  public static final String FRAGMENTSHADER = 
  "precision highp float;\n" +
        
        "uniform float time;\n" +
        "uniform vec2 resolution;\n" +

  "void main() \n" +
  "{\n" +
  //центры сгустков
  " vec2 move1;\n" +
  " move1.x = cos(time)*0.4;\n" +
  " move1.y = sin(time*1.5)*0.4;\n" +
  " vec2 move2;\n" +  
  " move2.x = cos(time*2.0)*0.6;\n" +
  " move2.y = sin(time*3.0)*0.6;\n" +
  " vec2 move3;\n" +  
  " move3.x = sin(time*2.0)*0.8;\n" +
  " move3.y = cos(time*3.0)*0.8;\n" +
  " vec2 move4;\n" +  
  " move4.x = cos(time*1.8)*0.2;\n" +
  " move4.y = sin(time*3.0)*0.2;\n" +
  " vec2 move5;\n" +  
  " move5.x = cos(time*4.8)*1.2;\n" +
  " move5.y = sin(time*1.0)*0.5;\n" +
  " vec2 move6;\n" +  
  " move6.x = sin(time*1.8)*0.2;\n" +
  " move6.y = cos(time*1.1)*0.2;\n" +
  " vec2 move7;\n" +  
  " move7.x = cos(time*1.8)*0.2;\n" +
  " move7.y = sin(time*3.0)*0.2;\n" +
  " vec2 move8;\n" +  
  " move8.x = sin(time*0.8)*0.2;\n" +
  " move8.y = cos(time*0.3)*0.5;\n" +
  " vec2 move9;\n" +  
  " move9.x = cos(time*1.8)*0.9;\n" +
  " move9.y = sin(time)*5.4;\n" +
  //координаты
  " vec2 p = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy;\n" +
  //радиусы
  " float r1 =(dot(p-move1,p-move1))*16.0;\n" +
  " float r2 =(dot(p+move2,p+move2))*24.0;\n" +
  " float r3 =(dot(p+move3,p+move3))*32.0;\n" +
  " float r4 =(dot(p+move4,p+move4))*40.0;\n" +
  " float r5 =(dot(p+move5,p+move5))*42.0;\n" +
  " float r6 =(dot(p+move6,p+move6))*50.0;\n" +
  " float r7 =(dot(p+move7,p+move7))*56.0;\n" +
  " float r8 =(dot(p+move8,p+move8))*65.0;\n" +
  " float r9 =(dot(p+move9,p+move9))*70.0;\n" +
  //сумма сгустков
  " float metaball =(1.0/r1-1.0/r2+1.0/r3-1.0/r4+1.0/r5-1.0/r6+1.0/r7-1.0/r8+1.0/r9);\n" +
  //места разрыва сгустков
  " float col = pow(metaball,8.0);\n" +
  //вывод конечного цвета фрагмента
  " gl_FragColor = vec4(col+move2.x,col+move1.x,col+move4.y,0.8);\n" +
  "}\n";

  
  private ShockwaveShaderProgram() {
   super(PositionTextureCoordinatesShaderProgram.VERTEXSHADER, FRAGMENTSHADER);
  }
  
        public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformTimeLocation = ShaderProgramConstants.LOCATION_INVALID;
        public static int sUniformResolutionLocation = ShaderProgramConstants.LOCATION_INVALID;
        
        @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);

            ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation = this.getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);     
            ShockwaveShaderProgram.sUniformResolutionLocation = this.getUniformLocation("resolution");
            ShockwaveShaderProgram.sUniformTimeLocation = this.getUniformLocation("time");
        }
        
        @Override
        public void bind(final GLState pGLState, final VertexBufferObjectAttributes pVertexBufferObjectAttributes) {
            GLES20.glDisableVertexAttribArray(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION);

            super.bind(pGLState, pVertexBufferObjectAttributes);
            
            GLES20.glUniformMatrix4fv(ShockwaveShaderProgram.sUniformModelViewPositionMatrixLocation, 1, false, pGLState.getModelViewProjectionGLMatrix(), 0);
            GLES20.glUniform2f(ShockwaveShaderProgram.sUniformResolutionLocation, WIDTH, HEIGHT);
            GLES20.glUniform1f(ShockwaveShaderProgram.sUniformTimeLocation, MetablobTest.Instance.mShockwaveTime);
        }

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

            super.unbind(pGLState);
        }
 }


 @Override
 public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
  if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_DOWN){
  mCenterX = pSceneTouchEvent.getMotionEvent().getX() / this.mCamera.getSurfaceWidth();
  mCenterY = 1-pSceneTouchEvent.getMotionEvent().getY() / this.mCamera.getSurfaceHeight();
  return true;
  }
  return false;
 } 
}

Копируйте, пользуйтесь, не забывайте про Adrian-а Boeing-а! :-)


Продвинутый пример смотрите в статье "Немного о Render-to-Texture..."
Скачать исходник с апк внутри.