Существуют 3 типа основных графических операций : очистка экрана ,
рисование геометрических обьектов , рисование растеризованных обьектов (текстур) .
Очистка RGBA mode window в черный цвет :
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
Чтобы очистить одновременно 2 видеобуфера в режиме RGBA - color buffer и depth buffer - нужно :
glClearColor(0.0, 0.0, 0.0, 0.0);
glClearDepth(1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Вообще-то буферов больше - это :
Color buffer
Depth buffer
Accumulation buffer
Stencil buffer
Разница между
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
и
glClear(GL_COLOR_BUFFER_BIT);
glClear(GL_DEPTH_BUFFER_BIT);
в том , что первый вариант очищает быстрее .
Цвет
Перед началом прорисовки в OpenGL нужно обязательно установить т.н. coloring scheme .
Следующий псевдокод :
set_current_color(red);
draw_object(A);
draw_object(B);
set_current_color(green);
set_current_color(blue);
draw_object(C);
нарисует обьекты А и В красными , а С - синим .
Цвет устанавливается командой glColor3f(), которая имеет три float-параметра в диапазоне
от 0 до 1 - red, green, blue - компоненты .
Вот таблица для основных цветов :
glColor3f(0.0, 0.0, 0.0); black
glColor3f(1.0, 0.0, 0.0); red
glColor3f(0.0, 1.0, 0.0); green
glColor3f(1.0, 1.0, 0.0); yellow
glColor3f(0.0, 0.0, 1.0); blue
glColor3f(1.0, 0.0, 1.0); magenta
glColor3f(0.0, 1.0, 1.0); cyan
glColor3f(1.0, 1.0, 1.0); white
Команда glFlush() - она должна присутствовать в цикле прорисовки после каждого фрейма .
glFinish() - аналогичная команда , которая к тому же может привести к некоторому
замедлению . Но glFlush() все же предпочтительнее .
Графические примитивы
Point - в OpenGL ее еще называют vertex . Имеет 3 координаты - x,y,z .
OpenGL работает в так называемых гомогенных(приведенных) координатах ,
поэтому у точки имеется 4-й дополнительный параметр - (x,y,z,w) .
Если w не равно нулю , фактически мы имеем дело с координатой (x/w,y/w,z/w).
Line - это линейный сегмент .
Polygon - закрытая область , определенная с помощью vertices . Ребра в полигоне
не должны пересекаться . Полигон также должен быть выпуклым . На число сегментов
ограничений нет .
Rectangles - этот графический примитив прорисовывается командой glRect*().
Все геометрические обьекты определяются в OpenGL с помощью набора вершин .
Вершина задается командой glVertex*() .
glVertex2s(2, 3);
glVertex3d(0.0, 0.0, 3.1415926535898);
glVertex4f(2.3, 1.0, -2.2, 2.0);
GLdouble dvect[3] = {5.0, 9.0, 1992.0};
glVertex3dv(dvect);
Все эти команды представляют 3-мерные вершины в различных форматах .
Последняя строка вроде как наиболее эффективная по скорости , поскольку имеет
единственный параметр .
Теперь давайте нарисуем полигон из 5 вершин :
glBegin(GL_POLYGON);
glVertex2f(0.0, 0.0);
glVertex2f(0.0, 3.0);
glVertex2f(4.0, 3.0);
glVertex2f(6.0, 1.5);
glVertex2f(4.0, 0.0);
glEnd();
В качестве параметра в функцию можно передавать следующие графические примитивы :
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
GL_QUADS
GL_QUAD_STRIP
GL_POLYGON .
Корректно работают в цикле glBegin()... glEnd() следующие команды :
glVertex*()
glColor*()
glIndex*()
glNormal*()
glTexCoord*()
glEdgeFlag*()
glMaterial*()
glArrayElement()
glEvalCoord*()
glEvalPoint*()
glCallList()
glCallLists()
Пример прорисовки круга :
#define PI 3.1415926535898
GLint circle_points = 100;
glBegin(GL_LINE_LOOP);
for (i = 0; i < circle_points; i++) {
angle = 2*PI*i/circle_points;
glVertex2f(cos(angle), sin(angle));
}
glEnd();
С точки зрения OpenGL-оптимизации - это бездарный код . Необходимо , конечно , предварительно
вычислить массив и потом делать на него ссылку в цикле glBegin()... glEnd() .
Каждый полигон имеет 2 стороны - front и back . Это позволяет правильно рисовать обьекты ,
имеющие внутренние невидимые полости .
Функция glPolygonMode() - контролирует прорисовку полигонов :
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE).
Полигоны , у которых вершины появляются в порядке против часовой стрелки , называются
front-facing. Функция glFrontFace() контролирует этот процесс.
Команда glCullFace() задает порядок прорисовки - front или back , выполняется
совместно с командой glEnable() .
Если рассматривать плоскость , то нормальный вектор к ней буден един для всех точек плоскости .
OpenGL позволяет определить нормаль для каждого полигона или вершины .
Нормаль определяет ориентацию полигона , например , относительно источника света .
Нормаль определяет , сколько света падает на вершину .
Нормаль устанавливается командой glNormal*() . Для каждой вершины она , как правило , различна .
glBegin (GL_POLYGON);
glNormal3fv(n0);
glVertex3fv(v0);
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glEnd();
Как известно , при вычислении ротации или трансформации вектор нужно нормализовать ,
для этого команда glEnable() выполняется с параметром GL_NORMALIZE .
Vertex arrays
Возьмем ситуацию когда нужно прорисовать полигон с 20 вершинами . По обычной схеме ,
в цикле между командами glBegin() - glEnd() нужно поставить как минимум 20 команд , по одной
на каждую вершину . Многовато что-то . К тому же другая известная проблема - это избыточность
обработки смежных вершин . По-хорошему , нужно 20 вершин положить в массив , а 20 нормалей
положить в другой массив и сделать по одному вызову функции к каждому из массивов .
При использвании vertex arrays нужно делать следующее :
1. Инициализируем 6 (!) массивов :
1. для вершин
2. для RGBA colors
3. для color indices
4. для surface normals
5. для texture coordinates
6. для polygon edge flags.
2. Проинициализировать массивы и установить указатели на них .
3. Можно рисовать ...
На практике инициализация vertex arrays выполняется с помощью glEnableClientState() .
Из 6 названных массивов , как правило , создаются 4 (без 2 и 3) .
2 команды для нормалей и вершин :
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);
Указатели на массивы создают команды :
1. glVertexPointer() - размерность (2,3,4)
2. glColorPointer() - размерность (3,4)
3. glIndexPointer() - размерность (1)
4. glNormalPointer() - размерность (3)
5. glTexCoordPointer() - размерность (1,2,3,4)
6. glEdgeFlagPointer() - размерность (1)
Пример: Здесь создаются 2 массива - один для вершин размерностью 2*6 ,
другой - для цвета размерности 3*6 :
static GLint vertices[] = {25, 25,
100, 325,
175, 25,
175, 325,
250, 25,
325, 325};
static GLfloat colors[] = {1.0, 0.2, 0.2,
0.2, 0.2, 1.0,
0.8, 1.0, 0.2,
0.75, 0.75, 0.75,
0.35, 0.35, 0.35,
0.5, 0.5, 0.5};
glEnableClientState (GL_COLOR_ARRAY);
glEnableClientState (GL_VERTEX_ARRAY);
glColorPointer (3, GL_FLOAT, 0, colors);
glVertexPointer (2, GL_INT, 0, vertices);
В OpenGL есть такое понятие - stride , с помощью которых несколько массивов можно
упаковать в один , как в данном случае - массив для 6 вершин имеет доп. информацию о цвете :
static GLfloat intertwined[] =
{1.0, 0.2, 1.0, 100.0, 100.0, 0.0,
1.0, 0.2, 0.2, 0.0, 200.0, 0.0,
0.2, 0.2, 1.0, 200.0, 100.0, 0.0};
Чтобы выборочно прочитать данные с такого массива , можно выбрать только цвет :
glColorPointer (3, GL_FLOAT, 6 * sizeof(GLfloat), intertwined);
Для выборки вершин из этого массива :
glVertexPointer(3, GL_FLOAT,6*sizeof(GLfloat), &intertwined[3]) .
Для получения информации о единственном элементе массива есть команда glArrayElement() .
Пример :
glBegin(GL_TRIANGLES);
glArrayElement (2);
glArrayElement (3);
glArrayElement (5);
glEnd();
Этот кусок эквивалентен следующему :
glBegin(GL_TRIANGLES);
glColor3fv(colors+(2*3*sizeof(GLfloat));
glVertex3fv(vertices+(2*2*sizeof(GLint));
glColor3fv(colors+(3*3*sizeof(GLfloat));
glVertex3fv(vertices+(3*2*sizeof(GLint));
glColor3fv(colors+(5*3*sizeof(GLfloat));
glVertex3fv(vertices+(5*2*sizeof(GLint));
glEnd();
Команда glDrawElements() прорисовывает примитивы . Следующий код делает рендер кубика ,
при этом вершины каждой грани должны быть положены в массив индексов :
static GLubyte frontIndices = {4, 5, 6, 7};
static GLubyte rightIndices = {1, 2, 6, 5};
static GLubyte bottomIndices = {0, 1, 5, 4};
static GLubyte backIndices = {0, 3, 2, 1};
static GLubyte leftIndices = {0, 4, 7, 3};
static GLubyte topIndices = {2, 3, 7, 6};
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, frontIndices);
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, rightIndices);
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, bottomIndices);
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, backIndices);
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, leftIndices);
glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, topIndices);
Другой вариант - все индексы свалить в кучу - что будет покруче :
static GLubyte allIndices = {4, 5, 6, 7, 1, 2, 6, 5,
0, 1, 5, 4, 0, 3, 2, 1,
0, 4, 7, 3, 2, 3, 7, 6};
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, allIndices);
Тут мы не используем цикл glBegin()/glEnd() .
Также имеется команда glInterleavedArrays() для продвинутого использования упакованных массивов
с кучей переменных булевского типа .
В OpenGL имеется механизм сохранения и восстановления данных . Имеются 2 стека :
attribute stack и client attribute stack , доступ к которым возможен по командам
glPushClientAttrib() , glPopClientAttrib() , glPushAttrib() , glPopAttrib() .
Хинты .
1. Все полигоны должны быть ориентированы по вершинам либо по часовой стрелке , либо против .
2. При разбиении поверхности в качестве единицы старайтесь использовать треугольный полигон ,
три вершины которого всегда лежат в одной плоскости .
3. Всегда приходится выбирать золотую середину между количеством треугольников ,
на которые мы разбиваем поверхность обьекта , и качеством изображения этого обьекта .
Если полигон далеко , то количество может быть небольшим . Чем более плоская
поверхность , тем меньше требуется полигонов .
4. Для более высокого качества изображения имеет смысл производить более детальное
разбиение ближе к граням обьекта . Если vector dot между нормальным вектором
вершины полигона и нормальным вектором поверхности близок или равен нулю - это
верный признак того , что прилегающую грань нужно разбивать более детально для
более рельефной прорисовки .
Рассмотрим пример создания икосаедра . Его можно принять как грубое приближение
к сфере .
#define X .525731112119133606
#define Z .850650808352039932
static GLfloat vdata[12][3] = {
{-X, 0.0, Z}, {X, 0.0, Z}, {-X, 0.0, -Z}, {X, 0.о0, -Z},
{0.0, Z, X}, {0.0, Z, -X}, {0.0, -Z, X}, {0.0, -Z, -X},
{Z, X, 0.0}, {-Z, X, 0.0}, {Z, -X, 0.0}, {-Z, -X, 0.0}
};
static GLuint tindices[20][3] = {
{0,4,1}, {0,9,4}, {9,5,4}, {4,5,8}, {4,8,1},
{8,10,1}, {8,3,10}, {5,3,8}, {5,2,3}, {2,7,3},
{7,10,3}, {7,6,10}, {7,11,6}, {11,0,6}, {0,1,6},
{6,1,10}, {9,0,11}, {9,11,2}, {9,2,5}, {7,2,11} };
int i;
glBegin(GL_TRIANGLES);
for (i = 0; i < 20; i++) {
/* color information here */
glVertex3fv(&vdata[tindices[i][0]][0]);
glVertex3fv(&vdata[tindices[i][1]][0]);
glVertex3fv(&vdata[tindices[i][2]][0]);
}
glEnd();
Константы X и Z выбраны для того , чтобы расстояние от центра икосаедра до каждой вершины
было одинаковым . Координаты 12 вершин определены в массиве vdata[][] . В данном куске
кода нет цветовой информации о раскраске каждой грани , поэтому образ на экране получается
не совсем реальным . Для этого надобно вычислить нормаль к каждой поверхности и уже на основе
этого выбирать цвет .
Для этого нужно подсчитать т.н. cross product . Для икосаедра это выглядит так :
GLfloat d1[3], d2[3], norm[3];
for (j = 0; j < 3; j++) {
d1[j] = vdata[tindices[i][0]][j] - vdata[tindices[i][1]][j];
d2[j] = vdata[tindices[i][1]][j] - vdata[tindices[i][2]][j];
}
normcrossprod(d1, d2, norm);
glNormal3fv(norm);
В общем случае вычисление нормали :
void normalize(float v[3]) {
GLfloat d = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
if (d == 0.0) {
error("zero length vector");
return;
}
v[0] /= d; v[1] /= d; v[2] /= d;
}
void normcrossprod(float v1[3], float v2[3], float out[3])
{
GLint i, j;
GLfloat length;
out[0] = v1[1]*v2[2] - v1[2]*v2[1];
out[1] = v1[2]*v2[0] - v1[0]*v2[2];
out[2] = v1[0]*v2[1] - v1[1]*v2[0];
normalize(out);
}
Следующий пример показывает , как сделать аппроксимацию икосаедра , приблизив его
поверхность к сферической . Поверхность разбивается на 80 треугольников :
void drawtriangle(float *v1, float *v2, float *v3)
{
glBegin(GL_TRIANGLES);
glNormal3fv(v1); vlVertex3fv(v1);
glNormal3fv(v2); vlVertex3fv(v2);
glNormal3fv(v3); vlVertex3fv(v3);
glEnd();
}
void subdivide(float *v1, float *v2, float *v3)
{
GLfloat v12[3], v23[3], v31[3];
GLint i;
for (i = 0; i < 3; i++) {
v12[i] = v1[i]+v2[i];
v23[i] = v2[i]+v3[i];
v31[i] = v3[i]+v1[i];
}
normalize(v12);
normalize(v23);
normalize(v31);
drawtriangle(v1, v12, v31);
drawtriangle(v2, v23, v12);
drawtriangle(v3, v31, v23);
drawtriangle(v12, v23, v31);
}
for (i = 0; i < 20; i++) {
subdivide(&vdata[tindices[i][0]][0],
&vdata[tindices[i][1]][0],
&vdata[tindices[i][2]][0]);
}
|