В своё время, выполняя множество уроков по 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. Согласен, что за чистоту языка бороться надо.
Отправить комментарий