DirectX Графика в проектах Delphi

       

Механизм трехмерной игры



Этот раздел я закончу примером, который можно считать заготовкой трехмерной игры. Но прежде, чем мы перейдем непосредственно к этому проекту, посмотрим решение двух связанных с ним задач: вывод текста в пространстве и раскрашивание модели.
Вам, наверняка, пригодится моя простая программа из каталога Ех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);

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


Содержание раздела