Array ( )
Вход:




Главная | OpenGL | GLSL | AI | Сеть | Примеры | Библиотека

OpenGL: Загрузка формата 3DS






Этот урок продемонстрирует способ загрузки файлов .3ds. Этот формат можно создать, например,
в 3d Studio Max, а также может импортироватся и экспортироватся многоми другими программами.
Отличная утилита для конвертирования 3D-форматов - "3D Exploration". Это shareware-программа,
которую вы можете найти в интернете.
Наш загрузчик 3DS сможет загружать только имена текстур, цвета обьектов, вершины, полигоны,
и текстурные координаты. Информация о кадрах игнорируется, так как пока что у нас не будет
покадровой анимации.

В уроке будет вращающаяся 3D-модель лица с текстурой. Клавиши управления:

ЛКМ: изменение режима рендера с нормального на каркасный и наоборот.
ПКМ: Вкл/Выкл освещение.
ВЛЕВО: Увелич. скорость вращения влево
ВПРАВО: Увелич. скорость вращения вправо


Исходный код взят из урока "загрузка текстур", добавлены два новых файла: 3ds.h и 3ds.cpp.


Файл main.h:
// Этот файл включает все структуры, необходимые нам для загрузки 3ds-файла. Если вы решите
// загружать и анимацию, нужно будет кое-что сюда добавить.

// Сначала включим новые хидеры:
#include <vector>
#include <math.h>
// Обьявим пространство имён:
using namespace std;

// Структура 3D-точки
struct CVector3{
    float x,y,z;
};

// Структура 2D-точки
struct CVector2{
    float x,y;
};

// Это структура полигона. Она используется для индексирования массивов координат
// вершин и текстур. Эта информация сообщает нам о том, какие номера вершин в массиве
// какому полигону принадлежат. То же самое касается текстурных координат.
struct tFace
{
    int vertIndex[3];           // indicies for the verts that make up this triangle
    int coordIndex[3];          // indicies for the tex coords to texture this face
};


// Эта структура хранит информацию о материале. Это может быть текстурная карта света.
// Некоторые значения не используются, но я оставил их, чтобы могли увидеть их для
// примера.
struct tMaterialInfo
{
    char  strName[255];         // Имя текстуры
    char  strFile[255];         // Имя файла текстуры
    BYTE  color[3];             // Цвет обьекта (R, G, B)
    int   texureId;             // ID текстуры
    float uTile;                // u-tiling текстуры (Сейчас не используется)
    float vTile;                // v-tiling текстуры (Сейчас не используется)
    float uOffset;              // u-offset текстуры (Сейчас не используется)
    float vOffset;              // v-offset текстуры (Сейчас не используется)
};




// Содержит всю информацию о модели/сцене.
// В реальном проекте лучше оберните всё это в класс с
// функциями вроде LoadModel(...); DrawObject(...); DrawModel(...); DestroyModel(...);
struct t3DObject
{
    int  numOfVerts;            // Число вершин в модели
    int  numOfFaces;            // Число полигонов в модели
    int  numTexVertex;          // Число текстурных координат
    int  materialID;            // ID текстуры для использования, индекс массива текстур
    bool bHasTexture;           // TRUE если есть текстурная карта для этого обьекта
    char strName[255];          // Имя обьекта
    CVector3  *pVerts;          // Массив вершин обьекта
    CVector3  *pNormals;            // Нормали обьекта
    CVector2  *pTexVerts;           // Текстурные координаты
    tFace *pFaces;              // Полигоны обьекта
};



// Содержит информацию о модели. Тоже неплохо бы обернуть в класс. Мы будем использовать
// класс вектора из STL (Standart Template Library) чтобы уменьшить трудности при связывании
// параметров.
struct t3DModel
{
    int numOfObjects;           // Число обьектов в модели
    int numOfMaterials;         // Число материалов модели
    vector<tMaterialInfo> pMaterials;   // Число обьектов материалов (текстуры и цвета)
    vector<t3DObject> pObject;      // Список обьектов в модели
};
 




Что такое STL (Standard Template Library) Vector?

Давайте я коротко обьясню, что такое векторы, если вы этого не знаете.
Чтобы использовать векторы, сначала нужно включить в программу и использовать
пространство имён: using namespace std;
Вектор - это базирующийся на массивах список ссылок. Он позволяет динамически
добавлять или удалять элементы. По сути это темплейт-класс, так что могут быть использованы
списки ЛЮБОГО типа. Чтобы создать вектор с типом "int", нужно ввести:
vector myIntList;
Теперь вы можете добавлять int-ы в динамический массив, говоря: myIntList.push_back(10);
Или: myIntList.push_back(num);. Чем больше вы добавляете элементов, тем больше становится
ваш массив. С векторами вы можете использовать индексы так же, как и с обычными массивами:
myIntList[0] = 0; Чтобы удалить элемент, используется функция pop_back(). Чтобы очистить
вектор, используется clear(). Но это очистит список не во всех случаях, например не в случае,
если у вас есть структуры данных, требующие очистки "изнутри", как например наш обьект.


Теперь создадим два новых файла, описывающих загрузку .3ds
Файл 3ds.h:
#ifndef _3DS_H
#define _3DS_H

//>------ Главный Chunk, в начале каждого 3ds-файла
#define PRIMARY       0x4D4D

//>------ Главнык Chunk-и
#define OBJECTINFO    0x3D3D            // Это предоставляет версию меша перед информацией об обьекте
#define VERSION       0x0002            // Предоставляет версию .3ds файла
#define EDITKEYFRAME  0xB000            // Хидер для всей информации о кадрах

//>------ под-дефайны OBJECTINFO
#define MATERIAL      0xAFFF        // Информация о текстурах
#define OBJECT        0x4000        // Полигоны, вершины, и т.д...

//>------ под-дефайны для MATERIAL
#define MATNAME       0xA000            // Название материала
#define MATDIFFUSE    0xA020            // Хранит цвет обьекта/материала
#define MATMAP        0xA200            // Хидер для нового материала
#define MATMAPFILE    0xA300            // Хранит имя файла текстуры

#define OBJECT_MESH   0x4100            // Даёт нам знать, что начинаем считывать новый обьект

//>------ под-дефайны для OBJECT_MESH
#define OBJECT_VERTICES     0x4110      // Вершины обьекта
#define OBJECT_FACES        0x4120      // Полигоны обьекта
#define OBJECT_MATERIAL     0x4130      // Дефайн находится, если обьект имеет материал, иначе цвет/текстура
#define OBJECT_UV       0x4140      // UV текстурные координаты


// Структура для индексов 3DS (так как .3ds хранит 4 unsigned short)
struct tIndices {
    unsigned short a, b, c, bVisible;   // Это хранит индексы для точки 1,2,3 массива
                        // вершин, плюс флаг видимости
};

// Хранит информацию о chunk-е
struct tChunk
{
    unsigned short int ID;          // ID chunk-а
    unsigned int length;            // Длинна chunk-а
    unsigned int bytesRead;         // Число читаемых байт для этого chunk-а
};

// Класс содержит весь код загрузки
class CLoad3DS
{
public:
    CLoad3DS();     // Тут будут инициализироватся все данные

    // Эта ф-я и будет вызыватся для загрузки 3DS
    bool Import3DS(t3DModel *pModel, char *strFileName);

private:
    // Читает строку и сохраняет её в переданный массив char-ов
    int GetString(char *);

    // Читает следующий chunk
    void ReadChunk(tChunk *);

    // Читает следующий длинный chunk
    void ProcessNextChunk(t3DModel *pModel, tChunk *);

    // Читает chunk-и обьекта
    void ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *);

    // Читает chunk-и материала
    void ProcessNextMaterialChunk(t3DModel *pModel, tChunk *);

    // Читает RGB-значение цвета обьекта
    void ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk);

    // Читает вершины обьекта
    void ReadVertices(t3DObject *pObject, tChunk *);

    // Читает информацию полигонов обьекта
    void ReadVertexIndices(t3DObject *pObject, tChunk *);

    // Читает текстурные координаты обьекта
    void ReadUVCoordinates(t3DObject *pObject, tChunk *);

    // Читает имя материала, присвоенного обьекту, и устанавливает materialID
    void ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk);

    // Рассчитывает нормали вершин обьекта
    void ComputeNormals(t3DModel *pModel);

    // This frees memory and closes the file
    // Освобождает память и закрывает файл
    void CleanUp();

    // Указатель на файл
    FILE *m_FilePointer;
};


#endif





Файл 3ds.cpp:
#include "main.h"
#include "3ds.h"

int gBuffer[50000] = {0};   // Используется для чтения нежелательных данных

// В этом файле находится весь код, необходимый для загрузки файлов .3ds.
// Как оно работает: вы загружаете chunk, затем проверяете его ID.
// В зависимости от его ID, загружаете информацию, хранящуюся в chunk-е.
// Если вы не хотите читать эту информацию, читаете дальше неё.
// Вы знаете, как много байт нужно пропустить, так как каждый chunk хранит
// свою длинну в байтах.


///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\*
/////
/////   Конструктор инициализирует данные tChunk
/////
///////////////////////////////// CLOAD3DS \\\\\\\\\\\\\\\\*

CLoad3DS::CLoad3DS()
{
    m_FilePointer = NULL;
}

///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\*
/////
/////   Вызывается клиентом для открытия, чтения и затем очистки .3ds
/////
///////////////////////////////// IMPORT 3DS \\\\\\\\\\\\\\\\*

bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName)
{
    char strMessage[255] = {0};
    tChunk currentChunk = {0};

    // Открываем .3ds файл
    m_FilePointer = fopen(strFileName, "rb");

    // Убедимся, что указатель на файл верен (мы открыли файл)
    if(!m_FilePointer)
    {
        sprintf(strMessage, "Unable to find the file: %s!", strFileName);
        MessageBox(NULL, strMessage, "Error", MB_OK);
        return false;
    }

    // Открыв файл, нужно прочитать хидер файла, чтобы убедится, что это 3DS.
    // Если это верный файл, то первым ID chunk-а будет PRIMARY

    // Читаем первый chunk файла, чтобы убедится, что это 3DS
    ReadChunk(&currentChunk);

    // Убедимся, что это 3DS
    if (currentChunk.ID != PRIMARY)
    {
        sprintf(strMessage, "Unable to load PRIMARY chuck from file: %s!", strFileName);
        MessageBox(NULL, strMessage, "Error", MB_OK);
        return false;
    }

    // Теперь начинаем чтение данных. ProcessNextChunk() - рекурсивная функция

    // Начинаем загрузку обьектов вызовом рекурсивной функции
    ProcessNextChunk(pModel, &currentChunk);

    // После прочтения всего файла нам нужно рассчитать нормали вершин
    ComputeNormals(pModel);

    // В конце подчищаем всё
    CleanUp();

    return true;
}

///////////////////////////////// CLEAN UP \\\\\\\\\\\\\\\\*
/////
/////   Функция чистит всю занятую память и закрывает файл
/////
///////////////////////////////// CLEAN UP \\\\\\\\\\\\\\\\*

void CLoad3DS::CleanUp()
{
    if (m_FilePointer) {
        fclose(m_FilePointer);  // Закрываем файл
        m_FilePointer = NULL;
    }
}


///////////////////////////////// PROCESS NEXT CHUNK\\\\\\\\\\\\\\\\*
/////
/////   Функция читает главную секцию файла, затем рекурсивно идёт глубже
/////
///////////////////////////////// PROCESS NEXT CHUNK\\\\\\\\\\\\\\\\*

void CLoad3DS::ProcessNextChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
    t3DObject newObject = {0};      // Используется для добавления обьекта в список
    tMaterialInfo newTexture = {0};     // Используется для добавления материала

    tChunk currentChunk = {0};      // Текущий chunk для загрузки
    tChunk tempChunk = {0};         // Временный chunk для хранения данных

    // Ниже проверяем ID chunk-a каждый раз при чтении нового. Затем,
    // если нужно вытащить данные из chunk-а, делаем это. Если же этот chunk нам
    // не нужен, просто читаем chunk в "мусорный" массив.

    // Продолжаем читать подсекции, пока не дойдем до общей длинны файла.
    // После чтения ЧЕГО УГОДНО, увеличиваем прочитанные байты и сравниваем
    // их с общей длинной.
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // Читаем следующий chunk
        ReadChunk(&currentChunk);

        // Получаем chunk ID
        switch (currentChunk.ID)
        {
        case VERSION:           // Версия файла

            // Читаем версию файла и добавляем прочитанные байты в переменную bytesRead
            currentChunk.bytesRead += fread(gBuffer, 1, currentChunk.length -
                        currentChunk.bytesRead, m_FilePointer);

            // Если версия файла больше 3, выведем предупреждение, что могут
            // возникнуть проблемы.
            if ((currentChunk.length - currentChunk.bytesRead == 4) && (gBuffer[0] > 0x03)) {
                MessageBox(NULL, "This 3DS file is over version 3 so it may load incorrectly",
                        "Warning", MB_OK);
            }
            break;

        case OBJECTINFO:        // Содержит версию меша
            {
            // Этот chunk содержит версию меша. Также это заголовок для chunk-ов MATERIAL
            // и OBJECT. Отсюда мы начинаем читать информацию материалов и обьектов.

            // Читаем следующий chunk
            ReadChunk(&tempChunk);

            // Получаем версию меша
            tempChunk.bytesRead += fread(gBuffer, 1, tempChunk.length -
                        tempChunk.bytesRead, m_FilePointer);

            // Увеличиваем bytesRead на число прочитанных байт
            currentChunk.bytesRead += tempChunk.bytesRead;

            // Переходим к следующему chunk-у, это будет MATERIAL, затем OBJECT
            ProcessNextChunk(pModel, &currentChunk);
            break;
        }
        case MATERIAL:          // Содержит информацию о материале

            // Этот chunk - хидер для информации о материале

            // Увеличиваем число материалов
            pModel->numOfMaterials++;


            // Добавляем пустую структуру текстуры в наш массив текстур.
            pModel->pMaterials.push_back(newTexture);

            // Вызываем функцию, обрабатывающую материал
            ProcessNextMaterialChunk(pModel, &currentChunk);
            break;

        case OBJECT:            // Хранит имя читаемого обьекта

            // Этот chunk - хидер для chunk-ов, хранящих информацию обьекта.
            // Также он хранит имя обьекта.

            // Увеличиваем счетчик обьектов
            pModel->numOfObjects++;

            // Добавляем новый элемент tObject к списку обьектов
            pModel->pObject.push_back(newObject);

            // Инициализируем обьект и все его данные
            memset(&(pModel->pObject[pModel->numOfObjects - 1]), 0, sizeof(t3DObject));

            // Получаем и сохраняем имя обьекта, затем увеличиваем счетчик прочитанных байт
            currentChunk.bytesRead += GetString(pModel->pObject[pModel->numOfObjects - 1].strName);

            // Переходим к чтению оставшейся информации обьекта
            ProcessNextObjectChunk(pModel, &(pModel->pObject[pModel->numOfObjects - 1]), &currentChunk);
            break;

        case EDITKEYFRAME:

            // Так как я хотел сделать ПРОСТОЙ урок, насколько это возможно, я не включил
            // информацию о покадровой анимации. Этот chunk - хидер для всей информации
            // об анимации. В будущих уроках этот аспект будет детально описан.

            //ProcessNextKeyFrameChunk(pModel, currentChunk);

            // Читаем в "мусорный" контейнер ненужные данные и увеличиваем счетчик
            currentChunk.bytesRead += fread(gBuffer, 1, currentChunk.length -
                        currentChunk.bytesRead, m_FilePointer);
            break;

        default:

            // Остальные chunk-и, которые нам не нужны, будут обработаны здесь. Нам
            // всё ещё нужно прочитать в "мусорную" переменную неизвестные или игнорируемые
            // chunk-и и увеличить счетчик прочитанных байт.
            currentChunk.bytesRead += fread(gBuffer, 1, currentChunk.length -
                        currentChunk.bytesRead, m_FilePointer);
            break;
        }

        // Прибавим прочитанные байты последнего chunk-а к счетчику
        pPreviousChunk->bytesRead += currentChunk.bytesRead;
    }
}


///////////////////////////////// PROCESS NEXT OBJECT CHUNK \\\\\\\\\\\\\\\\*
/////
/////   Функция сохраняет всю информацию об обьекте
/////
///////////////////////////////// PROCESS NEXT OBJECT CHUNK \\\\\\\\\\\\\\\\*

void CLoad3DS::ProcessNextObjectChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
    // Текущий chunk, с которым работаем
    tChunk currentChunk = {0};

    // Продолжаем читать эти chunk-и, пока не дошли до конца этой секции
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // Читаем следующую секцию
        ReadChunk(&currentChunk);

        // Проверяем, что это за секция
        switch (currentChunk.ID)
        {
        case OBJECT_MESH:       // Даёт нам знать, что мы читаем новый обьект

            // Нашли новый обьект, прочитаем его информацию рекурсией
            ProcessNextObjectChunk(pModel, pObject, &currentChunk);
            break;

        case OBJECT_VERTICES:       // Вершины нашего обьекта
            ReadVertices(pObject, &currentChunk);
            break;

        case OBJECT_FACES:      // Полигоны обьекта
            ReadVertexIndices(pObject, &currentChunk);
            break;

        case OBJECT_MATERIAL:       // Имя материала обьекта

            // Эта секция хранит имя материала, связанного с текущим обьектом. Это может быть
            // цвет или текстурная карта. Эта секция также содержит полигоны, к которым
            // привязана текстура (Если например на обьекте несколько текстур, или просто
            // текстура наложено только на часть обьекта). Сейчас у нас будет только одна
            // текстура на весь обьект, так что получим только ID материала.

            // Теперь мы читаем имя материала, привязанного к обьекту
            ReadObjectMaterial(pModel, pObject, &currentChunk);
            break;

        case OBJECT_UV:     // Хранит текстурные координаты обьекта

            // Эта секция содержит все UV-координаты обьекта. Прочитаем их.
            ReadUVCoordinates(pObject, &currentChunk);
            break;

        default:

            // Read past the ignored or unknown chunks
            // Читаем игнорируемые/неизвестные данные в "мусорный" массив
            currentChunk.bytesRead += fread(gBuffer, 1, currentChunk.length
                        - currentChunk.bytesRead, m_FilePointer);
            break;
        }

        // Прибавляем прочитанные данные к счетчику
        pPreviousChunk->bytesRead += currentChunk.bytesRead;
    }
}


///////////////////////////////// PROCESS NEXT MATERIAL CHUNK \\\\\\\\\\\\\\\\*
/////
/////   Эта функция хранит всю информацию о материале (текстуре)
/////
///////////////////////////////// PROCESS NEXT MATERIAL CHUNK \\\\\\\\\\\\\\\\*

void CLoad3DS::ProcessNextMaterialChunk(t3DModel *pModel, tChunk *pPreviousChunk)
{
    // Текущий chunk для работы
    tChunk currentChunk = {0};

    // Продолжаем читать эти chunk-и, пока не дошли до конца подсекции
    while (pPreviousChunk->bytesRead < pPreviousChunk->length)
    {
        // Читаем следующую секцию
        ReadChunk(&currentChunk);

        // Проверяем, что именно мы прочитали
        switch (currentChunk.ID)
        {
        case MATNAME:       // Эта секция хранит имя материала

            // читаем имя материала
            currentChunk.bytesRead +=
                        fread(pModel->pMaterials[pModel->numOfMaterials - 1].strName,
                        1, currentChunk.length - currentChunk.bytesRead, m_FilePointer);
            break;

        case MATDIFFUSE:        // Хранит RGB-цвет обьекта
            ReadColorChunk(&(pModel->pMaterials[pModel->numOfMaterials - 1]), &currentChunk);
            break;

        case MATMAP:            // Это хидер информации о текстуре

            // Читаем информацию информацию о материале
            ProcessNextMaterialChunk(pModel, &currentChunk);
            break;

        case MATMAPFILE:        // Хранит имя файла материала

            // Читаем имя файла материала
            currentChunk.bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strFile,
                        1, currentChunk.length - currentChunk.bytesRead, m_FilePointer);
            break;

        default:

            // Читаем остальные данные в "мусор"
            currentChunk.bytesRead += fread(gBuffer, 1,
                        currentChunk.length - currentChunk.bytesRead,
                        m_FilePointer);
            break;
        }

        // Прибавляем прочитанные данные к счетчику
        pPreviousChunk->bytesRead += currentChunk.bytesRead;
    }
}

///////////////////////////////// READ CHUNK \\\\\\\\\\\\\\\\*
/////
/////   Функция читает ID chunk-а и его длинну в байтах
/////
///////////////////////////////// READ CHUNK \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadChunk(tChunk *pChunk)
{
    // Функция читает ID секции (2 байта).
    // ID chunk-а - это, например, OBJECT/MATERIAL. Это говорит нам,
    // какие данные могут быть прочитаны в этой секции.
    pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);

    // Затем читаем длинну секции (4 байта). Теперь мы знаем,
    // сколько данных нам нужно будет прочитать.
    pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
}

///////////////////////////////// GET STRING \\\\\\\\\\\\\\\\*
/////
/////   Читает строку в массив char-ов
/////
///////////////////////////////// GET STRING \\\\\\\\\\\\\\\\*

int CLoad3DS::GetString(char *pBuffer)
{
    int index = 0;

    // Читаем 1 байт данных, первую букву строки
    fread(pBuffer, 1, 1, m_FilePointer);

    // Цикл, пока не получаем NULL
    while (*(pBuffer + index++) != 0) {

        // Читаем символы всё время, пока не получим NULL
        fread(pBuffer + index, 1, 1, m_FilePointer);
    }

    // Вернём длинну строки, т.е. сколько байтов мы прочитали (включая NULL)
    return strlen(pBuffer) + 1;
}


///////////////////////////////// READ COLOR \\\\\\\\\\\\\\\\*
/////
/////   Читает данные RGB-цвета
/////
///////////////////////////////// READ COLOR \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadColorChunk(tMaterialInfo *pMaterial, tChunk *pChunk)
{
    tChunk tempChunk = {0};

    // Читаем информацию о цвете
    ReadChunk(&tempChunk);

    // Читаем RGB-цвет (3 байта - от 0 до 255)
    tempChunk.bytesRead += fread(pMaterial->color, 1, tempChunk.length - tempChunk.bytesRead, m_FilePointer);

    // Увеличиваем счетчик
    pChunk->bytesRead += tempChunk.bytesRead;
}


///////////////////////////////// READ VERTEX INDECES \\\\\\\\\\\\\\\\*
/////
/////   Функция читает индексы для массива вершин
/////
///////////////////////////////// READ VERTEX INDECES \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadVertexIndices(t3DObject *pObject, tChunk *pPreviousChunk)
{
    unsigned short index = 0;       // Используется для чтения индекса текущего полигона

    // Чтобы прочитать индексы вершин для обьекта, нужно сначала прочитать их
    // число, затем уже их самих. Запомните, нам нужно прочитать только 3 из
    // 4 значений для каждого полигона. Это четвертое значение - флаг видимости
    // для 3DS Max, которое ничего для нас не значит.

    // Читаем число полигонов этого обьекта
    pPreviousChunk->bytesRead += fread(&pObject->numOfFaces, 1, 2, m_FilePointer);

    // Выделяем достаточно памяти для полигонов и инициализируем структуру
    pObject->pFaces = new tFace [pObject->numOfFaces];
    memset(pObject->pFaces, 0, sizeof(tFace) * pObject->numOfFaces);

    // Проходим через все полигоны этого обьекта
    for(int i = 0; i < pObject->numOfFaces; i++)
    {
        // Далее читаем A-B-C индексы для полигона, но игнорируем 4-е значение.
        for(int j = 0; j < 4; j++)
        {
            // Читаем первый индекс вершины для текущего полигона
            pPreviousChunk->bytesRead += fread(&index, 1, sizeof(index), m_FilePointer);

            if(j < 3)
            {
                // Сохраняем индекс в структуру полигонов
                pObject->pFaces[i].vertIndex[j] = index;
            }
        }
    }
}


///////////////////////////////// READ UV COORDINATES \\\\\\\\\\\\\\\\*
/////
/////   Функция читает UV-координаты обьекта
/////
///////////////////////////////// READ UV COORDINATES \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadUVCoordinates(t3DObject *pObject, tChunk *pPreviousChunk)
{
    // Чтобы прочитать индексы UV-координат для обьекта, сначала нужно
    // прочитать их полное количество, потом уже их самих.

    // Читаем число UV-координат
    pPreviousChunk->bytesRead += fread(&pObject->numTexVertex, 1, 2, m_FilePointer);

    // Выделяем память для хранения UV-координат
    pObject->pTexVerts = new CVector2 [pObject->numTexVertex];

    // Читаем текстурные координаты (массив из 2х float)
    pPreviousChunk->bytesRead += fread(pObject->pTexVerts, 1,
        pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);
}


///////////////////////////////// READ VERTICES \\\\\\\\\\\\\\\\*
/////
/////   Функция читает вершины обьекта
/////
///////////////////////////////// READ VERTICES \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadVertices(t3DObject *pObject, tChunk *pPreviousChunk)
{
    // Как и в большинстве chunk-ов, прежде чем читать сами вершины,
    // нужно найти их количество.

    // Читаем число вершин
    pPreviousChunk->bytesRead += fread(&(pObject->numOfVerts), 1, 2, m_FilePointer);

    // Выделяем память для вершин и инициализируем структуру
    pObject->pVerts = new CVector3 [pObject->numOfVerts];
    memset(pObject->pVerts, 0, sizeof(CVector3) * pObject->numOfVerts);

    // Читаем в массив вершин (массив из 3 float)
    pPreviousChunk->bytesRead += fread(pObject->pVerts, 1,
        pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);

    // Теперь все вершины прочитаны. Так как в моделях 3DS Max всегда перевёрнуты
    // оси, нужно поменять Y-значения и Z-значения наших вершин.

    // Проходим через все вершины и меняем y<->z
    for(int i = 0; i < pObject->numOfVerts; i++)
    {
        // Сохраняем старое знач-е Y
        float fTempY = pObject->pVerts[i].y;

        // Устанавливаем значение Y в Z
        pObject->pVerts[i].y = pObject->pVerts[i].z;

        // Устанавливаем значение Z в Y
        // И делаем его отрицательным, т.к. в 3ds max Z-ось перевернута
        pObject->pVerts[i].z = -fTempY;
    }
}


///////////////////////////////// READ OBJECT MATERIAL \\\\\\\\\\\\\\\\*
/////
/////   Функция читает имя материала, наложенного на обьект, и устанавливает materialID
/////
///////////////////////////////// READ OBJECT MATERIAL \\\\\\\\\\\\\\\\*

void CLoad3DS::ReadObjectMaterial(t3DModel *pModel, t3DObject *pObject, tChunk *pPreviousChunk)
{
    char strMaterial[255] = {0};        // Хранит имя материала

    // *Что такое материал?* - Материал - это цвет + текстурная карта обьекта.
    // Также он можетхранить другую информацию типа яркости, "блестящести" и т.д.
    // Сейчас нам нужно только цвет или имя текстурной карты.

    // Читаем имя материала, привязанного к текущему обьекту.
    // strMaterial теперь должен содержать строку с именем материала, типа "Material #2" и т.д...
    pPreviousChunk->bytesRead += GetString(strMaterial);

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

    // Проходим через все материалы
    for(int i = 0; i < pModel->numOfMaterials; i++)
    {
        // Если только что прочитанный материал совпадает с именем данного
        if(strcmp(strMaterial, pModel->pMaterials[i].strName) == 0)
        {
            // Проверяем, есть ли текстурная карта. Если strFile содержит
            // строку с длинной >=1, текстура есть.
            if(strlen(pModel->pMaterials[i].strFile) > 0)
            {
                // Устанавливаем ID материала в текущий индекс 'i' и заканчиваем проверку
                pObject->materialID = i;

                // Устанавливаем флаг текстурирования в true
                pObject->bHasTexture = true;
            }
            break;
        }
        else
        {
            // Проверяем флаг, чтобы увидеть, есть ли уже текстура на этом обьекте
            if(pObject->bHasTexture != true)
            {
                // Устанавливаем ID материала в -1, чтобы указать, что материала для обьекта нет
                pObject->materialID = -1;
            }
        }
    }

    // Остальное читаем в "мусор"
    pPreviousChunk->bytesRead += fread(gBuffer, 1,
        pPreviousChunk->length - pPreviousChunk->bytesRead, m_FilePointer);
}

// *Note*
//
// Ниже идут несколько математических функций, вычисляющих нормали вершин. Они нам
// нужны, чтобы эффект освещения рассчитывался верно. В прошлых уроках мы уже писали
// эти функции, так что при желании можно просто подключить файлы 3dmath.h/.cpp

//////////////////////////////  Math Functions  ////////////////////////////////*

// Рассчитывает величину нормали (magnitude = sqrt(x^2 + y^2 + z^2)
#define Mag(Normal) (sqrt(Normal.x*Normal.x + Normal.y*Normal.y + Normal.z*Normal.z))

// Рассчитывает векторы между 2 точками и возвращает результат
CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2)
{
    CVector3 vVector;           // Хранит результирующий вектор

    vVector.x = vPoint1.x - vPoint2.x;
    vVector.y = vPoint1.y - vPoint2.y;
    vVector.z = vPoint1.z - vPoint2.z;

    return vVector;             // Вернём результирующий вектор
}

// This adds 2 vectors together and returns the result
// Складывает 2 вектора и возвращает результат
CVector3 AddVector(CVector3 vVector1, CVector3 vVector2)
{
    CVector3 vResult;               // Хранит результирующий вектор

    vResult.x = vVector2.x + vVector1.x;
    vResult.y = vVector2.y + vVector1.y;
    vResult.z = vVector2.z + vVector1.z;

    return vResult;                 // Вернём результат
}

// Делит вектор на переданный номер и возвращает результат
CVector3 DivideVectorByScaler(CVector3 vVector1, float Scaler)
{
    CVector3 vResult;

    vResult.x = vVector1.x / Scaler;
    vResult.y = vVector1.y / Scaler;
    vResult.z = vVector1.z / Scaler;

    return vResult;
}

// Возвращает скалярное произведение (dot product) двух векторов
CVector3 Cross(CVector3 vVector1, CVector3 vVector2)
{
    CVector3 vCross;

    vCross.x = ((vVector1.y * vVector2.z) - (vVector1.z * vVector2.y));
    vCross.y = ((vVector1.z * vVector2.x) - (vVector1.x * vVector2.z));
    vCross.z = ((vVector1.x * vVector2.y) - (vVector1.y * vVector2.x));

    return vCross;
}

// Возвращает нормаль вектора
CVector3 Normalize(CVector3 vNormal)
{
    double Magnitude;

    Magnitude = Mag(vNormal);

    vNormal.x /= (float)Magnitude;
    vNormal.y /= (float)Magnitude;
    vNormal.z /= (float)Magnitude;

    return vNormal;
}

///////////////////////////////// COMPUTER NORMALS \\\\\\\\\\\\\\\\*
/////
/////   Функция рассчитывает нормали для обьекта и его вершин
/////
///////////////////////////////// COMPUTER NORMALS \\\\\\\\\\\\\\\\*

void CLoad3DS::ComputeNormals(t3DModel *pModel)
{
    CVector3 vVector1, vVector2, vNormal, vPoly[3];

    // Если обьектов нет, пропускаем этот шаг
    if(pModel->numOfObjects <= 0)
        return;

    // Что такое нормали вершин? Чем они отличаются от остальных нормалей? Если вы
    // нашли нормаль треугольника, это "нормаль полигона". Если вы передали OpenGL
    // нормаль полигона для освещения, ваш обьект будет выглядеть плоским и резким.
    // Если же вы найдете нормали для каждой вершины, освещенный обьект будет
    // выглядеть сглаженным, т.е. более реалистичным.

    // Проходим через все обьекты для вычисления их вершин
    for(int index = 0; index < pModel->numOfObjects; index++)
    {
        // Получим текущий обьект
        t3DObject *pObject = &(pModel->pObject[index]);

        // Выделяем память под нужные переменные
        CVector3 *pNormals      = new CVector3 [pObject->numOfFaces];
        CVector3 *pTempNormals  = new CVector3 [pObject->numOfFaces];
        pObject->pNormals       = new CVector3 [pObject->numOfVerts];

        // Проходим через все полигоны обьекта
        for(int i=0; i < pObject->numOfFaces; i++)
        {
            // Сохраняем 3 точки этого полигона, чтобы избежать большого кода
            vPoly[0] = pObject->pVerts[pObject->pFaces[i].vertIndex[0]];
            vPoly[1] = pObject->pVerts[pObject->pFaces[i].vertIndex[1]];
            vPoly[2] = pObject->pVerts[pObject->pFaces[i].vertIndex[2]];

            // Теперь вычислим нормали полигонов

            vVector1 = Vector(vPoly[0], vPoly[2]);  // вектор полигона (из 2х его сторон)
            vVector2 = Vector(vPoly[2], vPoly[1]);  // Второй вектор полигона

            vNormal  = Cross(vVector1, vVector2);   // получаем cross product векторов
            pTempNormals[i] = vNormal;      // временно сохраняем не-нормализированную нормаль
                                // для вершин
            vNormal  = Normalize(vNormal);      // нормализируем cross product для нормалей полигона

            pNormals[i] = vNormal;          // Сохраняем нормаль в массив
        }

        //////////////// Теперь получаем вершинные нормали /////////////////

        CVector3 vSum = {0.0, 0.0, 0.0};
        CVector3 vZero = vSum;
        int shared=0;

        for (int i = 0; i < pObject->numOfVerts; i++)   // Проходим через все вершины
        {
            for (int j = 0; j < pObject->numOfFaces; j++)   // Проходим через все треугольники
            {               // Проверяем, используется ли вершина другим полигоном
                if (pObject->pFaces[j].vertIndex[0] == i ||
                    pObject->pFaces[j].vertIndex[1] == i ||
                    pObject->pFaces[j].vertIndex[2] == i)
                {
                    vSum = AddVector(vSum, pTempNormals[j]);    // Прибавляем не-
                                // нормализированную нормаль другого полигона
                    shared++;       // Увеличиваем число полигонов с общими вершиными
                }
            }

            // Получаем нормаль делением на сумму общих полигонов. Делаем её отрицат.
            pObject->pNormals[i] = DivideVectorByScaler(vSum, float(-shared));

            // Нормализуем нормаль для вершины
            pObject->pNormals[i] = Normalize(pObject->pNormals[i]);

            vSum = vZero;           // Сбрасываем сумму
            shared = 0;         // И общие полигоны
        }

        // Освобождаем память временных переменных
        delete [] pTempNormals;
        delete [] pNormals;
    }
}
 



Это был БОЛЬШОЙ обьем знаний, и, наверно, пока что самый большой урок!
В следующих уроках мы узнаем, как загружать текстовые файлы .obj
Это самый распространённый 3D-формат, который понимает почти ВЕСЬ софт.

Ещё раз обращаю ваше внимание на то, что системы координат 3DS Max и OpenGL - разные.
Посколько ось Z в 3DS расположена вертикально, мы должны поменять местами значения
Y и Z. Такэе поскольку мы поменяли местами Y и Z, нужно сделать значение Z отрицательным,
чтобы модель была корректной.


CHUNK: Что это такое?

chunk ID - уникальный код, идентифицирующий тип данных в этом chunk-е. Длинна chunk-а
показывает длинну последующих данных, относящихся к этому chunk-у. Запомните, чтобы
пропустить ненужные данные раздела, нужно прочитать в мусорную переменную число байт
ненужных данных.

В двух словах, обьявление chunk-а выглядит так:
2 байта - хранит ID раздела (OBJECT, MATERIAL, PRIMARY, и т.д...)
4 байта - хранит длинну этого раздела. С её помощью вы определяете, прочитан ли уже этот раздел.

Таким образом, чтобы начать чтение 3DS файла,вы читаете первые 2 байта, затем длинну.
Первые 2 байта должны быть chunk-ом PRIMARY, иначе это не 3ds-файл.

Ниже - список порядка, в котором расположены разделы в файле .3ds
Для большей информации об этом формате, прочитайте 3ds_format.rtf


// MAIN3DS (0x4D4D)
// |
// +--EDIT3DS (0x3D3D)
// | |
// | +--EDIT_MATERIAL (0xAFFF)
// | | |
// | | +--MAT_NAME01 (0xA000) (See mli Doc)
// | |
// | +--EDIT_CONFIG1 (0x0100)
// | +--EDIT_CONFIG2 (0x3E3D)
// | +--EDIT_VIEW_P1 (0x7012)
// | | |
// | | +--TOP (0x0001)
// | | +--BOTTOM (0x0002)
// | | +--LEFT (0x0003)
// | | +--RIGHT (0x0004)
// | | +--FRONT (0x0005)
// | | +--BACK (0x0006)
// | | +--USER (0x0007)
// | | +--CAMERA (0xFFFF)
// | | +--LIGHT (0x0009)
// | | +--DISABLED (0x0010)
// | | +--BOGUS (0x0011)
// | |
// | +--EDIT_VIEW_P2 (0x7011)
// | | |
// | | +--TOP (0x0001)
// | | +--BOTTOM (0x0002)
// | | +--LEFT (0x0003)
// | | +--RIGHT (0x0004)
// | | +--FRONT (0x0005)
// | | +--BACK (0x0006)
// | | +--USER (0x0007)
// | | +--CAMERA (0xFFFF)
// | | +--LIGHT (0x0009)
// | | +--DISABLED (0x0010)
// | | +--BOGUS (0x0011)
// | |
// | +--EDIT_VIEW_P3 (0x7020)
// | +--EDIT_VIEW1 (0x7001)
// | +--EDIT_BACKGR (0x1200)
// | +--EDIT_AMBIENT (0x2100)
// | +--EDIT_OBJECT (0x4000)
// | | |
// | | +--OBJ_TRIMESH (0x4100)
// | | | |
// | | | +--TRI_VERTEXL (0x4110)
// | | | +--TRI_VERTEXOPTIONS (0x4111)
// | | | +--TRI_MAPPINGCOORS (0x4140)
// | | | +--TRI_MAPPINGSTANDARD (0x4170)
// | | | +--TRI_FACEL1 (0x4120)
// | | | | |
// | | | | +--TRI_SMOOTH (0x4150)
// | | | | +--TRI_MATERIAL (0x4130)
// | | | |
// | | | +--TRI_LOCAL (0x4160)
// | | | +--TRI_VISIBLE (0x4165)
// | | |
// | | +--OBJ_LIGHT (0x4600)
// | | | |
// | | | +--LIT_OFF (0x4620)
// | | | +--LIT_SPOT (0x4610)
// | | | +--LIT_UNKNWN01 (0x465A)
// | | |
// | | +--OBJ_CAMERA (0x4700)
// | | | |
// | | | +--CAM_UNKNWN01 (0x4710)
// | | | +--CAM_UNKNWN02 (0x4720)
// | | |
// | | +--OBJ_UNKNWN01 (0x4710)
// | | +--OBJ_UNKNWN02 (0x4720)
// | |
// | +--EDIT_UNKNW01 (0x1100)
// | +--EDIT_UNKNW02 (0x1201)
// | +--EDIT_UNKNW03 (0x1300)
// | +--EDIT_UNKNW04 (0x1400)
// | +--EDIT_UNKNW05 (0x1420)
// | +--EDIT_UNKNW06 (0x1450)
// | +--EDIT_UNKNW07 (0x1500)
// | +--EDIT_UNKNW08 (0x2200)
// | +--EDIT_UNKNW09 (0x2201)
// | +--EDIT_UNKNW10 (0x2210)
// | +--EDIT_UNKNW11 (0x2300)
// | +--EDIT_UNKNW12 (0x2302)
// | +--EDIT_UNKNW13 (0x2000)
// | +--EDIT_UNKNW14 (0xAFFF)
// |
// +--KEYF3DS (0xB000)
// |
// +--KEYF_UNKNWN01 (0xB00A)
// +--............. (0x7001) ( viewport, same as editor )
// +--KEYF_FRAMES (0xB008)
// +--KEYF_UNKNWN02 (0xB009)
// +--KEYF_OBJDES (0xB002)
// |
// +--KEYF_OBJHIERARCH (0xB010)
// +--KEYF_OBJDUMMYNAME (0xB011)
// +--KEYF_OBJUNKNWN01 (0xB013)
// +--KEYF_OBJUNKNWN02 (0xB014)
// +--KEYF_OBJUNKNWN03 (0xB015)
// +--KEYF_OBJPIVOT (0xB020)
// +--KEYF_OBJUNKNWN04 (0xB021)
// +--KEYF_OBJUNKNWN05 (0xB022)


Зная, как читать chunk-и, всё, что вам нужно знать - ID нужного раздела.
В этом уроке было очень уж много информации для одного раза. В следующем уроке
я в основном просто более подробно обьясню то, что мы сделали в этом.



Файл main.cpp:
// В начале файла добавим новый инклуд и новые переменные:
#include "3ds.h"

// Имя файла для загрузки:
#define FILE_NAME  "face.3ds"

TextureImage Textures[100];         // текстуры

CLoad3DS g_Load3ds;             // Наш 3DS класс.
t3DModel g_3DModel;             // Хранит загруженную 3D-модель

int   g_ViewMode    = GL_TRIANGLES;     // По умолчанию режим рендера - GL_TRIANGLES
bool  g_bLighting   = true;         // Триггер освещения
float g_RotateX     = 0.0f;         // Угол вращения модели
float g_RotationSpeed   = 0.8f;         // Скорость вращения




//////////////////////////////////////////////////////////////////////////////////////////////
//
// Модифицируем функцию init():
//

    // Сначала загружаем наш .3ds файл. Передаём указатель на структуру и имя файла.
    g_Load3ds.Import3DS(&g_3DModel, FILE_NAME);


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

    // Проходим через все материалы
    for(int i = 0; i < g_3DModel.numOfMaterials; i++)
    {
        // Проверяем, есть ли в загруженном материале имя файла
        if(strlen(g_3DModel.pMaterials[i].strFile) > 0)
        {
            // Используем имя файла для загрузки битмапа с текстурным ID (i).
            Texture->LoadTexture(IL_BMP, g_3DModel.pMaterials[i].strFile, &textures[i]);
        }

        // Устанавливаем ID текстуры для этого материала
        g_3DModel.pMaterials[i].texureId = i;
    }

    // Включаем освещение и цветные материалы.

    glEnable(GL_LIGHT0);
    glEnable(GL_LIGHTING);
    glEnable(GL_COLOR_MATERIAL);








///////////////////////////////////////////////////////////////////////////////////////////
//
// Добавим в функцию DeInit():
//
    for(int i = 0; i < g_3DModel.numOfObjects; i++)
    {
        // Очищаем структуры
        delete [] g_3DModel.pObject[i].pFaces;
        delete [] g_3DModel.pObject[i].pNormals;
        delete [] g_3DModel.pObject[i].pVerts;
        delete [] g_3DModel.pObject[i].pTexVerts;
    }








////////////////////////////////////////////////////////////////////////////////////////////
//
// Изменяем функцию RenderScene():
//

void RenderScene()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    gluLookAt(0, 1.5f, 8,0, 0.5f, 0,0, 1, 0);

    // Мы хотим, чтобы модель вращалась вокруг своей оси, так что передадим переменную
    // вращения.

    glRotatef(g_RotateX, 0, 1.0f, 0);   // Вращаем обьект по Y-оси
    g_RotateX += g_RotationSpeed;       // Увеличиваем скорость вращения

    // Я попытаюсь обьяснить, что происходит дальше. У нас есть модель с некоторым
    // количеством обьектов и текстур. Мы хотим пройти через все обьекты модели, биндя
    // на них текстуры, и рендерить их.
    // Чтобы отрендерить текущий обьект, проходим через все его полигоны. Полигон - это
    // просто один из (в данном случае) треугольников обьекта. Например, в кубе есть 12
    // полигонов, так как каждая из его 6 сторон содержит 2 треугольника. Вы можете подумать,
    // что если в обьекте 12 треугольника, то в нём 36 вершин. На самом деле это не так.
    // Так как многие вершины одинаковы, потому что используются разными сторонами куба,
    // нужно сохранять только 8 вершин, и игнорировать дублированные. Таким образом
    // у вас будет массив уникальных вершин обьекта, что сбережет большое число
    // памяти. После этого сохраняется массив индексов вершин для каждого полигона,
    // каждый индекс указывает на вершину в массиве вершин. Это может показатся громоздким,
    // но на самом деле это гораздо лучше, чем сохранять дублирующиеся вершины. То же
    // самое касается текстурных UV-координат. Вам не нужно сохранять дублирующиеся UV
    // координаты: сохраняете только уникальные, а затем создаёте массив индексов.
    // Это может смущать, но большинство форматов 3D-файлов так и делают.
    // Цикл ниже будет оставатся неизменным для большинства форматов, всё, что вам
    // нужно будет изменить - код загрузки. (это не касается анимации)


    // Так как мы знаем число обьектов в нашей модели, проходим через каждый из них
    for(int i = 0; i < g_3DModel.numOfObjects; i++)
    {
        // Убедимся, что передан верный обьект
        if(g_3DModel.pObject.size() <= 0) break;

        // Получим текущий обьект
        t3DObject *pObject = &g_3DModel.pObject[i];

        // Проверим, имеет ли обьект тексурную карту, если да - биндим на него текстуру
        if(pObject->bHasTexture) {

            // Включаем текстуры
            glEnable(GL_TEXTURE_2D);

            // Сбрасываем цвет
            glColor3ub(255, 255, 255);

            // Биндим текстурную карту на обьект по его materialID
            glBindTexture(GL_TEXTURE_2D, textures[materialID].texID);
        } else {

            // Иначе выключим текстуры
            glDisable(GL_TEXTURE_2D);

            // И сбросим цвет на нормальный
            glColor3ub(255, 255, 255);
        }

        // Начинаем отрисовку в выбранном режиме
        glBegin(g_ViewMode);    // Рисуем обьекты (треугольники или линии)

            // Проходим через все полигоны обьекта и рисуем их
            for(int j = 0; j < pObject->numOfFaces; j++)
            {
                // Проходим через каждый угол треугольника и рисуем его
                for(int whichVertex = 0; whichVertex < 3; whichVertex++)
                {
                    // Get the index for each point of the face
                    // Получаем индекс для каждой точки полигона
                    int index = pObject->pFaces[j].vertIndex[whichVertex];

                    // Передаём OpenGL нормаль этой вершины
                    glNormal3f(pObject->pNormals[ index ].x,
                        pObject->pNormals[ index ].y, pObject->pNormals[ index ].z);

                    // Если обьект имеет текстуру, передаем текст. координаты
                    if(pObject->bHasTexture) {

                        // Убедимся, что UVW-мап применена на обьект, иначе
                        // он не будет иметь текстурных координат
                        if(pObject->pTexVerts) {
                            glTexCoord2f(pObject->pTexVerts[ index ].x, pObject->pTexVerts[ index ].y);
                        }
                    } else {

                        // Убедимся, что у нас есть верный материал/цвет, привязанный
                        // к обьекту. Вообще практически всегда к обьекту привязан как
                        // минимум цвет, но просто на всякий случай проверим это.
                        // Если размер материала минимум 1, и materialID != -1,
                        // материал верен.
                        if(g_3DModel.pMaterials.size() && pObject->materialID >= 0)
                        {
                            // Получаем и устанавливаем цвет обьекта, если он
                            // не имеет текстуры
                            BYTE *pColor = g_3DModel.pMaterials[pObject->materialID].color;

                            // Применяем цвет к модели
                            glColor3ub(pColor[0], pColor[1], pColor[2]);
                        }
                    }

                    // Передаём текущую вершину обьекта
                    glVertex3f(pObject->pVerts[ index ].x, pObject->pVerts[ index ].y,
                                pObject->pVerts[ index ].z);
                }
            }

        glEnd();
    }

    SwapBuffers(g_hDC);
}












///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Изменим блок обработки клавиш в функции WinProc().
// Клавиши управления:
//
// Left Mouse Button - Изменяет режим рендера с нормального на сетку
// Right Mouse Button - Вкл/Выкл освещение
// Left Arrow Key - Увелич. вращение влево
// Right Arrow Key - Увелич. вращение вправо
// Escape - Выход

    case WM_LBUTTONDOWN:

        if(g_ViewMode == GL_TRIANGLES) {
            g_ViewMode = GL_LINE_STRIP;
        } else {
            g_ViewMode = GL_TRIANGLES;
        }
        break;

    case WM_RBUTTONDOWN:

        g_bLighting = !g_bLighting;

        if(g_bLighting) {
            glEnable(GL_LIGHTING);
        } else {
            glDisable(GL_LIGHTING);
        }
        break;

    case WM_KEYDOWN:

        switch(wParam) {
            case VK_ESCAPE:
                PostQuitMessage(0);
                break;

            case VK_LEFT:
                g_RotationSpeed -= 0.05f;
                break;

            case VK_RIGHT:
                g_RotationSpeed += 0.05f;
                break;
        }
        break;






Исходные коды к уроку




Комментарии:

Войдите, чтобы оставить комментарий:












Яндекс.Метрика
 Яндекс цитирования.