Сообщений 19    Оценка 230 [+2/-0]         Оценить  
Система Orphus

Работа с графикой средствами Direct3D

Глава из книги "Программирование графики: GDI+ и DirectX".

Авторы: Виталий Брусенцев
Алексей Поляков

Источник: RSDN Magazine #2-2005
Опубликовано: 24.11.2005
Версия текста: 1.0
Обзор библиотеки DirectX 9.0
Пакет DirectX 9.0 SDK
Первые программы
Создаем первую программу на C++
Обзор модели COM
Managed DirectX. Программирование для .NET
Начало работы. Инициализация Direct3D
Инициализация и перечисление графических устройств средствами C++
Перечисление устройств и видеорежимов для Managed Direct3D
Инициализация графического режима для C++
Инициализация графического режима: версия для C#
Удаление невидимых деталей
Пирамида видимости
Буфер глубины (Z-buffer)
Удаление нелицевых граней
Заключение

В этой главе рассматриваются:

Обзор библиотеки DirectX 9.0

Создание сложных и высокопроизводительных мультимедийных программ требует непосредственного доступа к аппаратным ресурсам. Библиотека DirectX содержит набор низкоуровневых программных интерфейсов, предоставляющий такой доступ Windows-программистам. Если вы собираетесь создать увлекательную сетевую игру, мультимедийное приложение или хранитель экрана с использованием трехмерной графики — безусловно, стоит начать с рассмотрения возможностей DirectX.

За время своего существования библиотека DirectX претерпела большие изменения. Мы будем рассматривать возможности самой современной (на момент написания этой книги) версии API DirectX — версии 9.0. Помимо различных технологических новшеств, отражающих появление новых устройств, в этой библиотеке впервые представлен managed-интерфейс, позволяющий легко использовать DirectX в программах для платформы .NET Framework.

Начнем со знакомства с составом библиотеки. Итак, DirectX 9.0 состоит из следующих компонентов:

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

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

Пакет DirectX 9.0 SDK

Для успешного запуска космической «стрелялки», использующей возможности Direct3D, вполне достаточно установить пакет DirectX Runtime, который содержит набор динамических библиотек, драйверов устройств и конфигурационных файлов. Этот набор файлов занимает около 36 мегабайт в полном составе, и его часто можно найти на компакт-дисках с драйверами для новых видеокарт или с играми последнего поколения.

Но для самостоятельного создания программ с использованием DirectX 9 его совершенно недостаточно. Для этого необходим пакет DirectX 9.0 SDK, находящийся в свободном доступе на сайте Microsoft.

ПРИМЕЧАНИЕ

DirectX 9.0 SDK занимает свыше 300 мегабайт, скачивать такой объем с сайта довольно проблематично. Однако его можно найти на компакт-диске к RSDN Magazine 3'2002. – прим.ред.

Вот что входит в состав этого пакета:


Рисунок 1. Оболочка для доступа к примерам и пошаговым руководствам DirectX.

Запустив инсталлятор DirectX, можно выбрать, какие компоненты пакета нужно установить на компьютер (рисунок 2). Рекомендуется выбрать полную инсталляцию.


Рисунок 2. Установка DirectX 9 SDK.

Итак, надеемся, что вы успешно выполнили установку пакета. Переходим к практической части: пора научиться подключать DirectX SDK к своим программным проектам.

Первые программы

Начнем с написания двух программ: для Windows и среды .NET. Выполнив пошаговые описания, вы в результате получите полноценные программы, использующие компонент DirectX Graphics для создания трехмерного изображения

Незнакомые технологии, использованные в примере, будут объяснены далее. Главное сейчас — это правильно настроить среду разработки и получить компилируемые примеры программ, на базе которых можно будет дальше изучать разнообразные возможности Direct3D.

Создаем первую программу на C++

К сожалению, мастера AppWizard, поставляемые в составе пакета DirectX 9.0 SDK, предназначены только для использования в средах Visual Studio 6.0 и 7.0. Поэтому мы приводим инструкцию, позволяющую подключить DirectX SDK к пакету Microsoft Visual Studio .NET 2003 (7.1).

Итак, запустите среду Visual Studio и создайте новый проект Win32 Project (рисунок 3). Присвойте ему имя (в нашем примере – DX_Sample1), выберите опцию Empty Project и подтвердите создание проекта.


Рисунок 3. Создание нового проекта на C++.

Теперь необходимо добавить в проект новый файл .cpp. Для этого в меню Project выберите пункт Add New Item… и найдите среди всевозможных элементов значок C++ File (.cpp). Файлу также необходимо присвоить имя (например, Hello.cpp), после чего можно приступать к «начинке».

Настройка среды. Заголовочные файлы и библиотеки

Прежде всего, необходимо убедиться, что среда Visual Studio правильно настроена для компиляции программ, использующих компоненты DirectX 9 SDK. Для этого зайдите в пункт меню Tools, Options… и найдите в левом списке папку Projects. Раскройте ее и выберите пункт VC++ Directories.

Нужно убедиться, что в перечне путей к заголовочным файлам (Include files) присутствует строка с указанием соответствующего каталога DirectX SDK. Если же такой строки нет, необходимо добавить ее самостоятельно. В нашем примере (рисунок 4) это G:\dx9sdk\Include.


Рисунок 4. Настройка путей к заголовочным файлам и библиотекам DirectX.

Точно так же настраивается путь к библиотекам DirectX. Для этого необходимо выбрать из выпадающего списка пункт Library files и добавить в перечень строку c указанием каталога Lib.

Важно, чтобы путь к каталогам dx9sdk/include и dx9sdk/lib стоял в списке раньше аналогичных каталогов (например, от Platform SDK).

Ну что же, теперь компилятор будет знать, в каком месте искать файлы заголовков и библиотек — самое время указать их. Добавьте в файл Hello.cpp следующие строки:

#include <d3d9.h>
#include <d3dx9mesh.h>

#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")

Эти строки означают, что мы будем использовать как библиотеку Direct3D, так и библиотеку вспомогательных компонентов Direct3D Extension (D3DX), в которой содержатся функции, облегчающие программирование графики.

Класс приложения и функция WinMain

Для создания программы применим объектно-ориентированный подход. Создадим класс приложения, в который поместим функции инициализации и очистки DirectX, а также код построения изображения. Назовем этот класс SampleApplication (листинг 1):

Листинг 1. Описание класса приложения [C++]
class SampleApplication
{
  IDirect3D9       *pD3D;
  IDirect3DDevice9 *pDevice;
  ID3DXMesh        *pTeapot;
  HWND             hWnd;

  static LRESULT WINAPI MsgProc(HWND, UINT, WPARAM, LPARAM);
  bool   CreateMainWindow();
  void   CreateTeapot();

public:

  SampleApplication(): 
  pD3D(NULL), pDevice(NULL), pTeapot(NULL), hWnd(NULL){}
  bool InitD3D();
  void CleanupD3D();
  void Render();
  void MessagePump();
};

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

Обратите внимание, что метод MsgProc является статическим. Это необходимо для корректной обработки Windows-сообщений внутри класса. Но внутри статического метода нельзя обращаться к полям класса, так как неизвестно, к какому именно экземпляру идет обращение. Для решения этой проблемы применен простой прием: описан глобальный экземпляр объекта SampleApplication. Все обращения извне будут производиться к полям этого объекта.

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

Итак, добавим объявления глобального экземпляра приложения и функции WinMain (листинг 2):

Листинг 2. Стартовая точка приложения - WinMain [C++].
static SampleApplication g_App;

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
  if(g_App.InitD3D())
  {
    g_App.MessagePump();
    g_App.CleanupD3D();
  }
  return 0;
}

Выполнение WinMain начинается с попытки инициализации Direct3D. В случае успеха программа входит в цикл обработки сообщений, а после его завершения выполняет очистку Direct3D.

Это единственная глобальная функция в программе, все остальные функции будут являться методами класса SampleApplication.

Создание окна

Как будет показано чуть ниже, метод InitD3D сначала создает главное окно приложения при помощи вызова CreateMainWindow. Вот текст этого метода (листинг 3):

Листинг 7.3. Функция создания главного окна [C++]
bool SampleApplication::CreateMainWindow()
{
  // Регистрация класса окна
  WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, 
                  MsgProc, 0L, 0L, 
                  GetModuleHandle(NULL), NULL, NULL, 
                  NULL, NULL,
                  "Direct3D Class", NULL };

  RegisterClassEx( &wc );

  // Создание окна приложения
  hWnd = CreateWindow( "Direct3D Class", 
                     "Direct3D для чайников", 
                     WS_OVERLAPPEDWINDOW, 
                     100, 100, 400, 300,
                     GetDesktopWindow(), NULL, 
                     wc.hInstance, 0 );
  if(!hWnd)
  {
    MessageBox(0, "Ошибка создания главного окна", 
           0, MB_OK|MB_ICONSTOP);
    return false;
  }
  return true;
}

Как и в обычном Windows-приложении, сначала происходит регистрация класса окна, в которой необходимо, в частности, указать процедуру обработки сообщений (вот и пригодился статический метод MsgProc) и имя класса (в нашем примере — «Direct3D Class»). Затем создается главное окно приложения с размерами 400x300 пикселов. В случае ошибки выдается диагностическое сообщение.

Обработка сообщений

Метод MessagePump «прокачивает» поступающие в программу Windows-сообщения. Помимо реакции на необходимые события (например, закрытие окна), это позволяет участвовать в кооперативной многозадачности Windows и снизить нагрузку на процессор. Исходный код метода приведен в листинге 4.

Листинг 4. Цикл сообщений приложения [C++]
void SampleApplication::MessagePump()
{
  MSG msg;
  while(GetMessage(&msg, 0, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
}

Метод MsgProc нашего простейшего приложения обрабатывает только два сообщения: WM_DESTROY (посылаемое окну при его закрытии) и WM_PAINT (сигнализирующее о необходимости перерисовки).

Листинг 5. Оконная функция приложения [C++]
  LRESULT WINAPI SampleApplication::MsgProc(
    HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  g_App.hWnd = hWnd;
  switch( msg )
  {
    case WM_DESTROY:
      PostQuitMessage( 0 );
      return 0;

    case WM_PAINT:
    {
      PAINTSTRUCT ps;
      BeginPaint(hWnd, &ps);
      g_App.Render();
      EndPaint(hWnd, &ps);
      return 0;
    }
  }
  return DefWindowProc(g_App.hWnd, msg, wParam, lParam);
}

К стандартной обработке сообщения WM_PAINT мы добавили только вызов метода Render для построения трехмерного изображения.

Но, прежде чем использовать библиотеку Direct3D, необходимо ее инициализировать. Рассмотрим соответствующие методы класса SampleApplication.

Инициализация и очистка библиотеки

Код метода InitD3D сначала создает главное окно приложения вызовом CreateMainWindow. В случае успеха программа приступает к инициализации библиотеки DirectX.

Для начала пытаемся создать объект Direct3D — стартовую точку для удобной инициализации компонентов Direct3D. Для этого вызовем функцию Direct3DCreate9 с константой D3D_SDK_VERSION в качестве параметра. Если этот вызов завершится неудачей, на компьютере отсутствует поддержка DirectX нужной нам версии, и дальше продолжать бессмысленно.

Если же функция вернула созданный объект Direct3D, то используем его для создания и инициализации графического устройства, вызвав у него метод CreateDevice с необходимыми параметрами (мы вернемся к рассмотрению инициализации ниже).

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

И, наконец, после успешной инициализации объектов DirectX, показываем главное окно. Теперь приложение сможет корректно перерисовывать его содержимое, используя Direct3D.

Текст функции инициализации приведен в листинге 6:

Листинг 6. Функция инициализации DirectX [C++]
bool SampleApplication::InitD3D()
{
  if(!CreateMainWindow())
    return false;

  pD3D = Direct3DCreate9(D3D_SDK_VERSION);

  if(!pD3D)
    return false;

  D3DPRESENT_PARAMETERS params; 
  ZeroMemory( &params, sizeof(params) );
  params.Windowed = TRUE;
  params.SwapEffect = D3DSWAPEFFECT_DISCARD;
  params.BackBufferFormat = D3DFMT_UNKNOWN;

  HRESULT hr = pD3D->CreateDevice(
    D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
    D3DCREATE_SOFTWARE_VERTEXPROCESSING,
    &params, &pDevice);
  if(FAILED(hr))
  {
    pD3D->Release();
    pD3D = 0;
    return false;
  }

  D3DXCreateTeapot(pDevice, &pTeapot, 0);

  ShowWindow(hWnd, SW_SHOWDEFAULT);
  UpdateWindow(hWnd);

  return true;
}

После закрытия окна функция вызывает метод приложения для корректной очистки всех выделенных ресурсов Direct3D. Как будет показано в следующем разделе, объекты DirectX подчиняются правилам COM, и для их освобождения также необходимо использовать метод Release (листинг 7):

Листинг 7. Метод CleanupD3D [C++]
void SampleApplication::CleanupD3D()
{
  if(pTeapot)
    pTeapot->Release();
  if(pDevice)
    pDevice->Release();
  if(pD3D)
    pD3D->Release();
}

Как уже говорилось, для отладки сложных приложений DirectX бывает полезно установить Debug-версию библиотеки, входящую в состав DirectX 9.0 SDK. Она не только позволяет контролировать правильность параметров методов, но и выдает диагностическое сообщение при завершении программы, если вы забудете удалить какой-нибудь объект.

Построение изображения

Для создания изображения обработчик сообщения WM_PAINT вызывает метод Render. Настало время познакомиться и с ним.

Сначала заполним область вывода синим цветом, вызвав метод Clear устройства. Затем приступим к подготовке изображения.

Все команды вывода графических объектов должны обрамляться операторными скобками BeginScene/EndScene для корректной отработки конвейера вывода.

Первоначально для сцены задается источник света и характеристики материала, из которого изготовлен чайник. Затем к устройству применяется матрица мирового преобразования (чайник слегка уменьшается и отодвигается по оси Z). Наконец, выводим долгожданное изображение. Объекты Mesh библиотеки D3DX сами умеют отображать себя в устройство Direct3D, достаточно вызвать метод DrawSubset. На самом деле, как мы убедимся позже, они состоят из множества мелких трехмерных примитивов (как правило, треугольников), но группировка примитивов в объект сильно облегчит нам задачу.

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

Полностью текст метода Render приведен в листинге 8.

Листинг 8. Метод Render [C++]
void SampleApplication::Render()
{
  if(pDevice)
  {
    // Очистим область вывода темно-синим цветом
    pDevice->Clear( 0, NULL, 
      D3DCLEAR_TARGET, 
      D3DCOLOR_XRGB(0,0,128), 0.0f, 0 );

    // Рисуем сцену
    if( SUCCEEDED( pDevice->BeginScene() ) )
    {
      // создаем источник света
      D3DLIGHT9 light=
        { D3DLIGHT_DIRECTIONAL, // бесконечно удаленный источник
          {1,1,0,0},            // диффузное освещение: желтый цвет
          {0,0,0,0},            
          {0.1f,0.1f,0.1f,1},   // рассеянный свет: тусклый белый 
          {0,0,0},              
          {7,-2,1}              // вектор направления
        } ;

      pDevice->SetLight( 1, &light ) ;
      pDevice->LightEnable( 1, TRUE ) ;
      
      // создаем материал
      D3DMATERIAL9 material = { 
          {1,1,1,1}  
        , {1,1,1,1}  
        , {1,1,1,1}  
        , {0,0,0,0}  
        , 1          
        } ;
      pDevice->SetMaterial( &material ) ;

      float scale = 0.4f;
      D3DMATRIX transform = { 
        scale,      0.0f,        0.0f,        0.0f,
        0.0f,       scale,       0.0f,        0.0f,
        0.0f,       0.0f,        scale,       0.0f,
        0.0f,       0.0f,        1.0f,        1.0f
      };

      pDevice->SetTransform(D3DTS_WORLD, &transform);

      pTeapot->DrawSubset(0);
      pDevice->EndScene();
    }

    // Выводим содержимое вторичного буфера
    pDevice->Present( NULL, NULL, NULL, NULL );
  }
}

Итак, все необходимые функции реализованы. Откомпилируйте полученную программу. Если не возникло ни одной ошибки (неужели с первого раза?), запустите ее на выполнение. Вот оно, творение наших рук (рисунок 5).


Рисунок 5. Внешний вид окна первой программы.

Мы обязательно улучшим этот пример, но сейчас давайте поближе познакомимся с основой основ библиотеки DirectX — моделью COM.

Обзор модели COM

В данном разделе мы кратко рассмотрим особенности компонентной модели COM, на которой построена реализация DirectX для платформы Windows. Знакомые с COM программисты свободно могут пропустить этот раздел без ущерба для понимания.

Программирование в терминах классов и объектов давно уже стало привычным. Язык C++ приобрел огромную популярность благодаря поддержке объектно-ориентированного программирования. За время существования C++ для него было создано большое количество библиотек классов.

Однако современный мир богат разнообразными технологиями, и совместимости на уровне исходных текстов уже недостаточно. Что, если вам понадобится использовать объект, созданный на другой версии компилятора или вообще на другом языке? Один из вариантов решения уже давно существует в операционной системе Windows.

Библиотеки динамической компоновки (DLL) позволяют создать программный код, доступный для загрузки и выполнения. Экспортируемые из DLL функции становятся доступными в вызывающей программе, как если бы они были в ней реализованы. Скрестив две эти идеи, мы и получим реализацию компонентного программирования.

Итак, компонентное программирование — это логическое развитие идей объектно-ориентированного программирования, которое позволяет решить проблемы повторного использования кода из различных программных сред и снизить затраты на сопровождение различных версий программного кода. Это достигается за счет отделения интерфейсов объектов от их реализации.

Интерфейс и реализация

Интерфейсом объекта называют описание его функциональности (методов, свойств и т.д.) на одном из языков высокого уровня. В компонентных средах (таких, как COM) запрещается обращаться к объекту иначе, как через интерфейсы. Зачем нужны такие строгие ограничения? Давайте посмотрим.

Если к какому-то программному объекту можно обратиться посредством определенного интерфейса, то говорят, что объект реализует (поддерживает) этот интерфейс. Объект может поддерживать одновременно несколько интерфейсов.

На языке С++ интерфейс обычно описывается как класс, содержащий только набор абстрактных виртуальных функций. К примеру, в листинге 9 описывается интерфейс для работы с домашними животными (Pets), содержащий два метода.

Листинг 9. Условное описание интерфейса животного [C++]
// интерфейсы принято именовать с заглавной буквы ‘I’
interface IPet // животное
{
  int  GetNumberOfLegs() = 0; // абстрактный метод - число ног
  void Speak() = 0;           // абстрактный метод - издать звук
};

Для читателей, незнакомых с описанием интерфейсов на С++, поясним, что interface – это не новое ключевое слово, а всего-навсего созданное для наглядности макроопределение (#define) привычного ключевого слова struct:

// где-то в системных заголовочных файлах...
#define interface struct

Почему struct, а не class – спросите вы? Очень просто: в структурах все члены по умолчанию имеют общий доступ, поэтому при описании интерфейса не придется специально выписывать модификатор доступа public. Отметим, что интерфейс всегда проектируется для доступа целиком: Вы либо можете получить доступ ко всему интерфейсу, либо нет.

Обратите внимание: явно указано, что все методы структуры (или интерфейса) IPet являются абстрактными (pure virtual). Это означает, что нельзя создать непосредственно экземпляр IPet. Вместо этого можно создать класс, унаследованный от IPet, в котором реализовать все абстрактные методы. Говорят, что такой объект называется реализацией интерфейса. В листинге 10 приведен пример такой реализации.

Листинг 10. Реализация интерфейса IPet для объекта "собака" [C++]
class CDog : public IPet
{
  int  GetNumberOfLegs();
  void Speak();
};

int CDog::GetNumberOfLegs()
{
  return 4;
}

void CDog::Speak()
{
  MessageBeep(-1); // издадим системный звук Windows
}

Теперь представим себе, как мог бы использоваться придуманный нами интерфейс в программе на C++. Допустим, мы собираемся создать объект "собака" и поуправлять им через наш интерфейс IPet. Вот очередной кусок кода, очень похожего на реальное использование COM-объектов (в том числе, компонентов DirectX):

Листинг 11. Пример использования интерфейса IPet [C++]
IPet *pDog = CreatePet("Dog");

// узнаем, является ли наш друг четвероногим
int nLegs = pDog->GetNumberOfLegs();

// если лап не 4, заставим собачку залаять
if (nLegs != 4)
  pDog->Speak();

Как видим, код использования интерфейса очень похож на обычную работу с объектами C++. Основное отличие заключается в том, что наша программа не использует объект CDog непосредственно (она о нем даже не знает), а выполняет всю работу через указатель на интерфейс IPet. Поэтому же в нашем примере содержится вызов гипотетической функции CreatePet, которая знала бы о деталях реализации интерфейса и умела создавать экземпляр объекта "собака".

Такие ограничения слегка усложняют работу с компонентами, но придают нашей программе огромную гибкость. Теперь мы можем создавать и использовать готовые компоненты не только на языке C++. Вообще говоря, исследовав представление в памяти, которое образует интерфейс, мы можем воссоздать это бинарное представление другими средствами (например, на языке C, в котором вообще нет классов и виртуальных функций).

Поддержка COM встроена и в некоторые другие программные среды, например, в Delphi и Visual Basic. Это позволяет создавать двоичные версии компонентов: скомпилировав DLL, содержащую код компонента на одном языке, вызвать и использовать этот объект на другом языке программирования.

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

Для создания COM-компонентов обычно применяются функции CoCreateInstance и CoGetClassObject. Эти функции предоставляет библиотека COM, поэтому перед их вызовом требуется ее инициализация.

Некоторые объекты DirectX также можно создавать таким образом, но обычно используется более простой метод. Библиотека предоставляет специальные функции для создания необходимых программных компонентов. Например, для создания объекта Direct3D версии 9 существует функция Direct3DCreate9, описанная в заголовочном файле d3d9.h. Именно этот способ был применен в нашем примере.

Интерфейс IUnknown

Приведенный в предыдущем разделе интерфейс IPet не является интерфейсом в стиле COM. Чтобы удовлетворять спецификации COM, все интерфейсы, включая интерфейсы DirectX, должны быть унаследованы от специального интерфейса IUnknown. Несмотря на свое название ("unknown" в переводе с английского означает "неизвестный"), это — интерфейс, о котором знают все. В листинге 12 приведено немного упрощенное описание интерфейса IUnknown для языка С++.

Листинг 12. Интерфейс IUnknown [C++]
interface IUnknown
{
  virtual HRESULT QueryInterface( const IID& iid, void** ppv ) = 0;
  virtual ULONG   AddRef() = 0;
  virtual ULONG   Release() = 0;
};

Рассмотрим назначение методов IUnknown:

QueryInterface

О произвольном COM-объекте обычно можно сказать только то, что он поддерживает интерфейс IUnknown. Назначение QueryInterface — определить, поддерживает ли данный объект любой другой интерфейс. Помните пример с интерфейсом IPet? Если бы он был создан с учетом спецификации COM, то у произвольного объекта COM можно было бы запросить, является ли он животным:

IUnknown *pUnk = ... // каким-то образом получаем указатель на объект
IPet *pPet = 0;
if(SUCCEEDED(pUnk->QueryInterface(IID_IPet, (void**)&pPet))
{
  ... // да, этот объект является животным
}

Поясним два момента из этого примера.

Обычно функции и методы объектов COM возвращают специальное значение типа HRESULT, для индикации успешности или неудачи вызова. Макросы SUCCEEDED и FAILED являются рекомендованным способом проверки такого результата.

Чтобы отличать один интерфейс от другого (и запрашивать их поддержку), недостаточно просто строки с именем. Имена вроде «IPet» довольно распространены, и существует вероятность одновременного использования таких имен двумя различными разработчиками. Во избежание таких конфликтов создателями COM было принято решение использовать для идентификации интерфейсов и объектов специальный 128-битный идентификатор – GUID, который является статистически уникальным. При создании нового интерфейса разработчик просто создает новый GUID и описывает его в заголовочном файле или библиотеке типов объекта. В нашем примере предполагалось, что такой GUID описан в заголовочном файле интерфейса под именем IID_IPet.

AddRef, Release

Обычный экземпляр класса C++ создается в программе вызовом конструктора, а удаляется при выходе из области видимости или вызовом оператора delete, если объект был создан в динамической памяти. Для COM типична ситуация, когда объект реализует сразу несколько интерфейсов, и указатели на эти интерфейсы сохраняются в клиентах.

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

Назначение методов AddRef и Release — управление временем жизни объекта. Получив в свое пользование интерфейс объекта, необходимо вызвать AddRef. Это позволит гарантировать, что объект не будет разрушен, пока используется хотя бы один его интерфейс.

Завершая работу с объектом, необходимо сообщить ему об этом. Для каждого полученного от него интерфейса необходимо вызвать метод Release.

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

Возвращаемое значение методов AddRef и Release чаще всего содержит текущее значение внутреннего счетчика ссылок объекта, но его рекомендуется игнорировать, так как на это значение нет четкой спецификации. Тем не менее, одному из авторов доводилось видеть вот такой сомнительный код "гарантированного удаления" объекта DirectX:

while(p->Release()); // (C) 1999, 2000 NVIDIA Corporation

Как видите, большие корпорации иногда могут позволить себе игнорировать рекомендации спецификаций COM.

ПРИМЕЧАНИЕ

На самом деле, этот код вызывался непосредственно перед аварийным завершением программы, что несколько оправдывает разработчиков в стремлении удалить объект во что бы то ни стало. Тем не менее, остается вероятность получить в таком коде бесконечный цикл.

Надеемся, что этого краткого знакомства с моделью COM будет достаточно для того, чтобы разобраться в приводимых примерах. Если вы хотите узнать о ней больше, рекомендуем прочесть для начала книгу Дейла Роджерсона «Основы COM».

Managed DirectX. Программирование для .NET

У .NET-программистов теперь тоже есть возможность легко создавать Direct3D-приложения. Начиная с девятой версии, в состав DirectX SDK включен так называемый Managed DirectX — программный интерфейс к библиотеке DirectX для платформы .NET. Он включает в себя .NET-сборки с реализацией компонентов DirectX, программные примеры и документацию.

Для создания программ с использованием Managed DirectX необходимо, чтобы помимо среды исполнения DirectX на компьютере была установлена последняя версия .NET Framework (на данный момент – 1.1), а также набор сборок Managed DirectX. Вот перечень этих файлов:

Мы создадим тестовый проект с использованием среды Visual Studio 2003, хотя простое DirectX-приложение вполне можно создать и без этой среды разработки: как и в случае с GDI+, достаточно текстового редактора и компилятора командной строки csc.exe.

Итак, создайте новый проект (рисунок 6). Из списка Project Types выберите пункт Visual C# Projects, а из появившегося перечня проектов — пункт Windows Application. Введите имя нового проекта, выберите каталог для сохранения его файлов и нажмите OK.


Рисунок 6. Создание нового приложения для платформы .NET.

У нас появится окно дизайнера формы – главного окна будущего приложения. Мы не будем использовать дизайнер, а введем исходный код всей программы в редакторе. Поэтому нажмите на дизайнере правую кнопку мыши и выберите из появившегося контекстного меню пункт View Code.

Удалите весь текст из редактора и введите вместо него код из листинга 13.

Листинг 13. Исходный код программы с использованием Managed DirectX [C#]
using System;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

class TestForm: Form
{
  static void Main()
  {
    TestForm form = new TestForm();
    form.Text = "Direct3D для .NET";
    form.Width = 400;
    form.Height = 300;
    form.InitD3D();
    form.Show();
    while(form.Created)
    {
      form.Render();
      Application.DoEvents();
    }
  }

  Device device;
  Mesh teapot;

  public void InitD3D()
  {
    PresentParameters parameters = new PresentParameters();
    parameters.Windowed = true;
    parameters.SwapEffect = SwapEffect.Discard;

    device = new Device(0, DeviceType.Hardware, this,
      CreateFlags.SoftwareVertexProcessing, parameters);
    teapot = Mesh.Teapot(device);
  }

  public void Render()
  {
    device.Clear(ClearFlags.Target, 
      System.Drawing.Color.Blue, 1.0f, 0);
    device.BeginScene();
    try
    {
      device.RenderState.Lighting = true;
      device.Lights[0].Type = LightType.Directional;
      device.Lights[0].Direction = new Vector3(7, -2, 1);
      device.Lights[0].Diffuse = System.Drawing.Color.Yellow;
      device.Lights[0].Commit();
      device.Lights[0].Enabled = true;

      Material material = new Material();
      material.Ambient = System.Drawing.Color.White;
      material.Diffuse = System.Drawing.Color.White;
      material.Specular = System.Drawing.Color.White;
      device.Material = material;

      Matrix matrix = new Matrix();
      matrix.Scale(0.4f, 0.4f, 0.4f);
      matrix.M43 = 1; // перенос по оси Z
      device.Transform.World = matrix;

      teapot.DrawSubset(0);
    }
    finally
    {
      device.EndScene();
    }
    device.Present();
  }
} 

Программа выполняет построение той же сцены, что и ее эквивалент на C++, но текст ее заметно короче. Это связано с тем, что нам не пришлось возиться с низкоуровневыми деталями: регистрацией класса окна и обработкой сообщений. Заметим также, что, хотя в .NET поддерживается концепция интерфейсов, при создании Managed API для интерфейсов DirectX было решено упаковать их в классы- «обертки», что заметно упрощает их использование.

Для успешной компиляции полученной программы необходимо добавить в проект ссылки на следующие сборки Managed DirectX:

Для этого в меню Project выберите пункт Add Reference… и в появившемся списке сборок поочередно выберите двойным щелчком мыши все три сборки (см. рисунок 7)

По умолчанию при установке Managed DirectX на компьютер эти файлы помещаются в каталог <Windows>\Microsoft.NET\Managed DirectX\v4.09.00.0900\. Если по какой-то причине инсталлятор не поместил их в глобальный кэш сборок (такое случается), то вы сможете найти их там, чтобы самостоятельно добавить ссылки на эти сборки в свой проект.


Рисунок 7. Добавление сборок Direct3D в проект на C#.

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

Можно приступать к более детальному знакомству с Direct3D. Начнем с инициализации.

Начало работы. Инициализация Direct3D

Любая библиотека содержит, как правило, структуры данных, нуждающиеся в правильной инициализации, и DirectX — не исключение.

В этом разделе мы расскажем об особенностях инициализации компонента Direct3D, вопросах выбора доступных графических устройств и установке видеорежимов.

Инициализация и перечисление графических устройств средствами C++

Как уже говорилось, в версии для C++ надлежащая подготовка библиотеки для работы выполняется созданием объекта Direct3D. Этот объект реализует COM-интерфейс IDirect3D9, основное назначение которого — проведение дальнейшей инициализации и настройка режимов графического устройства.

Для создания объекта Direct3D в нашем примере использовался код следующего вида:

IDirect3D9 *pD3D;
...
pD3D = Direct3DCreate9(D3D_SDK_VERSION);

Здесь D3D_SDK_VERSION — константа, определенная в заголовочном файле d3d9.h (вообще-то, в составе DirectX 9.0 SDK имеется и старый заголовочный файл d3d8.h, в котором эта константа имеет другое значение, так что будьте осторожны). Ее назначение – убедиться в том, что программа скомпилирована и собрана с одной и той же версией заголовочных файлов и библиотек.

В результате выполнения мы получаем указатель на интерфейс IDirect3D9, используя который можно проводить дальнейшие действия по настройке графического оборудования.

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

Для получения количества доступных в системе видеоадаптеров используется метод IDirect3D9::GetAdapterCount(). Дальнейший перебор доступных адаптеров можно осуществлять, последовательно вызывая метод IDirect3D9::GetAdapterIdentifier с увеличивающимся порядковым номером Adapter:

HRESULT GetAdapterIdentifier(UINT Adapter,
  DWORD Flags,
  D3DADAPTER_IDENTIFIER9 *pIdentifier );

В качестве значения Flags нужно передать 0 (для обычного перебора) или константу D3DENUM_WHQL_LEVEL для перебора только тех адаптеров, драйверы которых имеют сертификацию Microsoft Windows Hardware Quality Labs (WHQL). В последнем случае не удивляйтесь, если компьютер начнет «стучаться» в Интернет для загрузки новейших сертификатов.

Последний параметр – указатель на структуру D3DADAPTER_IDENTIFIER9, в которую заносится информация о выбранном видеоадаптере. Вот как, используя эту информацию, вывести в консоль данные обо всех установленных видеоадаптерах (листинг 14.):

Листинг 14. Перечисление установленных видеоадаптеров [C++]
#include <stdio.h>
#include <d3d9.h>

#pragma comment(lib, "d3d9.lib")

int main()
{
  IDirect3D9 *pD3D = Direct3DCreate9(D3D_SDK_VERSION);
  if(!pD3D)
  {
    printf("Direct3DCreate9 failed!\n");
    return 1;
  }
  int nCount = pD3D->GetAdapterCount();
  for(int i=0; i<nCount; i++)
  {
    D3DADAPTER_IDENTIFIER9 info;
    pD3D->GetAdapterIdentifier(i, 0, &info);
    printf("Adapter %d: \n"
           "\tDriver: %s\n"
           "\tDescription: %s\n"
           "\tDeviceName: %s\n",
           i, info.Driver, info.Description,
           info.DeviceName);
  }
  return 0;
}

Пример вывода этой программы на компьютере с одной видеокартой:

Adapter 0:
        Driver: ati2dvag.dll
        Description: RADEON 9600 SERIES
        DeviceName: \\.\DISPLAY1

А вот результат выполнения на машине с тремя видеокартами:

Adapter 0:
        Driver: nv4_disp.dll
        Description: NVIDIA GeForce DDR
        DeviceName: \\.\DISPLAY1
Adapter 1:
        Driver: perm2dll.dll
        Description: Appian Graphics Jeronimo Pro
        DeviceName: \\.\DISPLAY2
Adapter 2:
        Driver: perm2dll.dll
        Description: Appian Graphics Jeronimo Pro
        DeviceName: \\.\DISPLAY3

Похожим образом решается и другая задача: перечислить список доступных для данного адаптера видеорежимов. Для перечисления режимов интерфейс IDirect3D9 предоставляет методы GetAdapterModeCount и EnumAdapterModes.

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

Перечисление устройств и видеорежимов для Managed Direct3D

Версия Direct3D для .NET не требует явной инициализации библиотеки. Функция Direct3DCreate9 вызывается автоматически в статическом конструкторе класса Microsoft.DirectX.Direct3D.Manager.

С помощью этого класса также легко получить информацию о списке доступных графических устройств. Его свойство Adapters возвращает список структур AdapterInformation, содержащих все нужные данные. В листинге 15 приведен пример программы на C#, также выводящей список установленных адаптеров.

using System;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

class TestForm: Form
{
  static void Main()
  {
    foreach(AdapterInformation adapterInfo in Manager.Adapters)
      Console.WriteLine("Adapter: \n\tDriver: {0}\n\t"+
        "Description: {1}\n\tDeviceName: {2}\n",
        adapterInfo.Information.DriverName,
        adapterInfo.Information.Description,
        adapterInfo.Information.DeviceName);
  }
}

Задача получения списка доступных для каждого адаптера видеорежимов также решается очень просто: в структуре AdapterInformation существует свойство SupportedDisplayModes, предоставляющее список всех доступных режимов.

Как уже говорилось, этот список может содержать сотни комбинаций, поэтому разумно отбирать из него только режимы с подходящим цветовым форматом. Поэтому свойство SupportedDisplayModes поддерживает индексирование по коду формата, описанному в классе Microsoft.DirectX.Direct3D.Format – просто укажите код формата в квадратных скобках после имени свойства.

Вот пример вывода всех доступных для первого видеоадаптера комбинаций видеорежимов с глубиной цвета High Color 5-6-5 (по 5 бит для представления красного и синего цвета и 6 бит для передачи зеленого):

foreach(DisplayMode mode in
  Manager.Adapters[0].
  SupportedDisplayModes[Format.R5G6B5])
    Console.WriteLine("{0}x{1}, {2} Hz, Format: {3}", 
      mode.Width, mode.Height, mode.RefreshRate, mode.Format);

Вы, возможно, заметили, что нам опять помогает особенность .NET Framework: все константы перечислений, как, например, названия различных цветовых форматов, возвращают «читаемые» имена при их выводе.

Инициализация графического режима для C++

Для вывода изображений в Direct3D используется объект Device (устройство), который отвечает за взаимодействие с аппаратной частью видеоадаптера. При создании этого объекта необходимо указать ряд параметров, которые мы сейчас и рассмотрим.

Вспомним фрагменты кода создания объекта Direct3D Device в своей первой программе:

HRESULT hr = pD3D->CreateDevice(
  D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
  &params, &pDevice);

Первый параметр метода IDirect3D9::CreateDevice определяет адаптер, для которого создается объект Device. Можно указать либо порядковый номер, полученный перечислением (как в листинге 14), так и константу D3DADAPTER_DEFAULT, при задании которой будет использован адаптер, выбранный пользователем по умолчанию.

Следующий параметр определяет тип устройства и может принимать одно из трех значений: D3DDEVTYPE_HAL, D3DDEVTYPE_REF или D3DDEVTYPE_SW. Второй и третий режимы используют программную эмуляцию всех операций Direct3D и могут иметь смысл только для отладки, так как выполняются очень медленно.

Параметр hWnd необходим для указания дескриптора окна Windows, к которому будет привязано создание устройства.

Следующий параметр впервые появился в DirectX 8.0 благодаря реализации в видеокартах последнего поколения аппаратной поддержки T&L (Transform and Lighting, Трансформация и освещение). Мы указали значение D3DCREATE_SOFTWARE_VERTEXPROCESSING, чтобы использовать программную обработку вершин примитивов. Для включения аппаратной поддержки T&L используйте константу D3DCREATE_HARDWARE_VERTEXPROCESSING. Это снимет большую нагрузку с центрального процессора по обсчету вершин, но ограничит число видеокарт, поддерживающих программу.

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

  D3DPRESENT_PARAMETERS params; 
  ZeroMemory( &params, sizeof(params) );
  params.Windowed = TRUE;
  params.SwapEffect = D3DSWAPEFFECT_DISCARD;
  params.BackBufferFormat = D3DFMT_UNKNOWN;

Вот еще одна польза установки отладочной версии DirectX Runtime: указание значения D3DSWAPEFFECT_DISCARD в поле SwapEffect помогает при отладке приложений Direct3D. При этом каждый выводимый кадр перед построением сцены заполняется случайным «шумом», и вы легко заметите ситуацию, при которой выводимое изображение не заполняет всю область кадра.

Инициализация полноэкранного видеорежима требует дополнительного указания, как минимум, еще четырех полей: BackBufferWidth, BackBufferHeight (разрешение экрана), BackBufferFormat (цветовой формат) и FullScreenRefreshRateInHz (частота обновления экрана). В листинге 16 приведен пример инициализации видеорежима 1024x768, TrueColor, с частотой кадров по умолчанию:

Листинг 16. Инициализация полноэкранного видеорежима [C++]
D3DPRESENT_PARAMETERS params; 
ZeroMemory( &params, sizeof(params) );
params.Windowed = FALSE;
params.SwapEffect = D3DSWAPEFFECT_DISCARD;
params.BackBufferWidth = 1024;
params.BackBufferHeight = 768;
params.BackBufferFormat = D3DFMT_A8R8G8B8;
params.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;

HRESULT hr = pD3D->CreateDevice(
  D3DADAPTER_DEFAULT,
  D3DDEVTYPE_HAL, hWnd, 
  D3DCREATE_SOFTWARE_VERTEXPROCESSING, 
  &params, &pDevice);

Если видеокарта не поддерживает указанный цветовой формат, вызов CreateDevice вернет ошибку. В противном случае переменная pDevice будет содержать указатель на созданное устройство IDirect3DDevice9.

Инициализация графического режима: версия для C#

Установка видеорежима в реализации Managed DirectX очень похожа на версию для C++, с одним существенным отличием: объект Device создается вызовом конструктора, что выглядит более естественно. Вот описание этого конструктора:

public Device(
  int adapter,
  DeviceType deviceType,
  Control renderWindow,
  CreateFlags behaviorFlags,
  PresentParameters presentationParameters
);

Смысл параметров остался тем же, что и в версии для C++. Значения констант содержатся в классах-перечислениях DeviceType, CreateFlags и т.д., что не требует их запоминания. Выше уже был приведен код для инициализации оконного устройства, а в листинге 17 содержится пример установки полноэкранного видеорежима (с теми же характеристиками, что и в примере на C++)

Листинг 17. Инициализация полноэкранного видеорежима [C#]
PresentParameters parameters = new PresentParameters();
parameters.BackBufferWidth = 1024;
parameters.BackBufferHeight = 768;
parameters.BackBufferFormat = Format.A8R8G8B8;
parameters.FullScreenRefreshRateInHz = 
  PresentParameters.DefaultPresentRate;
parameters.Windowed = false;
parameters.SwapEffect = SwapEffect.Discard;

device = new Device(0, DeviceType.Hardware, this,
  CreateFlags.SoftwareVertexProcessing, parameters);

Указание неправильных или неподдерживаемых параметров видеорежима приведет к генерации исключения Microsoft.DirectX.Direct3D.InvalidCallException.

Удаление невидимых деталей

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

Но настало время двигаться дальше, и сейчас идиллия будет разрушена. Не пугайтесь, просто пора познакомиться с реализацией алгоритмов удаления невидимых деталей в Direct3D.

Пирамида видимости

Достаточно слегка изменить параметры сцены, как с чайником начнет твориться что-то неладное. Найдите в программе на C++ следующий фрагмент:

D3DMATRIX transform = { 
        scale,      0.0f,        0.0f,        0.0f,
        0.0f,       scale,       0.0f,        0.0f,
        0.0f,       0.0f,        scale,       0.0f,
        0.0f,       0.0f,        1.0f,        1.0f
      };

И замените его новым (обратите внимание на выделенный текст):

D3DMATRIX transform = { 
        scale,      0.0f,        0.0f,        0.0f,
        0.0f,       scale,       0.0f,        0.0f,
        0.0f,       0.0f,        scale,       0.0f,
        0.0f,       0.0f,        0.3f,        1.0f
      };

Аналогичный участок в программе на C# выглядит так:

matrix.M43 = 1; // перенос по оси Z

Также замените только константу в строке:

matrix.M43 = 0.3f; // перенос по оси Z

В обеих программах изменение коснулось только одного элемента матрицы преобразования, элемента с индексом (4, 3).

Мы еще будем подробно рассматривать геометрические преобразования в пространстве Direct3D, сейчас достаточно сказать, что этот элемент отвечает за перемещение объекта от центра координат по оси Z. То есть мы отодвинули чайник от камеры не на 1 единицу расстояния, как в первом случае, а на 0.3. Результат такого изменения показан на рисунке 8.


Рисунок 8. Объект отрезан со стороны наблюдателя

Не менее впечатляющий результат получится, если изменить тот же элемент матрицы со значения 1.0 до всего 1.05 (рисунок 9).


Рисунок 9. Объект отрезан с противоположной стороны

Как видно из рисунков, все детали объекта, выходящие за определенные пределы, аккуратно «отрезаются». Если вы проведете эксперименты с элементами матрицы (4, 1) и (4, 2), отвечающими за перемещение объекта по двум другим координатным осям, то заметите, что вывод ограничен и в этих координатах. Это наглядная демонстрация работы механизма отсечения в Direct3D. Зачем нужны такие ограничения?

Если провести четыре воображаемых луча от точки наблюдения к краям экрана, то получится пирамида с прямоугольником (экраном) в основании. Продолжая лучи дальше, вглубь виртуального мира, который «спроецирован» на наш экран, мы получим пирамиду видимости (viewing frustum). Все, что находится за пределами этой пирамиды, все равно будет вне поля проекции — так зачем рисовать эти детали, понапрасну тратя время?

Если поразмыслить, то не требуется рисовать и очень удаленные объекты, нужно уметь вовремя остановиться. Все равно на пути взгляда рано или поздно встретится препятствие. Да и человеческий глаз не увидит чайник, удаленный на расстояние в, скажем, десятки километров. То же самое касается, например, предметов, «находящихся» в виртуальном мире позади нас или перед экраном. Поэтому в DirectX и реализовано отсечение объектов по усеченной пирамиде, образованной передней, задней и четырьмя боковыми отсекающими плоскостями (рисунок. 10).


Рисунок 10. Проекция изображения и пирамида видимости.

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

Чтобы отказаться от отсечения в программе на C++, перед выводом объекта добавьте строчку:

pDevice->SetRenderState(D3DRS_CLIPPING, FALSE);

Эквивалентная строчка на C# выглядит так:

device.RenderState.Clipping = false;

Можете проверить: перемещения объекта по сцене больше не будут отражаться на его целостности.

Буфер глубины (Z-buffer)

Чтобы проиллюстрировать очередную проблему, возникающую при изображении сложных объектов (или групп объектов), добавим в сцену динамичность.

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

case WM_PAINT:
{
  PAINTSTRUCT ps;
  BeginPaint(hWnd, &ps);
  g_App.Render();
  EndPaint(hWnd, &ps);
  InvalidateRect(hWnd, 0, TRUE);
  return 0;
}

Для реализации вращения объекта воспользуемся библиотекой утилит D3DX. Нам поможет функция D3DXMatrixRotationY, которая создает матрицу поворота на требуемый угол. Для ее использования необходимо создать объект D3DXMATRIX и передать угол поворота в радианах.

Класс D3DXMATRIX, также предоставленный библиотекой D3DX, является просто оберткой над уже знакомой структурой D3DMATRIX. В этом классе реализован ряд полезных математических операций, а также операции сравнения матриц.

Для формирования окончательной матрицы умножим полученную матрицу поворота на исходную матрицу transform, существовавшую в первоначальной версии программы. Исправленный фрагмент метода Render представлен в листинге 18.

Листинг 18. Реализация вращения объекта в методе Render [C++]
float scale = 0.4f;
D3DXMATRIX transform( 
  scale,      0.0f,        0.0f,        0.0f,
  0.0f,       scale,       0.0f,        0.0f,
  0.0f,       0.0f,        scale,       0.0f,
  0.0f,       0.0f,        1.0f,        1.0f
);
D3DXMATRIX rotation;
D3DXMatrixRotationY(&rotation, GetTickCount()/1000.0f);
rotation *= transform;
pDevice->SetTransform(D3DTS_WORLD, &rotation);
pDevice->SetRenderState(D3DRS_CLIPPING, FALSE);
pTeapot->DrawSubset(0);
pDevice->EndScene();

Как видно из листинга, для получения монотонно возрастающего с каждым кадром угла поворота мы воспользовались функцией GetTickCount, возвращающей число миллисекунд с момента старта программы. Мы делим это число на 1000, таким образом, наш чайник вращается строго со скоростью 1 радиан в секунду (рисунок 11).


Рисунок 11. Изображение рисуется неправильно (носик «просвечивает» сквозь чайник)

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

Дело в том, что наш объект состоит из треугольных примитивов, а порядок их рисования не определен. Если треугольники, из которых состоит носик чайника, будут рисоваться после изображения корпуса, это неизбежно затрет уже созданное изображение.

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

При включении этого режима вывод примитива приводит к заполнению не только буфера кадра, но и Z-буфера: в него записываются значения «глубины» (степени удаленности) соответствующих точек. Точка выводится только при условии, что она ближе текущего значения в Z-буфере, при этом Z-буфер заполняется новым значением.

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

params.EnableAutoDepthStencil = TRUE;
params.AutoDepthStencilFormat = D3DFMT_D16;

Первое поле разрешает использование Z-буфера, второе определяет его формат (в данном случае, для каждого пиксела будет храниться 16-битовое значение глубины).

Перед выводом каждого кадра на экран необходимо очищать буфер глубины, иначе старые значения в нем будут мешать выводу новых кадров. В этом нам поможет уже знакомый метод Clear с использованием флага D3DCLEAR_ZBUFFER. В качестве третьего параметра нужно передать в этот метод значение 1.0 (максимальная глубина). Кроме того, необходимо включить проверку Z test и разрешить запись в буфер глубины. Вот как будет выглядеть начало метода Render:

pDevice->Clear( 0, NULL, 
  D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 
  D3DCOLOR_XRGB(0,0,128), 1.0f, 0 );
pDevice->SetRenderState(D3DRS_ZENABLE, TRUE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

После компиляции программы перекрывающиеся элементы объекта будут отрисовываться корректно (рисунок 12).


Рисунок 12. Правильное изображение (после включения Z-буфера)

В C#-программе рендеринг уже и так производится непрерывно, достаточно добавить код поворота и включения Z-буфера. Чтобы не повторяться, привнесем небольшое разнообразие: чайник в нашей программе будет не только вращаться, но и перемещаться по оси Z.

В листинге 19 приведен полный текст модифицированной программы:

Листинг 19. Вывод вращающегося объекта с включением Z-буфера [C#].
using System;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

class TestForm: Form
{
  static void Main()
  {
    TestForm form = new TestForm();
    form.Text = "Direct3D для .NET";
    form.Width = 400;
    form.Height = 300;
    form.InitD3D();
    form.Show();
    while(form.Created)
    {
      form.Render();
      Application.DoEvents();
    }
  }

  Device device;
  Mesh teapot;

  public void InitD3D()
  {
    try
    {
      PresentParameters parameters = new PresentParameters();
      parameters.Windowed = true;
      parameters.SwapEffect = SwapEffect.Discard;
      parameters.EnableAutoDepthStencil = true;
      parameters.AutoDepthStencilFormat = DepthFormat.D16;

      device = new Device(0, DeviceType.Hardware, this,
        CreateFlags.SoftwareVertexProcessing, parameters);
      teapot = Mesh.Teapot(device);
    }
    catch(Exception e)
    {
      MessageBox.Show(e.Message);
      return;
    }
  }

  public void Render()
  {
    try
    {
      device.Clear(ClearFlags.Target|ClearFlags.ZBuffer, 
        System.Drawing.Color.Blue, 1.0f, 0);
      device.RenderState.ZBufferEnable = true;
      device.RenderState.ZBufferWriteEnable = true;
      device.BeginScene();
      device.RenderState.Lighting = true;
      device.Lights[0].Type = LightType.Directional;
      device.Lights[0].Direction = new Vector3(7, -2, 1);
      device.Lights[0].Diffuse = System.Drawing.Color.Yellow;
      device.Lights[0].Commit();
      device.Lights[0].Enabled = true;

      Material material = new Material();
      material.Ambient = System.Drawing.Color.White;
      material.Diffuse =  System.Drawing.Color.White;
      material.Specular = System.Drawing.Color.White;
      device.Material = material;

      Matrix matrix = new Matrix();
      matrix.Scale(0.7f, 0.7f, 0.7f);
      matrix *= Matrix.RotationY( 
        System.Environment.TickCount / 300.0f );
      float move = (float)Math.Sin(
        System.Environment.TickCount/2000.0f)*4+6;
      matrix.M43 = move;
      Text = move.ToString("##.##");
      device.RenderState.Clipping = false;
      device.Transform.World = matrix;
      device.Transform.Projection = Matrix.PerspectiveFovLH(
        (float)Math.PI / 4, 1.0f, 1.0f, 100.0f );

      teapot.DrawSubset(0);
      device.EndScene();
      device.Present();
    }
    catch(Exception e)
    {
      return;
    }
  }
}

Удаление нелицевых граней

Другой способ оптимизации вывода сложных сцен заключается в делении всех примитивов объекта на «обращенные лицом к наблюдателю» и «обращенные лицом от наблюдателя». Если объект не содержит «дыр», то при любом его положении всегда видны только лицевые грани. Следовательно, отказавшись от вывода нелицевых примитивов, можно сэкономить примерно 50% времени на построение сцены. Этот способ отсечения называется cull (англ. «отбраковка»).

В Direct3D режим cull включен по умолчанию. Лицевыми считаются треугольники, вершины которых описываются против направления часовой стрелки (на их экранной проекции, разумеется). Как уже было показано, библиотека D3DX создает объекты с учетом этого режима. Если же установить обратный режим отсечения, то будут рисоваться только внутренние полости объекта, что приведет к получению довольно причудливого изображения (рисунок 13).


Рисунок 13. Изображение с обращенными лицевыми гранями.

Для установки такого режима Cull в программе на C++ используется метод SetRenderState:

pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

А в программе на C# применяется следующий синтаксис:

device.RenderState.CullMode = Cull.Clockwise;

Если созданный объект содержит «дыры» или полупрозрачные участки, сквозь них должны быть видны нелицевые грани. Для этого необходимо отключение режима cull (используйте, соответственно, константы D3DCULL_NONE и Cull.None).

Заключение

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


Эта статья опубликована в журнале RSDN Magazine #2-2005. Информацию о журнале можно найти здесь
    Сообщений 19    Оценка 230 [+2/-0]         Оценить