ПРОГРАМИРУЕМ 3D ГРАФИКУ ИСПОЛЬЗУЯ DirectX

       

Клипы


Глава 11 Клипы

Покадровая съемка


На память мне приходит анекдот времен моего детства, которым я поделюсь с вами, хочется вам того или нет. Известный режиссер снимает в пустыне фильм о ковбоях. Тысячи статистов, томящихся под палящим солнцем, готовы к участию в грандиозной массовой сцене. На вершинах трех холмов стоят камеры, которые должны запечатлеть происходящее в различных ракурсах. Режиссер дает сигнал, статисты бросаются в бой. После завершения сцены режиссер спрашивает операторов, нормально ли прошла съемка. Первый оператор отвечает, что у него в камере заело пленку и он пропустил всю сцену. Второй говорит, что его камеру окутало облако пыли и ему удалось отснять всего несколько секунд. Третий оператор молчит. Режиссер кричит: «Эй, там, третья камера! Как дела?» Оператор отвечает: «Все готово, можно снимать!»

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

1. Подготовить пустой список изображений.

2. Сохранить текущую палитру (для 256-цветной видеосистемы).

3. Сохранить текущий размер трехмерного окна.

4. Переместить объекты макета в новое положение.

5. Воспроизвести макет во вторичном буфере.

6. Сохранить содержимое вторичного буфера.

7. Включить полученное изображение в список.

8. Повторить нужное количество раз, начиная с шага 4.

Перед тем как приступать к изучению программы, реализующей этот алгоритм, я должен предупредить вас, что он быстро поглощает системную память. Например, давайте представим, что у нас имеется окно размером 320х200 пикселей на 256-цветном дисплее. На каждое изображение потребуется 76,800 байт.



Покадровая съемка '''^Ц' 251

Если съемка будет производиться с частотой 10 кадров в секунду (fps), то за 10 секунд будет израсходовано 7,680,000 байт памяти (не считая накладных расходов по ведению списка, заголовков изображений и т. д.). При запуске приложения следует приготовиться к тому, что после исчерпания всей свободной памяти ваш диск начнет быстро заполняться. Разумеется, существует несколько способов выйти из положения — например, сжимать графические данные или непосредственно записывать изображения в файл на диске, однако мы рассмотрим их позднее. А пока давайте посмотрим, как же выполняется простейшая покадровая съемка.


Классы для работы с клипами

Я включил в библиотеку 3dPlus три класса, предназначенных для записи клипов. Объекты CMovieFrame используются для хранения отдельных кадров, объекты класса CMovieTake — для хранения палитры и списка кадров (одного эпизода), а объекты CMovie — для хранения целого клипа (который теоретически может состоять из нескольких эпизодов). Тем не менее, в отличие от режиссера из анекдота, мы снимаем все с первого раза, так что объект CMovie будет содержать только один объект CMovieTake.

Наиболее интересные фрагменты приложения Movie связаны с сохранением текущей палитры 256-цветного дисплея и записью изображения из вторичного буфера.

Сохранение палитры

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

Для ускорения процесса сохранения палитры я буду предполагать, что во время съемки палитра не изменяется. Такое предположение справедливо, если механизм визуализации работает в RGB-режиме, использующем постоянную палитру, но не в монохромном режиме, в котором элементы палитры могут изменяться при необходимости. Однако в простейших случаях объекты эволюционируют незначительно, и палитра остается достаточно стабильной. Следовательно, однократное сохранение палитры выглядит достаточно разумно. Пожалуй, аргумент получился не очень убедительным, но, с другой стороны, для воспроизведения клипа с приемлемым качеством мы просто не можем себе позволить отдельную палитру для каждого кадра. Реализация новой палитры в Microsoft Windows происходит слишком медленно и, кстати говоря, довольно скверно выглядит.

На проблеме только одной палитры хлопоты не кончаются. Нам понадобится особая разновидность палитры, которая называется идентичной палитрой (identity palette) и точно совпадает с текущей системной палитрой. Идентичная палитра позволяет напрямую копировать растровое изображение в видеопамять, обходясь без преобразования цветов. Преобразование цветового индекса каждого пикселя растра в цветовой индекс физической палитры происходит чрезвычайно медленно. Более подробно этот вопрос рассмотрен в книге «Animation Techniques in Win32» (Thomson, Microsoft Press, 1995).



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

/b> ИЦГ Глава 11. Клипы

delete pip;

return pPal;

}

Для получения цветов палитры от интерфейса используется структура LOGPALETTE; они передаются приложению в объекте CPalette. Класс CPalette, входящий в библиотеку MFC, хорошо подходит для хранения набора цветов; мы убедимся в этом, когда будем рассматривать воспроизведение клипа в окне приложения.

Функция класса CMovie, запрашивающая палитру у объекта DirectDraw в начале съемки, выглядит следующим образом:

BOOL CMovie::Record() {

Stop () ;

// Удалить все, что было записано ранее m Take.DeleteAll();

m iCurFrame =0;

// Сохранить текущую палитру ASSERT(m_p3dWnd) ;

CDirectDraw* pDD = m_p3dWnd-»GetDD () ;

ASSERT(pDD) ;

m_Take. SetPalette (pDD-»GrabPalette () ) ;

// Сохранить размер кадра CRect re;

m_p3dWnd-»GetClientRect (&rc) ;

m_Take.SetSize(re.Width(), re.Height()) ;

// Начать запись m_b!sRecording = TRUE;

return TRUE;

}

Как нетрудно убедиться, палитра сохраняется в текущем (и только в текущем!) объекте класса CMovieTakenpH помощи функции CMovieTake::SetPalette. Кроме того, мы сохраняем размер клиентной области текущего окна. Запись кадров происходит во время пассивной работы приложения.

Запись изображений из буфера

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

/b> ШУ Глава 11. Клипы

// Получить указатель на объект DirectDraw в трехмерном

окне

ASSERT(m_p3dWnd) ;

CDirectDraw* pDD = m_p3dWnd-»GetDD () ;

ASSERT(pDD) ;

// Получить текущее изображение BITMAPINFO* pBMI = NULL;

BYTE* pBits = NULL;

HBITMAP hBmp = pDD-»GrabImage (&pBMI, (void**)&pBits);

ASSERT(hBmp) ;

ASSERT(pBMI) ;

ASSERT(pBits) ;



// Создать кадр клипа

CMovieFrame* pFrame = new CMovieFrame(hBmp, pBMI,

pBits) ;

// Включить кадр в эпизод m_Take.AddTail(pFrame) ;

m iCurFrame++;

По сути дела в данном фрагменте мы запрашиваем изображение у объекта DirectDraw, создаем новый объект CMovieFrame, инкапсулирующий полученное изображение, и добавляем его в конец списка кадров текущего эпизода.

Каждый кадр представляет собой область памяти, которая называется секцией аппаратно-независимого растра (device-independent bitmap section, или сокращенно DIB-секция). DIB-секцию можно использовать в качестве растра Windows (при помощи связанного с ней логического номера HBITMAP) и напрямую записывать в нее данные через указатель. Вскоре мы увидим, как создается DIB-секция, а пока следует обратить внимание на то, что переменные hBmp, pBMI и pBits, содержащиеся в приведенном выше фрагменте, связаны с той или иной частью полученного изображения. Данное обстоятельство может привести к определенным затруднениям при попытке освободить память, выделенную под DIB-секцию. Для этого необходимо вызвать функцию ::DeleteObject для логического номера HBITMAP (hBmp), не пытаясь удалить данные по указателю pBits. Указатель pBMI ссылается на заголовочный блок, который должен удаляться отдельно от графических данных растра. Кому-то все это покажется неоправданно усложненным, однако к DIB-секции можно обращаться несколькими способами, и все выше указанные переменные необходимы для эффективной работы с ней.

Функция CDirectDraw::Grablmage выглядит значительно сложнее, поскольку ей приходится поддерживать различные форматы буфера. В общих чертах процесс выглядит примерно так: мы получаем описание типа буфера, создаем структуру типа BITMAPINFO для описания DIB-секции, создаем DIB-секцию, затем задаем маски сдвига и наконец копируем графические данные. При этом возникают определенные сложности, поскольку формат пикселей поверхностей DirectDraw не всегда совпадает с форматом пикселей DIB-секции; а следовательно, нам при-

Покадровая съемка ''Д1 255



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

Полагаю, я достаточно подготовил вас. Ниже приведена функция, которая получает изображение из буфера:

// Получить текущее изображение из буфера.

// Если функция возвращает информационный заголовок

// или графические данные, вызывающая функция должна

// освобождать соответствующую память вызовами delete pBMI

// и ::DeleteObject(hBmp). Не пытайтесь применять delete

// к графическим данным, поскольку они принадлежат

// объекту-растру.

HBITMAP CDirectDraw::GrabImage(BITMAPINFO** ppBMI,

void** ppBits) {

// Задать исходные значения, возвращаемые функцией

if (ppBMI) *ppBMI= NULL;

if (ppBits) *ppBits = NULL;

if (!m_pBackBuffer) return NULL;

// Заблокировать вторичный буфер, чтобы получить

// его описание.

// ВНИМАНИЕ: вы не сможете осуществить пошаговое

// выполнение

// этого фрагмента в отладчике — поверхность GDI

// блокируется.

LPDIRECTDRAWSURFACE iS = m_pBackBuffer-»GetInterface () ;

ASSERT;iS) ;

DDSURFACEDESC ds;

ds.dwSize = sizeof(ds);

m_hr = i3-»Lock(NULL,

&ds,

DDLOCK_WAIT,

NULL) ;

if (m_hr != DD_OK) {

TRACE("Failed to lock surface\n");

return NULL; // Failed to lock surface }

// Разблокировать поверхность, чтобы можно было // воспользоваться отладчиком //и при необходимости выйти из приложения m_hr = m_pBackBuffer-»GetInterface()-»Unlock(ds.lpSurface) ;

// Убедиться, что поверхность относится к одному

// из типов,

// с которыми мы можем работать.

// Примечание: программа обрабатывает только

/b> ЯИЦ1 Глава 11. Клипы

// поверхности с 8-,

// 16- и 24-битной кодировкой пикселей.

if (!(ds.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8) && '(ds.ddpfPixelFormat.dwFlags & DDPF_RGB)) { return NULL; // Формат не поддерживается программой

} int iBitCount;

if (ds.ddpfPixelFormat.dwFiags & DDPF_PALETTEINDEXED8) {

iBitCount = 8;

} else if (ds.ddpfPixelFormat.dwFlags & DDPF_RGB) {

// Проверить поверхность на допустимость типа



// цветовой кодировки пикселей

iBitCount = ds.ddpfPixel Format.dwRGBBitCount;

if ((iBitCount != 16) && (iBitCount != 24)) ( return NULL; // He поддерживается программой

}

}

ASSERT(ds.dwFlags & DDSD_WIDTH);

int iWidth = ds.dwWidth;

ASSERT(ds.dwFlags & DDSD_HEIGHT) ;

int iHeight = ds.dwHeight;

// Проверить, нужно ли создавать цветовую таблицу int iCIrTabEntries = 0;

if (ds.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8

// Построить цветовую таблицу iCIrTabEntries = 256;

iBitCount = 8;

// Создать структуру BITMAPINFO, описывающую растр int iSize = sizeof(BITMAPINFO) + iCIrTabEntries * sizeof(RGBQUAD) ;

BITMAPINFO* pBMI = (BITMAPINFO*) new BYTE[iSize];

memsetfpBMI, 0, iSize);

pBMI-»bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

pBMI-»bmiHeader.biWidth = iWidth;

pBMI-»bmiHeader.biHeight = iHeight;

pBMI-»bmiHeader. biPlanes = 1;

pBMI-»bmiHeader.biBitCount = iBitCount;

pBMI-»bmiHeader.biClrUsed = iCIrTabEntries;

HOC hdcScreen = ::GetDC(NULL) ;

// Создать цветовую таблицу, если необходимо if (iCIrTabEntries » 0) (

ASSERT(iClrTabEntries«= 256);

PALETTEENTRY ре[256];

Покадровая съемка ''illi' 257

ASSERT(m_pPalette) ;

m_pPalette-»GetInterface () -»GetEntries (0, 0,

iCIrTabEntries, pe) ;

for (int i = 0; i « iCIrTabEntries; i++) ( pBMI-»bmiColors [i] .rgbRed = pe[i].peRed;

pBMI-»bmiColors [i] .rgbGreen = pe[i].peGreen;

pBMI-»bmiColors [i] .rgbBlue = pe[i].peBlue;

} >

// Создать DIB-секцию, размер которой

// совпадает с размером вторичного буфера

BYTE* pBits = NULL;

HBITMAP hBmp = ::CreateDIBSection(hdcScreen, pBMI,

DIB_RGB_COLORS, (VOID**)&pBits, NULL, 0);

::ReleaseDC(NULL, hdcScreen);

if (!hBmp) { delete pBMI;

return NULL;

}

ASSERT(pBits) ;

// Скопировать графические данные на поверхность DIB

int iDIBScan =

( ( (pBMI-»bmiHeader.biWidth

* pBMI-»bmiHeader.biBitCount) + 31) & -31) »» 3;

int iSurfScan = ds.lPitch;

BYTE* pDIBLine = pBits + (iHeight - 1) * iDIBScan;

BYTE* pSurfLine = (BYTE*)ds.IpSurface;



// Сдвигать вниз до тех пор, пока младший байт источника //не совпадет с младшим байтом приемника. // Сдвигать снова, пока не будет достигнута точность // в 5 бит. DWORD dwRShift = O.DWORD dwGShift = 0;

DWORD dwBShift = O.DWORD dwNotMask;

if ((ds.ddpfPixelFormat.dwFlags & DDPF RGB) &&

(iBitCount »= 16)) {

if (iBitCount == 16) { dwNotMask = OxFFFFFFEO;

/b> Глава 11. Клипы

} else {

dwNotMask = OxFFFFFFOO;

} DWORD dwMask = ds.ddpfPixelFormat.dwRBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) {

dwRShift++;

dwMask = dwMask »» 1;

} while ((dwMask & dwNotMask) != 0) {

dwRShift++;

dwMask = dwMask »» 1;

} dwMask = ds.ddpfPixelFormat.dwGBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) (

dwGShift++;

dwMask = dwMask »» 1;

) while ((dwMask & dwNotMask) != 0) {

dwGShift++;

dwMask = dwMask »» 1;

}

dwMask = ds.ddpfPixelFormat.dwBBitMask;

ASSERT(dwMask) ;

while ((dwMask & 0х01) == 0) {

dwBShift++;

dwMask = dwMask »» 1;

) while ((dwMask & dwNotMask) != 0) {

dwBShift++;

dwMask = dwMask »» 1;

} i

// Снова заблокировать поверхность // для получения графических данных m_hr = iS-»Lock(NULL,

Sds,

DDLOCK_SURFACEMEMORYPTR I DDLOCK_WAIT,

NULL) ;

ASSERT(m_hr == DD_OK) ;

for (int у = 0; у « iHeight; y++) { switch (iBitCount) { case 8: {

BYTE* pDIBPix = pDIBLine;

BYTE* pSurfPix = pSurfLine;

for (int x = 0; x « iWidth; x++) {*pDIBPix++ =

Покадровая съемка 'Ч^Щ 259

// *pSurfPix++;

} } break;

case 16: (

WORD* pDIBPix = (WORD*)pDIBLine;

WORD* pSurfPix = (WORD*)pSurfLine;

WORD r, g, b;

// (int x = 0; x « iWidth; x++) ( r = (*pSurfPix & (WORD)

ds.ddpfPixel Format.dwRBitMask) »» dwRShift;

g = (*pSurfPix & (WORD)

ds.ddpfPixelFormat.dwGBitMask) »» dwGShift;

b = (*pSurfPix & (WORD)

ds.ddpfPixeiFormat.dwBBitMask) »» dwBShift;

*pDIBPix++ = ((r & OxIF) «« 10) ¦ ( (g & OxIF) «« 5) I (b S OxIF) ;

p3urfPix++;

} } break;

case 24: {

BYTE* pDIBPix = pDIBLine;

BYTE* pSurfPix = pSurfLine;



for (int x = 0; x « iWidth; x++) (

// ВНИМАНИЕ: Предполагается, что RGB-маски // поверхности и DIB-секции совпадают, что // на самом деле не всегда справедливо. // Нам следовало бы рассматривать маску // ds.ddpfPixelFormat.dwRGBBitMask.

*pDIBPix++ = *pSurfPix++;

*pDIBPix++ = *pSurfPix++;

*pDIBPix++ = *pSurfPix++;

} } break;

default:

// Мы не должны сюда попасть

break;

} pDIBLine -= iDIBScan;

pSurfLine += iSurfScan;

}

// Разблокировать буфер

m hr = m pBackBuffer-»Get!nterface()

»Unlock(ds.lpSurface) ;

/b> ill5' Глава 11. Клипы

// Проверить, возвращен ли информационный // заголовок растра if (ppBMI) {

*ppBMI = pBMI;

} else {

delete pBMI;

}

// Проверить, возвращен ли указатель //на графические данные растра if (ppBits) *ppBit3- = pBits;

// Вернуть логический номер растра return hBmp;

}

Если объем кода начисто отбил у вас охоту снимать клипы, вспомните о том, что функция уже написана! Вам остается только вызвать ее. Подробное описание ее работы выходит за рамки главы и даже всей книги в целом. Я написал эту функцию, руководствуясь документацией по непосредственному режиму Direct3D и некоторыми примерами, входящими в комплект DirectX 2 SDK. 0 непосредственном режиме подробнее рассказывается в главе 13.

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

Просмотр клипа

Просмотреть записанную последовательность кадров относительно просто. Если вы знакомились с исходным текстом функции Grablmage, то эта задача может показаться вам тривиальной. Тем не менее существуют детали, которые заметно влияют на скорость работы. Перед тем как воспроизводить набор кадров, мы вызываем функцию CMovie::Stop, чтобы остановить процесс записи. Возможно, это покажется вам несущественным, однако в данной функции имеется одна исключительно важная строка (удастся ли вам найти ее?):

void CMovie::Stop()

(

if (m Displaying) {

m bIsPlaying = FALSE;



// Заново отобразить цвета палитры

m_Take.Optimize() ;

} if (m_b!sRecording) (

m_bIsRecording = FALSE;

}

Покадровая съемка <:^^; 261

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

Дело в том, что идентичная палитра очень цохожа на исходную, а в некоторых случаях они будут совпадать. Единственные цвета палитры, которые могут измениться, — 20 зарезервированных системных цветов (10 в начале и 10 в конце палитры). Эти цвета не используются механизмом визуализации, а их изменение не влияет на вид изображения. В любом случае, мелкие детали вряд ли заметно отразятся на качестве изображения. Возможно, мое объяснение вас не убеждает;

меня это вовсе не удивляет. Более подробные разъяснения затронутой деликатной темы можно найти в книге «Animation Techniques in Win32». Я предлагаю вам самостоятельно просмотреть код функции Optimize на досуге.

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

BOOL CMovie::Update() {

if (m_bIsPlaying) {

// Вывести следующий кадр if (m_Take.IsEmptyO) {

m_bIsPlaying = FALSE;

return FALSE;



}

// Найти текущий кадр POSITION pos = m_Take.FindIndex(m_iCurFrame);

if (!pos) {

m_bIsPlaying = FALSE;

return FALSE;

}

CMovieFrame* pFrame = m_Take.GetAt(pos);

ASSERT(pFrame) ;

// Получить текущий контекст устройства (DC) ASSERT(m_p3dWnd) ;

CDC* pdc = m_p3dWnd-»GetDC() ;

// Задать палитру 262 ЩЦ1 Глава 11. Клипы

CPalette* pOldPal ° NULL;

CPalette* pPal = m_Take.GetPalette();

if (pPal) {

pOldPal = pdc-»SelectPalette(pPal, FALSE);

pdc-»RealizePalette () ;

}

// Создать совместимый DC для растра CDC dcMem;

dcMem.CreateCompatibleDC(pdc);

// Выбрать палитру в совместимом DC CPalette* pOldMemPal = NULL;

if (pPal) {

pOldMemPal = dcMem.SelectPalette(pPal, FALSE);

dcMem.RealizePalette() ;

}

// Выбрать растр в совместимом DC

HBITMAP hOldBmp = (HBITMAP) ::SelectObject(dcMem, pFrame-»m hBmp) ;

// Скопировать растр в окно pdc-»BitBlt (0, О,

m_Take.GetWidth(), m_Take.GetHeight(),

SidcMem,

0, 0,

SRCCOPY) ;

// Восстановить прежнее состояние DC if (pOldMemPal) {

dcMem.SelectPalette(pOldMemPal, FALSE) ;

} ::SelectObject(dcMem, hOldBmp);

if (pOldPal) (

pdc-»SelectPalette (pOldPal, FALSE) ;

} m_p3dWnd-»ReleaseDC(pdc) ;

m_iCurFrame++;

return TRUE; // Остальные кадры

}

Покадровая съемка ''''Щ 263

Последовательность действий в приведенном выше фрагменте такова:

1. Найти следующий выводимый кадр.

2. Получить контекст устройства (DC) для окна.

3. Выбрать палитру в DC окна.

4. Создать в памяти совместимый DC.

5. Выбрать палитру в совместимом DC.

6. Выбрать логический номер растра в объекте CMovieFrame и поместить его в совместимый DC.

7. Вызвать функцию BitBIt для копирования изображения из совместимого в оконный DC.

8. Восстановить состояние DC и освободить их.

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

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



Наблюдательный читатель может спросить, почему мы пользуемся BitBIt — ведь в нашем распоряжении имеются функции DirectDraw, и вообще мы работаем с поверхностью DirectDraw. Разумеется, мы можем повернуть вспять процесс сохранения кадра и снова отправить изображение на поверхность DirectDraw с тем же результатом. Подобные вещи обычно называются «упражнениями для самостоятельной работы». Почему? Взгляните на огромный объем кода для сохранения кадра на стр. 256 и представьте себе, что вам нужно сделать то же самое в противоположном направлении. Теперь вы понимаете, почему этим придется заниматься вам, а не мне?! На самом деле конкретный способ выполнения данной задачи не имеет особого значения, и я выбрал то, что кажется мне привычным. Разумеется, вы можете попробовать сделать это по-другому.

Стоит ли игра свеч?

Подведем итог всему, что мы узнали к настоящему моменту: программа получилась довольно большой, и к тому же она использует много памяти. Это — нехороший признак. Если учесть, что свободной памяти обычно не хватает для хранения всего клипа (без выгрузки в файл), в большинстве случаев просмотр будет замедляться из-за работы с диском. Таким образом, методика покадровой съемки вовсе не идеальна. Однако мы можем сделать на этом пути еще один шаг, который стоит рассмотреть подробнее.

Создание AVI-файла

Мы можем взять список кадров и создать по нему AVI-файл, который может быть воспроизведен компонентами Microsoft Video for Windows практически на любой Windows-машине, независимо от того, установлен ли на ней DirectSD или нет. Если на вашем компьютере имеется устройство для сжатия/восстановления видеоинформации (CODEC), данные можно сжать (коэффициент сжатия зависит от CODEC). Клип, сжатый по стандарту MPEG (Motion Picture Expert Group),

/b> ДИГ Глава 11. Клипы

как правило, получается довольно маленьким. Функция, которую я собираюсь вам представить, не пользуется никакой методикой сжатия, а просто записывает последовательность кадров в AVI-файл. Для сжатия создаваемого ей файла придется написать дополнительный код или воспользоваться готовыми утилитами.



И снова я не стану подробно рассматривать представленный фрагмент, поскольку на это уйдет слишком много времени. Лучшая документация по Video for Windows содержится в Microsoft Development Library. Функция CMovie::Save, создающая AVI-файл по списку кадров, выглядит следующим образом:

BOOL CMovie::Save()

{

if (GetNumFrames() «= 0) return FALSE;

// Получить имя файла ASSERT(m_p3dWnd) ;

CFileDialog dig(FALSE,

"avi",

NULL,

OFN_OVERWRITEPROMPT,

"AVI Files (*.avi)¦*.avi¦¦",

m_p3dWnd) ;

if (dIg.DoModal() != IDOK) return FALSE;

// Открыть AVI-файл HRESULT hr;

PAVIFILE pfile = NULL;

hr = ::AVIFileOpen(Spfile,

dig.GetFileName(), OF_CREATE ¦ OF_WRITE, NULL) ;

if (FAILED(hr)) return FALSE;

// Создать видеопоток в файле PAVISTREAM pstream = NULL;

AVISTREAMINFO si;

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

si.fccType = streamtypeVIDEO;

si.fccHandler = mmioFOURCC('M','S','V,'C');

si.dwRate = 100; // Fps si.dwScale = 1;

si.dwLength = 0;

si.dwQuality = (DWORD) -1;

si.rcFrame.top = 0;

si.rcFrame.left = 0;

si.rcFrame.bottom = m Take.GetHeight() ;

si.rcFrame.right = m_Take.GetWidth() ;

strcpy(si.szName, "3dPlus Movie");

hr = ::AVIFileCreateStream(pfile, Spstream, &si);

ASSERT(SUCCEEDED(hr));

// Задать формат CMovieFrame* pFrame = m_Take.GetHead();

ASSERT(pFrame) ;

int iSize » sizeof(BITMAPINFOHEADER)

+

DIBColorEntries((BITMAPINFOHEADER*)(pFraine-

»m_pBMI))

* sizeof(RGBQUAD) ;

hr === : :AVIStreamSetFormat(pstream,

0,

pFrame-»m_pBMI,

iSize) ;

ASSERT(SUCCEEDED(hr));

// Записать кадры POSITION pos = m_Take.GetHeadPosition();

int iSample = 0;

while (pos) (

CMovieFrame* pFrame = m_Take.GetNext(pos);

BITMAPINFOHEADER* pBIH = (BITMAPINFOHEADER*)

& (pFrame-»m_pBMI-»bmiHeader) ;

int iBits = DIBStorageWidth(pBIH) * pBIH-»biHeight;

hr = ::AVIStreamWrite(pstream,

iSample, // Текущий кадр

1, // Всего один

pFrame-»m_pBits, // Графические данные

iBits, // Размер буфера

О,

NULL,

NULL) ;

ASSERT(SUCCEEDED(hr)) ;

iSample++;



}

// Освободить поток ::AVIStreamRelease(pstream);

// Освободить файл AVI : .-AVIFileRelease (pfile) ;

return TRUE;

}

В двух словах происходит следующее: мы создаем новый AVI-фаил и видеопоток в этом файле. Затем тип видеопогока задается с помощью структуры BITMAPINFOHEADER и кадры записываются в поток, после чего поток и файл закрываются. Файл можно просмотреть программой Windows 95 Media Viewer или ее аналогом.

9АА S^S^f Гпапа ЛЛ l^nurll.1

Запись отдельного кадра

Иногда требуется записать лишь один кадр. Зачем? Например, вы провели много времени за созданием великолепного трехмерного макета и хотите сохранить его внешний вид. Приложение Save на прилагаемом диске CD-ROM использует функцию сохранения изображения, приведенную на стр. 252, для записи отдельного кадра в формате DIB для Windows. Предлагаю самостоятельно рассмотреть код приложения Save, если оно вас интересует.

Запись данных объекта

Итак, покадровая запись клипа связана как с большим объемом кода, так и с расходом памяти, а скорость просмотра ограничивается параметрами обмена данными с жестким диском (сетью, дисководом CD-ROM и т. д.). Кроме того, записанные клипы неинтерактивны, что ограничивает их применение во многих приложениях. Если нам захочется исследовать трехмерные объекты в окне, стоит поискать другую методику.

В главе 6 мы уже создавали подвижные объекты, но их перемещение было ограничено круговой траекторией. Тогда я упомянул, что траектория вовсе не обязана быть круговой и что ей можно придать любую желаемую форму. Сейчас мы рассмотрим другой способ задания траектории, при котором запоминаются положение и ориентация объекта в нескольких точках траектории. Затем перемещение воспроизводится интерполяцией по записанным точкам, позволяющей построить гладкую траекторию. Чтобы выдержать единую терминологию с документацией по DirecQD, я буду называть такую методику созданием анимации. Попробовав ее на практике, вы убедитесь, что благодаря высокой производительности механизма визуализации она практически полностью вытесняет покадровую съемку.



Кватернионы

Для задания поворотов в анимационных последовательностях механизм визуализации Direct3D использует кватернионы. Хочу немедленно оправдаться перед читателями и заявить, что перед созданием приложения Movie для этой главы я понятия не имел о том, что же такое кватернион. После знакомства с многочисленными справочниками могу сказать, что кватернион — хитроумная математическая конструкция, которая проделывает всякие интересные штуки по очень малым исходным данным. Объяснить ее работу простым английским языком совершенно невозможно, хотя это не совсем справедливо, поскольку изобретатель кватернионов, В. Р. Гамильтон (W. R. Hamilton), довольно много написал о них в 1843 году для Ирландской Королевской Академии. Однако специально для читателя я все же приведу более строгое определение. Кватернионом называется математический инструмент для описания поворотов объекта в пространстве с использованием минимального количества переменных. В нашем трехмерном мире кватернион определяется всего четырьмя переменными. Итоговый поворот объекта может быть описан с помощью кватерниона, являющегося произведением всех кватернионов, описывающих отдельные повороты объекта. Короче говоря, кватернионы позволяют с высокой эффективностью описывать повороты.

Я довольно долго пытался добиться от класса C3dQuaternion нужного поведения. Тем не менее мне так и не удалось привести его в рабочее состояние, а сроки

Запись ляиных г»(тьйитя ''^Ш 2&7

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

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



Запись траектории объекта

Я решил, что при отсутствии кватернионов для записи состояния объекта CSdShape в макете необходимо сохранить его положение, направление и верхний вектор (для задания вектора направления и верхнего вектора необходимо шесть переменных вместо четырех для кватернионов, так что здесь имеется некоторая избыточность). Для хранения данных объекта я создал класс C3dAnimKey:

class C3dAnimKey : public C3d0bject

(

public:

C3dAnimKey(double time,

const D3DVECTOR& pos, const D3DVECTOR& dir, const D3DVECTOR& up) : m_vPos(pos), m_vDir(dir), m vUp(up), m_dTime(time)

t

}

public:

C3dVector m vPos;

C3dVector m vDir;

C3dVector m_vUp;

double m_dTime;

};

Из листинга видно, что объект класса C3dAnimKey представляет собой структуру, которая инициализируется в несложном конструкторе. Анимация строится на основе класса MFC CObjectList и состоит из списка объектов C3dAnimKey, указателя на фрейм перемещаемой фигуры и переменной, содержащей текущее время перемещения по траектории. Единицы времени выбираются произвольно. Мы запоминаем состояние объекта в отдельных точках в заданный момент времени. Точки заносятся в список в хронологическом порядке, а воспроизводящая

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

ПРИМЕЧАНИЕ

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

Запись происходит следующим образом: мы очищаем список, размещаем объект в макете и добавляем ключевую точку для каждого состояния объекта, включаемого в анимацию. Во время воспроизведения объект перемещается вдоль своей траектории с приращением в 0,1 единицы времени. Для простоты запись состояния объекта осуществляется с временными интервалами в 1,0 единицы, так что в процессе записи нельзя с полной уверенностью предсказать поведение объекта при воспроизведении. В приложении Movie имеется команда Edit ] Demonstration, которая записывает движение объекта и затем воспроизводит его в неограниченном цикле.



Чтобы упростить код приложения, я включил в него глобальный объект C3dAnimation, который позволяет записывать состояние только одного объекта.

Программная реализация настолько проста, что ее даже не стоит приводить здесь, однако для полноты картины мы все же кратко рассмотрим ключевые функции класса C3dAnimation. Давайте для начала посмотрим, как новая ключевая точка заносится в список. Предполагается, что фигура для анимации была выбрана ранее, при вызове функции C3dAnimation::Attach:

BOOL C3dAnimation::AddKey(double time) {

if (!m_pFrame) return FALSE;

// Определить положение и ориентацию фрейма C3dVector p, d, u;

m pFrame-»GetPosition (p) ;

m_pFrame-»GetDirection (d, u) ;

// Создать новый ключ C3dAnimKey* pKey = new C3dAnimKey(time, p, d, u);

// Внести ключ в список return AddKey(pKey);

}

Функция определяет текущее положение и ориентацию фрейма и создает по ним новый объект C3dAnimKey. Затем ключ вносится в список:

BOOL C3dAnimation::AddKey(C3dAnimKey* pNewKey) {

if (!pNewKey) return FALSE;

.'^япиг^ nauu^iy itfvi-ei/Ta ^IRK: ^AO

// Обновить текущее время m dCurTime = pNewKey-»m_dTime;

// Внести ключ в список if (IsEmptyO) {

AddTail(pNewKey) ;

return TRUE;

}

// Перебрать элементы списка в обратном направлении

POSITION роз = GetTailPositionO ;

ASSERT(pos) ;

do {

POSITION thispos = pos;

C3dAnimKey* pKey = (C3dAnimKey*) GetPrev(pos);

if (pKey-»m dTime «= pNewKey-»m_dTime) { // Вставить новый ключ после текущего InsertAfter(thispos, pNewKey);

return TRUE;

) } while (pos);

// Внести новый ключ в начало списка AddHead(pNewKey) ;

return TRUE;

}

Примечание

Если вы никогда не пользовались классом MFC СОЫ-ist, то можете растеряться при виде функций GetNext (см. выше) и GetPrev (см. ниже). Эти функции извлекают данные по ключу, после чего увеличивают или уменьшают значение ключевой переменной. Я ненавижу их, поскольку их использование противоречит здравому смыслу, однако в целом класс СОЫ-ist достаточно удобен — во всяком случае, до тех пор, пока я не разберусь с шаблонами и стандартными библиотеками C++ и не разработаю более удачное решение.



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

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

/b> Глава 11. Клипы

BOOL CMainFrame::Update() {

if (m bPlayAnimation) {

double 1 = m_Anim.GetLength();

double t = in_Anim.GetCurTime() ;

t += 0.1;

if (t » 1) (

if (m_bLoopAnim) {

m_Anim.SetTime(0.0) ;

} else {

m_bPlayAnimation = FALSE;

Status("End of animation");

} } else (

m Anim.SetTime (t);

}

i

Счетчик времени увеличивается с интервалом в 0,1 единицы. В конце анимация останавливается или запускается снова, в зависимости от состояния флага m_bLoopAnim. Ниже приведена функция C3dAnimationTime::SetTime, задающая состояние объекта для нужного момента времени:

BOOL C3dAnimation::SetTime(double time) (

m_dCurTime = time;

if (!m_pFrame) return FALSE;

if (IsEmptyO) return FALSE;

// Перебрать элементы списка в поисках пары ключей, // между которыми лежит заданная величина // (или точного совпадения) POSITION pos = GetHeadPositionO;

ASSERT(pos) ;

C3dAnimKey* pBefore = (C3dAnimKey*) GetNext(pos) ;

ASSERT(pBefore) ;

if (pBefore-»m_dTime » time) return FALSE;

// Слишком рано, // ключ отсутствует

C3dAnimKey* pAfter = NULL;

while (pos) {

pAfter = (C3dAnimKey*) GetNext(pos) ;

ASSERT(pAfter) ;

if ( (pBefore-»m_dTime «= time) && (pAfter-»m_dTime »= time) ) break;

pBefore = pAfter;

// Вычислить интерполированные значения C3dVector p, d, u;

double dt;

if (pAfter != NULL) {

dt = pAfter-»m dTime — pBefore-»m_dTime;

} else {

dt = pBefore-»m_dTime;

} if ((pAfter == NULL) ¦¦ (dt ==0)) {

p = pBefore-»m_vPos;

d = pBefore-»m_vDir;

u = pBefore-»m_vUp;

} else {

double r = (time — pBefore-»m_dTime) / dt;

p = pBefore-»m_vPos + (pAfter-»m_vPos — pBefore-



»m_vPos) * r;

d =s pBefore-»m_vDir + (pAfter-»m_vDir — pBefore-

»m vDir) * r;

u = pBefore-»m_vUp + (pAfter-»m_vUp — pBefore-

»iri_vUp) * r;

}

// Задать новое положение и направление m pFrame-»SetPosition (p) ;

m_pFrame-»SetDirection (d, u) ;

return TRUE;

}

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

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

Упражнения для вас

Погодите, не торопитесь переходить к следующей главе! Я подготовил несколько задач для самостоятельной работы. Однако перед тем, как приступать к ним, я

ЧТ> ИИЮ' Гг.-.ОЗ 1-1 1/П..П,-,

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

1. Разработайте для анимаций класс на базе списка и создайте макет с несколькими объектами, движущимися одновременно. Например, постройте модель солнечной системы.

2. Попробуйте закрепить анимационную последовательность непосредственно за объектом класса, производного от CSdShape, чтобы каждый объект содержал свою собственную траекторию. Снова создайте макет для задачи 1. Будет ли такой способ лучше, чем решение задачи I?

3. Модифицируйте функцию C3dAnimation::SetTime так, чтобы вместо линейной интерполяции в ней использовались сплайны.

4. Сравните методики покадровой съемки и точечной анимации. Существуют ли приложения, в которых одна из них обладает несомненными преимуществами?


Содержание раздела