1.7. Текстурирование Представьте, что нам нужно изобразить кирпичную стену. До сих пор мы рисовали цветные треугольники, очевидно, что при таком подходе для изображения стены понадобится огромное количество треугольников. Тут нам на помощь приходит текстурирование. Определение: Текстурирование – это использование плоского изображения (картинки) для закраски треугольника. Строго говоря, текстура не всегда является плоской картинкой, бывают одно- и трехмерные текстуры. Но пока не будем усложнять, подавляющее число текстур, используемых в 3D-программировании – это плоские картинки. Возьмем наш старый проект c треугольником (Pr02). Поменяем формат вертекса таким образом: Code Private Type vFormat PosX As Single PosY As Single PosZ As Single RHW As Single tu As Single tv As Single End Type И флаговое описание вертекса: Code Private Const vFlag = D3DFVF_XYZRHW Or D3DFVF_TEX1 В формате вертекса исчез цвет, но появились два новых поля – tu и tv, это координаты на текстуре (отсчет идет от верхнего левого угла), отмечающие ту точку текстуры, которая будет отображаться на наш вертекс. Еще раз осмыслите разницу – это координаты не в пространстве, а на текстуре. Для текстур в Direct3D существует специальный класс Direct3DTexture8. Создадим переменную для экземпляра этого класса: Code Dim Tex As Direct3DTexture8 Не забудьте внести соответствующую строку в ClearAll! Так же добавьте инициализацию уже знакомого нам D3DX, который поможет нам загрузить текстуру из файла: Code Set Tex = d3dx.CreateTextureFromFile(d3dDevice, App.Path & "\brick.jpg") В InitGeometry уберем строки, описывающие цвет вертексов и определим текстурные координаты: Code Vert(0).tu = 0 Vert(0).tv = 0 Vert(1).tu = 1 Vert(1).tv = 0 Vert(2).tu = 0 Vert(2).tv = 1 Остается в Render добавить строку, указывающую нашему устройству рендера использовать именно эту текстуру: Code d3dDevice.SetTexture 0, Tex Файл brick.jpg можно взять с компакт-диска и поместить в папку с проектом. Запускаем проект и видим «кирпичный» треугольник. Текстурные координаты «0, 0» соответствуют верхнему левому, «1, 0» – верхнему правому, а «0, 1» – нижнему левому углам. Поменяйте текстурные координаты: Code Vert(0).tu = -1 Vert(0).tv = -1 Vert(1).tu = 1.5 Vert(1).tv = -1 Vert(2).tu = -1 Vert(2).tv = 1.5 Этот опыт демонстрирует, что в качестве текстурных координат можно применять числа, выходящие из диапазона 0 .. 1, при этом текстура повторяется. Если затекстурить несколько треугольников с общими вертексами, равномерно распределив по вертексам текстурные координаты, стыки между треугольниками будут незаметны. Возьмем цилиндр из нашего предыдущего проекта и внесем подобные изменения. Так же, как в проекте с треугольником, уберем цвет и добавим текстурные координаты в формат вертекса, изменим флаговое описание, добавим инициализацию и уничтожение текстуры. Стоит остановиться на инициализации геометрии. Функция Vertex теперь приобретет такой вид: Code Private Function Vertex(X As Single, Y As Single, Z As Single, _ Tu As Single, Tv As Single) As vFormat Vertex.Pos = vec3(X, Y, Z) Vertex.Tu = Tu Vertex.Tv = Tv End Function А InitGeometry такой: Code Private Sub InitGeometry() Dim n As Long Set vBuffer = d3dDevice.CreateVertexBuffer(2 * 65 * vSize, 0, vFlag, D3DPOOL_DEFAULT) For n = 0 To 64 Vert(0) = Vertex(Sin(2 * Pi * n / 64), -1, Cos(2 * Pi * n / 64), 6 * n / 64, 2) Vert(1) = Vertex(Sin(2 * Pi * n / 64), 1, Cos(2 * Pi * n / 64), 6 * n / 64, 0) D3DVertexBuffer8SetData vBuffer, vSize * 2 * n, vSize * 2, 0, Vert(0) Next n End Sub Текстурная координата Tv распределена пропорционально высоте цилиндра (координата Y), а Tu пропорционально углу относительно центральной оси. Посмотрите на результат и, ради тренировки, попробуйте покрыть цилиндр этой же текстурой под углом 45? таким образом, чтобы не осталось шва. На компакт-диске в папках Pr06 и Pr07 находятся исходные коды проектов с затекстуренными треугольником и цилиндром. 1.8. Индексирование вертексов Представьте, что у нас есть карта некоторой поверхности, представляющая из себя список высот точек на прямоугольном участке. Точки расположены равномерно через равные интервалы, при таком расположении нам достаточно указать только координату Y (высота) точки, а другие координаты легко вычисляются из порядкового номера точки. Такая карта высот называется регулярной сеткой. Для простоты возьмем квадратную сетку размером 64 * 64. Часто для хранения таких карт применяют монохромные картинки, где яркость соответствующего пикселя интерпретируется как высота. В папке Pr08 будет наш новый проект, там можно взять соответствующую карту – файл HeightMap.tga. Сетка такого размера будет состоять из 63 * 63 квадратных ячеек, каждую из которых можно изобразить двумя треугольниками. То есть если использовать обычный TRIANGLELIST, то нам понадобится 63 * 63 * 2 * 3 = 23814 вертексов. Можно разрезать карту на 63 полосы TRIANGLESTRIP, тогда число вертексов уменьшится до 64 * 63 * 2 = 8064. Неплохая экономия, но ведь реально на сетке 64 * 64 = 4096 точки, можно ли обойтись таким же количеством вертексов? Да, можно, для этого создают не один, а два буфера, первый – вертексный, содержащий только необходимые вертексы без повторов. Второй буфер будет содержать индексы, то есть порядковые номера вертексов в вертексном буфере. Индексы расположены так, что вместо дублирования вертекса, мы дублируем индекс – указатель на вертекс. Представим, что вертексы пронумерованы рядами слева направо, от ближних к дальним, нумерация идет от нуля. Для вывода с использованием TRIANGLELIST можно расположить индексы так: Первый треугольник образован вертексами 0, 64, 65, то есть такими и будут первые три индекса – 0, 64, 65. Второй – 0, 65, 1 и так далее. Карта, описанная таким образом, будет содержать 4096 вертексов и 23814 индексов. Что же мы выиграли? Дело в том, что индекс – это, в отличие от вертекса, обычное 16-ти либо 32-х битное число, которое занимает значительно меньше памяти, чем вертекс, особенно при сложных форматах вертекса. Кроме того, при расчетах трансформаций процессор вынужден повторно производить вычисления для продублированных вертексов, так как считает их разными, а при индексировании один раз рассчитанный вертекс попадает в кэш, и при повторном обращении уже может не рассчитываться. Использование индексов дает и другие преимущества, которые мы рассмотрим позже. Создадим новый проект с уже знакомой нам инициализацией d3dDevice и вертексным буфером с таким форматом: Code Private Type vFormat Pos As D3DVECTOR Color As Long End Type Задайте соответствующую константу флагового описания вертекса. Добавьте три новых переменных: Code Dim numIndex As Long, numTri As Long, numVertex As Long И одну переменную нового типа Direct3DIndexBuffer8: Code Dim iBuf As Direct3DIndexBuffer8 Как и всякую переменную объектного типа добавьте iBuf в список для уничтожения. В соответствие с форматом вертекса функция Vertex приобретет такой вид: Code Private Function Vertex(x As Single, y As Single, z As Single, c As Long) As vFormat Vertex.Pos = vec3(x, y, z) Vertex.Color = c End Function Процедура InitGeometry будет состоять из двух частей, в первой инициализируется вертексный буфер. Интересующие нас данные расположены в TGA файле с 19-го байта, эта часть выглядит так: Code Dim x As Long, z As Long, nf As Integer, b As Byte Dim Vert(64 * 64 - 1) As vFormat vSize = Len(Vert(0)) numVertex = 64 * 64 Set vBuf = d3dDevice.CreateVertexBuffer(numVertex * vSize, 0, vFlag, D3DPOOL_DEFAULT) nf = FreeFile Open "HeightMap.tga" For Binary As #nf For z = 0 To 63 For x = 0 To 63 Get #nf, x + z * 64 + 19, b Vert(x + z * 64) = Vertex(x - 31.5, b * 0.05, z - 31.5, &H808080) Next x Next z Close #nf При вызове функции Vertex координаты x и z получены вычитанием 31.5 из соответствующих координат карты, это сделано с целью центровки карты относительно начала координат. Для получения координаты y данные о высоте, взятые из файла, умножаются на 0.05 – это вертикальный масштаб. Для всех вертексов задан серый цвет &H808080. Новая для нас вторая часть процедуры InitGeometry: Code Dim Ind(63 * 63 * 2 * 3 - 1) As Integer numTri = 63 * 63 * 2 numIndex = numTri * 3 Set iBuf = d3dDevice.CreateIndexBuffer(numIndex * 2, 0, D3DFMT_INDEX16, D3DPOOL_DEFAULT) For z = 0 To 62 For x = 0 To 62 Ind((z * 63 + x) * 2 * 3 + 0) = (z + 0) * 64 + x + 0 Ind((z * 63 + x) * 2 * 3 + 1) = (z + 1) * 64 + x + 0 Ind((z * 63 + x) * 2 * 3 + 2) = (z + 1) * 64 + x + 1 Ind((z * 63 + x) * 2 * 3 + 3) = (z + 0) * 64 + x + 0 Ind((z * 63 + x) * 2 * 3 + 4) = (z + 1) * 64 + x + 1 Ind((z * 63 + x) * 2 * 3 + 5) = (z + 0) * 64 + x + 1 Next x Next z D3DIndexBuffer8SetData iBuf, 0, numIndex * 2, 0, Ind(0) Двойным циклом проходим по всем ячейкам карты и шестью индексами задаем два треугольника в соответствие с рисунком в начале главы. В процедуре Render появится новая строка: Code d3dDevice.SetIndices iBuf, 0 Здесь мы указываем устройству рендера какой индексный буфер использовать и с какого индекса начинать выборку. И осталось изменить само рисование: Code d3dDevice.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, numVertex, 0, numTri Запускаем программу – и видим смутно угадываемый фрагмент ландшафта. Дело в том, что вся поверхность равномерно серая, независимо от расстояния и наклона. Попробуем сымитировать эффект освещения раскраской вертексов в зависимости от наклона поверхности, но для этого сначала немного теории. 1.9. Нормали, свет Представим, что все пространство пронизывает направленный свет. Каким образом можно определить, насколько ярко будет освещен произвольный участок поверхности? Очевидно, что если угол между направлением на источник света и вектором, перпендикулярным поверхности больше 90º, то свет на поверхность падать не будет вообще. Если этот угол меньше – свет на поверхность попадает, причем тем больше, чем меньше этот угол. Вектор, перпендикулярный поверхности, иначе называемый нормалью, является еще одним часто применяемым и очень полезным элементом формата вертекса. Найти нормаль не сложно, векторное произведение двух векторов обладает таким замечательным свойством, что результирующий вектор всегда перпендикулярен двум исходным векторам (либо равен нулю, если исходные вектора параллельны). Таким образом для нахождения нормали достаточно векторно перемножить два любых не параллельных вектора, лежащих на поверхности. И, как и с матрицами, нам самим не обязательно разбираться в дебрях векторной алгебры – в составе DirectX есть все необходимые готовые функции. Векторами, лежащими на поверхности в районе вертекса (x, z) приблизительно можно считать вектор, соединяющий вертекс (x - 1, z) с вертексом (x + 1, z), и вектор, соединяющий вертекс (x, z - 1) с вертексом (x, z + 1). Назовем их, соответственно vX и vZ: Code vX = vec3(2, Vert(((x - 1) And 63) + z * 64).Pos.y - Vert(((x + 1) And 63) + _ z * 64).Pos.y, 0) vZ = vec3(0, Vert(x + ((z - 1) And 63) * 64).Pos.y - Vert((x + ((z + 1) And 63) * _ 64)).Pos.y, 2) Далее две строки: Code D3DXVec3Cross v, vX, vZ D3DXVec3Normalize v, v Здесь вектора перемножаются (D3DXVec3Cross) и результирующий вектор нормализуется, то есть приводится к единичной длине. В зависимости от величины компоненты x полученной нормали вычисляем цвет вертекса, как будто свет распространяется вдоль оси x: Code If v.x > 0 Then Vert(x + z * 64).Color = Int(v.x * 255) * &H10101 Else Vert(x + z * 64).Color = 0 End If Теперь наш фрагмент поверхности выглядит значительно реалистичнее. Результат можно найти в папке Pr08. Продолжаем. Свет смотрится достаточно натурально, но что, если нам нужно менять направление света? Каждый раз рассчитывать цвет всех вертексов? Такой подход явно не годится, эту работу можно переложить на DirectX. Добавим в формат вертекса новую компоненту – нормаль: Code Private Type vFormat Pos As D3DVECTOR Normal As D3DVECTOR Color As Long End Type Соответственно изменим флаговое описание: Code Private Const vFlag = D3DFVF_XYZ Or D3DFVF_NORMAL Or D3DFVF_DIFFUSE В D3DInit разрешим использовать свет: Code d3dDevice.SetRenderState D3DRS_LIGHTING, 1 Direct3D может обрабатывать до восьми источников света, их нумерация идет от нуля. Разрешим использование нулевого источника: Code d3dDevice.LightEnable 0, 1 В функции Vertex будем принудительно окрашивать все вертексы в белый цвет: Code Private Function Vertex(x As Single, y As Single, z As Single) As vFormat Vertex.Pos = vec3(x, y, z) Vertex.Color = &HFFFFFF End Function В процедуре InitGeometry переделаем фрагмент, в котором вертексы окрашивались в зависимости от нормали. Теперь мы просто записываем вычисленное значение нормали в вертекс: Code For z = 0 To 63 For x = 0 To 63 vX = vec3(2, Vert(((x - 1) And 63) + z * 64).Pos.y - Vert(((x + 1) And 63) + z * 64).Pos.y, 0) vZ = vec3(0, Vert(x + ((z - 1) And 63) * 64).Pos.y - Vert((x + ((z + 1) And 63) * 64)).Pos.y, 2) D3DXVec3Cross v, vX, vZ D3DXVec3Normalize v, v Vert(x + z * 64).Normal = v Next x Next z И создадим новую процедуру InitLight, в которой будем присваивать источнику света необходимые параметры: Code Private Sub InitLight() Dim Light As D3DLIGHT8 Light.Type = D3DLIGHT_DIRECTIONAL Light.Direction = vec3(Sin(Timer), 0.6, Cos(Timer)) Light.diffuse.r = 1 Light.diffuse.g = 1 Light.diffuse.b = 1 d3dDevice.SetLight 0, Light End Sub Здесь создается переменная Light структурного типа D3DLIGHT8. В переменную записываем тип источника света – D3DLIGHT_DIRECTIONAL, то есть направленный свет. Раз направленный – зададим направление, в поле Direction записываем вектор, который будет менять направление с течением времени. Далее идет цвет, это все то же RGB, но задающиеся не тремя байтами, а Single значениями в диапазоне от 0 до 1. Цвет называется diffuse потому, что в дальнейшем он будет умножаться именно на diffuse компоненту в вертексе. Даем полный свет. И строкой d3dDevice.SetLight 0, Light «загоняем» наши параметры в нулевой источник света. Вызов процедуры InitLight необходимо поместить внутрь нашего главного цикла в Form_Load, ведь направление света будет изменяться во времени, однократного вызова InitLight недостаточно. Запускаем – если ошибок не было, то видим свет DirectX в действии, ну а если ошибки преследуют, то проект можно извлечь из папки Pr09 на компакт-диске. 1.10. Материал Обратили внимание на то, что поле Color в формате вертекса стало явно избыточным? Если во все вертексы все равно пишется одно и то же значение цвета, то может можно его туда не писать вообще, а задать цвет как-то по другому? DirectX предоставляет такую возможность – это использование материала. Уберем из формата вертекса поле Color, а также все, что с ним в программе было связано. Не буду уточнять, мы уже меняли формат вертекса неоднократно. Добавим в проект новую процедуру InitMaterial: Code Private Sub InitMaterial() Dim Mat As D3DMATERIAL8 Mat.diffuse.r = 1 Mat.diffuse.g = 1 Mat.diffuse.b = 1 d3dDevice.SetMaterial Mat End Sub Переменная структурного типа D3DMATERIAL8 заполняется параметрами используемого материала и с помощью d3dDevice.SetMaterial мы даем указание этот материал использовать. Параметры материала неизменны, поэтому достаточно вызвать InitMaterial один раз. Поместим вызов перед главным циклом. Остановим для разнообразия вращение света и заставим вращаться саму карту. Тогда и вызов InitLight можно переместить из главного цикла в начало. Программа готова – можно запускать. Использование материала вместо цвета в вертексе с одной стороны немного ограничило наши возможности – теперь мы не можем задать каждому вертексу свой цвет, с другой стороны у нас появились и новые возможности. Добавим к инициализации материала строку: Code Mat.Ambient = Mat.diffuse Ambient, как и diffuse – это тоже цвет, но не направленный, а рассеянный. Заполнив поле Ambient у источника света, мы заставим равномерно светиться даже затененные участки. Можно не трогать источник света, а задать общий рассеянный свет. Добавьте в D3DInit еще одну строку: Code d3dDevice.SetRenderState D3DRS_AMBIENT, &H302080 Теперь при запуске мы видим, что затененные участки карты не черные, а темно синие. Довольно похоже на снег. Пример использования материала находится в папке Pr10. Попробуйте, изменяя цвет света и материала, добиться различных оттенков, чтобы почувствовать работу света. Ну и проведем еще один эксперимент на нашей карте – покроем ее текстурой. Добавьте в вертекс текстурные координаты: Code Private Type vFormat Pos As D3DVECTOR Normal As D3DVECTOR tu As Single tv As Single End Type Так же добавьте все, что необходимо для создания, использования и удаления текстуры. Текстуру травы можно взять в папке Pr11. Текстурные координаты поставим в зависимость от координат x и z: Code Vert(x + z * 64) = Vertex(x - 31.5, b * 0.03, z - 31.5, x * 0.02, z * 0.02) Последние два параметра в функции Vertex – это и есть текстурные координаты. Мы получили изображение поверхности земли, покрытой травой. Формат вертекса, используемый в этом проекте, является одним из наиболее часто применяемых в Direct3D. Соответствующее ему флаговое описание – комбинация трех констант D3DFVF_XYZ Or D3DFVF_NORMAL Or D3DFVF_TEX1 даже имеет отдельное обозначение – константа D3DFVF_VERTEX. Продолжение |