31 марта 2012 г.

OpenGL ES в Android, класс GLSurfaceView

Базовым классом для вывода трехмерной графики в Android c использованием OpenGL ES является класс GLSurfaceView. Он содержит в себе встроенный интерфейс GLSurfaceView.Renderer, который управляет отображением трехмерных объектов. Можно представить себе GLSurfaceView как холст для рисования, а GLSurfaceView.Renderer как умение рисовать на холсте. Любой класс, который умеет рисовать на холсте, становится трехмерным художником. Таким образом, чтобы создать такого "художника" мы должны определить собственный класс, реализующий интерфейс GLSurfaceView.Renderer.
Например так:
public class MyClassRenderer implements GLSurfaceView.Renderer{
           //тело класса
}
Реализовав интерфейс GLSurfaceView.Renderer мы обязаны в классе MyClassRenderer переопределить абстрактные методы этого интерфейса. Таких методов три: onDrawFrame, onSurfaceCreated и onSurfaceChanged. В методе onDrawFrame производится рисование трехмерных объектов, метод onSurfaceCreated вызывается при создании экрана, метод onSurfaceChanged - при изменении экрана.

В методе onSurfaceChanged принято определять область просмотра на экране и задавать параметры проекции трехмерного изображения на экран:
-----------------------------------------------------------------------------------------------------------
public void onSurfaceChanged(GL10 gl, int width, int height) {
            // установим область просмотра равной размеру экрана
            gl.glViewport(0, 0, width, height);
            // подсчитаем отношение ширина/высота
            float ratio = (float) width / height;
            // перейдем в режим работы с матрицей проекции
            gl.glMatrixMode(GL10.GL_PROJECTION);
            // сбросим матрицу проекции на единичную
            gl.glLoadIdentity();
            // устанавливаем перспективную проекцию
            // угол обзора 60 градусов
            // передняя отсекающая плоскость 0.1
            // задняя отсекающая плоскость 100
            GLU.gluPerspective (gl, 60, ratio, 0.1f, 100f);
            // перейдем в режим работы с матрицей модели-вида
            gl.glMatrixMode(GL10.GL_MODELVIEW);
}
------------------------------------------------------------------------------------------------------------  
Метод onSurfaceCreated обычно служит для установки начальных параметров состояния OpenGL и для загрузки текстур из графических файлов. Например:
------------------------------------------------------------------------------------------------------------ 
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            // включим пересчет нормалей на единичную длину
            gl.glEnable(GL10.GL_NORMALIZE);
            // включим сглаживание цветов
            gl.glShadeModel(GL10.GL_SMOOTH);
            // включим проверку глубины
            gl.glEnable(GL10.GL_DEPTH_TEST);
            gl.glDepthFunc(GL10.GL_LEQUAL);
            // разрешим использовать освещение
            gl.glEnable(GL10.GL_LIGHTING); // и.т.д.
            
            // далее загружаем текстуры ..................
}
------------------------------------------------------------------------------------------------------------ 
Почему загружать текстуры нужно именно в методе onSurfaceCreated ? Почему я не могу загрузить текстуры один раз в конструкторе рендерера и обращаться к ним по мере необходимости ?
Реализация текстур в OpenGL ES имеет одну неприятную особенность. Всякий раз, когда заново создается экран, загруженные текстуры удаляются из памяти. Это происходит, например, когда экран телефона выходит из блокировки. Если не загрузить текстуры заново все трехмерные объекты лишатся нанесенных на них графических изображений. Повторная загрузка текстур приводит к задержке в пару секунд перед продолжением рисования.
В методе onDrawFrame производится перерисовка кадра. Перед выводом на экран нового кадра мы должны очистить экран и удалить из памяти все повороты и перемещения в пространстве, т.е. сбросить матрицу модели-вида на единичную.
Делается это следующим образом:
------------------------------------------------------------------------------------------------------------
public void onDrawFrame(GL10 gl) {
            // очищаем буферы глубины и цвета
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
            // перейдем в режим работы с матрицей модели-вида
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            // сбросим матрицу модели-вида на единичную
            gl.glLoadIdentity();
            // далее выполним расчет кадра и его рисование.......
}
------------------------------------------------------------------------------------------------------------ 
После определения класса рендерера MyClassRenderer мы должны создать его экземпляр в классе GLSurfaceView. В GLSurfaceView могут быть созданы несколько рендереров, но активным может быть только один.
Определим собственный класс MyClassSurfaceView, расширяющий GLSurfaceView:
------------------------------------------------------------------------------------------------------------  
public class MyClassSurfaceView extends GLSurfaceView{
            //создадим ссылку для хранения экземпляра нашего класса рендерера
            private MyClassRenderer renderer;
            // конструктор
            public MyClassSurfaceView(Context context) {
                        // вызовем конструктор родительского класса GLSurfaceView
                        super(context);
                        // создадим экземпляр нашего класса MyClassRenderer
                        renderer = new MyClassRenderer(context);
                        // запускаем наш экземпляр рендерера 
                        setRenderer(renderer);
                        // установим режим циклического запуска метода onDrawFrame
                        // в рендерере
                        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
            }
}
------------------------------------------------------------------------------------------------------------
Зачем мы передаем контекст в конструктор класса MyClassRenderer? В общем случае это делать необязательно. Контекст нужен для загрузки текстур из графических файлов. Если планируется использовать текстурирование - рендерер должен знать текущий контекст.
Что происходит при запуске рендерера методом setRenderer? При этом стартует отдельный поток, который вызывает у рендерера метод onDrawFrame. После запуска рендерера нужно определить, как будет производиться отрисовка кадров-автоматически в бесконечном цикле или по команде извне. Режимом смены кадров управляет метод setRenderMode. Если аргументом является RENDERMODE_CONTINUOUSLY то метод onDrawFrame вызывается циклически. Этот режим удобен для анимационных роликов. В другом случае, если в метод setRenderMode передается аргумент RENDERMODE_WHEN_DIRTY, метод onDrawFrame вызывается только один раз при старте рендерера или может быть вызван повторно командой requestRender. Такой режим можно использовать в играх, где требуется вручную управлять сменой кадров, запускать и останавливать анимацию.
Как вызвать рисование из Активити. Это несложно. Достаточно в методе onCreate в качестве текущего контента задать объект нашего класса MyClassSurfaceView. Примерно так:
------------------------------------------------------------------------------------------------------------
public class MyClassActivity extends Activity {
            // создадим ссылку на экземпляр нашего класса MyClassSurfaceView
            private MyClassSurfaceView mGLSurfaceView;
            
            // переопределим метод onCreate
            @Override
            public void onCreate(Bundle savedInstanceState){
                        super.onCreate(savedInstanceState);
                        //создадим экземпляр нашего класса MyClassSurfaceView
                         mGLSurfaceView = new MyClassSurfaceView(this);
                        //вместо вызова стандартного контента  
                        //setContentView(R.layout.main);
                        //вызовем экземпляр нашего класса MyClassSurfaceView  
                        setContentView(mGLSurfaceView);
                        // на экране появится поверхность для рисования в OpenGL ES
            }

            @Override
            protected void onPause() {
                        super.onPause();
                        mGLSurfaceView.onPause();
            }

            @Override
            protected void onResume() {
                        super.onResume();
                        mGLSurfaceView.onResume();
            }
}
------------------------------------------------------------------------------------------------------------
Кратко подведем итоги. 
Как осуществляется отображение трехмерной графики в Android OpenGL ES ? 
Запускается текущий Активити. В нем создается объект нашего класса MyClassSurfaceView, наследника GLSurfaceView. Внутри этого класса создается объект нашего рендерера и запускается на исполнение. Активити выводит на экран объект класса MyClassSurfaceView в качестве текущего контента.

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

  1. Как быть в случае Живых обоев.? Как вызвать рисование в WallpaperService.Engine

    ОтветитьУдалить
    Ответы
    1. Нужно создать свой WallpaperService. Существует готовый движок для живых обоев https://github.com/JesusFreke/Penroser/blob/156767e3eda2ee64f1ff5046b204b197e296ccc8/src/org/jf/GLWallpaper/GLWallpaperService.java

      Удалить
  2. Спасибо! Я нашел
    // Original code provided by Robert Green
    // http://www.rbgrn.net/content/354-glsurfaceview-adapted-3d-live-wallpapers

    Но он как-то сложнее выглядит чем по вашей ссылке.
    Да и у меня проблемы возникают на эмуляторах все супер. На 2.3.3 да и на старших версиях. Но на устройствах с 2.3.3. вообще текстур нет , а на 4х артефакты. Делал все на основе примера с пирамидами а движок использовал от автора в приведенной ссылке. Может вы сталкивались с такими проблемами?

    ОтветитьУдалить
    Ответы
    1. Движок от Роберта Грина (ваша ссылка) работает только на OpeGL ES 1.0. На OpeGL ES 2.0 он работать не будет. К тому же он глючит и тормозит. Наоборот, GLWallpaperService от Jesus Freke (моя ссылка) работает отлично как на OpeGL ES 1.0 так и на OpeGL ES 2.0. Испытания проводил как Android 2.3.3, 2.3.5 и 4.

      Удалить
    2. Подскажите пожалуйста есть-ли какой нибудь примерчик использования этого движка с OpenGL ES 2.0?

      Удалить
    3. Вставляем класс GLWallpaperService в свой проект.
      Затем создаем службу.
      public class LwpService extends GLWallpaperService {
      // конструктор
      public LwpService() {
      super();
      }

      @Override
      public void onCreate() {
      super.onCreate();
      }

      @Override
      public void onDestroy() {
      super.onDestroy();
      }

      public Engine onCreateEngine() {
      MyClassEngine engine = new MyClassEngine();
      return engine;
      }

      // внутренний класс MyClassEngine
      class MyClassEngine extends GLEngine {
      private MyClassRenderer renderer;

      // конструктор
      public MyClassEngine() {
      super();
      setEGLContextClientVersion(2);
      // или setEGLContextClientVersion(1); для OpenGL ES 1.0
      // создаем и запускаем рендерер
      renderer = new MyClassRenderer(getApplicationContext()) ;
      setRenderer(renderer);
      setRenderMode(1);

      }// конец конструктора Engine

      @Override
      public void onDestroy() {
      super.onDestroy();
      }

      @Override
      public void onTouchEvent(MotionEvent event) {
      // если нужно обработать тап по экрану
      renderer.onTouchEvent(event);
      }

      @Override
      public void onOffsetsChanged(float xOffset, float yOffset,
      float xOffsetStep, float yOffsetStep, int xPixelOffset,
      int yPixelOffset) {
      //если нужно обработать сдвиг экрана
      renderer.onOffsetsChanged(xOffset, yOffset, xOffsetStep,
      yOffsetStep, xPixelOffset, yPixelOffset);
      }

      } конец класса MyClassEngine

      }// конец класса LwpService

      Далее нужно в манифесте прописать ссылку на нашу службу LwpService








      Остается создать класс рендерера MyClassRenderer и все будет работать.

      Удалить
  3. Ок. Спасибо. Большое уже пробую.

    ОтветитьУдалить
  4. Урок не полный. Какими должны быть конструкторы MyClassRenderer?

    ОтветитьУдалить
    Ответы
    1. В простейшем случае конструктор может быть пустым.

      Удалить
  5. Здравствуйте, вы не могли бы подсказать аналог функции setEGLContextClientVersion(2) в WallpaperService. В приведенном вами примере, eclipse к сожалению, не может найти эту функцию и выдает ошибку

    ОтветитьУдалить
    Ответы
    1. поставьте в файле манифеста
      uses-sdk android:minSdkVersion="8"
      Тогда eclipse найдет setEGLContextClientVersion

      Удалить
  6. Вечер добрый.
    Подскажите пожалуйста, как сделать, чтобы GLSurfaceView отображалась не на всем экране, а только в его части? Допустим, у меня экран поделён 2мя LinearLayout-ми.

    ОтветитьУдалить
  7. или например чтобы выводил на 2 глаза под vr очки?

    ОтветитьУдалить
  8. Спасибо. Доступно. Разобрался.

    ОтветитьУдалить
  9. public void onDrawFrame(GL10 gl) {
    Random ra = new Random();
    float R = ra.nextFloat();
    float G = ra.nextFloat();
    float B = ra.nextFloat();

    GLES20.glClearColor(R, G, B, 255);
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    }

    ОтветитьУдалить
  10. Этот комментарий был удален автором.

    ОтветитьУдалить