原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Fonts
译注:并没有逐字逐句翻译一切,只翻译了自己觉得有用的部分。另外此翻译仅供参考,请一切以原文为准。代码例子文件链接什么的都请去原链接查找。括号里的内容一般也是译注,供理解参考用。总目录传送门:http://blog.csdn.net/zoharwolf/article/details/49472857
译注2:我自己亲自写代码测试时才发现这一篇教程多特么坑爹,仅支持ASCII字符啊亲,作为已经2015年的全新LWJGL3最高性能库是不是这教程的中心思想有点落后啊!!也就是不能搞出中文来。虽然LWJGL3的库里已经有了名为STBTrueType看起来非常高大上的字体类,但是目前官方关于它的教程几乎是0,具体要怎么用还不知。而我能找到的比较方便用的能和LWJGL相容的字体库比如Slick2D的,仅支持LWJGL2,在3里会报错= =不知道怎么办好了,有愿意研究的人去研究吧,或者再等官方更新教程了。
本教程教怎样在OpenGL里画文字,我们将用AWT的Font类读TrueType字体然后动态渲染之。读本教程前最好先看纹理教程。
你可能觉得渲染文字应该是某种很基本的功能,但是其实它是一种进阶功能,并非OpenGL自带基本功能。
有许多方法可以渲染文字,以下是两个常用的:
对初学者而言,第一个方法非常直接,但是用它只能搞一些很难变更的静态文本,所以一般只用在那些一次性的文本。
第二种方法更加有效率,所以还是看第二种方法吧。
创建每个字形的纹理图集需要几步,首先读取字体,然后决定纹理宽高,最后把字形画在图片上并用它生成纹理。
在Java里,读字体很直接,用java.awt.Font就可以用InputStream读到字体文件或者简直地用字体名来构造。
java.awt.Font font = new java.awt.Font(MONOSPACED, PLAIN, 16);
用以上代码我们以16号字读取一个默认的等宽字体。MONOSPACED和PLAIN是java.awt.Font里的静态常量。当然你也也可以读其他你想要的字体。
如果你想要读字体文件,那就用java.awt.Font.createFont(fontFormat, fontStream)。
如果用InputStream或java.awt.Font.createFont(fontFormat, fontFile)的话,并且想从File类读取,那fontFormat应该是TRUETYPE_FONT。读一个字体,字号是1,类型是PLAIN。想要更多的字号和类型,应该用deriveFont(style,size)。
字体读完,我们来决定字体的宽高,我们需要遍历在我们纹理中需要的ASCII字符。对于标准的字符,我们遍历ASCII字符#32到#256,我们可以忽略ASCII #0到#31,因为它们只是控制符,我们也可以跳过ASCII #127,因为它是删除控制符。最好把字体高度存下来,因为要反复用。
int imageWidth = 0;
int imageHeight = 0;
for (int i = 32; i < 256; i++) {
if (i == 127) {
continue;
}
char c = (char) i;
BufferedImage ch = createCharImage(font, c, antiAlias);
imageWidth += ch.getWidth();
imageHeight = Math.max(imageHeight, ch.getHeight());
}
fontHeight = imageHeight;
这循环代码很直观,其中重要的部分是写一个createCharImage(font, c antiAlias)方法。
首先想办法得到字体规格。创建一个临时图片并把要画在纹理上的字画在上面。之后只需根据图片创建一个Graphics2D对象,从此对象中可以获得字体规格。
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
if (antiAlias) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
FontMetrics metrics = g.getFontMetrics();
g.dispose();
如果我们想要反走样绘制,只需要设置渲染hint。但是其实这跟你选的字体有关,一些字体不用反走样已经很好了。
根据字体规格,我们就能提取出宽高了。
int charWidth = metrics.charWidth(c);
int charHeight = metrics.getHeight();
之后我们可以按此宽高创建一个新的图片并返回它,此图片上应画上字符。我们用白色来画,之后可以在渲染时再改变。
image = new BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB);
g = image.createGraphics();
if (antiAlias) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
g.setPaint(Color.WHITE);
g.drawString(String.valueOf(c), 0, metrics.getAscent());
g.dispose();
return image;
回到最开始我们创建纹理的方法,现在有了字体高度,还有宽度总和,我们可以用此宽高来创建图片了。
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
生成纹理之前,应该创建一个简单字形类,这样就可以保留每个字形的宽高并在纹理中找到它。
public class Glyph {
public final int width;
public final int height;
public final int x;
public final int y;
public Glyph(int width, int height, int x, int y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
}
}
在我们的Font类里,我们需要映射字符到对应的字形上,为此可以建一个Map,你也可以用简单的数组。
private Map<Character, Glyph> glyphs = new HashMap<>();
好极了,我们现在万事俱备,可以创建图集了,跟之前一样,我们遍历标准ASCII字符得到字符图片。还需要有一个创建字形类对象用的偏移值x
if (i == 127) {
continue;
}
char c = (char) i;
BufferedImage charImage = createCharImage(font, c, antiAlias);
int charWidth = charImage.getWidth();
int charHeight = charImage.getHeight();
用x和字符图片创建字形,要注意,y值取了图片高度减字符高度,这是因为最后我们要将图片垂直翻转以使其原点位于左下角。之后将字符图片画在纹理图片上,随着字符宽度增加偏移值x并将字形放在Map中。
Glyph ch = new Glyph(charWidth, charHeight, x, image.getHeight() - charHeight);
g.drawImage(charImage, x, 0, null);
x += ch.width;
glyphs.put(c, ch);
循环后,用纹理教程中的AffineTransform操作垂直翻转图片,把象素数据放到ByteBuffer里,之后再生成纹理句柄和上传象素数据到GPU。
为了描画文本,我们用已经填充好的Map取合适的纹理坐标,但是这之前,我们需要知道文本的高度,万一它有许多行呢。既然我们已经知道字体高度,通过遍历字符就很容易计算出来。
int lines = 1;
for(int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if(char == '\n') {
lines++;
}
}
int textHeight = lines * fontHeight;
文本高度很重要,因为习惯上OpenGL用窗口的左下角作为原点,所以为了能以正确的顺序描画每一行,我们必须从最高的位置开始。所以为了在(x,y)位置描画,得检查文本高度是否比字高度更大,那样的话,我们就知道文本不只有一行。
float drawX = x;
float drawY = y;
if(textHeight > fontHeight) {
drawY += textHeight - fontHeight;
}
现在到了需要注意的部分,决定了渲染过程的坐标之后,我们开始遍历每个字符,并且以储存的字形描画它们。
texture.bind();
renderer.begin();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '\n') {
/* Line feed, set x and y to draw at the next line */
drawY -= fontHeight;
drawX = x;
continue;
}
if (ch == '\r') {
/* Carriage return, just skip it */continue;
}
Glyph g = glyphs.get(ch);
renderer.drawTextureRegion(texture, drawX, drawY, g.x, g.y, g.width, g.height, c);
drawX += g.width;
}
renderer.end();
现在你应该明白,这一切实际上是用批量渲染完成的,我们将在下篇教程里介绍。