четверг, 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..."
Скачать исходник с апк внутри.

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

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

Отображение графиков на Android. AChartEngine. AsyncTask.

Пару недель назад возникла необходимость в отображении данных в виде графика. Покопавшись в интернетах и перебрав несколько вариантов, обратил внимание на библиотеку AChartEngine. В библиотеке реализован огромный потенциал по построению диаграмм и трендов, и самое главное - ее использование оказалось интуитивно понятным даже для такого новичка в Java как я.
Вместе с библиотекой идет хороший пример с демонстрацией возможностей библиотеки и, судя по отзывам на stackoverflow, она является очень популярной среди разработчиков.

Цель данного сообщения - показать обрубленную версию официального примера для более легкого понимания процесса вывода тренда, пример использования AsyncTask (хорошие статьи по использованию AsyncTask и загрузку данных из интернет можно прочитать на habrahabr.ru.),  для получения данных с WEB, а также прикрепления AChartEngine-а к реальной задаче.

Процесс подключения библиотеки achartengine-1.0.0.jar стандартный, рассказывать тут нечего. Единственное на что стоит обратить внимание - после подключения библиотеки убедитесь чтобы в Project Properties->Java Build Path-> Order&Export эта библиотека стояла самой первой с установленной галочкой, иначе получите ClassNotFoundException.

Итак, что мне было нужно от либы: построить график изменяющегося во времени физ.параметра (дискретность данных непостоянна, т.е. временные интервалы между значениями почти всегда разные), данные физ.параметра я получаю с WEB-сервера посредством http-запроса к php-скрипту.

Общий механизм получения данных:
I. В асинхронной задаче выполняем запрос к серверу посредством передачи параметров запроса php-скрипту.
II. php-скрипт соединяется с СУБД и выполняет хранимую процедуру.
III.Получаем данные для графика
IV. Парсим полученную строку и заполняем серию для нашего чарта.

Так, шаг за шагом и пойдем:
php-скрипт, в моем случае, принимает три параметра:
1 - идентификатор параметра (tag_id);
2 - дата/время начала временного промежутка
3 - дата/время окончания временного промежутка

Запрос к серверу может выглядеть так:
http://www.my_super_mega_web_server.ru/android/?start=2011-11-06%201:15:00&end=2011-11-07%200:00:00&tag=2116 (это нерабочая ссылка ;-) )

Все три параметра будут меняться в зависимости от того, что хочет посмотреть пользователь, поэтому нам необходимо создать функцию которая динамически собирает такой URL.

php-скрипт возвращает нам данные вот в таком виде (кому интересно - это температура воздуха на улице):

06.11.11 01:19     -11.46
06.11.11 01:31     -11.31
06.11.11 01:33     -11.46
06.11.11 01:47     -11.61
06.11.11 01:49     -11.76
06.11.11 02:05     -11.91
06.11.11 02:18     -12.06
06.11.11 02:21     -12.21
Код скрипта здесь рассматривать не будем, это за рамками сообщения.

По сути - это обычные строки, разделенные на дату/время и значение знаком табуляции.
Предвижу комментарии типа "почему строки, почему не бинарные данные в виде datetime и float?" Отвечаю: потому, что скрипт писал не я, + это пилотный, так сказать, проект.

Запрос данных и построение графика будет происходить сразу при создании активити, поэтому необходимо учесть, что при смене ориентации экрана - активити пересоздастся и снова начнется загрузка данных. В моем случае нет нужды отображать график в портретном режиме, поэтому я фиксирую разворот в положении landscape.

Код класса наследованного от AsyncTask:

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringTokenizer;

import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;

public class DownloadTagDataAsyncTask extends
  AsyncTask<String, Void, String> {

 private HttpURLConnection mHttpConn;
 
 @Override
 protected String doInBackground(String... params) {
  String strURL = "";
  if (params.length != 0) {
   strURL = params[0];
  }
  return DownloadData(strURL);
 }

 @Override
 protected void onProgressUpdate(Void... values) {
  super.onProgressUpdate(values);
 }

 @Override
 protected void onPostExecute(String result) {
  super.onPostExecute(result);
  if (hasError){
   hasError = false;
   Toast.makeText(GraphActivity.Instance, "Ошибка соединения. Повторите попытку.", Toast.LENGTH_SHORT);
   GraphActivity.Instance.finish();
   return;
  }
  StringTokenizer allData = new StringTokenizer(result, "\r\n", false);
  int l = 0;
  while (allData.hasMoreTokens()) {

   String s = allData.nextToken();
   l++;
   if (s.equals(" ")) {
    break;
   }
  }
  
  float[] values = new float[l];
  Date[] times = new Date[l];
  
  allData = new StringTokenizer(result, "\r\n", false);
  int i = 0;
  while (allData.hasMoreTokens() && (i < l-1)) {
   String s = allData.nextToken();
   if (s.equals(" ")) {
    break;
   }
   StringTokenizer record = new StringTokenizer(s, "\u0009", false);
   if (record.hasMoreTokens()) {
    String timePoint = record.nextToken();
    String valData = record.nextToken();
    values[i] = Float.valueOf(valData);
    SimpleDateFormat sdf = new SimpleDateFormat("yy.dd.MM HH:mm");
    Date ddd = null;
    try {
    ddd = sdf.parse(timePoint);
    times[i] = new Date();
    times[i] = ddd;
    }catch(Exception e) {

    }
    
    
   }
   i++;
  }
  GraphActivity.Instance.getDateDemoDataset(values, times);
  GraphActivity.Instance.getDemoRenderer();
  GraphActivity.Instance.buildChart();
  GraphActivity.Instance.endProgress();
 }

 private InputStream OpenHttpConnection(String urlString) throws IOException {
  InputStream in = null;
  int response = -1;

  URL url = new URL(urlString);
  URLConnection conn = url.openConnection();
  mHttpConn = null;
  if (!(conn instanceof HttpURLConnection))
   throw new IOException("Error. HTTP connection failed.");
  try {
   mHttpConn = (HttpURLConnection) conn;
   mHttpConn.setAllowUserInteraction(false);
   mHttpConn.setInstanceFollowRedirects(true);
   mHttpConn.setRequestMethod("GET");
   mHttpConn.connect();
   response = mHttpConn.getResponseCode();
   if (response == HttpURLConnection.HTTP_OK) {
    in = mHttpConn.getInputStream();
   }
  } catch (Exception ex) {
   hasError = true;
   
   throw new IOException("Connection error.");
  }
  return in;
 }

 private String DownloadData(String URL) {
  int BUFFER_SIZE = 20000;
  InputStream in = null;
  String str = "";
  try {
   in = OpenHttpConnection(URL);
  } catch (IOException e1) {

   e1.printStackTrace();
   return str;
  }
  if (in == null) {
   return str;
  }
  InputStreamReader isr = null;
  try {
   isr = new InputStreamReader(in, "cp-1251");
  } catch (UnsupportedEncodingException e1) {
   e1.printStackTrace();
  }
  int charRead;

  char[] inputBuffer = new char[BUFFER_SIZE];
  try {
   while ((charRead = isr.read(inputBuffer)) > 0) {
    // ---convert the chars to a String---
    String readString = String
      .copyValueOf(inputBuffer, 0, charRead);
    str += readString;
    inputBuffer = new char[BUFFER_SIZE];
   }
   in.close();
  } catch (IOException e) {
   e.printStackTrace();
   return "";
  }

  if (mHttpConn != null) {
   mHttpConn.disconnect();
  }
  return str;
 }

}
GraphActivity.Instance - это ссылка на саму себя, чтобы из AsyncTask.onPostExecute(...) можно было обратиться к методам нашей активити.
Теперь код GraphActivity:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.model.TimeSeries;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ProgressBar;

public class GraphActivity extends Activity {

 private View mMainView;
 protected static GraphActivity Instance;
 private int mTag;
 private String mSeriesCaption = "Chart";

 private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset();
 private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer();
 private GraphicalView mChartView;
 
 private static ProgressDialog pd;

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  
  //Remove title bar
  this.requestWindowFeature(Window.FEATURE_NO_TITLE);

  //Remove notification bar
  this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
  
  Instance = this;

  Intent intent = this.getIntent();
  
  if (intent.getExtras() != null) {
   mTag = intent.getExtras().getInt("tagid", 0);
   mSeriesCaption = intent.getExtras().getString("descr");
  }

  
  pd = new ProgressDialog(this);
  pd.setMessage("Загрузка данных. Ждите...");
  startProgress();
  
  mMainView = LayoutInflater.from(this).inflate(R.layout.graph, null);
  setContentView(mMainView);
  
  
  DownloadTagDataAsyncTask dat = new DownloadTagDataAsyncTask();
  dat.execute(makeURL(mTag));
 }
 
 public void getDateDemoDataset(float[] pValues, Date[] pTimes) {
  XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
     TimeSeries series = new TimeSeries(mSeriesCaption);
     for (int k = 0; k < pValues.length-1; k++) {
      series.add(pTimes[k], pValues[k]);
     }
     
     dataset.addSeries(series);
     mDataset = dataset;
   }
 
 public void getDemoRenderer() {
     XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
     renderer.setAxisTitleTextSize(16);
     renderer.setChartTitleTextSize(20);
     renderer.setLabelsTextSize(15);
     renderer.setLegendTextSize(20);
     renderer.setShowGrid(true);
     renderer.setGridColor(Color.GRAY);
     XYSeriesRenderer r = new XYSeriesRenderer();
     r.setColor(Color.GREEN);
     r.setFillPoints(true);
     renderer.addSeriesRenderer(r);
     mRenderer = renderer;
   }
 
 public void buildChart(){
  if (mChartView == null) {
        LinearLayout layout = (LinearLayout) findViewById(R.id.chart);
        mChartView = ChartFactory.getTimeChartView(this, mDataset, mRenderer,"HH:mm");
        //mRenderer.setClickEnabled(true);
        //mRenderer.setSelectableBuffer(100);
        layout.addView(mChartView, new LayoutParams(LayoutParams.FILL_PARENT,
            LayoutParams.FILL_PARENT));
      } else {
        mChartView.repaint();
      } 
 }
 
 public String makeURL(int pTagId) {
  Calendar cal = new GregorianCalendar();
  
  Date currentDate = new Date(); 
  Long time = currentDate.getTime(); 
  long anotherDate = -1; 
  time = time + (60*60*1000*anotherDate); 
  currentDate = new Date(time); 
  
  String res = "http://www.my_super_mega_web_server.ru/android/?start="
   + new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(currentDate)
   + "&end="
   + new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(cal.getTime()) + "&tag=" + pTagId;

  return res;
 }
 
 public void startProgress(){
  pd.show();
 }

 public void endProgress(){
  pd.hide();
  pd.dismiss();
 }
}

Поясню. При создании GraphActivity ей в Intent передается посылка содержащая идентификатор параметра(tagid) и его текстовое описание(descr), чтобы нам было чем подписать сам график. Если Вы собираетесь адаптировать пример под свои нужды, можете напрямую задать эти параметры и быстро посмотреть результат просто сделав эту активити главной. Итак, первый параметр для php-скрипта мы передали через Intent, параметр конца периода у нас будет текущее время, а параметр начала периода =  конец - 2 часа.
За формирование URL у нас отвечает функция makeURL(int pTag).

У себя в программе эту активити я вызываю нажатием на соответствующую View (SimpleIndicator - это собственный компонент, не ищите его в палитре):

@Override
 public boolean onTouch(View v, MotionEvent event) {
  if (event.getAction() == MotionEvent.ACTION_DOWN){
   if (v instanceof SimpleIndicator){
    Intent intent = new Intent(this, GraphActivity.class);
    intent.putExtra("tagid", ((SimpleIndicator)v).getmTag());
    intent.putExtra("descr", ((SimpleIndicator)v).getDescr());
    this.startActivity(intent);
   } 
  }
  
  return false;
 }

Вот собственно и все.

суббота, 7 июля 2012 г.

Делаем облака. ParallaxSprite.

Все что описано в этом сообщении, касается движка AndEngine версии GLES2, под версию GLES1 не проверялось.

Мне встречалась попытка реализации такого класса на stackoverflow.com, но приведенный там код у меня не заработал и сейчас, к сожалению,  я не могу дать на него прямую ссылку.

Итак, к делу.
В AndEngine есть великолепный класс - AutoParallaxBackground который позволяет сделать непрерывно движущуюся картинку бэкграунда. Конечно, картинка должна быть согласована, т.е. начало картинки должно совпадать с концом (чисто визуально), иначе, все получится, но будет виден разрыв. Благодаря такому поведению спрайта, создается впечатление глубины сцены, а если его сделать многослойным - получается очень красиво и профессионально.Ну про AutoParalaxBackground мы можем подробно узнать из AndEngine examples.
А, что делать, если нам нужно такое же "поведение" спрайта, но не на бэкграунде, а поверх сцены? Например, сделать непрерывно движущиеся облака поверх солнца или там луны, и при этом к объекту, который перекрывается облаками нам нужен доступ как к обычному спрайту (это максимально развязывает руки дизайнеру и разработчику), К сожалению, AndEngine не предоставляет такого класса... Хотя почему к сожалению? Есть повод пораскинуть мозгами! (а у меня есть тема для блога) Короче, начнем раскидывать:-)

public class ParallaxSprite extends Sprite {

    private float mParallaxSpeed;
    private float mOffsetX = 0;

    public ParallaxSprite(float pX, float pY, float mParallaxSpeed, ITextureRegion pTextureRegion, VertexBufferObjectManager pVertexBufferObjectManager) {
        super(pX, pY, pTextureRegion, pVertexBufferObjectManager);

        this.mParallaxSpeed = mParallaxSpeed;
        this.mOffsetX = pX - (pX * mParallaxSpeed);
    }

    @Override
    public void onManagedUpdate(float pSecondsElapsed){
     super.onManagedUpdate(pSecondsElapsed);
     this.mOffsetX += this.mParallaxSpeed * pSecondsElapsed; 
    }
    
    @Override
    protected void onManagedDraw(GLState pGLState, Camera pCamera) {
     pGLState.pushModelViewGLMatrix();
        {
         final float shapeWidthScaled = this.getWidthScaled();
         final float cameraWidth = pCamera.getWidth();
            float baseOffsetX = (this.mOffsetX * this.mParallaxSpeed)% shapeWidthScaled;

            while(baseOffsetX > 0) {
             baseOffsetX -= shapeWidthScaled;
   }
            
            pGLState.translateModelViewGLMatrixf(baseOffsetX, 0, 0);
            
            float currentMaxX = baseOffsetX;
   
   do {
    
    this.preDraw(pGLState, pCamera);
             this.draw(pGLState, pCamera);
             this.postDraw(pGLState, pCamera);
    pGLState.translateModelViewGLMatrixf(shapeWidthScaled, 0, 0);
    currentMaxX += shapeWidthScaled;
   } while(currentMaxX < cameraWidth);
        }
        pGLState.popModelViewGLMatrix();
    }
    
    public void setSpeed(float pSpeed){
     this.mParallaxSpeed = pSpeed;
    }
    
    public float getSpeed(float pSpeed){
     return this.mParallaxSpeed;
    }
}
Использование:
ParallaxSprite spriteClouds1 = new ParallaxSprite(0, 0, 5.0f, mRegionClouds,
    this.getVertexBufferObjectManager())

0,0 - это координаты левого верхнего угла спрайта относительно сцены.
mRegionClouds - это регион текстуры нашего спрайта.
this.getVertexBufferManager() - это так надо ;-)
Вот и все.
Что мы натворили: перекрыли метод отрисовки стандартного спрайта немного измененным кодом AutoParallaxBackground. Если интересуют какие именно отличия - велком в исходный код AutoParallaxBackground.
Еще тройка интересных, на мой взгляд, замечаний:
1.Здесь реализовано горизонтальное движение, а если хотим вертикальное? Посмотрите на этот незамысловатый код внимательней и увидите, что его ничего не стоит переделать и двигать по оси Y, причем делать это в зависимости от ситуации. Зачем двигать спрайт вертикально? Чтобы сделать джампер круче Doodle например ;-)
2.Наши облака представляют собой два спрайта имеющих одну и ту же текстуру. Они занимают всю сцену и имеют разные скорости, таким образом, создается впечатление, что облака постоянно меняются, хотя на самом деле две одинаковых картинки движутся относительно друг друга с разной скоростью.
3. Вы можете изменить направление движения поставив третий параметр < 0.

Кроме того, эта штука реализована у нас в обоях. Добро пожаловать и прокомментировать!