Очевидно, что способ, которым мы в предыдущей главе рисовали треугольники, неудобен и неполноценен для 3D игр. Неудобен потому, что мы, построив сложную сцену, лишены возможности взглянуть на нее с другой точки. Камера всегда направлена вдоль оси Z так, что ось X направлена вправо. Чтобы посмотреть, например, вдоль оси X, необходимо пересчитать позиции всех вертексов сцены! То же и в случае масштабирования, придется пересчитывать позиции вертексов, и при смене разрешения экрана или размеров формы – опять предстоит перерасчет, ведь шкала жестко привязана к размеру пикселя.
Неполноценность заключается в отсутствии перспективы – видимый размер наших треугольников не зависит от расстояния до них (координаты Z). Чтобы избавиться от этих недостатков, нужно отказаться от «приведенного» формата вертекса. Но сначала немного теории.

В математике существует понятие «матрица» – таблица чисел, построенная по определенным правилам. Из всех типов матриц нас будет интересовать только один – квадратная (4 × 4) таблица чисел типа Single. Фактически это таблица коэффициентов в системе уравнений, которая описывает почти любое изменение (трансформацию) пространства. Это прежде всего перемещение и масштабирование – наиболее простые трансформации, это также поворот вокруг любой из осей координат или вокруг произвольной оси. Матрицей можно описать перспективу – изменение видимого размера объекта в зависимости от расстояния до него. Кроме того, одной единственной матрицей можно задать любое сочетание всех перечисленных трансформаций. Правда есть некоторые ограничения – после любого преобразования прямая останется прямой (либо выродится в точку), а плоскость – плоскостью (либо выродится в прямую или точку).

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

Каким образом применять матрицы? d3dDevice содержит несколько трансформаций, которые задаются с помощью метода SetTransform, рассмотрим три основные:
      Первая – трансформация мира, обозначаемая D3DTS_WORLD. Записывая в неё матрицу, например, перемещения, мы вызываем соответствующее перемещение всей выводимой в дальнейшем геометрии.

      Вторая – трансформация камеры или обзора D3DTS_VIEW. Её действие противоположно трансформации мира. Записывая в D3DTS_VIEW матрицу вращения вокруг оси Y на 30 градусов, мы получим вращение мира на -30 градусов, что соответствует повороту камеры (иначе говоря, глаз наблюдателя) на 30 градусов. То есть с помощью этой трансформации мы задаем точку, из которой смотрим на наш 3D мир, и направление взгляда.

      Третья – трансформация проекции D3DTS_PROJECTION. Она задает проекцию изображения на экран монитора (точнее на BackBuffer). Это может быть ортогональная проекция или перспектива, левосторонняя или правосторонняя.
Создадим новый проект, взяв за основу предыдущий.
Добавим глобальную переменную типа D3DMATRIX:

код на языке vb
 
Dim Mtrx As D3DMATRIX
 
Из формата вертекса исключим поле RHW:
 
Private Type vFormat
  Pos As D3DVECTOR
  Color As Long
End Type
 

Поле Pos соответствует трем старым полям PosX, PosY, и PosZ.
Соответственно изменим флаговое описание вертекса:

код на языке vb
 
Private Const vFlag = D3DFVF_XYZ Or D3DFVF_DIFFUSE
 

Добавим функцию для быстрого создания векторов:

код на языке vb
 
Private Function vec3(X As Single, Y As Single, Z As Single) As D3DVECTOR
  vec3.X = X
  vec3.Y = Y
  vec3.Z = Z
End Function
 

Использование «неприведенного» формата вертекса подразумевает использование света по умолчанию. Поскольку свет мы еще не изучили – отключаем:

код на языке vb
 
  d3dDevice.SetRenderState D3DRS_LIGHTING, 0
 

И процедура, задающая начальные значения трансформаций:

код на языке vb
 
Private Sub InitMatrix()
  D3DXMatrixIdentity Mtrx
  d3dDevice.SetTransform D3DTS_WORLD, Mtrx
 
  D3DXMatrixPerspectiveFovLH Mtrx, 3.141593 / 4, Me.ScaleHeight / Me.ScaleWidth, 0.1, 10
  d3dDevice.SetTransform D3DTS_PROJECTION, Mtrx
 
  D3DXMatrixLookAtLH Mtrx, vec3(0, 0, -2), vec3(0, 0, 0), vec3(0, 1, 0)
  d3dDevice.SetTransform D3DTS_VIEW, Mtrx
End Sub
 

Рассмотрим ее подробнее. С помощью D3DXMatrixIdentity рассчитывается матрица «нулевого» преобразования (или идентичности) и в следующей строке записывается в трансформацию мира. Это означает, что координаты, указанные в вертексе, будут использоваться без каких-либо изменений, как есть.
С помощью D3DXMatrixPerspectiveFovLH рассчитывается матрица левосторонней перспективы, описывающая камеру с углом зрения 45º по вертикали (значение 3.141593 / 4 указано в радианах), и с отношением размера по вертикали к размеру по горизонтали, соответствующим размеру формы. Последние две величины – ограничение ближнего и дальнего планов видимости, зачем это нужно – рассмотрим чуть позже. Эта матрица записывается в трансформацию проекции.

И остается трансформация обзора, D3DXMatrixLookAtLH позволяет задать позицию наблюдателя в пространстве (первый вектор), направление его взгляда (второй вектор) и направление «вверх» (третий вектор). В нашем примере наблюдатель из точки «0, 0, -2» смотрит в начало координат. Направление «вверх» соответствует направлению оси Y.

Теперь зададим геометрию:

код на языке vb
 
Private Sub InitGeometry()
  Set vBuffer = d3dDevice.CreateVertexBuffer(2 * 3 * vSize, 0, vFlag, D3DPOOL_DEFAULT)
 
  Vert(0).Pos = vec3(-0.5, -0.5, 0)
  Vert(0).Color = &HFF
 
  Vert(1).Pos = vec3(0.5, -0.5, 0)
  Vert(1).Color = &HFF
 
  Vert(2).Pos = vec3(0, 0.5, 0)
  Vert(2).Color = &HFF
 
  D3DVertexBuffer8SetData vBuffer, 0, vSize * 3, 0, Vert(0)
 
  Vert(0).Pos = vec3(0, -0.5, -0.5)
  Vert(0).Color = &HFF0000
 
  Vert(1).Pos = vec3(0, -0.5, 0.5)
  Vert(1).Color = &HFF0000
 
  Vert(2).Pos = vec3(0, 0.5, 0)
  Vert(2).Color = &HFF0000
 
  D3DVertexBuffer8SetData vBuffer, vSize * 3, vSize * 3, 0, Vert(0)
End Sub
 

Два треугольника, синий и красный, расположены в начале координат и взаимно пересекаются, создавая фигуру, наподобие наконечника стрелы, направленной вверх. Жмем <F5> и видим синий треугольник. Почему нет красного? Правильно, красный треугольник расположен к нам боком и его не видно. Чтобы его увидеть – переместим наблюдателя:

код на языке vb
 
  D3DXMatrixLookAtLH Mtrx, vec3(1, 0, -2), vec3(0, 0, 0), vec3(0, 1, 0)
 

С этой позиции видно оба треугольника. Вернем наблюдателя на место и попробуем по-другому. Добавим перед вызовом Render такие строки:

код на языке vb
 
  D3DXMatrixRotationY Mtrx, Timer
  d3dDevice.SetTransform D3DTS_WORLD, Mtrx
 

Теперь, не меняя позиции наблюдателя, мы видим сцену с разных сторон. Мы вращаем саму сцену.
Вернемся к рассмотрению D3DTS_PROJECTION, почему бы не расширить зону видимости по оси Z от 0 до ∞? Дело в использовании ZBuffer, расстояние до рисуемого пикселя преобразуется перед записью в ZBuffer в значение от 0 до 1, что мы видели при использовании приведенного формата. Это значение имеет конечную точность, чем шире мы будем раздвигать границы, тем больше вероятность ошибки при Z-отсечении. Попробуем поменять значения zn и zf:

код на языке vb
 
  D3DXMatrixPerspectiveFovLH Mtrx, 3.141593 / 4, Me.ScaleHeight / Me.ScaleWidth, 0.0001, 1000
 

Запускаем программу и видим результат неточности.
Восстановим прежнее значение трансформации проекции и попробуем осуществить более сложное движение. Допустим, нам нужно так же вращать нашу «стрелу», но отодвинув ее на некоторое расстояние назад. Можно, конечно, отодвинуть наблюдателя, но что, если в сцене много разных объектов? Они отодвинутся все, а нам нужно, чтобы отодвинулась «стрела».

Второй вариант – перезаписать в вертексы новые значения, но этот вариант тоже неудобен, вдруг нам нужно не разово отодвинуть «стрелу», а передвигать ее постоянно, как мы постоянно ее вращаем. То есть нужно перемещение задавать матрицей, но как записать в одну трансформацию сразу две матрицы, вращения и перемещения? В математике для матриц определено действие умножения, при этом в результате получается матрица, объединяющая обе трансформации пространства. Добавим в основной цикл две строки:

код на языке vb
 
  Do While Running
    DoEvents
    D3DXMatrixRotationY Mtrx, Timer
    d3dDevice.SetTransform D3DTS_WORLD, Mtrx
    D3DXMatrixTranslation Mtrx, 0, 0, 1
    d3dDevice.MultiplyTransform D3DTS_WORLD, Mtrx
    Render
  Loop
 

Здесь мы рассчитали матрицу вращения, записали ее в D3DTS_WORLD, потом рассчитали матрицу перемещения и домножили D3DTS_WORLD на эту матрицу.

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

код на языке vb
 
    D3DXMatrixTranslation Mtrx, 0, 0, 1
    d3dDevice.SetTransform D3DTS_WORLD, Mtrx
    D3DXMatrixRotationY Mtrx, Timer
    d3dDevice.MultiplyTransform D3DTS_WORLD, Mtrx
 

Теперь цель достигнута, «стрела» отодвинулась на 1 вдоль оси Z и вращается вокруг своей оси. Того же результата можно достичь по-другому – сначала рассчитать матрицу необходимой трансформации, перемножив матрицы вращения и перемещения, а потом записать в D3DTS_WORLD полученную матрицу. Задайте еще одну матрицу:

код на языке vb
 
  Dim Mtrx2 As D3DMATRIX
 

И поменяйте строки в основном цикле так:

код на языке vb
 
  D3DXMatrixTranslation Mtrx, 0, 0, 1
  D3DXMatrixRotationY Mtrx2, Timer
  D3DXMatrixMultiply Mtrx, Mtrx2, Mtrx
  d3dDevice.SetTransform D3DTS_WORLD, Mtrx
 

Как обычно, код данного проекта можно найти в папке Pr04. Скачайте архив в 1 уроке.