График функции в 3D
Здесь представлено простое Windows приложение, которое создает и отображает 3D изображение (поверхность) функции, используя рендеринг OpenGL. Графические данные могут быть произвольными (указываются пользователем). С форматом данных можно разобраться, если заглянуть в функцию COGView::DefaultGraphic. При помощи данного приложения удобно просматривать результаты вычислений некоторых полей (например: поверхности равной температуры, давление скорости или магнитная плотность потока в трехмерном пространстве). Изображение можно свободно вращать мышкой, опции позволяют настроить желаемые параметры изображения OpenGL.


Итак, запускаем MFC AppWizard, выбираем шаблон SDI и называем его OG. В файл StdAfx.h необходимо поместить следующие директивы, чтобы в проекте были доступны библиотеки OpenGL и STL.
#include #include #include #include #include using namespace std;
Создаём структуру данных
Нам понадобится хранилище для вершин поверхности.Разместите следующий код в файле OGView.h. Упростим изначальный код класса View, удаляя некоторые функции и добавляя новые.
class CPoint3D
{
public:
float x, y, z;
CPoint3D () { x=y=z=0; }
CPoint3D (float c1, float c2, float c3)
{
x = c1; z = c2; y = c3;
}
CPoint3D& operator=(const CPoint3D& pt)
{
x = pt.x; z = pt.z; y = pt.y;
return *this;
}
CPoint3D (const CPoint3D& pt)
{
*this = pt;
}
};
class COGView : public CView
{
protected:
COGView();
DECLARE_DYNCREATE(COGView)
public:
//======== New data
long m_BkClr; // цвет фона
HGLRC m_hRC; // контекст рендеринга OpenGL
HDC m_hdc; // контекст устройства Windows
GLfloat m_AngleX; // угол вращения (по оси X)
GLfloat m_AngleY; // угол вращения (по оси Y)
GLfloat m_AngleView; // угол перспективы
GLfloat m_fRangeX; // диапазон графика (по оси X)
GLfloat m_fRangeY; // диапазон графика (по оси Y)
GLfloat m_fRangeZ; // диапазон графика (по оси Z)
GLfloat m_dx; // прирост смещения (по оси X)
GLfloat m_dy; // прирост смещения (по оси Y)
GLfloat m_xTrans; // смещение (по оси X)
GLfloat m_yTrans; // смещение (по оси Y)
GLfloat m_zTrans; // смещение (по оси Z)
GLenum m_FillMode; // режим заполнения полигонами
bool m_bCaptured; // флаг захвата мышки
bool m_bRightButton; // флаг правой кнопки мыши
bool m_bQuad; // Флаг использования GL_QUAD (либо GL_QUAD_STRIP)
CPoint m_pt; // текущая позиция мышки
UINT m_xSize; // Текущий размер клиентского окна (по оси X)
UINT m_zSize; // Текущий размер клиентского окна
vector m_cPoints; // Graphics dimension (по оси X)
int m_LightParam[11]; //
CPropDlg *m_pDlg; //
//======== Public methods
COGDoc* GetDocument()
{ return DYNAMIC_DOWNCAST(COGDoc,m_pDocument); }
virtual ~COGView();
//======== New methods
void DrawScene(); // Подготовка и хранение изображение
void DefaultGraphic(); // Create and save the default plot
void ReadData(); // Манипуляции с файлом данных
bool DoRead(HANDLE hFile); // чтение файла данных
//===== Берём данные из файла и помещаем в m_cPoints
void SetGraphPoints(BYTE* buff, DWORD nSize);
void SetLightParam (short lp, int nPos); // Устанавливаем параметры освещения
void GetLightParams(int *pPos); // Получаем параметры освещения
void SetLight(); // Устанавливаем освещение
void SetBkColor(); // Устанавливаем цвет фона
void LimitAngles(); // Ограничение углов вращения
//{{AFX_VIRTUAL(COGView)
public:
virtual void OnDraw(CDC* pDC);
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
//}}AFX_VIRTUAL
protected:
//{{AFX_MSG(COGView)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Добавляем обработчики
Используя ClassWizard добавляем в COGView обработчики для сообщений: WM_CREATE, WM_DESTROY, WM_ERASEBKGND, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SIZE и WM_TIMER. Добавляем в конструктор класса View инициализацию контролируемых параметров.
COGView::COGView()
{
//====== Контекста пока ещё не существует
m_hRC = 0;
//====== Изначальный поворот изображения
m_AngleX = 35.f;
m_AngleY = 20.f;
//====== Угол проекции матрицы
m_AngleView = 45.f;
//====== Изначальный цвет фона
m_BkClr = RGB(0, 0, 96);
// Инициализируем режим заполнения точек внутренних многоугольников (четвёрок)
m_FillMode = GL_FILL;
//====== Изначальное создание графика
DefaultGraphic();
//====== Начальный сдвиг изображения
//====== целое и половина размера объекта
m_zTrans = -1.5f*m_fRangeX;
m_xTrans = m_yTrans = 0.f;
//== Начальный квант сдвига (для анимации вращения)
m_dx = m_dy = 0.f;
//====== Мышка не захвачена
m_bCaptured = false;
//====== Правая кнопка мыши не нажата
m_bRightButton = false;
//====== Для создания поверхности мы используем четвёрки
m_bQuad = true;
//====== Инициализация параметров освещения
m_LightParam[0] = 50; // направление по оси X
m_LightParam[1] = 80; // направление по оси Y
m_LightParam[2] = 100; // направление по оси Z
m_LightParam[3] = 15; // Отражённый свет
m_LightParam[4] = 70; // Рассеянный свет
m_LightParam[5] = 100; // Зеркальный свет
m_LightParam[6] = 100; // Отражательность материала
m_LightParam[7] = 100; // Рассеивание материала
m_LightParam[8] = 40; // Зеркальность материала
m_LightParam[9] = 70; // Блеск материала
m_LightParam[10] = 0; // Эмиссия материала
}
Подготовка к открытию контекста OpenGL
Возможно Вы знаете, что для того, чтобы подготовить окно для рисования в нём при помощи OpenGL Вам необходимо:
- выбрать и установить формат пикселя,
- получить и сохранить контекст устройства окна,
- создать и сохранить контекст OpenGL,
- добавить к окну стили WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
- убрать очистку фона перед каждой перерисовкой (обработать WM_ERASEBKGND и возвращать только TRUE),
- специфически обработать сообщения WM_SIZE, WM_PAINT и
- освободить контексты, когда отображение будет закрыто.
Основные события для использования OpenGL помещаются в обработчик WM_CREATE.
int COGView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// Структура, описывающая формат
PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1, // Версия
PFD_DRAW_TO_WINDOW | // Поддерживает GDI
PFD_SUPPORT_OPENGL | // Поддерживает OpenGL
PFD_DOUBLEBUFFER, // Используем двойную буферизацию
// (более эффективная прорисовка)
PFD_TYPE_RGBA, // No pallettes
24, // Number of color planes
// in each color buffer
24, 0, // для компоненты Red
24, 0, // для компоненты Green
24, 0, // для компоненты Blue
24, 0, // для компоненты Alpha
0, // Number of planes
// of Accumulation buffer
0, // для компоненты Red
0, // для компоненты Green
0, // для компоненты Blue
0, // для компоненты Alpha
32, // Глубина Z-buffer
0, // Глубина Stencil-buffer
0, // Глубина Auxiliary-buffer
0, // Now is ignored
0, // Number of planes
0, // Now is ignored
0, // Цвет прозрачной маски
0 // Now is ignored
};
//====== Получаем контекст текущего окна
m_hdc = ::GetDC(GetSafeHwnd());
//====== Делаем запрос ближайшего совместимого формата пикселей
int iD = ChoosePixelFormat(m_hdc, &pfd);
if ( !iD )
{
MessageBox("ChoosePixelFormat::Error");
return -1;
}
//====== Устанавливаем данный формат
if ( !SetPixelFormat (m_hdc, iD, &pfd) )
{
MessageBox("SetPixelFormat::Error");
return -1;
}
//====== создаём контекст OpenGL
if ( !(m_hRC = wglCreateContext (m_hdc)))
{
MessageBox("wglCreateContext::Error");
return -1;
}
//====== Делаем его текущим
if ( !wglMakeCurrent (m_hdc, m_hRC))
{
MessageBox("wglMakeCurrent::Error");
return -1;
}
//====== Теперь можно выполнять команды OpenGL
// (т.e. вызывать функции gl***)
glEnable(GL_LIGHTING); // Будет использоваться освещение
//====== Только один (первый) источник света
glEnable(GL_LIGHT0);
//====== Depth of the Z-buffer will be taken into account
glEnable(GL_DEPTH_TEST);
//====== Material colors will be taken into account
glEnable(GL_COLOR_MATERIAL);
//====== Наша функция, устанавливающая бэкграунд
SetBkColor();
//====== Создаём и сохраняем изображение в специальном списке
// команд OpenGL
DrawScene();
return 0;
}
Перерисовка
Каждый раз, когда Вы (или Windows) решаете перерировать отображение, необходимо обработать сообщение WM_PAINT. Принимая во внимание особенности архитектуры Документ-Вид, Вы помещаете код перерисовки на в OnPaint, а в функцию OnDraw.
void COGView:: OnDraw(CDC* pDC)
{
//========== Очищаем фон и Z-буфер
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//========== Очищаем моделирующую матрицу
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
//======= Первичная установка света
// (иначе он будет вращаться вместе с изображением)
SetLight();
//====== Изменяем порядок коэффициентов матрицы
glTranslatef(m_xTrans,m_yTrans,m_zTrans); // для смещения
glRotatef (m_AngleX, 1.0f, 0.0f, 0.0f ); // и для вращения
glRotatef (m_AngleY, 0.0f, 1.0f, 0.0f );
//====== следующие координаты вершин
glCallList(1);
//====== Переключение между буферами
// (для отображения изменений)
SwapBuffers(m_hdc);
}
Освещение
Здесь представлен простой способ вычисления цвета каждого пикселя.
void COGView::SetLight()
{
//====== При вычислении цвета каждого пикселя при помощи формулы освещения
//====== принимаются во внимание обе стороны поверхности
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,1);
//====== положение источника света зависит от
// размеров объекта, мастабируемого от (0,100)
float fPos[] =
{
(m_LightParam[0]-50)*m_fRangeX/100,
(m_LightParam[1]-50)*m_fRangeY/100,
(m_LightParam[2]-50)*m_fRangeZ/100,
1.f
};
glLightfv(GL_LIGHT0, GL_POSITION, fPos);
//====== Интенсивность отражённого света
float f = m_LightParam[3]/100.f;
float fAmbient[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_AMBIENT, fAmbient);
//====== Интенсивность разброса света
f = m_LightParam[4]/100.f;
float fDiffuse[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_DIFFUSE, fDiffuse);
//====== Интенсивность зеркальности света
f = m_LightParam[5]/100.f;
float fSpecular[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_SPECULAR, fSpecular);
//====== Свойства отражения поверхности материала для
// каждой компоненты света
f = m_LightParam[6]/100.f;
float fAmbMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);
f = m_LightParam[7]/100.f;
float fDifMat[4] = { f, f, f, 1.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);
f = m_LightParam[8]/100.f;
float fSpecMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);
//====== Освещённость материала
float fShine = 128 * m_LightParam[9]/100.f;
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, fShine);
//====== Свойство световой эмиссии материала
f = m_LightParam[10]/100.f;
float fEmission[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission);
}
Установка цвета фона очень проста.
void COGView::SetBkColor()
{
//====== Разделяем цвет на три составляющие
GLclampf red = GetRValue(m_BkClr)/255.f,
green = GetGValue(m_BkClr)/255.f,
blue = GetBValue(m_BkClr)/255.f;
//====== Устанавливаем белый (фон) цвет
glClearColor (red, green, blue, 0.f);
//====== Стираем фон
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
Создание и хранение изображения
Основные операции по созданию изображения хранятся в функции DrawScene, которая создаёт и хранит изображение, состоящее из координат вершин. Мы предполагаем, что все координаты уже размещены в STL-контейнере m_cPoints. Изображение сконструировано как серия изогнутых четырёхугольников (GL_QUADS) или как серия изогнутых четвёрок (GL_QUAD_STRIP). Ниже мы добавим команды по переключению между двумя этими режимами. Точки поверхности располагаются в координатной сетке (X, Z). Размер этой сетки хранится в переменных m_xSize и m_zSize. Несмотря на 2-мерность сетки, мы используем линейный массив (одномерный) m_cPoints или (точнее) контейнер типа вектор для хранения значений координат. Это съэкономит усилия при файловых операциях.
Выбор 4 соседних точек одного примитива (например GL_QUADS) будет сделан при помощи 4 индексов (n, i, j, k). Индекс n проходит последовательно через все вершины по порядку слева направо по оси X (Z=0) а затем процедура повторяется (постепенно увеличивая значение Z). Другие индексы вычисляются относительно индекса n. Обратите внимание, что только два индекса работают в режиме связанных четвёрок, который включается или выключается флагом m_bQuad flag.
void COGView::DrawScene()
{
//====== Создаём новый список команд OpenGL
glNewList(1, GL_COMPILE);
//====== Устанавливаем режим заполнения полигонами
glPolygonMode(GL_FRONT_AND_BACK, m_FillMode);
//====== Размеры сетки изображения
UINT nx = m_xSize-1,
nz = m_zSize-1;
//====== Включаем режим связанных примитивов
// (не связан)
if (m_bQuad)
glBegin (GL_QUADS);
for (UINT z=0, i=0; z
Нормальные векторы должны быть правильно расчитаны для каждого примитива. Иначе мы не получим качественного освещения и, как следствие качественного изображения.
Операции с файлом
Контейнер m_cPoints будет заполнен после того как данные будут считаны из файла. Нам необходимо иметь файл данных, который будет загружаться по умолчанию при запуске приложения. Для этого мы создаём бинарный файл и размещаем в самом начале размеры сетки (m_xSize и m_zSize) , а затем значения функции y = f (x, z). Перед записью в файл, все данные размещены во временном буфере типа BYTE (т.е. unsigned char).
void COGView::DefaultGraphic()
{
//====== Размеры сетки
m_xSize = m_zSize = 33;
//====== Количество граней меньше, чем количество узлов
UINT nz = m_zSize - 1,
nx = m_xSize - 1;
// Размер файла в байта
DWORD nSize =
m_xSize * m_zSize * sizeof(float) + 2*sizeof(UINT);
//====== Временный буфер
BYTE *buff = new BYTE[nSize+1];
//====== Указатель на начало буфера
UINT *p = (UINT*)buff;
//====== помещаем два UINT-а
*p++ = m_xSize;
*p++ = m_zSize;
//====== Изменяем тип указателя, чтобы продолжать работать
// числами с плавающей точкой
float *pf = (float*)p;
//=== коэффициенты формулы по умолчанию
double fi = atan(1.)*6,
kx = fi/nx,
kz = fi/nz;
//====== Проводим вычисления для всех узлов сетки
//=== вычисляем и размещаем значения функции по умолчанию
// в том же буфере
for (UINT i=0; i
Чтение данных
Функция ReadData создаёт файловый диалог, позволяющий пользователю выбрать файл данных, считать данные и создать изображение. Стандартный файловый диалог использует стиль OFN_EXPLORER, который работает только в Windows 2000.
void COGView::ReadData()
{
//=== Здесь мы помещаем путь к файлу
TCHAR szFile[MAX_PATH] = { 0 };
//====== фильтр расширений файла
TCHAR *szFilter =TEXT("Graphics Data Files (*.dat)\0")
TEXT("*.dat\0")
TEXT("All Files\0")
TEXT("*.*\0");
//====== Запрашиваем в текущей директории
TCHAR szCurDir[MAX_PATH];
::GetCurrentDirectory(MAX_PATH-1,szCurDir);
//=== Структура, используемая стандартным файловым диалогом
OPENFILENAME ofn;
ZeroMemory(&ofn,sizeof(OPENFILENAME));
//====== Параметры диалога
ofn.lStructSize = sizeof(OPENFILENAME);
//====== Окно, которое владеет диалогом
ofn.hwndOwner = GetSafeHwnd();
ofn.lpstrFilter = szFilter;
//====== Фильтры строкового индекса (начиная с 1)
ofn.nFilterIndex = 1;
ofn.lpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
//====== Заголовок диалога
ofn.lpstrTitle = _T("Find a data file");
ofn.nMaxFileTitle = sizeof (ofn.lpstrTitle);
//====== Стиль диалога (только в Win2K)
ofn.Flags = OFN_EXPLORER;
//====== Создаём и открываем диалог (возвращая 0 при ошибке)
if (GetOpenFileName(&ofn))
{
// Пытаемся открыть файл (который должен существовать)
HANDLE hFile = CreateFile(ofn.lpstrFile, GENERIC_READ,
FILE_SHARE_READ, 0, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, 0);
//=== При ошибке CreateFile возвращаем -1
if (hFile == (HANDLE)-1)
{
MessageBox(_T("Could not open this file"));
return;
}
//====== Пытаемся считать данные
if (!DoRead(hFile))
return;
//====== Создаём изображение
DrawScene();
//====== Перерисовываем клиентскую часть окна
Invalidate(FALSE);
}
}
Макрос TEXT эквивалентен следующей строке
TEXT("Graphics Data Files (*.dat)\0*.dat\0All Files\0*.*\0");
Фактически чтение производится в DoRead, где мы запрашиваем размер файла, распределяем необходимое количество памяти, а так же считываем весь файл в буфер. После этого мы устанавливаем контейнер точек m_cPoints при помощи функции SetGraphPoints.
bool COGView::DoRead(HANDLE hFile)
{
//===== Запрашиваем размер файла
DWORD nSize = GetFileSize (hFile, 0);
if (nSize == 0xFFFFFFFF)
{
GetLastError();
MessageBox(_T("Could not get file size"));
CloseHandle(hFile);
return false;
}
//===== Пытаемся распределить буфер
BYTE *buff = new BYTE[nSize+1];
if (!buff)
{
MessageBox(_T("The data file is too big"));
CloseHandle(hFile);
return false;
}
DWORD nBytes;
//===== Читаем содержимое файла в буфер
ReadFile (hFile, buff, nSize, &nBytes, 0);
CloseHandle(hFile);
if (nSize != nBytes)
{
MessageBox(_T("Error while reading data file"));
return false;
}
//===== Устанавливаем вектор координат вершин
SetGraphPoints (buff, nSize);
delete [] buff;
return true;
}
void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
{
//===== Опять используем технологию изменения
// типа указателя
UINT *p = (UINT*)buff;
//==== Считываем размеры сетки
m_xSize = *p;
m_zSize = *++p;
//===== Проверяем размер файла (в данном случае)
if (m_xSize fMinY ? range/(fMaxY-fMinY) : 1.f;
for (n=0; n
Манипуляции с мышкой
Левая кнопка мыши позволяет контролировать два направления вращения, а так же включать и выключать режим автоматического вращения. Правая же кнопка используется для перемещения по оси Z, т.е. удаления или приближения графика.
void COGView::LimitAngles()
{
while (m_AngleX 360.f)
m_AngleX -= 360.f;
while (m_AngleY 360.f)
m_AngleY -= 360.f;
}
void COGView::OnLButtonDown(UINT nFlags, CPoint point)
{
//====== Останавливаем вращение
KillTimer(1);
//====== Zero the quantums
m_dx = 0.f;
m_dy = 0.f;
//====== Захватываем сообщение мышки и направляем его
// в наше окно
SetCapture();
//====== Запоминаем захват мышки
m_bCaptured = true;
//====== и точку, в которой это произошло
m_pt = point;
}
void COGView::OnRButtonDown(UINT nFlags, CPoint point)
{
//====== Запоминаем нажатие правой кнопки мыши
m_bRightButton = true;
//====== and reproduce the left button response
OnLButtonDown(nFlags, point);
}
void COGView::OnLButtonUp(UINT nFlags, CPoint point)
{
//====== Если мы захватили мышку,
if (m_bCaptured)
{
//=== query the desired quantum value
//=== if it exeeds the sensativity threshold
if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//=== Turn on the constant rotation
SetTimer(1,33,0);
else
//=== Выключаем константу вращения
KillTimer(1);
//====== Очищаем флаг захвата
m_bCaptured = false;
ReleaseCapture();
}
}
void COGView::OnRButtonUp(UINT nFlags, CPoint point)
{
m_bRightButton = m_bCaptured = false;
ReleaseCapture();
}
void COGView::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_bCaptured)
{
// Desired rotation speed components
m_dy = float(point.y - m_pt.y)/40.f;
m_dx = float(point.x - m_pt.x)/40.f;
//====== Если Ctrl была нажата
if (nFlags & MK_CONTROL)
{
//=== сдвигаем изображение
m_xTrans += m_dx;
m_yTrans -= m_dy;
}
else
{
//====== Если нажата правая кнопка мыши
if (m_bRightButton)
//====== сдвигаем по оси z
m_zTrans += (m_dx + m_dy)/2.f;
else
{
//====== иначе мы вращаем изображение
LimitAngles();
double a = fabs(m_AngleX);
if (90.
Оставить комментарий
Комментарии






