Для работы с файлом мы пользовались буфером
переменных типа BYTE. Для работы с данными в
памяти значительно более удобной структурой
данных является динамический контейнер. Мы, как
вы помните, выбрали для этой цели контейнер,
скроенный по шаблону vector. При заказе на его
изготовление указали тип данных для хранения в
контейнере. Это объекты класса CPointSD (точки
трехмерного пространства). Мы пошли по простому
пути и храним в файле только один компонент Y из
трех координат точек поверхности в 3D. Остальные
две координаты (узлов сетки на плоскости X-Z)
будем генерировать на регулярной основе. Такой
подход оправдан тем, что изображение OpenGL все
равно претерпевает нормирующие преобразования,
перед тем как попасть на двухмерный экран.
Создание контейнера точек производится в теле
функции SetGraphPoints, к разработке которой
сейчас и приступим.
На вход функции подается временный буфер (и его
размер), в который попали данные из файла. В
настоящий момент в буфере находятся данные
тестовой поверхности, а потом, при вызове из
функции ReadData, в него действительно попадут
данные из файла. Выбор данных из буфера
происходит аналогично их записи. Здесь мы
пользуемся адресной арифметикой, определяемой
типом указателя. Так, операция ++ в применении к
указателю типа UINT сдвигает его в памяти на
sizeof (UINT) байт. Смена типа указателя (на
float*) происходит в тот момент, когда выбраны
данные о размерах сетки узлов.
Для надежности сначала проверяем данные из
буфера на внутреннюю непротиворечивость в смысле
размерностей. Затем мы уничтожаем данные
контейнера и генерируем новые на основе
содержимого буфера. В процессе генерации
трехмерных координат точек их ординаты (Y)
масштабируются для того, чтобы график имел
пропорции, удобные для просмотра:
void
COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
{
//====== Готовимся к расшифровке данных буфера
//====== Указываем на него указателем целого
типа
UINT *p = (UINT*)buff;
//=== Выбираем данные целого типа, сдвигая
указатель
m_xSize = *р;
m_zSize = *++p;
//====== Проверка на непротиворечивость
if
(m_xSize<2 || m_zSize<2 ||
m_xSize*m_zSize*sizeof(float)
+ 2 * sizeof(UINT) != nSize)
{
MessageBox (_T ("Данные
противоречивы")
) ;
return;
}
//====== Изменяем размер контейнера
//====== При этом его данные разрушаются
m_cPoints . resize
(m_xSize*m_zSize) ;
if
(m_cPoints .empty () )
{
MessageBox (_T ("He
возможно разместить данные")
return;
}
//====== Подготовка к циклу пробега по буферу
//====== и процессу масштабирования
float
x,
z,
//====== Считываем первую ординату
*pf = (float*) ++р,
fMinY = *pf,
fMaxY = *pf,
right = (m_xSize-l) /2 . f ,
left = -right,
read = (m_zSize-l) /2 . f ,
front = -rear,
range = (right + rear) /2. f;
UINTi, j, n;
//====== Вычисление размаха изображаемого
объекта
m_fRangeY = range;
m_fRangeX =
float (m_xSize) ;
m_fRangeZ =
float (m_zSize) ;
//====== Величина
сдвига вдоль оси
Z
m_zTrans = -1.5f *
m_fRangeZ;
//====== Генерируем координаты сетки (X-Z)
//====== и совмещаем с ординатами Y из буфера
for
(z=front, i=0, n=0; i<m_zSize; i++, z+=l.f)
{
for
(x=left, j=0; j<m_xSize; j++, x+=l.f, n++)
{
MinMax (*pf, fMinY,
fMaxY) ;
m_cPoints[n] =
CPoint3D(x, z, *pf++) ;
}
}
//======
Масштабирование ординат
float
zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)
: l.f;
for
(n=0; n<m_xSize*m_zSize;n++)
{
m_cPoints [n] .
у
= zoom * (m_cPoints [n] .
у
- fMinY) - range/2. f;
}
}
При изменении размеров контейнера методом (resize)
все его данные разрушаются. В двойном цикле
пробега по узлам сетки мы восстанавливаем
(генерируем заново) координаты X и Z всех вершин
четырехугольников. В отдельном цикле пробега по
всему контейнеру происходит масштабирование
ординат (умножение на предварительно вычисленный
коэффициент zoom). В используемом алгоритме
необходимо искать экстремумы функции у = f (x,
z). С этой целью удобно иметь глобальную функцию
MinMax, которая корректирует значение минимума
или максимума, если входной параметр превышает
существующие на сей момент экстремумы. Введите
тело этой функции в начало файла реализации
оконного класса (ChildView.cpp):
inline void
MinMax (float d, floats Min, float&
Max)
{
//====== Корректируем переданные по ссылке
параметры
if
(d > Max)
Max = d; //
Претендент на максимум
else if
(d < Min)
Min = d; //
Претендент на минимум
}
Диалог по управлению светом
В окне редактора диалогов (Resource View >
Dialog > Контекстное меню > Insert Dialog)
создайте окно диалога по управлению светом,
которое должно иметь такой вид:
Обратите внимание на то, что справа от каждого
движка расположен элемент типа static Text, в
окне которого будет отражено текущее положение
движка в числовой форме. Три регулятора
(элемента типа Slider Control) в левом верхнем
углу окна диалога предназначены для управления
свойствами света. Группа регуляторов справа от
них поможет пользователю изменить координаты
источника света. Группа регуляторов,
объединенная рамкой (типа Group Box) с
заголовком Material, служит для изменения
отражающих свойств материала. Кнопка с надписью
Data File позволит пользователю открыть файловый
диалог и выбрать файл с данными для нового
изображения. Для диалогов, предназначенных для
работы в немодальном режиме, необходимо
установить стиль Visible. Сделайте это в окне
Properties > Behavior. Идентификаторы элементов
управления мы сведем в табл.
Таблица -
Идентификаторы элементов управления
Элемент |
Идентификатор |
Диалог |
IDD_PROP |
Ползунок
Ambient в группе Light |
IDC_AMBIENT |
Ползунок
Diffuse в группе Light |
IDC_DIFFUSE |
Ползунок
Specular в группе Light |
IDC_SPECULAR |
;
Static Text справа от Ambient в группе
Light |
IDC_AMB_TEXT |
, Static
Text справа от Diffuse в группе Light |
IDC_DIFFUSE_TEXT |
Static Text
справа от Specular в группе Light |
IDC_SPECULAR_TEXT |
Ползунок
Ambient в группе Material |
IDC_AMBMAT |
Ползунок
Diffuse в группе Material |
IDC_DIFFMAT |
'
Ползунок
Specular
в группе
Material |
IDC_SPECMAT |
f Static Text
справа от
Ambient
в группе
Material
|
IDC_AMBMAT_TEXT
|
:! Static
Text справа от Diffuse. в группе
Material |
IDC_DIFFMATJFEXT |
; Static Text
справа от
Specular
в группе
Material |
IDC_SPECMAT_TEXT |
Ползунок Shim'ness |
IDC_SHINE |
Ползунок Emission |
IDC_EMISSION |
« Static Text
справа от
Shininess |
IDC_SHINE_TEXT |
Static Text
справа от
Emission |
IDC_EMISSION_TEXT |
Ползунок X |
IDC_XPOS |
| Ползунок Y |
IDC_YPOS |
1 Ползунок Z |
IDC_ZPOS |
Static Text справа от X |
IDC_XPOS_TEXT |
Static Text справа от Y |
IDC_YPOS_TEXT |
Static Text справа от Z |
IDC_ZPOS_TEXT |
Кнопка Data File |
IDC_FILENAME |
Диалоговый класс
Для управления диалогом следует создать новый
класс. Для этого можно воспользоваться
контекстным меню, вызванным над формой диалога.
-
Выберите в контекстном меню команду Add
Class.
-
В левом окне диалога Add Class раскройте
дерево Visual C++, сделайте выбор MFC >
MFC Class и нажмите кнопку Open.
-
В окне мастера MFC Class Wizard задайте имя
класса CPropDlg, в качестве базового класса
выберите CDialog. При этом станет доступным
ноле Dialog ID.
-
В это поле введите или выберите из
выпадающего списка идентификатор шаблона
диалога IDD_PROP и нажмите кнопку Finish.
Просмотрите объявление класса CPropDlg, которое
должно появиться в новом окне PropDlg.h. Как
видите, мастер сделал заготовку функции
DoDataExchange для обмена данными с элементами
управления на форме диалога. Однако она нам не
понадобится, так как обмен данными будет
производиться в другом стиле, характерном для
приложений не MFC-происхождения. Такое решение
выбрано в связи с тем, что мы собираемся
перенести рассматриваемый код в приложение,
созданное на основе библиотеки шаблонов ATL. Это
будет сделано в уроке 9 при разработке элемента
ActiveX, а сейчас введите в диалоговый класс
новые данные. Они необходимы для эффективной
работы с диалогом в немодальном режиме. Важным
моментом в таких случаях является использование
указателя на оконный класс. С его помощью легко
управлять окном прямо из диалога. Мы слегка
изменили конструктор и ввели вспомогательный
метод GetsiiderNum. Изменения косметического
характера вы обнаружите сами:
#pragma once
class
COGView; // Упреждающее объявление
class
CPropDlg : public CDialog
{
DECLARE_DYNAMIC(CPropDlg)
public:
COGView *m_pView; // Адрес представления
int
m_Pos[ll]; // Массив позиций ползунков
CPropDlg(COGView* p) ;
virtual
~CPropDlg();
//
Метод для выяснения
ID
активного ползунка
int GetsiiderNum(HWND hwnd, UINT& nID) ;
enum
{ IDD = IDD_PROP };
protected: virtual void
DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
};
Откройте файл реализации диалогового класса и с
учетом сказанного про адрес окна введите
изменение в тело конструктора, который должен
приобрести такой вид:
CPropDlg::CPropDlg(COGView* p)
: CDialog(CPropDlg::IDD, p)
{
//======
Запоминаем адрес объекта
m_pView = p;
}
Инициализация диалога
При каждом открытии диалога все его элементы
управления должны отражать текущие состояния
регулировок (положения движков), которые
хранятся в классе представления. Обычно эти
установки производят в коде функции OninitDialog.
Введите в класс CPropDlg стартовую заготовку
этой функции (CPropDlg > Properties >
Overrides > OninitDialog > Add) и
наполните ее кодами, как показано ниже:
BOOL CPropDlg: rOnlnitDialog (void)
{ CDialog: :OnInitDialog () ;
//====== Заполняем массив текущих параметров
света
m_pView->GetLightParams (m _Pos) ;
//====== Массив идентификаторов ползунков
UINT IDs[] =
{
IDC_XPOS, IDC_YPOS, IDC_ZPOS,
IDC_AMBIENT,
IDC_DIFFUSE,
IDC_SPECULAR,
IDC_AMBMAT,
IDC_DIFFMAT,
IDC_SPECMAT,
IDC_SHINE,
IDCEMISSION
//====== Цикл прохода по всем регуляторам
for
(int i=0; Ksizeof (IDs) /sizeof
(IDs [ 0] ) ; i++)
{
//=== Добываем Windows-описатель окна ползунка H
WND hwnd = GetDlgItem(IDs[i] } ->GetSafeHwnd ()
;
UINT nID;
//======
Определяем его идентификатор
int num = GetSliderNum(hwnd, nID) ;
// Требуем установить ползунок в положение m_Pos[i]
: :SendMessage(hwnd, TBM_SETPOS, TRUE, (LPARAM)
m_Pos [i] )
char
s [ 8 ] ;
//====== Готовим текстовый аналог текущей
позиции
sprintf (s, "%d" ,m_Pos [ i] ) ;
//====== Помещаем текст в окно справа от
ползунка
SetDlgltemText (nID, (LPCTSTR) s) ;
}
return
TRUE;
}
Вспомогательная функция GetsliderNum по
переданному ей описателю окна (hwnd ползунка)
определяет идентификатор связанного с ним
информационного окна (типа Static text) и
возвращает индекс соответствующей ползунку пози
ции в массиве регуляторов:
int
CPropDlg: :GetSliderNum (HWND hwnd, UINT& nID)
{
//==== GetDlgCtrllD по известному hwnd
определяет
//==== и возвращает идентификатор элемента
управления
switch
( : : GetDlgCtrllD (hwnd) )
{
// ====== Выясняем идентификатор окна
справа
case
IDC_XPOS:
nID = IDC_XPOS_TEXT;
return 0;
case
IDC_YPOS:
nID = IDC_YPOS_TEXT;
return 1;
case
IDC_ZPOS:
nID = IDC_ZPOS_TEXT;
return 2;
case
IDC_AMBIENT:
nID = IDC_AMB_TEXT;
return 3;
case
IDC_DIFFUSE:
nID = IDC_DIFFUSE_TEXT;
return 4 ;
case
IDC_SPECULAR:
nID = IDC_SPECULAR_TEXT;
return 5; case
IDC_AMBMAT:
nID = IDC_AMBMAT_TEXT;
return 6 ;
case
IDC_DIFFMAT:
nID = IDC_DIFFMAT_TEXT;
return 7 ;
case
IDC_SPECMAT:
nID = IDC_SPECMAT_TEXT;
return 8 ; case
IDC_SHINE:
nID = IDC_SHINE_TEXT;
return 9;
case
IDC_EMISSION:
nID = IDC_EMISSION_TEXT;
return 10;
}
return 0;
}
Работа с группой регуляторов
В диалоговый класс введите обработчики сообщений
WM_HSCROLL и WM_CLOSE, a также реакцию на
нажатие кнопки IDC_FILENAME. Воспользуйтесь для
этого окном Properties и его кнопками Messages и
Events. В обработчик OnHScroll введите логику
определения ползунка и управления им с помощью
мыши и клавиш. Подобный код мы подробно
рассматривали в уроке 4. Прочтите объяснения
вновь, если это необходимо, Вместе с сообщением
WM_HSCROLL система прислала нам адрес объекта
класса GScrollBar, связанного с активным
ползунком. Мы добываем Windows-описатель его
окна (hwnd) и передаем его в функцию
GetsliderNum, которая возвращает целочисленный
индекс. Последний используется для доступа к
массиву позиций ползунков. Кроме этого, система
передает nSBCode, который соответствует
сообщению об одном из множества событий, которые
могут произойти с ползунком (например,
управление клавишей левой стрелки — SB_LINELEFT).
В зависимости от события мы выбираем для
ползунка новую позицию:
void
CPropDlg::OnHScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar)
{
//====== Windows-описатель
окна активного ползунка
HWND hwnd = pScrollBar->GetSafeHwnd();
UINT nID;
//=== Определяем индекс в массиве позиций
ползунков
int
num = GetSliderNum(hwnd, nID) ;
int
delta, newPos;
//====== Анализируем код события
switch
(nSBCode)
{
case
SBJTHUMBTRACK:
case
SB_THUMBPOSITION: //
Управление мышью
m_Pos[num] = nPos;
break; case
SB_LEFT: //
Клавиша
Home
delta = -100;
goto
New_Pos; case SB_RIGHT: //
Клавиша
End
delta = + 100;
goto
New__Pos; case SB_LINELEFT: //
Клавиша
<-
delta = -1;
goto
New_Pos; case SB_LINERIGHT: //
Клавиша
->
delta = +1;
goto
New_Pos; case SB_PAGELEFT: //
Клавиша
PgUp
delta = -20;
goto
New_Pos; case SB_PAGERIGHT: //
Клавиша
PgDn
delta = +20-;
goto
New_Pos;
New_Pos: //
Общая ветвь
//====== Устанавливаем новое значение регулятора
newPos = m_Pos[num] + delta;
//====== Ограничения
m_Pos[num] = newPos<0 ?
0 :
newPos>100 ? 100 : newPos;
break; case
SB ENDSCROLL:
default:
return;
}
//====== Синхронизируем текстовый аналог позиции
char s
[ 8 ] ;
sprintf (s, "%d",m__Pos [num] ) ;
SetDlgltemText (nID, (LPCTSTR)s);
//----
Передаем изменение в класс
COGView
m_pView->SetLightParam (num, m_Pos [num] ) ;
}
Особенности немодального режима
Рассматриваемый диалог используется в качестве
панели управления освещением сцены, поэтому он
должен работать в немодальном режиме.
Особенностью такого режима, как вы знаете,
является то, что при закрытии диалога он сам
должен позаботиться об освобождении памяти,
выделенной под объект собственного класса. Эту
задачу можно решить разными способами. Здесь мы
покажем, как это делается в функции обработки
сообщения WM_CLOSE. До того как уничтожено
Windows-окно диалога, мы обнуляем указатель
m_pDlg, который должен храниться в классе
COGView и содержать адрес объекта диалогового
класса. Затем вызываем родительскую версию
функции OnClose, которая уничтожает
Windows-окно. Только после этого мы можем
освободить память, занимаемую объектом своего
класса:
void
CPropDlg: :OnClose (void)
{
//=== Обнуляем указатель на объект своего класса
m_pView->m_pDlg = 0;
//====== Уничтожаем окно
CDialog: :OnClose () ;
//======
Освобождаем память
delete this;
}
Реакция на нажатие кнопки IDC_FILENAME совсем
проста, так как основную работу выполняет класс
COGView. Мы лишь вызываем функцию, которая
реализована в этом классе:
void
CPropDlg:: OnClickedFilename (void)
{
//=== Открываем файловый диалог и читаем данные
m_pView->ReadData ( ) ;
}
Создание немодального диалога должно происходить
в ответ на выбор команды меню Edit >
Properties. Обычно объект диалогового класса,
используемого в немодальном режиме, создается
динамически. При этом предполагается, что класс
родительского окна хранит указатель m_pDlg на
объект класса диалога. Значение указателя обычно
используется не только для управления им, но и
как признак его наличия в данный момент. Это
позволяет правильно обработать ситуацию, когда
диалог уже существует и вновь приходит команда о
его открытии. Введите в класс COGView новую
public-переменную:
CPropDlg *m_pDlg; // Указатель на объект диалога
В начало файла заголовков OGView.h вставьте
упреждающее объявление класса
CPropDlg:
class
CPropDlg; // Упреждающее объявление
В конструктор COGView вставьте обнуление
указателя:
m_pDlg =0; // Диалог отсутствует
Для обеспечения видимости класса CPropDlg
дополните список директив препроцессора файла
OGView.cpp директивой:
linclude "PropDlg.h"
Теперь можно ввести коды функции, которая
создает диалог и запускает его вызовом функции
Create (в отличие от DoModal для модального
режима). Если происходит попытка повторного
открытия диалога, то возможны два варианта
развития событий:
-
новый диалог не создается, но окно
существующего диалога делается активным;
-
команда открытия диалога недоступна, так как
ее состояние зависит от значения указателя
m_pDlg.
Реализуем первый вариант:
void COGView::OnEditProperties (void)
{
//====== Если диалог еще не открыт
if
(!m_pDlg)
{
//=== Создаем его и запускаем в немодальном
режиме
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP);
}
else
// Иначе, переводим фокус в окно диалога
m_pDlg->SetActiveWindow();
}
Реакция на команду обновления пользовательского
интерфейса при этом может быть такой:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->SetCheck (m_pDlg != 0);
}
Второй вариант потребует меньше усилий:
void
COGView::OnEditProperties (void)
{
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP); }
Но при этом необходима другая реакция на команду
обновления интерфейса:
void
COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_pDlg == 0);
}
Выберите и реализуйте один из вариантов.
Панель управления
Завершая разработку приложения, вставьте в
панель управления четыре кнопки
Для команд
ID_EDIT_BACKGROUND, ID_EDIT_PROPERTIES,
ID_VIEW_FILL
И
ID_VIEW_
QUAD. Заодно уберите из нее неиспользуемые нами
кнопки с идентификаторами
ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE,
ID_FILE_PRINT, ID__EDIT_CUT,
ID_EDIT_COPY, ID_EDIT_PASTE.
Запустите приложение, включите диалог Edit >
Properties и попробуйте управлять регуляторами
параметров света. Отметьте, что далеко не все из
них отчетливым образом изменяют облик
поверхности. Нажмите кнопку Data File, при этом
должен открыться файловый диалог, но мы не
сможем открыть никакого другого файла, кроме
того, что был создан по умолчанию. Он имеет имя
«Sin.dat» и должен находиться (и быть виден) в
папке проекта. В качестве упражнения создайте
какой-либо другой файл с данными, отражающими
какую-либо поверхность в трехмерном
пространстве. Вы можете воспользоваться для этой
цели функцией DefaultGraphic, немного
модифицировав ее код. На рис. 49 и 50 приведены
поверхности, полученные таким способом. Вы
можете видеть эффект, вносимый различными
настройками параметров освещения.
Если вы тщательно протестируете поведение
приложения, то обнаружите недостатки. Отметим
один из них. Закрытые части изображения при
некотором ракурсе просвечивают сквозь те части
поверхности, которые находятся ближе к
наблюдателю. Причину этого дефекта было
достаточно трудно выявить. И здесь опять пришли
на помощь молодые, талантливые слушатели
Microsoft Authorized Educational Center (www.Avalon.ru)
Кондрашов С. С. (scondor@rambler.ru) и Фролов Д.
С. (dmfrolov@rambler.ru). Оказалось, что при
задании типа проекции с помощью команды
gluPerspective значения ближней границы фрустума
не должны быть слишком маленькими:
gluPerspective
(45., dAspect, 0.01, 10000.);
В нашем случае этот параметр равен 0.01.
Замените его на 10. и сравните качество
генерируемой поверхности.
Подведем итог. В этой главе мы:
-
научились превращать окно, поддерживаемое
классом cview, в окно OpenGL;
-
вновь использовали стандартный контейнер
объектов класса GPoint3D, который удобен для
хранения вершин изображаемой поверхности;
-
убедились, что использование списка команд
OpenGL повышает эффективность передачи
сложного изображения;
-
применили формулу вычисления нормали к
поверхности и убедились в необходимости
уделять этой проблеме достаточное внимание;
-
научились управлять освещенностью сцены
OpenGL с помощью группы регуляторов;
-
оценили удобство управления группой
регуляторов типа slider Control в функции
обработки сообщения о прокрутке WM_HSCROLL.
|