1 апреля 2012 г.

OpenGL ES 1. Основы рисования для начинающих

Основным изображаемым объектом в OpenGL является вершина. Вершина-это точка в трёхмерном пространстве. Вершина имеет ряд атрибутов. Главными атрибутами вершины являются ее координаты X, Y, Z. В OpenGL принято, что относительно экрана, на который проецируется изображение, ось X направлена слева направо, ось Y-снизу вверх, ось Z-из глубины экрана к его поверхности. По умолчанию точка с координатами X=0, Y=0, Z=0 находится в центре экрана. Другими атрибутами вершины являются цвет, вектор нормали и координаты текстуры.


Aтрибут координат вершины.

В отличие от "большого" OpenGL в "маленьком" OpenGL ES при рисовании многоугольников не существует возможности передачи в графический конвейер координат каждой точки отдельно. Команда glVertex не включена в данный API. Также отсутствует блок рисования glBegin...glEnd. Единственным способом передачи координат вершин в OpenGL ES являются массивы. Однако, массивы чисел с плавающей точкой в OpenGL ES напрямую тоже не передаются. OpenGL ES в качестве координат вершин принимает только найтивные буферы класса FloatBuffer. Поэтому мы должны предварительно записать координаты вершин в буфер, а затем передать этот буфер в OpenGL ES. На первом этапе главным является правильное вычисление размера буфера.
Например, нам требуется нарисовать треугольник. Каждой вершине треугольника присвоены три координаты как числа с плавающей точкой, каждой число с плавающей точкой занимает в памяти 4 байта. Итого, получаем 12 байт на точку. Всего для координат вершин треугольника, который имеет три точки, требуется буфер размером 36 байт. В нашем случае подготовка буфера для координат выглядит следующим образом:
FloatBuffer vertexBuffer;
ByteBuffer bb = ByteBuffer.allocateDirect(36);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();      
Приступим к заполнению буфера координатами вершин. Для этого служит метод put класса FloatBuffer. Рассмотрим это на примере прямоугольного треугольника:
// задаем координаты первой вершины треугольника
float x1=0;
float y1=0;
float z1=0;
//  задаем координаты второй вершины треугольника
float x2=1;
float y2=0;
float z2=0;
//  задаем координаты третьей вершины треугольника
float x3=0;
float y3=1;
float z3=0;
// поставим текущую позицию в буфере на начало
vertexBuffer.position(0);
// записываем в буфер координаты вершин треугольника
vertexBuffer.put(x1);
vertexBuffer.put(y1);
vertexBuffer.put(z1);
vertexBuffer.put(x2);
vertexBuffer.put(y2);
vertexBuffer.put(z2);
vertexBuffer.put(x3);
vertexBuffer.put(y3);
vertexBuffer.put(z3);
// снова ставим текущую позицию в буфере на начало
vertexBuffer.position(0);
Координаты передаются в буфер последовательно X,Y,Z первой точки, X,Y,Z второй точки, и.т.д. Кроме того, важно соблюдать порядок обхода точек. У треугольника есть две стороны - лицевая и обратная. Лицевой стороной считается та, у которой обход вершин выполнен против часовой стрелки. Обратной стороной та, у которой обход выполнен по часовой стрелке. По умолчанию лицевая сторона будет видна нам, а обратная-нет.
На практике я выяснил, что метод put является затратным с точки зрения времени выполнения. Чем реже используется метод put, тем быстрее выполняется отрисовка кадра.
Поэтому при большом количестве точек целесообразно сначала завести массив чисел, а потом одной командой put "загнать" его в буфер. Например, для треугольника это выглядит так:
float [] trianglecoord={0,0,0,  1,0,0,  0,1,0};
vertexBuffer.position(0);
vertexBuffer.put(trianglecoord);
vertexBuffer.position(0);
После подготовки буфера координат можно приступать к передаче его OpenGL ES.
Однако подготовительные мероприятия на этом не закончены. Мы должны сначала разрешить использование массивов вершин командой
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
Теперь начинаем передачу координат вершин OpenGL. Мы должны сообщить OpenGL, что в данный момент для рисования мы будем использовать именно наш буфер координат vertexBuffer, а не какой-нибудь другой. Для установки текущего буфера координат используется команда:
gl.glVertexPointer(3,GL10.GL_FLOAT,0,vertexBuffer);
Первый аргумент 3 - количество координат на вершину. Каждая вершина имеет три координаты X,Y,Z.
Второй аргумент GL10.GL_FLOAT указывает, что буфере заполнен числами с плавающей точкой.
Третий аргумент 0 - сдвиг в байтах между координатами одной вершины и координатами следующей вершины. Сдвиг использовать не будем, чтоб не усложнять статью.
Четвертый аргумент vertexBuffer - это наш буфер координат вершин.
Теперь координаты вершин переданы в  OpenGL.
Приступаем к отображению треугольника на экране:
gl.glDrawArrays(GL10.GL_TRIANGLES,0,3); 
Первый аргумент этой команды GL10.GL_TRIANGLES сообщает OpenGL, что нужно соединить все три точки в треугольник и отобразить его со сплошной заливкой цветами вершин.
Второй аргумент 0 - это индекс начального элемента.
Третий аргумент 3 - количество индексов, т.е. количество отображаемых точек. У треугольника их три.     

Треугольник является базовой фигурой для рисования поверхностей в OpenGL. Почему именно треугольник ? Потому что все три точки треугольника лежат в одной плоскости. Для расчета освещения нам понадобится вычислить нормаль к поверхности. Для всех точек, лежащих в одной плоскости, нормаль всегда одинакова и определяется однозначно. Если же взять за основу  четырехугольник, то совсем не обязательно, что все четыре точки будут находиться в одной плоскости. Поэтому в общем случае нормаль для четырехугольника не определяется однозначно. Однако, всегда можно вычислить четвертую точку исходя из первых трёх, чтобы все четыре точки лежали в одной плоскости. 
Из треугольников можно составить любую поверхность если сделать эти треугольники достаточно маленькими, чтобы поверхность отображалась ровной. Даже если поверхность при выводе на экран кажется ступенчатым многогранником можно привести её к ровному виду если включить режим  сглаживания цветов командой glShadeModel(GL10.GL_SMOOTH). При этом все же нужно обеспечить на поверхности достаточное количество треугольников, иначе изображение будет размыто и потеряет четкость.

Как составить поверхность из треугольников ? Вернемся к описанию команды glDrawArrays.
При использование аргумента GL10.GL_TRIANGLES каждая тройка точек рисуется как отдельный треугольник. Например, если у нас имеется буфер координат из 9 точек, пронумерованных от 0 до 8, то при выполнении команды glDrawArrays(GL10.GL_TRIANGLES,0,9) будут нарисованы три треугольника: первый - из точек 0-1-2, второй - из точек 3-4-5, третий-из точек 6-7-8.

Если вместо GL10.GL_TRIANGLES использовать аргумент GL10.GL_TRIANGLE_STRIP будет нарисована сплошная лента из связанных треугольников. При этом порядок рисования треугольников будет такой - сначала рисуется треугольник из точек 0-1-2, затем 2-1-3, затем 2-3-4, затем 4-3-5 и.т.д.
Порядок обхода вершин при использовании GL_TRIANGLE_STRIP
При использовании аргумента GL10.GL_TRIANGLE_FAN получаем веер треугольников с общей точкой 0. Порядок рисования при этом изменится: сначала рисуется треугольник с вершинами 0-1-2, затем 0-2-3, затем 0-3-4 и.т.д.

Порядок обхода вершин при использовании GL_TRIANGLE_FAN

С помощью комбинаций GL_TRIANGLE_STRIP и GL_TRIANGLE_FAN можно построить довольно сложную поверхность. Например, поверхность земного шара в виде сферы :) На полюсах можно построить веер, а параллели изобразить виде лент.

Цвет как атрибут вершины.

Цвет имеет четыре компоненты-красную, зеленую и синюю и альфа. Яркость каждой компоненты можно регулировать в диапазоне от 0 до 1. Всё многообразие цветов можно представить в виде комбинаций красной, зелёной и синей компоненты различной яркости. Последнюю четвертую компоненту цвета называют Альфой. Альфа - это степень непрозрачности цвета. Если Альфа=0 цвет считается абсолютно прозрачным, т.е. невидимым. Если Альфа=1 цвет считается абсолютно непрозрачным. Цвет-это свойство вершины. Как передать цвет вершин в OpenGL ? С помощью тех же буферов. Сначала подготовим буфер для цвета. Каждая вершина имеет четыре значения цвета как числа с плавающей точкой. Каждое число с плавающей точкой занимает в памяти 4 байта. Таким образом, цвет одной вершины занимает в памяти  16 байт. Тогда для треугольника нужно создать буфер размером 16*3=48 байт.
FloatBuffer colorBuffer;     
ByteBuffer bb = ByteBuffer.allocateDirect(48);
bb.order(ByteOrder.nativeOrder());
colorBuffer = bb.asFloatBuffer();

Запись цветов в буфер производится в следующем порядке: сначала красная компонента, затем зеленая, затем синяя и в конце альфа. Например, для треугольника выглядит так:
// цвет первой вершины - красный
float red1=1;
float green1=0;
float blue1=0;
float alpha1=1;
// цвет второй вершины - зелёный
float red2=0;
float green2=1;
float blue2=0;
float alpha2=1;
// цвет третьей вершины - синий
float red3=0;
float green3=0;
float blue3=1;
float alpha3=1;
// поставим текущую позицию в буфере на начало
colorBuffer.position(0);
// записываем в буфер цвета первой вершины
colorBuffer.put(red1);
colorBuffer.put(green1);
colorBuffer.put(blue1);
colorBuffer.put(alpha1);
// записываем в буфер цвета второй вершины  
colorBuffer.put(red2);
colorBuffer.put(green2);
colorBuffer.put(blue2);
colorBuffer.put(alpha2);
// записываем в буфер цвета третьей вершины  
colorBuffer.put(red3);
colorBuffer.put(green3);
colorBuffer.put(blue3);
colorBuffer.put(alpha3);
// снова поставим текущую позицию в буфере на начало
colorBuffer.position(0);
Как альтернативу, можно сначала составить массив цветов треугольника, а затем записать массив в буфер:
float [] colorArray={1,0,0,1,  0,1,0,1,  0,0,1,1};
colorBuffer.position(0);
colorBuffer.put(colorArray);
colorBuffer.position(0);
Буфер цветов готов. Далее мы должны разрешить использование массивов цветов командой
gl.glEnableClientState(GL10.GL_COLOR_ARRAY) и передать подготовленный буфер в OpenGL. Для передачи буфера в OpenGL служит команда glColorPointer:
gl.glColorPointer(4,GL10.GL_FLOAT,0,colorBuffer);
Первый аргумент 4 - количество цветов на вершину. Каждая вершина имеет четыре компоненты цвета (красный, зеленый, синий и альфа).
Второй аргумент GL10.GL_FLOAT указывает, что буфере заполнен числами с плавающей точкой.
Третий аргумент 0 - сдвиг в байтах между цветами одной вершины и цветами следующей вершины.
Четвертый аргумент  colorBuffer  - это наш буфер цветов.
Теперь цвета вершин переданы в OpenGL.
Когда мы вызовем команду рисования glDrawArrays на экране появится треугольник, закрашенный цветами вершин. При этом, если включен режим сглаживания командой glShadeModel(GL10.GL_SMOOTH), цвета в треугольнике будут плавно изменяться при переходе от одной вершины к другой (эффект радуги).


Координаты вектора нормали.

Что такое вектор нормали ? Это вектор единичной длины, перпендикулярный к поверхности в данной точке этой поверхности. Вектор нормали, как и всякий другой вектор имеет три проекции на оси координат X,Y,Z, которые называют координатами вектора. Координаты вектора нормали присваиваются каждой вершине отдельно и являются её атрибутом. Вектор нормали очень важен для правильного расчета освещения поверхности, т.к. определяет направление отражённого света. Как можно рассчитать вектор нормали ? Для расчета  нормали необходимо иметь три точки на поверхности. Из трёх точек можно составить два вектора, которые всегда будут лежать в одной плоскости. Проведем из точки 1 в точку 2 вектор A, из точки 1 в точку 3 вектор B. Вектор N, перпендикулярный A и B рассчитывается как векторное произведение N=[AxB]:
Nx=Ay*Bz-By*Az=(y2-y1)*(z3-z1)-(y3-y1)*(z2-z1)
Ny=Bx*Az-Ax*Bz=(x3-x1)*(z2-z1)-(x2-x1)*(z3-z1)
Nz=Ax*By-Bx*Ay=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1)
Далее нужно вычислить длину вектора N и его координаты Nx,Ny,Nz поделить на длину, т.е. нормализовать вектор N. Однако мы не будем заниматься нормализацией, т.к. при этом требуется вычисление квадратного корня, а это операция затратная по времени исполнения.
За нас нормализацию выполнит OpenGL. Для этого достаточно выполнить команду glEnable(GL10.GL_NORMALIZE), т.е. разрешить автоматическую нормализацию внутри графического конвейера. 
Предположим, что у нас имеется треугольник, состоящий из вершин с координатами  x1,y1,z1  первой вершины, x2,y2,z2-второй вершины, x3,y3,z3-третьей вершиныПрисвоение нормалей вершинам производится аналогично координатам точек. Сначала создадим для треугольника буфер необходимой длины:
FloatBuffer normalBuffer;
ByteBuffer bb = ByteBuffer.allocateDirect(36);
bb.order(ByteOrder.nativeOrder());
normalBuffer = bb.asFloatBuffer();

Затем вычислим координаты вектора нормали исходя из координат точек треугольника:
float nx=(y2-y1)*(z3-z1)-(y3-y1)*(z2-z1);
float ny=(x3-x1)*(z2-z1)-(x2-x1)*(z3-z1);
float nz=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1);
Запишем  координаты вектора нормали в буфер. Поскольку нормаль является одинаковой для всех вершин треугольника повторим запись в буфер три раза:
normalBuffer.position(0);
for (int i=1;i<4;i++){
          normalBuffer.put(nx);
          normalBuffer.put(ny);
          normalBuffer.put(nz);
}
normalBuffer.position(0);
Далее включаем использование массивов нормалей:
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
И передаем наш буфер нормалей normalBuffer в OpenGL:
gl.glNormalPointer(GL10.GL_FLOAT,0,normalBuffer);

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

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

  1. Спасибо! Очень полезная статья!

    ОтветитьУдалить
  2. А как быть, если для каждого треугольника есть не одна нормаль, а три, т.е. для каждой вершины есть сглаживающая нормаль. Как массив сглаживающих нормалей передать видяхе?

    ОтветитьУдалить
  3. Каждой вершине присвоен индивидуальный вектор нормали. Т.е. мы имеем массив нормалей для точек поверхности. Перепишем массив в буфер normalBuffer, а затем передадим в видюху gl.glNormalPointer(GL10.GL_FLOAT,0,normalBuffer);

    ОтветитьУдалить
    Ответы
    1. Одна и та же вершина может быть у нескольких полигонов, не лежащих в одной плоскости, как тогда быть?

      Удалить
    2. Складываем векторы нормалей смежных полигонов и делим на их количество, т.е. вычисляем среднюю нормаль.

      Удалить
  4. Какой-то геморрой этот OpenGL ES, так сложно делаются такие простые вещи...

    ОтветитьУдалить
    Ответы
    1. Простые и сложные вещи делаются одинаково. OpenGL ES - это набор команд для управления видеокартой.

      Удалить
  5. В каком классе должен находиться этот код?

    ОтветитьУдалить
  6. Спасибо большое, очень помогло :)
    Буду дальше изучать ваши статьи :)

    ОтветитьУдалить
  7. Спасибо за статью, мне очень надо отрисовать траекторию ракеты в движении, уперся в то что шейдеру надо знать размер буффера а у меня он должен увеличиваться каждую секунду (полетного времени) , glBegin glEnd нет так бы в цикле по таймеру отрисовывал а в ES буксую.
    На большой OpenGL перейти не могу т.к. в qt 5 только ES.
    Заранее благодарствую

    ОтветитьУдалить
  8. Андрей, подскажите почему FloatBuffer приходится получать через ByteBuffer? Только для того, чтобы задать направление (я так понял в FloatBuffer этого сделать нельзя)?

    Спасибо за статьи. Наконец начал понимать что к чему.

    ОтветитьУдалить
  9. Эта тема актуальна с OpenGL ES 2.0 ?

    ОтветитьУдалить
  10. Тем, кто также как и я - собрал весь код с этого и прошлого уроков,
    и вместо треугольника получил черный экран.

    Предлагаю:
    1. Отключить перспективу, закоментировав строчку:
    GLU.gluPerspective (gl, 60, ratio, 0.1f, 100f);

    ИЛИ прописать в вершинах треугольника z=-1 т.е.:

    float z1=-1;
    float z2=-1;
    float z3=-1;

    2. Также для отображения корректных цветов отключить освещение,
    закоментировав строчку:
    gl.glEnable(GL10.GL_LIGHTING);

    Автору респект.
    Про метод put вообще основательно изложено
    Такие вещи часто забываются, а то потом - сидишь и думаешь куда уходят ресурсы?...

    Спасибо.

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