ГЛАВА 10 Визуальные эффекты

Источник света и свойства материала
Туман
Двусторонние поверхности
Соприкасающиеся поверхности
Частичная прозрачность объемных фигур
Наложение текстуры на трехмерные объекты
Механизм трехмерной игры
Что вы узнали в этой главе
Последняя глава в основном посвящена рассмотрению вопросов повышения реалистичности создаваемых построений.
Примеры располагаются в каталоге \Examples\Chapter10.

Источник света и свойства материала
Изучив предыдущие примеры, вы получили представление о направленном источнике света и материале объектов. Теперь нам предстоит разобраться с этими вещами основательнее.
Направленный источник располагается в бесконечности. Вектор, задаваемый при его инициализации, определяет направление потока испускаемых лучей. Лучи света параллельны. Интенсивность источника постоянна для каждой точки пространства. Данный источник света можно считать моделью солнечного освещения.
При такой модели освещения если для всех вершин квадрата задать одну и ту же нормаль, то при любом его положении все точки имеют один и тот же цвет. Цвет этот определяется комбинацией цвета материала и источника света. Если квадрат материала желтого цвета освещать белым светом, результат будет точно таким же, как и при освещении квадрата белого материала источником света с наложенным желтым светофильтром.
Для получения действительно реалистичных изображений направленный источник не годится в принципе, например, стены комнаты будут иметь ровный оттенок. Для таких целей предусмотрен точечный источник света, отличающийся от направленного именно тем, что при его использовании учитывается реальное положение источника в пространстве. Точечный источник света похож на лампочку или свечу, лучи света испускаются из какой-то точки во всех направлениях.
Помимо положения, параметрами такого источника являются его интенсивность и ослабление. Интенсивность точечного источника - это его изначальная яркость, мощность. Явно она не задается, ее определяют значения цветовых составляющих поля Diffuse. Ослабление складывается из нескольких составляющих: область действия источника и коэффициенты, задающие закон ослабления освещенности. Область действия определяется линейной характеристикой, расстоянием. Все точки, расположенные от источника дальше этого расстояния, никак им не освещаются. Коэффициенты закона ослабления (их три) задают, как падает освещенность в пространстве. Первый коэффициент соответствует неизменному, постоянному освещению. Если установить такое правило, то, независимо от расстояния до источника света, все точки, попадающие в область освещения, освещаются одинаково. Второй коэффициент соответствует линейному затуханию. По мере удаления от источника света интенсивность освещения падает по линейному закону так, что на границе области его интенсивность становится нулевой. Последний коэффициент определяет квадратичное убывание интенсивности, степень падения освещенности - квадрат расстояния.
Коэффициенты задаются вещественными, обычно их значения нулевые или единичные. Самой распространенной схемой является линейный закон убывания, но вы можете строить и собственный, сложный закон освещенности, а не использовать определенную схему (если задать единичными все три коэффициента, интенсивность падает по полиномиальному закону).
Давайте закрепим пройденное, познакомившись с проектом каталога Ex01, в котором на экране рисуется тор. Во внутренней области тора перемещается точечный источник света, в точке его текущего положения рисуется сфера (рис. 10.1).

Рис.10.1. Пример использования точечного источника света

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

procedure TfrmD3D.SetupLights;
var
Material : TD3DMaterial8;
begin
Material := InitMaterial(1, 1, 0, 0); // Материал желтого цвета
FDSDDevice.SetMaterial(Material);
ZeroMemory(@Light, SizeOf(Light));
with Light do begin
_Type := D3DLIGHT_POINT; // Тип источника - точечный
Diffuse.R := 1.0; // Цвет источника
Diffuse.G := 1.0;
Diffuse.В := 1.0;
Specular := Diffuse; // Дополнительные параметры
Ambient := Diffuse;
Position := DSDVector(0.0, 0.0, 0.0); // Позиция в пространстве
AttenuationO := 1.0; // Коэффициенты закона ослабления
Attenuationl := 1.0;
Attenuation2 := 1.0;
Range := 2.5; // Расстояние, задающее область освещенности
end;
FD3DDevice.SetLight(0, Light);
FDSDDevice.LightEnable(0, True);
end;

Первое поле записи содержит константу, задающую тип источника. Структура Diffuse определяет цветовой фильтр, накладываемый на источник. Позиция источника света будет устанавливаться в текущей системе координат, ее значение остается действующим до следующего вызова метода SetLight (не обязательно заново инициализировать все поля структуры). Чтобы сфера освещалась так, как будто источник света находится внутри нее, необходимо переместить источник света в ее систему координат:

procedure TfrmD3D.DrawScene;
var
matTranslate, matScale : TDSDMatrix;
begin
// Вычисляем текущее положение источника
Light.Position := DSDVector(0.0, cos (Angle) * 2, 0.0);
with FDSDDevice do begin
// Возвращаем мировую систему координат
SetTransform(D3DTS_WORLD, IdentityMatrix);
// Устанавливаем источник света в новом положении
SetLight(0, Light);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 864); // Вывод тора
end;
// Источник света будет внутри сферы
Light.Position := D3DVector(0.О, 0.0, 0.0);
// Матрица трансформаций для сферы
SetTranslateMatrix(matTranslate, 0.0, cos (Angle) * 2, 0.0);
SetScaleMatrix(matScale, 0.1, 0.1, 0.1);
with FDBDDevice do begin
SetTransform(D3DTS_WORLD, MatrixMul(matScale, matTranslate));
SetLight(0, Light);
DrawPrimitive(D3DPT_TRIANGLELIST, 864 * 3, 1200);
end;
end;

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

Рис. 10.2. Эту композицию сделаем тестовой для дальнейших иллюстраций

Матрицы трансформаций полностью заполняются один раз, в начале работы приложения:

procedure TfrmD3D.FormCreate(Sender: TObject);
var
hRet : HRESULT;
matView, matProj : TD3DMatrix;
matRotate, matTranslate, matScale : TD3DMatrix;
begin
hRet := InitDSD;
if Failed (hRet) then ErrorOut ('InitDBD', hRet);
hRet := InitVB;
if Failed (hRet) then ErrorOut ( ' InitVertex' , hRet);
// Голубоватый материал конуса
MaterialConus := InitMaterial(0, 0.5, 1, 0) ;
// Белый материал стен комнаты
MaterialWhite := InitMaterial(1, 1, I, 0);
// Светло-коричневый материал сферы
MaterialSphere := InitMaterial(1, 0.5, 0, 0) ;
// Точка зрения задается один раз
SetViewMatrix(matview, D3DVector(0, 0, 2.577), D3DVector(0, 0, -5),
D3DVector(0, 1, 0));
FD3DDevice.SetTransform(D3DTS_VIEW, matView);
// Матрица проекций
SetProjectionMatrix (matProj, 1, 1, 1, 10);
FD3DDevice.SetTransform(D3DTS_PROJECTION, matProj);
// Инициализация источников света
SetupLights;
// Поворот конуса вокруг оси X
SetRotateXMatrix(matRotate, -Pi / 2);
// Переносим конус, его вершина в центре сцены
SetTranslateMatrixfmatTranslate, 0.0, -1.0, 0.0);
// Масштабируем конус
SetScaleMatrixfmatScale, 0.25, 1.0, 0.2);
// Матрица трансформаций конуса вычисляется один раз
matCone := MatrixMul(matScale, MatrixMul(matTranslate, matRotate));
// Инициализация матрицы трансформаций сферы
matSphere := IdentityMatrix;
// Переносим сферу по оси Y
matSphere._42 := -0.5;
end;

Я ввел в сцену четыре источника света. Три точечных источника предназначены для освещения стен комнаты, конус и сфера освещаются направленным источником света:

procedure TfrmDSD.SetupLights,
var
LightO : TD3DLight8;
Lightl : TD3DLight8;
Light2 : TD3DLight8;
Light3 : TD3DLight8;
begin
ZeroMemory(@LightO, SizeOf(LightO));
with LightO do begin
Type := D3DLIGHT POINT;
Diffuse.r := 0.4; // Поскольку присутствует три источника,
Diffuse.g := 0.4; // их яркость задается небольшой
Diffuse.b := 0.4;
Specular := Diffuse;
Ambient := Diffuse;
Position := D3DVector(0.5, 0.75, 1.5);
AttenuationO := 1.0;
Attenuationl := 1.0;
Attenuation2 := 0.0;
Range := 2.56; end;
ZeroMemory(@Light1, SizeOf(Light1));
with Lightl do begin
_Type := D3DLIGHT_POINT;
Diffuse.r := 0.4;
Diffuse.g := 0.4;
Diffuse.b := 0.4;
Specular := Diffuse;
Ambient := Diffuse;
Position := D3DVector(0.5, 0.3, 0.3);
AttenuationO := 1.0;
Attenuationl := 1.0;
Attenuation2 := 0.0;
Range := 2.5;
end;
ZeroMemory(@Light2, SizeOf(Lightl));
with Light2 do begin
_Type := D3DLIGHT_POINT;
Diffuse.r := 0.4;
Diffuse.g := 0.4;
Diffuse.b := 0.4;
Specular := Diffuse;
Ambient := Diffuse;
Position := DSDVector(0.5, -0.3, 0.3);
AttenuationO := 1.0;
Attenuationl := 1.0;
Attenuation2 := 0.0;
Range := 2.5;
end;
// Один направленный источник света
Lights:=InitDirectionalLight(DSDVector(-0.5, -0.5, -1),
1.0, 1.0, 1.0, 0);
// Источники только инициализируются, но пока не включаются
with FDSDDevice do begin SetLight(0, LightO);
SetLight(1, Lightl);
SetLight(2, Light2);
SetLight(3, Light3);
end;
end;

При рисовании объектов включаем только определенные источники света:

procedure TfrmD3D.DrawScene;
begin
// Стены комнаты - 10 независимых треугольников
with FD3DDevice do begin
// Матрица идентичности возвращает в мировую систему координат
SetTransform(D3DTS_WORLD, IdentityMatrix);
SetMaterial(Materialwhite); // Стены из белого материала
LightEnable(0, True); // Работают только точечные источники
LightEnabled, True);
LightEnable (2, True);
LightEnable(3, False); // Направленный источник выключаем
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 10);
end;
// Конус и сфера освещаются только направленным источником with FDSDDevice do begin
LightEnable(0, False);
LightEnabled, False);
LightEnable(2, False);
LightEnable(3, True);
SetMaterial(MaterialConus); // Синий материал конуса
SetTransform(D3DTS_WORLD, matCone); // Неизменное положение конуса
DrawPrimitive(D3DPTJTRIANGLEFAN, 30, 49); // Сам конус
DrawPrimitive(D3DPT_TRIANGLEFAN, 81, 49); // Основание конуса
end;
// Перемещаем сферу в новое положение
matSphere._41 := cos (Angle) / 2; // Меняем только два элемента
matSphere._43 := sin (Angle) / 2; // текущей матрицы трансформаций
// Вывод сферы; источник света - текущий, направленный
with FDSDDevice do begin
// Переносим систему координат
SetTransform(D3DTS_WORLD, matSphere);
SetMaterial(MaterialSphere) ;
DrawPrimitive(D3DPTJFRIANGLELIST, 30 + 51 + 51, 1200);
end;
end;

Обратите внимание, что среди задаваемых режимов воспроизведения появилось что-то новое для нас.

with FD3DDevice do begin
// Все вершины примитивов перечисляются по часовой стрелке
SetRenderState(D3DRS_CULLMODE, D3DCOLL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZBJTRUE);
SetRenderState(D3DRS_AMBIENT, S00202020);
SetRenderState{D3DRS_LIGHTING, Dword (True));
// Конус масштабируется, поэтому включаем пересчет нормалей
SetRenderState(D3DRS_NORMALIZENORMALS, DWORD (True));
end;

Включение режима DSDRS_AMBIENT равносильно включению дополнительного источника света, эмулирующего окружающую среду. Свет этого рассеянного источника излучается из всех направлений. Предназначен такой источник для передачи присутствия на сцене, в данном случае, воздуха, в котором лучи света рассеиваются во всех направлениях.
Записи, определяющие источник света и материал, содержат поля Diffuse, Ambient и specular. Первая структура соответствует диффузным свойствам объекта: для источника света это светофильтр, накладываемый на него; для материала это непосредственно цвет материала, та составляющая падающего света, которая не поглощается поверхностью. Это самая весомая составляющая получающегося цвета. Вторая, рассеянная составляющая проявляется в областях, примыкающих к области, на которую непосредственно падает свет. Используется она в комбинации с третьей, зеркальной составляющей для передачи таких свойств, как гладкость или матовость. Комбинируя значения этих составляющих, можно получать яркие или тусклые блики на поверхности объекта.
Разницы в том, задаются оптические свойства материала или источника, нет, но вы можете комбинировать свойства источника и материала для того, чтобы передать, что на сцене присутствуют, например, светящиеся объекты и объекты с обычными свойствами.
Проект из каталога Ех03 наглядно демонстрирует смысл атрибутов свойств материала и источника света. Это развитие примера с тором. Теперь мы можем произвольно задавать значения всех параметров (рис. 10.3).

Рис. 10.3. Пример с заданием цветовых параметров

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

procedure TfrmD3D.Button2Click(Sender: TObject);
begin
// Предоставляем пользователю увидеть установленный диффузный цвет
ColorDialogl.Color :=
Round(MaterialTorus.Diffuse.R * 255) +
Round(MaterialTorus.Diffuse.G * 255 * $100) +
Round(MaterialTorus.Diffuse.В * 255 * $10000);
if ColorDialogl.Execute then
with MaterialTorus.Diffuse do begin
R := (ColorDialogl.Color and SFF) / 255;
G := ((ColorDialogl.Color and 3FFOO) shr 8) / 255;
В := ((ColorDialogl.Color and SFFOOOO) shr 16) / 255;
end;
end;

По умолчанию зеркальная составляющая в расчет не принимается, блики на поверхностях объектов не появляются. Чтобы учесть ее, надо включить режим D3DRS_SPECULARENABLE.
Я советую вам внимательно поработать с этим примером. Для начала по отдельности включите одну из трех составляющих, чтобы увидеть, как они проявляются на поверхности объектов. Назначьте ей белый цвет, а всем остальным - черный, и посмотрите результат.
Этот пример может стать очень полезным в моменты, когда вам потребуется подобрать материал для построений. Ведь наверняка далеко не у каждого из вас под рукой окажется справочник оптических свойств материалов.
После того как вы хорошенько поработаете с этим примером, я хочу обсудить с вами важную проблему, напрямую не относящуюся к основной теме главы. Поговорим с вами на тему выбора объектов. Выбор по цвету, предлагаемый мною в предыдущих примерах, напрямую использовать очень сложно. Если мы вернемся к тестовой сцене с конусом и сферой и внимательно посмотрим на получающуюся картинку, то увидим, что значение пиксела экрана никак не поможет решить задачу выбора: оба объекта имеют
участки черного или очень темного цвета. Даже в таком случае, когда цвета объектов различаются кардинально, их очень тяжело отличать. Например, на поверхности объектов могут появляться блики одинакового цвета. А если на объекты накладывается текстура, или объекты покрашены одинаковым цветом, задача выбора по цвету становится неразрешимой. В случае DirectDraw мы решали подобную проблему использованием вспомогательной поверхности, на которой объекты раскрашивались по произвольной схеме, аналогичный метод можно применять и в Direct3D. Мы можем на вспомогательном, невидимом зрителю экране, повторить построения сцены, окрашивая объекты так, как нам удобно для их идентификации, и ориентироваться при выборе по значению пиксела в определенной точке этого экрана.
Вспомним, что нам системой предоставлены два экрана, передний и задний буферы, причем второй экран скрыт от зрителя до тех пор, пока не вызывается метод Present объекта устройства. Поэтому данным экраном мы можем воспользоваться для наших целей, осуществляя в него построения по нужной схеме, и не выкладывать его содержимое на передний экран. Система предоставляет нам доступ к содержимому заднего буфера, с помощью метода GetBackBuffer объекта устройства, результат помещается в объект типа IDirect3DSurface8.
Чтобы окрашивать объекты в чистые цвета, можно в формат вершин включить диффузный компонент, аналогично нашим первым смоделированным объектам, и отключать при построениях в заднем буфере источники света, запретив работу с освещением. Таким образом, мы добьемся, что все пикселы, занимаемые объектом, примут одинаковый, сплошной цвет.
Переходим к иллюстрации - проекту из каталога Ех04, где рисуется знакомая тестовая сцена, при щелчке кнопки мыши сообщается, какой объект находится под курсором (рис. 10.4).

Рис. 10.4. В примере осуществляется выбор пространственных объектов

Первым делом обращаю ваше внимание на то, что при инициализации графической системы необходимо указать возможность запирания поверхности заднего буфера, для чего в поле Flags структуры ТD3DРRЕSЕNТ_РАРАМЕТЕRS не обходимо занести соответствующую константу:

ZeroMemory(@d3dpp, SizeOf(d3dpp));
with d3dpp do begin
Windowed := True;
SwapEffect := D3DSWAPEFFECT_DISCARD;
// Разрешаем запирание поверхности заднего буфера
Flags := D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
BackBufferFormat := d3ddm.Format;
EnableAutoDepthStencil := True;
AutoDepthStencilFormat := D3DFMT_D16;
end;

Это очень важный момент, не упустите его.
Формат вершин включает в себя координаты, нормаль и цветовую составляющую:

D3DFVF_CUSTOMVERTEX = D3DFVF_XYZ or D3DFVF_NORMAL or D3DFVF_DIFFUSE;

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

with FDSDDevice do begin
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
SetRenderState(D3DRS_AMBIENT, $00202020);
SetRenderState(D3DRS_LIGHTING, Dword (True));
SetRenderState(D3DRS_NORMALIZENORMALS, DWORD (True));
// Явно указываем использование материала
SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_MATERIAL);
SetRenderState(D3DRS_SPECULARMATERIALSOURCE, D3DMCS_MATERIAL);
SetRenderState(D3DRS_AMBIENTMATERIALSOURCE, D3DMCS_MATERIAL);
end;

При движении курсора мыши по поверхности окна отслеживаются его координаты:

var
OX, OY : DWORD;
procedure TfrmD3D.FormMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
OX := X;
OY := Y; end;

Вы можете оптимизировать часть кода, связанную с определением позиции, ведь для получения положение курсора в любой момент времени можно использовать функцию GetCursorPos.
Помимо функции Render, я ввел функцию укороченного воспроизведения, которая отображает сцену с измененными установками и не заканчивается переключением буферов:

function TfrmD3D.Draw : HRESULT;
var
hRet : HRESULT;
begin
if FD3DDevice = nil then begin
Result := E_FAIL;
Exit;
end;
// Очищаем только Z-буфер
hRet := FD3DDevice.Clear(0, nil, D3DCLEAR_ZBUFFER, 0, 1.0, 0);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3DDevice.BeginScene;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
with FD3DDevice do begin
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
// Работа с освещением запрещена
SetRenderState(D3DRS_LIGHTING, Dword (False));
end;
DrawScene; // Рисуем комнату
Result := FD3DDevice.EndScene;
end;

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

Рис. 10.5. Содержимое заднего буфера в момент выбора

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

procedure TfrmD3D.FormClick(Sender: TObject);
var
Back : IDirect3DSurface8; // Поверхность заднего буфера
d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD;
hRet : HRESULT;
DWColor : DWORD;
R, G, В : Byte;
begin
R := 0; // Инициализация для предотвращения предупреждений компилятора
G := 0;
В := 0;
FActive := False; // Перерисовку кадра временно отменяем
Back := nil;
hRet := Draw; // Рисуем упрощенный вариант сцены, в задний буфер
if Failed (hret) then ErrorOut ('Draw', hRet); // Получаем доступ к заднему буферу
hRet := FDSDDevice.GetBackBuf fer (0, D3DBACKBUFFER_TYPE_MONO, Back) ;
if Failed (hret) then ErrorOut ( 'GetBackBuf fer ' , hRet); // Обнуляем поля вспомогательной структуры
ZeroMemory (@d3dlr, SizeOf (d3dlr) ) ; // Поверхность заднего буфера запирается
hRet := Back.LockRect (d3dlr, nil, D3DLOCK__READONLY) ;
if Failed (hret) then ErrorOut {'LockRect', hRet); // Значение смещения при выравнивании поверхности
dwDstPitch := dSdlr. Pitch;
case d3ddm. Format of // Текущий формат рабочего стола
D3DFMT_X8R8G8B8 : begin // 32-битный RGB
// Пиксел, соответствующий позиции курсора
DWColor := PDWORD (DWORD (d3dlr .pBits) + OY *
dwDstPitch + OX * 4)A; // Цветовые веса пиксела
R := (DWColor shr 23) and $lf;
G := (DWColor shr 7) and $lf;
В := DWColor and $lf;
end;
D3DFMT_R5G6B5 : begin // 16-битный 5-6-5
DWColor := PDWORD (DWORD (d3dlr .pBits) + OY *
dwDstPitch + OX * 2)^;
R := (DWColor shr 11) and $lf;
G := (DWColor shr 5) and $3f;
В := DWColor and $lf;
end;
end;
Back.UnLockRect; // Возможное исключение не обрабатывается
if Assigned (Back) then begin // Удаляем поверхность
Back._Release;
Back := nil;
end;
// Интерпретация результата
if В о 0 then ShowMessage ('Выбран конус') else
if R <> 0 then ShowMessage ('Выбрана сфера') else
if G <> 0 then ShowMessage ('Выбран объект зеленого цвета')
else
ShowMessage ('Ничего не выбрано');
Factive := True;
end;

Первый аргумент метода GetBackBuffer указывает номер присоединенного буфера, основан на нуле. Вторым аргументом является константа. В момент написания книги здесь можно использовать единственно возможное значение, D3DBACKBUFFER_TYPE_MONO. Последний аргумент метода - переменная типа Direct3DSurface8, в которую помещается результат. Поверхности в Direct3D очень похожи на знакомые нам по DirectDraw, на время доступа к их содержимому они должны запираться.
При анализе содержимого пиксела я предусмотрел поддержку только двух, наиболее распространенных, форматов пиксела, и этот код, возможно, вам придется дополнить.
Зеленую составляющую пиксела мы в этом примере никак не используем, но я оставил рассмотрение ее значения для предотвращения замечаний компилятора. Удалять этот код я не стал, вам он может понадобиться для выбора из трех объектов.
Выбор по цвету, разобранный в данном примере, вы можете использовать для идентификации сотен объектов. Ведь объекты могут различаться оттенками, и совсем не обязательно, чтобы они окрашивались именно в чистые цвета: вы можете использовать смеси всех трех цветов.

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

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

При линейном законе плотность дымки равномерно увеличивается по мере удаления от глаза наблюдателя. Дымка действует в пределах интервала от передней до задней плоскостей отсечения. Этот интервал можно сузить, задавая значение параметров D3DRS__FOGSТАRТ и D3DRS_FOGEND. Есть две схемы расчета тумана: пикселная и вершинная. Если задана первая схема, значения связанных с расстоянием параметров лежат в пределах от нуля до единицы и задают расстояния относительно текущих видовых параметров. Минимальное значение соответствует расстоянию до передней плоскости отсечения, максимальное соотносится с задней плоскостью. Во второй, вершинной схеме тумана значения параметров указывают на действительное расстояние в мировом пространстве. Для большей определенности я буду применять только одну, первую схему. Ей соответствует режим D3DRS_FOGTABLEKODE. Для использования вершинной схемы необходимо менять установки состояния D3DRS_FOGVERTEXMODE. В обеих схемах объекты, располагающиеся дальше границы действия тумана, становятся совершенно неразличимыми.
Нелинейных законов два: оба опираются на экспоненциальную зависимость, но в одном из них используется экспонента квадрата. Аргументом экспоненты в обоих случаях является произведение расстояния и весового фактора, называемого плотностью. Этот параметр должен быть вещественным и не превышать 1.
Проект каталога Ех05 поможет вам глубже постичь все вышесказанное. Тестовая композиция воспроизводится на панели, рядом с которой располагаются элементы, позволяющие менять текущие параметры тумана .Для возможности динамической смены параметров их значения хранятся в переменных:

var
FogDensity : Single = 1.0; // Плотность
FogStart : Single =0.4; // Расстояние, с которого туман действует
FogEnd : Single =1.0; // Граничное расстояние действия тумана
FogColor : DWORD = $00FFFFFF; // Цвет тумана, первоначально - белый
FOGTABLEMODE : DWORD = D3DFOG_LINEAR; // Закон тумана
with FD3DDevice do begin
// Включаем режим использования дымки
SetRenderState(D3DRS_FOGENABLE, DWORD (True));
// Используем пикселную схему расчета тумана
SetRenderState(D3DRS_FOGTABLEMODE, FOGTABLEMODE);
// Устанавливаем текущие параметры тумана
SetRenderState(D3DRS_FOGCOLOR, FogColor);
SetRenderState(D3DRS_FOGDENSITY, PDWORD (@FogDensity)л);
SetRenderState(D3DRS_FOGSTART, PDWORD (@FogStart)л);
SetRenderState(D3DRS_FOGEND, PDWORD (@FogEnd)");
end;

При изменении пользователем состояний интерфейсных элементов меняются значения соответствующих переменных:

procedure TfrmD3D.tbStartChange(Sender: TObject); // Ползунок "Fog Start''
begin
FogStart := tbStart.Position / 10;
end;
procedure TfrmD3D.tbEndChange{Sender: TObject); // Ползунок "Fog End"
begin
FogEnd := tbEnd.Position / 10;
end;
procedure TfrmDSD.tbDensityChange(Sender: TObject); // Ползунок "Density"
begin
FogDensity := tbDensity.Position / 10;
end;
// Ползунки, связанные с цветовыми весами тумана procedure TfrmD3D.tbRedChange(Sender: TObject);
begin
FogColor := tbBlue.Position + (tbGreen.Position shl 8) +
(tbRed.Position shl (4 * 4));
end;
// Закон тумана
procedure TfrraD3D.cmbxFOGTABLEMODEChange(Sender: TObject);
begin
case cmbxFOGTABLEMODE.Itemlndex of
0 : FOGTABLEMODE := D3DFOG_NONE;
1 : FOGTABLEMODE := D3DFOG EXP;
2 : FOGTABLEMODE := D3DFOG_EXP2;
3 : FOGTABLEMODE := D3DFOG_LINEAR;
end;
end;

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

Двусторонние поверхности
Я обращал ваше внимание на то, что Direct3D умеет окрашивать примитивы только с одной стороны. В этом небольшом разделе, на примере проекта каталога Ех07 мы разберем принципы построения двусторонних поверхностей. Работа примера очень простая: на экране вращается квадрат, с одной стороны окрашенный в синий цвет, с другой - в красный. Цвета разные только для наглядности, чтобы мы могли различать стороны площадки. Используется два материала, но вы можете получать таким же способом примитивы, выглядящие одинаково независимо от точки обзора.
Метод очень прост: примитивы фигуры описываются дважды, с одинаковыми координатами, но противоположным направлением нормалей. В моем примере первые четыре вершины описывают связанные треугольники, образующие квадрат. Нормаль к вершинам задается из расчета, что описывается передняя сторона квадрата. Затем буфер наполняется четверкой вершин, с противоположным направлением нормали. Считаем, что это соответствует задней стороне квадрата. В обоих случаях вершины перечисляются по часовой стрелке.
При воспроизведении выводим переднюю сторону квадрата, отсекая примитивы, вершины которых перечисляются в поле зрения против часовой стрелки. Затем выводим заднюю сторону квадрата, меняя правило отсечения на противоположное:

with FD3DDevice do begin
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetMaterial(MaterialRed);
DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
SetMaterial(MaterialBlue);
DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 2);
end;

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

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

Рис. 10.6. Соприкасающиеся поверхности порождают нежелательные узоры

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

procedure TfrmD3D.DrawScene;
var
matRotateY, matTranslate : TD3DMatrix;
begin
// Сдвиг и поворот первого квадрата
SetTranslateMatrix (matTranslate, -0.5, -0.5, 0);
SetRotateYMatrix(matRotateY, Angle);
with FD3DDevice do begin
SetTransform(D3DTS WORLD, MatrixMul(matRotateY, matTranslate);;
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetMaterial(MaterialRed);
DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Сдвиг второго квадрата
SetTranslateMatrix (matTransiate, -0.4, -0.4, 0);
SetTransform(D3DTS_WORLD, MatrixMul(matRotateY, matTransiate) SetMaterial(MaterialBlue);
DrawPrimitive (D3DPT_TRIA1-IGLESTRIP, 0, 2) ;
end;
end;

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

SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
SetRenderState(D3DRS_ZENABLE, D3DZBJTRUE);

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

Частичная прозрачность объемных фигур
Предыдущие разделы подготовили нас к тому, чтобы вплотную заняться выводом объемных полупрозрачных объектов. Для начала рассмотрим вывод одной единственной фигуры на сцене (проект каталога Ех10), в которой сфера стала наполовину прозрачной (рис. 10.7).

Рис. 10.7. Первый пример частичной прозрачности объемных фигур

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

with FD3DDevice do begin
SetTransform(D3DTS WORLD, matSphere) ;
// Устанавливаем полупрозрачный материал SetMaterial(MaterialSphere); // Включаем режим смешения
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(True)); // Первой выводится внутренняя сторона сферы SetRenderState(D3DRS_CULLMODE, D3DCULL__CW);
DrawPrimitive(D3DPT_TRIANGLELIST, 30 + 51 + 51 + 960, 960); // Внешняя сторона сферы
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
DrawPrimitive(D3DPT_TRIANGLELIST, 30 +o 51 + 51, 960);
SetRenderState(D3DRS_ALPHABLENDENABLE, DWORD(False));
end;

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

// Первой воспроизводится внутренняя поверхность конуса
SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
DrawPrimitive(D3DPT_TRIANGLEFAN, 81 + 51, 49); // Сам конус
// Дно конуса рисуется с отключенной работой Z-буфера
SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
DrawPrimitive(D3DPT_TRIANGLEFAN, 81 + 51 + 51, 49); // Дно
SetRenderState(D3DRS_ZENABLE, D3DZBJTRUE); // Второй воспроизводится внешняя поверхность конуса
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
DrawPrimitive(D3DPTjrRIANGLEFAN, 30, 49);
SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
DrawPrimitive(D3DPT_TRIANGLEFAN, 81, 49);

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

Рис. 10.8. При присутствии на сцене нескольких полупрозрачных объектов требуется сортировка вывода

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

if Angle < Pi
then // Конус расположен дальше, чем сфера; первым выводится конус
else // Конус загораживает сферу; первой выводится сфера

Наложение текстуры на трехмерные объекты
Для использования трехмерных объектов, покрытых текстурой, необходимо, конечно, описание их вершин дополнить текстурными координатами. Помимо инициализации текстуры, это является минимальным действием, резко усиливающим зрелищность наших построений.
Проект каталога Ех13 представляет собой первый пример на данную тему. Это вариация одного из наших примеров с вращающимся кубиком. Формат вершин включает в себя нормали и текстурные координаты. Нормали, правда, в примере не применяются и оставлены мною "про запас". Не используются они постольку, поскольку на сцене отсутствуют источники света. Так что их удаление из описания вершин не приведет к каким-либо изменениям в работе данного примера. Работа с текстурой в рассматриваемом примере ничем не отличается от наших плоских построений, и запомните, что задание режимов текстуры в привычное для нас значение приводит к тому, что работа с освещенностью не осуществляется:

SetTextureStageStatefO, D3DTSS_COLOROP, D3DTAJTEXTURE);

Файл текстуры для этого примера я взял на сайте nehe.gamedev.net, она мне показалась очень подходящей для наложения на кубик (рис. 10.9).

Рис. 10.9. Простой, но эффектный пример использования текстуры в пространственных построениях

Конечно, большая часть того, что мы наблюдаем в играх, представляет собой наложение текстур. В проекте из каталога Ех14 вы можете увидеть, как наложение текстур на стороны куба позволяет создать окружение игрока. Здесь глаз наблюдателя помещается внутрь куба, на каждую сторону которого наложена текстура, имитирующая картину, наблюдаемую зрителем при повороте головы. Запомните, что поверхность покрывается текстурой с обеих сторон. Растры для примера взяты мною с сайта gamedeveloper.org/delphi3d.
Если необходимо модулировать, т. е. накладывать освещенность на поверхность, покрытую текстурой, то параметры следует задавать так:

SetTextureStageStatefO, D3DTSS_COLOROP, D3DTOP_MODULATE);

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

type
TCUSTOMVERTEX = packed record
X, У, Z : Single;
nX, nY, nZ : Single;
DColor : DWORD;
U, V : Single; end;
const
D3DFVF_CUSTOMVERTEX = D3DFVF_XYZ or D3DFVF_NORMAL or
D3DFVF_DIFFUSE or D3DFVFJTEX1;

Буфер вершин заполняется данными о сфере. В этом примере пространственные и текстурные координаты, а также нормали вершин, рассчитываются, и строится последовательность связанных треугольников.
На сферу накладывается растр с изображением поверхности Земли, который я позаимствовал из набора файлов, поставляемых в составе DirectX SDK.
В примере диффузная составляющая вершин сферы задается белым цветом. Поскольку по умолчанию в DirectSD при разрешенной работе с освещенностью используется диффузный компонент вершины, в примере просто включается направленный источник света, материалы не используются. Так как в этом примере включена модуляция, участки глобуса отличаются по своей яркости (рис. 10.10).

Рис. 10.10. Пример использования текстуры совместно с источником света

Сейчас в качестве упражнений выполните следующие задания:

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

Точечный источник света также подходит для освещения объектов, покрытых текстурой. Так, в примере проекта каталога Ех16 рисуется тестовая сцена комнаты с конусом и сферой. На стены комнаты здесь накладываются разнообразные текстуры.
В предыдущих примерах текстура накладывалась на простые объекты, но вам, наверняка, захочется узнать, возможно ли использование текстуры с объектами сложной формы. Если у вас возникли вопросы по этому поводу, отсылаю вас к следующему примеру, проектам каталога Ех17, в одном из которых выводится модель игрока из игры Quake, а во втором - вращающаяся голова. Программа импорта, которой я пользовался для подготовки примеров этой книги, позволяет записывать в результирующем файле и текстурные координаты моделей. Мне оставалось только отметить такую опцию при записи результирующего файла.

Механизм трехмерной игры
Этот раздел я закончу примером, который можно считать заготовкой трехмерной игры. Но прежде, чем мы перейдем непосредственно к этому проекту, посмотрим решение двух связанных с ним задач: вывод текста в пространстве и раскрашивание модели.
Вам, наверняка, пригодится моя простая программа из каталога Ех18, с помощью которой создается файл, содержащий координаты вершин треугольников, образующих нужный символ установленного для формы шрифта. Программа основана на материале моей книги по OpenGL, подробно рассматривать ее здесь не буду, ограничусь лишь небольшими замечаниями по поводу ее использования.
Требуемый символ должен устанавливаться аргументом процедуры OutText, вызываемой в коде два раза: первый раз - для получения координат вершин треугольников, второй раз - для контрольного отображения на экране. В текстовый файл выводятся построчно две координаты очередной вершины треугольника, по оси X и по оси Y. Количество треугольников заранее неизвестно и зависит от базового символа. Выводимые в файл координаты вершин соответствуют оконным, поэтому при дальнейшем использовании должны быть масштабированы. Как правило, вершины треугольников перечисляются по часовой стрелке, но возможны исключения.
Еще один проект (из каталога Ех19) строит средствами Direct3D символ, используя файл, полученный по результатам работы предыдущей программы. Количество считываемых треугольников необходимо установить равным константе NumTriangies. Считываемые координаты вершин масштабируются при заполнении буфера вершин.
Замечу также, что оба примера могут использоваться и для вывода фраз целиком, а не только отдельных символов.
Сейчас перейдем к очередному примеру (проекту из катаюга Ех20), во время работы которого на экране воспроизводится симпатичная модель человечка из детского конструктора (рис. 10.11).

Рис. 10.11. Во время работы примера человечек шевелит конечностями

Подходящую модель я нашел по Internet-адресу http://www.people.zeelandnet.nl /nihil/download/legoman.zip. Автор модели, Kortekaas, любезно предоставил разрешение на использование ее в этой книге.
Эта модель также конвертирована мною с помощью программы импорта 3D Exploration, а код был преобразован из программы на языке C++. При импортировании комплексных моделей, состоящих, как в данном примере, из нескольких частей, в код вставляются метки-имена составляющих элементов. По этим меткам можно ориентироваться для получения данных о том, сколько треугольников потрачено на описание отдельной части, чтобы идентифицировать каждый элемент:

procedure TfrmD3D.DrawScene;
begin
with FD3DDevice do begin
// Ноги покрашены материалом серого цвета
SetMaterial(MaterialGray);
SetTransform(D3DTS_WORLD, matLeftFoot);
// Левая нога
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 112);
// Правая нога
SetTransform(D3DTS_WORLD, matRightFoot) ;
DrawPrimitive(D3DPT TRIANGLELIST, (112 + 204) * 3, 112);
// Руки покрашены красным цветом SetMaterial(MaterialRed) ; // Левая рука
SetTransform(D3DTS_WORLD, matLeftHand);
DrawPrimitive(D3DPT_TRIANGLELIST, (112+204 + 112 + 620 + 6141*3, 612); // Кисти - желтого цвета
SetMaterial(MaterialYellow) ; // Левая кисть
DrawPrimitive(D3DPT_TRIANGLELIST,(112+204+112+620+614+612)*3, 324);
SetMaterial(MaterialRed); SetTransform(D3DTS_WORLD, matRightHand); // Правая рука
DrawPrimitive(D3DPTJTRIANGLELIST, (112 + 204 + 112 + 620) * 3, 614); // Правая кисть
SetMaterial(MaterialYellow) ;
DrawPrimitive(D3DPT_TRIANGLELIST,
(112+204+112+620+614+612+324)*3, 324); // Голова
S.etTransform(D3DTS_WORLD, matRot) ;
DrawPrimitive(D3DPTJTRIANGLELIST, (112 + 204 + 112) * 3, 620); // Туловище, красного цвета
SetMaterial(MaterialRed) ;
DrawPrimitive(D3DPTJTRIANGLELIST, 112 * 3, 204);
end;
end;

Буфер вершин заполняется данными на всю модель целиком, а при воспроизведении отдельных частей из него последовательно выбираются соответствующие треугольники. Перед воспроизведением каждого элемента устанавливается предварительно рассчитанная матрица трансформаций, поэтому изначально монолитная модель пришла в движение. Для каждого элемента модели задается индивидуальный материал, поэтому модель стала разноцветной. Фигурирующие числа получены следующим образом: я подсчитал количество отдельных фасетов между метками, расставленными программой моделирования трехмерных объектов в описании массива face^indicies.
Матрицы, связанные с поворотом конечностей, из соображений оптимизации вычисляются не при каждой перерисовке кадра, а только при изменении значений управляющих переменных. Обратите внимание, что поворот конечностей в точках крепления осуществляется следующим образом: система координат перемещается в точку крепления, выполняется поворот, а затем система координат возвращается в первоначальное положение:

procedure TfrmDSD.MoveMan;
begin
// Поворот глобальной системы координат,
// вращение всей модели вокруг своей оси
SetRotateZMatrix (matRot, Angle);
// Переменная, задающая вращение конечностей
AngleFoot := AngleFoot + StepFoot;
if (AngleFoot > Pi / 4) or (AngleFoot < -Pi / 4}
then StepFoot := -StepFoot; // Ноги вращаются в противофазе
SetRotateXMatrix (rotLeftFoot, AngleFoot);
SetRotateXMatrix (rotRightFoot, -AngleFoot); // Поворот левой ноги, в три этапа
matLeftFoot := MatrixMul(matRot,
MatrixMul(transFoot2, MatrixMul(rotLeftFoot, transFootl))); // Поворот правой ноги
matRightFoot := MatrixMul(matRot,
MatrixMul(transFoot2,
MatrixMul(rotRightFoot, transFootl))); // Поворот левой руки
matLeftHand := MatrixMul(matRot,
MatrixMul(transHand2,
MatrixMul(rotRightFoot, transHandl))); // Поворот правой руки
matRightHand := MatrixMul(matRot,
MatrixMul(transHand2, MatrixMul(rotLeftFoot, transHandl)));
end;

Рабочие матрицы, связанные с перемещениями в точки крепления конечностей, инициализируются один раз, в начале работы приложения:
SetTranslateMatrix(transFootl, О, О, 0.25);
SetTranslateMatrix(transFoot2, О, О, -0.25);
SetTranslateMatrix(transHandl, 0.25, 0.0, -0.23);
SetTranslateMatrix(transHand2, -0.25, 0.0, 0.23);

Этот пример я подготовил для использования в дальнейшем в расчете на то, что человечком можно будет легко управлять, перемещая его в пространстве. Но если на сцене присутствует только одна модель, для оптимизации можно сократить количество операций с матрицами. В самом деле, в этом примере матрица matRot, связанная с глобальной системой координат, может вообще не использоваться: модель можно не вращать и оставить неподвижной, а перемещать точку зрения наблюдателя. Эффект вращения модели останется, а количество операций существенно уменьшится.
И теперь мы можем перейти к разбору заключительного примера - проекта каталога Ех21. Как я уже говорил, это заготовка трехмерной игры: игрок попадает внутрь комнаты, населенной движущимися человечками .
Окружение игрока построено из текстур, накладываемых на треугольники, описание окружающего мира загружается из текстового файла.DirectX.

type
// Формат вершин для треугольников окружения
TNormDiffTextVertex = packed record
X, Y, Z : Single;
nX, nY, nZ : Single;
DColor : DWORD;
U, V : Single;
end;
// Формат вершин для треугольников человечков
TNormVertex = packed record
X, Y, Z : Single;
nX, nY, nZ : Single;
end;
// Отдельный треугольник описания окружения
TTriangle '= record
NumTexture : Integer; // Номер текстуры
DIFFUSE : DWORD; // Диффузная составляющая треугольника
end;
const
// FVF-флаг для треугольников окружения
D3DFVF_NORMDIFFTEXTVERTEX = D3DFVF_XYZ or D3DFVF_NORMAL or
D3DFVF_DIFFUSE or D3DFVFJTEX1; // FVF-флаг для треугольников человечков
D3DFVFJSIORMVERTEX = D3DFVF_XYZ or D3DFVFJTORMAL;
// Имя файла с описанием мира
WorldFile = 'Data/World.txt';
// Имя файла с треугольниками символов, для вывода FPS
NumbersFile = 'Data/Numbers.txt';
// Количество треугольников в описании окружения
NumTriangles = 58;
($1 legoman.pas) // Данные модели
var
frmD3D: TfrmD3D;
Frames : Integer =0; // Счетчик кадров
FpsOut : String = ''; // Значение FPS
// Вспомогательная матрица, для вывода символов FPS
LetTrans : TDSDMatrix;
// Используется как вспомогательный массив для хранения образа текстуры
TexPointer : Pointer;
// Характеристики образа текстуры
wrkTexWidth, wrkTexHeight :
Integer;
// Флаг, выводить ли FPS
flgFPS : BOOL = True;
// Угол зрения по вертикали
Lookupdown : Single = 0.0;
// Вспомогательный вектор для оптимизации
ZVector : TD3DVector;
// Угол зрения по горизонтали и положение игрока
RotY, XPos, ZPos : Single;
// Массив описания мира
World : Array [0..NumTriangles - 1] of TTriangle;
// Переменные для обработки устройств ввода
DInput : IDIRECTINPUT8 = nil;
DIMouse : IDIRECTINPUTDEVICE8 = nil;
DIKeyboard : IDirectlnputDeviceS;
KeyBuffer : TDIKeyboardState;
// Угол поворота красного человечка
Angle : Single = 0.0;
// Угол поворота конечностей человечков
AngleFoot : Single = 0.0;
StepFoot : Single = 0.1;
// Тестовая точка для определения столкновений с препятствиями
TestPointX, TestPointY : DWORD;

В файле описания окружения данных идут в следующем порядке:

- строка комментария;
- номер текстуры;
- цвет треугольника;
- три строки описания вершин треугольника, которые включают координаты в пространстве;
- нормаль и текстовые координаты каждой вершины треугольника.

Вот что записано в текстовом файле для первого треугольника:

// Потолок
4
$00FF0000
-3.0 1.0 3.0 0.0 -1.0 0.0 0.0 0.0
-3.0 1.0 -3.0 0.0 -1.0 0.0 0.0 12.0
1.0 3.0 0.0 -1.0 0.0 12.0 0.0

Пол и потолок комнаты представляют собой квадраты с координатами точек углов по диагонали (-3; -3) и (3; 3). Координата Y для всех вершин пола нулевая, для вершин потолка - единичная. При считывании данных предусматриваем обработку исключений на случай отсутствия файла данных или присутствия ошибки при описании треугольников:

procedure TfrmD3D.SetupWorld;
var
t : TextFile;
i, j : Integer;
Vertices : /4TNormDiffTextVertex;
wrkStr : tring;
begin
if FileExists(WorldFile) then begin AssignFile(t, WorldFile);
try
Reset(t); FD3DVB.Lock(0, NumTriangles * 3 * SizeOf(TNormDiffTextVertex),
PByte(Vertices), 0) ;
for i := 0 to NumTriangles - 1 do begin
// Строка комментария, в программе не используется
ReadLn (t, wrkStr) ;
ReadLn (t, World[i].NumTexture); // Текстура треугольника
ReadLn (t, World[i].DIFFUSE); // Цвет вершин треугольника
for j := 0 to 2 do begin // Три вершины треугольника
ReadLn (t, Vertices.X, Vertices.Y, Vertices.Z,
Vertices.nX, Vertices.nY, Vertices.nZ,
Vertices.U, Vertices.V);
Vertices.DColor := World[i].DIFFUSE;
Inc(Vertices);
end;
end;
FD3DVB.Unlock;
except // Данные на треугольник заданы неверно
raise EAbort.Create ('Can''t read file: ' + WorldFile);
end;
CloseFile(t) ;
end else raise EAbort.Create ('Can''t read file: ' + WorldFile);
end;

При возникновении исключений программа завершается, описание ошибки выводится в текстовый файл.
Помимо треугольников, образующих стены комнаты, на сцене присутствуют треугольники стоящего в комнате ящика и пирамиды источника света, прикрепленного к потолку. Обратите внимание, что треугольники пола и потолка окрашены красным цветом, а треугольники препятствий, стен и ящика - синим. Позже я поясню смысл этого окрашивания.
Координаты игрока задаются значениями переменных xpos и Zpos, переменная RotY определяет угол поворота головы наблюдателя вокруг своей оси, а переменная Lookupdown - наклон головы по вертикали. Сразу после запуска игрок "располагается" в точке (0, 0, 0), направление взгляда параллельно оси X.
Текстуры треугольников задаются обычным образом, но текстуры, накладываемые на квадраты выходов из сектора, инициализируются отдельной функцией:

procedure TfrmD3D.FormCreate(Sender: TObject);
var
hRet : HRESULT;
matView, matProj : TD3DMatrix;
wrkMat : TDSDMatrix; // Вспомогательная матрица разворота человечков
begin
// Приложение полноэкранное, курсор отключаем
ShowCursor (False);
Randomize;
hRet := InitDSD;
if Failed (hRet) then ErrorOut ('InitD3D', hRet);
hRet := InitVB;
if Failed (hRet) then ErrorOut ('InitVertex', hRet);
try
InitVBLetter; // Считываются треугольники цифр
except // Возможно, файл удален
on E : EAbort do ErrorOut (PChar(E.Message), S_FALSE);
end;
InitMan; // Инициализация буфера вершин человечков
try
SetupWorld; // Считываем данные мира
// Вспомогательный вектор для видовой трансформации
ZVector := D3DVector(0, 1, 0);
// Матрица перемещений букв при выводе FPS
LetTrans := IdentityMatrix;
LetTrans._42 := 0.5;
LetTrans._43 := 0.9;
// Первоначальные положения человечков
transManl := IdentityMatrix;
transMan2 := IdentityMatrix;
transMan2._41 := 3.1; // Синий человечек перемещается по оси X
transManS := IdentityMatrix;
// Зеленый человечек устанавливается в первоначальное положение
transMan3._41:= МапЗРозХ;
transMan3._43 := ManSPosZ;
// Разворот модели человечков
SetRotateYMatrix (wrkMat, -Pi / 2);
SetRotateXMatrix (matWrkl, -Pi / 2) ;
matWrk2 := MatrixMul (wrkMat, Matwrkl);
matWrk3 := matWrk2;
// Вспомогательные матрицы для поворота конечностей
SetTranslateMatrix(transFootl, 0, 0, -0.1);
SetTranslateMatrix(transFoot2, 0, 0, 0.1);
SetTranslateMatrix(transHandl, 0.25, 0.0, -0.2);
SetTranslateMatrix(transHand2, -0.25, 0.0, 0.2);
SetupLights;
// Первоначальные установки, в дальнейшем переопределяются
SetViewMatrix(matView, D3DVector(0, 0, 0), D3DVector(0, 0, 1),
ZVector);
FDSDDevice.SetTransform(D3DTS_VIEW, matView);
SetProjectionMatrixfmatProj, 1, 1, 0.01, 6) ;
FDSDDevice.SetTransform(D3DTS_PROJECTION, matProj);
// Инициализация текстур
try
InitTexture (FD3DTextures [0], 'data/0.bmp');
InitTexture (FD3DTextures [1], 'data/1.bmp1);
InitTexture (FD3DTextures [2], 'data/2.bmp');
InitTexture (FD3DTextures [3], 'data/3.bmp');
InitTexture (FD3DTextures [4], 'data/4.bmp');
InitTexture (FDSDTextures [5], 'data/5.bmp');
BukupTexture (FD3DTextures [6], 'data/6.bmp1);
except
on E : EAbort do ErrorOut (PChar(E.Message), S_FALSE) ;
end;
OnCreateDevice; // Инициализация устройств ввода
end;

Всего предусмотрено три источника света: два направленных и один точечный, располагающийся под потолком в центре комнаты:

procedure TfrmD3D.SetupLights;
var
LightO : TD3DLight8;
Lightl : TD3DLight8;
Light2 : TD3DLight8;
begin
// Направленные источники светят во взаимно противоположных направлениях
LightO := InitDirectionalLight(D3DVector(-0.5, -0.5, -1) , 0.5,
0.5, 0.5, 0); Lightl := InitDirectionalLight(VectorNormalize(DSDVector(0.5, 0.5, D),
0.5, 0.5, 0.5, 0); // Точечный источник
ZeroMemory(@Light2, SizeOf(Light2));
with Light2 do begin
JType := D3DLIGHT_POINT;
Diffuse.r := 0.5;
Diffuse.g := 0.5;
Diffuse.b := 0.5;
Specular := Diffuse;
Ambient := Diffuse;
Position := DSDVector(0.0, 1.0, 0.0);
Attenuation0 := 1.0;
Attenuationl := 0.0;
Attenuation2 := 0.0;
Range := 2.5;
end;
with FD3DDevice do begin SetLight(0, LightO);
SetLight(l, Lightl);
SetLight(2, Light2);
LightEnable(0, True);
LightEnable(1, True);
LightEnable (2, True);
end;
end;

Все объекты сцены, за исключением человечков, освещаются тремя источниками света. При воспроизведении человечков точечный источник выключается.
При воспроизведении сцены голову наблюдателя "помещаем" в точку, соответствующую его текущему положению в пространстве, и поворачиваем ее в направлении RotY:

procedure TfrmDSD.DrawScene;
var
i : Integer;
matView : TD3DMatrix;
begin
// Видовая матрица, в соответствии с текущими параметрами игрока
SetViewMatrix(matView, D3DVector(XPos, 0.25, ZPos).,
D3DVector (XPos + cos (RotY) ,* 0.25 + Lookupdown,
ZPos - sin (RotY)), ZVector);
with FD3DDevice do begin
SetTransform(D3DTS_VIEW, matView);
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
SetRenderState(D3DRS_LIGHTING, DWORD (True));
end;
// При необходимости выводим значение
FPS if flgFPS then DrawLetters; // Рисуем человечков
DrawManl; // Красный
DrawMan2; // Синий
DrawMan3; // Зеленый
// Подготовка к рисованию стен
with FD3DDevice do begin
// Учитывать освещение
SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
// Проводить интерполяцию текстур
SetTextureStageState(0,D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
SetTextureStageState(0,D3DTSS_MINFILTER, D3DTEXF_LINEAR);
// He учитывать диффузию треугольников окружения
SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_MATERIAL);
SetRenderState(D3DRS_AMBIENT, $OOOFOFOF);
// Задаем белый материал
SetMaterial(MaterialWhite);
// Направляем потоки на буфер вершин описания мира
SetStreamSource(0, FD3DVB, SizeOf(TNormDiffTextVertex));
SetVertexShader(D3DFVF_NORMDIFFTEXTVERTEX);
// Координаты треугольников заданы в глобальной системе координат
SetTransform(D3DTS_WORLD, IdentityMatrix);
end;
// Цикл вывода треугольников окружения
for i := 0 to NumTriangles - 1 do with FDSDDevice do begin
// Устанавливаем нужную текстуру в соответствии с описанием
SetTexture(0, FD3DTextures[World [i].NumTexture]);
DrawPrimitive(D3DPT_TRIANGLELIST, i * 3, 1); // Вывод треугольника
end;
FD3Ddevice.SetTexture(0, nil); // Текстура больше не используется
end;

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

procedure TfrmD3D.MoveManl;
begin
// Поворот вокруг вертикальной оси
SetRotateYMatrix (rotManl, Angle + Pi);
// Перемещение по кругу
transManl._41 := cos (-Angle) / 2;
transManl._43 := sin(-Angle) / 2;
// Опорная трансформация первого человечка
matManl := MatrixMul(transManl, MatrixMul(rotManl, matWrkl));

Второй человечек пересекает комнату, появляясь из одной стены и исчезая в противоположной:

procedure TfrmD3D.MoveMan2;
begin
// Изменение Х-координаты
transMan2._41 := transMan2._41 - 0.01;
// При прохождении комнаты процесс начинается сначала
if transMan2._41 < -3.1 then transMan2._41 := 3.1;
matMan2 := MatrixMul(transMan2, matWrk2);

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

procedure TfrmD3D.MoveMan3;
var
wrkAngle : Single;
distX, distZ : Single;
begin
// Расстояния до игрока
distX := XPos - МапЗРозХ;
distZ := ZPos - ManSPosZ;
// Вычисляем угол поворота человечка
if distZ < 0
then wrkAngle := arctan (distX / distZ) - Pi / 2 else
wrkAngle := arctan (distX / distZ) + Pi / 2; // Разворот человечка лицом к игроку
SetRotateYMatrix (rotMan3, wrkAngle);
// Если человечек удален от зрителя, то двигается,в его направлении
if (abs(distX) > 0.02) and (abs (distZ) > 0.02) then begin
МапЗРозХ := МаnЗРозХ + distX / 20; // Новое положение человечка
Man3PosZ := Man3PosZ + distZ / 20;
transMan3._41 := МаnЗРозХ;
transMan3._43 := Man3PosZ;
end;
// Опорная матрица третьего человечка
matMan3 := MatrixMul(transManS, MatrixMul(rotMan3, matWrk2));

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

procedure TfrmD3D.DrawLetters;
var
i : Integer; nS, nW : Integer;
begin
with FDSDDevice do begin
// Некоторые треугольники построены против часовой
SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
//Не тратить время на освещение
SetRenderState(D3DRS_LIGHTING, DWORD (False));
// Направляем поток на буфер символов
SetStreamSource(0, FD3DVBLetter, SizeOf(TNormVertex));
SetVertexShader(D3DFVF_NORMVERTEX);
end;
// Цикл вывода в пространстве символов FPS for i := 1 to Length(FpsOut) do begin
// Получаем положение треугольников символа
PiaceLetter (FpsOut[i], nS, nW);
// Сдвигаемся в пространстве для вывода очередного символа
LetTrans._41 := i * 0.1;
FD3DDevice.SetTransform(D3DTS_WORLD, LetTrans);
FD3DDevice.DrawPrimitive(D3DPTJTRIANGLELIST, nS, nW);
end;
// Возвращаем обычные установки with FD3DDevice do begin
SetRenderState(D3DRS_COLLMODE, D3DCULL_CCW);
SetRenderState(D3DRS_LIGHTING, DWORD (True) ) ;
end;
end;

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

function TfrmD3D.BukupTexture (var FDSTextBMP : IDIRECT3DTEXTURE8;
const FileName : String) : HRESULT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED__RECT;
dwDstPitch : DWORD;
X, Y : DWORD;
Bmp : TBitmap;
R, G, В : Byte;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromfile (FileName);
except
raise EAbort.Create ('Can''t open file: ' + FileName);
Result := S_FALSE;
Exit;
end;
hRet := FD3DDevice.CreateTexture (Bmp.Width, Bmp.Height, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL MANAGED, FD3TextBMP);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
hRet := FD3TextBMP.LockRect(0, d3dlr, nil, 0) ;
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
dwDstPitch := d3dlr.Pitch;
for Y := 0' to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do begin
R := GetRValue (Bmp.Canvas.Pixels [X,
DWORD (Bmp.Height -1) - Y] ) ;
G := GetGValue (Bmp.Canvas.Pixels [X,
DWORD (Bmp.Height -I) - Y] ) ;
В := GetBValue (Bmp.Canvas.Pixels [X,
DWORD (Bmp.Height -I) - Y] ) ;
PDWORD(DWORD(d3dlr.pBits)+Y*dwDstPitch+X*4)л :=
D3DCOLOR_XRGB(R, G, B);
end;
// Резервируем место для копии первоначального растра
GetMem (TexPointer, 4 * Bmp.Width * Bmp.Height); // Запоминаем первоначальньй растр
CopyMemory (TexPointer, d3dlr.pBits, 4 * Bmp.Width * Bmp.Height)
wrkTexWidth := Bmp.Width; wrkTexHeight := Bmp.Height; Bmp.Free;
Result := FDSTextBMP.UnlockRect(0);
end;
// Покрытие снегом текстуры
function TfrmDSD.SnowTexture (var FD3TextBMP : IDIRECT3DTEXTURE8)
HRESULT;
var
hRet : HRESULT;
d3dlr : TD3DLOCKED_RECT;
i : Integer;
dwDstPitch : DWORD;
begin
// Запираем прямоугольник текстуры
hRet := FDSTextBMP.LockRect(0, d3dlr, nil, 0);
if FAILED(hRet) then begin
Result := hRet;
Exit;
end;
// Копируем в него первоначальный растр
CopyMemory (d3dlr.pBits, TexPointer, 4 * wrkTexWidth * wrkTexHeight);
dwDstPitch := d3dlr.Pitch;
// Произвольные точки текстуры закрашиваем черным
for i := 1 to 10000 do
PDWORD (DWORD(d3dlr.pBits) + DWORD(random(wrkTexHeight)) * dwDstPitch +
DWORD(random(wrkTexWidth)) * 4)Л := 0; Result := FD3TextBMP.OnlockRect(0);
end;

Одно из самых важных мест кода - управление игроком. Перемещения мыши изменяют его положение и угол поворота головы по горизонтали, клавиши управления курсором отвечают за положение игрока в пространстве, клавиши <Page Up> и <Page Down> ответственны за угол поворота головы по вертикали:

function TfrmD3D.ReadImmediateData : HRESULT;
var
hRet : HRESULT; dims2 : TDIMOUSESTATE2;
NewXPos, NewZPos : Single;
begin
Zero-Memory (8dims2, SizeOf (dims2) ) ;
hRet := DIMouse.GetDeviceState(SizeOf(TDIMOUSESTATE2), @dims2);
if Failed (hRet) then begin
hRet := DIMouse.Acquire;
while hRet = DIERR_INPUTLOST do
hRet := DIMouse.Acquire; end;
// Перемещение курсора мыши влево-вправо
if dims2.1X <> О
// Меняем угол поворота головы по горизонтали
then RotY := RotY + 0.01 * dims2.1X; // Перемещение курсора мыши вперед-назад
if dims2.1Y > 0 then begin // Движение игрока назад
// Вычисляем новое положение
NewXPos := XPos + sin(RotY - Pi / 2) * 0.05;
NewZPos := ZPos + cos(RotY - Pi / 2) * 0.05;
// Нет ли препятствий к движению назад, голову разворачиваем
if TestRender (NewXPos, NewZPos, RotY - Pi) then begin
XPos := NewXPos; // Препятствий нет, перемещаем игрока
ZPos := NewZPos;
end
end else if dims2.1Y < 0 then begin // Движение вперед
NewXPos := XPos + sin(RotY + Pi / 2) * 0.05;
NewZPos := ZPos + cos(RotY + Pi / 2) * 0.05;
// Есть ли препятствия к движению
if TestRender (NewXPos, NewZPos, RotY) then begin
XPos := NewXPos; ZPos := NewZPos;
end;
end;
// Обработка клавиатуры
Result := DIKeyboard.GetDevicestate(SizeOf(KeyBuffer), @KeyBuffer);
if KeyBuffer[DIK_ESCAPE] and $80 <> 0 then begin // Esc
Close;
Exit;
end;
// Нажата клавиша "вправо", вычисляем новое положение в пространстве
if KeyBuffer[DIK_RIGHT] and $80 <> 0 then begin
XPos := XPos - sin(RotY) * 0.05;
ZPos := ZPos - cos(RotY) * 0.05;
end;
// Нажата клавиша "влево"
if KeyBuffer[DIK_LEFT] and $80 <> 0 then begin
XPos := XPos + sin(RotY) * 0.05;
ZPos := ZPos + cos(RotY) * 0.05;
end;
// Нажата клавиша "вниз"
if KeyBuffer[DIK_DOWN] and $80 о 0 then begin
XPos := XPos + sin(RotY - Pi / 2) * 0.05;
ZPos := ZPos + cos(RotY - Pi / 2) * 0.05;
end;
// Нажата клавиша "вверх" if KeyBuffer[DIK_UP] and $80 <> 0 then begin
XPos := XPos + sin(RotY + Pi / 2) * 0.05;
ZPos := ZPos + cos(RotY + Pi / 2) * 0.05;
end;
// Нажата клавиша "F", показывать ли значение FPS
if KeyBuffer[DIK_F] and $80 <> 0 then begin
flgFPS := not flgFPS; // Обращение значения флага
Sleep (50); // Маленькая пауза
end;
// Клавиша <Page Up>, голову задираем вверх
if KeyBuffer[DIK_PRIOR] and $80 <> 0 then begin
Lookupdown := Lookupdown + 0.05;
if Lookupdown > 1 then Lookupdown := 1;
end;
// Клавиша <Page Down>, голову опускаем вниз
if KeyBuffer[DIK_NEXT] and $80 <> 0 then begin
Lookupdown := Lookupdown - 0.05;
if Lookupdown < -1 then Lookupdown := -1;
end;
end;

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

function TfrmD3D.TestRender (const XPos, ZPos, RotY : Single) : BOOL;
var
i : Integer; matView : TD3DMatrix; d3dlr : TD3DLOCKED_RECT;
dwDstPitch : DWORD; DWColor : DWORD;
В : Byte; // Доля синего пиксела контрольной точки
begin
В := 0; // Предотвращение замечаний компилятора
// Смотрим на сцену из новой точки, по вертикали - ближе к полу
SetViewMatrix(matView, D3DVector(XPos, 0,1, ZPos),
D3DVector(XPos + cos(RotY), 0.1,
ZPos -sin(RotY)), ZVector); // Упрощенное воспроизведение сцены
with FD3DDevice do begin
Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER,
$000000FF, 1.0, 0); BeginScene;
// Отключаем источники света
-SetRenderState(D3DRS_LIGHTING, DWORD (False)); // Использовать диффузный компонент описания вершин SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
SetTransform(D3DTS_VIEW, matView);
SetStreamSource(0, FD3DVB, SizeOf(TNormDiffTextVertex));
SetVertexShader(D3DFVF_NORMDIFFTEXTVERTEX); SetTransform(D3DTS_WORLD, IdentityMatrix);
end;
// Рисуем только комнату
for i := 0 to NumTriangles - 1 do with FD3DDevice do
DrawPrimitive(D3DPT_TRIANGLELIST, 1*3, 1);
with FD3DDevice do begin
EndScene;
// Получаем доступ к заднему буферу
GetBackBuffer (О, D3DBACKBUFFER_TYPE_MONO, FD3SurfBack);
SetRenderState(D3DRS_LIGHTING, DWORD (True));
end;
// Запираем задний буфер
FD3SurfBack.LockRect (d3dlr, nil, D3DLOCK_READONLY);
dwDstPitch := d3dlr.Pitch;
// Определяем долю синего в контрольной точке case
FD3DfmtFullscreen of D3DFMT_X8R8G8B8 : begin
DWColor := PDWORD (DWORD(d3dlr.pBits) + TestPointY * dwDstPitch +
TestPointX * 4)^; В := DWColor and $lf;
end;
D3DFMT_R5G6B5 : begin
DWColor := PDWORD (DWORD(d3dlr.pBits) + TestPointY * dwDstPitch +
TestPointX * 2}Л; В := DWColor and $lf;
end;
end;
FDSSurfBack.UnLockRect;
// Нет синего цвета, значит можно пройти
Result := not (В <> 0);
end;

Синий цвет взят мною в качестве контрольного, поскольку для его вырезки требуется минимум операций. Замечу, что код этой функции можно дополнительно оптимизировать, например, вполне можно обойтись без использования промежуточной переменной DWColor.
Если установлен 24-битный режим, соответствующий формату D3DFMT_R8G8B8, то Х-координату контрольной точки надо умножить на 3, именно столько байт отводится для одного пиксела в этом режиме.
Контрольная точка для определения столкновения с препятствиями берется одна - посередине экрана по горизонтали, на 10 пикселов выше нижней границы экрана:

ScreenWidth := GetSystemMetrics(SM_CXSCREEN);
ScreenHeight := GetSystemMetrics(SM_CYSCREEN);
TestPointX := ScreenWidth div 2;
TestPointY := DWORD{ScreenHeight - 10);

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

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