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

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

С недавнего времени заинтересовался шейдерами под Android используя в качестве движка AndEngine.
Копаясь в данной теме выяснил кое-что, но явно недостаточное для написания собственного шейдера. Тогда я решил пойти проторенной дорожкой - найти готовые шейдера и попробовать прикрутить их к своему аппу. Вот здесь находиться библиотека шейдеров часть которых мне удалось портировать под Android. Кроме того, для понимания процесса, я рекомендую статью на форуме AndEngine, кое-какие материалы я взял оттуда.

Вот, на мой взгляд, интересный шейдер под названием Pulse ('Pulse' by Danguafer/Silexars (2010)). По сути он представляет собой Ripple Effect (все любят эффект воды ёкарный бабаааай) с постоянно движущимся центром источника волн. 
Вот как *примерно* он будет выглядеть:

Попробуем воссоздать его для Android.

Коротко о том, что нам предстоит сделать:
1. Создать специальную текстуру, которая будет содержать проекцию обычной текстуры.
2. Создать специальный спрайт, который будет отрисовывать измененное шейдером изображение.
3. Создать текстуру с изображением из файла.
4. Создать на ее основе спрайт.
5. Написать программу шейдера.
6. Все это дело связать между собой и задать значения uniform-переменных.

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

Приступим:

package com.expedition107.shadershock;

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.modifier.LoopEntityModifier;
import org.andengine.entity.modifier.RotationByModifier;
import org.andengine.entity.scene.IOnSceneTouchListener;
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.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 android.opengl.GLES20;
import android.util.Log;

public class ShockwaveTest extends BaseGameActivity implements
  IOnSceneTouchListener {

 public static final int WIDTH = 800;

 public static final int HEIGHT = 480;

 protected static ShockwaveTest 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;

 @Override
 public EngineOptions onCreateEngineOptions() {

  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();

   }

   // метод инициализации RenderTexture

   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() {
      public InputStream open() throws IOException {
       return getAssets().open("gfx/tex2.jpg");
      }
     });

   mTexture.load();

   this.mTextureRegion = TextureRegionFactory
     .extractFromTexture(mTexture);

  } catch (IOException e) {

  }

  this.getShaderProgramManager().loadShaderProgram(
    ShockwaveShaderProgram.getInstance());

  pOnCreateResourcesCallback.onCreateResourcesFinished();

 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback)
   throws Exception {
  // Создаем главную сцену
  final Scene scene = new Scene();
  // Задаем цвет бэкграунда
  scene.setBackground(new Background(0.09804f, 0.6274f, 0.8784f));
  // Регистрируем FPSLogger чтобы оценить fps во время исполнения
  getEngine().registerUpdateHandler(new FPSLogger());
  // Назначением слушателя прикосновения пальца к сцене
  scene.setOnSceneTouchListener(this);

  pOnCreateSceneCallback.onCreateSceneFinished(scene);

 }

 @Override
 public void onPopulateScene(Scene pScene,
 OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {

  // Создаем спрайт и растягиваем его на всю ширину и высоту сцены (можно
  // и не растягивать)
  this.mSprite = new Sprite(0f, 0f, 800, 480, this.mTextureRegion,
    getVertexBufferObjectManager());
  // Добавляем спрайт на сцену (иначе не увидим его)
  pScene.attachChild(mSprite);
  // Регистрируем хендлер обновления чтобы менять нашу переменную времени
  getEngine().registerUpdateHandler(new IUpdateHandler() {
   public void reset() {
    // TODO Auto-generated method stub
   }

   public void onUpdate(float pSecondsElapsed) {
    mShockwaveTime += pSecondsElapsed * 0.5;
   }
  });

  pOnPopulateSceneCallback.onPopulateSceneFinished();

 }

 // Класс программы шейдера

 public static class ShockwaveShaderProgram extends ShaderProgram {

  // Указатель на созданный экземпляр программы
  private static ShockwaveShaderProgram instance;
  // Метод возвращающий указаетль на созданный экземпляр
  public static ShockwaveShaderProgram getInstance() {
   if (instance == null)
    instance = new ShockwaveShaderProgram();
   return instance;
  }

  // Текст вершинного шейдера здесь отсутствует т.к. используется шейдер
  // из библиотеки AndEngine

  // Текст фрагментного шейдера

  public static final String FRAGMENTSHADER =
  "precision highp float;\n" +
  "uniform float time;\n" +
  "uniform vec2 resolution;\n" +
  "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ";\n" +
  "varying mediump vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n" +
  "void main(void)\n" +
  "{\n" +
  "vec2 halfres = resolution.xy/2.0;\n" +
  "vec2 cPos = gl_FragCoord.xy;\n" +
  "cPos.x -= 0.5*halfres.x*sin(time/2.0)+0.3*halfres.x*cos(time)+halfres.x;\n" +
  "cPos.y -= 0.4*halfres.y*sin(time/5.0)+0.3*halfres.y*cos(time)+halfres.y;\n" +
  "float cLength = length(cPos);\n" +
  "vec2 uv = gl_FragCoord.xy/resolution.xy+(cPos/cLength)*sin(cLength/30.0-time*10.0)/25.0;\n" +
     "vec3 col = texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ",uv).xyz;\n" +
  "gl_FragColor = vec4(col,1.0);\n" +
  "}\n";

  // Конструктор программы шейдера

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

  // Константы для связывания внешних переменных с uniform-переменными
  // шейдеров

  public static int sUniformModelViewPositionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformTimeLocation = ShaderProgramConstants.LOCATION_INVALID;
  public static int sUniformResolution = 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.sUniformTexture0Location = this
     .getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_0);
   // Линковка переменной resolution
   ShockwaveShaderProgram.sUniformResolution = this
     .getUniformLocation("resolution");
   // Линковка переменной time
   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.glUniform1i(ShockwaveShaderProgram.sUniformTexture0Location,0);

   GLES20.glUniform1f(ShockwaveShaderProgram.sUniformTimeLocation,
     ShockwaveTest.Instance.mShockwaveTime);

   GLES20.glUniform2f(ShockwaveShaderProgram.sUniformResolution,
     ShockwaveTest.Instance.mCamera.getSurfaceWidth(),
     ShockwaveTest.Instance.mCamera.getSurfaceHeight());

  }

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

 // здесь при касании переменная mShockwaveTime сбрасывается в 0, что
 // незамедлительно передается
 // переменной time внутри шейдера и мы это увидим прикоснувшись к сцене

 @Override
 public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
  if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_DOWN) {
   mShockwaveTime = 0;
  }
  return false;
 }
}

Несколько замечаний по коду:
1. Текстура, содержащая картинку, в этом примере грузиться немного непривычным для меня способом. Мне привычнее делать так:

final BitmapTextureAtlas texture = new BitmapTextureAtlas(
    getTextureManager(), 800, 400, TextureOptions.NEAREST);
mRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(
    textureMain, this, "gfx/kartinko.png", 0, 0);

Но в данном конкретном случае подход оправдан, так как для быстрой проверки, не надо будет указывать размеры текстуры в зависимости от размеров картинки в файле.
2. Получившийся у нас эффект от шейдера не будет выглядеть так как на картинке выше, она сделана скриншотом с WebGL и надо понимать, что видео-карта компьютера немножко получше чем карта смартфона.
3. Не забудьте бросить в assets/gfx какой-нибудь файл с картинкой (у меня tex1.jpg).
4. Обратите внимание, что программа шейдера в Java-коде по сути представляет собой строку, т.е. вы можете наделать разных файлов с текстами шейдеров и подгружать их стандартными методами Java по мере необходимости.
5. В AndEngine для описания шейдеров используется язык GLSL.
6. Поиграйте с кодом шейдера. Попробуйте добавить свои uniform-переменные (например, при касании сцены задавать начальную точку движения пульса) и связать их с переменными в своей программе.

Ну что же, вот такой вот пример получился.
Кому надо, можете спокойно копировать код и использовать как вам потребуется, но помните: шейдер хоть и изменен, но все же у него есть автор (приведен выше).  Полностью проект выкладывать не буду, потому как предполагается, что у вас уже стоит Eclipse, есть свой workspace и подключена библиотека AndEngine GLES2. В следующем сообщении распишу еще один немного модифицированный шейдер из той же библиотеки, который называется Metablob. Очень симпатишный. :)  Всем удачи!

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

5 комментариев:

  1. Не могу запустить пример. Что находится в классе CopyOfShockwaveTest?

    ОтветитьУдалить
  2. Спасибо за замечание. Поправил.

    ОтветитьУдалить
  3. Алексей ещё пара вопросов.
    1. Шейдер работает на всей сцене, а как сделать , чтобы он работал только на одном спрайте и не выходил за его границы?
    2. Как я понял из метода initRenderTexture() для связывания в шейдер передаётся текстура с индексом 0, как передать дополнительные текстуры?

    ОтветитьУдалить
    Ответы
    1. Максим, по поводу первого вопроса посмотри вот это сообщение http://expedition107.blogspot.ru/2013/01/andengine-blur.html =)
      По поводу второго вопроса пока не могу ничего сказать. На неделе попытаюсь портировать шейдер FrostedGlass или NightVision. Если все получится - нарисую статейку.

      Удалить