Введение в компьютерную графику

Как известно, трехмерная графика формируется из треугольных полигонов (face). Они имеют различные материалы, параметры освещенности и текстуры (изображения).


Иллюстрация построения трехмерной графики из треугольных полигонов

Современная видеокарта способна рисовать на экране 200-500 тысяч треугольных полигонов за один кадр с частотой 60 кадров в секунду и выше независимо от размера этих полигонов. Быстродействие видеокарты определяется не размером полигонов, а их количеством. Поэтому при отображении трехмерной графики в реальном времени стоит задумываться об оптимизации моделей.

Обычно для этого используют низкополигональные модели (до 50 тысяч полигонов на модель).

Обмен данными между центральным процессором (CPU) и видеокартой (GPU)

Самым тонким местом при отображении трехмерной графики является шина обмена данными между центральным процессором и видеокартой. Данная шина более быстрая в сторону передачи данных от центрального процессора на видеокарту, и обычно на порядки более медленная в обратную сторону. Причины понятны – обычно большие объемы данных нужно передавать на видеокарту, а не обратно.

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

Для оптимизации обмена данными между центральным процессором и видеокартой разработчики трехмерной графики предварительно загружают на видеокарту текстуры и координаты трехмерных объектов.

Вершины

Видеокарта персонального компьютера позволяет на аппаратном уровне рисовать на экране треугольные полигоны, заданные тремя трехмерными координатами (x,y,z). Координаты вершин обычно задают в формате float.

Системы координат

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


Иллюстрация принципа формирования координат точек в локальной системе координат объекта


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

Нормали

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


Иллюстрация эффекта выгнутости треугольного полигона путем задания ему трех несонаправленных нормалей.

Координаты каждой нормали (Nx, Ny, Nz) также задаются в локальной системе координат. Длина вектора нормали должна быть равна 1.

Материалы

Цвет и параметры блеска, которые используются для закраски полигона, задаются с помощью материалов. Текущий материал, используемый при рисовании, устанавливается программистом перед рисованием очередной группы полигонов. Блеск материала определяется путем расчета ориентации полигона к источнику света.

Источники света

Источники света задаются программистом предварительно перед рисованием всей трехмерной сцены. Источники света определяются координатами положения в пространстве, направлением и конусом свечения (если это точечный источник света), а также цветом излучения. Видеокарта автоматически производит расчет освещенности рисуемых полигонов в зависимости от материала полигонов.

Текстуры

Для закраски полигона могут использоваться текстуры (изображения). Изображения всех текстур загружается в память видеокарты на этапе загрузки сцены. Каждой загруженной текстуре присваивается дескриптор (порядковый номер) текстуры. При рисовании текстуры процессор указывает лишь этот дескриптор.

Важное требование – размер изображения текстуры по длине и ширине должен быть кратен степени 2. Т.е. подходят размеры 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048. Некоторые современные видеокарты (отнюдь не все!) поддерживают расширение ARB_texture_non_power_two, позволяющее отображать текстуры, размер которых не кратен степени 2. Однако для совместимости со всеми видеокартами желательно все же использовать текстуры, кратные степени 2.

Желательно чтобы размер текстуры был не более, чем 512x512. Использование текстур больших размеров связан с дополнительными ограничениями скорости отрисовки таких текстур, т.к. видеокарта размещает такую текстуру в разные аппаратные блоки.

Текстурные координаты

Чтобы разместить текстуру на объекте у каждой вершины полигона задаются текстурные координаты (u,v). Текстурная координата u вершины – это нормированная координата X в системе координат изображения, соответствующая данной вершине. А координата v – это координата Y в системе координат изображения. Нормировка производится длиной и шириной изображения соответственно по u и по v.


Иллюстрация использования текстурных координат

Следует отметить, что видеокарта за один проход рендера позволяет накладывать на один полигон сразу несколько текстур, каждая из которых может содержать свои текстурные координаты, однако в нашем случае механизм мультитестурирования рассмотрен не будет.

Индекс-буферы

Для оптимизации объема данных, передаваемых на видеокарту от центрального процессора, уменьшения объема памяти, необходимого для хранения информации о трехмерной фигуре, а также для ускорения рисования используют технологию индекс-буферов.

Заметьте, что, например, у сферы каждая вершина используется как минимум 4 смежными полигонами, а полюса 32 сегментной сферы – аж 32 полигонами! Зачем каждому их этих полигонов хранить и всякий раз при рисовании пересчитывать координаты этой общей вершины? Можно отдельно хранить и расчитывать координаты всех вершин, нормалей и текстурных координат (вертекс-буфер), а отдельно хранить последовательность соединения этих вершин полигонами (индекс-буфер).

В индекс-буфере хранится массив номеров вершин. Размер массива всегда кратен 3. Каждые три числа их этого массива формируют номера вершин для рисования одного полигона.

Иногда индекс буфер рассматривают как массив из n троек чисел. Каждую такую тройку номеров вершин называют face.


Пояснение понятия индекс-буфера

Отсечение невидимых поверхностей. Z-буфер

При выводе полигонов на экран видеокарта отсекает невидимые полигоны, применяя технологию Z-буфера. Технология подразумевает, что помимо буфера с цветом пикселей экрана (цветового буфера) будет организован буфер с координатами глубины (Z-буфер). Z-буфер имеет ту же размерности, что и цветовой буфер, а каждый его пиксель соответствует пикселю экрана. В буфере глубины хранится экранная глубина пикселя. Изначально во все пиксели буфера глубины записывается значение бесконечной глубины. Перед выводом каждый пиксель полигона проверяет собственную глубину с глубиной, записанной в Z-буфере в тех же координатах. Если записанная в Z-буфер глубина меньше, чем глубина пикселя полигона, то данный пиксель не рисуется ни в цветовом буфере, ни в буфере глубины. Иначе пиксель отображается в цветовом буфере, а в буфер глубины записывается глубина нарисованного пикселя.


Иллюстрация отсечение невидимых поверхностей с помощью технологии Z-буфера.

На рисунке цветовой буфер совмещен с Z-буфером. Цифра в ячейках показывает значение, записанное в пиксель Z-буфера. Цвет ячейки позывает значение цвета, записанное в пиксель цветового буфера. Полигоны 1, 2, 3 выводились на экран в указанной последовательности. При этом глубина всех пикселей полигона 1 равна 5 условных единиц глубины, глубина всех пикселей полигона 2 равна 9, а глубина всех пикселей полигона 3 равна 2. Заметьте, что при попытке нарисовать полигон 2 в пикселях, перекрытых полигоном 1, рисование не производилось, т.к. в буфере глубины в данных пикселях было значение (5) меньше, чем глубина (9) пикселей полигона 2. При рисовании полигона 3 проверка глубины во всех случаях завершилась успехом, и все пиксели полигона были отрисованы в цветовом буфере.

В общем случае глубина полигона в разных его точках различна, она линейно интерполируется от вершины к вершине.

Отсечение невидимых поверхностей по технологии CULL_FACE

В трехмерной графике трехмерные фигуры обычно представляют собой закрытые объемы. При этом обычно число полигонов с передней стороны таких объектов примерно совпадает с числом полигонов с задней стороны объекта. Повернутые к камере задней своей стороной полигоны никогда не бывают видимы.

Определить какой стороной повернут к камере полигон до его рисования на экране достаточно просто. Поэтому число рисуемых полигонов можно раза в два сократить (повысив быстродействие) путем предварительного просчета их ориентации к камере.

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

Отсечение невидимых полигонов, повернутых к камере своей задней стороной, называется технологией CULL_FACE или технологией односторонних полигонов.

Создание трехмерных моделей

Разработка моделей с помощью трехмерных редакторов

Координаты полигонов можно задавать непосредственно в коде программы, однако такой способ рисования очень не удобен. Попробуйте задать координаты 100000 треугольных полигонов вручную!

Поэтому трехмерную сцену рисуют в каком-либо 3D-редакторе, экспортируют в файл, который потом загружаются через графический движок. В качестве такого редактора удобно использовать 3D Studio MAX.

При подобном подходе встает вопрос об описании формата хранения трехмерной сцены.

Соображения о формате хранения трехмерной сцены

Удобно, чтобы информация о трехмерной сцене хранилась не в виде разрозненного массива координат полигонов, а в виде организованной структуры, разделенной, как минимум, на трехмерные объекты.

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

Порядки обхода вершин (индекс-буферы) удобно сгруппировать по материалам. Если у объекта один материал, то и индекс-буфер тоже будет один. Если у объекта несколько материалов, то для каждого материала удобно создать отдельный индекс-буфер, который будет хранить информацию о полигонах, рисуемых этим материалом.

Удобно хранить геометрический объект (mesh) отдельно от трехмерного объекта, задающего его систему координат. В этом случае, если на сцене несколько одинаковых объектов, отличающихся лишь положением в пространстве, то под каждый такой объект можно создать отдельный трехмерный объект и поместить в него указатель на один и тот же геометрический объект (mesh). Это значительно сократит память для хранения таких объектов.

Кроме того, трехмерный объект может использоваться не только для геометрического объекта, но и использоваться как система координат источника света или камеры. По этим двум причинам удобно, чтобы трехмерный объект был отделен от геометрического объекта.

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

Для формирования локальной системы координат трехмерного объекта удобно использовать матрицы преобразования 4x4. Причем у каждого объекта удобно хранить две матрицы преобразования: локальную матрицу преобразования (matrix), которая определяет положение и ориентацию объекта относительно своего родительского объекта, а также мировую матрицу преобразования (worldMatrix), которая определяет положение объекта в мировой системе координат. Мировые матрицы преобразования будут автоматически рассчитываться перед рисованием очередного кадра на основе информации локальных матриц преобразования.

В силу того, что обычно несколько объектов трехмерной сцены используют один и те же текстуры, удобно организовать менеджер текстур, который позволяет загружать каждую текстуру лишь один раз, а затем использовать ссылки на нее. Такому менеджеру текстур удобно написать метод запроса текстуры с указанным названием (именем файла), а менеджер просмотрит уже загруженные текстуры и вернет указатель на текстуру, если она уже загружена в память видеокарты. Если же текстура с указанным названием еще не загружена, то данный метод ее загрузит. Такой менеджер текстур существенно упростит процесс разработки трехмерного приложения.

Рекомендуемый формат описания 3D-сцены


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

Во главе структуры стоит объект CScene. В нет размещена ссылка на все трехмерные объекты (objects) и ссылка на менеджер текстур.

Менеджер текстур (CTextureManager) хранит ссылки на все текстуры сцены, позволяет их загружать с диска, следя за тем чтобы одна и та же тестура не загружалась дважды.

Текстуры организованы в виде двунаправленной очереди. Каждая текстура имеет ссылку на следующую текстуру очереди (next) и предыдущую текстуру очереди (prev).

Изображения текстур всегда загружаются в память видеокарты и храняться только там. В каждом классе CTexture имеется дескриптор handle, указывающий на текстуру в памяти видеокарты.

Трехмерные объекты (CObject3D) организованы в виде дерева. Свойства child и parent указывают соответственно на дочерний (привязанный) и родительский объект. Свойство next и prev указывают на объекты одного уровня иерархии.

Объект может содержать ссылку на камеру (camera), источник света (light) или на геометрический объект (mesh). Кроме того объект хранит локальную матрицу преобразования (matrix) и мировую матрицу преобразования (worldMatrix).

Локальная матрица преобразования является первопричинной матрицей. Она загружается из файла экспорта. Матрица содержит информацию о преобразовании координат от текущего объекта в родительский объект.

Мировая матрица преобразования используется только на этапе прорисовки сцены (render). Первый этапом прорисовки с помощью метода object->UpdateMatrix() производится пересчет мировых матриц всех объектов по информации, хранимой в локальных матрицах преобразования (matrix).

Кроме всего прочего объект хранит свое название, которое ему дали в 3D Studio MAX.

Камеры сцены содержат поле FOV - угол обзора. Также имеется ссылка объект, к которому принадлежит камера. Объект сцена (CScene) содержит ссылку на активную камеру сцены. Камер на сцене может быть несколько, но только одна активная. По умолчанию активная камера задается в 3D Studio MAX.

Источники света содержат информацию о цвете (color) источника, признак включения/выключения (onOff), а также прочие свойства источников света. Также класс CLight содержит указатель на объект, к которому он принадлежит.

Геометрические объекты (CMesh) имеют ссылку на один или несколько индексных буферов, а также на вершины, нормали и текстурные координаты (вертексный буфер). Вертексный буфер для совместимости с возможностью загрузки информации на видеокарту организован специальным образом - выделяется единый непрерывный блок памяти для хранения в нем вершинных координат, нормалей и текстурных координат. Таким образом ссылка vertices указывает на начало этого буфера, а normals и texCoords указывают на части этого буфера. Количество вершин, нормалей и текстурных координат одинаково и равно vCount.

Вертексный буфер может быть размещен в памяти видеокарты. В этом случае дескриптор handle класса CMesh указывает на данный буфер в видеопамяти. При загрузке вертексного буфера в память видеокарты из памяти центрального процессора он удаляется (ссылки vertices, normals, texCoords указывают на NULL).

Индексный буфер содержит информацию о порядке соединения вершин полигонами (face). Каждый индексный буфер отвечает за нанесение на объект одного из материалов.

Индексный буфер содержит ссылку на массив индексов indices из count элементов (count всегда кратно 3). Каждая тройка индексов из этого массива содержит номера вершин, соединив которые образуется очередной трехмерный полигон. Все полигоны одного индексного буфера имеют один и тот же материал. Также индексный буфер содержит ссылки на материал (material) и следующий индексный буфер (next), если он есть у данной mesh. Последний индексный буфер данного mesh указывает на NULL.

Индексный буфер может быть загружен в память видеокарты. В этом случае дескриптор handle класса CIndexBuffer указывает на данный буфер в памяти видеокарты. Если индексный буфер находится в памяти видеокарты, то массив индексов из памяти центрального процесса удаляется, а указатель indices класса CIndexBuffer указывает на NULL.

Материал (CMaterial) содержит 4 цвета материала (ambientColor - цвет в тени, diffuseColor - основной цвет объекта, specularColor - цвет блика, emmisionColor - цвет самосвечения), параметр глянца (glossiness), свойство twoSided сообщающее OpenGL о необходимости использования двухсторонних материалов. Также материал содержит указатель на текстуру (если она есть) и матрицу преобразования данной текстуры (matrix). Матрица преобразования текстуры содержит информацию о повороте, повторе и смещения текстуры.



Рекомендуемый формат описания 3D-сцены. В скачиваемом файле представлен код заголовочного файла (h-файла), в котором может быть описана 3D-сцена. Студент должен самостоятельно создать соответствующий cpp-файл, в котором будут реализованы предлагаемые функции и методы классов, заголовок которых описан в h-файле.

Скачать h-файл библиотеки

#ifndef glUnitH
#define glUnitH


#include <windows.h>

#include <gl/gl.h>

#include <gl/glu.h>


#include <math.h>


#pragma comment (lib, "opengl32.lib") 

#pragma comment (lib, "glu32.lib") 



#pragma warning( disable: 4996 )

#pragma warning( disable: 4244 )

#pragma warning( disable: 4312 )

#pragma warning( disable: 4311 )




typedef struct { float x,y,z; } VECTOR;

typedef struct { float u,v; } TEXCOORD;

typedef struct { float r,g,b,a; } COLOR3D;


//   MATRIX A;


typedef union
{

        struct
        {
                VECTOR rowX; float zero0;		
                VECTOR rowY; float zero1;
                VECTOR rowZ; float zero2;
                VECTOR position; float one;
        } rows;
} MATRIX;





class CTextureManager;
class CMesh;
class CObject3D;
class CScene;

class CTexture
{
public:


};

class CTextureManager
{
public:


	CTexture *Get(const char *texName);	
};

class CMaterial
{
public:



};


class CIndexBuffer
{
public:



};

class CMesh
{
public:





	void SetVCount(int n, bool isNormals, bool isTexCoord);

	
};


class CLight
{
public:


    void Render(int n);
};

class CCamera
{
public:

	CCamera(CObject3D *obj);	

	~CCamera();					
};

class CObject3D
{
public:
	CObject3D *next,*prev,*child,*parent;


	CObject3D	*id;	

	


	void SetParent(CObject3D *newParent);

	void UpdateTransform();

	void RenderLights(int *n);

	void Render();

	CObject3D *FindObject(const char *Name);

	void Translate(float x, float y, float z);

	void RotateX(float a);

	void RotateY(float a);

	void RotateZ(float a);
};

void MatrixMul(MATRIX &res, const MATRIX &M1, const MATRIX &M2);


class CScene
{
public:





	void UploadToGPU();
	
};


#endif


Код инициализации OpenGL

Предлагаемый код инициализации OpenGL:
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>

....

bool CScene::InitGL(HDC DC, int w, int h)
{
	// инициализация переменных
	dc = DC;
	width = w;
	height = h;
	
    // инициализация OpenGL
    PIXELFORMATDESCRIPTOR PFD;
    
    memset(&PFD,0,sizeof(PFD));
    PFD.nSize=sizeof(PFD);
    PFD.nVersion = 1;
    PFD.dwFlags=PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
    PFD.iPixelType = PFD_TYPE_RGBA;
    PFD.iLayerType = PFD_MAIN_PLANE;

    PFD.cColorBits = 32;
    PFD.cDepthBits = 32;
    int f = ChoosePixelFormat(dc, &PFD);
    if (!f) return false;

    SetPixelFormat(dc, f, &PFD);
    rc = wglCreateContext(dc);
    wglMakeCurrent(dc, rc);

    glViewport(0,0,width, height);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glAlphaFunc(GL_GREATER,0.3f);
	
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    return true;
}

Предлагаемый код деинициализации 3D:
void CScene::Close3D()
{
	// удалить все дочерние объекты от objects
	while (objects->child) delete objects->child;
	textureManager.Clear();

	wglMakeCurrent(0, 0);
	if (rc)
	{
		wglDeleteContext(rc);
		rc = 0;
	}
}


Пример рисования в OpenGL. Простой, но медленный код

Далее приводится пример рисования произвольных фигур в OpenGL. Данный код медленный, поэтому использовать его не рекомендуется.
	// рисование произвольных фигур из треугольников

	// установить матрицу преобразования
	glMatrixMode( GL_MODELVIEW);   // режим матриц сцены

	glPushMatrix();	// запоминить текущую матрицу
	glMultMatrixf( obj->worldMatrix.m[0] );  // домножить на матрицу объекта

	material->Bind(); // установить материал

	glBegin(GL_TRIANGLE);	// начать создание фигуры

	// треугольник 1
	glNormal3f( normal1A.x, normal1A.y, normal1A.z); // нормаль точки 1
	glTexCoord2f( texCoord1A.x, texCoord1A.y, texCoord1A.z); // текст. координаты точки 1
	glVertex3f( vertex1A.x, vertex1A.y, vertex1A.z); // вертекст точки 1

	glNormal3f( normal1B.x, normal1B.y, normal1B.z); // нормаль точки 2
	glTexCoord2f( texCoord1A.x, texCoord1A.y, texCoord1B.z); // текст. координаты точки 2
	glVertex3f( vertex1B.x, vertex1B.y, vertex1B.z); // вертекст точки 2


	glNormal3f( normal1C.x, normal1C.y, normal1C.z); // нормаль точки 3
	glTexCoord2f( texCoord1C.x, texCoord1C.y, texCoord1C.z); // текст. координаты точки 3
	glVertex3f( vertex1C.x, vertex1C.y, vertex1C.z); // вертекст точки 3



	// треугольник 2
	glNormal3f( normal2A.x, normal2A.y, normal2A.z); // нормаль точки 1
	glTexCoord2f( texCoord2A.x, texCoord2A.y, texCoord2A.z); // текст. координаты точки 1
	glVertex3f( vertex2A.x, vertex2A.y, vertex2A.z); // вертекст точки 1

	glNormal3f( normal2B.x, normal2B.y, normal2B.z); // нормаль точки 2
	glTexCoord2f( texCoord1A.x, texCoord2A.y, texCoord2B.z); // текст. координаты точки 2
	glVertex3f( vertex2B.x, vertex2B.y, vertex2B.z); // вертекст точки 2


	glNormal3f( normal2C.x, normal2C.y, normal2C.z); // нормаль точки 3
	glTexCoord2f( texCoord2C.x, texCoord2C.y, texCoord2C.z); // текст. координаты точки 3
	glVertex3f( vertex2C.x, vertex2C.y, vertex2C.z); // вертекст точки 3

	....

	glEnd();

	glPopMatrix(); // восстановить матрицу

Данный способ рисования подходит для рисования отдельных небольших объектов, например, для рисования различных эффектов, 3D-элементов интерфейса, наложенного поверх трехмерного изображения и т.п.



Рекомендуемый быстрый способ рисования трехмерных фигур

Данный способ рисования трехмерных фигур подразумевает наличие вертексных-буферов и индекс-буферов, поэтому наиболее подходит для предлагаемой структуры 3D-сцены. Кроме того, т.к. вся геометрия загружается в GPU одним массивом (одной командой), данный код позволяет сократить число обращений к видеокарте, а, следовательно, ускорить прорисовку.

Данный способ позволяет обращаться к видеокарте с использованием индекс-буфера, что в некоторых случаях позволяет сократить объем данных, передаваемых видеокарте. Меньше данных — быстрее отрисовка.

В коде приводится закомментированный вариант, использующий аппаратные индекс-буферы и вертексные буферы. Данные вариант предназначен для продвинутых студентов, скачавших библиотеки расширение OpenGL (см. ниже). Аппаратные буферы предварительно должны быть созданы и загружены в память видеокарты. Это позволяет вообще избавиться от необходимости передачи какие-либо массивов данных в видеокарту во время отрисовки кадра, что позволяет в несколько раз повысить скорость прорисовки кадра.

CMaterial defMaterial;	// материал по умолчанию

// отрисовка индекс-буфера
void CIndexBuffer::Render(CMesh *mesh)
{
	if (material)
		material->Bind();	// установить материал индекс-буфера, если есть
	else
	{
		// установить материал по умолчанию, установив ему цвет объекта
		defMaterial.ambientColor = mesh->wireColor;
		defMaterial.deffuseColor = mesh->wireColor;
		defMaterial.Bind();
	}

	if (!handle)
	{
		// отрировать, если индекс-буфер не загружен в GPU
		if (isOpenGL11)
		{
			// быстрая прорисовка
			glDisableClientState(GL_INDEX_ARRAY);
			glDrawElements(GL_TRIANGLES, сount, GL_UNSIGNED_INT, indices);
		}
		else
		{
                  // если нет поддержки быстрой прорисовки. Прорисовка первым методом:
                  int i;
                  glBegin(GL_TRIANGLES);
                  for(i = 0; i < count; i++)
                  {
                     if(mesh->hasNormals)
                        glNormal3fv(&mesh->normals[ indices[i] ].x);
                     if(mesh->hasTexCoord)
                        glTexCoord2fv(&mesh->texCoords[ indices[i] ].u);
                     glVertex3fv(&mesh->vertices[indices[i] ].x);
        	      
                  }
                  glEnd();
		}
	}
	else
	{
		
		// отрировать, если индекс-буфер загружен в GPU
		// (в случае использования расширения openGL)
		/*
		glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, handle);
		glEnableClientState(GL_INDEX_ARRAY);
		glIndexPointer(GL_UNSIGNED_INT, 0, 0);
		glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, NULL);
		glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
		*/
		
	}
}



void CMesh::Render(CObject3D *obj)
{
	// установить матрицу преобразования
	glMatrixMode( GL_MODELVIEW);   // режим матриц сцены

	glPushMatrix();	// запоминить текущую матрицу
	glMultMatrixf( obj->worldMatrix.m[0] );  // домножить на матрицу объекта

	if (isOpenGL11)
	{
		// если есть поддержка быстрой прорисовки

		// установить вертекс-буфер
		glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT); // применить вертексные буфера
		if (!handle)
		{
			// если геометрия не загружена в GPU
			
			glVertexPointer(3, GL_FLOAT, 0, vertices);
			glEnableClientState(GL_VERTEX_ARRAY);

			if (hasNormals)
			{
				glNormalPointer(GL_FLOAT, 0, normals);
				glEnableClientState(GL_NORMAL_ARRAY);
			}
		
			if (hasTexCoords)
			{
				glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
				glEnableClientState(GL_TEXCOORD_ARRAY);
			}
		}
		else
		{
			
			// если геометрия была загружена в GPU
			/*
			int offset = 0;

			// установить текущий аппаратный буфер
			glBindBufferARB(GL_ARRAY_BUFFER, handle);

			// установить в OpenGL смещение вертекс-буфера
			glEnableClientState(GL_VERTEX_ARRAY);
			glVertexPointer(3, GL_FLOAT, 0, (void*)offset);
			offset += (int)sizeof(VECTOR) * vCount;

			// установить в OpenGL смещение буфера нормалей
			if (hasNormals)
			{
				glNormalPointer(GL_FLOAT, 0, (void*)offset);
				glEnableClientState(GL_NORMAL_ARRAY);
				offset += (int)sizeof(VECTOR) * vCount;
			}

			// установить в OpenGL смещение буфера текстурных координат
			if (hasTexCoords)
			{
				glTexCoordPointer(2, GL_FLOAT, 0, (void*)offset);
				glEnableClientState(GL_TEXCOORD_ARRAY);
			}
			*/
			
		}
	}


	// установить направление обхода
	if (ccw)
		glCullFace(GL_FRONT);
	else
		glCullFace(GL_BACK);

	// разрешить Z-буфер (мало ли кто его запретил)
	glDepthMask(TRUE);
	glEnable(GL_DEPTH_TEST);


	// рисуем индекс-буферы
	CIndexBuffer *ibuf = indexBuffers;
	while(ibuf)
	{
		ibuf->Render( this );
		ibuf = ibuf->next;
	}

	// деинициализация аппаратного буфера
	if (handle) glBindBufferARB(GL_ARRAY_BUFFER, 0);

	//  вернуть прошлые атрибуты
	if (isOpenGL11) glPopClientAttrib();

	glPopMatrix(); // восстановить матрицу
}

Следует отметить, что быстрая прорисовка будет работать не на всех видеокартах. Поэтому перед его использованием следует проверить совместимость видеокарты.

Далее приводится код проверки совместимости видеокарты:

bool isOpenGL11 = false;
const char * extensions = (const char *) glGetString ( GL_EXTENSIONS );
if (strstr(extensions, "GL_EXT_draw_range_elements"))
{
	// проверка пойдена
	isOpenGL11 = true;
}

Экспорт моделей из 3D Studio MAX

Скачать скрипт для экспорта моделей в cg-файл
С помощью данного скрипта на языке MAX Script можно экспортировать модели из 3D Studio MAX в простой и удобный бинарный формат.
Скачать описание формата cg-файла
Описание формата файла экспорта.

Порядок экспорта
  1. Один раз за сеанс работы 3D Studio MAX запустите скрипт cg.ms.




    Первый пункт можно не выполнять, если файл cg.ms скопировать в подпапку scripts/startup в директории 3D Studio MAX. В этом случае данный скрипт попадет в автозагрузку 3D Studio MAX.

  2. В командной строке 3D Studio MAX выполните команду cgExport()



  3. Выберите имя файла для экспорта.



  4. Дождитесь окончания экспорта.


Шаблон кода загрузчика cg-файлов

Приведенный ниже пример кода поможет написать загрузчик cg-файла:
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <stdlib.h>
#include <stdio.h>
#include "unit3D.h";

....

//******************* загрузчик ******************/
bool LoadCG(CScene *scene, const char *fileName)
{
	FILE *f;             // файловая переменная
	int bodySize;        // размер файла
	unsigned char *body; // указатель на область памяти, куда будет считан файл


	// открыть файл
	f = fopen(fileName, "rb");	
	if (!f) return false;

	// определить размер файла
	fseek(f, 0, SEEK_END); // указатель чтения из файла на конец файла
	bodySize=(int)ftell(f);	// определить положение указателя от начала файла 
	fseek(f, 0, SEEK_SET);	// переместить указатель чтения из файла на начало файла

	// файл не может быть меньше 4 байт
	if (bodySize < 4) { fclose(f); return false; }

	// считать файл в body
	body = (unsigned char*)malloc( bodySize );
	fread( body, bodySize, 1, f);

	// закрыть файл
	fclose(f);


	//--------------- массив загруженных объектов ----------//
	int objectCount;            // кол-во объектов
	CObject3D **objects = NULL; // массив загруженных объектов

	
	//--------------- используемые переменные ----------//
	CObject3D *obj = NULL;             // последний прочитанный объект
	CMesh *mesh = NULL;                // последний прочитанный mesh
	CIndexBuffer *indexBuffer = NULL;  // последний прочитанный индекс-буфер
	CMaterial *material = NULL;        // последний прочитанный материал


	//--------------- разбор файла ----------//
	char tagName[5] = "xxxx"; // буфер для считывания тэга
	int  tagSize;             // размер тэга
	int  pos = 0;             // текущая позиция чтения
	int  tagPos;

	// проверить сигнатуру файла
	memcpy(&tagName, body + pos, 4); // скопировать 4 байта из адреса (body+pos) в адрес tagName
	pos+=4;                          // передвинуть указатель
	if (strcmp(tagName, "cg10")!=0) { free(body); return false; }

	while(pos < bodySize - 8)
	{
		// считать тэг
		memcpy(&tagName, body + pos, 4); // скопировать 4 байта из адреса (body+pos) в адрес tagName
		pos+=4;                          // передвинуть указатель
		memcpy(&tagSize, body + pos, 4); // скопировать 4 байта из адреса (body+pos) в адрес переменной tagSize 
		pos+=4;                          // передвинуть указатель

		tagPos = pos;	// запомнить текущее значение pos

		// проверить, чтобы размер тэга не превышал оставшегося размера буфера
		if (pos + tagSize > bodySize) break;

		// в зависимости от значения тэга
		if (strcmp(tagName, "node")==0)
		{
			// считать node

			// создать объект obj и добавить в массив objects
			obj = new CObject3D();
			objects = (CObject3D**)realloc( objects, sizeof(CObject3D*) * (objectCount + 1));
			objects[ objectCount ] = obj;
			objectCount++;

			// прочитать и установить название объекта
			char buf[64];
			ReadString(body, bodySize, &pos, buf, sizeof(buf));  // прочитать название объекта
			                                      // функция ReadString будет описана ниже 
			obj->SetName( buf );

			memcpy(&obj->id, body + pos, 4);  // прочитать ID-объекта
			pos+=4;
			memcpy(&obj->parent, body + pos, 4);  // прочитать ID родительского объекта.
			                                      //Пока obj->parent не действительный указатель!!!!
			                                      //т.к. мы прочитали в 32-битный адрес объекта 32-битный
			                                      //индентификатор объекта. Но другого специального буфера
			                                      //для временного хранения идентификатора родительского объекта
			                                      //у нас нет, в то время как сам указатель obj->parent
			                                      //нам на этапе чтения файла не нужен.
			                                      //После чтения всего файла необходимо будет связать объекты,
			                                      //используя считанные в поле obj->parent идентификаторы.
			                                      
			                                      
			pos+=4;

			// прочитать матрицу. В файле записана матрица 4x3, а у нас 4x4
			memcpy(&obj->matrix.rows.row0, body + pos, 12); pos+=12;
			memcpy(&obj->matrix.rows.row1, body + pos, 12); pos+=12;
			memcpy(&obj->matrix.rows.row2, body + pos, 12); pos+=12;
			memcpy(&obj->matrix.rows.position, body + pos, 12); pos+=12;
		}
		else
		if (strcmp(tagName, "mesh")==0)
		{
			// считать mesh
			if (obj)
			{
			    mesh = new CMesh();
			    obj->mesh = mesh;
			    .....
			}

		}
		else
		if (strcmp(tagName, "IBUF")==0)
		{
			// считать индекс-буфер
			if (mesh)
			{
				indexBuffer = new CIndexBuffer(mesh);
			 
				// просчитать и установить кол-во индексов
				int n;
				memcpy(&n, body + pos, 4);
				pos+=4;
				indexBuffer->SetCount(n);
				
				// просчитать индексы
				memcpy(indexBuffer->indices, body + pos, n * sizeof(int)); 
				pos+=n * sizeof(int));
			}
		}
		else
		if (strcmp(tagName, "mat ")==0)
		{
			// считать mat
			if (indexBuffer)
			{
			    material = new CMaterial();
			    indexBuffer->material = material;
			    .....
			}
		}
		else
		if (strcmp(tagName, "tex ")==0)
		{
			// считать текстуру
			if (material)
			{
				// прочитать имя файла с текстурой	
				char buf[64];
				ReadString(body, bodySize, &pos, buf, sizeof(buf));  // прочитать название объекта

				// найти в списке или загрузить текстуру	
				material->texture = scene->textureManager.Get(buf);

				// прочитать матрицу. В файле записана матрица 4x3, а у нас 4x4
				memcpy(&material->matrix.rows.row0, body + pos, 12); pos+=12;
				memcpy(&material->matrix.rows.row1, body + pos, 12); pos+=12;
				memcpy(&material->matrix.rows.row2, body + pos, 12); pos+=12;
				memcpy(&material->matrix.rows.position, body + pos, 12); pos+=12;
			}
		}
		else
		if (strcmp(tagName, "VBUF")==0)
		{
			// считать вертекс-буфер
			if (mesh)
			{
				// просчитать и установить кол-во вертексов
				int n;
				memcpy(&n, body + pos, 4); 
				pos+=4;
				mesh->SetVCount(n, mesh->normals != NULL, mesh->texCoord != NULL);

				// просчитать вертексы
				memcpy(mesh->vertices, body + pos, n * sizeof(VECTOR)); 
				pos+=n * sizeof(VECTOR);
			}
		}
		else
		if (strcmp(tagName, "NBUF")==0)
		{
			// считать буфер нормалей
			if (mesh)
			{
				// просчитать и установить кол-во нормалей
				int n;
				memcpy(&n, body + pos, 4); 
				pos+=4;
				mesh->SetVCount(n, true, mesh->texCoord != NULL);

				// просчитать нормали
				memcpy(mesh->normals, body + pos, n * sizeof(VECTOR)); 
				pos+=n * sizeof(VECTOR);
			}
		}
		else
		if (strcmp(tagName, "TBUF")==0)
		{
			// считать буфер текстурных координат
			if (mesh)
			{
				// просчитать и установить кол-во текст.координат
				int n;
				memcpy(&n, body + pos, 4); 
				pos+=4;
				mesh->SetVCount(n, mesh->normals != NULL, true);

				// просчитать текст. координаты
				memcpy(mesh->texCoords, body + pos, n * sizeof(TEXCOORD)); 
				pos+=n * sizeof(TEXCOORD);
			}
		}
		else
		if (strcmp(tagName, "cam ")==0)
		{
			// считать камеру
			.....
		}
		else
		if (strcmp(tagName, "lght")==0)
		{
			// считать свет
			.....
		}
		else
		if (strcmp(tagName, "view")==0)
		{
			// считать индекс активной камеры
			....
		}							
		else
		if (strcmp(tagName, "scen")==0)
		{
			// считать параметры сцены
			.....
		}

		// перейти к следующему тэгу
		pos = tagPos + tagSize;
	}

	// освободить body
	free(body);

	// произвести привязку объектов друг к другу
	int i,j;
	for(i=0; i < objectCount; i++)
	{
		if (objects[i]->parent)
		{
			CObject3D *searchID = objects[i]->parent;
			objects[i]->parent = NULL;

			// найти объект у которого свойство id равно searchID
			for(j=0; j < objectCount; j++)
			{
				if (objects[j]->id == searchID)
				{
					// установить i-ому объекту j-ый родительский объект	
					objects[i]->SetParent( objects[j] );
					break;
				}
			}
		}
		// если нет родительского объекта, то привязать объект к первому объекту сцены	
		if (!objects[i]->parent) objects[i]->SetParent( scene->objects );
	}

	// освободить память от массива objects
	free(objects);

	return true; 	// успешное завершение загрузки
}


Ниже предстален код, позволяющий считывать тип данных STRING. Данная функция используется в загрузчике, поэтому она должна быть объявлена выше, чем функция загрузчика.
/****** читает строку в str. Возвращает указатель на str ********/
char* ReadString(unsigned char *body, // тело файла
                 int bodySize,        // размер тела файла
                 int *pos,            // указатель на тек.байт
                 char *str,           // буфер, куда будет прочитана строка
                 int strSize          // размер буфера str
                )
(
	int i = 0;
	int p = *pos;
	while( p < bodySize && body[ p ] )
	{
		if ( i < strSize - 1 )
		{
			str[i] = (char*)body[ p ];
			i++;
		}
		p++;
	}
	str[i] = 0;
	*pos = p + 1;
	return str;
)

Для продвинутых студентов: расширения OpenGL

Студентам, которые хотят изучить возможности OpenGL сверх программы курса "Компьютерная графика", предлагается установить расширения OpenGL.

Скачать библиотеки расширения openGL

Данный архив сдедует распаковать в папку с проектом, подключить с помощью директивы #include бибиотеку glext.h и libExt.h, а также включить в проект файл libExt.cpp

Расширения OpenGL позволяют использовать мультитекстурирование, конвеерную обработку текстур, сжатые текстуры, вертекстые и текстурные шейдеры, аппаратные индексные и вертексные буферы и прочие расширенные возможности OpenGL.

Все расширения OpenGL можно разбить:

  • общие расширения, поддерживаемые всеми производителями видеокарт (функции и константы этого расширения имеют окончание _EXT или _ARB, например GL_COMBINE_EXT);
  • на расширения, поддерживаемые картами nVidia (расширения имеют окончание _NV);
  • на расширения, поддерживаемые картами ATI (расширения имеют окончание _ATI).

Рекомендуется использовать только общие расширения (_EXT или _ARB). В этом случае разработанное приложение будет работать на всех современных видеокартах.

Полное описание расширений можно найти в google.

Проверка поддержки аппаратных вертекс-буферов и индекс-буферов

	#include <windows.h>
	#include <gl/gl.h>
	#include <gl/glu.h>
	#include "libExt.h"
	#include "glExt.h"

	....

	initExtensions();
	if (isExtensionSupported("GL_ARB_vertex_buffer_object"))
	{
		// имеется поддежка аппаратных буферов
	}

Пример загрузки индекс-буфера в GPU

	GLuint handle; // переменная, объявленная в классе CIndexBuffer 

	....

	glGenBuffersARB(1, &handle); // создать один буфер и записать его дескриптор в handle
	glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, handle);  // сделать буфер текущим

	// передать данные из массива indices в аппаратный буфер в GPU
	glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 4 * indexCount, indices, GL_STATIC_DRAW_ARB);

	// сделать текущим пустой буфер
	glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);

	// освободить память от индексов, т.к. они записаны в GPU, и в CPU они больше не нужны
	free(indices); indices = NULL;

Пример загрузки вертексов, нормалей и текстурных координат в GPU

Как уже отмечалось выше, следует так организовать память под хранение вертексов, нормалей и текстурных координат, чтобы они все хранились в одном блоке памяти, зарезервированного функцией malloc. В этом большом буфере памяти сначала должен быть записан массив всех вертексов, сразу же за ним массив всех нормалей (если они есть), а сразу же за ним массив всех текстурных координат (если они есть). Таким образом, указатель vertices будет указывать на начало этого блока памяти.

Сформированный таким образом буфер передается на видеокарту в виде одного блока, в котором есть и вертексы, и нормали и текстурные координаты.

	GLuint handle; // переменная, объявленная в классе CMesh

	....

	// определить размер вертекс-буфера в зависимости от наличия нормалей и текст. координат
	int sz = sizeof(VECTOR) * vCount;
	if (hasNormals) sz+=sizeof(VECTOR) * vCount;
	if (hasTexCoords) sz+=sizeof(TEXCOORD) * vCount;

			
	glGenBuffersARB(1, &handle); // создать один буфер и записать его дескриптор в handle
	glBindBufferARB(GL_ARRAY_BUFFER_ARB, handle); // сделать буфер текущим

	// передать данные из буфера vertices (в нем же нормали и текстурные координаты) в аппаратный буфер в GPU
	glBufferDataARB(GL_ARRAY_BUFFER_ARB, sz, vertices, GL_STATIC_DRAW_ARB);

	// сделать текущим пустой буфер
	glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

	// освободить память от буфера, т.к. массив уже записан в GPU, и в CPU он больше не нужен
	free(vertices);
	vertices = NULL;
	normals = NULL;
	texCoords = NULL;


	
Следует отметить, что константу GL_STATIC_DRAW_ARB имеет смысл заменить на GL_DYNAMIC_DRAW_ARB, если подразумевается дальнейшая манипуляция координатами геометрии, например, в случае с Skin.

Удаление аппаратного буфера

	if (handle) glDeleteBuffersARB(1, &handle);

Пример простейшей программы под windows для Microsoft Visual C++

Если разрабатывается проект на Microsoft Visual C++, то следует сначала создать минимальную программу под Windows, в которую будет внедрен код отрисовки. Пример такой программы приведен ниже:


#include <windows.h>

// #include "glUnit.h"   // подключение Вашего модуля с графикой


//-----  глобальные переменные -----
HINSTANCE instance;	// дескриптор приложения
HWND gWnd;			// дескриптор главного окна
HDC gDC;			// дескриптор холста (device context) главного окна


//---- на каждое событие назначим функцию обработки данного события ----

// событие создания окна
void OnCreate(HWND wnd)
{
	gWnd = wnd;
	gDC = GetDC(wnd);	// получить device context окна


	// инициализация графики
	//....
}


// событие изменение размеров окна
void OnResize()
{
	// получить новые размеры окна	
	RECT r;
	GetClientRect(gWnd, &r);

	// установить размер окна для графики r.right x r.bottom
	//.....
}

// холостой ход приложения (отсутствие событий)
void OnIdle()
{
	// Отрисовать кадры
	// ...
}


// Событие закрытие приложения.
void OnClose()
{
	
	// деинициализация графики
	// ....
	ReleaseDC(gWnd, gDC);
}

// Событие нажатия клавиши на клавиатуре
void OnKeyDown(short key)
{
	switch(key)
	{
		case VK_ESCAPE:
			// клавиша ESC
			CloseWindow(hWnd);
			break;
	}
}


//----- функция обработки событий главного окна ----------
LRESULT CALLBACK WindowProc(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch(msg)
	{
		case WM_CREATE:
			OnCreate(wnd);
			break;
		case WM_SIZE:
			OnResize();
			break;
		case WM_KEYDOWN:
			OnKeyDown((short)wParam);
			break;
		case WM_CLOSE:
			OnClose();
			DestroyWindow(wnd);
			PostQuitMessage(0);
			break;
	}
	return DefWindowProc(wnd, msg, wParam, lParam);
}


//--------------- главная функция приложения ------------------
int APIENTRY WinMain(HINSTANCE hInstance,
					 HINSTANCE hPrevInstance,
					 LPSTR lpCmdLine,
					 int nCmdShow)
{
	// запомнить декриптор приложения
	instance = hInstance;

	// зарегистрировать класс главного окна
	WNDCLASS cls;
	cls.cbClsExtra = 0;
	cls.cbWndExtra = 0;
	cls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	cls.hCursor = LoadCursor(0, IDC_ARROW);
	cls.hIcon = 0;
	cls.hInstance = instance;
	cls.lpfnWndProc = WindowProc;
	cls.lpszClassName = "MyClass";
	cls.lpszMenuName = NULL;
	cls.style = 0;


	RegisterClass(&cls);
	

	// создать главное окно
	HWND wnd = CreateWindowEx(0, cls.lpszClassName, "Моя первая программа", WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, 0, 0, instance, 0);
	ShowWindow(wnd, SW_SHOW);


	// главный цикл приложения
	MSG msg;
	while(1)
	{
		if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) // есть событие для приложения?
		{
			// Обработать событие
			if (msg.message == WM_QUIT) break;
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// нет события - холостой ход
			OnIdle();
		}
	}

	// выход
	return 0;
};




Следует отметить, что данная программа для простоты не использует Unicode-кодировку. Для этого следует перед компиляцией в свойствах проекта (меню "Project | xxx Properties...") отключить UNICODE-кодировку (В списке "Configuration Properties -> General" найти свойство "Character Set" и установить его равным "Use Multi-Byte Character Set").

Демонстрационный проект

Демострационный проект, позволяющий воспроизводить 3D-графику средствами OpenGL с применением аппаратных буферов:

Скачать демо-проект



Обратная связь

Компания Дин-Софт
E-mail: main@dynsoft.ru


© 2011-2013 г, Евстигнеев Д.В. Дин-Софт