Метка «игровые движки»

Здравствуйте!

Статья предназначена для тех, кто ведет разработку на Torque3D и умеет компилировать движок в VisualStudio, а не только для тех, кто пользуется WorldEditor и пишет/дописывает torqueScript *.cs скрипты.

В Torque3D 3.5 есть небольшой баг с нормалями на кромках террейна (Terrain). Именно он мешает создавать множество террейнов «без швов» (и не только он). Если запустить какую нибудь демку, например, Empty, открыть в ней WorldEditor, создать 2 террейна, подогнать их друг к другу и попробовать «нарисовать кисточкой высоту» в области «склейки» этих террейнов, то мы увидим неприятный баг. Похожий баг можно увидеть, если мы создадим всего лишь 1 террейн и попробуем отредактировать высоту в областях кромок этого террейна.

Что есть террейн?

Террейн — это регулярная сетка ячеек с высотами ( имеющая размеры 256, 512, 1024 — кратная степени двойки), которая легко преобразуется в поверхность (mesh).
У меша есть не только полигоны и вершины, но также и нормали к вершинам, которые как раз дают нужную освещаемость конкретной вершины.

Дело в том, что в движке Torque3D нормаль в точке x,y рассчитывается исходя из разниц высот:
normal( height(x+1, y) — height (x — 1, y), height(x, y+1) — height(x, y-1), 'какая то постоянная высота' ),
где float height( int x, int y ) — функция взятия высоты по целочисленным координатам регулярной сетки ячеек в террейне.

Да, в Torque3D координата по Z — это высота.

Возникает вопрос: а если x+1 или x-1 или y+1 или y-1 выходят за границу террейна?
Тогда берется точка на противоположенной стороне террейна и возникает артефакт в виде необоснованной повышенной/пониженной освещенности такой «пограничной» вершины. Т.е. если, например, x=0, то x-1 = -1, который превращается в size — 1 (размер террейна минус единичка, например 256 — 1 = 255).

Почему так превращается? Потому что вот функция, возвращающая высоту в точке x, y:

inline U16 TerrainFile::getHeight( U32 x, U32 y ) const
{
x %= mSize;
y %= mSize;
return mHeightMap[ x + ( y * mSize ) ];
}

где mSize — это размер террейна, например 256, а оператор % берет остаток от деления. Тип U32 — это беззнаковый тип, а вызывается эта функция из функции TerrainBlock::getSmoothNormal(...) вот здесь:

F32 h1 = fixedToFloat( mFile->getHeight( x+1, y ) );
F32 h2 = fixedToFloat( mFile->getHeight( x, y+1 ) );
F32 h3 = fixedToFloat( mFile->getHeight( x-1, y ) );
F32 h4 = fixedToFloat( mFile->getHeight( x, y-1 ) );

а x и y они типа S32, что означает знаковый тип. Если приходит -1 на вход этой функции, то значение автоматически преобразуется к беззнаковому, причем к предельному (максимальному) для 32 битных чисел. Это будет 2^32 — 1 = 4 294 967 295 и остаток от деления на 256 будет 255.
4 294 967 295 % 256 = 255.

Я подумал, что нам такого не надо и решил немного подкорректировать исходный код Torque, попутно запилив некий функционал.

Функционал заключается в следующем:
если x-1 < 0 или x+1 > mSize — 1 или y-1<0 или y+1> mSize — 1, то значение высоты в данной точке брать не с текущего террейна, а с тех террейнов, которые мы указали соответствующими соседями.

Для этого поменялся инспектор в редакторе мира для террейна, теперь там есть вкладка «Attached terrains» и поля «ForwardId», «BackwardId», «LeftId» и «RightId», куда пользователь ручками вбивает id'шники соседних террейнов (которые он сочтет нужными, хаха).

Для того, чтобы поменять инспектор, был вставлен код в функцию void TerrainBlock::initPersistFields():

addGroup( "Media" );

addProtectedField( "terrainFile", TypeStringFilename, Offset( mTerrFileName, TerrainBlock ), &TerrainBlock::_setTerrainFile, &defaultProtectedGetFn,"The source terrain data file." );

endGroup( "Media" );

// Вкладка в инспекторе WorldEditor'а для "соседей" террейна
addGroup( "Attached terrains" );
addField( "ForwardId", TypeS32, Offset( m_Forward, TerrainBlock ), "Id of forward attached terrain" );
addField( "BackwardId", TypeS32, Offset( m_Backward, TerrainBlock ), "Id of backward attached terrain" );
addField( "LeftId", TypeS32, Offset( m_Left, TerrainBlock ), "Id of left attached terrain" );
addField( "RightId", TypeS32, Offset( m_Right, TerrainBlock ), "Id of right attached terrain" );
endGroup( "Attached terrains" );
// end of Вкладка

addGroup( "Misc" );
...

В прототип класса class TerrainBlock : public SceneObject были вставлены поля:
S32 m_Forward;
S32 m_Backward;
S32 m_Left;
S32 m_Right;

То есть у меня выглядит вот так:
...
///
FileName mTerrFileName;

/// Attached terrains Соседи террейна
S32 m_Forward;
S32 m_Backward;
S32 m_Left;
S32 m_Right;

/// The maximum detail distance found in the material list.
F32 mMaxDet

Для того, чтобы эти поля сериализовались, надо вставить код в функции TerrainBlock::packUpdate и TerrainBlock::unpackUpdate.

В TerrainBlock::packUpdate вставляется:

// для соседей террейна
if( stream->writeFlag( mask & NextFreeMask ) )
{
stream->write( m_Forward );
stream->write( m_Backward );
stream->write( m_Left );
stream->write( m_Right );
}
//
После строчек:
if ( stream->writeFlag( mask & FileMask ) )
{
stream->write( mTerrFileName );
stream->write( mCRC );
}

В TerrainBlock::unpackUpdate нужно вставить:

// Соседи террейна
if ( stream->readFlag() )
{
stream->read( &m_Forward );
stream->read( &m_Backward );
stream->read( &m_Left );
stream->read( &m_Right );
}
// end of Соседи террейна

После строчек:

if ( stream->readFlag() ) // FileMask
{
FileName terrFile;
stream->read( &terrFile );
stream->read( &mCRC );

if ( isProperlyAdded() )
setFile( terrFile );
else
mTerrFileName = terrFile;
}

Внимание! Код в этих двух функциях должен быть в определенном порядке, а не абы где. Именно там, где я указал, идет работа со stream'ом (наподобии STLвского стрима).

Теперь дошла очередь и до самой функции TerrainBlock::getSmoothNormal(...).

Изменения в terrData.cpp.

Сначала перед самой функцией TerrainBlock::getSmoothNormal(...) надо объявить:

namespace Sim
{
// Defined in simManager.cpp
extern SimIdDictionary *gIdDictionary;
}

Это объявит неймспейс локально прямо в этом файле реализации и продекларирует, что есть такая переменная SimIdDictionary *gIdDictionary.
Прошу заметить, это не создание новой глобальной переменной Sim::gIdDictionary, а лишь указание компилятору (впоследствии линковщику), что такая переменная уже есть в какой то другой единице трансляции — в другом *.cpp файле (а следовательно — в другом *.obj файле, после того как компилятор скушает *.cpp)

По этому указателю расположено глобальное хранилище всех Id для Sim объектов со своим интерфейсом (все объекты видимые в редакторе мира игры — террейны, мешы, солнце, плейны, точки респауна, инфо о левеле, и т.д. — являются потомками SimObject и имеют свой SimId).

В функции TerrainBlock::getSmoothNormal(...) после строчек

const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( skipEmpty && sq->flags & TerrainSquare::Empty )
return false;

Вставляю следующий код:

Resource File1, File2, File3, File4;
File1 = File2 = File3 = File4 = mFile;

S32 x1 = x + 1;
S32 x2 = x — 1;
S32 y1 = y + 1;
S32 y2 = y — 1;

if( x1 > mFile->mSize — 1 )
{
x1 = mFile->mSize — 1;
TerrainBlock *rBlk = dynamic_cast<TerrainBlock*>( Sim::gIdDictionary->find( m_Right ) );
if( rBlk )
{
x1 = 1;
File1 = rBlk->mFile;
}
}

if( y1 > mFile->mSize — 1 )
{
y1 = mFile->mSize — 1;
TerrainBlock *fBlk = dynamic_cast<TerrainBlock*>( Sim::gIdDictionary->find( m_Forward ) );
if( fBlk )
{
y1 = 1;
File2 = fBlk->mFile;
}
}

if( x2 < 0 )
{
x2 = 0;
TerrainBlock *lBlk = dynamic_cast<TerrainBlock*>( Sim::gIdDictionary->find( m_Left ) );
if( lBlk )
{
x2 = lBlk->mFile->mSize — 2;
File3 = lBlk->mFile;
}
}

if( y2 < 0 )
{
y2 = 0;
TerrainBlock *bBlk = dynamic_cast<TerrainBlock*>( Sim::gIdDictionary->find( m_Backward ) );
if( bBlk )
{
y2 = bBlk->mFile->mSize - 2;
File4 = bBlk->mFile;
}
}

А строчки:

F32 h1 = fixedToFloat( mFile->getHeight( x+1, y ) );
F32 h2 = fixedToFloat( mFile->getHeight( x, y+1 ) );
F32 h3 = fixedToFloat( mFile->getHeight( x-1, y ) );
F32 h4 = fixedToFloat( mFile->getHeight( x, y-1 ) );

Заменить строчками:

F32 h1 = fixedToFloat( File1->getHeight( x1, y ) );
F32 h2 = fixedToFloat( File2->getHeight( x, y1 ) );
F32 h3 = fixedToFloat( File3->getHeight( x2, y ) );
F32 h4 = fixedToFloat( File4->getHeight( x, y2 ) );

Вроде бы все! Перекомпилировать — и ПРОФИТ! Ура!

Кстати, можно оптимизировать по скорости, то есть указатель на соседний террейн не искать в функции TerrainBlock::getSmoothNormal(...), как здесь: например:

TerrainBlock *rBlk = dynamic_cast<TerrainBlock*>( Sim::gIdDictionary->find( m_Right ) );

Ибо функция сглаживания нормали вызывается очень много раз( mSize*mSize раз ) для каждой вершины террейна, если террейн поднимается/опускается/сглаживается.

Тогда нужно создать 4 соответствующих указателя как поля класса и, скорее всего, инициализировать их там, где происходит «сериализация» (скорее всего ф-ции TerrainBlock::packUpdate или TerrainBlock::unpackUpdate).

Если бы была возможность вставить сюда измененные файлы исходников — с удовольствием вставил бы!

ТоркEnginesourceterrainterrData.cpp
и
ТоркEnginesourceterrainterrData.h

Для того, чтобы увидеть результат — заменить файл, перекомпилировать, запустить, создать до фига террейнов с монотонной текстурой, назначить соответствующих соседей в инспекторе, сохранить сцену и можно либо перезапустить, либо подредактировать высоты террейнов для запуска процедуры пересчета его нормалей.

Пока фиксит только нормали. Для сглаживания текстур — отдельная история.

Кстати, если соседей не назначить, то нормали будут пересчитываться все равно более адекватно — высоты с противоположенных сторон браться не будут — не будет ужасных непонятных артефактов, будет браться высота текущей точки, для которой и рассчитывается нормаль.

Выглядеть будет офигенчиком как здесь, использована монотонная текстура.
imageЧитать полностью »

ID Software продолжает публиковать исходники Doom III

ID Software начала выкладывать на Гитхаб исходники своих игровых движков около года назад. Теперь репозиторий компании пополнился движком Doom III BFG edition. Это обновлённая версия движка Doom III с улучшенной графикой, звуковыми эффектами, поддержкой трёхмерных дисплеев, шлемов виртуальной реальности. Полный список отличий можно найти здесь. Код опубликован под лицензией GNU GPL.

Читать полностью »

Теперь Unity3D Beta доступна для скачивания.

Читать полностью »

На днях мы пообщались с Олегом Придиуком (Oleg Pridiuk), техническим евангелистом из Unity Technologies. Он рассказал о том, кому стоит работать с Unity 3D, в чем преимущества этого движка по сравнению с другими и как им удается не портить свою карму.

Это не перевод и не перепечатка, интервью эксклюзивное и пока только для Хабра.

Про Unity 3D из первых рук. Интервью
Читать полностью »


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js