Цвет и текстуры
Глава 8 Цвет и текстуры
Цвет
Целый раздел, посвященный цвету, — не слишком ли расточительно? Впрочем, надо же с чего-то начинать, а работа с цветом относится к числу необходимейших навыков. Как было сказано в главе 1, цвет состоит из трех компонентов (красный, зеленый и синий). Значение каждого компонента может изменяться от О до 1. Для определения цвета используется набор RGB-значений, заданных в виде вещественных величин. Например, красный цвет задается как 1.0, 0.0, 0.0, синий цвет — 0.0, 0.0, 1.0 и т. д. Поскольку компилятор легко преобразовывает целые константы в double, иногда в тексте встречаются цвета, заданные в следующем виде: 1,0,0 (красный). Нужно лишь помнить, что эти значения всегда интерпретируются как вещественные.
ПРИМЕЧАНИЕ
Возможно, программисты для Windows станут жаловаться, что их заставляют учиться какому-то новому способу задания цвета. Я даже подумывал о создании класса C++ с несколькими конструкторами, облегчающего работу с цветами. Однако в итоге я все же решил, что это лишь вызовет дополнительные сложности, а особой пользы не принесет. Если вы привыкли к тому, что красный цвет задается тройкой 255, О, О, то вам придется все цветовые компоненты разделить на 255.
Самое простое, что можно сделать с цветом, — присвоить один цвет всему объекту. Давайте сразу посмотрим, как это делается. Выполните команду Edit ¦ Color в приложении Sample. На экране появляется стандартное окно диалога Color, в котором можно выбрать цвет для текущего выделенного объекта:
void CMainFrame::OnEditColor() {
ASSERT(m_pCurShape) ;
CColorDialog dig;
if (dIg.DoModal() i= IDOK) return;
m_pCurShape-»SetColor (GetRValue (dlg.m_cc. rgbResult) /
255.0,
GetGValue(dig.m cc.rgbResult) /
255.0,
GetBValue(dig.m_cc.rgbResult) /
255.0);
}
Окно диалога Color возвращает выбранный цвет в виде структуры COLORREF (rgbResult). Макросы с именами GetRValue, GetGValue и GetBValue извлекают из нее отдельные компоненты красного, зеленого и синего цветов, которые затем преобразуются в вещественные значения, лежащие в диапазоне от 0.0 до 1.0. Процедура завершается вызовом функции C3dShape::SetColor:
BOOL C3dShape::SetColor( double r, double g, double b)
{
ASSERT(m_pIMeshBld) ;
m_hr = m_pIMeshBld-»SetColorRGB(r, g, b) ;
return SUCCEEDED(m hr) ;
Как видите, ничего особенного здесь не происходит. Интерфейс построения сеток содержит функцию SetColorRGB, которая и выполняет всю основную работу. Просмотрев документацию по DirectX 2 SDK, вы увидите, что интерфейс построения сеток содержит также функцию SetColor, которая получает в качестве аргумента структуру D3DCOLOR. Я не стал включать поддержку этой структуры в класс C3dShape, но если она вам понадобится, то реализация будет вполне тривиальной. Если вам приходится много работать с цветами, можно создать специальный класс C++, производный от D3DCOLOR, по аналогии с классом C3dVector, производным от D3DVECTOR. В такой класс разумно включить преобразование значений COLORREF, операцию сложения и т. д.
Давайте проделаем что-нибудь поинтереснее и раскрасим случайными цветами все грани объекта (Рисунок 8-1 и раскрашенный самолет на цветной вставке). Зачем? Мне показалось, что вам будет интересно загрузить объект, первоначально нарисованный в 3D Studio, и проследить за созданием его граней. Случайная раскраска превосходно демонстрирует общую идею. Класс C3dShape содержит функцию SetFaceColor, которая применяет цвет к отдельной грани и упрощает процесс случайной раскраски всего объекта:
void CMainFrame::OnEditRandcolor() (
ASSERT(m_pCurShape) ;
int iFaces = m_pCurShape-»GetFaceCount () ;
for (int i = 0; i « iFaces; i++) { m_pCurShape-»SetFaceColor (i,
rand() % 100) / 100.0, rand() % 100) / 100.0, randf) % 100) / 100.0) ;
}
I
/b> ед¦:" Глава 8. Цвет и текстуры
Рисунок. 8-1. Самолет со случайной раскраской граней
Цвет фрейма
Возможно, вам показалось, что мы перебрали все варианты применения цвета. Однако на самом деле осталась еще одна методика, простая, но довольно важная. Когда мы занимались созданием фигур в главе 4, я уже говорил о том, что один визуальный элемент может быть присоединен сразу к нескольким фреймам, что фактически позволяет создавать копии фигуры с минимальными накладными расходами. Такая методика хорошо работает при создании 97 объектов одинакового цвета, но что делать, если нам понадобятся объекты одинаковой формы, но разных цветов?
До настоящего момента мы связывали цвет с сеткой объекта. Оказывается, связать цвет можно и с фреймом, содержащим визуальный элемент объекта. Тем самым мы указываем фрейму, что при воспроизведении визуального элемента на экране его цвет должен быть взят из фрейма, а не из сетки. Позднее мы увидим, что аналогичная методика также используется и для наложения различных текстур на дубликаты одной и той же фигуры (текстура связывается с фреймом, а не с визуальным элементом объекта).
Цвет фрейма задается функцией C3dShape::SetFrameColor. Кроме того, вы должны разрешить использование цвета фрейма, вызывая функцию C3dFrame::SetMaterialMode(D3DRMMATERIAL_FROMFRAME). В приложении Color работа с ней продемонстрирована на примере функции, которая создает красную сферу и затем копирует ее, варьируя цвет (команда Edit Color from Frame). Ниже приведен фрагмент функции, в котором создается красная сфера и первая копия:
Цвет
/b>
void CMainFrame::OnEditClrframe()
t
// Создать первую сферу C3dShape* pShape = new C3dShape;
pShape-»Create3phere (1) ;
pShape-»SetColor (1, 0, 0); // Красный цвет m_pScene-»AddChild(pShape) ;
m p3cene-»m_ShapeList. Append (pShape) ;
MakeCurrent(pShape) ;
pShape-»SetName ("Red master") ;
// Создать копию C3dShape* pClonel = m_pCurShape-»Clone () ;
m_pScene-»m_ShapeList. Append (pClonel) ;
m_pScene-»AddChild(pClonel) ;
pClonel-»SetPosition(-2, 0, 0) ;
// Задать цвет, связанный с фреймом копии pClonel-»SetMaterialMode(D3DRMMATERIAL_FROMFRAME) ;
pClonel-»SetFrameColor(0, 1, 0); // Green pClonel-»SetName ( "Green clone") ;
}
Чтобы задать цвет объекта-копии, достаточно связать этот цвет с фреймом копии (вместо визуального элемента) и вызвать функцию SetMaterialMode с аргументом D3DRMMATERIAL FROMFRAME.
Свойства материала
Различные материалы по-разному отражают свет. Пластиковые поверхности обычно выглядят тусклыми, тогда как металл хорошо отражает свет. Рассматриваемый нами механизм визуализации не генерирует отражений, однако он воспроизводит объекты с учетом их отражающих свойств.
Например, допустим, что мы хотим изобразить на экране металлический шарик, обладающий твердой полированной поверхностью с хорошими отражающими свойствами. Отраженные лучи света почти не рассеиваются, поскольку поверхность шарика достаточно гладкая. Если положить шарик под источник света, мы увидим на нем четкий блик. Более внимательное изучение показывает, что самая яркая часть отражения на самом деле имеет цвет источника света, а не цвет шарика.
Это явление связано с тем, что отраженный свет делится на два вида: диффузный и зеркальный. Диффузное отражение присутствует в нормальных условиях, оно придает объекту его естественный цвет. Зеркальное отражение возникает на блестящих поверхностях и имеет цвет освещения. Таким образом, красный металлический шарик, освещенный белым цветом, порождает красные диффузные отражения и белое зеркальное отражение.
Вид зеркальных отражений зависит от отражающих свойств поверхности. На очень блестящей поверхности зеркальное отражение ограничивается малым углом, что приводит к появлению небольших, резко очерченных бликов. На менее
/b> lly Глава 8. Цвет и текстуры
блестящем объекте (например, на воздушном шарике) зеркальное отражение занимает больше места, но выглядит более тусклым.
Чтобы объект выглядел блестящим, можно сузить угол зеркального отражения; для имитации пластиковой поверхности следует расширить этот угол. На практике нам не приходится задавать конкретное значение угла. Вместо этого мы указываем степень, в которую должен возводиться косинус этого угла при вычислении интенсивности отраженного луча. Проще говоря, маленькое значение (скажем, 5) дает пластиковую поверхность, а высокое (например, 300) — металлическую.
Для некоторых поверхностей (например, цветных металлов) цвет зеркального отражения совпадает с цветом материала, а не источника света. Если вы посмотрите на полированное золотое кольцо под ярким светом, то увидите, что блики на кольце золотые, а не белые.
Кроме того, некоторые поверхности сами излучают свет. Примеры: лампа дневного света, фосфор, люминофор электронно-лучевой трубки. Поскольку эти поверхности еще и отражают свет, расчет их освещенности достаточно сложен.
Задавая параметры материала, можно до некоторой степени управлять тем, насколько блестящим будет выглядеть материал и будет ли он самостоятельно излучать свет. Интерфейс IDirect3DRMMaterial содержит три функции, определяющие основные свойства материала: цвет испускаемого им света (если он имеется), цвет зеркального отражения и показатель степени для уравнения зеркального отражения.
Вполне возможно, что от всего сказанного вы чувствуете себя слегка не в себе. В таком случае давайте рассмотрим пример. На Рисунок 8-2 изображены сферы с различными параметрами (показателями степени и свойствами излучаемого света). Однако на печати уловить отличия между ними довольно трудно, так что я советую присмотреться к сферам на экране вашего компьютера (кроме того, посмотрите на полноценный вариант этого рисунка на цветной вкладке). Запустите приложение Color и выполните команду Edit ¦ Materials.
Рисунок. 8-2. Сферы с различными свойствами материала
Свойства материала '^il 189
Посмотрите на верхний ряд. Левый шар был создан со свойствами материала, принятыми по умолчанию. Центральный шар излучает красный свет, так что он выглядит как бы светящимся. Правый шар тоже излучает красный свет, но он обладает более высоким показателем степени зеркального отражения (400), отчего его поверхность становится более похожей на металл. Все шары в среднем ряду имеют белый цвет, но их показатели степеней равны 3, 10 и 50. В нижнем ряду все шары красные, а показатели степени равны 100, 500 и 2000.
Чтобы немного облегчить работу со свойствами материала, я создал класс C3dMaterial:
class C3dMaterial : public C3d0bject
{
public:
DECLARE_DYNAMIC(C3dMaterial) ;
C3dMaterial() ;
virtual --C3dMaterial () ;
void SetEmissiveColor(double r, double g, double b) ;
void SetSpecularPower(double p);
void SetSpecularColor(double r, double g, double b) ;
IDirect3DRMMaterial* Getlnterface() {return m_pIMat;}
protected:
IDirect3DRMMaterial* m_pIMat;
};
Ниже приведен фрагмент кода, в котором создается красный шар в середине нижнего ряда на Рисунок 8-2. Это является типичным примером использования класса C3dMaterial:
void CMainFrame::OnEditMaterials() f
pShape = new C3dShape;
p3hape-»CreateSphere (1) ;
pShape-»SetName ( "Specular power 500") m_pScene-»AddChild(pShape) ;
m_p3cene-»m_ShapeList .Append (pShape) ;
pShape-»SetPosition(0, -2, 0);
pShape-»SetColor (1, 0, 0) ;
C3dMaterial m7;
m7.SetSpecularPower(500) ;
pShape-»SetMaterial (&m7) ;
Растровые изображения
От простейших цветов мы переходим к растровым изображениям, которые служат для разных целей. Пока мы будем применять растровые изображения в
/b> 1У Глава 8. Цвет и текстуры
качестве фона для макета, а несколько позже научимся пользоваться ими для создания текстур. На момент написания книги, функции Direct3D могли работать только с изображениями в формате Public Pixel Map (PPM), поэтому я создал класс C3dlmage, который загружает растры Windows (BMP) из дискового файла или из ресурсов приложения. Наличие такого класса заметно облегчает эксперименты с изображениями, поскольку в любой Windows-системе найдется хотя бы одна программа для создания растров.
Растровые файлы Windows имеют различный формат. Во всех растровых изображениях, находящихся на прилагаемом диске CD-ROM, на один пиксель отводится 8 бит — это значит, что такие растры могут иметь не более 256 цветов. На практике количество цветов на 256-цветном экране оказывается еще меньше. Чтобы понять причину, давайте посмотрим, что происходит, когда изображение используется в качестве текстуры.
Допустим, некоторый пиксель текстуры имеет зеленый цвет. При воспроизведении поверхности на экране зеленый пиксель может быть окрашен в один из многих оттенков зеленого, в зависимости от освещения поверхности. Другими словами, для каждого цвета, входящего в изображение, механизму визуализации приходится создавать несколько оттенков. Если ваша аппаратура не имеет реальных ограничений по цветам (видеосистема отображает до 24 бит/пиксель), беспокоиться не о чем. Тем не менее, если ваше приложение должно работать в 256-цветном режиме, следует продумать распределение цветов палитры. Если изображения включают много цветов, то при их воспроизведении окажутся занятыми многие элементы палитры. Чтобы добиться наилучшего эффекта, следует поэкспериментировать с цветами перед тем, как приказать художникам нарисовать тысячу растровых изображений.
По умолчанию для воспроизведения текстуры используются 8 цветов, а для каждого цвета — 16 оттенков. Короче говоря, при разработке эффектных фоновых изображений вам придется обходиться всего восемью цветами!
Самое простое, что можно сделать с растровым изображением, — превратить его в фон для макета. Приложение Color содержит команду меню Edit [ Background Image, которая позволяет загрузить любой растр и сделать его фоном для текущего макета. Функция выглядит предельно просто:
void CMainFrame::OnEditBkgndImg() {
C3dlmage* pimg = new C3dlmage;
if ( !pImg-»Load() ) { delete pimg;
return;
}
ASSERT(m_p3cene) ;
m_pScene-»m_ImgList .Append (pimg) ;
m_pScene-»SetBackground (pimg) ;
t
Мы создаем новый объект C3dlmage и вызываем его функцию Load без аргументов. Функция Load выводит окно диалога File Open с фильтром, настроенным на отображение только BMP-файлов. После того как пользователь выберет
•!;ЙЙ^,
Растровые изображения ''vis 191
растр и нажмет кнопку ОК, растровый файл открывается и загружается в память. Графические данные хранятся в объекте C++ класса C3d Image с помощью структуры D3DRMIMAGE, которая используется механизмом визуализации при работе с растровыми изображениями.
После того как растр загружен, он присоединяется к списку изображении текущего макета, а затем включается в макет в качестве текущего фона.
ПРИМЕЧАНИЕ
Очень важно, чтобы вы случайно не удалили изображение во время его использования. Структура D3DRMIMAGE не является СОМ-объектом и не имеет счетчика обращений, так что вам придется самостоятельно следить за всеми загруженными изображениями. Если вы будете пользоваться классом СЗШтаде, то сможете предохранить свои объекты СЗсПтаде, включая их в список изображений макета. При этом ваши растры останутся в живых до удаления макета. Предупреждение относится и к текстурам, которые аналогичны в этом отношении растровым изображениям.
Запустите приложение Color и задайте в нем фоновое изображение. Обратите внимание — фоновый растр растягивается, чтобы заполнить весь макет, поэтому его пропорции изменяются в зависимости от формы окна приложения.
Должен признаться, что при реализации функции C3dScene::SetBackground я немного смошенничал. На самом деле механизм визуализации требует, чтобы в качестве фона была задана текстура, но мне показалось, что логичнее будет ограничиться растровым изображением. Функция реализована так, что ей достаточно передать растр, а текстура создается без вашего участия. На Рисунок 8-3 показан пример простого макета с одним объектом (танком) и фоновым изображением — лужайкой перед моим домом. Наличие фона заметно украшает макет.
Рисунок. 8-3. Танк с фоновым изображением
/b> lip" Глава 8. Цвет и текстуры
Текстуры
Думаю, текстуры — один из самых интересных элементов трехмерного мира. Хорошая текстура способна оживить самую заурядную фигуру. Например, благодаря текстурам обычный конус превращается в елку, а сфера — в планету. Разумеется, в действительности дело обстоит немного сложнее, но небольшая доза энтузиазма не повредит.
Текстура представляет собой растровое изображение, которое определенным образом накладывается на поверхность и заполняет ее. Текстура не изменяет координат точек поверхности и не делает ее более рельефной, а просто «раскрашивает» поверхность, подобно тому, как в театре расписывают декорации, создавая иллюзию окон, дверей и т. д. В некоторых графических программах можно действительно изменить поверхность, добавляя к ней выступы или углубления за счет применения карты микрорельефа (bump map). Поскольку в нашей системе эта возможность отсутствует, для создания эффектов придется полагаться на художественное качество наших текстур.
Растровые изображения, на основе которых строятся текстуры, должны обладать определенными атрибутами. Самый важный из них — размер изображения. Каждая сторона должны состоять из пикселей, количество которых равно целой степени двойки. Следовательно, изображения 32х32, 128><256 или 4х4 могут использоваться для создания текстур, а изображение размером 320х240 — нет. Данное ограничение призвано повысить производительность при воспроизведении текстур. Разумеется, механизм визуализации может взять любое изображение и растянуть его так, чтобы стороны приняли требуемые размеры, однако разработчики решили, что вам лучше сделать это самостоятельно, чтобы максимально сохранить степень контроля за качеством изображения.
Раз уж разговор зашел о качестве, я бы хотел напомнить вам, что если ваше приложение должно работать в системе с 256 цветами (которые на сегодняшний день являются самыми распространенными), необходимо но возможности ограничить количество цветов в текстуре. Как было сказано в разделе «Растровые изображения» на стр. 190, я постарался обойтись восемью цветами (принятым по умолчанию количеством цветов в текстуре). При этом механизм визуализации может более гибко пользоваться системной палитрой для представления всех оттенков, необходимых для воспроизведения всего макета. Разумеется, некоторые объекты обладают похожими цветами, и это также помогает снизить общие требования. Поскольку не существует «железного» правила относительно того, сколько цветов нужно для той или иной текстуры или изображения, я бы посоветовал немного поэкспериментировать с числом цветов перед тем, как приступать к созданию окончательного варианта графики.
Механизм визуализации содержит функции, которые ограничивают количество цветов, используемых на устройстве воспроизведения (по умолчанию — 32) и количество оттенков в текстуре (по умолчанию — 16). Разумеется, если все ваши пользователи работают с 24-битным цветом, вам не нужно беспокоиться об этих проблемах, поскольку механизм визуализации сгенерирует все необходимые цвета. При желании можно изменить стандартное количество цветов в текстуре и количество создаваемых оттенков одного цвета, функциями C3dTexture::SetColors и C3dTexture::SetShades соответственно.
Текстуры 'W 193
Наложение текстур
В компьютерном мире нет ничего простого (возьмите хотя бы OLE — людей, которые разбираются в нем, постоянно не хватает). К счастью, наложить текстуру гораздо легче, чем внедрить в приложение поддержку OLE, и все же придется немало потрудиться.
Текстура может накладываться на поверхность объекта четырьмя различными способами. Каждый из них связан с отдельным математическим алгоритмом, определяющим, каким образом текстура покрывает поверхность объекта. В простейшем случае (плоское покрытие) текстура в большей или меньшей степени растягивается, чтобы заполнить всю поверхность. При более сложных алгоритмах — цилиндрическом, сферическом и хромовом покрытии — объект фактически «заворачивается» в текстуру. Давайте рассмотрим каждый из четырех способов наложения текстуры на объект.
Плоское покрытие
Плоское покрытие является самым простым способом наложения текстуры на поверхность. Вероятно, его даже не следовало бы называть «покрытием», поскольку на самом деле оно ничего не покрывает, а скорее напоминает раскрашенную декорацию, повешенную перед поверхностью. Начнем с самого тривиального примера — наложения текстуры на объект с одной гранью, с применением плоского покрытия. Приложение Color позволяет вывести на экран грань с текстурой, изображенную на Рисунок 8-4 (команда Edit ¦ Insert Тех Map Face).
Рисунок. 8-4. Текстура, наложенная на повернутую грань
Грань на Рисунок 8-4 слегка повернута вокруг оси у, и текстура напоминает фотографию, на которую смотрят сбоку. Для получения такого результата к текстуре была дополнительно применена коррекция перспективы. По умолчанию механизм
/h2>
Глава 8. Цвет и текстуры
визуализации не корректирует перспективу, однако я счел эту возможность исключительно полезной, и потому библиотека 3dPlus настраивает механизм визуализации на выполнение коррекции перспективы. Впрочем, я опережаю события — давайте сначала рассмотрим функцию, построившую объект на Рисунок 8-4, и поймем, для чего же нужна коррекция перспективы. Наш объект с одной гранью создан следующим образом:
void CMainFrame::OnEditInstxface ()
{
// Создать фигуру с одной гранью //и наложить на нее текстуру C3dShape* pShape = new C3dShape() ;
D3DVECTOR vlist[] = (
(-1.0, -1.0, 0.0},
{ 1.0, -1.0, 0.0},
{ 1.0, 1.0, 0.0},
(-1.0, 1.0, 0.0} };
int iVectors = sizeof(vlist) / sizeof(D3DVECTOR);
int iFaces[] = {4, 0, 3, 2, 1, // Передняя грань 4, 0, 1, 2, 3, // Задняя грань 0);
pShape-»Create (vlist, iVectors, iFaces);
// Раскрасить заднюю грань, чтобы видеть ее pShape-»SetFaceColor(l, О, О, 1); // Синий цвет
// Загрузить текстуру C3dTexture* pTex = new C3dTexture;
if ( !pTex-»Load(IDB_Gl) ) ( return;
} m_pScene-»m_ImgList. Append (pTex) ;
// Присоединить текстуру к передней грани p3hape-»SetFaceTexture(0, pTex) ;
// Создать покрытие.
// Грань имеет размеры 2х2 единицы, поэтому
// мы масштабируем
// текстуру для того, чтобы она поместилась на грани
// ровно один раз. Кроме того, текстура также
// инвертируется,
// чтобы изображение не получилось перевернутым.
C3dWrap wrap;
wrap.Create(D3DRMWRAP_FLAT, NULL,
Текстуры '•$11 195
-1, -1, 0, // Базовая точка О, 0, 1, // Направление О, 1, 0, // Верх О, pTex-»GetHeight() -1,
// Базовая точка текстуры 0.5, -0.5);// Масштаб текстуры
//(с инверсией)
// Наложить покрытие на передний грань wrap.Apply(pShape, 0) ;
pShape-»SetName ("Face") ;
m_pScene-»AddChild(p3hape) ;
m_pScene-»m_ShapeLi St. Append (pShape) ;
MakeCurrent(pShape) ;
}
Вам может показаться, что функция получилась слишком длинной для одной грани, но меньшего кода вряд ли можно ожидать, если последовательно рассмотреть все действия. Первое, что необходимо сделать, — создать сам объект. Мы строим списки вершин и граней, а затем конструируем объект функцией C3dShape::Create. Я намеренно создал объект с двумя гранями, чтобы вы могли развернуть объект и при этом видеть его. Задняя грань окрашена в синий цвет.
Текстура передней грани загружается в новый объект C3dTexture из ресурсов приложения. Растр текстуры включается в приложение точно так же, как и любой другой ресурс — с помощью AppStudio из Visual C++. Поскольку текстура должна находиться в памяти во время работы с ней, инкапсулирующий ее объект C++ включается в список изображений макета, чтобы предотвратить случайное удаление текстуры до уничтожения макета. Затем текстура присоединяется к нужной грани объекта функцией C3dShape::SetFaceTexture.
Тем не менее присоединения текстуры к грани объекта еще недостаточно. Необходимо также определить объект-покрытие, который управляет процессом наложения текстуры на грань. В нашем случае объект C3dWrap создается с аргументом D3DRMWRAP_FLAT, определяющим плоское покрытие. Затем покрытие накладывается на переднюю грань объекта функцией C3dWrap::Apply, аргументами которой является указатель на фигуру и номер грани.
Возможно, вы заметили, что я ни слова не сказал о большей части тех 15 параметров, по которым создается покрытие. Давайте посмотрим, для чего они нужны.
Параметры покрытия
На Рисунок 8- 5 изображена текстура, наложенная на грань с применением плоского покрытия.
Держа перед глазами Рисунок 8-5, рассмотрим назначение параметров функции создания покрытия. Первый набор (параметры с третьего по пятый) задает базовую точку текстуры на грани. На Рисунок 8-5 — это -1, -1, 0. Два следующих набора параметров задают вектор направления и верхний вектор; эта пара векто-
/b> ЯЕ Глава 8. Цвет и текстуры
Рисунок* 8-5* Наложение текстуры на грань
ров определяет ориентацию покрытия по отношению к грани. Вектор направления показывает, как «движется» текстура для того, чтобы закрыть объект, а верхний вектор поворачивает текстуру на определенный угол. Далее необходимо задать положение базовой точки на текстуре. На Рисунок 8-5 базовая точка текстуры находится в левом нижнем углу (базовая точка растрового изображения расположена наверху слева и совпадает с началом координат). Два последних параметра задают масштабные коэффициенты по осям х и у. Чтобы их определить, следует предположить, что размер текстуры равен 1х1. Поскольку грань имеет размер 2х2 единицы, необходимо вдвое растянуть текстуру по каждому направлению, и правильный коэффициент будет равен 0.5. Обратите внимание — поскольку мы выбрали базовую точку текстуры внизу, масштабный коэффициент для оси у становится отрицательным. Я взял базовую точку и масштабный коэффициент с таким расчетом, чтобы изображение правильно накладывалось на грань.
Кому-то может показаться, что количество параметров слишком велико. Это действительно так, однако среди них нет ни одного лишнего. Представьте себе, что вы должны наложить изображение 4)асада дома на объект, форма которого повторяет форму дома. Необходимо позаботиться о том, чтобы нарисованный парадный вход точно попал на место парадного входа дома и чтобы труба находилась на крыше, а не на боковой стене. Для полного контроля над процессом наложения требуется много параметров.
Вероятно, труднее всего понять смысл вектора направления. Данный параметр можно рассматривать как направление, в котором необходимо двигать текстуру для того, чтобы «набросить» ее на грань. Лично мне на первых порах пришлось немало повозиться с параметрами покрытия. Я много экспериментировал, пока не убедился, что в полной мере осознал все происходящее — мои усилия окупились при освоении более сложных покрытий, которыми мы вскоре займемся.
Параметры покрытия
/h2>
Коррекция перспективы
Давайте вернемся к коррекции перспективы, о которой уже упоминалось выше на стр.194. Рассмотрим текстуру, которая представляет собой черный крест на фоне белого квадрата. Посмотрим, как эта текстура накладывается на квадратную грань, расположенную под некоторым углом к камере. Исходная ситуация изображена на Рисунок 8-6.
Рисунок. 8-6. Квадратная текстура, которая накладывается на квадратную грань, расположенную под углом к камере
Чтобы воспроизвести текстуру на грани, обе поверхности (грань и текстура) делятся на треугольники (триангулируются), как показано на Рисунок 8-7.
Грань и текстура, разделенные на треугольники
Теперь мы копируем треугольники с текстуры на грань, производя линейную интерполяцию. После того как наложение будет закончено, концы креста окажутся на серединах ребер грани, как показано на Рисунок 8-8.
Как видите, наша грань выглядит так, словно ее сложили вдоль общей стороны двух треугольников. Чтобы справиться с этой проблемой, алгоритм наложения текстуры на поверхность должен выполнять операцию деления вместо простой линейной интерполяции. Разумеется, дополнительное деление замедляет работу, но это — цена, которую приходится платить за реализм.
/h2>
Глава 8. Цвет и текстуры
Рисунок. 8-8. Текстура, наложенная без коррекции перспективы
Коррекция перспективы осуществляется функцией SetPerspective, входящей в интерфейс построения сеток. Коррекция происходит при каждом вызове функции C3dShape::Create или любой другой функции для создания фигуры, которая обращается к C3dShape::Create. Следовательно, если вы работаете с классом CSdShape, то вам не придется беспокоиться о коррекции перспективы — она включается автоматически.
Наложение разных текстур на смежные грани
Благополучно разобравшись с наложением текстур на одну грань, я решил сделать «кубик с картинками» — куб, на каждую грань которого наложена собственная текстура. Я создал куб функцией C3dShape::CreateCuboid, загрузил шесть различных текстур и наложил их все с использованием одного плоского покрытия. На двух гранях получились вполне нормальные картинки, зато остальные четыре грани содержали хаотическое нагромождение линий.
— Я знаю, сэр, пожалуйста, вызовите меня!
— Да?
— Сэр, нужно использовать разные покрытия для разных направлений! Я подумал точно так же, создал отдельное покрытие для каждого вектора направления и попробовал снова. Опять ничего не вышло. Я оказался в полном замешательстве. Пришлось вступать в переписку с разработчиками механизма визуализации и выяснять, что же было сделано неверно. Оказалось, что сведения о наложенных текстурах сохраняются для вершин, а не для граней. Таким образом, каждый раз, когда я полагал, будто накладываю текстуру на грань, на самом деле механизм визуализации накладывал ее на вершины этой грани. Каждая вершина получала информацию о текстуре, и когда в дальнейшем я пытался наложить другую текстуру на ту же самую вершину (хотя и принадлежащую смежной грани!), она перекрывала старую текстуру и обезображивала предыдущую грань. В результате все правильно происходило лишь для грани с последней наложенной текстурой, а все остальные грани, смежные с ней, оказывались испорченными.
Выход заключается в том, чтобы создать куб из шести граней, не имеющих общих вершин. Затем мы накладываем на эти грани текстуры и получаем нужный результат, как нетрудно убедиться, выполнив в приложении Color команду Edit ¦ Insert Picture Cube. Ниже приведен исходный текст функции, создающей куб с текстурами:
Параметры покрытия
/b>
void CMainFrame::OnEditPiccube()
{
// Создать куб с ребром в 2 единицы и раздельными // гранями double s = 2;
C3dShape* pShape = new C3dShape;
D3DVECTOR vlist[] = (
(-1.0, -1.0, -1.0},
( 1.0, -1.0, -1.0},
{ 1.0, -1.0, 1.0},
{-1.0, -1.0, 1.0), // Нижняя грань
(-1.0, 1.0, -1.0),
{ 1.0, 1.0, -1.0),
{ 1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0), // Верхняя грань
(-1.0, -1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, 1.0, 1.0),
(-1.0, -1.0, 1.0), // Левая грань
{ 1.0, -1.0, -1.0),
{ 1.0, 1.0, -1.0),
( 1.0, 1.0, 1.0),
{ 1.0, -1.0, 1.0), // Правая грань
(-1.0, -1.0, -1.0),
(-1.0, 1.0, -1.0),
{ 1.0, 1.0, -1.0),
{ 1.0, -1.0, -1.0), // Ближняя грань
(-1.0, -1.0, 1.0),
(-1.0, 1.0, 1.0),
{ 1.0, 1.0, 1.0),
( 1.0, -1.0, 1.0) // Дальняя грань
int iVectors = sizeof(vlist) / sizeof(D3DVECTOR);
int iFaces[] = (4, 0, 1, 2, 3,
4, 4, 7, 6, 5, 4, 8, 11, 10, 9, 4, 12, 13, 14, 15, 4, 16, 17, 18, 19, 4, 20, 23, 22, 21, 0);
/b> IJI'*' Глава 8. Цвет и текстуры
p3hape-»Create (vlist, iVectors, iFaces);
for (int i = 0; i « 6; i++) {
// Загрузить текстуру char buf[64] ;
sprintf(buf, "g%d.bmp", i+1) ;
C3dTexture* pTex = new C3dTexture;
m pScene-»m_ImgList .Append (pTex) ;
if (pTex-»Load(IDB_Gl+i) ) (
// Присоединить текстуру к грани p3hape-»SetFaceTexture(i, pTex) ;
// Получить нормаль к грани C3dVector vn = pShape-»GetFaceNormal (i) ;
// Изменить направление вектора нормали, чтобы он // показывал направление покрытия vn = -vn;
// Вычислить произвольный верхний вектор C3dVector vu •= vn.GenerateUp ();
// Создать покрытие, ориентированное по данной
// грани
C3dWrap wrap;
wrap.Create(D3DRMWRAP_FLAT, NULL,
-s/2, -s/2, -s/2, // Базовая точка vn.x, vn.y, vn.z, // Направление vu.x, vu.y, vu.z, // Верх // Базовая точка текстуры О, pTex-»GetHeight () -1, // Масштаб текстуры (с инверсией) 1.0/s, -1.0/s);
// Наложить покрытие на переднюю грань wrap.Apply(pShape, i);
} }
pShape-»SetName ("Picture cube") ;
m_pScene-»AddChild(pShape) ;
m pScene-»m_ShapeList .Append (pShape) ;
MakeCurrent(pShape) ;
)
Параметры покрытия "^l 201
Цилиндрическое покрытие
Давайте рассмотрим следующий вид покрытия, при котором текстура оборачивается вокруг объекта по цилиндрической поверхности. На Рисунок 8-9 изображен пример наложения цилиндрического покрытия на объект.
Рисунок. 8-9. Цилиндрическое покрытие
Текстура сворачивается в цилиндр, который затем проектируется на поверхность объекта. Я обнаружил, что эта методика придает кроне и стволам моих деревьев более реалистичный вид. На Рисунок 8-10 изображен результат наложения текстур с цилиндрическим покрытием. Вы можете увидеть его на экране, запустив приложение Color и выполнив команду Edit ¦ Insert Тех Map Face.
. Пример наложения текстуры с цилиндрическим покрытием
/b> ¦Д1^'' Глава 8. Цвет и текстуры
Приведенная ниже функция не очень сильно отличается от функции для наложения текстуры на одну грань, за исключением того, что дерево состоит не из одного объекта, а из двух, и вместо плоского покрытия используется цилиндрическое.
void CMainFrame::OnEditTree()
(
// Загрузить текстуры C3dTexture* pTexl = new C3dTexture;
pTexl-»Load(IDB_LEAVES) ;
m_pScene-»m_ImgList .Append (pTexl) ;
C3dTexture* pTex2 = new C3dTexture;
pTex2-»Load(IDB_BARK) ;
m__pScene-»m_ImgList. Append (pTex2) ;
// Создать цилиндрическое покрытие C3dWrap wrap;
wrap.Create(D3DRMWRAP_CYLINDER,
NULL,
0, 0, 0, // Базовая точка
О, 0, 1, // Направление
О, 1, 0, // Верх
О, 0, // Базовая точка текстуры
1, 1); // Масштаб текстуры
// Создать крону и ствол double h = (rand() % 100) / 50.0 + 1.0;
double x = ((rand() % 100) - 50) / 10.0;
double z = ((randf) % 100) - 50) / 10.0;
double у = -2;
C3dShape* pTree = new C3dShape;
pTree-»CreateCone (x, y+h/4, z, h/4, TRUE,
x, y+h, z, 0, FALSE);
m_pScene-»m_ShapeList. Append (pTree) ;
C3dShape* pTrunk = new C3dShape;
pTrunk-»CreateRod(x, y, z,
x, y+h/4, z,
h/20);
m_pScene-»m_ShapeList. Append (pTrunk) ;
pTree-»AddChild(pTrunk) ;
// Наложить текстуры pTree-»SetTexture (pTexl) ;
wrap.Apply(pTree) ;
pTrunk-»SetTexture (pTex2) ;
wrap.Apply(pTrunk) ;
Параметры покрытия '''^
203
pTree-»SetName ("Tree") ;
m_pScene-»AddChild(pTree)
MakeCurrent(pTree) ;
Обратите внимание — для кроны и ствола используется всего один объект C3dWrap. Ориентация и масштаб в обоих случаях совпадают, и нам не пришлось создавать разные объекты для покрытии. В качестве упражнения запустите приложение Color и вставьте в макет дерево. Затем разверните его так, чтобы видеть основание конуса. Можете ли вы объяснить, почему текстура выглядит так странно? Как справиться с этой проблемой?
Сферическое покрытие
Не стоит долго гадать, для чего нам нужно сферическое покрытие — разумеется, мы займемся операцией «Генезис»*. Прежде чем углубляться в волнующие подробности, давайте сразу взглянем на конечный результат. На Рисунок 8-11 показано наложение текстуры на сферу с использованием сферического покрытия (на цветной вкладке имеется более качественный вариант рисунка). Вы можете увидеть его па экране, запустив приложение Color и выполнив команду Edit ¦ Insert A World.
Рисунок. 8-11. Планета, созданная с помощью сферического покрытия
* Автор имеет ii пилу ;{ наменитый американский телесериал Star Trek, n одном и.ч .')ии:юдо¦1 которого операция «Гснсдис» превращает пустынную планету п райский уголок. — Примеч. пкрев.
/b> Ян1 Глава 8. Цвет и текстуры
На Рисунок 8-12 изображен растр, на основе которого была создана текстура.
. Текстура планеты
Как видите, для получения большой шапки полярных льдов придется нанести много льда на растр. Мне пришлось некоторое время повозиться с текстурой, прежде чем я добился приемлемых результатов. Функция очень похожа на то, что мы видели раньше, за исключением того, что покрытие стало сферическим:
void CMainFrame::OnEditInsworld() (
// Создать сферу (планету)
C3dShape* pPlanet = new C3dShape;
pPlanet-»CreateSphere (2) ;
// Загрузить текстуру C3dTexture* pTexl = new C3dTexture;
pTexl-»Load(IDB_WORLD) ;
m_pScene-»m_ImgList. Append (pTexl) ;
// Присоединить текстуру к сфере pPlanet-»SetTexture (pTexl) ;
// Создать сферическое покрытие C3dWrap wrap;
wrap.Create(D3DRMWRAPJ3PHERE,
NULL,
0, 0, 0, // Базовая точка
О, 0, 1, // Направление
О, 1, 0, // Верх
О, 0, // Базовая точка текстуры
1, 1); // Масштаб текстуры
// Наложить покрытие на сферу wrap.Apply(pPlanet) ;
pPlanet-»SetDirection(0.5, 0.8, 0);
pPlanet-»SetName ( "World" ) ;
m_pScene-»AddChild (pPlanet) ;
Параметры покрытия "тЩ: 205
m_pScene-»m_ShapeList.Append (pPlanet) ;
MakeCurrent(pPlanet) ;
}
Хромовые покрытия
Покрытия, с которыми нам приходилось работать до настоящего момента, просто накладывали текстуру на объект в фиксированном положении. В общем случае вид поверхности не зависит от положения объекта (не считая эффектов освещения). Но вдруг объект окажется блестящим? Например, на поверхности хромированного объекта отражается все, что находится вокруг него. При перемещении такого объекта отражения на нем будут незначительно меняться, но вращаться вместе с объектом они не будут.
Хромовым покрытием называется специальный вид покрытия, при котором текстура ориентируется по отношению к макету, а не к оси самого объекта. В результате возникает иллюзия отражения на поверхности объекта при его перемещении. Для получения наилучшего эффекта следует связать текстуру с фоном макета. На Рисунок 8-13 изображен пример хромового покрытия, при котором одно и то же изображение использовано в качестве текстуры и фона.
Рисунок. 8-13. Хромовое покрытие
Разглядеть этот эффект на рисунке довольно сложно, поскольку объект не вращается и, соответственно, выглядит скорее аляповато раскрашенным, нежели отражающим ближние предметы (возможно, цветной вариант этого рисунка, приведенный на вкладке, даст вам лучшее представление о том, как он смотрится на экране).
Наложение хромового покрытия требует несколько больших усилии, по сравнению с обычным, поскольку мы не можем просто наложить покрытие на фигуру и забыть о нем. При каждом перемещении объекта покрытие придется накладывать заново, поскольку при наложении на объект текстуры, имитирующей отражение, придется обеспечить ее правильную ориентацию. Можно рассматривать хромовое покрытие как неподвижное сферическое покрытие на вращающемся
/h2>
Глава 8. Цвет и текстуры
объекте. Как бы ни изменялось положение объекта, покрытие накладывается на поверхность объекта так, чтобы сохранить прежнюю ориентацию текстуры по отношению к макету. Возможно, вам покажется, что многократные наложения текстуры будут происходить медленно, но на самом деле это не так — и к тому же у нас нет выбора!
Чтобы хромовое покрытие правильно накладывалось при каждом перемещении объекта, необходимо сделать одно из двух: либо включить в цикл визуализации специальный код, который заново накладывает все хромовые покрытия на соответствующие объекты перед обновлением изображения на экране, либо потребовать, чтобы каждый объект уведомлял приложение о своем перемещении, чтобы приложение могло заново наложить хромовое покрытие. Второй способ более эффективен, поскольку он позволяет избежать наложения покрытий для неподвижных объектов, к тому же вам не придется держать в памяти список всех объектов макета с хромовыми покрытиями.
Интерфейс IDirect3DRMFrame содержит функцию AddMoveCallback, которая задает функцию, вызываемую при перемещении объекта. Я решил, что для наших целей все же следует избежать возни с уведомляющими сообщениями и вместо этого работать с хромовыми покрытиями по аналогии со всеми остальными. По этой причине я создал класс C3dChromeWrap, производный от C3dWrap, в котором реализована функция косвенного вызова для обработки перемещений фрейма. Код приложения, предназначенный для работы с хромовыми покрытиями, сильно напоминает остальные примеры для работы с покрытиями:
void CMainFrame::OnEditChroroe() {
// Загрузить фигуру C3dShape* pShape = new C3dShape;
if (!pShape-»Load() ) ( delete pShape;
return;
}
NewScene() ;
ASSERT(m_pScene) ;
// Создать хромовое покрытие m__pChromeWrap = new C3dChromeWrap;
m_pChromeWrap-»Create (pShape,
m_wnd3d.GetStage () -»GetCamera () ) ;
// Загрузить фоновое изображение макета C3dlmage* pimg = new C3dlmage;
p!mg-»Load(IDB_CHROME) ;
m_pScene-»m_InigList.Append (pimg) ;
m_pScene-»SetBackground (pimg) ;
// Загрузить текстуру C3dTexture* pTex = new C3dTexture;
pTex-»Load(IDB CHROME);
Параметры покрытия '"^Ц; 207
m_pScene-»m_ImgList .Append (pTex) ;
// Наложить текстуру pShape-»SetTexture (pTex) ;
// Сделать ее очень блестящей C3dMaterial matt-mat.SetSpecularPower (2000) ;
pShape-»SetMaterial (&mat) ;
// Присоединить новую фигуру m_pScene-»AddChild(pShape) ;
m pScene-»m_ShapeList .Append (pShape) ;
MakeCurrent(pShape) ;
// Слегка повернуть фигуру p3hape-»SetRotation (1, 1, 1, 0.03);
(
Я решил, что вам будет интересно опробовать хромовое покрытие на разных объектах, и поэтому первым делом на экране появляется окно диалога для выбора объекта. Загрузив объект, выбранный пользователем, мы создаем хромовое покрытие. Для этого необходимо задать два параметра: фигуру, на которую оно накладывается, и эталонный фрейм. В качестве эталона я выбрал камеру. Это обеспечивает постоянную ориентацию покрытия по отношению к камере, а внешний вид объекта остается постоянным при его перемещении.
Для фона макета и текстуры выбирается одно и то же растровое изображение, IDB_CHROME. Текстура присоединяется к объекту функцией C3dShape::SetTexture. Обратите внимание — мы не пользуемся функцией C3dWrsp::Apply, как это делалось для других типов покрытий; данная функция вызывается позже, во время движения фрейма. С помощью объекта C3dMaterial мы изменяем отражающие свойства поверхности и придаем ей металлический блеск. Наконец, остается лишь включить фигуру в макет и привести ее во вращение, чтобы продемонстрировать эффект хромирования.
Код класса C3dChromeWrap находится в файле 3dlmage.cpp и состоит из двух основных частей: создание покрытия и наложение его при перемещении объекта. Покрытие задается следующим образом:
BOOL C3dChromeWrap::Create(C3dShape* pShape,
C3dCamera* pCamera) f
if (!C3dWrap::Create(D3DRMWRAP_CHROME,
pCamera,
О, О, О,
О, 1, О,
О, 0, -1,
О, О,
1, -1)) (
/b> Щр* Глава 8. Цвет и текстуры
return FALSE;
// Задать функцию косвенного вызова для перемещения ASSERT(pShape) ;
m_p3hape = pShape;
IDirect3DRMFrame* pIFrame = pShape-»Get!nterface () ;
ASSERT(pIFrame) ;
m hr = pIFrame-»AddMoveCallback-(C3dChromeWrapCallback, - this);
return SUCCEEDED(m hr) ;
Начало фрагмента напоминает процесс создания покрытии другого типа, однако есть пара важных отличий. Верхний вектор имеет координаты О, О, -1, благодаря чему «соединительный шов» текстуры проходит в задней части объекта и невидим для вас. Если бы координаты этого вектора были равны О, О, 1, то в середине объекта появились бы раздражающие нарушения текстуры (зависящие от вида противоположных краев текстуры). Второй фокус заключается в том, что масштаб по оси у устанавливается равным -1, чтобы ориентация «отражений» на объекте совпадала с ориентацией фона (при условии, что в обоих случаях было использовано одинаковое изображение).
После создания покрытия следующим шагом является обращение к функции косвенного вызова фрейма AddMoveCallback. Давайте посмотрим, как работает функция косвенного вызова:
static void C3dChromeWrapCallback(IDirect3DRMFrame* pIFrame,
void* pArg, D3DVALUE delta) f
C3dChromeWrap* pThis = (C3dChromeWrap*) pArg;
ASSERT(pThis) ;
ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dChromeWrap) ) ) ;
pThis-»ApplyRelative (pThis-»m_pShape, pThis-»m pShape);
Обратите внимание — это статическая функция, не принадлежащая классу C3dChromeWrap. Соответственно, для нее не определен указатель this. Для передачи указателя this используется второй аргумент функции косвенного вызова, предоставляющий функции доступ к данным класса.
Единственная задача функции C3dChromeWrapCallback — наложение покрытия функцией ApplyRelative. Первым аргументом ApplyRelative является фрейм, на который должно налагаться покрытие, а вторым — фигура, визуальные элементы которой будут изменены при наложении хромового покрытия. Поскольку объект C3dShape содержит и то и другое, аргументы ApplyRelative в нашем случае выглядят одинаково.
Параметры покрытия ^Щ 209
Загрузка объектов с текстурами
Предположим, вы создали в 3D Studio какой-нибудь эффектный объект с текстурой, затем сохранили его в файле 3DS, а текстуру — в растровом файле Windows (BMP-файле). Такой объект можно загрузить вместе с текстурой, необходимо лишь преобразовать файл 3DS в формат . х с помощью специальной утилиты из DirectX 2 SDK. Функция C3dShape::Load берет на себя все хлопоты по загрузке текстуры. Давайте рассмотрим этот процесс более внимательно, поскольку он несколько ненадежен и когда-нибудь у вас наверняка возникнут проблемы. Научившись загружать объекты с текстурами из файлов, мы поймем, как включить те же самые файлы, содержащие объект и текстуру, в число ресурсов приложения и загрузить их более надежным способом.
Загрузка из файлов
Имена графических файлов, содержащих текстурные изображения, хранятся в файле объекта. Когда загрузчик объекта находит имя файла с текстурой, он вызывает вспомогательную функцию для загрузки изображения и создания по нему объекта-текстуры. Вскоре мы увидим, как это происходит, но пока давайте выясним, где же должны находиться указанные файлы, чтобы загрузка прошла успешно. Поскольку в файле объекта нет никакой информации о пути к файлу текстуры, а есть только его имя, файл текстуры должен находиться в одном каталоге с файлом объекта. К сожалению, на этом проблемы не кончаются. Если вы экспериментировали с нашими приложениями-примерами, то могли заметить, что загруженные при прошлых запусках файлы включаются в список последних загруженных файлов приложения. Если выбрать объект из списка, чтобы попытаться загрузить его снова, может оказаться, что объект благополучно загрузился, а текстура — нет. Дело в том, что список последних файлов содержит полный путь к файлу объекта, однако файл объекта содержит лишь имя файла текстуры (без информации о пути). Если файл объекта не находится в текущем каталоге приложения, файл текстуры не будет найден.
Как же справиться с этой проблемой? Существуют по крайней мере три решения:
• Модифицировать функцию открытия файла, чтобы перед загрузкой объекта она изменяла текущий каталог на тот, в котором находится файл объекта.
• Включить файлы объекта и текстуры в ресурсы приложения и загружать их оттуда.
• Разработать свой собственный файловый формат, в котором данные объекта и текстуры хранятся в одном файле.
Давайте посмотрим, как работает функция C3dShape::Load. Ниже приведен ее полный исходный текст:
const char* C3dShape::Load(const char* pszFileName) {
static CString strFile;
if (!pszFileName ¦¦ !strlen(pszFileName)) { // Вывести окно диалога File Open
/b> ЭД!!5' Глава 8. Цвет и текстуры
CFileDialog dig(TRUE,
NULL,
NULL,
OFN_HIDEREADONLY,
_3DOBJ_LOADFILTER,
NULL) ;
if (dlg.DoModal() != IDOK) return NULL;
// Получить путь к файлу strFile = dlg.m_ofn.IpstrFile;
} else (
strFiie = pszFileName;
)
// Удалить любые существующие визуальные элементы New () ;
// Попытаться загрузить файл ASSERT(m_pIMeshBld) ;
m_hr = m_pIMeshBld-»Load( (void*) (const char*) strFile,
NULL,
D3DRMLOAD_FROMFILE ¦ D3DRMLOAD_FIRST,
C3dLoadTextureCallback,
this);
if (FAILED(m_hr)) { return NULL;
)
AttachVisual(m_pIMeshBld) ;
m strName = "File object: ";
m_strName += pszFileName;
return strFile;
В начале функции Load определяется имя файла. Если имя файла не было передано при вызове функции, оно запрашивается у пользователя в окне диалога File Open. Затем мы удаляем из объекта-фигуры все существующие визуальные элементы и вызываем функцию интерфейса построения сеток для загрузки объекта из заданного файла на диске. Четвертый параметр, C3dLoadTextureCallback, содержит указатель на функцию, которая загружает текстурное изображение. Пятый параметр представляет собой произвольную величину, которая задается пользователем и передается функции, загружающей текстуру. Мы передаем в нем this, указатель на объект CSdShape, поскольку функция загрузки текстуры является статической и не принадлежит классу C3dShape.
Загрузка объектов с текстурами ''^Цё- 211
Давайте рассмотрим функцию косвенного вызова, в которой происходит загрузка текстуры:
static HRESULT C3dLoadTextureCallback(char* pszName, void* pArg,
LPDIRECT3DRMTEXTURE* ppITex)
{
C3dShape* pThis = (C3dShape*)•pArg;
ASSERT(pThis) ;
ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dShape) ) ) ;
ASSERT(pszName) ;
// Загрузить текстуру ASSERT(ppITex) ;
C3dTexture* pTex = new C3dTexture;
if ( !pTex-»Load( (const char* ) pszName) ) {
delete pTex;
return E FAIL;
} *ppITex = pTex-»Get!nterface () ;
// Включить текстуру в список изображений фигуры pThis-»m_ImgList .Append (pTex) ;
return NOERROR;
1
Сначала мы преобразуем пользовательский аргумент к типу указателя на класс C3dShape и проверяем его. Далее создается новый объект C3dTexture и вызывается его функция Load для того, чтобы загрузить текстуру из файла. После того как по данным изображения будет создан объект-текстура, указатель на интерфейс текстуры возвращается построителю сеток, чтобы он мог продолжить загрузку объекта.
Основная проблема приведенного выше сценария заключается в том, что созданная текстура должна оставаться в памяти до тех пор, пока существует фигура. Для этого мы не уничтожаем объект C3dTexture и заносим указатель на него в список изображений данной фигуры. Когда фигура уничтожается, вместе с ней уничтожаются все изображения (и текстуры) в списке изображений:
C3dShape::~C3dShape() {
if (m_pIVisual) m_pIVisual-»Release () ;
if (m_pIMeshBld) m_pIMeshBld-»Release () ;
m_ImgList.DeleteAll() ;
}
Возможно, вы только что подумали: «Эй, постойте-ка, ведь у нас уже имеется список изображений для всего макета!» Вы правы. Если среди аргументов функции C3dShape::Load передавать указатель на макет, то наши изображения вполне можно было бы просто занести в список изображений макета. Однако я решил,
/b> Щу Глава 8. Цвет и текстуры
что такая реализация будет выглядеть небрежно — раз текстура относится только к данной фигуре, а не ко всему макету, логичнее следить за ней на уровне фигуры. Если ведение отдельного списка изображений для каждой фигуры кажется вам излишней роскошью, попробуйте самостоятельно найти и реализовать более удачное решение.
Загрузка из ресурсов
Самый простой способ избежать всех проблем, связанных с местонахождением файлов, — внести объекты и их текстуры в число ресурсов приложения. В качестве примера я включил корпус танка и две текстуры, используемые в нем, в состав ресурсов приложения Color. В меню Edit присутствует команда Tank Hull (resource), реализованная следующим образом:
void CMainFrame::OnEditTank()
{
C3dShape* pShape = new C3dShape;
BOOL b = pShape-»Load(IDX_TANK) ;
ASSERT(b) ;
ASSERT(m_pScene) ;
m_pScene-»AddChild (pShape) ;
m_pScene-»m_ShapeList .Append (pShape) ;
MakeCurrent(pShape) ;
Все, что нам понадобилось, — идентификатор ресурса нашего объекта-танка. Но как происходит загрузка текстур? И как же включить в приложение те ресурсы, которые нам нужны?
Приведенная ниже функция устроена почти так же, как и функция загрузки из файлов. Отличие лишь в том, что загружаемые компоненты берутся из ресурса, а не из файла. Функция для загрузки объекта из ресурса выглядит следующим образом:
// Загрузить фигуру из XOF-pecypca BOOL C3dShape::Load(UINT uiResid) {
// Удалить любые существующие визуальные элементы
New () ;
// Попытаться загрузить файл ASSERT(m_pIMeshBld) ;
D3DRMLOADRESOURCE info;
info.hModule = AfxGetResourceHandle() ;
info.lpName = MAKEINTRESOURCE(uiResid);
info.lpType = "XOF";
m_hr = m_pIMeshBld-»Load (sinfo,
NULL,
D3DRMLOAD_FROMRESOURCE,
C3dLoadTextureResCallback,
this) ;
ASSERT;SUCCEEDED(m_hr)) ;
if (FAILED(m_hr)) ( return FALSE;
}
AttachVisual(m_pIMeshBld) ;
m strName = "Resource object";
return TRUE;
Обратите внимание — здесь также используется функция косвенного вызова для загрузки текстур. Только на этот раз текстуры являются ресурсами приложения и загрузка их должна осуществляться по-другому:
static HRESULT CSdLoadTextureResCallback(char* pszName, void* pArg,
LPDIRECT3DRMTEXTURE* ppITex)
{
C3dShape* pThis = (C3dShape*) pArg;
ASSERT(pThis) ;
ASSERT (pThis-»IsKindOf (RUNTIME_CLASS (C3dShape) ) ) ;
ASSERT(pszName) ;
// Загрузить текстуру ASSERT(ppITex) ;
C3dTexture* pTex = new C3dTexture;
if ( !pTex-»LoadResource ( (const char* ) pszName) ) (
delete pTex;
return E_FAIL;
} *ppITex = pTex-»Get!nterface () ;
// Включить текстуру в список изображений фигуры pThis-»m_ImgList .Append (pTex) ;
return NOERROR;
}
Обратите внимание на то, как при загрузке текстуры из ресурса используется имя текстуры (имя файла, pszName). Для этого приходится включать текстуры в ресурсы приложения таким образом, что мы могли пользоваться именем файла в качестве идентификатора ресурса.
Поскольку AppStudio не позволяет использовать обычное имя файла (например, Camo.bmp) как идентификатор ресурса, мы не сможем включить текстуру в ресурсы приложения с помощью AppStudio. Вместо этого придется вручную отредактировать файл RC2 по аналогии с тем, как мы добавляли к ресурсам файл объекта .X:
/b> iHl' Глава 8. Цвет и текстуоы
// Корпус танка
IDX_TANK XOF res\t_hull.x
camo.bmp BITMAP res\camo.bmp
camousa.bmp BITMAP res\camousa.bmp
Сами файлы находятся в подкаталоге RES каталога проекта. Разумеется, если вы собираетесь использовать имена в качестве идентификаторов ресурсов, придется позаботиться о том, чтобы имена всех файлов с текстурами были различными.
И последнее замечание: растры всегда занимают много места, и включение многочисленных растров в ресурсы заметно увеличивает размер приложения. Если вас это не устраивает, можно объединить все файлы макета в одном файле и загружать его в тот момент, когда вам понадобится воспроизвести данный макет.
Довольно о цветах
Нам довелось увидеть немало разнообразных примеров того, как цвета и текстуры комбинируются с трехмерными фигурами. Однако на этом дело не кончается — мы не рассмотрели еще один случай, связанный с текстурами, прозрачностью... и только двумя измерениями! Речь о нем пойдет в следующей главе.