Шрифты в Direct3D.

Вступление.

Недавно, во время написания очередного D3D приложения, возникла острая необходимость в выводе текста. После некоторых поисков, оказалось, что DX8SDK не в состоянии предложить мне что-то, что меня удовлетворило бы в полной мере. Поэтому среди ночи, когда сознание уже практически покинуло меня, было решено написать свой класс (с возможностью дальнейшего расширения и улучшения) для вывода текстовой информации.

Реализация.

к началу статьи
Парадигма будет такая же, как и при работе с текстурами – класс-оболочка для самого шрифта плюс менеджер для загрузки и манипулирования. По-правде говоря, все манипулирование не выйдет за рамки хранения шрифтов в массиве. Динамическая загрузка/выгрузка ведь не требуется (вряд ли Вы будете их тысячами грузить). Ставя ТЗ, я сразу решил, что шрифты будут грузиться из текстуры. Соответственно придется отдельно хранить ширину букв (в SDK шрифты брались стандартные, а следовательно и ширина букв была стандартная – существенное ограничение). После проектирования получился вот такой класс:
class	cD3DFont{
__forceinline void FillVData( char * , float , float , float , float );
public:
cTexture FontTexture;
cD3DFont( void ){FontTextureID=-1;}
cD3DFont( float ){FontTextureID=-1;}
cArray<float> LettersWidth;
cD3DVertexBuffer VData;
int FontTextureID;

void PrintString( LPDIRECT3DDEVICE8 , char * , float ,
float , float , float );
void SetString( char * , float , float , float , float );
void AppendString( char * , float , float , float , float );
void AppendString( char * , float );
void AppendFloat( float , float );
void AppendFloat( float , float , float , float , float );
void PrintString( LPDIRECT3DDEVICE8 );

void LoadFont( LPDIRECT3DDEVICE8 , char * , char * );
void LoadFont( char * , char * );
};
Сначала нам неплохо бы загрузить шрифт. Шрифт определяют текстура (объект или курсор на текстуру в TextureManager’е) и массив float’ов, в котором хранятся ширина каждой буквы (стандартная высота буквы = 1).
void	cD3DFont::LoadFont( LPDIRECT3DDEVICE8 D3DDevice , char * tex_name , 
char * desc_path ){
ValidatePath( tex_name , "Неправильный путь к текстуре при инициализации шрифта." );

FontTexture.CreateTextureFromFile( D3DDevice , tex_name );

FontTexture.RecreateWithAlpha( D3DDevice );

FontTexture.SetTransparent( 0x00ffffff );

ValidatePath( desc_path , "Неправильный путь к описателю шрифта." );

FILE *f_stream = fopen( desc_path , "rb" );

char directive[ STRING_LENGTH ];

for( int i( 0 ) ; i < 256 ; i++ )
LettersWidth.AddEnd( 1 );

for( ; EOF != fscanf( f_stream , "%s" , directive ) ; ){
if( directive[ 0 ] == 'v' && directive[ 1 ] == '_' ){
float CharWidth( 1 );

int CharCode( atoi( directive + 2 ) );

fscanf( f_stream , "%f" , &CharWidth );

LettersWidth[ CcharCode ] = CharWidth;
}
}
}
Здесь первый параметр – девайс, второй параметр – путь к текстуре, третий параметр – путь к описателю ширин букв. Функцией ValidatePath проверяем существование файлов, затем грузим текстуру, затем (ВНИМАНИЕ) пересоздаем текстуру, так что бы она была с альфа каналом (я использовал *.bmp, поэтому такое преобразование было необходимо), затем выставляем альфу в зависимости от цвета текселя. Создаем ширины букв по умолчанию (= =1). Осталось только считать файл с описателями ширин. Как Вы видите из кода, описатели могут быть не для всех 256 символов. Для знаков, чью ширину Вы не определите явно, будет присвоено значение по умолчанию. Второй вариант функции:
void	cD3DFont::LoadFont( char * tex_name , char * desc_path ){

FontTextureID = TextureManager.GetTextureCursor( tex_name );

ValidatePath( desc_path , "Неправильный путь к описателю шрифта." );

FILE *f_stream = fopen( desc_path , "rb" );

char directive[ STRING_LENGTH ];

for( int i( 0 ) ; i < 256 ; i++ )
LettersWidth.AddEnd( 1 );

for( ; EOF != fscanf( f_stream , "%s" , directive ) ; ){
if( directive[ 0 ] == 'v' && directive[ 1 ] == '_' ){
float CharWidth( 1 );

int CharCode( atoi( directive + 2 ) );

fscanf( f_stream , "%f" , &CharWidth );

LettersWidth[ CcharCode ] = CharWidth;
}
}
}
Все то же самое, только вместо создания текстуры, мы вытаскиваем из менеджера её курсор. Первая функция предназначена исключительно для “ручного” создания. Т.е. если Вам понадобится просто по - быстрому сварганить какую-нибудь демку, то эта функция будет очень кстати. Вторая функция по моему замыслу будет использоваться только в менеджере шрифтов во время загрузки, хотя ничего не мешает Вам вызвать эту функцию самостоятельно.

Все загружено, теперь можно и писАть.

void	cD3DFont::PrintString( LPDIRECT3DDEVICE8 D3DDevice , char *str , float x , 
float y , float z , float s ){
VData.Vertexes.Cursor = 0;
FillVData( str , x , y , z , s );

D3DDevice->SetRenderState( D3DRS_ALPHABLENDENABLE , true );
D3DDevice->SetRenderState( D3DRS_SRCBLEND , D3DBLEND_SRCALPHA );
D3DDevice->SetRenderState( D3DRS_DESTBLEND , D3DBLEND_INVSRCALPHA );

if( FontTextureID != -1 )
TextureManager[ FontTextureID ].SetTexture( D3DDevice , 0 );
else
FontTexture.SetTexture( D3DDevice , 0 );
VData.Render( D3DDevice , FVF_CD3DVERTEX );
if( FontTextureID != -1 )
TextureManager[ FontTextureID ].ResetTexture( D3DDevice , 0 );
else
FontTexture.ResetTexture( D3DDevice , 0 );

D3DDevice->SetRenderState( D3DRS_ALPHABLENDENABLE , false );
}
Эта функция принимает в качестве параметра строку, положение в абсолютных координатах (текст располагается в плоскости XY), и масштаб – коэффициент растяжения по осям X и Y (делать два коэффициента я не вижу особого смысла) и конечно же девайс (куда уж без него!). Затем обнуляем массив в котором хранятся полигоны, на которых будет выводится текст (по два полигона на букву). Затем заполняем этот вершинный буфер. Устанавливаем настройки вывода (эти три SetRenderState включают и настраивают самый простой вывод с альфа каналом). Затем проверяем: текстура загружена или в нашем распоряжении только курсор, устанавливаем текстуру и рендеррим содержимое буфера. После чего отключаем альфа тест. Теперь функция FillVData
__forceinline void	cD3DFont::FillVData( char *str , float x , 
float y , float z , float s ){
float char_incomex( 0 );
float char_incomey( 0 );
int VData_Cursor( VData.Vertexes.Cursor );

for( int i( 0 ) ; i < strlen( str ) ; i++ ){
if( str[ i ] == '\n' ){
char_incomex = 0;
char_incomey -= s;
goto end;
}
int m( ( ( unsigned char )str[ i ] ) % 16 );
int n( ( ( unsigned char )str[ i ] ) / 16 );

int c( n * 16 + m );

char Number[100];

float dtc( 1.0 / 16.0 );

VData.AddVertex( cD3DVertex( char_incomex , char_incomey + s ,
0 , 0xffffff , m * dtc , n * dtc ) );
VData.AddVertex( cD3DVertex( char_incomex , char_incomey , 0 ,
0xffffff , m * dtc , ( n + 1 ) * dtc ) );
VData.AddVertex( cD3DVertex( char_incomex + s * LettersWidth[ c ] ,
char_incomey + s , 0 , 0xffffff , m * dtc +
dtc * LettersWidth[ c ] , n * dtc ) );

VData.AddVertex( cD3DVertex( char_incomex , char>_incomey , 0 ,
0xffffff , m * dtc , ( n + 1 ) * dtc ) );
VData.AddVertex( cD3DVertex( char_incomex + s * LettersWidth[ c ] ,
char_incomey , 0 , 0xffffff ,
m * dtc + dtc * LettersWidth[ c ] , ( n + 1 ) * dtc ) );
VData.AddVertex( cD3DVertex( char_incomex + s * LettersWidth[ c ] ,
char_incomey + s , 0 , 0xffffff ,
m * dtc + dtc * LettersWidth[ c ] , n * dtc ) );

char_incomex += s * LettersWidth[ c ];

end:;
}

for( int i( VData_Cursor ) ; i < VData.Vertexes.Cursor ; i++ )
VData.Vertexes[ i ] += cVector3( x , y , z );
}
В этой функции мы сначала определяем char_incomex/char_incomey – положения левого нижнего угла буквы, потом сохраняем количество вершин в буфере (на случай если мы хотим добавить строку к тому, что уже есть в буфере). После чего в цикле пробегаем по символам строки и в соответствии с шириной символа, хранящейся в массиве LettersWidth заполняем текстурные координаты и координаты вершин. Если в строке встречаем признак перевода строки, то меняем char_incomex и char_incomey. После чего смещаем часть вершинного буфера (ту часть, которая была только что создана). Основная работа сделана, оставшиеся функции пробежимся “по диагонали”: Просто выводим строку, которая уже находится в буфере.
void	cD3DFont::PrintString( LPDIRECT3DDEVICE8 D3DDevice ){
D3DDevice->SetRenderState( D3DRS_ALPHABLENDENABLE , true );
D3DDevice->SetRenderState( D3DRS_SRCBLEND , D3DBLEND_SRCALPHA );
D3DDevice->SetRenderState( D3DRS_DESTBLEND , D3DBLEND_INVSRCALPHA );

if( FontTextureID != -1 )
TextureManager[ FontTextureID ].SetTexture( D3DDevice , 0 );
else
FontTexture.SetTexture( D3DDevice , 0 );

VData.Render( D3DDevice , FVF_CD3DVERTEX );

if( FontTextureID != -1 )
TextureManager[ FontTextureID ].ResetTexture( D3DDevice , 0 );
else
FontTexture.ResetTexture( D3DDevice , 0 );
D3DDevice->SetRenderState( D3DRS_ALPHABLENDENABLE , false );
}
Добавляем строку к тому, что уже хранится в буфере, при этом строка str дописывается в конец предыдущее строки.
void	cD3DFont::AppendString( char * str , float s ){

float x( VData[ VData.Vertexes.Cursor - 2 ].x );
float y( VData[ VData.Vertexes.Cursor - 2 ].y );
float z( VData[ VData.Vertexes.Cursor - 2 ].z );

FillVData( str , x , y , z , s );
}
Просто добавляем в буфер новую строку, которая может располагаться независимо от первой.
void	cD3DFont::AppendString( char *str , float x , 
float y , float z , float s ){
FillVData( str , x , y , z , s );
}
Затираем содержимое буфера и записываем туда новую строку.
void	cD3DFont::SetString( char *str , float x , 
float y , float z , float s ){
VData.Vertexes.Cursor = 0;
FillVData( str , x , y , z , s );
}
Конвертируем float в строку, а затем записываем её в буфер (опять-таки не зависимо от расположения предыдущей строки).
void	cD3DFont::AppendFloat( float f , float x , 
float y , float z , float s ){
char Number[10];
_gcvt( f , 9 , Number );
FillVData( Number , x , y , z , s );
}
Добавляем float к предыдущей строке.
void	cD3DFont::AppendFloat( float f , float s ){
char Number[10];
_gcvt( f , 9 , Number );
AppendString( Number , s );
}
Вот такой у нас получился набросок. Тут ещё есть что улучшить (подумайте об этом на досуге). Пока мне этого хватает, когда же мои потребности возрастут, я его доработаю и незамедлительно обновлю статью. А на сегодня всё!

к началу статьи

Вопросы, пожелания, замечания шлите на dodonov_a_a (___AT) inbox.ru.


Смежные вопросы:
Урок 1. Инициализация. Вершинные буфферы и камера.
Урок 2. Проигрывание медиа-файлов.
Урок 3. Автоматизация загрузки и управления аудио и видео ресурсами.
Урок 4. Текстурирование. Текстурный менеджер.
Исходные коды.
© 2004-2005 Savardge.ru
Hosted by uCoz