Основным изображаемым объектом в OpenGL является вершина. Вершина-это точка в трёхмерном пространстве. Вершина имеет ряд атрибутов. Главными атрибутами вершины являются ее координаты X, Y, Z. В OpenGL принято, что относительно экрана, на который проецируется изображение, ось X направлена слева направо, ось Y-снизу вверх, ось Z-из глубины экрана к его поверхности. По умолчанию точка с координатами X=0, Y=0, Z=0 находится в центре экрана. Другими атрибутами вершины являются цвет, вектор нормали и координаты текстуры.
Aтрибут координат вершины.
Например, нам требуется нарисовать треугольник. Каждой вершине треугольника присвоены три координаты как числа с плавающей точкой, каждой число с плавающей точкой занимает в памяти 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 |
Порядок обхода вершин при использовании GL_TRIANGLE_FAN |
С помощью комбинаций GL_TRIANGLE_STRIP и GL_TRIANGLE_FAN можно построить довольно сложную поверхность. Например, поверхность земного шара в виде сферы :) На полюсах можно построить веер, а параллели изобразить виде лент.
Цвет как атрибут вершины.
FloatBuffer colorBuffer;
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;
// поставим текущую позицию в буфере на начало
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);
colorBuffer.position(0);
// записываем в буфер цвета первой вершины
colorBuffer.put(red1);
colorBuffer.put(green1);
colorBuffer.put(blue1);
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(red2);
colorBuffer.put(green2);
colorBuffer.put(blue2);
colorBuffer.put(alpha2);
// записываем в буфер цвета третьей вершины
colorBuffer.put(red3);
colorBuffer.put(green3);
colorBuffer.put(blue3);
colorBuffer.put(alpha3);
colorBuffer.put(red3);
colorBuffer.put(green3);
colorBuffer.put(blue3);
colorBuffer.put(alpha3);
// снова поставим текущую позицию в буфере на начало
colorBuffer.position(0);
Как альтернативу, можно сначала составить массив цветов треугольника, а затем записать массив в буфер:
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.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), цвета в треугольнике будут плавно изменяться при переходе от одной вершины к другой (эффект радуги).
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)
FloatBuffer normalBuffer;
ByteBuffer bb = ByteBuffer.allocateDirect(36);
bb.order(ByteOrder.nativeOrder());
normalBuffer = bb.asFloatBuffer();
float ny=(x3-x1)*(z2-z1)-(x2-x1)*(z3-z1);
float nz=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1);
Второй аргумент 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-третьей вершины. Присвоение нормалей вершинам производится аналогично координатам точек. Сначала создадим для треугольника буфер необходимой длины:
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);
При рисовании полигонов нужно обязательно соблюдать правило: порядок обхода вершин при записи в буферы должен быть одинаков для координат вершин, цветов, векторов нормалей и координат текстур.
Как уже отмечалось, вектор нормали нужен для правильного расчета освещения поверхности. Освещение, а также текстуры будут рассмотрены отдельно в следующих статьях.
Спасибо! Очень полезная статья!
ОтветитьУдалитьА как быть, если для каждого треугольника есть не одна нормаль, а три, т.е. для каждой вершины есть сглаживающая нормаль. Как массив сглаживающих нормалей передать видяхе?
ОтветитьУдалитьКаждой вершине присвоен индивидуальный вектор нормали. Т.е. мы имеем массив нормалей для точек поверхности. Перепишем массив в буфер normalBuffer, а затем передадим в видюху gl.glNormalPointer(GL10.GL_FLOAT,0,normalBuffer);
ОтветитьУдалитьОдна и та же вершина может быть у нескольких полигонов, не лежащих в одной плоскости, как тогда быть?
УдалитьСкладываем векторы нормалей смежных полигонов и делим на их количество, т.е. вычисляем среднюю нормаль.
УдалитьКакой-то геморрой этот OpenGL ES, так сложно делаются такие простые вещи...
ОтветитьУдалитьПростые и сложные вещи делаются одинаково. OpenGL ES - это набор команд для управления видеокартой.
УдалитьВ каком классе должен находиться этот код?
ОтветитьУдалитьВ классе рендерера
УдалитьСпасибо большое, очень помогло :)
ОтветитьУдалитьБуду дальше изучать ваши статьи :)
Спасибо за статью, мне очень надо отрисовать траекторию ракеты в движении, уперся в то что шейдеру надо знать размер буффера а у меня он должен увеличиваться каждую секунду (полетного времени) , glBegin glEnd нет так бы в цикле по таймеру отрисовывал а в ES буксую.
ОтветитьУдалитьНа большой OpenGL перейти не могу т.к. в qt 5 только ES.
Заранее благодарствую
Андрей, подскажите почему FloatBuffer приходится получать через ByteBuffer? Только для того, чтобы задать направление (я так понял в FloatBuffer этого сделать нельзя)?
ОтветитьУдалитьСпасибо за статьи. Наконец начал понимать что к чему.
Эта тема актуальна с OpenGL ES 2.0 ?
ОтветитьУдалитьТем, кто также как и я - собрал весь код с этого и прошлого уроков,
ОтветитьУдалитьи вместо треугольника получил черный экран.
Предлагаю:
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 вообще основательно изложено
Такие вещи часто забываются, а то потом - сидишь и думаешь куда уходят ресурсы?...
Спасибо.