Программирование 3D графики на Visual Basic 6 и DirectX 8. Часть 3
2.1. Организация проекта До сих пор все наши проекты состояли из одной единственной формы. Весь код проекта не был распределен по модулям. Это, конечно, неправильный подход, он применялся для более легкого освоения начальных понятий программирования с использованием Direct3D, ведь пока вы не уясните основные принципы такого программирования, не будет понятна и логика деления кода на модули. Однако постепенно у нас выделились процедуры, которые мы переносили из проекта в проект практически не меняя. В первую очередь это D3DInit – инициализация Direct3D. Создадим новый модуль, в котором будем собирать самые необходимые процедуры, относящиеся к Direct3D. Объявим в нем глобальные переменные:
Code
Public dx8 As New DirectX8 Public d3d As Direct3D8 Public d3dx As New D3DX8 Public d3dDevice As Direct3DDevice8
Также имеет смысл объявить константу Pi:
Code
Public Const Pi = 3.141593
И сама процедура D3DInit:
Code
Public Sub D3DInit(hWnd As Long) Dim DispMode As D3DDISPLAYMODE Dim d3dpp As D3DPRESENT_PARAMETERS
Set d3d = dx8.Direct3DCreate d3d.GetAdapterDisplayMode D3DADAPTER_DEFAULT, DispMode
Set d3dDevice = d3d.CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd _ , D3DCREATE_SOFTWARE_VERTEXPROCESSING, d3dpp) End Sub
Для уничтожения объектных переменных, созданных в модуле, послужит процедура D3DTerminate:
Code
Public Sub D3DTerminate() Set d3dx = Nothing Set d3dDevice = Nothing Set d3d = Nothing Set dx8 = Nothing End Sub
Так же сюда может войти неизменная функция vec3:
Code
Public Function vec3(x As Single, y As Single, z As Single) As D3DVECTOR vec3.x = x vec3.y = y vec3.z = z End Function
В форме желательно держать только код обработки событий контролов формы. Один из них – таймер, поместим на форму соответствующий контрол и настроим его на 1000 мс интервал. С помощью таймера мы будем в дальнейшем замерять быстродействие. Еще одно важнейшее событие формы – ее закрытие. Немного изменим код его обработки:
Code
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) If Running Then Cancel = 1: Running = False End Sub
А вот для синхронизации движения таймер применять мы больше не будем, он применялся для простоты в самых начальных проектах. Дело в том, что таймер обладает невысокой точностью при измерении небольших интервалов времени, теперь для этого мы применим более современную API функцию – QueryPerformanceCounter. Для функций API можно создать отдельный модуль и поместить туда такой код:
Code
Option Explicit
Private Type int64 dw1 As Long dw2 As Long End Type
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As int64) As Long Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As int64) As Long Dim QSpeed As Double
Public Function QTime() As Double Dim QD As int64, t As Double QueryPerformanceCounter QD If QD.dw1 < 0& Then t = QD.dw1 + 4294967296# Else t = QD.dw1 If QD.dw2 < 0& Then t = t + (QD.dw2 + 4294967296#) * 4294967296# _ Else t = t + QD.dw2 * 4294967296# QTime = t * QSpeed End Function
Public Sub QFreqIni() Dim QD As int64 QueryPerformanceFrequency QD If QD.dw1 < 0& Then QSpeed = QD.dw1 + 4294967296# Else QSpeed = QD.dw1 If QD.dw2 < 0& Then QSpeed = QSpeed + (QD.dw2 + 4294967296#) * 4294967296# _ Else QSpeed = QSpeed + QD.dw2 * 4294967296# QSpeed = 1# / QSpeed End Sub
В начале работы программы однократно вызываем QFreqIni, и для получения значения текущего времени в секундах пользуемся функцией QTime. И создадим еще один, пока последний, модуль – modMain, в свойствах проекта укажем, что стартовым должен быть именно этот модуль, а не форма. В этом модуле будут находиться все наши «эксперименты» до тех пор, пока они не удостоятся переноса в уже существующий или вновь созданный модуль. Если поместить в modMain такой код:
Code
Option Explicit
Public Running As Boolean Public FPS As Long
Public Sub Main() frmD3D.Show QFreqIni D3DInit frmD3D.hWnd Running = True Do While Running DoEvents Render FPS = FPS + 1 Loop Unload frmD3D ClearAll End Sub
Private Sub Render() d3dDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &HFFFFFF * Rnd, 1, 0 d3dDevice.Present ByVal 0, ByVal 0, 0, ByVal 0 End Sub
Private Sub ClearAll() D3DTerminate End Sub
Мы получим приблизительный аналог нашего первого проекта, где мы закрашивали форму в разные цвета. Можно в обработчик события Timer формы поместить следующий код, чтобы измерять быстродействие нашей программы:
Code
Private Sub TimerFPS_Timer() Me.Caption = FPS FPS = 0 End Sub
Этот проект находится на компакт-диске в папке Pr12. Теперь, когда наша «перестройка» завершена, продолжим заниматься изучением Direct3D.
2.2. Мип-мэппинг, фильтрация текстур При текстурировании существует проблема. Текстура состоит из конечного числа точек, которые называются текселями. На текстурируемой поверхности текстурные координаты могут располагаться с различной плотностью, кроме того сама поверхность может находиться дальше или ближе к камере, что приводит к изменению видимых размеров. В результате текстура накладывается некорректно. Представьте, что текстура, шириной в 16 текселей должна отобразиться на участок бэкбуфера, шириной в 17 или 15 пикселей. В первом случае один из текселей должен будет отобразиться на два пикселя, во втором одному из текселей не хватает места. Когда размер изображения значительно превышает размер текстуры, последняя, как говорят, «распадается на клетки». Для наглядности сделаем проект. В модуль modMain предыдущего проекта добавьте все необходимое для создания вертексного буфера на четыре вертекса и одной текстуры. Вертекс должен содержать координаты XYZ и текстурные координаты. Создайте квадрат, размером 2 * 2 с центром в начале координат так, чтобы текстура десять раз укладывалась на поверхность квадрата:
Code
Private Sub InitGeometry() Dim Vert(3) As vFormat vSize = Len(Vert(0)) Set vBuf = d3dDevice.CreateVertexBuffer(4 * vSize, 0, vFlag, D3DPOOL_DEFAULT) Vert(0) = Vertex(-1, 0, -1, -5, -5) Vert(1) = Vertex(-1, 0, 1, -5, 5) Vert(2) = Vertex(1, 0, -1, 5, -5) Vert(3) = Vertex(1, 0, 1, 5, 5) D3DVertexBuffer8SetData vBuf, 0, 4 * vSize, 0, Vert(0) End Sub
Камеру расположите низко над квадратом, чтобы смотреть вдоль его поверхности:
Проинициализируйте остальные необходимые трансформации. Поместите в процедуру Render команду рисования квадрата, используя D3DPT_TRIANGLESTRIP для двух треугольников. Можно взять уже знакомую текстуру кирпичей. Если при запуске программы вы видите черное поле, видимо вы забыли запретить использование света. Так же поместите перед основным циклом команду инициализации QueryPerformanceCounter – QFreqIni, а в сам цикл вращение трансформации мира:
Code
D3DXMatrixRotationY Mtrx, QTime * 0.1
Теперь мы видим медленно вращающийся квадрат, в таком ракурсе прекрасно видно все виды искажения текстуры. Вдали текстура вообще превращается в мелкую рябь, вблизи распадается на клетки и, лишь на небольшом отдалении, где истинный размер текстуры близок к отображаемому, поле выглядит достаточно прилично. Для начала избавимся от самого неприятного искажения – ряби в удалении. Для этого применяется мип-мэппинг. Суть его состоит в том, что для отображения мелких или удаленных объектов, когда истинный размер текстуры превышает отображаемый, используется другая текстура более мелкого размера, полученная из первоначальной. При загрузке текстуры сразу создаются эти дополнительные изображения – их называют мип-уровнями. Линейные размеры каждого следующего мип-уровня вдвое меньше, чем у предыдущего. Последний мип-уровень, если мы явно не указали другое, имеет размер 1 * 1. Текстурой в Direct3D называется не отдельное изображение, а весь этот набор мип-уровней, при текстурировании Direct3D сам выбирает из них наиболее подходящий. И самое интересное, что все эти мип-уровни уже созданы, нам лишь осталось разрешить их применение. Для этого в инициализацию добавим всего одну строку:
И вот рябь исчезла, а быстродействие даже возросло! Но текстура по-прежнему распадается на клетки, для борьбы с этим применяется другой метод – фильтрация. При выборке из текстуры берется усредненное значение текселей, соседних с точкой выборки. Способ вычисления может быть разный, но, как правило, применяется линейная фильтрация. Для ее включения достаточно одной строки:
Это мы включили фильтрацию на увеличение текстуры. Картинка вблизи стала лучше, но при удалении клетки все еще есть. Включим такую же фильтрацию и на уменьшение:
Теперь картинка почти идеальна, есть лишь один недостаток – при удалении текстура сильно «размазана» по поверхности. Это происходит оттого, что мы видим поверхность под острым углом, и при подборке такого мип-уровня, чтобы размер текселя «вдоль» направления взгляда примерно соответствовал размеру пикселя, размер текселя в направлении «поперек» значительно превышает размер пикселя, что сильно заметно на глаз. С этим тоже можно бороться, для этого существует так называемая анизотропная фильтрация. Заменим MINFILTER на такой:
Это ограничение нужно потому, что анизотропная фильтрация – это очень тяжелые вычисления, сильно снижающие производительность, мы вынуждены искать баланс между качеством и скоростью. Кроме того, различные видеоадаптеры поддерживают различный максимальный уровень анизотропии, и желательно не превышать этот уровень, хотя опыт показывает, что превышение этого уровня не приводит к сбою работы программы. Вполне возможно, что вы сейчас не увидели эффекта от включения анизотропной фильтрации, это означает, что ваш видеоадаптер ее не поддерживает. Этот проект находится на компакт-диске в папке Pr13.
2.3. Caps. Проверка совместимости До сих пор мы применяли в основном стандартные функции Direct3D, совместимые практически с любыми современными видеоадаптерами. И только в предыдущем проекте мы столкнулись с возможной несовместимостью. Возникает вопрос, а можно ли написать программу так, чтобы она узнавала «на месте» возможности оборудования и автоматически выбирала наиболее оптимальный режим работы, либо отказывалась работать при необходимости? Можно. Для этого в DirectX имеется структура Caps, содержащая всю необходимую информацию об оборудовании. Пользоваться ей очень просто. Добавьте в модуль modDX общую переменную типа D3DCAPS8:
Теперь мы можем узнавать необходимые нам сведения. Например, в предыдущем проекте можно таким образом выбрать уровень анизотропии:
Code
If Caps.MaxAnisotropy >= 4 Then d3dDevice.SetTextureStageState 0, D3DTSS_MINFILTER, D3DTEXF_ANISOTROPIC d3dDevice.SetTextureStageState 0, D3DTSS_MAXANISOTROPY, 4 ElseIf Caps.MaxAnisotropy >= 2 Then d3dDevice.SetTextureStageState 0, D3DTSS_MINFILTER, D3DTEXF_ANISOTROPIC d3dDevice.SetTextureStageState 0, D3DTSS_MAXANISOTROPY, 2 Else d3dDevice.SetTextureStageState 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR End If
Можно также предоставить возможность пользователю программы самому выбрать режим фильтрации из возможных вариантов. Полный список всех полей Caps имеется в DirectX SDK. Мы еще не раз будем возвращаться к этой полезной структуре.
2.4. Мультитекстурирование До сих пор мы накладывали на наши модели не более одной текстуры, в то время как Direct3D позволяет использовать одновременно до восьми текстур. Мы уже не раз использовали такую команду:
Code
d3dDevice.SetTextureStageState
Далее шло три аргумента – первый 0, далее константа из набора (Enum) CONST_D3DTEXTURESTAGESTATETYPE, определяющая какой именно параметр текстурирования мы хотим установить, например D3DTSS_MIPFILTER означает выбор типа мип-фильтрации. Последний параметр функции – непосредственное значение для данного параметра. Для большинства параметров текстурирования созданы наборы констант с допустимыми значениями, но для некоторых, например D3DTSS_MAXANISOTROPY, далее идет просто численное, типа Long, значение. Текстуры накладываются стадиями. То есть некоторое количество текстур (до трех) определенным образом смешиваются, и результат уже выводится на экран. Точнее так обстоит дело для последней используемой стадии, результаты предыдущих стадий являются одним из аргументов для следующих. Наравне с текстурами в стадиях текстурирования участвуют и некоторые другие величины, например Diffuse – цвет вертекса либо материала. Вспомним, что первым аргументом d3dDevice.SetTextureStageState был 0. Это и есть номер стадии текстурирования, настройку параметров которой мы производим. Максимальное количество стадий, как и текстур – восемь. К сожалению, такое количество текстур и стадий текстурирования поддерживает далеко не любая видеокарта. Допустимые количества можно узнать из Caps.MaxSimultaneousTextures и Caps.MaxTextureBlendStages. Со стадиями и текстурами немного разобрались. А как же параметры текстурирования? Мы пользовались уже текстурой в сочетании с цветом (Diffuse), и они сочетались корректно, хотя мы не указывали никаких конкретных способов их смешивания. Дело в том, что многие настройки в Direct3D сделаны заранее и являются настройками по умолчанию. Так по умолчанию используется одна стадия текстурирования, в которой происходит умножение текстуры и цвета (Diffuse). Если мы не установили текстуру либо цвет – по умолчанию используется белый цвет. «Внутри» Direct3D любой цвет представлен так же, как мы привыкли, компонентами R, G и B (есть еще A, но об этом позже). Но в Direct3D это не байты от 0 до 255, а дробные значения от 0 до 1. Таким образом, при умножении любого цвета на белый, цвет не меняется. Давайте вспомним наш первый пример с цилиндром. Вместо того, чтобы увидеть текстуру, мы сначала увидели черный цвет, текстура стала видна лишь после запрещения использования света. А все дело в том, что если свет разрешен, но не включено ни одного источника света, Diffuse будет черным, а значит, черным будет и произведение на Diffuse любой текстуры. Вместо строки:
И текстура станет видимой! Вместо того, чтобы выключить свет, мы изменили способ, каким смешиваются аргументы нулевой стадии текстурирования. По умолчанию действовали следующие установки:
То есть первый аргумент – текстура, второй – цвет, действие – умножение. Мы заменили умножение D3DTOP_MODULATE на выбор первого аргумента D3DTOP_SELECTARG1. Попробуем добиться какого-нибудь полезного эффекта, используя две текстуры. За основу берем проект из главы «Мип-мэппинг, фильтрация текстур». Для начала то, что мы уже отладили – фильтрация текстур, перенесем в модуль ModDX. Для этого создадим такую процедуру:
Code
Public Sub TexFilter(Stage As Long, TF As TexF, Optional MaxAnisotropy As Long = 2) Select Case TF Case TexF_None d3dDevice.SetTextureStageState Stage, D3DTSS_MIPFILTER, D3DTEXF_NONE d3dDevice.SetTextureStageState Stage, D3DTSS_MAGFILTER, D3DTEXF_NONE d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_NONE Case TexF_BiLinear d3dDevice.SetTextureStageState Stage, D3DTSS_MIPFILTER, D3DTEXF_POINT d3dDevice.SetTextureStageState Stage, D3DTSS_MAGFILTER, D3DTEXF_LINEAR d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_LINEAR Case TexF_TriLinear d3dDevice.SetTextureStageState Stage, D3DTSS_MIPFILTER, D3DTEXF_LINEAR d3dDevice.SetTextureStageState Stage, D3DTSS_MAGFILTER, D3DTEXF_LINEAR d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_LINEAR Case TexF_Anisotropic d3dDevice.SetTextureStageState Stage, D3DTSS_MIPFILTER, D3DTEXF_LINEAR d3dDevice.SetTextureStageState Stage, D3DTSS_MAGFILTER, D3DTEXF_LINEAR If Caps.MaxAnisotropy >= MaxAnisotropy Then d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_ANISOTROPIC d3dDevice.SetTextureStageState Stage, D3DTSS_MAXANISOTROPY, MaxAnisotropy ElseIf Caps.MaxAnisotropy >= 2 Then d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_ANISOTROPIC d3dDevice.SetTextureStageState Stage, D3DTSS_MAXANISOTROPY, Caps.MaxAnisotropy Else d3dDevice.SetTextureStageState Stage, D3DTSS_MINFILTER, D3DTEXF_LINEAR End If End Select End Sub
Аргументами процедуры TexFilter являются номер стадии текстурирования, один из четырех стандартных вариантов фильтрации и, необязательный аргумент – уровень анизотропии. Для вариантов фильтрации можно сделать набор констант, чтобы обращаться к ним по именам:
Code
Public Enum TexF TexF_None TexF_BiLinear TexF_TriLinear TexF_Anisotropic End Enum
Имена констант говорят сами за себя, так что расшифровывать не буду. Теперь нам понадобится две пары текстурных координат, поэтому переделаем формат вертекса:
Code
Private Type vFormat Pos As D3DVECTOR tu0 As Single tv0 As Single tu1 As Single tv1 As Single End Type
Так же переделаем его флаговое описание:
Code
Private Const vFlag = D3DFVF_XYZ Or D3DFVF_TEX2
И функцию Vertex:
Code
Private Function Vertex(x As Single, y As Single, z As Single, tu0 As Single _ , tv0 As Single, tu1 As Single, tv1 As Single) As vFormat Vertex.Pos = vec3(x, y, z) Vertex.tu0 = tu0 Vertex.tv0 = tv0 Vertex.tu1 = tu1 Vertex.tv1 = tv1 End Function
Вспомним, как мы генерировали цилиндр, и немного переделаем с учетом применения двух текстур:
Code
Private Sub InitGeometry() Dim Vert(1) As vFormat Dim n As Long vSize = Len(Vert(0)) Set vBuf = 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), _ 3 * n / 64, 0, 12 * n / 64, 0) Vert(1) = Vertex(Sin(2 * Pi * n / 64), -1, Cos(2 * Pi * n / 64), _ 3 * n / 64, 1, 12 * n / 64, 4) D3DVertexBuffer8SetData vBuf, vSize * 2 * n, vSize * 2, 0, Vert(0) Next n End Sub
Создадим две текстуры:
Code
Set Tex0 = d3dx.CreateTextureFromFile(d3dDevice, App.Path & "\brick.jpg") Set Tex1 = d3dx.CreateTextureFromFile(d3dDevice, App.Path & "\detail.jpg")
Файлы brick.jpg и detail.jpg есть в папке Pr14 на компакт-диске. Также подберите подходящие матрицы для основных трансформаций (это вы уже можете), впишите все необходимое для создания и уничтожения цилиндра с двумя текстурами и инициализации QueryPerformanceCounter. В основной цикл можно вписать такие строки:
Здесь мы заставляем выводимую геометрию вращаться, а камеру циклично приближаться и удаляться от начала координат. Можно также включить фильтрацию текстур обеих стадий, не зря же мы писали процедуру TexFilter:
Теперь все в порядке, цвет появился, но текстура только одна, первая стадия текстурирования выключена. Во-первых, в процедуре Render нужно указывать не одну, а две текстуры:
Здесь мы первый аргумент, текстуру, умножаем на второй – D3DTA_CURRENT, этот аргумент – результат работы предыдущей стадии текстурирования. Обратите внимание, что умножение используется не D3DTOP_MODULATE, а D3DTOP_MODULATE2X. Этот оператор домножает результат перемножения аргументов на 2, что позволяет использовать одну текстуру в качестве карты теней для другой. Вспомните, что в Direct3D компоненты цвета лежат в диапазоне от 0 до 1. Если использовать в качестве второго аргумента серую текстуру (R = 128, G = 128, B = 128), мы умножаем первый аргумент сначала на 0.5, потом на 2 – то есть не меняем. Если на серой текстуре сделать более темные и более светлые места – в этих местах первый аргумент будет соответственно затемняться и высветляться. Обратите внимание, что текстурные координаты у первой текстуры расположены более «густо», чем у нулевой. Это позволяет добиться детализации нулевой текстуры при сильном приближении. Этот проект находится на компакт-диске в папке Pr14.
Также если вы считаете, что данный материал мог быть интересен и полезен кому-то из ваших друзей, то вы бы могли посоветовать его, отправив сообщение на e-mail друга:
Игровые объявления и предложения:
Если вас заинтересовал материал «Программирование 3D графики на Visual Basic 6 и DirectX 8. Часть 3», и вы бы хотели прочесть что-то на эту же тему, то вы можете воспользоваться списком схожих материалов ниже. Данный список сформирован автоматически по тематическим меткам раздела.
Предлагаются такие схожие материалы:
Если вы ведёте свой блог, микроблог, либо участвуете в какой-то популярной социальной сети, то вы можете быстро поделиться данной заметкой со своими друзьями и посетителями.
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи. [ Регистрация | Вход ]