С недавнего времени заинтересовался шейдерами под 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. Если все получится - нарисую статейку.