График функции в 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.