5 апреля 2012 г.

OpenGL ES 1. Зеркальное отражение при помощи кубических текстур

Кубическая текстура представляет из себя шесть фотографий окружающего пространства: "Правая сторона", "Левая сторона", "Верх", "Низ", "Передняя сторона", "Задняя сторона", которые должны быть подготовлены заранее. Всем шести картинкам присваивается одно имя текстуры и обращение идет к текстуре производится по данному имени. Для создания эффекта зеркального отражения используется автоматическая генерация текстур при помощи команды glTexGeni с параметром GL_REFLECTION_MAP. При этом каждой вершине полигона присваивается координата текстуры, соответствующая зеркальному отражению от поверхности полигона.
Автоматическая генерация текстур несмотря на высокую скорость расчета текстурных координат имеет свои недостатки. В частности, при перемещении камеры зеркальное отражение перемещается в месте с ней и расплывается. Чтобы избавиться от таких нежелательных эффектов, нужно изменять матрицу текстуры. Матрица текстуры - это аналог матрицы модели-вида, применяемый не к координатам вершин X, Y, Z, а координатам текстур S, T, R. Все операции, которые можно применить к матрице модели-вида, можно применять и к матрице текстуры, но действовать они будут на текстурные координаты. По умолчанию матрица текстуры равна единичной матрице. Чтобы зеркальное отражение выглядело корректно нужно компенсировать поворот и перемещение камеры. Для этого записываем в массив текущую матрицу модели-вида с учетом все поворотов и перемещений камеры, получаем из нее обратную матрицу модели-вида, а затем зануляем у обратной матрицы модели-вида элементы, связанные с перемещением. Результат всех этих манипуляций записываем в матрицу текстуры. Перед рисованием следующего полигона матрицу текстуры нужно снова превратить в единичную.

Продемонстрирую это на примере. За основу возьмем код статьи, в которой мы рисовали полупрозрачные пирамиды. Изменим код класса Texture на следующий:
------------------------------------------------------------------------------------------------------------
package com.blogspot.andmonahov.pyramidmirror;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11ExtensionPack;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public class Texture{
            private int name;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// конструктор кубической текстуры
public Texture(GL10 gl, Context context, 
            int idpicture1,int idpicture2,int idpicture3,
            int idpicture4,int idpicture5,int idpicture6){
            // передаем в конструктор шесть фотографий с идентификаторами
            // idpicture1, idpicture2, idpicture3, idpicture4, idpicture5, idpicture6
            int[] names=new int[1];
            // генерируем свободное имя текстуры
            gl.glGenTextures(1, names, 0);
            name=names[0];
            gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT,1);
            // Вместо параметра состояния GL10.GL_TEXTURE_2D будем 
            // использовать GL11ExtensionPack.GL_TEXTURE_CUBE_MAP
             gl.glBindTexture(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP, name);
            // устанавливаем параметры текстуры
             gl.glTexParameterf(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                        GL10.GL_TEXTURE_MIN_FILTER,
                        GL10.GL_LINEAR_MIPMAP_LINEAR);
            gl.glTexParameterf(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                        GL10.GL_TEXTURE_MAG_FILTER,
                        GL10.GL_LINEAR);
            gl.glTexParameterx(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                        GL10.GL_TEXTURE_WRAP_S,
                        GL10.GL_CLAMP_TO_EDGE);
            gl.glTexParameterx(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                        GL10.GL_TEXTURE_WRAP_T,
                        GL10.GL_CLAMP_TO_EDGE);
            gl.glTexParameterx(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                        GL11.GL_GENERATE_MIPMAP, GL11.GL_TRUE);
            //загружаем картинки
            //правая сторона (положительная координата X)
            Bitmap bitmap1 = BitmapFactory.decodeResource
                                    (context.getResources(),idpicture1);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_X,
                        0, bitmap1, 0);
            bitmap1.recycle();
            //левая сторона (отрицательная координата X)
            Bitmap bitmap2 = BitmapFactory.decodeResource
                                    (context.getResources(),idpicture2);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
                        0, bitmap2, 0);
            bitmap2.recycle();
            //верх ( положительная координата Y)   
            Bitmap bitmap3 = BitmapFactory.decodeResource
                        (context.getResources(),idpicture3);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
                         0, bitmap3, 0);
            bitmap3.recycle();
            //низ (отрицательная координата Y)          
            Bitmap bitmap4 = BitmapFactory.decodeResource
                        (context.getResources(),idpicture4);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
                         0, bitmap4, 0);
            bitmap4.recycle();
            //задняя часть (положительная координата Z)
            Bitmap bitmap5 = BitmapFactory.decodeResource
                        (context.getResources(),idpicture5);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
            0, bitmap5, 0);
            bitmap5.recycle();
            //передняя часть (отрицательная координата Z)
            Bitmap bitmap6 = BitmapFactory.decodeResource
                        (context.getResources(),idpicture6);
            GLUtils.texImage2D(
                        GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
                         0, bitmap6, 0);
            bitmap6.recycle();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// метод возвращает имя текстуры
public int getName(){
            return name;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
} //конец класса
------------------------------------------------------------------------------------------------------------
Далее нам не нужно задавать координаты текстуры для вершин полигонов, т.к. мы попросим OpenGL генерировать их автоматически с параметром GL_REFLECTION_MAP, создающим координаты зеркального отражения. В классе Triangle заменим метод draw, рисующий треугольник:
------------------------------------------------------------------------------------------------------------
public void draw (GL10 gl){
            vertexBuffer.position(0);
            normalBuffer.position(0);
            texcoordBuffer.position(0);
            // включаем использование массивов вершин
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); 
            // указываем, что буфер с именем vertexBuffer является буфером вершин
            gl.glVertexPointer(3,GL10.GL_FLOAT,0,vertexBuffer);
            // включаем использование массивов нормалей
            gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); 
            // указываем, что буфер с именем normalBuffer является буфером нормалей
            gl.glNormalPointer(GL10.GL_FLOAT,0,normalBuffer);
            if (textureName!=0){
                        // если имя текстуры не пустое
                        // включаем использование кубических текстур
                        gl.glEnable(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP);
                        // для учета освещения при использовании текстур 
                        //включаем режим модуляции
                        gl.glTexEnvx(GL10.GL_TEXTURE_ENV,
                                    GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);
                        // устанавливаем активной текстуру с именем textureName
                        gl.glBindTexture(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP,
                                                            textureName);
                        GL11ExtensionPack gl11ep = (GL11ExtensionPack) gl;
                        GL11 gl11 = (GL11) gl;
                        // переходим в режим работы с текстурной матрицей
                        gl.glMatrixMode(GL10.GL_TEXTURE);
                        gl.glLoadIdentity();
                        // записываем в массив modelViewMatrix 
                        //текущую матрицу модели-вида
                        float[] modelViewMatrix = new float[16];
                        gl11.glGetFloatv(GL11.GL_MODELVIEW_MATRIX, modelViewMatrix, 0);
                        //записываем в массив invertModelViewMatrix 
                        //обратную матрицу модели-вида
                        float[] invertModelViewMatrix=new float [16];
                        Matrix.invertM(invertModelViewMatrix,0,modelViewMatrix,0);
                        // зануляем у обратной матрицы модели-вида компоненты,
                        // связанные с переносом
                        invertModelViewMatrix[12]=0;
                        invertModelViewMatrix[13]=0;
                        invertModelViewMatrix[14]=0;
                        // записываем invertModelViewMatrix в матрицу текстуры
                        gl.glLoadMatrixf(invertModelViewMatrix,0);
                        //генерируем координаты зеркального отражения
                        gl.glEnable(GL11ExtensionPack.GL_TEXTURE_GEN_STR);
                        gl11ep.glTexGeni(GL11ExtensionPack.GL_TEXTURE_GEN_STR, 
                                    GL11ExtensionPack.GL_TEXTURE_GEN_MODE, 
                                    GL11ExtensionPack.GL_REFLECTION_MAP);
                        //переходим в режим работы с матрицей модели-вида
                        gl.glMatrixMode(GL10.GL_MODELVIEW);
            }else{
                        // если имя текстуры пустое
                        // отключаем использование кубических текстур
                        gl.glDisable(GL11ExtensionPack.GL_TEXTURE_CUBE_MAP);
            }
            // рисуем треугольник
            // последний параметр - это количество точек треугольника (т.е. три)
            gl.glDrawArrays(GL10.GL_TRIANGLES,0,3);
}
------------------------------------------------------------------------------------------------------------
Аналогично заменим метод draw в классе Quadr. 
Отличие будет только в последней строке. Вместо glDrawArrays(GL10.GL_TRIANGLES,0,3) у прямоугольника будет glDrawArrays(GL10.GL_TRIANGLE_FAN,0,4);
Изменим код класса MyClassRenderer. Вместо объявления пяти текстур объявим только одну  private Texture texcubemap; и выполним в методе onSurfaceCreated загрузку только одной кубической текстуры 
texcubemap=new Texture(gl,context, R.drawable.icon1,R.drawable.icon2,
R.drawable.icon3,R.drawable.icon4,
R.drawable.icon5,R.drawable.icon6);
Для улучшения эффекта зеркальности увеличим Альфа-компоненту отраженного рассеянного света:
private float [] diffusematerialArray={0.8f, 0.8f, 0.8f, 0.8f};
Привожу код класса MyClassRenderer полностью:
-----------------------------------------------------------------------------------------------------------
package com.blogspot.andmonahov.pyramidmirror;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public class MyClassRenderer implements GLSurfaceView.Renderer{
// интерфейс GLSurfaceView.Renderer содержит
// три метода onDrawFrame, onSurfaceChanged, onSurfaceCreated
// которые должны быть переопределены
//
// текущий контекст
private Context context;
//
// позиция камеры
public static float xposition,yposition,zposition;
// направление взгляда камеры
private float xlook,ylook,zlook;
// координаты вектора, указывающего камере, где верх
private float xtop,ytop,ztop;
//
// массивы для хранения цветов материала
// цвета общего фонового освещения
private float [] ambientmaterialArray={0.2f, 0.2f, 0.2f, 1f};
// цвета отраженного рассеянного света
private float [] diffusematerialArray={0.8f, 0.8f, 0.8f, 0.8f};
// цвета отраженного зеркального света
private float [] specularmaterialArray={0.5f, 0.5f, 0.5f,1f};
// соответствующие им буферы цветов материала
private FloatBuffer ambientmaterialBuffer,
diffusematerialBuffer,specularmaterialBuffer;
//
// массив для хранения координат источника света
private float [] positionlightArray={0.5f,0,0.2f,0};
// массивы для хранения цветов источника света
// цвета общего фонового освещения
private float [] ambientlightArray={0.5f, 0.5f, 0.5f, 1f};
// цвета отраженного рассеянного света
private float [] diffuselightArray={0.8f, 0.8f, 0.8f, 1f};
// цвета отраженного зеркального света
private float [] specularlightArray={0.8f, 0.8f, 0.8f,1f};
// соответствующие им буферы источника света
private FloatBuffer positionlightBuffer,ambientlightBuffer,
diffuselightBuffer,specularlightBuffer;
//
//текстуры
private Texture texcubemap;
//пирамиды
private Pyramid p1, p2;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//конструктор  
public MyClassRenderer(Context context) {
// запомним контекст
// он нам понадобится для загрузки текстур
this.context=context;
// настройки камеры
xposition=0.3f;
yposition=0.3f;
zposition=1.2f;
xlook=0;
ylook=0;
zlook=0;
xtop=0;
ytop=1;
ztop=0;
// переписываем цвета материалов из массивов в буферы
ByteBuffer b1 = ByteBuffer.allocateDirect(4 * 4);
b1.order(ByteOrder.nativeOrder());
ambientmaterialBuffer = b1.asFloatBuffer();
ambientmaterialBuffer.put(ambientmaterialArray);
ambientmaterialBuffer.position(0);
//
ByteBuffer b2 = ByteBuffer.allocateDirect(4 * 4);
b2.order(ByteOrder.nativeOrder());
diffusematerialBuffer = b2.asFloatBuffer();
diffusematerialBuffer.put(diffusematerialArray);
diffusematerialBuffer.position(0);
//
ByteBuffer b3 = ByteBuffer.allocateDirect(4 * 4);
b3.order(ByteOrder.nativeOrder());
specularmaterialBuffer = b3.asFloatBuffer();
specularmaterialBuffer.put(specularmaterialArray);
specularmaterialBuffer.position(0);
//
// переписываем координаты источника света в буфер
ByteBuffer b4 = ByteBuffer.allocateDirect(4 * 4);
b4.order(ByteOrder.nativeOrder());
positionlightBuffer = b4.asFloatBuffer();
positionlightBuffer.put(positionlightArray);
positionlightBuffer.position(0);
//
// переписываем цвета источника света из массивов в буферы
ByteBuffer b5 = ByteBuffer.allocateDirect(4 * 4);
b5.order(ByteOrder.nativeOrder());
ambientlightBuffer = b5.asFloatBuffer();
ambientlightBuffer.put(ambientlightArray);
ambientlightBuffer.position(0);
//
ByteBuffer b6 = ByteBuffer.allocateDirect(4 * 4);
b6.order(ByteOrder.nativeOrder());
diffuselightBuffer = b6.asFloatBuffer();
diffuselightBuffer.put(diffuselightArray);
diffuselightBuffer.position(0);
//
ByteBuffer b7 = ByteBuffer.allocateDirect(4 * 4);
b7.order(ByteOrder.nativeOrder());
specularlightBuffer = b7.asFloatBuffer();
specularlightBuffer.put(specularlightArray);
specularlightBuffer.position(0);

// создаем пирамиды
p1=new Pyramid(-0.15f, 0, 0, 0.3f, 0.45f);
p2=new Pyramid( 0.3f, 0.2f,-1, 0.3f, 0.45f);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void onDrawFrame(GL10 gl) {
//этот метод вызывается циклически
//здесь мы будем выполнять рисование
// очищаем буферы глубины и цвета
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// перейдем в режим работы с матрицей модели-вида
gl.glMatrixMode(GL10.GL_MODELVIEW);
// сбросим матрицу модели-вида на единичную
gl.glLoadIdentity();
//
// поворачиваем пирамиды  вокруг осей
// относительно текущего положения пирамиды
p1.rotatecenter0(0.5f);
p1.rotatecenter1(0.2f);
p2.rotatecenter1(-0.5f);
p2.rotatecenter2(-0.2f);
//
// устанавливаем камеру
GLU.gluLookAt(gl, xposition,yposition,zposition, xlook,ylook,zlook, xtop,ytop,ztop);
//
// включаем источник света с номером 0
gl.glEnable(GL10.GL_LIGHT0);
// устанавливаем координаты источника света
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, positionlightBuffer);
// устанавливаем цвета источника света
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientlightBuffer);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuselightBuffer);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularlightBuffer);
//
// устанавливаем цвета материала
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientmaterialBuffer);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffusematerialBuffer);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularmaterialBuffer);
//
//рисуем дальнюю пирамиду
p2.draw(gl);
//рисуем ближнюю пирамиду
p1.draw(gl);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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);

// включаем режим освещения внешних и внутренних сторон 
gl.glLightModelx(GL10.GL_LIGHT_MODEL_TWO_SIDE, GL10.GL_TRUE);
// отключаем отсечение невидимых граней
gl.glDisable(GL10.GL_CULL_FACE); 
// включаем альфа-компонету цвета
gl.glEnable(GL10.GL_ALPHA);
// включаем смешивание цветов
gl.glEnable(GL10.GL_BLEND); 
// устанавливаем режим смешивания цветов 
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
//
//загружаем фотографии в кубическую текстуру
texcubemap=new Texture(gl,context, R.drawable.icon1,R.drawable.icon2,
R.drawable.icon3,R.drawable.icon4,
R.drawable.icon5,R.drawable.icon6);
//привязываем текстуры к пирамидам
p1.setTextureName(texcubemap.getName());
p2.setTextureName(texcubemap.getName());
}
}
-------------------------------------------------------------------------------------------------------
Получаем две красивые вращающиеся пирамиды

video

Исходный код можно скачать отсюда Ссылка
Исполняемый файл для телефона можно скачать отсюда Ссылка

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

  1. Спасибо большое за статью, очень познавательно.

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

    ОтветитьУдалить
  3. на Samsung Galaxy Nexus i9250 не выводятся текстуры. С чем это может быть связано?

    ОтветитьУдалить
    Ответы
    1. Все 6 частей кубической текстуры должны быть квадратными и иметь одинаковый размер. Кроме того, некоторые видеокарты требуют, чтобы ширина и высота текстур должны быть степенью двойки (32, 64, 128, 256, 512, 1024 и.т.д.)

      Удалить
    2. В вашем примере текстуры как раз квадратные 512х512. И они не выводятся.

      Удалить
    3. разобрался!

      Вместо:
      Bitmap bitmap =
      BitmapFactory.decodeResource(context.getResources(), idpicture);
      заменил на:
      InputStream is = context.getResources()
      .openRawResource(idpicture);
      Bitmap bitmap;
      try {
      bitmap = BitmapFactory.decodeStream(is);
      } finally {
      try {
      is.close();
      } catch(IOException e) {
      // Ignore.
      }
      }
      Огромное спасибо за примеры!

      Удалить
    4. Странно! Вопрос. В изначальном варианте
      Bitmap bitmap =
      BitmapFactory.decodeResource(context.getResources(), idpicture);
      система выдавала какую-нибудь ошибку или просто был черный экран ?

      Удалить
    5. просто черный экран. И это проявлялось только на I9250 с ICS 4.2.2! На таблетке Galaxy Note 10.1 с ICS 4.1 нормально работают оба варианта.

      Удалить
  4. А у меня еще интересней. Изначальный вариант работает, а с потоком на телефоне Alcatel OneTouch 992D текстуры белые!

    ОтветитьУдалить
    Ответы
    1. Попробуйте поместить файлы с текстурами в папку res\drawable-nodpi

      Удалить