第四章 Text and Font Rendering (一) 【已完成】【附带源码】

Text 文本好像并不如我们在游戏开发中 想的那么重要, 就像一项不成熟的特性, 并不能该游戏开发带来什么影响。 但事实上果真如此吗?当你完成了一个游戏后, 就会发现文本并不仅仅用在屏幕上显示标题 和 滚动的字幕。 文本在整个游戏场景内是非常有用的: 给玩家提示, 指派游戏目标, 或者显示给玩家背包里里还有多少金币等等。 文本是很重要的UI交互工具, 标示弹药数量, 玩家生命值、防护盾抵抗值、能量剩余等等。

在本章我们将涉及如下几个话题:

  》为什么在 游戏开发中 文本占据着非常重要的地位

  》字体系统是如何工作的

  》如何自己亲手创建一个简单的字体系统

  》如何在屏幕上那个绘制文本

Text in Games

 站在程序员角度来看, 文本应该是最好的调试工具; 我们可以在运行时 添加 各种 real-time 实时的度量值,such as frame-rate count or text overlays for player names( 如帧频, 文本显示玩家名?)下图的商业游戏正是这样显示了 游戏帧频 和 玩家游戏角色名:

另外, 很多游戏都有实现一种叫 debug console(调试控制台)的文本工具, 通过调试控制台可以进行跟踪 和 修改游戏的不同区域信息。

调试控制台 是一种特殊的屏幕区域, 可以输入指令出发不同的各种动作。 试想一下吧, 通过调试控制台, 我们在游戏中通过输入一个指令可以进行 修改 数值、状态,在Unreal Tournament 中显示 FPS count, 输入作弊码, 开启调试信息统计, 召唤(产生)所需物品对象, 修改游戏属性等等, 那么我们的开发过程一定变得更加顺畅。 下图正是 调试控制台的一种实现:

Text 文本 还广泛运用与 pc 在线竞技游戏的 玩家交流中, 通常是通过先按下某个特殊的 键(例如 键盘上的  T 键), 就可以输入 文本进行队伍或者全服 聊天, 聊天文本信息还会显示在 玩家及其他玩家的屏幕上。现在的家用机如 Microsoft‘s Xbox 360 和 Sony's Playstation 3 上的游戏 使用 麦克风 语音输入折本进行 在线玩家简单额交流, 但是PC 游戏历来使用 文本交流作为主要的 玩家交互方式。

下面是 Unreal 引擎实例游戏 中使用 文本提示 的截图:

 

Adding Text

近年来, 游戏中的文本有很多种实现方式: 显示文本纹理的贴图几何体, 矢量绘图, 甚至是直接使用标准的 messagebox API 弹窗。现代游戏开发中, 有很多游戏开发库 都已实现 文本渲染的功能, 免去了我们需要自己实现的负担。(很不幸)Direct3D11 之前 DirectX 确实 支持一些 内置的 文本渲染函数, 但是 Direct3D 11 不再提供。 现在我们需要自己手动实现 渲染文本, 通过使用字体系统在 动态地绘图到精灵上; 这也就是我们这一章节的内容。

好吧, 下面让我们来看看 字体系统是如何组织在一块儿的。

Textured Fonts

字体是由一系列 字母  和符号组成的 图形样式; 例如 Microsoft Word 的 Times New Roman 字体 很 Verdana 和 Arial 字体是有明显不同之处 的。2D 和 3D 游戏中, 文本通常是 通过绘制 被贴图的几何体是实现的( 正如我们在 第3张中的精灵)。As we’ve described here, the term font is actually applied typography , since the letters and symbols that comprise the font are of a specific artistic style(font 是一种印刷技术, 因为 字母和 符号组成 的字体设计是一门艺术?) 

Typography is defined as the study, design, and arrangement of type in media. Graphic designers for the web, print, televison, etc. look at  tyography as a very important element in their work. Typography can make or break a design, and there are numerous books dedicated to the subject. Typography 在游戏中的作用是 不可磨灭的。 鉴于我们的主要焦点 是如何尽心编写游戏, 将不会追赶最新的 字体设计艺术的 最新趋势, 但应该命 字体设计 对于 设计者来说是很重要。

 贴图化(纹理化)字体, 也称 位图字体是 预先将字母 贴图到纹理图片上; 这些字母按照每个字母占占据在一个格子内, 进行产生单词时, 这些字母贴图将动态的组合在一起输出到 屏幕上; 采用这种一个列的字母组合 可以很容易产生句子和段落。 此外, 这种字体是基于预先绘制的贴图, 只需要修改些字母的贴图, 就能轻松改变 字体的外观和 感觉。

即便很拥有, 预贴图字体也有一些不足:

》预贴图字体放大效果较差。受渲染技术的 限制, 贴图化的字体在只能在 小尺寸范围进行 放大缩小, 否则将变得非常难看(狗牙效果严重?)

》为支持所有可能到的字母 和 符号, 贴图化字体文件将会变得很大。

》本地化问题。 如果游戏需要支持多语言, 需要贴图能够正确显示 各种字符集(英文, 日文, 中文的), 还要进行不同语言间进行转换。

》不同字母的前导、后接空间是不同的, 这将导致 贴图化字体在 游戏显示特定单词时将会变得很怪异,因为 每个字母是限制在大小相同的格子内, 而非基于每个字母实际的大小, 例如 字母“P” 回避 字母“l” 宽得多; 致使有些单词虽然长度一样, 都会因为 字母前后空间的不一致而有些单词的空间要比其他松散。

》第一次创建 一种健壮的字体系统很据挑战。

Textured fonts allow for the buildup of words as ther're needed and are managed through some type of font system. Also, textured fonts can be created to match the look and feel of the remainder of the user interface.

 

A Font System Explained

有些情况下, 在游戏中使用 硬编码的文本 会比 预先生成的 图片要好得多, 这时只需要使用 字体系统 动态绘制所需的文字.例如告知玩家进行输入(这时需要回显), 显示玩家的聊天信息等.如果文本不是动态的, 就没有一点必要使用 字体系统了, 文本贴图已足以胜任当前任务; 当然, 万事也没有绝对.

想象以下, 在RPG游戏中, 当你想要和村里的一个居民谈话时, 若果采用硬编码法方式显示角色之间的对话, 那么每个文本都必须是 预先生成好的 文本贴图;也就是说, 文本贴图必须数量足够多以包含所有的对话内容.

动态文本系统允许在加载时, 调节各个 字幕贴图的组合生成出我们需要的单词/字句; 这样能够很好的节省时间,空间, 显示时只需要使用纹理左边进行映射即可.

 

Creating a Font System using Sprites

到了 Direct3D 11 , Direct3D 已提供字体文本渲染支持了, 为此我们必须手动编写已实现 文本贴图功能, 使用 动态 vertex buffers 和 texture mapping 足以实现我们的目的.

Direct3D Text demo 修改自 Texture mapping demo: 添加代码将上个demo中的 vertex buffer 改为 动态buffer, 一个函数 对 该动态buffer 填充 贴图精灵. 动态buffer 适用于  内容需要经常修改的 buffer; 不需要在 每个帧frame 中进行创建和 销毁 buffer.

如下 D3DTextDemo 类 可知, 该类添加了 DrawString() 函数, 用于绘制字符串到 屏幕上, 输入参数为要显示的文本, 绘制的坐标X 和 Y:

#ifndef _D3D_TEXT_DEMO_
#define _D3D_TEXT_DEMO_

#include "Dx11DemoBase.h"

class D3DTextDemo : public Dx11DemoBase
{
    public:
        D3DTextDemo();
        virtual ~D3DTextDemo();

        bool LoadContent();
        void UnloadContent();

        void Update( float dt);
        void Render();

    private:
        bool DrawString( char* message, float startX, float startY);        

    private:
        ID3D11VertexShader *solidColorVS_;
        ID3D11PixelShader  *solidColorPS_;

        ID3D11InputLayout  *inputLayout_;
        ID3D11Buffer        *vertexBuffer_;

        ID3D11ShaderResourceView    *colorMap_;
        ID3D11SamplerState            *colorMapSampler_;

};

#endif

这个 demo 以正常的方式工作. 首先, 记载一张包含 A Z 的纹理图片, A在最左边 Z在最右边:

是 demo简单, 这里只使用 最大写字母 且不包含各种符号, 待掌握了基本工作原理后, 在进行扩展. demo的 纹理渲染算法很简单, 适用上图, 为每个字母创建一个精灵; 第一个精灵位于 "传入到 DrawString() 函数的两个参数" X , Y.

后续的精灵(字母)紧接上一个精灵(字母).

纹理图片 中的每个字母大小为 32 * 32像素; 共有26个字母 和一个空格(空字母), 这张纹理图片分辨率为 32 * ( 26 + 1) = 32 * 27 = 864 ; 为了示例简单, 这里的字符串水平方向显示, 并且彼此相邻, 因为没有考虑多行文本和各种符号 将会使 纹理坐标的映射变得简单.

 

 回顾一下, 我们遍历字符串, 为每个单词创建几何体, 精灵(精灵之间相互紧挨着下一个), 接着创建文本贴图的 纹理坐标, 为每个字母按照正确的坐标进行映射; 适用empty space 在纹理的结尾作为 不合法字母标记.

在开始深入 生成文本几何体前, 让我们先来看看修改后的 Render() 函数, Texture mapping demo的 d3dContext_->Draw() 替换成 DrawString() 函数. DrawString() 函数负责 创建文本几何体 和 渲染工作; 函数的剩下部分保持原样, 处理添加了亮蓝色的颜色作为背景清除颜色.

void D3DTextDemo::Render()
{
    if( d3dContext_ == 0 )
        return;

    float clearColor[ 4] = { 0.2f, 0.22f, 0.24f, 1.0f};
    d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor);

    unsigned int stride = sizeof( VertexPos);
    unsigned int offset = 0;

    d3dContext_->IASetInputLayout( inputLayout_);
    d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset);
    d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    d3dContext_->VSSetShader( solidColorVS_, 0, 0);
    d3dContext_->PSSetShader( solidColorPS_, 0, 0);
    d3dContext_->PSSetShaderResources( 0, 1, &colorMap_);
    d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_);

    DrawString( "HELLO WORLD", 0.0f, 0.0f);

    swapChain_->Present( 0, 0);
}

LoadContent()函数也必须进行更新, 唯一的改变是适用 font.dds 替换 decal.dds, 创建动态vertex buffer 分为以下几个步骤:

 

1.buffer 描述符 usage 标记为 D3D11_USAGE_DYNAMIC , 允许buffer CPU动态改变( 以前的demo 都是 D3D11_USAGE_DEFAULT 静态buffer 标记);

2.CPU 存取权限标记为 D3D11_CPU_ACCESS_WRITE , 这个必须被设置, 才能让 CPU 有权限 GPU适用适当的资源; 当然寸曲线还有 read read/write , 但是本例中CPU 不需要从其他地方进行 read.

3.创建 vertex buffer, 设置 sub-resource 参数 null, 因为 vertex buffer的内容数据会被对哦功能太修改, 现在不需要设置任何数据.

 

动态buffer 创建完成后, 我们就可以随时更改他们的内容. 既然 DrawString() 函数创建几何体, LoadContent() 函数中也不需要进行定义两个三角形级拷贝 sub-resource Direct3D 11 对象了; 此外, vertex buffer 被限制为最多存储 24个精灵.

bool D3DTextDemo::LoadContent()
{
    ID3DBlob    *vsBuffer = 0;
    
    // 第一步 加载并创建 shader
    bool compileResult = CompileD3DShader( "TextureMap.fx", "VS_Main", "vs_4_0", &vsBuffer);
    if( compileResult == false)
    {
        MessageBox( 0, "Error loading vertex shader !", "Compile Error", MB_OK);
        return false;
    }


    HRESULT d3dResult;
    d3dResult = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer(), vsBuffer->GetBufferSize(), 0, &solidColorVS_);
    if( FAILED( d3dResult))
    {
        if( vsBuffer)
            vsBuffer->Release();

        return false;
    }

    // 第二步, 创建 inputlayout等信息
    D3D11_INPUT_ELEMENT_DESC solidColorLayout[] = {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, 
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,    0,12, D3D11_INPUT_PER_VERTEX_DATA, 0}
    };

    unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout);
    d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout, totalLayoutElements, vsBuffer->GetBufferPointer(), vsBuffer->GetBufferSize(), &inputLayout_);
    vsBuffer->Release();

    if( FAILED( d3dResult))
    {
        return false;
    }


    // pixel shader 部分
    ID3DBlob *psBuffer = 0;
    compileResult = CompileD3DShader( "TextureMap.fx", "PS_Main", "ps_4_0", &psBuffer);
    if( compileResult == false)
    {
        MessageBox( 0, "Error loading pixel shader !", "Compile Error", MB_OK);
        return false;
    }

    d3dResult = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer(), psBuffer->GetBufferSize(), 0, &solidColorPS_);
    psBuffer->Release();
    if( FAILED( d3dResult))
    {
        return false;
    }


    d3dResult = D3DX11CreateShaderResourceViewFromFile( d3dDevice_, "font.dds", 0, 0, &colorMap_, 0);
    if( FAILED( d3dResult))
    {
        DXTRACE_MSG( "Failed to load the texture image!");
        return false;
    } 

    D3D11_SAMPLER_DESC colorMapDesc;
    ZeroMemory( &colorMapDesc, sizeof( colorMapDesc));
    colorMapDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    colorMapDesc.AddressV = D3D11_TEXTURE_ADDRESS_MIRROR;
    colorMapDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    colorMapDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
    colorMapDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    colorMapDesc.MaxLOD = D3D11_FLOAT32_MAX;
    colorMapDesc.BorderColor[0] = 0.25;

    d3dResult = d3dDevice_->CreateSamplerState( &colorMapDesc, &colorMapSampler_);
    if( FAILED( d3dResult))
    {
        DXTRACE_MSG( "Failed to create color map sampler state!");
        return false;
    }


    ID3D11Resource *colorTex;
    colorMap_->GetResource( &colorTex);

    D3D11_TEXTURE2D_DESC colorTexDesc;
    ( ( ID3D11Texture2D*) colorTex)->GetDesc( &colorTexDesc);
    colorTex->Release();

    float halfWidth = ( float)colorTexDesc.Width / 2.0f;
    float halfHeight = ( float )colorTexDesc.Height / 2.0f;

    

    D3D11_BUFFER_DESC vertexDesc;
    ZeroMemory( &vertexDesc, sizeof( vertexDesc) );
    vertexDesc.Usage = D3D11_USAGE_DYNAMIC;
    vertexDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

    const int sizeOfSprite = sizeof( VertexPos) * 6;
    const int maxLetters = 24 ;
    vertexDesc.ByteWidth = sizeOfSprite * maxLetters;

    d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, 0, &vertexBuffer_);
    if( FAILED( d3dResult))
    {
        return false;
    }

    return true;
}

本demo的最后一个函数是 DrawString(), 也是实现主要功能的函数之一; 这个函数有些长, 我们将分小节进行讨论, 第一小节设置我们 算法所需的 各种数值;

sizeOfSprite : 单个精灵的字节单位大小; 一个精灵 由两个三角形构成, 有6个顶点. This constant is here to aid in readability.

maxLetters : 限制本demo 至多只能渲染 24 个字母, 当掌握了本demo后, 可以自由进行扩宽;

 

length : 要进行渲染的字符串长度;

charWidth :  屏幕坐标系中 每个字母的 宽度;

charHeight :  屏幕坐标系中 每个字母的 高度;

texelWidth :  纹理图片中 每个字母的宽度;

verticesPerletter :  每个精灵的所需顶点数( 常量: 两个三角形共 6个顶点).

 

多于24 个字母的字符串, 将会被剪切至 24 个单词.

第一小节 用于设置 DrawString() 函数所需要的各种 数值代码如下:

bool D3DTextDemo::DrawString( char* message, float startX, float startY)
{
    // Size in bytes for a single sprite.
    const int sizeOfSprite = sizeof( VertexPos) * 6;

    // Demo's dynamic buffer set up for max of 24 letters.
    const int maxLetters = 24;

    int length = strlen( message);

    // clamp for strings too long.
    if( length > maxLetters )
    {
        length = maxLetters;
    }

    // char's width on screen.
    float charWidth = 32.0f / 800.0f;

    // char's height on screen.
    float charHeight = 32.0f / 600.0f;

    // char's texel width.
    float texelWidth = 32.0f / 864.0f;

    // verts per-triangle (3) * total triangles(2) = 6
    const int verticesPerLetter = 6;

    ................
}

通过调用 Direct3D context 的 Map() 函数进行更新 动态buffer 的内容数据, Map() 函数需要适用如下参数: 进行mapping映射的buffer ,sub-resource 的所应值( 本处没有 多个sub-resource ,设为 0), 映射类型, 映射标记  和 最终存储映射数据的 sub-resource.

mapping type 映射类型, 本例是 D3D11_MAP_FLAG_DISCARD, 告知Direct3D buffer 理的就数据将被考虑为 undefined未定义. 其他的映射类型 如 D3D11_MAP_FLAG_DO_NOT_WAIT 此项和 D3D11_MAP_FLAG_WRITE_DISCARD 冲突, 将不被考虑.一旦Map() 函数调用成功后, 我们就拥有存取 buffer的内容了( 即 传入Map()函数最后一参数的 SUBRESOURCE 的对应目标), 只需拷贝我们需要更新的数据到 D3D11_MAPPED_SUBRESOURCE 的 pData. 映射的第二小节代码如下( 呃. 这里还保存了 字母 A 和 Z 的ASCII 码值, 用于遍历 所有的字母 和生成字符串几何体):

bool D3DTextDemo::DrawString( char* message, float startX, float startY)
{
        ................
    D3D11_MAPPED_SUBRESOURCE mapResource;
    HRESULT d3dResult = d3dContext_->Map( vertexBuffer_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapResource);
    if( FAILED( d3dResult))
    {
        DXTRACE_MSG( "Failed to map resource.");
        return false;
    }

    // point to our vertex buffer's internal data.
    VertexPos *spritePtr = ( VertexPos*)mapResource.pData;

    const int indexA = static_cast<char>( 'A');
    const int indexZ = static_cast<char>( 'Z');

    .......................
}

上述代码中, 我们维护了一个 指向 sub-resource结构体成员 pData 的指针, 通过循环遍历 字符串后, 我们将通过此 指针创建 所需的几何体. 未达到这目的, for循环遍历整个 字符串的每个单词,  并为之设置 精灵(几何体)的开始坐标 X 和 Y; 而字符串的第一个单词, X值 刚好是传入 DrawString() 函数的第一个参数 startX; 当在遍历每个单词的时候, 我们实际上是通过但前单词的在字符串中的位置, 每个字母的宽度,  再加上 第一个字母的X值 ,就可以产生各自的 X值, 而不需要计算并存储每个字母的X值.

设置好 vertex position 后, 哦我们需要设置 顶点的纹理坐标, 纹理坐标的设置采用相同的方式, 唯一的区别是, 无需适用 循环遍历每个 单词, 应为纹理坐标可以通过  纹理文件值对应字母的 ASCII值对应产生; 在 font.dds 文件中, 字母A 处于第一个, Z 处在最后一个; 如果将 每个字母的ACSII 码值 减去 字母 A的ASCII 码值, 就可以得到对应的所应值 : 0表示 字母A, 1表示 字母B, 等等. 通过调用 texLoopup 变量 表示 当前字母的 ASCII码值, 进行这项纹理坐标的遍历产生, 只不过此处的开始值 是0( 替换 startX 哟), 亦即 大写字母的A.

 

字母表的所有字母纹理坐标生成方式 和 几何体坐标的产生方式一样; 但对于 不合法字母或符号, 其纹理坐标将 取自 纹理文件中 Z字母坐标的后面, 只是一个 内容为空大小也为 32 的空间( 正如前面说说的, 不合法字母和符号将用 空字符表示). 第三小节代码如下:

bool D3DTextDemo::DrawString( char* message, float startX, float startY)
{
        ......................
    for( int i = 0; i< length ; ++i)
    {
        float thisStartX = startX + ( charWidth * static_cast<float>( i));
        float thisEndX = thisStartX + charWidth;
        float thisEndY = startY + charHeight;

        spritePtr[ 0].pos = XMFLOAT3( thisEndX,        thisEndY,        1.0f);
        spritePtr[ 1].pos = XMFLOAT3( thisEndX,        startY,            1.0f);
        spritePtr[ 2].pos = XMFLOAT3( thisStartX,    startY,            1.0f);
        
        spritePtr[ 3].pos = XMFLOAT3( thisStartX,    startY,            1.0f);
        spritePtr[ 4].pos = XMFLOAT3( thisStartX,    thisEndY,        1.0f);
        spritePtr[ 5].pos = XMFLOAT3( thisEndX,        thisEndY,        1.0f);

        int texLookup = 0; // offset from chacter'A' in the texture image
        int letter = static_cast< char>( message[i]);
        if( letter < indexA || letter > indexZ)
        {
            // Grab one index past Z, which is a blank space in the texture.
            texLookup = ( indexZ - indexA) + 1 ;
        }else{
            texLookup = ( letter - indexA);
        }


        float tuStart = 0.0f + ( texelWidth * static_cast< float>( texLookup));
        float tuEnd = tuStart + texelWidth;

        spritePtr[ 0].tex0 = XMFLOAT2( tuEnd, 0.0f);
        spritePtr[ 1].tex0 = XMFLOAT2( tuEnd, 1.0f);
        spritePtr[ 2].tex0 = XMFLOAT2( tuStart, 1.0f);

        spritePtr[ 3].tex0 = XMFLOAT2( tuStart, 1.0f);
        spritePtr[ 4].tex0 = XMFLOAT2( tuStart, 0.0f);
        spritePtr[ 5].tex0 = XMFLOAT2( tuEnd, 0.0f);

        spritePtr += 6;
      }


    ............................
}

DrawString() 函数的最后一步是 对 vertex buffer进行unmap操作(解除映射); 每个曾经进行过映射的buffer 都必须机型unmapp操作, 者有 设备上下文的 Unmap() 函数完成,有两个深入参数: 要进行unmap的buffer  和 sub-resourct 索引.vertex buffer 的数据 内容更新对应的精灵成功后,调用 Draw() 函数即可绘制显示我们想要的结果. Draw() 函数采用6倍于字符串长度的大小, 因为每个精灵采用6个 顶点表示, 字符串的长度决定了所需精灵的个数.最后一步的代码如下:

bool D3DTextDemo::DrawString( char* message, float startX, float startY)
{
        ............................
    d3dContext_->Unmap( vertexBuffer_, 0);
    d3dContext_->Draw( 6 * length , 0);

    return true;
}

最终效果:

 

最后是 源码

posted @ 2012-12-10 23:10  Wilson-Loo  阅读(1309)  评论(0编辑  收藏  举报