7 декабря 2012 г.

OpenGL ES 2.0. Урок 4. Гладкие поверхности. Полигональная сетка.

В этом уроке мы рассмотрим рисование гладких 3D-функций с помощью полигональной сетки.  Пусть наша сетка будет лежать в плоскости XZ, а значение Y будет вычисляться как функция от X и Z, т.е. y=f(x,z). В узлах сетки будут находиться вершины. Обозначим порядковый номер  узла сетки вдоль оси X как i, а вдоль оси Z как j. Номера узлов могут меняться от нуля до imax или jmax соответственно.


Обозначим шаг сетки вдоль оси X как dx, а шаг сетки вдоль оси Z как dz. Тогда мы можем легко вычислить координаты X и Z для всех вершин:
//зададим массивы для хранения координат X и Z
float [] x=new float [imax+1];
float [] z=new float [jmax+1];
//заполним массивы X и Z координатами сетки
for (int i=0; i<=imax; i++){
        x[i]=x0+i*dx; 
 }
for (int j=0; j<=jmax; j++){
        z[j]=z0+j*dz;
}
//где x0 и z0 координаты нулевой точки
Координата Y у нас меняется в зависимости от двух координат других координат X и Z. Поэтому будем хранить ее в двумерном массиве:
float [] [] y=new float [jmax+1][imax+1];
Придумаем какую-нибудь функцию, например функцию "холма с центром в начале координат", и вычислим её для всех вершин:
// заполним массив Y значениями функции
for (int j=0; j<=jmax; j++){
        for (int i=0; i<=imax; i++){
                y[j][i]=(float)Math.exp(-3*(x[i]*x[i]+z[j]*z[j]));
        }
}
Итак, у нас теперь есть все координаты вершин. Теперь нужно переписать их в байтовый буфер, который будет понятен для OpenGL. Двумерный  массив координаты Y в буфер никак загнать не получится. Поэтому мы должны переписать координаты вершин в одномерный массив в последовательности x0, y0, z0,  x1, y1, z1,  x2, y2, z2  и.т.д., а затем его уже переписать в буфер. Создадим такой одномерный массив:
float [] vertex=new float[(jmax+1)*(imax+1)*3];
где (jmax+1)*(imax+1)*3 - это количество координат вершин (каждая вершина имеет три координаты, поэтому умножаем количество вершин на три).
Определимся с правилом хранения координат в этом массиве. Будем хранить вершины последовательно по строчкам как показано на рисунке выше. Т.е. сначала перепишем в массив vertex координаты первой строки, затем второй, третьей и.т.д.
// заполним массив координат vertex
int k=0;
for (int j=0; j<=jmax; j++){
        for (int i=0; i<=imax; i++){
                vertex[k]=x[i];
                k++;
                vertex[k]=y[j][i];
                k++;
                vertex[k]=z[j];
                k++;
       }
}
Теперь создадим буфер для хранения координат вершин и перепишем в него массив vertex:
ByteBuffer vb = ByteBuffer.allocateDirect((jmax+1)*(imax+1)*3*4);
vb.order(ByteOrder.nativeOrder());
vertexBuffer = vb.asFloatBuffer();
vertexBuffer.position(0);

vertexBuffer.put(vertex);
vertexBuffer.position(0);


В перспективе наша функция Y будет изменяться с течением времени и картинка будет двигаться , поэтому удобно оформить ее расчет в виде метода:
private void getVertex(){
       
// заполним массив Y значениями функции
        for (int j=0; j<=jmax; j++){
                for (int i=0; i<=imax; i++){
                        y[j][i]=(float)Math.exp(-3*(x[i]*x[i]+z[j]*z[j]));
                }
        }
        // заполним массив координат vertex
        int k=0;
        for (int j=0; j<=jmax; j++){
                for (int i=0; i<=imax; i++){
                        vertex[k]=x[i];
                        k++;
                        vertex[k]=y[j][i];
                        k++;
                        vertex[k]=z[j];
                        k++;
                }
        }
        //перепишем координаты вершин из массива vertex в буфер
        vertexBuffer.put(vertex);
        vertexBuffer.position(0);
}

Расчет нормалей.

Для того чтобы использовать освещение нам нужно вычислить нормаль для каждой  вершины сетки. Напомню, что вектор нормали - это вектор единичной длины, перпендикулярный к поверхности в данной точке этой поверхности и направленный от обратной стороны поверхности к лицевой стороне. Чтобы нарисовать поверхность, нам потребуется ее разбить на множество треугольников. Лицевой стороной треугольника является сторона, которая при рисовании обходится по вершинам против часовой стрелки:
Нормаль к плоскости может быть вычислена как векторное произведение двух векторов A и B, лежащих в данной плоскости:

Как видно из рисунка направление нормали определяется по правилу правого винта при переходе от вектора A к вектору B. При переходе против часовой стрелки нормаль направлена вверх, при переходе по часовой стрелке - вниз. Направление "вверх" у нас совпадает с направление оси Y.  В математическом виде компоненты векторного произведения записываются так:
normalX=ay*bz-by*az
normalY=bx*az-ax*bz
normalZ=ax*by-bx*ay
где ax, ay, az - координаты вектора A
bx, by, bz - координаты вектора B
Чтобы вычислить нормаль для конкретной вершины сетки, нам нужно определить для нее векторы A и B. Возьмем вершину с индексами [j] [i] и достроим от нее два вектора - вниз на один шаг - вектор A и вправо один шаг - вектор B:

Зададим для хранения координат векторов нормалей три массива:
float [] normalX=new float[jmax+1][imax+1];
float [] normalY=new float[jmax+1][imax+1];
float [] normalZ=new float[jmax+1][imax+1];

Теперь мы можем вычислить нормаль для вершины [j] [i]:
ax = 0
ay = y [j+1] [i] -y [j] [i]
az = z [j+1] - z [j] = dz    
шаг сетки по оси Z
bx = x [i+1] - x [i] = dx   шаг сетки по оси X
by = y [j] [i+1] - y [j] [i]
bz=0
normalX [j] [i] = ay*bz-by*az = - by*az = - ( y [j] [i+1] - y [j] [i] ) * dz
normalY [j] [i] = bx*az-ax*bz =   bx*az = dx * dz
normalZ [j] [i] = ax*by-bx*ay = - bx*ay = - dx * ( y [j+1] [i] - y [j] [i] )


По этим формулам мы можем вычислить нормали в диапазоне номеров i от 0 до (imax-1)  и j от 0 до (jmax-1). Отдельно нужно рассчитать нормали для крайнего правого столбца (i=imax) и крайней нижней строки (j=jmax), а также для правого нижнего угла (i=imax, j=jmax). Для расчета нормалей нужно правильно подобрать векторы A и B:
Расчет вектора нормали для вершин с индексами [ j ] [ imax ] в крайнем правом столбце:
ax = x [ imax -1] - x [imax] = - dx
ay = y [ j ] [ imax -1] - y [ j ] [ imax]
az = 0   

bx = 0   
by = y [ j+1 ] [ imax] - y [ j ] [ imax ]
bz = z [ j+1] - z [ j ] = dz 
normalX [j] [imax] = ay*bz-by*az = ay*bz = ( y [ j ] [ imax -1] - y [ j ] [ imax] ) * dz
normalY [j] [imax] = bx*az-ax*bz =  
- ax*bz = dx  * dz
normalZ [j] [imax] = ax*by-bx*ay = 
ax*by = - dx * ( y [ j+1 ] [ imax] - y [ j ] [ imax ] )

Расчет вектора нормали для вершин с индексами [ jmax ] [ i ] в крайней нижней строке:
ax =  x [ i+1 ] - x [ i ] = dx
ay =  y [ jmax ] [ i+1 ] - y [ jmax ] [ i ]
az =  0
 
bx =  0 
by = y [ jmax-1 ] [ i ] - y [ jmax ] [ i ]
bz = z [ jmax -1] - z [ jmax ] = - dz 
normalX [jmax] [ i ] = ay*bz-by*az = ay*bz = - ( y [ jmax ] [ i+1 ] - y [ jmax ] [ i ] ) * dz
normalY [jmax] [ i ] = bx*az-ax*bz = 
-ax*bz =  dx * dz
normalZ [jmax] [ i ] = ax*by-bx*ay = 
ax*by = dx * ( y [ jmax-1 ] [ i ] - y [ jmax ] [ i ] )

Расчет вектора нормали для правого нижнего угла [ jmax ] [ imax ]:
ax =  0 
ay =  y [ jmax-1 ] [ imax ] - y [ jmax ] [ imax ]
az =  
z [ jmax -1 ] - z [ jmax ] = - dz
bx =  x [ imax -1 ] - x [ imax ] = - dx
by = y [ jmax ] [ imax -1 ] - y [ jmax ] [ imax ]
bz = 0 
normalX [jmax] [ imax ]=ay*bz-by*az =-by*az =(y [ jmax] [ imax-1] - y [ jmax] [imax]) * dz
normalY [jmax] [ imax ] = bx*az-ax*bz =  
bx*az = dx * dz
normalZ [jmax] [  imax ] =ax*by-bx*ay = 
-bx*ay=dx * (y [jmax-1] [imax] - y[jmax ] [imax])

По аналогии с координатами вершин мы должны переписать координаты нормалей последовательно в одномерный массив и отправить его в байтовый буфер:
float [] normal=new float [(jmax+1)*(imax+1)*3];

ByteBuffer nb = ByteBuffer.allocateDirect((jmax+1)*(imax+1)*3*4);
nb.order(ByteOrder.nativeOrder());
normalBuffer = nb.asFloatBuffer();
normalBuffer.position(0);


int k=0;
for (int j=0; j<=jmax; j++){
        for (int i=0; i<=imax; i++){

                normal[k]=normalX[j][i];
                k++;
                normal[k]=normalY[j][i];
                k++;
                normal[k]=normalZ[j][i];
                k++;
        }
}

normalBuffer.put(normal);
normalBuffer.position(0);

Итак, мы научились по координатам вершин в сетки вычислять вектор нормали для каждой вершины. Оформим расчет нормалей в виде метода:
private void getNormal(){
        for (int j=0; j<jmax; j++){
                for (int i=0; i<imax; i++){
                        normalX [j] [i] = - ( y [j] [i+1] - y [j] [i] ) * dz;
                        normalY [j] [i] = dx * dz;
                        normalZ [j] [i] = - dx * ( y [j+1] [i] - y [j] [i] );
                }
        }
        //нормаль для i=imax
        for (int j=0; j<jmax; j++){
                normalX [j] [imax] = ( y [ j ] [ imax -1] - y [ j ] [ imax] ) * dz;
                normalY [j] [imax] = dx * dz;
                normalZ [j] [imax] = - dx * ( y [ j+1 ] [ imax] - y [ j ] [ imax ] );
        }
        //нормаль для j=jmax
        for (int i=0; i<imax; i++){
                normalX [jmax] [ i ] = - ( y [ jmax ] [ i+1 ] - y [ jmax ] [ i ] ) * dz;
                normalY [jmax] [ i ] = dx * dz;
                normalZ [jmax] [ i ] = dx * ( y [ jmax-1 ] [ i ] - y [ jmax ] [ i ] );
        }
        //нормаль для i=imax и j=jmax
        normalX [jmax] [ imax ]= (y [ jmax] [ imax-1] - y [ jmax] [imax]) * dz;
        normalY [jmax] [ imax ] = dx * dz;
        normalZ [jmax] [ imax ] = dx * (y [jmax-1] [imax] - y[jmax ] [imax]);
        //переписываем координаты вектора нормали в одномерный массив normal
        int k=0;
        for (int j=0; j<=jmax; j++){
                for (int i=0; i<=imax; i++){
                        normal[k]=normalX[j][i];
                        k++;
                        normal[k]=normalY[j][i];
                        k++;
                        normal[k]=normalZ[j][i];
                        k++;
                }
        }
        //отправляем одномерный массив normal в буфер 
        normalBuffer.put(normal);
        normalBuffer.position(0); 
} // конец метода
Мы не будем нормализовать нормали на CPU, поручим нормализацию шейдерам. 

Разбиваем сетку на треугольники и рисуем поверхность.

Далее мы будем рисовать нашу поверхность в одном проходе, т.е. с использованием одной команды glDrawElements и правила обхода вершин GL_TRIANGLE_STRIP. Разобьем нашу сетку на ленты из треугольников. В нашем примере сначала идет лента слева-направо и вершины передаются в OpenGL в следующем порядке  0,5,1,6,2,7,3,8,4,9. Правило GL_TRIANGLE_STRIP автоматически создает ряд треугольников 0-5-1, 1-5-6, 1-6-2, 2-6-7, 2-7-3, 3-7-8, 3-8-4, 4-8-9. Порядок перечисления вершин выбран так, чтобы обход выполнялся против часовой стрелки и верхняя сторона сетки считалась лицевой.

Дойдя до 9-ой вершины мы переходим к ленте справа-налево :
 Нам удобно перечислять вершины в таком же порядке 9,14, 8,13, 7,12, т.е. сверху-вниз. Однако при этом топология правила GL_TRIANGLE_STRIP нарушится и обход будет выполняться по часовой стрелке. Для того чтобы склеить две ленты я продублировал 9-ю вершину между лентами и мысленно продолжил ряд. На рисунке добавленная вершина отмечена красным цветом:
















Теперь порядок перечисления вершин будет такой:
 0,5,1,6,2,7,3,8,4,9, 9,  9,14,8,13,7,12,6,11,5,10, 10,  10,15,11,16,12,17,13,18,14,19
Красным цветом отмечены добавленные вершины.
Таким образом, чтобы сделать единую цепь вершин для правила GL_TRIANGLE_STRIP нужно дублировать последнюю вершину в каждой ленте. Порядок перечисления вершин называют массивом индексов и передают OpenGL в виде буфера. Создадим такой массив:
// временный массив индексов
short[] index;
// 2*(imax+1) - количество индексов в ленте
// jmax - количество лент
// (jmax-1) - добавленные индексы для связки лент
// размер индексного массива

sizeindex=2*(imax+1)*jmax + (jmax-1);
index = new short[sizeindex];
Для того, чтобы получить порядковый номер вершины по известным номерам i и j введем дополнительную функцию chain: 
private short chain(int j, int i){
        return (short) (i+j*(imax+1));
}
Например, для i=3, j=2 функция chain возвратит 13, т.к. в нашем примере imax=4.
Приступим к расчету массива индексов:
int k=0;
int j=0;
while (j < jmax) {
        // лента слева направо
        for (int i = 0; i <= imax; i++) {
                index[k] = chain(j,i);
                k++;
                index[k] = chain(j+1,i);
                k++;
        }
        if (j < jmax-1){
                // вставим хвостовой индекс для связки
                index[k] = chain(j+1,imax);
                k++;
        }
        // переводим ряд
        j++;

        // проверяем достижение конца
        if (j < jmax){
                // лента справа налево
                for (int i = imax; i >= 0; i--) {
                        index[k] = chain(j,i);
                        k++;
                        index[k] = chain(j+1,i);
                        k++;
                }
               if (j < jmax-1){
                        // вставим хвостовой индекс для связки
                        index[k] = chain(j+1,0);
                        k++;
                }
                // переводим ряд
                j++;
        }
}

// буфер индексов - тип short содержит 2 байта
ByteBuffer bi = ByteBuffer.allocateDirect(sizeindex * 2);
bi.order(ByteOrder.nativeOrder());
indexBuffer = bi.asShortBuffer();
// заполняем буфер индексов
indexBuffer.put(index);
indexBuffer.position(0);
// уничтожаем временный массив индексов,
// т.к. в дальнейшем нужен только буфер индексов
index = null;

Для того, чтобы нарисовать нашу поверхность достаточно вызвать команду:
GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, sizeindex,
                                 GLES20.GL_UNSIGNED_SHORT, indexBuffer);


Полный код класса рендерера, рисующего поверхность.

Для примера приведу полный код класса MyClassRenderer, который создает  сетку размером 50x50 и рисует холм.
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;

public class MyClassRenderer implements GLSurfaceView.Renderer{
// интерфейс GLSurfaceView.Renderer содержит
// три метода onDrawFrame, onSurfaceChanged, onSurfaceCreated
// которые должны быть переопределены
// текущий контекст

private Context context;
//координаты камеры
private float xСamera, yCamera, zCamera;
//координаты источника света
private float xLightPosition, yLightPosition, zLightPosition;
//матрицы
private float[] modelMatrix;
private float[] viewMatrix;
private float[] modelViewMatrix;
private float[] projectionMatrix;
private float[] modelViewProjectionMatrix;
//размеры сетки
private int imax=49;
private int jmax=49;
//размер индексного массива
private int sizeindex;
//начальная координата x
private float x0=-1f;
//начальная координата z
private float z0=-1f;
//шаг сетки по оси x
private float dx=0.04f;
//шаг сетки по оси z
private float dz=0.04f;
// массив для хранения координаты x
private float [] x;
// массив для хранения координаты y
private float [][] y;
//массив для хранения координаты z
private float [] z;
//массив для хранения координат вершин для записи в буфер
private float [] vertex;
//массивы для хранения координат вектора нормали
private float [][] normalX;
private float [][] normalY;
private float [][] normalZ;
//массив для хранения координат вектора нормали для записи в буфер
private float [] normal;
//буферы для координат вершин и нормалей
private FloatBuffer vertexBuffer, normalBuffer;
//буфер индексов
private ShortBuffer indexBuffer;
//шейдерный объект
private Shader mShader;
//------------------------------------------------------------------------------------------
//конструктор
public MyClassRenderer(Context context) {
         // запомним контекст
         // он нам понадобится в будущем для загрузки текстур
         this.context=context;
         //координаты точечного источника света
         xLightPosition=5f;
         yLightPosition=5f;
         zLightPosition=5f;
         //матрицы
         modelMatrix=new float[16];
         viewMatrix=new float[16];
         modelViewMatrix=new float[16];
         projectionMatrix=new float[16];
         modelViewProjectionMatrix=new float[16];
         //мы не будем двигать объекты
         //поэтому сбрасываем модельную матрицу на единичную
         Matrix.setIdentityM(modelMatrix, 0);
         //координаты камеры
         xСamera=0.3f;
         yCamera=1.7f;
         zCamera=1.5f;
         //пусть камера смотрит на начало координат
         //и верх у камеры будет вдоль оси Y
         //зная координаты камеры получаем матрицу вида
         Matrix.setLookAtM(viewMatrix, 0, xСamera, yCamera, zCamera, 0, 0, 0, 0, 1, 0);
         // умножая матрицу вида на матрицу модели
         // получаем матрицу модели-вида
         Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
         // создаем массивы
         x=new float [imax+1];
         z=new float [jmax+1];
         y=new float [jmax+1][imax+1];
         vertex=new float[(jmax+1)*(imax+1)*3];
         normalX=new float[jmax+1][imax+1];
         normalY=new float[jmax+1][imax+1];
         normalZ=new float[jmax+1][imax+1];
         normal=new float[(jmax+1)*(imax+1)*3];
         //заполним массивы x и z координатами сетки
         for (int i=0; i<=imax; i++){
                  x[i]=x0+i*dx; 
         }
         for (int j=0; j<=jmax; j++){
                  z[j]=z0+j*dz; 
         }
         //создадим буфер для хранения координат вершин
         // он заполняется в методе getVertex()
         ByteBuffer vb = ByteBuffer.allocateDirect((jmax+1)*(imax+1)*3*4);
         vb.order(ByteOrder.nativeOrder());
         vertexBuffer = vb.asFloatBuffer();
         vertexBuffer.position(0);
         //создадим буфер для хранения координат векторов нормалей
         // он заполняется в методе getNormal()
         ByteBuffer nb = ByteBuffer.allocateDirect((jmax+1)*(imax+1)*3*4);
         nb.order(ByteOrder.nativeOrder());
         normalBuffer = nb.asFloatBuffer();
         normalBuffer.position(0);
         //индексы
         // временный массив индексов
         short[] index;
         // 2*(imax+1) - количество индексов в ленте
         // jmax - количество лент
         // (jmax-1) - добавленные индексы для связки лент
         sizeindex=2*(imax+1)*jmax + (jmax-1);
         index = new short[sizeindex];
         // расчет массива индексов для буфера
         int k=0;
         int j=0;
         while (j < jmax) {
                  // лента слева направо
                  for (int i = 0; i <= imax; i++) {
                           index[k] = chain(j,i);
                           k++;
                           index[k] = chain(j+1,i);
                           k++; 
                  }
                  if (j < jmax-1){
                           // вставим хвостовой индекс для связки
                           index[k] = chain(j+1,imax);
                           k++;
                  }
                  // переводим ряд
                  j++;

                  // проверяем достижение конца
                  if (j < jmax){
                           // лента справа налево
                           for (int i = imax; i >= 0; i--) {
                                    index[k] = chain(j,i);
                                    k++;
                                    index[k] = chain(j+1,i);
                                    k++; 
                           }
                           if (j < jmax-1){
                                    // вставим хвостовой индекс для связки
                                    index[k] = chain(j+1,0);
                                    k++;
                           }
                           // переводим ряд
                           j++;
                  }
         }
         // буфер индексов - тип short содержит 2 байта
         ByteBuffer bi = ByteBuffer.allocateDirect(sizeindex * 2);
         bi.order(ByteOrder.nativeOrder());
         indexBuffer = bi.asShortBuffer();
         // заполняем буфер индексов
         indexBuffer.put(index);
         indexBuffer.position(0);
         // уничтожаем временный массив индексов,
         // т.к. в дальнейшем нужен только буфер индексов
         index = null;
         //начальное заполнение буферов вершин и нормалей
         getVertex();
         getNormal();
}//конец конструктора
//------------------------------------------------------------------------------------------
// вспомогательная функция
// возвращает порядковый номер вершины по известным j и i

private short chain(int j, int i){
         return (short) (i+j*(imax+1));
}
//------------------------------------------------------------------------------------------
//метод выполняет расчет координат вершин
private void getVertex(){
         // заполним массив Y значениями функции
         for (int j=0; j<=jmax; j++){
                  for (int i=0; i<=imax; i++){
                           y[j][i]=(float)Math.exp(-3*(x[i]*x[i]+z[j]*z[j]));
                  }
         }
         // заполним массив координат vertex
         int k=0;
         for (int j=0; j<=jmax; j++){
                  for (int i=0; i<=imax; i++){
                           vertex[k]=x[i];
                           k++;
                           vertex[k]=y[j][i];
                           k++;
                           vertex[k]=z[j];
                           k++;
                  }
         }
         //перепишем координаты вершин из массива vertex в буфер координат вершин
         vertexBuffer.put(vertex);
         vertexBuffer.position(0);
}//конец метода
//------------------------------------------------------------------------------------------
//метод выполняет расчет векторов нормалей
//по известным координатам вершин

private void getNormal(){
         for (int j=0; j<jmax; j++){
                  for (int i=0; i<imax; i++){
                           normalX [j] [i] = - ( y [j] [i+1] - y [j] [i] ) * dz;
                           normalY [j] [i] = dx * dz;
                           normalZ [j] [i] = - dx * ( y [j+1] [i] - y [j] [i] );
                  }
         }
         //нормаль для i=imax
         for (int j=0; j<jmax; j++){
                  normalX [j] [imax] = ( y [ j ] [ imax -1] - y [ j ] [ imax] ) * dz;
                  normalY [j] [imax] = dx * dz;
                  normalZ [j] [imax] = - dx * ( y [ j+1 ] [ imax] - y [ j ] [ imax ] );
         }
         //нормаль для j=jmax
         for (int i=0; i<imax; i++){
                  normalX [jmax] [ i ] = - ( y [ jmax ] [ i+1 ] - y [ jmax ] [ i ] ) * dz;
                  normalY [jmax] [ i ] = dx * dz;
                  normalZ [jmax] [ i ] = dx * ( y [ jmax-1 ] [ i ] - y [ jmax ] [ i ] );
         }
         //нормаль для i=imax и j=jmax
         normalX [jmax] [ imax ]= (y [ jmax] [ imax-1] - y [ jmax] [imax]) * dz;
         normalY [jmax] [ imax ] = dx * dz;
         normalZ [jmax] [ imax ] = dx * (y [jmax-1] [imax] - y[jmax ] [imax]);
         //переписываем координаты вектора нормали в одномерный массив normal
         int k=0;
         for (int j=0; j<=jmax; j++){
                  for (int i=0; i<=imax; i++){
                           normal[k]=normalX[j][i];
                           k++;
                           normal[k]=normalY[j][i];
                           k++;
                           normal[k]=normalZ[j][i];
                           k++;
                  }
         }
         //отправляем одномерный массив normal в буфер
         normalBuffer.put(normal);
         normalBuffer.position(0);
} // конец метода
//------------------------------------------------------------------------------------------
//метод, который срабатывает при изменении размеров экрана
//в нем мы получим матрицу проекции и матрицу модели-вида-проекции

public void onSurfaceChanged(GL10 unused, int width, int height) {
         // устанавливаем glViewport
         GLES20.glViewport(0, 0, width, height);
         float ratio = (float) width / height;
         float k=0.055f;
         float left = -k*ratio;
         float right = k*ratio;
         float bottom = -k;
         float top = k;
         float near = 0.1f;
         float far = 10.0f;
         // получаем матрицу проекции
         Matrix.frustumM(projectionMatrix, 0, left, right, bottom, top, near, far);
         // матрица проекции изменилась,
         // поэтому нужно пересчитать матрицу модели-вида-проекции
         Matrix.multiplyMM( modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0);
} //конец метода
//------------------------------------------------------------------------------------------
//метод, который срабатывает при создании экрана
//здесь мы создаем шейдерный объект

public void onSurfaceCreated(GL10 unused, EGLConfig config) {
         //включаем тест глубины
         GLES20.glEnable(GLES20.GL_DEPTH_TEST);
         //включаем отсечение невидимых граней
         GLES20.glEnable(GLES20.GL_CULL_FACE);
         //включаем сглаживание текстур, это пригодится в будущем
         GLES20.glHint(GLES20.GL_GENERATE_MIPMAP_HINT, GLES20.GL_NICEST);
         //простые шейдеры для освещения
         String vertexShaderCode=
         "uniform mat4 u_modelViewProjectionMatrix;"+
         "attribute vec3 a_vertex;"+
         "attribute vec3 a_normal;"+
         "varying vec3 v_vertex;"+
         "varying vec3 v_normal;"+
          "void main() {"+
                   "v_vertex=a_vertex;"+
                   "vec3 n_normal=normalize(a_normal);"+
                   "v_normal=n_normal;"+
                   "gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
          "}"; 

         String fragmentShaderCode=
         "precision mediump float;"+
         "uniform vec3 u_camera;"+
         "uniform vec3 u_lightPosition;"+
          "varying vec3 v_vertex;"+
          "varying vec3 v_normal;"+
          "void main() {"+
                "vec3 n_normal=normalize(v_normal);"+
                 "vec3 lightvector = normalize(u_lightPosition - v_vertex);"+
                "vec3 lookvector = normalize(u_camera - v_vertex);"+
                "float ambient=0.1;"+
                "float k_diffuse=0.7;"+
                "float k_specular=0.3;"+
                "float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);"+
                "vec3 reflectvector = reflect(-lightvector, n_normal);"+
                "float specular=k_specular*pow( max(dot(lookvector,reflectvector),0.0),40.0);"+
                "vec4 one=vec4(1.0,1.0,1.0,1.0);"+
                "vec4 lightColor=(ambient+diffuse+specular)*one;"+ 
                "gl_FragColor=lightColor;"+
          "}";
         //создадим шейдерный объект
         mShader=new Shader(vertexShaderCode, fragmentShaderCode);
         //свяжем буфер вершин с атрибутом a_vertex в вершинном шейдере
         mShader.linkVertexBuffer(vertexBuffer);
         //свяжем буфер нормалей с атрибутом a_normal в вершинном шейдере
         mShader.linkNormalBuffer(normalBuffer);
         //связь атрибутов с буферами сохраняется до тех пор,
         //пока не будет уничтожен шейдерный объект
}//конец метода
//------------------------------------------------------------------------------------------
//метод, в котором выполняется рисование кадра
public void onDrawFrame(GL10 unused) {          //передаем в шейдерный объект матрицу модели-вида-проекции
         mShader.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
         //передаем в шейдерный объект координаты камеры
         mShader.linkCamera(xСamera, yCamera, zCamera);
         //передаем в шейдерный объект координаты источника света
         mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
         //вычисляем координаты вершин
         getVertex();
         //вычисляем координаты нормалей
         getNormal();
         //очищаем кадр
         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT
                                  | GLES20.GL_DEPTH_BUFFER_BIT);
         //рисуем поверхность
         GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, sizeindex,
                           GLES20.GL_UNSIGNED_SHORT, indexBuffer); 
}//конец метода
//------------------------------------------------------------------------------------------
}//конец класса
//------------------------------------------------------------------------------------------
Запустим код на исполнение и получим следующую картинку:



Теперь заставим изображение двигаться. Изобразим волну и заставим ее двигаться. Поднимем вверх источник света, чтобы на волне не было очень темных областей:
xLightPosition=5f;
yLightPosition=30f;
zLightPosition=5f;
и в методе getVertex заменим вычисление функции Y:
// определим фактор времени
double time=System.currentTimeMillis();
// заполним массив Y значениями функции
for (int j=0; j<=jmax; j++){
         for (int i=0; i<=imax; i++){
                  y[j][i]=0.2f*(float)Math.cos(0.005*time+5*(z[j]+x[i]));
         }
}
По экрану побежит волна:


Полный код урока можно скачать отсюда PolygonCell.zip
Если непонятно, что такое класс Shader и его методы читайте первый урок http://andmonahov.blogspot.ru/2012/10/opengl-es-20-1.html


24 комментария:

  1. Отлично!!! Проверил на телефоне и планшете работает замечательно.(На планшете ставил 100x100 для получения того же эффекта)
    хотя при роверке 500X500 и больше на планшете начались артифакты... но тут хз...
    Возник еще 1 вопрос: Как определить клик(тап) на объекте в пространстве? если брать к примеру простейшею фигу треугольник. Как я понимаю необходимо перевести как то двоичные координаты тапа в трехмерные и построить отрезок основываясь на положении камеры, затем проверить попадает ли отрезок в треугольник....

    ОтветитьУдалить
    Ответы
    1. 500х500 это 250000 вершин. Индекс может быть типа byte или short. Максимальное значение числа для типа short 32767. Поэтому для 100х100 (т.е.10000 вершин) нет артефактов, а для 500х500-они есть. Если вершин больше, чем 32767 нужно рисовать поверхность по кускам.

      Как определить тап ?
      1. Получить viewport. В методе onSurfaceChanged определить массив viewport и получить его с помощью команды GLES20.glGetIntegerv:
      // устанавливаем glViewport
      GLES20.glViewport(0, 0, width, height);
      int[] viewport = new int[4];
      // записываем glViewport в массив
      GLES20.glGetIntegerv(GLES20.GL_VIEWPORT, viewport, 0);

      2.Перевести экранные координаты точки касания winx, winy в мировые координаты:
      winx=touchEvent.getX();
      winy=touchEvent.getY();
      int realY = viewport[3] - (int) winy - 1;
      float[] point = new float[4];
      GLU.gluUnProject(winx, realY, 100, modelViewMatrix, 0,
      projectionMatrix, 0, viewport, 0, point, 0);
      point[0] = point[0] / point[3];
      point[1] = point[1] / point[3];
      point[2] = point[2] / point[3];
      //мировые координаты точки касания
      float xtouch=point[0];
      float ytouch=point[1];
      float ztouch=point[2];

      3.Построить прямую, соединяющую камеру и точку (xtouch, ytouch, ztouch)

      4.Найти точку пересечения этой прямой и плоскости треугольника.

      5.Определить лежит ли точка пересечения внутри треугольника.

      Удалить
  2. Сглаживание поверхности отличное, но при загрузке 3D моделей сглаживание пропадает, увеличение количества вершин не помогает. Подскажите что нужно добавить?

    ОтветитьУдалить
    Ответы
    1. Для замкнутых поверхностей, например сферы, сетку нужно строить по-другому, например в полярных координатах.

      Удалить
    2. Перевёл координаты вершин и нормали в сферические координаты, фигура сильно деформировалась. А в самом OpenGL 2.0 нужно объявлять, что работаем с полярными координатами.

      Удалить
    3. Допустим нам нужно нарисовать сферу. Начинаем с северного полюса и меняем углы по широте и долготе с небольшим шагом. Получается спираль, которая начинается на северном полюсе и заканчивается на южном. Полученную спиральную ленту заполняем треугольниками, переводим сферические координаты в пространственные XYZ и рисуем с помощь правила GL_TRIANGLE_STRIP

      Удалить
  3. Здравствуйте! Вопрос не много не по теме. Как добиться сглаженных 2D линий шириной 1px, в OpenGL ES 2.0? Пробывал MSAA 2x,4x не помогает. Уже весь гугл облазил, толкового решения для 2.0 не нашёл. Что вы можете посоветовать для решения проблемы?(платформа Android)

    Спасибо.

    ОтветитьУдалить
    Ответы
    1. Попробуйте два раза нарисовать одну и ту же линию. Сначала непрозрачную, затем поверх первой полупрозрачную с небольшим сдвигом.

      Удалить
  4. А разве нормаль вершины не должна зависеть от соседних треугольников ?
    К примеру вершина 12 от треуголников 7-11-12, 7-12-8, 8-12-13, 12-11-16, 12-16-17, 12-17-13 ?

    ОтветитьУдалить
    Ответы
    1. Чтобы учесть влияние соседних вершин можно подсчитать нормаль 12-ой вершины так:
      1 вариант-вертикальный крест. Определить два вектора - A из точки 7 в точку 17, B - из точки 11 в точку 13 и вычислить их векторное произведение.
      2 вариант-косой крест. A - из точки 6 в точку 18, B - из точки 16 в точку 8.
      3 вариант. Сложить нормали, рассчитанные по вертикальному и косому кресту и поделить на два. Т.е. вычислить их среднее значения.
      Пробовал по всякому. Разницы в качестве изображения не заметил.

      Удалить
  5. Андрей, когда будут новые статьи?

    ОтветитьУдалить
    Ответы
    1. Через неделю выйдет новая статья о моделировании эффекта преломления света с помощью шейдеров. Прошу прощения за отсутствие новых публикаций. У меня сейчас мало свободного времени.

      Удалить
    2. Ждем статей))) А то без них очень тяжело разбираться.

      Удалить
    3. Читайте http://andmonahov.blogspot.ru/2013/02/opengl-es-20-5.html

      Удалить
  6. Андрей, такой вопрос , в методе onDrawFrame() передаю как униформу переменную времени:
    float time = System.currentTimeMillis();
    mShader.linkTime(time);
    но в шейдере время не обновляется. Как правильно передать время?

    ОтветитьУдалить
    Ответы
    1. Я не экстрасенс. Поэтому чтобы найти ошибку мне нужно видеть Ваш метод linkTime и код шейдера, в который передается time.

      Удалить
  7. Метод:
    public void linkTime (float time){
    GLES20.glUseProgram(program_Handle);
    int u_time_Handle=GLES20.glGetUniformLocation(program_Handle, "u_time");
    GLES20.glUniform1f(u_time_Handle, time);
    }
    Шейдер, умножаю на color для проверки:
    uniform mat4 u_modelViewProjectionMatrix;
    uniform float u_time;

    attribute vec3 a_vertex;
    attribute vec3 a_normal;
    attribute vec4 a_color;

    varying vec3 v_vertex;
    varying vec3 v_normal;
    varying vec4 v_color;

    void main(){
    v_vertex=a_vertex;
    vec3 n_normal=normalize(a_normal);
    v_normal=n_normal;
    v_color.x=a_color.x*u_time;
    v_color.y=a_color.y*u_time;
    v_color.zw=a_color.zw;
    gl_Position=u_modelViewProjectionMatrix*vec4(a_vertex,1.0);
    }

    ОтветитьУдалить
  8. v_color.x=a_color.x*u_time; v_color.y=a_color.y*u_time; :)))
    Цвет умножаем на системное время !!! Это же огромная величина, намного больше единицы. Яркость цвета в шейдере может меняться от 0 до 1. Все, что больше 1 обрезается до единицы.

    ОтветитьУдалить
  9. и тут я крякнул :) Да братан спасибо за труды, но паходу пошел я заново рисовать треугольники, а то чето нифига не понятно, вернусь к этому уроку позже, а за труды спасибо :)

    ОтветитьУдалить
    Ответы
    1. Если непонятно, нужно сначала почитать это http://andmonahov.blogspot.ru/2012/04/blog-post.html
      Статья про OpenGL ES 1.0, но принцип применения индексов берется отсюда.

      Удалить
  10. Хочу выразить Вам благодарность за уроки! Буду ждать новых!

    ОтветитьУдалить
  11. Андрей, великолепные уроки, очень понятно и подробно все изложено. Сначала вопрос по уроку: три лишних полигона обозначенных красным цветом вообще не будут рисоваться или все-таки будет что-то нарисовано (например линия для 9-4-9 или точка для 9-9-9), другими словами видеосистема встретив одинаковые индексы просто пропустит этот полигон или что-то сделает? И просьба о помощи: сейчас вот думаю как сделать тоже самое но для треугольной сетки. Как в этом случае лучше построить индекс ? (равносторонний треугольник разбитый на 4 таких же, каждый из которых тоже на 4 , и т д. сами вершины в массиве так же как и у вас идут подряд слева направо сверху вниз, но могу сделать как угодно по другому, лишь бы отрисовать все за один вызов )

    ОтветитьУдалить
    Ответы
    1. Для треугольников 9-4-9 и 9-9-9 не будет нарисовано ничего, т.к. правило GL_TRIANGLE_STRIP не рисует границы полигонов. Для треугольников попробуйте менять длину ленты.

      Удалить
  12. Добрый день,очень понравился ваш блог!а не могли бы свои уроки продублировать для IOS? я понимаю что основной упор на Open GL ES,а он везде одинаков,но все же в IOS есть свои подводные камни,а русскоязычной литературы НЕТ СОВСЕМ,в англоязычной обсуждаются только задолбавшие уже куб со сферой,и я еще не нашел примера КАК ЖЕ МНЕ ПОСТРОИТЬ плоскость??а по нажатию кнопки вывести на ней такую же "горку" как у вас в примере...заранее спасибо.

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