С недавнего времени заинтересовался шейдерами под Android используя в качестве движка AndEngine.
Копаясь в данной теме выяснил кое-что, но явно недостаточное для написания собственного шейдера. Тогда я решил пойти проторенной дорожкой - найти готовые шейдера и попробовать прикрутить их к своему аппу. Вот здесь находиться библиотека шейдеров часть которых мне удалось портировать под Android. Кроме того, для понимания процесса, я рекомендую статью на форуме AndEngine, кое-какие материалы я взял оттуда.
Вот, на мой взгляд, интересный шейдер под названием Pulse ('Pulse' by Danguafer/Silexars (2010)). По сути он представляет собой Ripple Effect (все любят эффект воды ёкарный бабаааай) с постоянно движущимся центром источника волн.
Вот как *примерно* он будет выглядеть:
Попробуем воссоздать его для Android.
Коротко о том, что нам предстоит сделать:
1. Создать специальную текстуру, которая будет содержать проекцию обычной текстуры.
2. Создать специальный спрайт, который будет отрисовывать измененное шейдером изображение.
3. Создать текстуру с изображением из файла.
4. Создать на ее основе спрайт.
5. Написать программу шейдера.
6. Все это дело связать между собой и задать значения uniform-переменных.
Сразу оговорюсь, что шейдер в этом примере будет действовать на всю поверхность сцены т.е., вы можете добавить на сцену кучу спрайтов, но шейдер будет работать для всей сцены а не для каждого спрайта по отдельности. Как прицепить шейдер к конкретному спрайту я возможно расскажу в следующих сообщениях.
Приступим:
Копаясь в данной теме выяснил кое-что, но явно недостаточное для написания собственного шейдера. Тогда я решил пойти проторенной дорожкой - найти готовые шейдера и попробовать прикрутить их к своему аппу. Вот здесь находиться библиотека шейдеров часть которых мне удалось портировать под 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

Не могу запустить пример. Что находится в классе CopyOfShockwaveTest?
ОтветитьУдалитьВопрос снят. С переменными разобрался.
УдалитьСпасибо за замечание. Поправил.
ОтветитьУдалитьАлексей ещё пара вопросов.
ОтветитьУдалить1. Шейдер работает на всей сцене, а как сделать , чтобы он работал только на одном спрайте и не выходил за его границы?
2. Как я понял из метода initRenderTexture() для связывания в шейдер передаётся текстура с индексом 0, как передать дополнительные текстуры?
Максим, по поводу первого вопроса посмотри вот это сообщение http://expedition107.blogspot.ru/2013/01/andengine-blur.html =)
УдалитьПо поводу второго вопроса пока не могу ничего сказать. На неделе попытаюсь портировать шейдер FrostedGlass или NightVision. Если все получится - нарисую статейку.