# 第十一課:2D文本
本課將學習如何在三維場景之上繪制二維文本。本例是一個簡單的計時器:

## API
我們將實現這些簡單的接口(位于`common/text2D.h`):
~~~
void initText2D(const char * texturePath);
void printText2D(const char * text, int x, int y, int size);
void cleanupText2D();
~~~
為了讓代碼在640*480和1080p分辨率下都能正常工作,x和y的范圍分別設為[0-800]和[0-600]。頂點著色器將根據實際屏幕大小做對它做調整。
完整的實現代碼請參閱`common/text2D.cpp`。
## 紋理
`initText2D`簡單地讀取一個紋理和一些著色器,很好理解。來看看紋理:

該紋理由[CBFG](http://www.codehead.co.uk/cbfg/)生成。CBFG是諸多從字體生成紋理的工具之一。把紋理加載到Paint.NET,加上紅色背景(僅為了觀察方便;本教程中的紅色背景,都代表透明)。
`printText2D()`在屏幕的適當位置,生成一個紋理坐標正確的四邊形。
## 繪制
首先,填充這些緩沖區:
~~~
std::vector<glm::vec2> vertices;
std::vector<glm::vec2> UVs;
~~~
文本中的每個字母,都要計算其四邊形包圍盒的頂點坐標,然后添加兩個三角形(組成一個四邊形):
~~~
for ( unsigned int i=0 ; i<length ; i++ ){
glm::vec2 vertex_up_left??? = glm::vec2( x+i*size???? , y+size );
glm::vec2 vertex_up_right?? = glm::vec2( x+i*size+size, y+size );
glm::vec2 vertex_down_right = glm::vec2( x+i*size+size, y????? );
glm::vec2 vertex_down_left? = glm::vec2( x+i*size???? , y????? );
vertices.push_back(vertex_up_left?? );
vertices.push_back(vertex_down_left );
vertices.push_back(vertex_up_right? );
vertices.push_back(vertex_down_right);
vertices.push_back(vertex_up_right);
vertices.push_back(vertex_down_left);
~~~
輪到UV坐標了。計算左上角的坐標:
~~~
char character = text[i];
float uv_x = (character%16)/16.0f;
float uv_y = (character/16)/16.0f;
~~~
這樣做是可行的(基本可行,詳見下文),因為[A的ASCII值](http://www.asciitable.com/)為65。65%16 = 1,因此A位于第1列(列號從0開始)。
65/16 = 4,因此A位于第4行(這是整數除法,所以結果不是想象中的4.0625)
兩者都除以16.0以使之落于[0.0 - 1.0]區間內,這正是OpenGL紋理所需的。
現在只需對頂點重復相同的操作:
~~~
glm::vec2 uv_up_left = glm::vec2( uv_x , 1.0f - uv_y );
glm::vec2 uv_up_right = glm::vec2( uv_x+1.0f/16.0f, 1.0f - uv_y );
glm::vec2 uv_down_right = glm::vec2( uv_x+1.0f/16.0f, 1.0f - (uv_y + 1.0f/16.0f) );
glm::vec2 uv_down_left = glm::vec2( uv_x , 1.0f - (uv_y + 1.0f/16.0f) );
UVs.push_back(uv_up_left );
UVs.push_back(uv_down_left );
UVs.push_back(uv_up_right );
UVs.push_back(uv_down_right);
UVs.push_back(uv_up_right);
UVs.push_back(uv_down_left);
}
~~~
其余的操作和往常一樣:綁定緩沖區,填充,選擇著色器程序,綁定紋理,開啟、綁定、配置頂點屬性,開啟混合,調用glDrawArrays。歐也,搞定了。
注意非常重要的一點:這些坐標位于[0,800][0,600]范圍內。也就是說,這里**不需要**矩陣。vertex shader只需簡單換算就可以把這些坐標轉換到[-1,1][-1,1]范圍內(也可以在C++代碼中完成這一步)。
~~~
void main(){
// Output position of the vertex, in clip space
// map [0..800][0..600] to [-1..1][-1..1]
vec2 vertexPosition_homoneneousspace = vertexPosition_screenspace - vec2(400,300); // [0..800][0..600] -> [-400..400][-300..300]
vertexPosition_homoneneousspace /= vec2(400,300);
gl_Position = vec4(vertexPosition_homoneneousspace,0,1);
// UV of the vertex. No special space for this one.
UV = vertexUV;
}
~~~
fragment shader的工作也很少:
~~~
void main(){
color = texture( myTextureSampler, UV );
}
~~~
順便說一下,別在工程中使用這些代碼,因為它只能處理拉丁字符。否則你的產品在印度、中國、日本(甚至德國,因為紋理上沒有?這個字母)就別想賣了。這幅紋理是我用法語字符集生成的,在法國用用還可以(注意 é, à, ?等字母)。修改其他教程的代碼時注意庫的使用。其他教程大多使用OpenGL 2,和本教程不兼容。很可惜,我還沒找到一個足夠好的、能處理UTF-8字符集的庫。
順帶提一下,您最好看看Joel Spolsky寫的[The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](http://www.joelonsoftware.com/articles/Unicode.html)。
如果您需要處理大量的文本,可以參考這篇[Valve的文章](http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf)。