31 января 2015 31.01.15 6 2462

Разработка игры. Бродилка. [Лог#7]

+18

Темой этого лога станет рендер. Расскажу немного о том как я себе представляю виртуального художника и почему лучше реализовать интерфейс IRender и класс-реализацию CRenderOGL, например.

Хочется провести некоторые аналогии, которые по моему очень чётко вписываются.
Класс «рисовальщик» именовал я раньше как RenderDevice, но что это такое вообще? Зачем какие-то классы городить, когда есть, например, OpenGL? Конечно для удобства использования.
Класс IRenderDevice можно назвать художником, причём очень хорошо ему это имя подходит.
Можно сказать художнику «нарисуй мне вот тут квадрат», собственно это было бы эквивалентно такому вот вызову в коде «pRenderDevice->DrawQuad(… )».
Художник не может нарисовать ничего, если у него нет холста, красок и кистей, и тут вновь можно провести аналогию.
Роль холста будет играть «контекст устройства»( как я понимаю это просто особая видеопамять, вернее её часть) — имеется у всех окон. Тут всё ясно холст — окно.
Кисти или их эквивалент — модели и геометрия, даже не имеет значения 2Д или 3Д. Возможно, кто-то знает, что геометрия(статичная?) в играх на движке Source, idTech и UnrealEngine называется Brush ( кисть ) — наверно просто совпадение, не думаю.
Кисть по холсту художник проведёт, но ничего не нарисует, поэтому нужны краскитекстуры, материалы, шейдеры (shaders). Думаю многие знают что из себя представляет шейдер — это подпрограмма для видеокарты, которая говорит как будет рисоваться 3Д модель. Экранный пост-процессы \ пост-эффекты — это тоже шейдер, например, свечение (glow, bloom), переход картинки в серость\цвет ( такой эффект был в игре Saboteur ).

Аналогии очень хорошо описывают набор классов графической библиотеки, однако, надо заметить, что нет конкретики. Это некая абстракция.
IRenderDevice — художник, а CRenderDeviceOGL — какой-нибудь Вася Пупкин, который вообще то тоже художник, но как видно уже конкретный.

Примерно вот так может выглядеть класс, вернее интерфейс для «художника»:

class IRenderDevice
{
public:
// Первичная инициализация GAPI-устройства
virtual TResult Init( WindowT *Window, int param ) = 0;

// Инициализировать графический контекст для конкретного окна приложения
virtual TResult InitContextForWindow( WindowT *Window ) = 0;

// Освободить граф.контекст окна
virtual TResult CleanupContextForWindow( WindowT *Window ) = 0;

// Сделать текущим(главным) контекст окна
virtual TResult MakeCurrent( WindowT *Window ) = 0;

// Сменить буфер изображения окна
virtual TResult SwapBuffer( WindowT *Window ) = 0;

// Финальная очистка граф.устройства
virtual TResult Cleanup() = 0;

virtual void ApplyView( GView * View ) = 0;
virtual void SetMaterial( GMaterial * material ) = 0;
virtual void DrawMesh( GMesh * Mesh ) = 0;
};

У методов этого класса нет реализации (конкретного кода) и они все виртуальные. Видно, что несколько первых методов — это инициализация, обновление кадра и освобождение ресурсов ( Init, SwapBuffer и Cleanup соответственно) — эти функции платформозависимые, что не есть хорошо вообще то. Но можно чуть-чуть избавится от зависимости «спрятав» зависимые от платформы типы и функции, например, тип переменной WindowT — произвольный (определённый мной, а не платформой), внутри которого есть платформозависимые переменные. Это сделано для того, чтобы не менять вызов функции, например:
// Этот код не нужно будет менять для каждой платформы
pRenderDevice->Init( pMainWindow, 0 );

Для каждой ОС придётся лишь менять\написать код для класса WindowT и некоторые части от RenderDevice ( и\или других систем ).
Насколько это правильное решение — не знаю точно, возможно это вообще плохое решение.

Последние три функции интерфейса IRenderDevice — «применить вид», «установить материал» и «нарисовать модель». Конечно это не полный список функций, который пригодится, но для очень простых игр думаю даже такого хватит.
Функция ApplyView, которая принимает указатель на некий объект класса GView — особая штука, которой я можно сказать горжусь (хотя на самом деле нечем). GView — это что-то вроде виртуальной камеры, границ вида (вьюпорт \ viewport ) и матрицы проекции. Хранит в себе две матрицы — проекции (перспективная\ортогональная) и вида (положение и направление камеры).

Классы материала и сетки(mesh) вообще могут быть какими угодно, вернее информация которая в них хранится может быть различной и зависеть от «старшей» графической библиотеки (OpenGL \ DirectX).

Но не только материал и 3Д модель зависит от выбора библиотеки, собственно вся реализация интерфейса зависит от неё. Вот например если взять OpenGL, то может быть что-то вроде этого:

class GRenderDevice: public IRenderDevice
{
public:
GRenderDevice();
~GRenderDevice();

// Первичная инициализация GAPI-устройства
TResult Init( WindowT *Window, int param );
// Инициализировать графический контекст для конкретного окна приложения
TResult InitContextForWindow( WindowT *Window );
// Освободить граф.контекст окна
TResult CleanupContextForWindow( WindowT *Window );
// Сделать текущим(главным) контекст окна
TResult MakeCurrent( WindowT *Window );
// Сменить буфер изображения окна
TResult SwapBuffer( WindowT *Window );
// Финальная очистка граф.устройства
TResult Cleanup();

void ApplyView( GView * View );
void DrawMesh( GMesh * Mesh );

void DrawVText( float x, float y, const char *strText, float size, bool bReversY = false );

// Flags = 1 — color, 2 — depth, 4 — stencil
enum{
BIT_COLOR = 0x1,
BIT_DEPTH = 0x2,
BIT_STENCIL=0x4
};
void ClearScreen( int Flags = 0 );
void ClearScreenColor( const float r, const float g, const float b, const float a );

void BindTexture( GTexture * Texture );
void SetWorldMatrix( const float * matrix );
void SetWireframe( bool show = true );

void DrawRect( float x, float y, float xx, float yy );

protected:
TResult InitExtensions();
HGLRC m_hContext; // На самом деле зависимая от платформы переменная
WindowT *m_pOwnerWindow;
};

Видно, что функций больше, да и переменные появились. (не забывайте, что это примеры, а в реальном проекте будет иначе). Можно создавать экземпляр этого класса и пользоваться всеми этими функциями, однако, если нужна независимость от OpenGL\DirectX, то нужно использовать IRenderDevice у которого функций меньше. Зачем тогда использовать этот урезанный класс? Дело в том, что реализацию художника можно загружать из DLL (если это реализовано, конечно), например, так сделано в Quake2 — рендер и механика игры загружаются из DLL.

Ну вот скажем та же функция ApplyView для OpenGL реализации будет выглядеть примерно так:

void GRenderDevice::ApplyView( GView * View )
{
if( !View ) return;
// Задать вьюпорт
View->SetViewport( 0, 0, GetBufferWidth(), GetBufferHeight() );

glViewport( View->GetX(), View->GetY(), View->GetWidth(), View->GetHeight() );

// Утсановить матрицы (OpenGL-специичные функции)
glMatrixMode( GL_PROJECTION );
glLoadMatrixd( &View->GetProjectionMatrix().x[0][0] );
glMatrixMode( GL_MODELVIEW );
glLoadMatrixd( &(View->GetViewMatrix()).x[0][0] );
}

Или вот функция рисования сетки:
void GRenderDevice::DrawMesh( GMesh * Mesh )
{
if( !Mesh ) return;

GVertex3D * V = Mesh->GetVertexArray();
unsigned int Count = Mesh->GetCountVertex();

// вновь специфичные функции
glVertexPointer( 3, GL_FLOAT, sizeof(GVertex3D), &(V[0].x) );
glNormalPointer( GL_FLOAT, sizeof(GVertex3D), &(V[0].nx) );
glTexCoordPointer( 2, GL_FLOAT, sizeof(GVertex3D), &(V[0].tu) );

// получить количество под-сеток
unsigned int count_batch = Mesh->GetCountBatches();

// для каждой под-сетки…
for( unsigned int i=0; i< count_batch; ++i )
{
GBatch *b = 0;
if( b = Mesh->GetBatch(i) )
{

if( b->m_MaterialID != -1 )
{
// установить текстуру
BindTexture( FindTextureByID( b->m_MaterialID ) );
}

// Нарисовать треугольники
glDrawElements( GL_TRIANGLES, b->GetCount(), GL_UNSIGNED_INT, b->GetPtr() );
}
}
}

Для DirectX реализации были бы совсем другие функции, но смыл бы не изменился.
Поэтому вызывая pRenderDevice->ApplyView( pView ) будет срабатывать специфичный код.

Честно говоря не очень хорошо получается у меня объяснить весь смысл, да что там — вообще не понятно что я тут написал :D

Конечно хотелось бы больше конкретного кода, но не учитель я :(
Да ещё и версия OpenGL, которую я ранее и использовал устарела, теперь придётся чуть обновить свои знания и подтянуть версию хотя бы OpenGL 3.3.

Что дальше ?
Думаю либо чуть увеличить разрыв между логами, либо небольшую паузу взять. Думаю начать уже конкретно проектировать само приложение (не писать код) и сделать небольшой альфа-прототип на каком-нибудь Unity (хотел вообще на GameMaker, но похоже с новым обновлением они отказались от WinXP Steam отказывается запускать GM).

Заметьте ни одной картинки D:!


Лучшие комментарии

Вообще это ПК, но не ПК вовсе так как это не ПэКа, хотя вообще то ПК, если посмотреть со стороны банана.
Ну что, я поднял перчатку принял вызов.
Если честно, то неудобно в бункере с одинаковыми помещениями без карты и эти крутящиеся штуки меня нервируют.
Я думаю, что мало кто будет читать код, еще и без выравнивания, всем интересны картинки, но ты молодец. Такими темпами, вселенная точно заставит меня что-нибуть сделать.
принял вызов

Молодец ) Какие впечатления от этой поделки?

Я думаю, что мало кто будет читать код, еще и без выравнивания, всем интересны картинки

Пришёл к аналогичному выводу — картинки интереснее. Форматирование стопгейма не держит символы табуляции и подсветку кода (а нужно ли это?), поэтому читабельность кода падает.
Какие впечатления от этой поделки?

Неожиданная концовка, я ожидал fallout: «Ваши радиоактивные останки остались лежать на выжженной пустыне и даже пожиратели трупов не отобедали ими». Можно покататься на пауках)
Если бы был fallout-стайл, то это было бы жестоко наверно )
Да, с пауками и некоторыми моментами есть недочёты, по большей части в столкновениях и физике.

Похоже со sg.ru только ты и поиграл, честно ожидал большего :D
Если лень качать и проходить, то вот есть летсплей запись стрима который надо перемотать на 53 минуту, но можете посмотреть и другие игры конечно. Спасибо Коту за этот стрим кстати :)
Кстати, вот подумал о сюжете и персонажах и пришёл к очень интересной на мой взгляд штуке — продолжить историю другой игры, но долго сомневался надо ли сделать про кого-то нового или оставить старого перса. Изначально бродилка и задумывалась как продолжение. Короче вот небольшая игруля сделанная на конкурс Gaminator15 (но выкладываю пост-конкурсную версию) — ссылка на архив, кто пройдёт от начала и до конца — тому «респект и уващужа вообще!» :)
Читай также