D3DRM Tutorials Уроки по DirectX
Урок 2. D3DRM

 

Обзор

 

Direct 3D Retained Mode (Абстрактный режим) – это набор интерфейсов для работы с трехмерной графикой. С помощью этого набора интерфейсов можно манипулировать сетками (это другое название моделей и объектов), источниками освещения, камерами, а также фреймами. Последние обеспечивают мощный механизм для быстрого построения и манипулирования сценами. Поэтому, Retained Mode (далее просто RM) предназначен для работы непосредственно со сценой, а Непосредственный Режим (Immediate Mode) предназначен для вывода текстурированных треугольников. Используя, RM Вы сможете загрузить сетки, расположить источники освещения и оживить полученную сцену.

 

RM очень распространенный набор интерфейсов, скорее всего он есть на всех компьютерах с Windows 9x, NT, Me, 2000, так как RM входил в состав DirectX до версии 7. RM не модернизировался фирмой Microsoft после выхода DirectX v6.1, но был в составе DirectX v7, как часть DirectX Media. Предполагалось, его заменит компонент Fahrenheit Scene Graph (FSG)  в составе графической библиотеки Fahrenheit. Но этого не произошло. Тем не менее, RM не потерял привлекательности, так как в составе DirectX пока нет другого набора высокоуровневых интерфейсов для работы с трехмерной графикой на уровне сцены.

 

Описание основных интерфейсов RM

 

Надеюсь, Вы знаете, как программировать на Си с помощью Microsoft Visual C++ v4 или выше. Если нет, то Вам будет трудно понять эту статью. Также я предполагаю, что Вы знаете, как работают COM-объекты, так как все интерфейсы RM реализованы на этой основе. Начальные знания по трехмерной графике тоже будут не лишними.

 

Главный интерфейс RM называется Direct3DRM. Он позволяет создавать другие интерфейсы RM и выполнять ряд сервисных функций. В частности связывать RM с устройствами для вывода графики.

 

Интерфейс для работы с устройствами Direct3DRMDevice. Устройства – это фактически драйвера видеокарт. Есть два основных типа устройств RM: программные и аппаратные. Первые выполняют трехмерные расчеты на центральном процессоре компьютера, а вторые используют специализированный графический процессор на видеокарте, что обеспечивает несравненно более быстрый вывод графики.

 

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

 

Интерфейс Direct3DRMWinDevice позволяет RM сообщаться с Windows. Он проделывает «непонятную», но необходимую работу.

 

Интерфейс Direct3DRMViewport позволяет создавать области просмотра, т.е. по-другому говоря камеры, посредством которых мы, и созерцаем трехмерный мир.

 

Интерфейс Direct3DRMFrame позволяет работать с фреймами. Фреймы – это точки првязки объектов в пространстве, а не кадры в анимациию. Direct3DRMFrame основной интерфейс RM. Именно он позволяет создавать сцены и эффективно с ними работать. С помощью фреймов можно связывать группы объектов и легко ими манипулировать. Фрейм позволяет привязать сетку или источник освещения к определенному месту в пространстве. К фрейму можно привязать другие фреймы и перемещая главный фрейм, будут перемещаться и связанные фреймы. Т.е. фреймы позволяют обеспечить прямую кинематику, но, к сожалению, инверсной кинематики фреймы не обеспечивают.

 

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

 

Зато интерфейс сеток Direct3DRMMesh предназначен для увеличения быстродействия, но очень не прост в «общении». Он позволяет изменять внешний вид сетки (форму и закраску). Для этого используется группа граней внутри сетки. С помощью этого интерфейса можно создавать сетку непосредственно в приложении и манипулировать ее формой, например, для морфинга.

 

Сетки состоят из граней. Интерфейс для работы с ними Direct3DRMFace. Это не высокоуровневый интерфейс, он предназначен для «тонкой» работы над сеткой (на уровне вершин граней). Рассмотренные три последних интерфейса позволяют эффективно оперировать с сетками на любом уровне работы с сеткой.

 

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

 

Интерфейс текстуры Direct3DRMTexture позволяет назначить текстуры на сетку. Текстуры могут быть загружены из BMP или PPM файлов. Они могут находиться в ресурсах программы, в файлах или в памяти. Также этот интерфейс позволяет работать с декалами. Декал – это спрайт, который Вы можете добавить к сцене. С помощью них можно эмулировать дождь, снег, взрывы. Но их видно только с лицевой стороны.

 

Интерфейс покрытия текстурой Direct3DRMWrap необходим, для того чтобы задать метод наложения текстуры на сетку. Это не очень просто. Но без этого сетка будет выглядеть уродливо. Если Вы разработали модель в программе трехмерного моделирования и конвертировали ее в формат сетки RM (файлы с расширением “.x”), то покрытие уже задано, и Вам не надо об этом беспокоиться. Есть три типа покрытия: плоское, сферическое и цилиндрическое. Покрытие назначается на всю сетку, т.е. расположение текстуры на гранях вычисляется автоматически.

 

Интерфейс материала Direct3DRMMaterial позволяет определить, как свет отражается от объекта и как сетка излучает свет. Т.е. можно имитировать металлические или пластиковые объекты. А также объекты, испускающие свет.

 

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

 

Интерфейс для анимации Direct3DRMAnimation позволяет двигать и вращать объекты на сцене. Этот интерфейс позволяет создавать ключи. В ключах задается перемещение, вращение и масштабирование объекта. Последовательность, основанная на ключах позволяет создавать анимацию в целом.

 

Для загрузки подготовленных сцен с анимацией служит интерфейс анимационного набора Direct3DRMAnimationSet. Он поддерживает совокупность анимированных объектов.

 

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

 

Типы данных RM

 

Есть несколько типов данных, которые широко используются в RM.

 

D3DVALUE имеет тип float. Если Вы хотите, чтобы Ваша программа была переносимой на другие платформы (вдруг, это случиться), то используйте макрос D3DVALUE. Например, D3DVALUE(3) будет равно 3.0f.

 

Структура D3DVECTOR  определена так:

 

typedef struct _D3DVECTOR {

    union {

        D3DVALUE x;

        D3DVALUE dvX;

    };

    union {

        D3DVALUE y;

        D3DVALUE dvY;

    };

    union {

        D3DVALUE z;

        D3DVALUE dvZ;

    };

} D3DVECTOR, *LPD3DVECTOR;

 

Эта структура нужна для представления вектора или точки.

 

Для представления цвета в RM используется тип D3DCOLOR (длина 4 байта). Для приведения в тип D3DCOLOR можно использовать макросы D3DRGB или D3DRGBA.

 

D3DCOLOR color=D3DRGB(1,1,1); // Белый цвет

D3DCOLOR color=D3DRGBA(1,1,1,0); // Белый цвет с нулевой прозрачностью

 

Для задания размера объекта используется структура D3DRMBOX.

 

typedef struct _D3DRMBOX

{

    D3DVECTOR min, max;

} D3DRMBOX;

 

Переменные min, max описывают параллелепипед, который ограничивает объект (оболочка объекта).

 

Большинство функций RM возвращает в качестве кода ошибки HRESULT. Если код равен константе D3DRM_OK, то вызов функции был успешен. Иначе, вы получите код ошибки.

 

Инициализация Direct3D Retained Mode

 

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

 

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

 

Итак, начнем!

 

Вначале приведем объявления переменных, которые мы будем использовать в примере. Пока без обсуждения.

 

#include <windows.h>

#include <windowsx.h>

#include "ddraw.h"

#include "d3drm.h"

#include "d3drmwin.h"

 

BOOL CreateScene();

void DestroyScene();

void ScaleMesh(LPDIRECT3DRMMESHBUILDER mesh, D3DVALUE dim);

 

int yes_full, yes_render, yes_hardware;

HWND hwnd;

 

LPDIRECT3DRM rm=0;

LPDIRECTDRAWCLIPPER clip=0;

LPDIRECT3DRMDEVICE device=0;

LPDIRECT3DRMFRAME scene=0;

LPDIRECTDRAW ddraw=0;

LPDIRECTDRAWSURFACE primsurf=0;

LPDIRECTDRAWSURFACE backsurf=0;

LPDIRECTDRAWSURFACE zbufsurf=0;

LPDIRECT3DRMFRAME camera;

LPDIRECT3DRMVIEWPORT viewport;

LPDIRECT3DRMMESHBUILDER meshbuilder;

 

D3DDEVICEDESC DrvDesc;

 

RECT rect_win;

int bpp;

 

GUID* ptr_guid=0;

 

Функцию инициализации RM лучше всего вызывать, после того как создано окно. Т.е. после получения сообщения WM_CREATE функцией обработки сообщений окна. Функция инициализации RM должна быть вызвана, прежде всего. У нее шесть параметров.

 

BOOL  InitD3DRM

(HWND hWnd, int full_win, int wid, int hei, int bit, int yes_hw )

{

  hwnd=hWnd;

  yes_full=full_win;

  yes_hardware=yes_hw;

 

hWnd – указатель на окно, если full_win равно единице, то предполагается использовать RM на полноэкранном режиме, иначе в окне на рабочем столе. Wid, hei,bit задают ширину, высоту и цветность полноэкранного режима. yes_hw равно 1, значит надо попытаться использовать аппаратное устройство, иначе программное.

 

RM можно использовать либо в окне на рабочем столе Windows или на полный экран. Причем, видеорежим полного экрана может и не совпадать с видеорежимом рабочего стола. В случае окна wid, hei, bit не имеют смысла, так как окно всегда располагается на рабочем столе и его размер задается при инициализации. И по этому нам придется запросить их отдельно:

 

  GetClientRect(hwnd,&rect_win);

 

  HDC hdc=GetDC(hwnd);

  bpp=GetDeviceCaps(hdc,BITSPIXEL);

  ReleaseDC(hwnd,hdc);

 

Затем мы находим указатель на устройство, которое поддерживает либо аппаратную  (yes_hw равен 1) или программную (yes_hw равен 0) работу. Обсудим эту функцию позже.

 

  ptr_guid=FindBestGUID ( yes_full==1?bit:bpp, yes_hardware, &DrvDesc);

  if ( ptr_guid==0 ) yes_hardware=0;

 

Создадим главный интерфейс Direct3DRM.

 

  HRESULT rc;

  rc=Direct3DRMCreate(&rm);

  if ( rc!=D3DRM_OK ) return FALSE;

 

Порядок выполнения нашего кода зависит от переменной yes_full. Если она равна нулю, значит надо инициализировать RM в окне.

 

  if ( yes_full==0 )

  {

    rc=DirectDrawCreateClipper(0,&clip,NULL);

    if ( rc!=D3DRM_OK ) return FALSE;

 

Здесь мы инициализировали переменную clip - указатель на интерфейс DirectDrawClipper. Так как мы работаем в окне, то необходимо использовать область отсечения для управления обновлением в окне. Предположим мы ввели сцену в окне и сверху на это изображение наложили несколько окон, мы вправе ожидать, что RM не будет затирать содержимое чужих окон. Вот это и будет обеспечивать интерфейс DirectDrawClipper, который ведает сложными операциями отсечения частей окна, где не должен быть идти графический вывод.

 

    clip->SetHWnd(NULL,hwnd);

    if ( rc!=DD_OK ) return FALSE;

 

Здесь мы назначаем объекту отсечения окно, в котором он и будет работать.

 

Создадим устройство RM.

 

    rc=rm->CreateDeviceFromClipper( clip, ptr_guid, rect_win.right, rect_win.bottom, &device );

 

    if ( rc!=D3DRM_OK )

    {

      if ( clip ) { clip->Release(); clip=0; }

      if ( rm ) { rm->Release(); rm=0; }

      return FALSE;

    }

  }

 

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

 

Инициализировать RM для работы на полный экран значительно сложнее.

 

Вначале погасим курсор мыши.

 

  else

  {

    ShowCursor(FALSE);

 

Затем инициализируем указатель на интерфейс DirectDraw. Здесь уже не нужен объект отсечения.

 

    DirectDrawCreate(0,&ddraw,0);

 

Переключим видеорежим.

 

    ddraw->SetCooperativeLevel( hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );

    ddraw->SetDisplayMode( wid, hei, bit );

    rect_win.right=wid;

    rect_win.bottom=hei;

 

Но это далеко не все! Надо создать три поверхности! Первичную, вторичную и Z-буфер. Спросите, зачем это?! Дело в том, что полноэкранное приложение игнорирует рабочий стол Windows и поэтому мы должны управлять видеоадаптером сами. В награду Вам будет плавная анимация, так как, используя полноэкранное приложение, Вы можете подготовить сцену во вторичной поверхности, а затем сделать ее первичной (выполнить переключение поверхности). Тем самым поэтапная прорисовка сцены не будет видна зрителю.

 

Создадим первичную поверхность.

 

    DDSURFACEDESC desc;

    desc.dwSize = sizeof(desc);

    desc.dwFlags = DDSD_BACKBUFFERCOUNT | DDSD_CAPS;

    desc.dwBackBufferCount = 1;

    desc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_3DDEVICE |

                          DDSCAPS_FLIP | DDSCAPS_COMPLEX;

    ddraw->CreateSurface(&desc,&primsurf,0);

 

Затем добавим к ней вторичную.

 

    DDSCAPS ddscaps;

    ddscaps.dwCaps = DDSCAPS_BACKBUFFER;

    primsurf->GetAttachedSurface(&ddscaps,&backsurf);

 

Теперь создадим Z-буфер.

 

    memset(&desc,0,sizeof(desc));

    desc.dwSize = sizeof(DDSURFACEDESC);

    desc.dwFlags = DDSD_WIDTH | DDSD_HEIGHT

   | DDSD_CAPS | DDSD_ZBUFFERBITDEPTH;

    desc.dwWidth = wid;

    desc.dwHeight = hei;

    int bit_z;

    if ( yes_hardware )

    {

      int devDepth = DrvDesc.dwDeviceZBufferBitDepth;

      if (devDepth & DDBD_32)

        bit_z = 32;

      else if (devDepth & DDBD_24)

        bit_z = 24;

      else if (devDepth & DDBD_16)

        bit_z = 16;

      else if (devDepth & DDBD_8)

        bit_z = 8;

    }

    else  bit_z=16;

      desc.dwZBufferBitDepth = bit_z;

    desc.ddsCaps.dwCaps = DDSCAPS_ZBUFFER;

    if ( yes_hardware )

      desc.ddsCaps.dwCaps |= DDSCAPS_VIDEOMEMORY;

    else

      desc.ddsCaps.dwCaps |= DDSCAPS_SYSTEMMEMORY;

    ddraw->CreateSurface(&desc,&zbufsurf,0);

    backsurf->AddAttachedSurface(zbufsurf);

 

Ух, как много! Помимо настроек самого Z-буфера, мы использовали настройки устройства (DrvDesc), а именно значение которое определяет глубину Z-буфера. Как правило, программные устройства поддерживают любую глубину Z-буфера и 8 и 16 и 24 и 32 разрядную. Но в случае аппаратного устройства это не всегда так. Надо четко определить максимальную разрядность и использовать именно ее, иначе при выборе несуществующей разрядности Z-буфера RM невозможно будет инициализировать.

 

Кроме этого при использовании аппаратного устройства можно использовать аппаратный Z-буфер, а не в основной памяти компьютера.

 

Тем самым все готово для инициализации устройства RM.

 

    rc=rm->CreateDeviceFromSurface(ptr_guid,ddraw,backsurf,&device);

    if ( rc!=D3DRM_OK )

    {

      ShowCursor(TRUE);

      if ( zbufsurf ) { zbufsurf->Release(); zbufsurf=0; }

      if ( backsurf ) { backsurf->Release(); backsurf=0; }

      if ( primsurf ) { primsurf->Release(); primsurf=0; }

      if ( ddraw ) { ddraw->Release(); ddraw=0; }

      if ( rm ) { rm->Release(); rm=0; }

      return FALSE;

    }

  }

 

Мы создаем устройство для вторичной поверхности (т.е. вывод будет идти на вторичную поверхность) и инициализируем device. Довольно сложно? Вы правы, я видел много разных вариантов подобного кода, и они не всегда работали корректно, в основном по причине неправильного выбора глубины Z-буфера.

 

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

 

  device->SetQuality(D3DRMRENDER_GOURAUD);

 

Установим режим закрашивания (освещения) по методу Гуро. RM также поддерживает режимы закрашивания каркаса (только вершины без граней), неосвещенный метод, равномерная закраска. Закраска по методу Фонга поддерживается, как правило, только программными устройствами.

 

Далее следует настройка устройства и Direct3DRM в зависимости от цветности.

 

  switch ( yes_full==1?bit:bpp )

  {

  case 16:

    device->SetShades( 32 );

    rm->SetDefaultTextureColors( 64 );

    rm->SetDefaultTextureShades( 32 );

    device->SetDither( FALSE );

    break;

  case 24:

  case 32:

    device->SetShades( 256 );

    rm->SetDefaultTextureColors( 64 );

    rm->SetDefaultTextureShades( 256 );

    device->SetDither( FALSE );

    break;

  }

 

Как Вы видите, я не предполагаю, что Вы будете использовать 8-битные палитровые режимы. Так как они видимо устарели. Так же я не обсуждаю эти установки, т.к. не играют большой роли. Таким образом, устройство RM создано, настроено, и можно создавать сцену.

 

Сначала создадим головной фрейм.

 

  rc=rm->CreateFrame(NULL,&scene);

  if ( rc!=D3DRM_OK ) return FALSE;

 

Это простой, но крайне важный шаг. Это центр нашего мира! Без этого шага наше приложение работать не будет. К головному фрейму привязываются все объекты на сцене. Головных фреймов может быть несколько, но в этом нет необходимости.

 

Теперь создадим сцену.

 

  rc=CreateScene();

  if ( rc!=TRUE ) return FALSE;

 

  yes_render=1;

 

  return TRUE;

}

 

Как это делается, я расскажу позже. А теперь вернемся к функции FindBestGUID.

 

#define MAX_DRV 5

 

struct {

  GUID  Guid;

  char  Name[50];

  char  About[50];

  BOOL  isHardware;

  D3DDEVICEDESC  Desc;

} DrvInfo[MAX_DRV];

 

int ptr_drv=0;

 

static DWORD BPPToDDBD(int bpp)

{

  switch(bpp)

  {

  case 1:  return DDBD_1;  case 2:  return DDBD_2;

  case 4:  return DDBD_4;  case 8:  return DDBD_8;

  case 16:  return DDBD_16;  case 24:  return DDBD_24;

  case 32:  return DDBD_32;  default:  return 0;

  }

}

 

static HRESULT WINAPI enumDeviceFunc(

  LPGUID lpGuid, LPSTR lpDeviceDescription,

    LPSTR lpDeviceName, LPD3DDEVICEDESC lpHWDesc,

    LPD3DDEVICEDESC lpHELDesc, LPVOID lpContext )

{

  if ( ptr_drv>=MAX_DRV ) return D3DENUMRET_CANCEL;

 

memcpy(&DrvInfo[ptr_drv].Guid, lpGuid, sizeof(GUID));

lstrcpy(DrvInfo[ptr_drv].About, lpDeviceDescription);

lstrcpy(DrvInfo[ptr_drv].Name, lpDeviceName);

if ( lpHWDesc->dcmColorModel )

{

    DrvInfo[ptr_drv].isHardware=1;

    memcpy(&DrvInfo[ptr_drv].Desc, lpHWDesc,sizeof(D3DDEVICEDESC));

}

else

{

    DrvInfo[ptr_drv].isHardware=0;

    memcpy(&DrvInfo[ptr_drv].Desc, lpHELDesc, sizeof(D3DDEVICEDESC));

}

 

ptr_drv++;

 

return D3DENUMRET_OK;

}

 

 

GUID* FindBestGUID(int ptr_bpp, int hw, LPD3DDEVICEDESC lpDesc)

{

  GUID* lpguid=0;

  LPDIRECTDRAW ddraw;

  LPDIRECT3D d3d;

  HRESULT rc;

  int i;

 

  rc = DirectDrawCreate( 0, &ddraw, 0 );

  if (rc!=DD_OK)

    return lpguid;

 

  rc = ddraw->QueryInterface( IID_IDirect3D, (void**)&d3d );

  if ( rc != D3DRM_OK )

    return lpguid;

 

  rc=d3d->EnumDevices( &enumDeviceFunc, 0 );

  if ( rc != D3D_OK )

    return lpguid;

 

  d3d->Release();

  ddraw->Release();

 

  for ( i=0; i<ptr_drv; i++ )

  {

    if ( DrvInfo[i].isHardware==hw )

    if ( DrvInfo[i].Desc.dwDeviceRenderBitDepth & BPPToDDBD(ptr_bpp))

    if ( DrvInfo[i].Desc.dcmColorModel==D3DCOLOR_RGB )

    {

      lpDesc=&DrvInfo[i].Desc;

      return &DrvInfo[i].Guid;

    }

  }

  return lpguid;

}

 

Я привел весь код сразу, в надежде, что Вы сами во всем разберетесь (если не поймете, то ничего плохого в этом нет – это вспомогательный код). Отмечу только, что здесь используется функция перечисления устройств Direct3D. Именно их использует RM внутри себя для непосредственной визуализации полигонов. Все данные, полученные при перечислении сохраняются в массив структур DrvInfo. Затем мы выбираем наилучшее устройство. Оно должно быть или аппаратным или программным устройством, поддерживать необходимую цветность, и цветное освещение.

 

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

 

Мне кажется, что инициализация работы самая сложная и рутинная часть работы с RM. Если я помог Вам разобраться с этим, то я выполнил свою задачу.

 

Следующие три функции необходимы для корректной работы RM в окне. После получения сообщения WM_ACTIVATE от Windows необходимо известить об этом RM.

 

void  ActivateD3DRM(WPARAM wParam)

{

  if ( yes_render==0 ) return;

  if ( yes_full==1 ) return;

 

  LPDIRECT3DRMWINDEVICE   win_dev;

  if ( device==0 ) return;

  if ( device->QueryInterface(IID_IDirect3DRMWinDevice,

(void **)&win_dev)!=D3DRM_OK ) return;

  win_dev->HandleActivate((WORD)wParam);

  win_dev->Release();

}

 

Вначале надо запросить интерфейс Direct3DRMWinDevice и затем вызвать функцию его HandleActivate.

 

Посмотрим на функцию извещения RM о получении сообщения WM_PAINT.

 

void  PaintD3DRM()

{

  if ( yes_render==0 ) return;

  if ( yes_full==1 ) return;

 

  HDC hdc=GetDC(hwnd);

  int bpp_ptr=GetDeviceCaps(hdc,BITSPIXEL);

  ReleaseDC(hwnd,hdc);

 

  if ( bpp!=bpp_ptr )

  {

    DestroyD3DRM();

    InitD3DRM(hwnd,yes_full,0,0,0,yes_hardware);

  }

 

  RECT        rect;

  PAINTSTRUCT             ps;

  LPDIRECT3DRMWINDEVICE   win_dev;

 

  if ( GetUpdateRect(hwnd,&rect,FALSE)==FALSE ) return;

  BeginPaint( hwnd,&ps );

  if ( device->QueryInterface(IID_IDirect3DRMWinDevice,(void **)&win_dev)!=D3DRM_OK )

    return;

  win_dev->HandlePaint(ps.hdc);

  win_dev->Release();

  EndPaint( hwnd,&ps );

}

 

В этой функции надо повторно иницализировать RM, если изменена текущая цветность. Далее запрашивается интерфейс Direct3DRMWinDevice и вызывается его функция HandlePaint.

 

Если изменились размеры окна, то об этом также необходимо известить RM.

 

void  SizeD3DRM(WPARAM wParam,LPARAM lParam)

{

  if ( yes_render==0 ) return;

  if ( yes_full==1 ) return;

 

  int w = LOWORD(lParam);

  int h = HIWORD(lParam);

 

  if ( (w==rect_win.right) && (h==rect_win.bottom) ) return;

 

  DestroyD3DRM();

  InitD3DRM(hwnd,yes_full,0,0,0,yes_hardware);

}

 

Но это извещение носит несколько странный характер! Мы вынуждены повторно инициализировать RM?! Что делать – по-другому никак нельзя.

 

Теперь рассмотрим, как надо производить сброс всех интерфейсов.

 

void  DestroyD3DRM()

{

 

  if ( yes_render==0 ) return;

 

  yes_render=0;

 

  DestroyScene();

 

  if ( scene ) { scene->Release(); scene=0; }

 

  if ( yes_full==1 )

  {

    ShowCursor(TRUE);

    if ( device ) { device->Release(); device=0; }

    if ( zbufsurf ) { zbufsurf->Release(); zbufsurf=0; }

    if ( backsurf ) { backsurf->Release(); backsurf=0; }

    if ( primsurf ) { primsurf->Release(); primsurf=0; }

    if ( ddraw ) { ddraw->Release(); ddraw=0; }

    if ( rm ) { rm->Release(); rm=0; }

  }

  else

  {

    if ( device ) { device->Release(); device=0; }

    if ( clip ) { clip->Release(); clip=0; }

    if ( rm ) { rm->Release(); rm=0; }

  }

}

 

При этом если приложение работало в полноэкранном режиме, то происходит возврат в видеорежим рабочего стола.

 

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

 

void  RenderD3DRM()

{

  if ( yes_render==0 ) return;

  if ( yes_full==0 )

  {

    rm->Tick(D3DVALUE(1));

  }

  else

  {

    if ( primsurf->IsLost() == DDERR_SURFACELOST )

      primsurf->Restore();

    scene->Move(D3DVALUE(1.0));

    viewport->Clear();

    viewport->ForceUpdate(0,0,rect_win.right-1,rect_win.bottom-1);

    viewport->Render( scene );

    device->Update();

    primsurf->Flip(0,DDFLIP_WAIT);

  }

}

 

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

 

Что же делает функция Render? Вначале рассмотрим, как осуществляется визуализация в режиме окна. Вызывается только одна функция Tick. С помощью этой функции производится передвижение всех анимированных объектов (функция Move), очистка экрана (Clear), собственно визуализация (Render) и обновление устройства (Update). Таким образом, если для полноэкранного режима приходиться вызывать все четыре функции отдельно, то для оконного режима хватило только одной.

 

У функции Tick один аргумент и тот же аргумент у функции Move. Он управляет скоростью анимации. Если задать ему значение 0.5, то, например, если задано вращение у какого-то объекта, он будет вращаться в два раза медленнее, чем при значении 1.0. Если значение скорости анимации 2.0, то в два раза быстрее. Тем самым можно добиться одинаковой скорости анимации на разных компьютерах. Т.е. на медленных компьютерах надо увеличить значение скорости анимации, на быстрых компьютерах уменьшить, и тогда время, за которое происходит анимация, будет постоянным. Но на практике все сложнее. Фактически, не стоит менять это значение 1.0 на другое. Особенно не стоит подстраивать скорость анимации на лету (вычисляя как надо уменьшить или увеличить скорость анимации в зависимости от скорости выполнения программы), т.к. анимация уже не будет плавной. Она то будет очень плавной, то отдельные фазы движений будут проскакивать. Самое простое решением исходить из скорости обновления экрана. Выставить скорость анимации в 1.0 и ограничить скорость обновления экрана, например, 75 раз в секунду.

 

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

 

Необходимо восстановить первичную поверхность, если она потеряна (Restore). Отмечалось выше, надо выполнить анимацию (Move) и очистку экрана (Clear). Кроме этого, укажем устройству RM, что надо обновить все изображение в области просмотра (ForceUpdate). Визуализируем сцену (Render), обновим вторичную поверхность (Flip). Теперь изображение создано на вторичной поверхности, и надо только переключить первичную поверхность, чтобы увидеть его. По-моему мнению, все это могла сделать функция Tick, но она не делает этого.

 

Как Вы уже заметили, здесь используется переменная viewport. Это камера! Мы ее будет инициализировать при создании сцены.

 

И сейчас мы перейдем к рассмотрению основной функции нашего примера CreateScene. С помощью этой функции будет загружена сетка, мы заставим ее вращаться, и осветим ее коричневым цветом. До этого момента мы только и делали, что готовились к созданию сцены. Пора ее создать!

 

BOOL CreateScene()

{

  LPCSTR searchpath = "C:\\mesh";

  rm->AddSearchPath(searchpath);

 

Зададим директорию, где будет находиться наша сетка. Но она может находиться и в текущей директории.

 

  HRESULT rc;

  rm->CreateMeshBuilder(&meshbuilder);

  rc = meshbuilder->Load("test.x",NULL,D3DRMLOAD_FROMFILE,NULL,NULL);

  if ( rc!=D3DRM_OK ) return FALSE;

  ScaleMesh(meshbuilder,D3DVALUE(25));

 

Создадим сетку (CreateMeshBuilder), с помощью интерфейса конструктора сеток загрузим ее из файла (Load). После этого мы масштабируем ее (ScaleMesh). Как видите это довольно просто. Масштабирование сетки необходимо, но необязательно. Просто разные сетки могут иметь разные размеры, и соответственно после загрузки и отображения, они могут быть или слишком маленькими, или слишком большими.

 

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

 

  LPDIRECT3DRMFRAME meshframe;

  rm->CreateFrame(scene,&meshframe);

 

Фрейм meshframe создается относительно фрейма scene. Как указано выше фрейм scene был создан как корневой фрейм сцены. Можно создать несколько корневых фреймов. Для этого надо в первый аргумент функции CreateFrame поместить NULL. Но все корневые фреймы будут находиться в начале координат.

 

Здесь мы относительно фрейма scene сделали фрейм meshframe. Он будет, по умолчанию, находиться в местоположении фрейма scene, т.е. сетка будет в центре сцены. Если конечно центр сетки, совпадает с его геометрическим центром.

 

Теперь зададим вращение сетки.

 

  meshframe->SetRotation( scene,

      D3DVALUE(0), D3DVALUE(1), D3DVALUE(0),

      D3DVALUE(.1) );

 

Вращение задается фрейму, а не сетке! Если вращается фрейм, то вращается и сетка. Если, например, задать вращение корневого фрейма, то будет вращаться вся сцена. Первый аргумент функции SetRotation указатель scene, т.е. meshframe дочерний фрейм по отношению к фрейму scene. Затем, три аргумента задают вектор относительно, которого происходит вращение (x=0, y=1, z=0). Этот вектор направлен вверх, и вращение будет происходить вокруг оси Y. Пятый аргумент задает угол вращения 0.1 радиан, т.е. при каждом обновлении экрана (вызов функции Move) сетка повернется на 0.1 радиан.

 

  meshframe->AddVisual(meshbuilder);

  meshframe->Release();meshframe=0;

 

Вызовом функции AddVisual мы привяжем сетку к фрейму (сделаем его видимым). Очень важно это делать только после всех настроек фрейма и сетки. Так как указатель meshframe нам больше не нужен, то освободим его. Но он не пропадет – указатель на него храниться в головном фрейме.

 

  LPDIRECT3DRMLIGHT slight;

  rm->CreateLightRGB(D3DRMLIGHT_SPOT,

    D3DVALUE(1.0),D3DVALUE(0.7),D3DVALUE(0.5),&slight);

 

Для того чтобы на сцене появился свет, создадим прожектор (D3DRMLIGHT_SPOT). Создадим цветной, не монохромный свет. Три значения задают коричневый цвет источника света. В результате получим указатель slight. Теперь создадим фрейм для него.

 

  LPDIRECT3DRMFRAME slightframe;

  rm->CreateFrame(scene,&slightframe);

 

Сдвинем фрейм источника света.

 

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

 

  slightframe->SetPosition(scene, D3DVALUE(0.),D3DVALUE(0.),D3DVALUE(-100.));

 

Мы сдвигаем источник света далеко за камеру. При этом не смещаем его ни вверх, ни вниз, ни влево, ни вправо.

 

  slightframe->AddLight(slight);

  slightframe->Release();slightframe=0;

  slight->Release();slight=0;

 

Но в любом случае надо присоединить прожектор к фрейму и освободить указатели на прожектор и его фрейм.

 

Последние, что нам осталось сделать создать камеру. Камеру можно было создать и значительно раньше, у нее нет привязок к объектам сцены.

 

  rm->CreateFrame(scene,&camera);

  camera->SetPosition(scene,D3DVALUE(0),D3DVALUE(30),D3DVALUE(-50));

 

Создадим фрейм (CreateFrame) и зададим ему позицию (SetPosition). “Оттащим” камеру подальше от центра координат и вверх, чтобы видеть весь объект.

 

  camera->SetOrientation(scene,

    D3DVALUE(0.),D3DVALUE(-0.7),D3DVALUE(1.),

    D3DVALUE(0.),D3DVALUE(1.),D3DVALUE(0.));

 

Зададим камере ориентацию. Для этого надо задать два вектора. Один вектор лицевой, другой головной. Фактически, все это очень похоже на голову. Представьте себе, что Вы смотрите на сцену. При этом нос будет ассоциироваться с лицевым вектором, а макушка с головным. Тогда вы сразу поймете, что лицевой вектор направлен вдоль оси Z от зрителя, но при этом немного смещен вниз (Вы как бы опустили нос и он смотрит на точку с координатами x=0, y=-0.7, z=1), а головной строго вверх. Т.е. камера будет поднята вверх, и мы будем смотреть на объект сверху вниз.

 

  rm->CreateViewport(device,camera,0,0,

    device->GetWidth(),device->GetHeight(),&viewport);

 

  return TRUE;

}

 

Помимо этого надо создать область просмотра. Для ее инициализации надо использовать устройство (device) и фрейм камеры. В результате мы получаем указатель viewport.

 

Все! Остальное RM сделает за нас! Мы указали все, что хотели. Дальше сетка будет вращаться, свет освещать ее и камера наблюдать за этим сверху. Пока вызывается функция Render.

 

В завершение приведем вспомогательную функцию для масштабирования.

 

void ScaleMesh(LPDIRECT3DRMMESHBUILDER mesh, D3DVALUE dim)

{

  D3DRMBOX box;

  mesh->GetBox(&box); 

  D3DVALUE sizex = box.max.x - box.min.x;

  D3DVALUE sizey = box.max.y - box.min.y;

  D3DVALUE sizez = box.max.z - box.min.z;

  D3DVALUE largedim = D3DVALUE(0); 

  if ( sizex>largedim ) largedim=sizex;

  if ( sizey>largedim ) largedim=sizey;

  if ( sizez>largedim ) largedim=sizez; 

  D3DVALUE scalefactor = dim/largedim;

  mesh->Scale(scalefactor,scalefactor,scalefactor);

}

 

И функцию для уничтожения сцены.

 

void DestroyScene()

{

  if ( camera ) camera->Release();camera=0;

  if ( viewport ) viewport->Release();viewport=0;

  if ( meshbuilder ) meshbuilder->Release();meshbuilder=0;

}

 

Теперь у Вас есть весь код примера инициализации RM и построения простейшей сцены. Нет только код создания окна и обработки сообщений. Его можно найти по адресу в Интернете: rm01.zip.

 

Если у Вас нет DirectX Media (SDK или Runtime), то по этому адресу его можно найти:

DirectX Media

 

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

 

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

 

PMG  9 июня 2001 (c)  Сергей Анисимов