10 июня 2011 г.

Фреймворк для учебных примеров OpenGL и DirectX на C++

В своё время, выполняя множество уроков по 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();
}

Буду очень признателен за обоснованную критику или предложения по усовершенствованию.

Загрузить все файлы можно здесь.

Используемая информация (кого вспомнил):

4 комментария:

Alexander Samusev комментирует...

...
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. Согласен, что за чистоту языка бороться надо.

Отправить комментарий