Введение в OpenGL

Начало|Подключаем OpenGL|
Основные команды-рисуем примитивы|
Операции с матрицами|Свет и материалы.|Quadric-объекты









-->Начало: о чём это всё и зачем нужно.

На сегодняшний день существуют две достаточно продвинутые библиотеки для работы с 3D-графикой - OpenGL и Direct3D. После появления Direct3D 8 господство OpenGL несколько пошатнулось, однако с её использованием до сих пор пишутся разные продвинутые игрухи, например, Quake III. Сейчас я попытаюсь рассказать что-нибудь про OpenGL. Данное руководство не подлежит осуждению по поводу малых подробностей - это всего лишь введение, не претендующее на доскональность документации. Цель этого руководства - дать читателю хоть какое-то понятие о библиотеки в короткий срок.

OpenGL - обыкновенная DLL библоитека, находящаяся по адресу % Systemroot%/system32/opengl32.dll, а также надстройка к ней % Systemroot%/system32/glu32.dll. Существуют, конечно, не Microsoft-овские версии OpenGL, опережающие стандартную по некоторым скоростным характеристикам, но я их сейчас обсуждать не буду. Напомню, что существуют два способа загрузки Dll-библиотеки - явная загрузка через функцию LoadLibrary (заголовочный файл Windows.pas), позволяющий контролировать загрузку библоитеки и загружать её по пожеланию пользователя, и загрузка вместе с программой путем дописывания (в Delphi) ключевого слова external после прототипа неодходимой функции. Выглядит это так:

uses ...
//Подключили необходимые модули
type ...
//Описали типы
var
//Переменные
implementation

{$R *.dfm}

procedure glBegin(); external opengl32.dll;

...

end.

Однако, экспортировать так все необходимые функции довольно утомительно, поэтому все нормальные люди пользуются стандартными заголовочными файлами, к сожалению, до сих не свободными от ошибок. Существуют несколько популярных заголовочних файлов - gl, glut, glaux, все они переведены на Delphi. Я буду использовать модуль OpenGL.pas (GL.pas). Файл DGLUT.pas можно стырить здесь. Файл GLUT особенно удобно использовать при программировании на C/C++, на Delphi менее, более того, на Delphi 7 он даже не компилится (очередной баг Delphi 7). Однако, пора увидеть OpenGL в действии.

-->Подключаем OpenGL

Как правило, программисты игр и прочих программ, использующих OpenGL, отказываются от использования VCL. Однако нашей целью является освоение OpenGL, следовательно, попользуемся VCL. OpenGL в целом достаточно неплохо уживается со стандартными компонентами, однако ставить кнопки и другие контролы, а также дочерние окна, поверх окна OpenGL не рекомендую - иногда не пашет. Итак, создайте новое приложение и добавьте в раздел uses модуль OpenGL. В раздел private формы добавьте поля "DC:HDC;" и "hrc : HGLRC;". HDC - это тип контекста устройства в Windows. Он имеется у любого устройства вывода. В данном случае окно выводит информацию на экран. В VCL контекстом устройства НDС является хэндл Canvas.Handle. Тип HGLRC - контекст рендеринга OpenGL - внутренний тип библиотеки OpenGL. Это указатель на структуру, содержащую необходимые данные для данного устройства. Создайте новую функцию для установки формата пиксела - SetDCPixelFormat. Код её будет выглядеть так(MainForm - имя формы):

function TMainForm.SetDCPixelFormat():Boolean;
var pfd : TPixelFormatDescriptor;
nPixelFormat : Cardinal;
begin
FillChar(pfd, SizeOf(pfd),0);//Заполним нулями, OpenGL всё заполнит сама
pfd.dwFlags := PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER;
nPixelFormat := ChoosePixelFormat(DC,@pfd);//Заполняем структуру
if nPixelFormat=0 then begin
Result := false;
exit;
end;

SetPixelFormat(DC,nPixelFormat,@pfd);
end;

Можно заполнять структуру и вручную, но вышеуказанный метод работает почти на любой машине, тогда как вручную заполненная структура может на заработать. В некоторых отдельных случаях надо вручную устанавливать значания некоторых полей, например, "pfd.cStencilBits:=64;"(чтобы проэнэйблить буфер трафарета). В конструкторе Create(); окна необходимо инициализировать OpenGL:

procedure TMainForm.FormCreate();
begin
DC := Canvas.Handle;
if not SetDCPixelFormat() then begin
Application.MessageBox('Не удаётся подключить OpenGL!');
SendMessage(Handle, WM_DESTROY, 0, 0);
exit;
end;
hrc:=wglCreateContext(DC);//Создаём контекст рендеринга
wglMakeCurrent(DC, hrc);//Активируем контекст рендеринга
glMatrixMode(GL_PROJECTION);//Работаем с матрицей проекции
glViewPort(0,0,ClientWidth-1,ClientHeight-1);//Устанавливаем область вывода
glClearColor(1,1,1,1);//цвет фона end;

Надо ещё не забыть завершить работу с OpenGL (в данном примере по завершении работы приложения):

procedure TMainForm.FormDestroy();
begin
wglMakeCurrent(0,0);//Освобождаем контекст
wglDeleteContext(hrc);//Уничтожаем контекст рендеринга OpenGL
ReleaseDC(Handle,DC);
DeleteDC(DC);
end;

Это обязано работать. Можно считать, что мы умеем подключать OpenGL. Осталось научиться рисовать. Процедура "glViewport (x,y: GLint; width, height: GLsizei);" задаёт область воспроизведения, однако она изменяет только вывод на экран (как матрица проекции). Для того, чтобы воспроизведение производилось не на всё окно, необходимо вызвать процедуру "glScissor (x,y: GLint; width, height: GLsizei);", задающую область вырезки. Перед её использованием необходимо включить проверку выхода за область вырезки : "glEnable(GL_SCISSOR_TEST);". Отключается он, соответственно, с помощью glDisable. В качестве параметров необходимо задать область воспроизведения. Если параметры в процедуре glViewPort оставить прежними, то мы будем видеть только ту часть прежнего изображения, которая попадает в область вырезки.

-->Основные команды - рисуем примитивы

Начну с того, что OpenGL - это вам не GDI, и перед рисованием не желательно, а обязательно надо вызывать функцию BeginPaint, а по завершении рисования EndPaint, иначе на экране будут появлятся цветные куски в самых разнообразных местах и надо указать система контроллировать их появление. Перед рисованием очистим (это не всегда нужно делать) буфер цветов. Для этого выполним команду glClear с флагом GL_COLOR_BUFFER_BIT. Цвет фона мы задавали процедурой glClearColor, требующую в качастве параметров четыре числа от 0 до 1 типа GLсlampf=Single(числа с плавающей запятой)-R,G,B,A. Цвета задаются значениями от 0 до 1. Весьма полезно очистить буфер глубины или Z-буфер(если трёхмерное изображение меняется от прорисовки к прорисовке). Это делается добавлением в процедуру glClear флага GL_DEPTH_BUFFER_BIT. Для отображения примитивов не в порядке выполнения процедур внутри FormPaint-а, а прорисовывать более близкие объекты позднее более отдалённых, необходимо запустить процедуру glEnable c параметром GL_DEPTH_TEST - включить тест глубины. В конце рисования (поскольку был запрошен режим двойной буферизации) надо поменять местами главный и вторичный буферы: "SwapBuffers(DC);". В OpenGL существуют две процедуры-скобки - glBegin(...) и glEnd(), между которыми следует выполнять рисование. В качестве параметра в процедуру glBegin передаётся переменная, характеризующая тип изображаемых объектов.

procedure TMainForm.FormPaint;
var ps:TPaintStruct;
begin
BeginPaint(Handle,ps);
glClearColor(1,1,1,1);
glDepthFunc(GL_LESS);
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glFrustum(-1,1,-1,1,-2,0);
glColor3f(0.5, 0.1, 0.9);
glBegin(GL_POINTS);
glVertex3f(-0.5, 0.5, -1);
glVertex3f( 0.5, 0.5, -1);
glVertex3f( 0.5,-0.5, -1);
glVertex3f(-0.5,-0.5, -1);
glEnd();
SwapBuffers(DC);
EndPaint(Handle,ps);
end;

Процедура glVertex3f рисует точку в пространстве с тремя заданными координатами. Аналогично, функция glVertex2f рисует точку на плоскости и требует два параметра. Цифра в командах OpenGL характеризует количество аргументов, а буква на конце - их тип. Например, f - вещественное число, i - целое, ub - байт(UNSIGNED_BYTE). Как правило, в OpenGL существуют несколько аналогичных процедур с разными типами параметров. Из них предпочтительнее те, у которых агрументы вещественны, поскольку сама библиотека хранит их в вещественном виде. Процедура glColor3f требует в качестве параметра три вещественных числа - R, G, B - в диапазоне от 0 до 1 - тип GLclampf. (Кстати, получить теущий цвет можно с помощью команды glGetFloatv (GL_CURRENT_COLOR, @ColorArray). ColorArray - массив из 4-х вещественных чисел (RGBA)). Левый нижний угол (на плоскости, без масштабирования) имеет координату (-1, -1), правый верхний, соответственно, (1, 1). Если вы заметили, точки рисуются квадратными, чтобы их сгладить, необходимо включить режим сглаживания точек : "glEnable(GL_POINT_SMOOTH);". Проверить, проэнейблен ли какой-нибудь режим, необходимо вызвать функцию glIsEnabled(<"режим">).

Сейчас несколько поподробнее об агрументах команды glBegin(mode : Integer). Вот возможно неполный список возможных параметров:

  • GL_POINTS-рисование точек
  • GL_LINES-линии (точки берутся попарно); ширину линий можно задавать процедурой glLineWidth(width : GLfloat);
  • GL_LINE_STRIP-ломаная
  • GL_LINE_LOOP-замкнутая ломаная
  • GL_TRIANGLES-треугольники
  • GL_TRIANGLE_STRIP-группа связанных треугольников
  • GL_TRIANGLE_FAN-связанные треугольники с всеобщей вершиной
  • GL_QUADS-четырёхугольники
  • GL_QUAD_STRIP-группа четырёхугольников, каждый следующий имеет 1 общую сторону с предыдущим
  • GL_POLYGON-многоугольник

Из употреблённых команд осталась ещё нерассмотренной команда glFrustum, задающая перспективу. Просто рисование мысленно происходит внутри комнаты и всё выходящее за её пределы отсекается. glFrustum задаёт доступную облать по осям x, y и z (наименьшее возможное значение, затем - наибольшее). Ось X направлена слева направо, Y - снизу вверх, ось Z - от нас.

-->Матрицы OpenGL

Прочитав предыдущий пример, читатель может подумать, что поворот, растяжение, сдвиг и прочие операции воспроизвести достаточно затруднительно. Но это не так. В OpenGL это достигается при помощи матриц. Рассмотрим некоторую точку в пространстве с точки зрения библиотеки (например, вершину какой-нибудь фигуры). Реально изображаемый трёхмерный объект будет получен путём умножения вектора на так называемую матрицу модели (GL_MODELVIEW). Далее он будет спроектирован на плоскость экрана путём умножения на матрицу проекции. Теперь опишу некоторые полезные команды, связанные с матрицами.

  • Сделать "текущей" (в смысле выполнения операций) матрицу - glMatrixMode(<матрица>). Например, команда "glMatrixMode(GL_MODELVIEW);" делает текущей матрицу модели, если в качестве параметра передать GL_PROJECTION, то текущая - матрица проекции.
  • Узнать, какая матрица установлена сейчас, можно с помощью команды "glGetIntegerv(GL_MATRIX_MODE, @<некоторая переменная типа Integer>);". Данная переменная и будет, соответсвенно, равняться GL_MODELVIEW или GL_PROJECTION.
  • Загрузка единичной матрицы производится с помощью функции "glLoadIdentity();".
  • Загрузка матрицы из массива производится функцией "glLoadMatrixf(@<массив типа array [0..3, 0..3] of GLfloat>);".
  • Умножение текущей матрицы на данную производится командой "glMultMatrix(@<массив 4х4 из GLfloat>);".
  • Затолкать матрицу(модели)в стек - "glPushMatrix();".
  • Вытолкнуть матрицу(модели) из стека - "glPopMatrix();"
  • Масштабирование - "glScalef(x, y, z : GLfloat);", x, y, z - коэффициенты по осям.
  • Перенос - "glTranslatef(x, y, z : GLfloat);", x, y, z - координаты вектора переноса.
  • Поворот - "glRotatef(angle, x, y, z : GLfloat);", angle - угол(в градусах), x, y, z - координаты вектора, вокруг которого происходит поворот.

Сейчас я приведу пример "человеческой"(рекомендуемой) последовательности команд в OnResize или (похуже, но тоже возможно) перед прорисовкой в OnPaint, правильно реагирующую на изменение размеров окна. Выглядеть она будет так:

glViewPort(0,0,ClientWidht, ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glFrustum(-1, 1, -1, 1, 2, 8);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

Это некий стандарт вывода изображения в окно с переменными размерами. То, что он будет работать, и так понятно, но почему это оптимальная последовательность... Ну хотя бы потому, что здесь мы везде грузим единичные матрицы - что может выполняться быстрее?

Теперь немного о командах, изменяющих матрицу проекции. Матрица проекции формирует вывод на экран и определяется местоположением "глаза" и перспективой. Задание перспективы происходит при помощи команды glFrustum(...), описанной выше, команды "gluPerspective(fovy, aspect, zNear, zFar : GLDouble);"(угол видимости к оси Y, угол видимости к оси X, ближняя плоскость отсечения по оси Z, дальняя плоскость отсечения по оси Z), "gluOrtho(left, right, bottom, top, zNear, zFar);"("плоская" проекция, не видимость, свойственная глазу, а проекция на плоскость по стандартным правилам начертальной геометрии), "gluOrtho2D(left, right, bottom, top);"(то же, что и "gluOrtho(...)", но zNear = -1, zFar = 1 и работает несколько побыстрее). Местоположение глаза определяется командой "gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz : Double);". Здесь eye* - соответствующая координата глаза, center* - соответствующая координата центра экрана, up* - соответствующая координата вектора поворота. Как пользоваться первыми двумя параметрами, интуитивно понятно, но как быть с вектором up? На самом деле OpenGL выбирает максимальную (по модулю) из координат вектора и делит оставшиеся на неё. Поэтому вариант (0,0,0) не запашет. Далее происходит поворот по каждой из осей из расчёта "1" = 180o. Иллюстрация к команде gluLookAt и матрицам GL_MODELVIEW и GL_PROJECTION можно качнуть здесь.

-->Свет и материалы

Чтобы разрешить работу со светом, необходимо выполнить команду "glEnable(GL_LIGHTING);". Чтобы выключить, соответственно, "glDisable(GL_LIGHTING);" Далее нужно разрешить работу с каждым используемым источником по отдельности: "glEnable(GL_LIGHT0);". Чтобы узнать максимально возможное число источников света, небходимо вызвать "glGetIntegerv(GL_MAX_LIGHTS, @<целое число>);". Для каждого многоугольника при этом необходимо прописать нормаль - "glNormal3f(x, y, z : GLfloat);". Можно запретить рисование передней или задней сторон(с одной стороны полигон прозрачен): glEnable(GL_CULL_FACE); glCullFace(GL_FRONT);//(или, наоборот, GL_BACK). Свойства источника можно задавать функцией glLightfv(light, pname : GLEnum; params : PGLfloat);. Параметр light - индекс источника. Параметр pname может принимать значения GL_POSITION, GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_SPOT_CUTOFF, GL_SPOT_DIRECTION, GL_SPOT_EXPONENT и GL_SPECULAR. GL_POSITION задаёт координаты источника, читаемые из массива "array [0..3] of GLfloat;", первые три ячейки которого задают координаты источника, а последний параметр равен 1.0. Замечу также, что при преобразованиях системы координат источник остаётся на месте! Параметры GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR задают свойства окружающей среды.

Теперь поподробнее о свойствах источника, то есть о флажках.

Итак, массив по адресу params должен содержать:

  • GL_AMBIENT : четыре вещественных числа RGBA, которые определяют цвет фонового освещения.
  • GL_DIFFUSE : четыре вещественных числа RGBA, которые определяют цвет диффузного отражения.
  • GL_SPECULAR : четыре вещественных числа RGBA, которые определяют цвет зеркального отражения.
  • GL_POSITION : три координаты местоположения источника
  • GL_SPOT_EXPONENT : одно число от 0 до 128(иначе число усекается до 7 бит, преобразуясь перед этим в Integer)"сфокусированность" источника
  • GL_SPOT_CUTOFF : одно число - разброс (угол) освещения. Правда, не телесный, а от 0 до 90 либо 180 - рассеянный свет
  • GL_SPOT_DIRECTION : четыре числа - направление источника. Последнее число ставьте равным единице.

Ещё существует замечательная функция glLightModelfv(pname : GLenum, params : PGLtype). Параметр pname может принимать следующие значения :

  • GL_LIGHT_MODEL_LOCAL_VIEWER : по адресу params либо FALSE, либо TRUE, влияет на видимость. Если TRUE, наблюдатель находится в начале видовой системы координат. По умолчанию FALSE.
  • GL_LIGHT_MODEL_TWO_SIDE : по адресу params либо FALSE, либо TRUE. Означает, проводится ли расчёт освещения для задних граней.
  • GL_LIGHT_MODEL_AMBIENT : цвет общего уровня освещённости. При этом params содержит четыре вещественных числа - цвет RGBA.

Теперь о свойствах материала. Свойства материала задаются с помощью команды "glMaterialfv(face, pname: GLenum; params: PGLfloat);". Перед использованием этой процедуры необходимо включить режим "GL_COLOR_MATERIAL" : "glEnable(GL_COLOR_MATERIAL);". Команда "glMaterialfv" устроена аналогично команде "glLightfv". Параметр face может принимать значения GL_FRONT, GL_BACK и GL_FRONT_AND_BACK, в зависимости от того, у какой стороне полигонов мы хотим изменить свойства материала. Параметр face может принимать следующие значения (для справки, запись "четыре вещественных числа" означает указатель на массив из четырёх вещественных чисел типа array [0..3] of GLfloat;):

  • GL_AMBIENT : параметр params - четыре вещественных числа - рассеянный цвет материала (цвет в тени)
  • GL_DIFFUSE : параметр params - четыре вещественных числа - диффузный(матовый отражённый) цвет материала. Диффузный цвет обычно влияет наиболее сильно из всех параметров
  • GL_AMBIENT_AND_DIFFUSE : параметр params задаёт сразу два вышеуказанных параметра
  • GL_SPECULAR : параметр params - 4 вещественных числа - цвет зеркального отражения
  • GL_EMISSION : параметр params - 4 вещественных числа - интенсивность излучаемого света
  • GL_SHININESS : параметр params - указатель на вещественное число в диапазоне от 0 до 128 - степень зеркального отражения.

В качестве примера к свойствам материала я предложу вам проект - набор шариков с разными свойствами. Понаблюдать за перемещением источника света можно тут.

-->Quadric-объекты

В библиотеке glu находятся некоторые полезные функции, без которых, в принципе, обойтись можно. Одним из нововведений являются Quadric-объекты. Для использования их, необходимо объявить переменную типа GLUquadricObj внутри объекта окна или глобально. Объект необходимо создать (выделить память) и удалить (освободить память). Это можно делать в FormPaint-е соответственно перед и после рисования, можно в "FormCreate" и "FormDestroy". Создание Quadric-объекта производится функцией "gluNewQuadric();", а его удаление функцией "gluDeleteQuadric(state: GLUquadricObj);". Функция "gluNewQuadric" возвращает созданный объект, а процедура "gluDeleteQuadric" принимает в качестве параметра подлежащий удалению объект. Для обработки ошибок при работе с Quadric-объектами предназначена команда gluQuadricCallback(quadObject: GLUquadricObj; which: GLenum; callback: Pointer). Первый параметр - Quadric-объект. Второй равен константе GLU_TESS_ERROR. Третий параметр callback - процедура со стандартным вызовом (допишите ключевое слово StdCall; после объявления процедуры), в которой у вас будет происходить обработка ошибки, например, уведомление пользователя об ошибке. Данная процедура не может быть объявлена внутри класса. Процедура "gluQuadricDrawStyle (quadObject: GLUquadricObj; drawStyle: GLenum);" задаёт режим отображения Quadric-объекта. Первый параметр - объект. Второй может быть равен GLU_POINT,{рисовать только точки} GLU_LINE,{рисовать только линии (рёбра)} GLU_FILL,{рисовать всё (залить грани)} GLU_SILHOUETTE{рисовать только контур (силуэт)}. Направление нормалей задаётся процедурой gluQuadricOrientation(quadObject: GLUquadricObj; orientation: GLenum);". Первый параметр - объект, второй - ориентация нормалей, может быть либо "GLU_INSIDE" (внутрь) или "GLU_OUTSIDE" (наружу). О том, как их(нормали) строить, говорит функция "gluQuadricNormals(quadObject: GLUquadricObj; normals: GLenum);", где первый параметр - сам объект, а второй равен "GLU_NONE"(не строить), "GLU_FLAT"(перпендикулярно плоскости грани), "GLU_SMOOTH"(строить под углом, таким образом, чтобы сгладить объект). В качестве иллюстрации я предложу вам достаточно наглядный проект-иллюстрацию книге. Понажимайте на клавиши "1", "2", "3", "4", "up", "down", "left" и "right". Скачать его можно здесь.


Используются технологии uCoz