Анимацией вершин называется изменение местоположения одной или нескольких вершин сетки. Реализовать этот вид анимации достаточно просто, гораздо труднее решить когда и куда перемещать вершины.
Анимация вершин может быть выполнена как с использованием интерфейса Direct3DRMMeshBuilder, так и с помощью интерфейса Direct3DRMMesh. Поскольку применение интерфейса Direct3DRMMeshBuilder приводит к снижению быстродействия и увеличению объема используемой памяти, в данной главе мы будем использовать исключительно интерфейс Direct3DRMMesh.
В приложении MorphPlay есть четыре функции для реализации возможности вращения сетки: OnLButtonDown(), OnLButtonUp(), UpdateDrag() и OnIdle(). Давайте сначала взглянем на код функции OnLButtonDown():
void MorphPlayWin::OnLButtonDown(UINT nFlags, CPoint point) { if (!drag) { drag = TRUE; last_x = GetMouseX(); last_y = GetMouseY(); ShowCursor(FALSE); SetCapture(); } MorphWin::OnLButtonDown(nFlags, point); }Функция начинает новую операцию перетаскивания, если только она уже не начата. Переменная drag используется для контроля текущего состояния операции перетаскивания. В начале операции перетаскивания сохраняется текущая позиция указателя мыши, сам указатель убирается с экрана, и мышь захватывается приложением с помощью функции MFC SetCapture(). После вызова функции SetCapture() все поступающие от мыши сообщения будут направляться приложению, которое захватило мышь, независимо от того, в каком месте экрана находится указатель мыши.
Функция OnLButtonUp() выглядит следующим образом:
void MorphPlayWin::OnLButtonUp(UINT nFlags, CPoint point) { if (drag) { end_drag = TRUE; ReleaseCapture(); ShowCursor(TRUE); } MorphWin::OnLButtonUp(nFlags, point); }Сначала функция OnLButtonUp() проверяет, существует ли начатая операция перетаскивания. Если да, то переменной end_drag присваивается значение TRUE, что указывает на необходимость прекращения операции перетаскивания. Затем приложение освобождает мышь с помощью функции ReleaseCapture() и разрешает отображение указателя мыши.
Функция обратного вызова UpdateDrag() отвечает за вращение трансформируемой сетки при ее перетаскивании. Вот как выглядит код функции:
void MorphPlayWin::UpdateDrag(LPDIRECT3DRMFRAME frame, void*, D3DVALUE) { if (drag) { double delta_x, delta_y; int x = GetMouseX(); int y = GetMouseY(); delta_x = x - last_x; delta_y = y - last_y; last_x = x; last_y = y; double delta_r = sqrt(delta_x * delta_x + delta_y * delta_y); double radius = 50; double denom; denom = sqrt(radius * radius + delta_r * delta_r); if (!(delta_r == 0 || denom == 0)) frame->SetRotation(0, D3DDivide(D3DVAL((float)-delta_y), D3DVAL((float)delta_r)), D3DDivide(D3DVAL((float)-delta_x), D3DVAL((float)delta_r)), D3DVAL(0.0), D3DDivide(D3DVAL((float)delta_r), D3DVAL((float)denom))); }Функция UpdateDrag() преобразует данные о перемещении мыши (сохраненные в переменных x, y, last_x и last_y) для вычисления вектора вращения сетки. Новые атрибуты вращения назначаются с помощью функции SetRotation() интерфейса Direct3DRMFrame.
В конце приведем код функции OnIdle(), которая используется для предотвращения беспорядочного вращения сетки в момент операции перетаскивания.
void MorphPlayWin::OnIdle(LONG) { if (drag && frame) frame->SetRotation(0, D3DVAL(0.0), D3DVAL(1.0), D3DVAL(0.0), D3DVAL(0.0)); }В приложении Cube2 меню Render немного изменено, по сравнению с его обычным вариантом, поскольку каждая группа граней может иметь свои собственные параметры визуализации. По этой причине меню Render состоит из двух подменю — по одному для каждой группы граней сетки. Структура меню показана на рис. 8.3.
Рис. 8.3. Меню Render приложения Cube2
Для реализации меню Render приложения Cube2 требуется двенадцать функций, но все они отличаются друг от друга весьма незначительно. Ниже приведен пример одной из функций, вызываемых при выборе одного из пунктов меню:
void Cube2Win::OnRenderGroup1Wireframe() { if (mesh) mesh->SetGroupQuality(group1, D3DRMRENDER_WIREFRAME); }Данная функция отвечает за работу пункта меню Render|Group1|Wireframe и использует функцию SetGroupQuality() интерфейса Direct3DRMMesh чтобы установить каркасный метод визуализации. Обратите внимание, что в первом аргументе функции SetGroupQuality() передается идентификатор группы граней group1.
Приведем также пример функции, устанавливающей флажок рядом с пунктом меню, соответствующим текущему методу визуализации группы граней сетки:
void Cube2Win::OnUpdateRenderGroup1Flat(CCmdUI* pCmdUI) { if (mesh) { D3DRMRENDERQUALITY meshquality = mesh->GetGroupQuality(group1); pCmdUI->SetCheck(meshquality == D3DRMRENDER_FLAT); } }Данная функция предназначена для плоского режима визуализации первой группы граней сетки.
Оставшиеся функции класса CubeWin предназначены для реализации меню Render. Их код приведен ниже:
void CubeWin::OnRenderWireframe() { if (mesh) mesh->SetGroupQuality(group, D3DRMRENDER_WIREFRAME); } void CubeWin::OnRenderFlat() { if (mesh) mesh->SetGroupQuality(group, D3DRMRENDER_FLAT); } void CubeWin::OnRenderGouraud() { if (mesh) mesh->SetGroupQuality(group, D3DRMRENDER_GOURAUD); } void CubeWin::OnUpdateRenderWireframe(CCmdUI* pCmdUI) { if (mesh) { D3DRMRENDERQUALITY meshquality = mesh->GetGroupQuality(group); pCmdUI->SetCheck(meshquality == D3DRMRENDER_WIREFRAME); } } void CubeWin::OnUpdateRenderFlat(CCmdUI* pCmdUI) { if (mesh) { D3DRMRENDERQUALITY meshquality = mesh->GetGroupQuality(group); pCmdUI->SetCheck(meshquality == D3DRMRENDER_FLAT); } } void CubeWin::OnUpdateRenderGouraud(CCmdUI* pCmdUI) { if (mesh) { D3DRMRENDERQUALITY meshquality = mesh->GetGroupQuality(group); pCmdUI->SetCheck(meshquality == D3DRMRENDER_GOURAUD); } }Обратите внимание, что для получения и изменения параметров визуализации сетки используется идентификатор групп граней сетки group.
Кроме того, вы можете заметить, что при выборе команд меню Render практически не заметна разница между плоским методом визуализации и визуализацией по методу Гуро. Обычно визуализация по методу Гуро смягчает изображение граней и углов. Однако, при выборе метода Гуро в приложении Cube грани сетки остаются ясно различимыми. Это происходит потому, что нормали, которые мы использовали при создании сетки вычисялись не тем способом, который использует для вычисления нормалей интерфейс Direct3DRMMeshBuilder. Используемые в приложении Cube нормали перпендикулярны той грани, частью которой является данная вершина. Чтобы вычислить нормаль с учетом всех граней, сходящихся в данной вершине, следует использовать функцию GenerateNormals() интерфейса Direct3DRMMeshBuilder.
Код функции CreateScene() из приложения Cube2 приведен в листинге8.2.
Листинг 8.2. Функция Cube2Win::CreateScene() |
BOOL Cube2Win::CreateScene() { //---------- СЕТКА -------- d3drm->CreateMesh(&mesh); mesh->AddGroup(12, 3, 4, vertorder, &group1); mesh->AddGroup(12, 3, 4, vertorder, &group2); mesh->SetVertices(group1, 0, 12, vertexlist); mesh->SetVertices(group2, 0, 12, vertexlist + 12); mesh->Translate(D3DVALUE(-0.5), D3DVALUE(-0.5), D3DVALUE(-0.5)); mesh->Scale(D3DVALUE(15), D3DVALUE(15), D3DVALUE(15)); //--------- ТЕКСТУРА ---------- HRSRC texture_id = FindResource(NULL, MAKEINTRESOURCE(IDR_WIN95TEXTURE), "TEXTURE"); LPDIRECT3DRMTEXTURE texture; d3drm->LoadTextureFromResource(texture_id, &texture); mesh->SetGroupTexture(group1, texture); mesh->SetGroupMapping(group1, D3DRMMAP_PERSPCORRECT); mesh->SetGroupTexture(group2, texture); mesh->SetGroupMapping(group2, D3DRMMAP_PERSPCORRECT); texture->Release(); texture = 0; //---------- ФРЕЙМ ---------- LPDIRECT3DRMFRAME meshframe; d3drm->CreateFrame(scene, &meshframe); meshframe->AddVisual(mesh); meshframe->SetRotation(scene, D3DVALUE(0), D3DVALUE(1), D3DVALUE(0), D3DVALUE(-.05)); static CallbackData cbdata; cbdata.mesh = mesh; cbdata.group1 = group1; cbdata.group2 = group2; meshframe->AddMoveCallback(UpdateCube, &cbdata); meshframe->AddMoveCallback(UpdateColors, &cbdata); meshframe->Release(); meshframe = 0; // -------- СВЕТ -------- LPDIRECT3DRMLIGHT dlight, alight; d3drm->CreateLightRGB(D3DRMLIGHT_DIRECTIONAL, D3DVALUE(1.00), D3DVALUE(1.00), D3DVALUE(1.00), &dlight); d3drm->CreateLightRGB(D3DRMLIGHT_AMBIENT, D3DVALUE(0.50), D3DVALUE(0.50), D3DVALUE(0.50), &alight); LPDIRECT3DRMFRAME lightframe; d3drm->CreateFrame(scene, &lightframe); lightframe->SetOrientation(scene, D3DVALUE(0), D3DVALUE(-1), D3DVALUE(5), D3DVALUE(0), D3DVALUE(1), D3DVALUE(0)); lightframe->AddLight(dlight); lightframe->AddLight(alight); dlight->Release(); dlight = 0; alight->Release(); alight = 0; lightframe->Release(); lightframe = 0; //------ КАМЕРА ---------- d3drm->CreateFrame(scene, &camera); camera->SetPosition(scene, D3DVALUE(0), D3DVALUE(0), D3DVALUE(-50)); d3drm->CreateViewport(device, camera, 0, 0, device->GetWidth(), device->GetHeight(), &viewport); return TRUE; } |
Функция CreateScene() выполняет следующие действия:
Создание сетки с помощью интерфейса Direct3DRMMesh. Сетка состоит из двух различных групп граней. Создание текстуры для сетки. Создание фрейма для размещения сетки и установка функции обратного вызова. Создание двух источников света. Создание порта просмотра.На первом этапе выполняется создание сетки:
d3drm->CreateMesh(&mesh); mesh->AddGroup(12, 3, 4, vertorder, &group1); mesh->AddGroup(12, 3, 4, vertorder, &group2); mesh->SetVertices(group1, 0, 12, vertexlist); mesh->SetVertices(group2, 0, 12, vertexlist + 12); mesh->Translate(D3DVALUE(-0.5), D3DVALUE(-0.5), D3DVALUE(-0.5)); mesh->Scale(D3DVALUE(15), D3DVALUE(15), D3DVALUE(15));Сначала для инициализации указателя mesh вызывается функция CreateMesh() интерфейса Direct3DRM. Затем к пустой сетке добавляются две группы граней, каждая из которых создается функцией AddGroup(). Каждая группа включает 12 вершин: 3 грани по 4 вершины в каждой. В качестве третьего аргумента функции AddGroup() используется массив vertorder. В приложении Cube массив vertorder содержал 24 элемента; в приложении Cube2 в этом массиве только 12 элементов:
unsigned vertorder[] = { 0,1,2,3,4,5,6,7,8,9,10,11 };Последний аргумент функции AddGroup() является адресом идентификатора групп граней сетки. Функция AddGroup() назначает каждой группе граней уникальное значение идентификатора.
Теперь, чтобы присвоить значения вершинам каждой группы, воспользуемся функцией SetVertices(). Первым параметром этой функции является идентификатор группы вершин. Второй аргумент — это индекс первой изменяемой вершины. Третий аргумент — число вершин, которым будут присвоены новые значения. Последний, четвертый аргумент является массивом структур D3DRMVERTEX. Обратите внимание, что для инициализации двух групп граней используется один и тот же массив структур. Во втором вызове функции SetVertices() указано смещение, приводящее к тому, что для инициализации второй группы граней используется вторая половина массива.
Затем выполняется вызов функции Translate() интерфейса Direct3DRMMesh и функции Scale(). В отличие от функций AddGroup() и SetVertices(), функции Translate() и Scale() модифицируют все входящие в сетку группы. Вызов этих функций ничем не отличается от использованного в приложении Cube.
На втором этапе выполняется создание текстуры и ее наложение на обе группы граней сетки:
HRSRC texture_id = FindResource(NULL, MAKEINTRESOURCE(IDR_WIN95TEXTURE), "TEXTURE"); LPDIRECT3DRMTEXTURE texture; d3drm->LoadTextureFromResource(texture_id, &texture); mesh->SetGroupTexture(group1, texture); mesh->SetGroupMapping(group1, D3DRMMAP_PERSPCORRECT); mesh->SetGroupTexture(group2, texture); mesh->SetGroupMapping(group2, D3DRMMAP_PERSPCORRECT); texture->Release(); texture = 0;На третьем этапе создается фрейм для сетки и устанавливаются две функции обратного вызова:
LPDIRECT3DRMFRAME meshframe; d3drm->CreateFrame(scene, &meshframe); meshframe->AddVisual(mesh); meshframe->SetRotation(scene, D3DVALUE(0), D3DVALUE(1), D3DVALUE(0), D3DVALUE(-.05)); static CallbackData cbdata; cbdata.mesh = mesh; cbdata.group1 = group1; cbdata.group2 = group2; meshframe->AddMoveCallback(UpdateCube, &cbdata); meshframe->AddMoveCallback(UpdateColors, &cbdata); meshframe->Release(); meshframe = 0;Фрейм создается функцией CreateFrame() интерфейса Direct3DRM и используется для размещения сетки. После этого фрейму назначаются атрибуты вращения.
Затем инициализируется экземпляр статической структуры CallbackData. Структура модифицирована таким образом, чтобы в ней можно было хранить идентификаторы обеих групп граней сетки.
Функция AddMoveCallback() интерфейса Direct3DRMFrame используется для установки двух функций обратного вызова: UpdateCube() и UpdateColors(). Каждая функция обратного вызова будет получать при вызове указатель на объявленную ранее статическую структуру данных. Функция UpdateCube() выполняет анимацию вершин для обоих групп граней сетки. Функция UpdateColors() выполняет анимацию цвета второй группы граней.
На двух заключительных этапах выполняется создание двух источников света и порта просмотра. В этой главе мы не будем обсуждать данные действия.
Функция UpdateColors() выполняет анимацию цветов для второй группы граней сетки. Код функции выглядит следующим образом:
void Cube2Win::UpdateColors(LPDIRECT3DRMFRAME, void* p, D3DVALUE) { CallbackData* data = (CallbackData*)p; static D3DVALUE clr = D3DVALUE(.5); static D3DVALUE inc = D3DVALUE(.2); clr += inc; if (clr < D3DVALUE(.3) || clr > D3DVALUE(1)) { inc = -inc; clr += inc; } data->mesh->SetGroupColorRGB(data->group2, clr, D3DVALUE(0), D3DVALUE(0)); }Переменная clr используется для вычисления и хранения текущего цвета группы граней сетки. Сразу после вычисления значения переменной clr, оно устанавливается с помощью функции SetGroupColorRGB() интерфейса Direct3DRMMesh. Анимируется только красная составляющая цвета группы граней сетки. Зеленая и синяя составляющая цвета всегда равны нулю.
Функция UpdateCube() выполняет две задачи: она осуществляет анимацию вершин и, кроме того, она периодически изменяет атрибуты вращения сетки.
void Cube2Win::UpdateCube(LPDIRECT3DRMFRAME frame, void* p, D3DVALUE) { CallbackData* data = (CallbackData*)p; static const D3DVALUE lim = D3DVALUE(5); static D3DVALUE control; static D3DVALUE inc = D3DVALUE(.25); static D3DRMVERTEX vert[12]; data->mesh->GetVertices(data->group1, 0, 12, vert); vert[0].position.x += inc; vert[0].position.y += inc; vert[0].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; data->mesh->SetVertices(data->group1, 0, 12, vert); data->mesh->GetVertices(data->group2, 0, 12, vert); vert[2].position.x += inc; vert[2].position.y += inc; vert[2].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; data->mesh->SetVertices(data->group2, 0, 12, vert); control += inc; if (control > lim || control < -lim) inc = -inc; static UINT delay; if (++delay < 20) return; delay = 0; LPDIRECT3DRMFRAME scene; frame->GetScene(&scene); D3DVECTOR spinvect; D3DRMVectorRandom(&spinvect); D3DVALUE spin = D3DDivide(rand() % 50 + 1, 400); frame->SetRotation(scene, spinvect.x, spinvect.y, spinvect.z, spin); }При изменении местоположения вершин сетки функция UpdateCube() использует следующие статические переменные:
static const D3DVALUE lim = D3DVALUE(5); static D3DVALUE control; static D3DVALUE inc = D3DVALUE(.25); static D3DRMVERTEX vert[12];Переменные lim, control и inc применяются для управления анимацией вершин. Массив vert используется для временного хранения данных каждой из групп граней сетки. Обратите внимание, что в данном случае массив состоит из 12 элементов, а в приложении Cube в массиве vert было 24 элемента.
В каждой из двух групп вершин сетки есть анимируемые вершины, поэтому функции GetVertices() и SetVertices() применяются для изменения параметров обоих групп вершин сетки:
data->mesh->GetVertices(data->group1, 0, 12, vert); vert[0].position.x += inc; vert[0].position.y += inc; vert[0].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; data->mesh->SetVertices(data->group1, 0, 12, vert); data->mesh->GetVertices(data->group2, 0, 12, vert); vert[2].position.x += inc; vert[2].position.y += inc; vert[2].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; data->mesh->SetVertices(data->group2, 0, 12, vert);Массив vert используется для хранения данных обоих групп вершин. Оставшаяся часть функции UpdateCube() полностью аналогична функции UpdateCube() приложения Cube.
Сцена приложения Cube создается в функции CreateScene(), код которой представлен в листинге8.1.
Листинг 8.1. Функция CubeWin::CreateScene() |
BOOL CubeWin::CreateScene() { // ------- СЕТКА -------- d3drm->CreateMesh(&mesh); mesh->AddGroup(24, 6, 4, vertorder, &group); mesh->SetVertices(group, 0, 24, vertexlist); mesh->Translate(D3DVALUE(-0.5), D3DVALUE(-0.5), D3DVALUE(-0.5)); mesh->Scale(D3DVALUE(12), D3DVALUE(12), D3DVALUE(12)); //-------- ТЕКСТУРА ------ HRSRC texture_id = FindResource(NULL, MAKEINTRESOURCE(IDR_WIN95TEXTURE), "TEXTURE"); LPDIRECT3DRMTEXTURE texture; d3drm->LoadTextureFromResource(texture_id, &texture); mesh->SetGroupTexture(group, texture); mesh->SetGroupMapping(group, D3DRMMAP_PERSPCORRECT); texture->Release(); texture = 0; //------- ФРЕЙМ -------- LPDIRECT3DRMFRAME frame; d3drm->CreateFrame(scene, &frame); frame->AddVisual(mesh); frame->SetRotation(scene, D3DVALUE(0), D3DVALUE(1), D3DVALUE(0), D3DVALUE(.04)); static CallbackData cbdata; cbdata.mesh = mesh; cbdata.group = group; frame->AddMoveCallback(UpdateCube, &cbdata); frame->Release(); frame = 0; // --------- СВЕТ -------- LPDIRECT3DRMLIGHT dlight; d3drm->CreateLightRGB(D3DRMLIGHT_DIRECTIONAL, D3DVALUE(1.00), D3DVALUE(1.00), D3DVALUE(1.00), &dlight); LPDIRECT3DRMLIGHT alight; d3drm->CreateLightRGB(D3DRMLIGHT_AMBIENT, D3DVALUE(0.50), D3DVALUE(0.50), D3DVALUE(0.50), &alight); LPDIRECT3DRMFRAME lightframe; d3drm->CreateFrame(scene, &lightframe); lightframe->SetOrientation(scene, D3DVALUE(0), D3DVALUE(-1), D3DVALUE(1), D3DVALUE(0), D3DVALUE(1), D3DVALUE(0)); lightframe->AddLight(dlight); lightframe->AddLight(alight); dlight->Release(); dlight = 0; alight->Release(); alight = 0; lightframe->Release(); lightframe = 0; //------ КАМЕРА ---------- d3drm->CreateFrame(scene, &camera); camera->SetPosition(scene, D3DVALUE(0), D3DVALUE(0), D3DVALUE(-50)); d3drm->CreateViewport(device, camera, 0, 0, device->GetWidth(), device->GetHeight(), &viewport); return TRUE; } |
Функция CreateScene() выполняет следующие действия:
Создание сетки куба. Создание текстуры и ее наложение на сетку. Создание фрейма для сетки и установка функции обратного вызова. Создание двух источников света. Создание порта просмотра.На первом этапе мы воспользуемся интерфейсом Direct3DRMMesh чтобы создать сетку куба. Давайте взглянем на код:
d3drm->CreateMesh(&mesh); mesh->AddGroup(24, 6, 4, vertorder, &group); mesh->SetVertices(group, 0, 24, vertexlist); mesh->Translate(D3DVALUE(-0.5), D3DVALUE(-0.5), D3DVALUE(-0.5)); mesh->Scale(D3DVALUE(12), D3DVALUE(12), D3DVALUE(12));Сперва для инициализации указателя mesh вызывается функция CreateMesh() интерфейса Direct3DRM. Обратите внимание, что ранее в тех приложениях, где использовался интерфейс Direct3DRMMesh, для создания сетки применялась функция CreateMesh() интерфейса Direct3DRMMeshBuilder, а не функция CreateMesh() интерфейса Direct3DRM. Создать сетку с помощью конструктора сеток очень легко, потому что можно использовать функцию Load() интерфейса Direct3DRMMeshBuilder чтобы загрузить сетку из файла. Поскольку мы используем функцию CreateMesh() интерфейса Direct3DRM, у нас будет создана пустая сетка.
Затем для инициализации сетки вызывается функция AddGroup() интерфейса Direct3DRMMesh. В результате будет создана группа граней сетки. Группой называется набор граней одной сетки, которыми можно управлять вместе как единой сущностью. Первым аргументом функции AddGroup() является количество вершин в группе. У нашего куба 24 вершины, поскольку он состоит из 6 граней у каждой из которых по 4 вершины. Второй аргумент функции AddGroup() задает количество граней в группе, а третий — количество вершин у каждой из граней. Четвертый аргумент функции AddGroup() — это массив индексов вершин. Определение массива vertorder, используемого в нашей программе в качестве четвертого аргумента, выглядит так:
unsigned vertorder[] = { 0,1,2,3,4,5,6,7,8,9,10,11, 12,13,14,15,16,17,18,19,20,21,22,23 };Данный массив задает порядок вершин. В нашем случае индексы отсортированы в простейшем из возможных, последовательном порядке. Когда сетка создается с нуля, как это делаем мы, применение непоследовательного порядка вершин не дает особых преимуществ. Однако в данных сеток, экспортируемых из программ трехмерного моделирования, таких как 3D Studio, вершины вряд ли будут отсортированы по порядку номеров.
Пятый и последний аргумент функции AddGroup() это указатель на член данных group. Функция AddGroup() инициализирует эту переменную, записывая в нее идентификатор, который в последующих вызовах функций будет применяться для определения интересующей нас группы граней сетки. Если сетка содержит только одну группу граней (как в нашем случае), в качестве идентификатора группы используется ноль.
Функция AddGroup() добавляет грани к сетке и устанавливает порядок вершин, но все данные граней имеют устанавливаемые по умолчанию значения. Значения всех координат и нормалей новых вершин равны нулю. Новые грани окрашены в белый цвет и на них не наложены никакие текстуры. Чтобы присвоить начальные значения вершинам, необходимо воспользоваться функцией SetVertices():
mesh->SetVertices(group, 0, 24, vertexlist);Функция SetVertices() присваивает значения координат, нормалей и позицию текстуры для одной или нескольких вершин группы граней сетки. Первый аргумент функции SetVertices() определяет модифицируемую группу граней сетки. Мы используем член данных group, который был инициализирован при вызове функции AddGroup(). Второй аргумент — это номер вершины с которой мы начнем изменения (индекс первой модифицируемой вершины). Третий аргумент — число модифицируемых вершин. Поскольку мы хотим задать значения для всех 24 вершин в группе граней сетки, используем значения 0 и 24. Четвертый аргумент является массивом структур D3DRMVERTEX в котором содержатся новые данные для вершин.
Перед тем, как взглянуть на массив vertexlist (используемый в качестве четвертого аргумента функции SetVertices()), давайте поговорим о структуре D3DRMVERTEX. Ее определение в Direct3D выглядит следующим образом:
typedef struct _D3DRMVERTEX { D3DVECTOR position; D3DVECTOR normal; D3DVALUE tu, tv; D3DCOLOR color; } D3DRMVERTEX;Вектор position используется чтобы задать местоположение вершины. Вектор normal задает вектор нормали для вершины. Поля tu и tv определяют координаты текстуры. Если на сетку будет наложена текстура, эти два значения определяют какая именно точка текстуры совпадет с вершиной. И, наконец, поле color задает цвет вершины.
Чтобы в нашей программе определить сетку куба, необходимо предоставить массив структур D3DRMVERTEX. Для упрощения этой задачи мы воспользуемся следующим макросом:
#define VERTEX(px,py,pz,nx,ny,nz,tu,tv) \ { { D3DVALUE(px),D3DVALUE(py),D3DVALUE(pz) }, \ { D3DVALUE(nx),D3DVALUE(ny),D3DVALUE(nz), }, \ D3DVALUE(tu),D3DVALUE(tv),D3DCOLOR(0) }Макрос получает восемь аргументов и создает из них одну структуру D3DRMVERTEX. Главная польза этого макроса в том, что он избавляет нас от необходимости загромождать объявление массива операциями приведения каждого поля структуры D3DRMVERTEX к типу D3DVALUE. Вот как выглядит определение массива вершин vertexlist, используемого в приложении Cube для инициализации сетки:
static D3DRMVERTEX vertexlist[]= { // левая грань VERTEX( 0,0,0, -1,0,0, 0,1 ), // вершина 0 VERTEX( 0,0,1, -1,0,0, 0,0 ), VERTEX( 0,1,1, -1,0,0, 1,0 ), VERTEX( 0,1,0, -1,0,0, 1,1 ), // правая грань VERTEX( 1,0,0, 1,0,0, 0,0 ), VERTEX( 1,1,0, 1,0,0, 1,0 ), VERTEX( 1,1,1, 1,0,0, 1,1 ), // вершина 6 VERTEX( 1,0,1, 1,0,0, 0,1 ), // передняя грань VERTEX( 0,0,0, 0,0,-1, 0,0 ), // вершина 8 VERTEX( 0,1,0, 0,0,-1, 1,0 ), VERTEX( 1,1,0, 0,0,-1, 1,1 ), VERTEX( 1,0,0, 0,0,-1, 0,1 ), // задняя грань VERTEX( 0,0,1, 0,0,1, 0,1 ), VERTEX( 1,0,1, 0,0,1, 0,0 ), VERTEX( 1,1,1, 0,0,1, 1,0 ), // вершина 14 VERTEX( 0,1,1, 0,0,1, 1,1 ), // верхняя грань VERTEX( 0,1,0, 0,1,0, 0,0 ), VERTEX( 0,1,1, 0,1,0, 1,0 ), VERTEX( 1,1,1, 0,1,0, 1,1 ), // вершина 18 VERTEX( 1,1,0, 0,1,0, 0,1 ), // нижняя грань VERTEX( 0,0,0, 0,-1,0, 0,0 ), // вершина 20 VERTEX( 1,0,0, 0,-1,0, 1,0 ), VERTEX( 1,0,1, 0,-1,0, 1,1 ), VERTEX( 0,0,1, 0,-1,0, 0,1 ), };В коде выделено шесть групп по четыре вершины. Каждая группа из четырех вершин описывает одну из граней куба. Первые три аргумента в описании каждой из вершин задают ее координаты. Следующие три аргумента определяют нормаль к вершине. Обратите внимание, что нормали всех вершин одной грани одинаковы. Это обеспечивает четкий контраст между примыкающими друг к другу гранями. Последние два аргумента являются координатами текстуры.
Обратите внимание на комментарии, расположенные справа от описания шести из вершин. Таким образом отмечены те вершины, которые будут анимироваться.
Вернемся к коду создания сетки (этапу 1) в функции CreateScene(). Последние два вызова функций выглядят так:
mesh->Translate(D3DVALUE(-0.5), D3DVALUE(-0.5), D3DVALUE(-0.5)); mesh->Scale(D3DVALUE(12), D3DVALUE(12), D3DVALUE(12));Функция Translate() применяется для настройки осей сетки. Куб создан таким образом, что в начале координат расположена одна из его вершин. Мы используем функцию Translate() для перемещения куба таким образом, чтобы с началом координат совпадал его центр. Функция Scale() используется чтобы увеличить куб в 12 раз. Обратите внимание, что порядок вызова этих двух функций очень важен. Если мы выполним операцию масштабирования перед операцией перемещения, начало координат не будет совпадать с центром куба.
На втором этапе своей работы функция CreateScene() создает текстуру и накладывает ее на созданную сетку:
HRSRC texture_id = FindResource(NULL, MAKEINTRESOURCE(IDR_WIN95TEXTURE), "TEXTURE"); LPDIRECT3DRMTEXTURE texture; d3drm->LoadTextureFromResource(texture_id, &texture); mesh->SetGroupTexture(group, texture); mesh->SetGroupMapping(group, D3DRMMAP_PERSPCORRECT); texture->Release(); texture = 0;Текстура загружается из ресурсов программы функцией LoadTextureFromResource() интерфейса Direct3DRM. Затем вызывается функция SetGroupTexture() интерфейса Direct3DRMMesh чтобы связать текстуру с сеткой. Для разрешения перспективной коррекции используется функция SetGroupMapping() интерфейса Direct3DRMMesh. Обратите внимание, что обе эти функции требуют, чтобы в первом аргументе им был передан идентификатор группы граней сетки.
На третьем этапе создается фрейм для сетки:
LPDIRECT3DRMFRAME frame; d3drm->CreateFrame(scene, &frame); frame->AddVisual(mesh); frame->SetRotation(scene, D3DVALUE(0), D3DVALUE(1), D3DVALUE(0), D3DVALUE(.04)); static CallbackData cbdata; cbdata.mesh = mesh; cbdata.group = group; frame->AddMoveCallback(UpdateCube, &cbdata); frame->Release(); frame = 0;Новый фрейм будет потомком фрейма scene и создается с помощью функции CreateFrame() интерфейса Direct3DRM. Сетка присоединяется к новому фрейму функцией AddVisual() интерфейса Direct3DRMFrame. С помощью функции SetRotation() новому фрейму назначается атрибут вращения. Этот атрибут является временным, поскольку присутствующая в приложении функция обратного вызова будет периодически изменять атрибут вращения фрейма сетки.
Далее объявляется статическая структура CallbackData. Эта структура используется в приложении Cube для передачи необходимых данных в функцию обратного вызова. Она объявлена статической потому что используется после того, как функция CreateScene() завершит свою работу, и объявленные в ней локальные переменные перестанут существовать.
В структуре сохраняются указатель на сетку и идентификатор группы граней сетки. Затем выполняется установка функции обратного вызова (UpdateCube()) с помощью функции AddMoveCallback() интерфейса Direct3DRMFrame. Указатель на структуру CallbackData передается функции AddMoveCallback() во втором аргументе.
На четвертом и пятом этапах работы функции CreateScene() выполняется создание источника света и порта просмотра. Мы не будем обсуждать эти действия здесь, поскольку они подробно рассмотрены в других главах и не имеют отношения к анимации вершин.
Функция UpdateCube() является функцией обратного вызова, которая выполняет анимацию вершин в приложении Cube. Код функции UpdateCube() выглядит так:
void CubeWin::UpdateCube(LPDIRECT3DRMFRAME frame, void* p, D3DVALUE) { CallbackData* data = (CallbackData*)p; static const D3DVALUE lim = D3DVALUE(5); static D3DVALUE control; static D3DVALUE inc = D3DVALUE(.25); static D3DRMVERTEX vert[24]; data->mesh->GetVertices(data->group, 0, 24, vert); vert[0].position.x += inc; vert[0].position.y += inc; vert[0].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; vert[14].position.x += inc; vert[14].position.y += inc; vert[14].position.z += inc; vert[18].position.x += inc; vert[18].position.y += inc; vert[18].position.z += inc; vert[20].position.x += inc; vert[20].position.y += inc; vert[20].position.z += inc; data->mesh->SetVertices(data->group, 0, 24, vert); control += inc; if (control > lim || control < -lim) inc = -inc; static UINT delay; if (++delay < 20) return; delay = 0; LPDIRECT3DRMFRAME scene; frame->GetScene(&scene); D3DVECTOR spinvect; D3DRMVectorRandom(&spinvect); D3DVALUE spin = D3DDivide(rand() % 50 + 1, 400); frame->SetRotation(scene, spinvect.x, spinvect.y, spinvect.z, spin); }Сначала функция подготавливает указатель на структуру CallbackData:
CallbackData* data = (CallbackData*)p;Параметр p — это указатель на статическую структуру cbdata объявленную в функции CreateScene(). В то же время, переменная p объявлена в объявлении функции как указатель на void. По этой причине мы для доступа к необходимым данным будем использовать в функции обратного вызова локальный указатель data, присвоив ему приведенное к требуемому типу значение указателя p.
Затем следует объявление четырех статических переменных:
static const D3DVALUE lim = D3DVALUE(5); static D3DVALUE control; static D3DVALUE inc = D3DVALUE(.25); static D3DRMVERTEX vert[24];Переменная lim является константой, ограничивающей перемещение вершин. Переменная control используется для определения текущего местоположения анимируемых вершин. Переменная inc хранит значение на которое будет изменяться значение переменной control. И, наконец, переменная vert является массивом структур D3DRMVERTEX. Мы будем использовать ее при получении, изменении и присваивании данных вершин сетки.
Теперь для получения текущих данных вершин сетки используется функция GetVertices() интерфейса Direct3DRMMesh:
data->mesh->GetVertices(data->group, 0, 24, vert);Список аргументов функции GetVertices() аналогичен списку аргументов функции SetVertices(). Обратите внимание, что и указатель на сетку и идентификатор группы граней сетки передаются через указатель data. После вызова функции GetVertices() массив vert будет заполнен текущими параметрами вершин сетки.
Настало время изменить координаты вершин сетки:
vert[0].position.x += inc; vert[0].position.y += inc; vert[0].position.z += inc; vert[6].position.x += inc; vert[6].position.y += inc; vert[6].position.z += inc; vert[8].position.x += inc; vert[8].position.y += inc; vert[8].position.z += inc; vert[14].position.x += inc; vert[14].position.y += inc; vert[14].position.z += inc; vert[18].position.x += inc; vert[18].position.y += inc; vert[18].position.z += inc; vert[20].position.x += inc; vert[20].position.y += inc; vert[20].position.z += inc;Каждая из координат изменяется на значение, хранящееся в переменной inc. Порядок изменения координат вершин не имеет значения, поскольку все эти изменения не вступят в силу до вызова функции SetVertices():
data->mesh->SetVertices(data->group, 0, 24, vert);Функция SetVertices() изменяет сетку, присваивая ее вершинам измененные параметры.
Затем изменяется значение переменной control:
control += inc; if (control > lim || control < -lim) inc = -inc;В этом коде используется переменная lim чтобы изменять значение переменной inc если значение переменной control достигло заданного предела.
Оставшаяся часть функции UpdateCube() осуществляет периодическое изменение атрибутов вращения фрейма:
static UINT delay; if (++delay < 20) return; delay = 0; LPDIRECT3DRMFRAME scene; frame->GetScene(&scene); D3DVECTOR spinvect; D3DRMVectorRandom(&spinvect); D3DVALUE spin = D3DDivide(rand() % 50 + 1, 400); frame->SetRotation(scene, spinvect.x, spinvect.y, spinvect.z, spin);После каждых 20 обновлений экрана этот код вычисляет новые вектор и скорость вращения. Затем новые значения устанавливаются функцией SetRotation() интерфейса Direct3DRMFrame.
Функция MorphPlayWin::CreateScene() выглядит следующим образом:
BOOL MorphPlayWin::CreateScene() { // --------НАПРАВЛЕННЫЙ И РАССЕЯННЫЙ СВЕТ-------- LPDIRECT3DRMLIGHT dlight; d3drm->CreateLightRGB(D3DRMLIGHT_DIRECTIONAL, D3DVALUE(1.00), D3DVALUE(1.00), D3DVALUE(1.00), &dlight); LPDIRECT3DRMLIGHT alight; d3drm->CreateLightRGB(D3DRMLIGHT_AMBIENT, D3DVALUE(0.40), D3DVALUE(0.40), D3DVALUE(0.40), &alight); LPDIRECT3DRMFRAME lightframe; d3drm->CreateFrame(scene, &lightframe); lightframe->AddLight(dlight); lightframe->AddLight(alight); lightframe->SetOrientation(scene, D3DVALUE(0), D3DVALUE(-1), D3DVALUE(1), D3DVALUE(0), D3DVALUE(1), D3DVALUE(0)); alight->Release(); alight = 0; dlight->Release(); dlight = 0; lightframe->Release(); lightframe = 0; //------ КАМЕРА---------- d3drm->CreateFrame(scene, &camera); camera->SetPosition(scene, D3DVALUE(0), D3DVALUE(0), D3DVALUE(-50)); d3drm->CreateViewport(device, camera, 0, 0, device->GetWidth(), device->GetHeight(), &viewport); return TRUE; }В отличие от большинства других функций CreateScene(), рассмотренных нами в этой книге, версия из класса MorphPlayWin выполняет только два действия. На первом этапе выполняется создание двух источников света. На втором этапе создается порт просмотра.
Поскольку к сцене не добавляется никаких видимых объектов, сразу после запуска приложение отображает пустое окно. Чтобы загрузить последовательность трансформаций, следует воспользоваться командой Open меню File.
Функция InitMorphSequence() использует унаследованные от класса MorphWin функции для начала новой последовательности трансформаций:
BOOL MorphPlayWin::InitMorphSequence(const CString& filename) { if (frame) frame->DeleteVisual(mesh); else { d3drm->CreateFrame(scene, &frame); frame->AddMoveCallback(UpdateDrag, NULL); frame->AddMoveCallback(UpdateMorph, this); } if (LoadMorphSequence(filename) == FALSE) return FALSE; DWORD targets = GetNumMorphTargets(); for (DWORD i = 0; i < targets; i++) AddMorphKey(i, D3DVALUE(i)); maxmorphtime = D3DVALUE(targets - 1); morphtime = D3DVALUE(0.0); mesh = GetMorphMesh(); mesh->SetGroupColorRGB(0, D3DVALUE(.67), D3DVALUE(.82), D3DVALUE(.94)); frame->AddVisual(mesh); return TRUE; }Сначала функция проверяет значение переменной frame. Если указатель frame не инициализирован, функция создает новый фрейм и устанавливает две функции обратного вызова: UpdateDrag() и UpdateMorph(). Если указатель frame уже был инициализирован при предыдущем вызове функции, текущая сетка удаляется из сцены с помощью функции DeleteVisual() интерфейса Direct3DRMFrame.
Затем вызывается функция LoadMorphSequence(). Если она завершается неудачно (возвращает FALSE), немедленно осуществляется возврат из функции InitMorphSequence().
Потом в цикле устанавливаются ключи трансформации для каждого шага. Кроме того, инициализируются члены данных maxmorphtime и morphtime.
В конце вызывается функция MorphWin::GetMorphMesh() чтобы получить указатель на вновь созданную трансформируемую сетку. Цвет сетки изменяется и сетка добавляется к сцене.
Выбор команды Open меню File приводит к тому, что MFC вызывает функцию OnFileOpen(). Код этой функции показан ниже:
void MorphPlayWin::OnFileOpen() { static char BASED_CODE filter[] = "Morph Files (*.mrf)|*.mrf|X Files (*.x)|*.x||"; CFileDialog opendialog(TRUE, 0, 0, OFN_FILEMUSTEXIST, filter, this); if (opendialog.DoModal() == IDOK) { CString filename = opendialog.GetPathName(); CWaitCursor cur; InitMorphSequence(filename); } }Функция использует класс MFC CFileDialog для представления диалогового окна выбора файлов в котором будут отображаться только файлы с расширениями MRF и X. Если пользователь выбирает файл, или вводит его имя, выполняется вызов функции InitMorphSequence() и имя выбранного файла передается ей как аргумент.
Воспроизведением последовательности трансформаций управляет функция обратного вызова UpdateMorph():
void MorphPlayWin::UpdateMorph(LPDIRECT3DRMFRAME, void* ptr, D3DVALUE) { MorphPlayWin* win = (MorphPlayWin*)ptr; const D3DVALUE maxtime = win->maxmorphtime; const int morphspeed = win->morphspeed; const D3DVALUE morphtimeinc = win->morphtimeinc; if (morphspeed == MORPH_FORWARD) { morphtime += morphtimeinc; if (morphtime > maxtime) morphtime = D3DVALUE(0); } else if (morphspeed == MORPH_REVERSE) { morphtime -= morphtimeinc; if (morphtime < D3DVALUE(0)) morphtime = maxtime; } else if (morphspeed == MORPH_BOTH) { static BOOL forward = TRUE; if (forward) morphtime += morphtimeinc; else morphtime -= morphtimeinc; if (morphtime < D3DVALUE(0) || morphtime > maxtime) forward= 1 - forward; } win->SetMorphTime(morphtime); }Значение времени для последовательности трансформаций вычисляется на основе переменной morphspeed. Если значение переменной morphspeed равно MORPH_FORWARD, то время в последовательности трансформаций непрерывно увеличивается, пока не достигнет максимально возможного значения. После этого отсчет времени снова начинается с нуля. Текущее время трансформации хранится в переменной morphtime.
Если значение переменной morphspeed равно MORPH_REVERSE, новое значение времени вычисляется путем вычитания заданного значения из переменной morphtime. Константа MORPH_BOTH означает, что последовательность трансформаций сначала воспроизводится в прямом направлении, а затем— в обратном.
После вычисления нового значения переменной morphtime вызывается функция MorphWin::SetMorphTime(). В результате выполняется вычисление новых данных вершин и производится обновление трансформируемой сетки.
Функция AddMorphKey() связывает шаг трансформации с заданным моментом времени анимации. В качестве аргументов функция получает индекс шага и временную метку.
BOOL MorphWin::AddMorphKey(DWORD target, D3DVALUE time) { if (target < 0 || target > nummorphtargets) return FALSE; for (DWORD i = 0; i < nummorphvertices; i++) { D3DVECTOR& pos = morphmeshdata[target][i].position; posanimation[i]->AddPositionKey(time, pos.x, pos.y, pos.z); D3DVECTOR& norm = morphmeshdata[target][i].normal; normanimation[i]->AddPositionKey(time, norm.x, norm.y, norm.z); } return TRUE; }Сначала выполняется проверка допустимости индекса шага трансформации. Если индекс выходит за допустимые пределы, функция возвращает FALSE. В ином случае полученный индекс шага трансформации используется для добавления позиционных ключей в объект анимации местоположения и объект анимации нормали каждой из вершин. Для добавления ключей применяется функция AddPositionKey() интерфейса Direct3DRMAnimation. Обратите внимание, что в качестве первого аргумента функции AddPositionKey() используется параметр time.
Функция CreateAnimations() вызывается только после успешного завершения работы функции LoadMeshes(). Она отвечает за инициализацию объектов Direct3DRMAnimation, которые будут выполнять вычисление местоположения вершин и нормалей вершин при трансформации. Код этой функции выглядит так:
BOOL MorphWin::CreateAnimations() { static int vertexcount; ReleaseAnimations(vertexcount); vertexcount = nummorphvertices; posanimation = new LPDIRECT3DRMANIMATION[nummorphvertices]; posframe = new LPDIRECT3DRMFRAME[nummorphvertices]; normanimation = new LPDIRECT3DRMANIMATION[nummorphvertices]; normframe = new LPDIRECT3DRMFRAME[nummorphvertices]; for (DWORD vert = 0; vert < nummorphvertices; vert++) { d3drm->CreateAnimation(&posanimation[vert]); posanimation[vert]->SetOptions(D3DRMANIMATION_LINEARPOSITION | D3DRMANIMATION_POSITION); d3drm->CreateFrame(scene, &posframe[vert]); posanimation[vert]->SetFrame(posframe[vert]); d3drm->CreateAnimation(&normanimation[vert]); normanimation[vert]->SetOptions(D3DRMANIMATION_LINEARPOSITION | D3DRMANIMATION_POSITION); d3drm->CreateFrame(scene, &normframe[vert]); normanimation[vert]->SetFrame(normframe[vert]); } return TRUE; }Сначала с помощью функции ReleaseAnimations() выполняется освобождение любых выделенных ранее ресурсов. Количество вершин сохраняется в статической переменной vertexcount для последующих вызовов функции ReleaseAnimations().
Затем инициализируются массивы posanimation, posframe, normanimation и normframe. В цикле создается по два экземпляра объекта Direct3DRMAnimation для каждой вершины. Объекты анимации в массиве posanimation будут использованы для вычисления местоположения вершин, а объекты анимации из массива normanimation применяются для вычисления нормалей вершин.
Функция GetMorphMesh() просто возвращает указатель на трансформируемую сетку. Она определена в объявлении класса следующим образом:
LPDIRECT3DRMMESH GetMorphMesh() { return morphmesh; }Данная функция позволяет классам, наследуемым от MorphWin отображать сетку и изменять ее параметры. Как это делается мы увидим ниже.
Функция GetNumMorphTargets() определена в объявлении класса MorphWin:
DWORD GetNumMorphTargets() { return nummorphtargets; }Функция просто возвращает значение, хранящееся в переменной класса nummorphtargets.
Функция LoadMeshes() отвечает за загрузку сеток из указанного файла. Она должна проверить, что каждая сетка содержит одно и тоже количество вершин, а также извлечь и сохранить данные вершин. По этой причине код функции получился весьма длинным (и уродливым). Вы можете взглянуть на него в листинге 8.3.
Листинг 8.3. Функция MorphWin::LoadMeshes() |
BOOL MorphWin::LoadMeshes(const CString& filename) { for (DWORD i = 0; i < nummorphtargets; i++) delete [] morphmeshdata[i]; nummorphtargets = 0; if (morphmesh) { morphmesh->Release(); morphmesh = 0; } BOOL load_ok = TRUE; for (i = 0; i <MAXMORPHTARGETS && load_ok; i++) { CString msg; HRESULT r; LPDIRECT3DRMMESHBUILDER builder; d3drm->CreateMeshBuilder(&builder); r = builder->Load((void*)(LPCTSTR)filename, (void*)morphmeshname[i], D3DRMLOAD_FROMFILE | D3DRMLOAD_BYNAME, NULL, NULL); load_ok = r == D3DRM_OK; if (r == D3DRMERR_FILENOTFOUND) { TRACE("file not found\n"); CString msg = filename + ": file not found"; AfxMessageBox(msg); morphing = FALSE; } if (!load_ok) goto nomoremeshes; D3DVALUE scale; if (i == 0) scale = ScaleMesh(builder, D3DVALUE(25)); else builder->Scale(scale, scale, scale); builder->SetQuality(D3DRMRENDER_FLAT); LPDIRECT3DRMMESH mesh; builder->CreateMesh(&mesh); unsigned vcount, fcount; DWORD ds; mesh->GetGroup(0, &vcount, &fcount, 0, &ds, 0); if (i == 0) nummorphvertices = vcount; else if (vcount != nummorphvertices && load_ok) { TRACE("invalid vertex count\n"); AfxMessageBox("Invalid vertex count"); morphing = FALSE; return FALSE; } morphmeshdata[i] = new D3DRMVERTEX[nummorphvertices]; r = mesh->GetVertices(0, 0, nummorphvertices, morphmeshdata[i]); if (r != D3DRM_OK) TRACE("mesh->GetVertices() failed\n"); msg.Format("Mesh %d - %d vertices, %d faces", i+1, vcount, fcount); SetWindowText(msg); if (i == 0) morphmesh = mesh; else { mesh->Release(); mesh = 0; } nomoremeshes: builder->Release(); builder = 0; } nummorphtargets = i - 1; morphing = TRUE; return TRUE; } |
Данная функция вызывается при загрузке новой последовательности трансформаций, а значит в момент обращения к функции LoadMeshes() уже может существовать загруженная ранее последовательность. Поэтому первое, что должна сделать функция, — освободить любые выделенные ранее ресурсы.
Затем функция в цикле пытается загрузить 26 шагов трансформации. Цикл прекращается если очередная сетка не будет найдена, либо если обнаруженная сетка содержит другое количество вершин. Когда сетка загружена, функция GetVertices() интерфейса Direct3DRMMesh извлекает данные вершин сетки. Кроме того, в заголовке окна отображается новое сообщение. Функция также инициализирует переменные класса nummorphvertices и nummorphtargets.
Функция LoadMorphSequence() получает в качестве аргумента имя файла и пытается создать на основе содержимого файла последовательность трансформаций:
BOOL MorphWin::LoadMorphSequence( const CString& filename ) { CString windowtext; GetWindowText(windowtext); CString txt = "Loading: " + filename; SetWindowText(txt); BOOL ret = FALSE; if (LoadMeshes(filename)) if (CreateAnimations()) ret = PrepareMorphVertices(); SetWindowText(windowtext); return ret; }Сначала функция получает текст, который в данный момент отображается в заголовке окна. Для этого используется функция MFC GetWindowText(), а полученный текст сохраняется в объекте windowtext. Затем текст в заголовке окна заменяется на строку с имененм загружаемого файла. Для отображения этой строки используется функция SetWindowText(). Обратите внимание, что перед возвратом из функции LoadMorphSequence() функция SetWindowText() вызывается еще раз— теперь для восстановления оригинального текста заголовка окна.
Затем в функции LoadMorphSequence() расположены три взаимосвязанных вызова функций. Функция LoadMeshes() применяется для извлечения сеток из файла. Функция CreateAnimation() подготавливает анимационные последовательности для каждой из вершин. Функция PrepareMorphVertices() используется для инициализации массива, который будет хранить вычисленные данные вершин. Если любая из этих функций завершится неудачно, функция LoadMorphSequence() возвращает FALSE.
Функция PrepareMorphVertices() выделяет память для массива, который будет использоваться для хранения вычисленных данных вершин:
BOOL MorphWin::PrepareMorphVertices() { if (morphvertex) { delete [] morphvertex; morphvertex = 0; } morphvertex = new D3DRMVERTEX[nummorphvertices]; return TRUE; }Как и в двух предыдущих функциях, в случае обнаружения ранее выделенных ресурсов сначала выполняется их освобождение, после чего выделяются новые ресурсы.
Функция SetMorphTime() отвечает за генерацию и установку местоположения вершин и нормалей. Код функции выглядит следующим образом:
BOOL MorphWin::SetMorphTime(D3DVALUE time) { for (DWORD v = 0; v < nummorphvertices; v++) { posanimation[v]->SetTime(time); D3DVECTOR pos; posframe[v]->GetPosition(scene, &pos); morphvertex[v].position = pos; normanimation[v]->SetTime(time); D3DVECTOR norm; normframe[v]->GetPosition(scene, &norm); morphvertex[v].normal = norm; } morphmesh->SetVertices(0, 0, nummorphvertices, morphvertex); return TRUE; }Для перебора всех вершин используется цикл. Функция SetTime() интерфейса Direct3DRMAnimation применяется для обновления анимационных последовательностей для местоположения вершин и для нормалей вершин. Затем фрейм, присоединенный к каждому объекту анимации используется для присваивания значений элементам массива morphvertex.
Как только для каждой вершины будут сгенерированы новые значения координат и нормалей, вычисленные данные присваиваются сетке с помощью функции SetVertices() интерфейса Direct3DRMMesh.
Сейчас мы более подробно рассмотрим тему анимации. В главе 4 мы уже выполняли анимацию сетки с помощью атрибутов вращения. В главе 5 мы познакомились с анимацией текстур. В главе 7 мы обсудили анимацию фреймов и технику ключевых кадров.
В каждом из этих случаев мы либо анимировали наложенную на сетку текстуру, либо перемещали и вращали всю сетку целиком. В данной главе мы займемся анимацией отдельных фрагментов сетки и вершин.
Анимация вершин означает управление местоположением отдельных вершин сетки с целью изменения формы сетки во время работы программы. Эта техника имеет множество применений, наиболее известным из которых является трансформация сеток.
Изучить новые техники нам помогут следующие демонстрационные программы:
Cube Cube2 MorphPlayПриложение Cube познакомит нас с базовыми сведениями об анимации вершин. Приложение Cube2 научит создавать в одной сетке несколько групп граней и управлять ими. В конце главы мы с помощью приложения MorphPlay изучим трансформацию сеток.
В приложении Cube мы создали сетку, содержащую только одну группу граней. Все шесть граней куба являлись частью одной группы и изменялись как единая сущность.
Однако, сетка может содержать и несколько групп граней. Каждая из групп добавляется к сетке с помощью функции AddGroup() интерфейса Direct3DRMMesh. Позднее для получения данных или модификации группы используется идентификатор, назначаемый группе функцией AddGroup(). Использование нескольких групп позволяет назначать различные цвета, текстуры и материалы отдельным фрагментам одной сетки.
Функциональность приложения Cube2 реализована классом Cube2Win:
class Cube2Win : public RMWin { public: Cube2Win(); BOOL CreateScene(); protected: //{{AFX_MSG(Cube2Win) afx_msg void OnRenderGroup1Flat(); afx_msg void OnRenderGroup1Wireframe(); afx_msg void OnRenderGroup1Gouraud(); afx_msg void OnRenderGroup2Wireframe(); afx_msg void OnRenderGroup2Flat(); afx_msg void OnRenderGroup2Gouraud(); afx_msg void OnUpdateRenderGroup1Wireframe(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGroup1Flat(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGroup1Gouraud(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGroup2Wireframe(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGroup2Flat(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGroup2Gouraud(CCmdUI* pCmdUI); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: static void UpdateCube(LPDIRECT3DRMFRAME, void*, D3DVALUE); static void UpdateColors(LPDIRECT3DRMFRAME, void*, D3DVALUE); private: LPDIRECT3DRMMESH mesh; D3DRMGROUPINDEX group1, group2; };В классе объявлены две открытые функции: конструктор и функция CreateScene(). Конструктор инициализирует члены данных класса. Функция CreateScene() создает сетку, источник света и порт просмотра.
Основная функциональность приложения Cube обеспечивается классом CubeWin:
class CubeWin : public RMWin { public: CubeWin(); BOOL CreateScene(); protected: //{{AFX_MSG(CubeWin) afx_msg void OnRenderWireframe(); afx_msg void OnRenderFlat(); afx_msg void OnRenderGouraud(); afx_msg void OnUpdateRenderWireframe(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderFlat(CCmdUI* pCmdUI); afx_msg void OnUpdateRenderGouraud(CCmdUI* pCmdUI); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: static void UpdateCube(LPDIRECT3DRMFRAME, void*, D3DVALUE); private: LPDIRECT3DRMMESH mesh; D3DRMGROUPINDEX group; };В классе объявлены две открытые функции: конструктор и функция CreateScene(). Конструктор используется для инициализации членов данных класса. Функция CreateScene() создает сцену приложения, и ее мы рассмотрим чуть позже.
Шесть защищенных функций необходимы для реализации меню Render.
Далее следует объявление функции обратного вызова UpdateCube(), которая будет выполнять анимацию вершин.
Кроме того, вы можете видеть объявления двух переменных класса. Первая является указателем на интерфейс Direct3DRMMesh, который после инициализации будет указывать на единственную сетку приложения. Вторая переменная — это идентификатор группы граней сетки. Ее мы будем использовать при различных манипуляциях с созданной сеткой куба.
Класс MorphPlayWin построен на базе класса MorphWin для создания законченного приложения. Определение класса выглядит следующим образом:
class MorphPlayWin : public MorphWin { public: MorphPlayWin(); BOOL CreateScene(); protected: //{{AFX_MSG(MorphPlayWin) afx_msg void OnFileOpen(); afx_msg void OnMorphForward(); afx_msg void OnMorphReverse(); afx_msg void OnMorphBoth(); afx_msg void OnUpdateMorphForward(CCmdUI* pCmdUI); afx_msg void OnUpdateMorphReverse(CCmdUI* pCmdUI); afx_msg void OnUpdateMorphBoth(CCmdUI* pCmdUI); afx_msg void OnSpeedExtrafast(); afx_msg void OnSpeedFast(); afx_msg void OnSpeedMedium(); afx_msg void OnSpeedSlow(); afx_msg void OnSpeedExtraslow(); afx_msg void OnUpdateSpeedExtrafast(CCmdUI* pCmdUI); afx_msg void OnUpdateSpeedFast(CCmdUI* pCmdUI); afx_msg void OnUpdateSpeedMedium(CCmdUI* pCmdUI); afx_msg void OnUpdateSpeedSlow(CCmdUI* pCmdUI); afx_msg void OnUpdateSpeedExtraslow(CCmdUI* pCmdUI); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: BOOL InitMorphSequence(const CString&); static void UpdateMorph(LPDIRECT3DRMFRAME frame, void*, D3DVALUE); static void UpdateDrag(LPDIRECT3DRMFRAME frame, void*, D3DVALUE); void OnIdle(LONG); private: LPDIRECT3DRMFRAME frame; LPDIRECT3DRMMESH mesh; int morphspeed; D3DVALUE morphtimeinc; D3DVALUE maxmorphtime; static D3DVALUE morphtime; static BOOL drag; static BOOL end_drag; static int last_x, last_y; };В классе объявлены две открытые функции: конструктор и функция CreateScene(). Конструктор выполняет инициализацию членов данных класса. Функция CreateScene() инициализирует источники света и порт просмотра приложения.
Для обработки команды Open меню File класс предоставляет функцию OnFileOpen(). Для представления диалогового окна выбора файлов используется функция класса MFC CFileDialog.
Для поддержки меню Morph предназначены три обработчика сообщений: OnMorphForward(), OnMorphReverse() и OnMorphBoth(). Следующие пять обработчиков предназначены для реализации команд меню Speed: OnSpeedExtrafast(), OnSpeedFast(), OnSpeedMedium(), OnSpeedSlow() и OnSpeedExtraslow(). Каждой из этих функций соответствует вспомогательная функция OnUpdate...(). Функции OnLButtonDown() и OnLButtonUp() используются для начала и прекращения операций перетаскивания объекта мышью (для вращения сетки).
Далее расположено объявление функции InitMorphSequence(). Она применяется для загрузки новой последовательности трансформаций. Функция обратного вызова UpdateMorph() контролирует скорость и направление воспроизведения последовательности трансформаций. Функция обратного вызова UpdateDrag() используется для реализации операций перетаскивания объекта мышью, когда сетка вращается в соответствии с перемещениями мыши. Функция OnIdle() останавливает вращение сетки на время перетаскивания.
Далее расположены объявления членов данных класса. Переменные frame и mesh являются соответственно указателем на фрейм трансформируемой сетки и указателем на трансформируемую сетку. Переменные morphspeed, morphtimeinc, maxmorphtime и morphtime применяются для управления последовательностью трансформаций. Переменные drag, end_drag, last_x и last_y используются при вращении фрейма сетки во время операции перетаскивания.
Функциональность класса MorphWin предоставляется следующими функциями:
LoadMorphSequence() GetNumMorphTargets() GetMorphMesh() AddMorphKey() DeleteMorphKey() SetMorphTime()Функция LoadMorphSequence() получает имя MRF или X-файла и создает последовательность трансформаций, основываясь на содержимом файла. Функция GetNumMorphTargets() возвращает число шагов трансформации в текущей последовательности трансформаций. Функция GetMorphMesh() возвращает указатель на интерфейс Direct3DRMMesh, представляющий трансформируемую сетку.
Функции AddMorphKey(), DeleteMorphKey() и SetMorphTime() вдохновлены интерфейсом Direct3DRMAnimation. Ключ трансформации (morph key) — это то же самое, что и шаг трансформации. Функция AddMorphKey() позволяет указать какой шаг трансформации должен вступить в действие в заданный момент последовательности трансформаций. Функция SetMorphTime() позволяет указать момент последовательности трансформаций, который должен быть вычислен.
Объявление класса MorphWin выглядит следующим образом:
class MorphWin : public RMWin { public: MorphWin(); LPDIRECT3DRMMESH GetMorphMesh() { return morphmesh; } DWORD GetNumMorphTargets() { return nummorphtargets; } BOOL LoadMorphSequence(const CString& filename); BOOL AddMorphKey(DWORD target, D3DVALUE time); BOOL DeleteMorphKey(D3DVALUE time); BOOL SetMorphTime(D3DVALUE time); private: BOOL LoadMeshes(const CString& filename); BOOL CreateAnimations(); BOOL PrepareMorphVertices(); BOOL ReleaseAnimations(int count); protected: //{{AFX_MSG(MorphWin) afx_msg void OnDestroy(); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: LPDIRECT3DRMMESH morphmesh; D3DRMVERTEX* morphmeshdata[MAXMORPHTARGETS]; D3DRMVERTEX* morphvertex; DWORD nummorphvertices; DWORD nummorphtargets; LPDIRECT3DRMANIMATION* posanimation; LPDIRECT3DRMFRAME* posframe; LPDIRECT3DRMANIMATION* normanimation; LPDIRECT3DRMFRAME* normframe; BOOL morphing; };Шесть функций, которые мы обсудили, и конструктор класса объявлены открытыми. Кроме того, в классе объявлены четыре вспомогательные закрытые функции. Для освобождения ресурсов в классе объявлен обработчик сообщения OnDestroy().
Массив morphmeshdata используется для хранения данных вершин каждого входящего в последовательность шага трансформации. Массив morphvertex используется для хранения данных вершин трансформируемой сетки.
Переменная nummorphvertices хранит число вершин в шаге трансформации. Нам не требуются отдельные переменные для хранения числа вершин в каждом из шагов трансформации, поскольку во всех шагах должно быть одно и то же количество вершин. Переменная nummorphtargets используется для хранения числа шагов трансформации в последовательности.
Массивы posanimation и posframe используются для вычисления новых позиций вершин. Массив posanimation — это массив указателей на интерфейс Direct3DRMAnimation. Позиции вершин определяются путем генерации анимационной последовательности для каждой вершины трансформируемой сетки.
Массив posframe является массивом фреймов, которые будут использоваться исключительно для получения данных анимации из объекта Direct3DRMAnimation.
Массивы normanimation и normframe используются для вычисления нормалей для каждой из вершин. Это необходимо, чтобы во время воспроизведения последовательности трансформации правильно рассчитывалось освещение граней. Нормали вершин трансформируются совместно с позициями вершин с использованием той же самой технологии.
И, наконец, логическая переменная morphing показывает загружена ли последовательность трансформаций.
В приложении Cube анимация вершин применяется для изменения формы куба. Поскольку при написании программы во главу угла ставилась простота примера, а не красота результата, анимируются только две вершины. Вид окна приложения Cube показан на рис.8.1.
Рис. 8.1. Приложение Cube
Кроме анимации вершин в приложении выполняется вращение куба путем назначения случайных векторов вращения. Это позволяет скрыть излишнюю простоту анимации вершин. Приложение Cube также предоставляет пользователю стандартное меню Render, позволяющее изменять метод визуализации сетки во время работы программы.
Приложение Cube демонстрирует нам использование следующих технологий:
Создание сетки с самого начала (сетка не загружается с диска, а собирается после запуска приложения). Использование интерфейса Direct3DRMMesh для выполнения анимации вершин. Изменение метода визуализации сетки с помощью команд меню.Приложение MorphPlay является универсальным проигрывателем последовательностей трансформаций. Оно позволяет загрузить и воспроизвести файл трансформации (.MRF). На идущем вместе с книгой CD-ROM вы можете найти несколько файлов MRF в каталоге MESHES.
Файлы MRF — это X-файлы, отвечающие следующим требованиям:
Файл содержит не менее двух сеток. все сетки в файле имеют одинаковое количество вершин. Все сетки последовательно именуются буквами латинского алфавита в нижнем регистре, начиная с буквы «a».Расширение MRF является необязательным. Приложение MorphPlay может загружать как файлы с расширением MRF, так и файлы с расширением X. После загрузки файла, содержащиеся в нем сетки становятся шагами трансформации. Файл может содержать до 26 шагов трансформации — по числу букв алфавита. Шаги трансформации используются в алфавитном порядке.
В приложении есть меню Morph, состоящее из трех пунктов: Forward, Reverse и Both. При выборе пункта Forward процесс трансформации начинается с первой сетки (сетки a), которая преобразуется во вторую сетку (сетку b) и т.д. Процесс трансформации продолжается пока не будет достигнута последняя из содержащихся в файле сеток. Затем воспроизведение начинается заново с самой первой сетки. При выборе пункта Reverse трансформация идет в обратном направлении — от последней сетки к первой. Выбранный по умолчанию пункт, Both, вызывает попеременную трансформацию в прямом и обратном направлениях.
Меню Speed позволяет изменить скорость трансформации. В приложении можно выбирать одну из пяти возможных скоростей. Меньшее значение скорости означает более медленное приращение времени, и в результате получается более плавное движение.
С помощью мыши можно поворачивать и наклонять трансформируемую сетку. Нажмите и удерживайте левую кнопку мыши, после чего перемещайте мышь — это позволит вам рассмотреть процесс трансформации с различных углов.
Внешний вид окна приложения MorphPlay показан на рис. 8.4.
Рис. 8.4. Приложение MorphPlay
Приложение MorphPlay демонстрирует использование следующих технологий:
Создание последовательности трансформаций с использованием в качестве шагов трансформации одной или нескольких сеток. Использование интерфейса Direct3DRMAnimation для выполнения анимации вершин. Использование заголовка окна для отображения различной информации. Использование класса MFC CFileDialog.В отличие от большинства демонстрационных программ на CD-ROM, функциональность приложения MorphPlay разделена между двумя классами. Функции, необходимые для выполнения трансформации сосредоточены в классе MorphWin, а вся специфическая для данного приложения функциональность находится в классе MorphPlayWin. Иерархия классов приложения показана на рис.8.5.
Рис. 8.5. Иерархия классов приложения MorphPlay
Изоляция функций трансформации в классе MorphWin позволяет упростить создание ваших собственных приложений, использующих трансформацию. Наследование класса окна от класса MorphWin (а не от класса RMWin) означает, что класс окна будет наследовать возможности трансформации.
Термин «трансформация» часто ассоциируется с телевизионной рекламой или спецэффектами в фильмах, когда один объект плавно превращается в другой. Обычно начальная и конечная форма объекта весьма отличаются. В данном разделе мы обсудим общие принципы трансформации.
Для наших целей не потребуется сосредотачиваться на том, насколько отличаются две формы объекта, достаточно только чтобы одна форма могла быть преобразована в другую (и наоборот). Чтобы два объекта (сетки) могли быть преобразованы один в другой, они должны содержать одинаковое количество вершин. Выполенеие этого условия не гарантирует, что полученная в результате последовательность трансформаций будет хорошо выглядеть. Одинаковое число вершин означает только возможность существования такой последовательности.
С практической точки зрения, количество вершин это только половина вопроса. Грани в двух сетках должны быть упорядочены одинаковым образом. Фактически, в большинстве случаев трансформации используется несколько вариантов одной и той же сетки.
Сетка, являющаяся частью набора трансформируемых сеток, называется шагом трансформации (morph target). В нашем обсуждении мы не будем касаться вопросов разработки и создания шагов трансформации. Мы сосредоточимся на том, как выполнить трансформацию, включающую два или более совместимых шага.
Формально трансформация означает вычисление позиций вершин сетки, основываясь на следующих трех критериях:
Позиция вершины на начальном шаге трансформации. Позиция вершины на конечном шаге трансформации. Значение, указывающее количество последовательных изменений, выполняемых при переходе от одного шага к другому.Предположим, что сетка начального шага трансформации представляет собой птицу, сетка конечного шага трансформации— самолет, а число промежуточных изменений равно 100. Если мы используем значение 1, то полученная в результате сетка будет выглядеть как птица. Если мы используем значение 100, — сетка будет выглядеть как самолет. Если же мы используем значение 50, то полученная в результате сетка будет выглядеть как нечто среднее между птицей и самолетом. Чем ближе используемое значение к 1, тем больше сетка похожа на птицу. Аналогично, чем ближе значение к 100, тем больше сетка похожа на самолет.
Шаги трансформации не обязаны полностью отличаться друг от друга. Последовательность трансформаций может включать сгибание, искривление, скручивание, сжатие или вытягивание объекта.
Также нет никаких причин, по которым количество шагов трансформации должно ограничиваться двумя. Можно использовать любое количество шагов. Например, птица может сначала трансформироваться в шар, а затем шар трансформируется в самолет. Для этого достаточно добавить сферу в качестве шага трансформации расположенного между шагами птицы и самолета. Последовательность трансформаций может быть создана, если каждый шаг трансформации имеет одинаковое количество вершин.