В своё время, выполняя множество уроков по OpenGL и DirectX под Windows у меня выработалась определенная схема построения приложения. И только недавно до меня дошло, что можно объединить функционал создания окна Windows и для OpenGL и для DirectX! Пошерстив Интернет на предмет данной темы, у меня не получилось найти нечто подобное, поэтому сделал свой "велосипед" и выкладываю результат для общего пользования.
Объединив наработки и причесав, получился набор модулей. Сейчас, используя данный код, в большинстве случаев (для учебных целей), не придется ломать голову над функционалом создания окна и присоединения контекста OpenGL или создания устройства DirectX. Достаточно скопировать файлы себе в проект и прилинковать нужные библиотеки.
Вообще, я больше занимался OpenGL и разрабатывал под MinGW. С OpenGL там совершенно никаких проблем нет. А вот под DirectX при усложнении фунционала начинали возникать всяческие ошибки, бороться с которыми у меня не было совершенно никакого желания, поэтому учебные проекты DirectX я компилил уже в Visual Studio 2008.
Итак, приложение стартует в файле main.cpp. Вот его код:
#include <windows.h> #include "Application.h" int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrev, LPSTR args, int) { Application::instance().start(); return 0; }
Я намеренно решил вынести функционал создания окна в отдельный класс (здесь Application). Так немного больше возни, но гораздо удобнее управлять кодом.
Декларация класса Application:
#ifndef _APPLICATION_H #define _APPLICATION_H #include <windows.h> #include <string> #include "Timer.h" using namespace std; class Application { public: static Application& instance(); BOOL start(); BOOL showFatalError(const string&); HWND getHwnd(); HDC getHdc(); protected: static Application Instance; private: string className; string appTitle; string oglTitle; string dxTitle; UINT width; UINT height; int bits; WNDCLASSEX wc; HWND hWnd; HDC hDC; bool isFullscreen; static bool isActive; static bool keys[256]; Timer* renderTimer; int fps; int renderer; Application(); ~Application(); static LRESULT CALLBACK winProc(HWND, UINT, WPARAM, LPARAM); BOOL createWindow(); BOOL run(); VOID cleanup(); }; #endif
Реализация класса Application:
#include "Application.h" #include "Renderer.h" #define KEY_DOWN(vk_code) \ ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0) #define KEY_UP(vk_code) \ ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1) extern const int OPEN_GL_RENDERER; extern const int DIRECT_X_RENDERER; bool Application::isActive = true; bool Application::keys[256]; Application Application::Instance; /** * Constructor */ Application::Application() { className = "OGLDXWindowsApp010"; appTitle = "framework 0.1.0, by .p.i.x.e.l."; oglTitle = "OpenGL"; dxTitle = "DirectX"; width = 640; height = 480; bits = 32; hWnd = NULL; hDC = NULL; isFullscreen = true; renderTimer = new Timer(); fps = 60; renderer = OPEN_GL_RENDERER; // OPEN_GL_RENDERER or DIRECT_X_RENDERER } /** * Destructor */ Application::~Application() { // } /** * Access for instace */ Application& Application::instance() { return Instance; } /** * Startup application */ BOOL Application::start() { if( MessageBox( NULL, "Запустить приложение в полноэкранном режиме?", "Выбор режима окна", MB_YESNO | MB_ICONQUESTION ) == IDNO ) { isFullscreen = false; } if ( createWindow() ) run(); delete renderTimer; renderTimer = NULL; return FALSE; } /** * Window's handle */ HWND Application::getHwnd() { return hWnd; } /** * Device context */ HDC Application::getHdc() { return hDC; } /** * Main loop */ BOOL Application::run() { static DWORD ms = (DWORD)(1000 / fps); MSG msg; ZeroMemory( &msg, sizeof( msg ) ); while( msg.message != WM_QUIT ) { // keyboards processing will here... if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) ) { if( msg.message == WM_QUIT || KEY_DOWN(VK_ESCAPE) ) { cleanup(); PostQuitMessage( 0 ); } else { TranslateMessage( &msg ); DispatchMessage( &msg ); } } else if (isActive) { renderTimer->start(); Renderer::instance().render(); DWORD newTime = renderTimer->get(); if (newTime < ms) renderTimer->wait(ms - newTime); } if (isActive && keys[VK_F1]) { keys[VK_F1] = false; cleanup(); isFullscreen =! isFullscreen; createWindow(); } if (isActive && keys[VK_F2]) { keys[VK_F2] = false; cleanup(); renderer = 1 - renderer; createWindow(); } } return 0; } /** * Message for user about fatal error */ BOOL Application::showFatalError(const string& msg) { cleanup(); MessageBox(NULL, (char*)msg.c_str(), "Ошибка", MB_OK | MB_ICONSTOP); return FALSE; } /** * Windows message process procedure */ LRESULT CALLBACK Application::winProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch( msg ) { case WM_CLOSE: PostQuitMessage( 0 ); return 0; case WM_ACTIVATE: isActive = HIWORD( wParam ) ? false : true; return 0; case WM_SYSCOMMAND: /* TODO (#1#): add here alt+tab or alt+enter */ if (wParam == SC_SCREENSAVE || wParam == SC_MONITORPOWER) return 0; else break; case WM_PAINT: ValidateRect( hWnd, NULL ); return 0; case WM_SIZE: Renderer::instance().resize( (int)LOWORD(lParam), (int)HIWORD(lParam) ); return 0; case WM_KEYDOWN: keys[wParam] = true; return 0; case WM_KEYUP: keys[wParam] = false; return 0; } return DefWindowProc( hWnd, msg, wParam, lParam ); } /** * Creating window */ BOOL Application::createWindow() { DWORD dwExStyle; DWORD dwStyle; ZeroMemory(&wc, sizeof(wc)); wc.cbSize = sizeof(wc); wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpfnWndProc = winProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = GetModuleHandle( NULL ); wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = CreateSolidBrush(RGB(0,0,0)); wc.lpszMenuName = NULL; wc.lpszClassName = (char*)className.c_str(); wc.hIconSm = NULL; if ( !RegisterClassEx(&wc) ) { string msg = "Невозможно зарегистрировать класс окна: "; msg += className; return showFatalError(msg); } if (isFullscreen) { DEVMODE dmScreenSettings; ZeroMemory( &dmScreenSettings, sizeof( dmScreenSettings ) ); dmScreenSettings.dmSize = sizeof( dmScreenSettings ); dmScreenSettings.dmPelsWidth = width; dmScreenSettings.dmPelsHeight = height; dmScreenSettings.dmBitsPerPel = bits; dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT; // Possible switch in fullscreen if( ChangeDisplaySettings( &dmScreenSettings, CDS_FULLSCREEN ) != DISP_CHANGE_SUCCESSFUL ) { if( MessageBox( NULL, "Полноэкранный режим не поддерживается вашей видеокартой. Запустить в оконном режиме?", "Ошибка", MB_YESNO | MB_ICONEXCLAMATION ) == IDYES ) { isFullscreen = false; } else { return showFatalError("Программа будет закрыта"); } } } if (isFullscreen) { dwExStyle = WS_EX_APPWINDOW; dwStyle = WS_POPUP; ShowCursor( false ); } else { dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; dwStyle = WS_OVERLAPPEDWINDOW; } hWnd = CreateWindowEx( dwExStyle, (char*)className.c_str(), (char*)((renderer ? dxTitle : oglTitle) + ": " + appTitle).c_str(), WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, width, height, NULL, NULL, wc.hInstance, NULL ); if (!hWnd) { string txt = "Невозможно создать окно: "; txt += appTitle; return showFatalError(txt); } if ( !( hDC = GetDC( hWnd ) ) ) return showFatalError("Невозможно создать контекст устройства!"); if ( !Renderer::instance().activate(renderer) ) return FALSE; ShowWindow( hWnd, SW_SHOW ); SetForegroundWindow( hWnd ); SetFocus( hWnd ); Renderer::instance().resize( width, height ); Renderer::instance().initialize(); return TRUE; } /** * Release resources */ VOID Application::cleanup() { if (isFullscreen) { ChangeDisplaySettings( NULL, 0 ); ShowCursor( true ); } Renderer::instance().cleanup(); if (hDC) { ReleaseDC( hWnd, hDC ); hDC = NULL; } if (hWnd) { DestroyWindow(hWnd); hWnd = NULL; } UnregisterClass((char*)className.c_str(), wc.hInstance); }
Надо отметить, что класс Application реализован как singleton. Думаю, не стоит говорить почему. И для его функционирования я решил выделить связанное с таймером в отдельный класс Timer. Его можно использовать для работы со временем в приложении. Объявление класса Timer:
#include <windows.h> class Timer { public: Timer(); ~Timer(); void start(); DWORD get(); void wait(DWORD count); void reset(); private: DWORD start_millis; };
и реализация:
#include "Timer.h" Timer::Timer() { start_millis = 0; } Timer::~Timer() { // } void Timer::start () { start_millis = GetTickCount(); } DWORD Timer::get () { return (DWORD)(GetTickCount() - start_millis); } void Timer::wait (DWORD count) { Sleep(count); } void Timer::reset () { start_millis = GetTickCount(); }
Теперь, для того чтобы отделить процесс создания окна от подсоединения к нему интерфесов 3-мерной графики мы абстрагируемся в класс Renderer в котором и будем выбирать контекст визуализации. Вот его декларирование:
#ifndef _RENDERER_H #define _RENDERER_H #include <windows.h> #include "Application.h" const int OPEN_GL_RENDERER = 0; const int DIRECT_X_RENDERER = 1; class Renderer { public: static Renderer& instance(); BOOL activate(int); VOID initialize(); VOID resize(int width, int height, float fov = 45.f, float _near = .1f, float _far = 1000.f); VOID render(); VOID cleanup(); protected: static Renderer Instance; private: int mode; Renderer(); ~Renderer(); }; #endif
а вот реализация:
#include "Renderer.h" #include "OglRenderer.h" #include "DxRenderer.h" Renderer Renderer::Instance; /** * Constructor */ Renderer::Renderer() { // } /** * Destructor */ Renderer::~Renderer() { // } /** * Access for instace */ Renderer& Renderer::instance() { return Instance; } /** * Connect render API to device context */ BOOL Renderer::activate(int renderer) { mode = renderer; if (mode == OPEN_GL_RENDERER) OglRenderer::instance().activate(); if (mode == DIRECT_X_RENDERER) DxRenderer::instance().activate(); return TRUE; } /** * Release resources */ VOID Renderer::cleanup() { if (mode == OPEN_GL_RENDERER) OglRenderer::instance().cleanup(); if (mode == DIRECT_X_RENDERER) DxRenderer::instance().cleanup(); } /** * Configure render API */ VOID Renderer::initialize() { if (mode == OPEN_GL_RENDERER) OglRenderer::instance().initialize(); if (mode == DIRECT_X_RENDERER) DxRenderer::instance().initialize(); } /** * Resize view */ VOID Renderer::resize(int width, int height, float fov, float _near, float _far) { if (height == 0) height = 1; if (mode == OPEN_GL_RENDERER) OglRenderer::instance().resize(width, height, fov, _near, _far); if (mode == DIRECT_X_RENDERER) DxRenderer::instance().resize(width, height, fov, _near, _far); } /** * Render frame */ VOID Renderer::render() { if (mode == OPEN_GL_RENDERER) OglRenderer::instance().render(); if (mode == DIRECT_X_RENDERER) DxRenderer::instance().render(); }
Буду очень признателен за обоснованную критику или предложения по усовершенствованию.
Загрузить все файлы можно здесь.
Используемая информация (кого вспомнил):
- Ричард С. Райт-мл., Бенджамин Липчак. OpenGL. Суперкнига
- Уроки от NeHe (есть и на русском)
- Андре Ламот. Программирование трехмерных игр для Windows. Советы профессионала по трехмерной графике и растеризации.
- Герберт Шилдт. Полный справочник по C++
- Николас А. Солтер, Скотт Дж. Клепер. C++ для профессионалов
- MSDN :)
4 комментария:
...
VOID Renderer::resize(int width, int height, float fov, float _near, float _far)
{
if (height == 0) height = 1;
if (mode == OPEN_GL_RENDERER)
OglRenderer::instance().resize(width, height, fov, _near, _far);
if (mode == DIRECT_X_RENDERER)
DxRenderer::instance().resize(width, height, fov, _near, _far);
}
Вместо подобных конструкций лучше иметь базовый абстрактный класс IRenderer с нуливыми виртульными методами типа resize =)
Вообще переключение в реальном времени между двумя API редко когда используется.
Согласен.
Интересно, выложить на гитхаб? Или приложуха лажа ненужная?
Автор, все классно! НО!!!
ПОЖАЛУЙСТА!
НИКОГДА И НИГДЕ БОЛЬШЕ!
НЕ ИСПОЛЬЗУЙ ДЕЕПРИЧАСТИЯ!
Да, с деепричастием фраза выходит короче и приятнее... Если оно написано правильно!
Одну косячную фразу можно пропустить, но у тебя каждое предложение с этой блевотной конструкцией "прогулявшись по лесу, мои ноги пришли домой".
УБЕРИ УБЕРИ УБЕРИ!!!
Спасибо за критику! Но у меня по русскому была конкретная 3, поэтому я уже и не вспомню, что такое деепричастие :)
P.S. Согласен, что за чистоту языка бороться надо.
Отправить комментарий