diff --git a/source/12-game-hud.md b/source/12-game-hud.md new file mode 100644 index 0000000..57adf3b --- /dev/null +++ b/source/12-game-hud.md @@ -0,0 +1,618 @@ +# 游戏HUD(Game HUD) + +本章中我们将为游戏实现一个HUD(平视显示器)。换句话说,就是在三维场景上用一组二维图形和文本显示相关信息。我们将创建一个简单的HUD,接下来将说明一些如何显示这些信息的基本方法。 + +当你查看本章的源代码时,还将看到我们重构了一些代码,特别是`Renderer`类,以便为HUD渲染做好准备。 + +## 文本渲染 + +创建HUD所要做的第一件事是渲染文本。为了实现它,我们将把包含字母字符的纹理的纹理映射到一个矩形中,该矩形将被分割为一组表示各个字符的片段。之后,我们将使用该纹理绘制文本。所以第一步是创建含有所有字符的纹理,你可以使用很多程序来做,例如[CBFG](http://www.codehead.co.uk/cbfg/)、[F2IBuilder](http://sourceforge.net/projects/f2ibuilder/)等等。现在我们使用Codehead’s Bitmap Font Generator(CBFG)。 + +CBFG有很多设置,例如纹理大小、字体类型、要使用的反走样等等。下图是我们将用来生成纹理文件的配置。在本章中,我们将假设文本编码为ISO-8859-1格式,如果需要处理不同的编码格式,则需要稍微修改代码。 + +![CBFG配置](_static/12/CBG.png) + +当你设置CBFG的所有配置后,可以导出为多种图片格式。现在我们将它导出为BMP文件,然后再转换为PNG文件,以便将它作为纹理加载。当转换为PNG格式时,我们也可以将黑色背景设置为透明,也就是说,我们将黑色的Alpha值设置为0(可以使用GIMP这样的工具)。最后,你会得到与下图类似的东西。 + +![字体纹理](_static/12/font_texture.png) + +如上所试,所有的字符都排列在图像中。现在图像有15列和17行。通过 +As you can see, the image has all the characters displayed in rows and columns. In this case the image is composed by 15 columns and 17 rows. By using the character code of a specific letter we can calculate the row and the column that is enclosed in the image. The column can be calculated as follows: $$column = code \space mod \space numberOfColumns$$. Where $$mod$$ is the module operator. The row can be calculated as follows: $$row = code / numberOfCols$$, in this case we will do a integer by integer operation so we can ignore the decimal part. + +我们将创建一个名为`TextItem`的类,它将储存渲染文本所需的内容。这是一个简化的实现,不考虑多行文本等特性,但它能在HUD中显示文本信息。下面是这个类的实现。 + +```java +package org.lwjglb.engine; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import org.lwjglb.engine.graph.Material; +import org.lwjglb.engine.graph.Mesh; +import org.lwjglb.engine.graph.Texture; + +public class TextItem extends GameItem { + + private static final float ZPOS = 0.0f; + + private static final int VERTICES_PER_QUAD = 4; + + private String text; + + private final int numCols; + + private final int numRows; + + public TextItem(String text, String fontFileName, int numCols, int numRows) throws Exception { + super(); + this.text = text; + this.numCols = numCols; + this.numRows = numRows; + Texture texture = new Texture(fontFileName); + this.setMesh(buildMesh(texture, numCols, numRows)); + } +``` + +这个类继承了`GameItem`,这是因为我们希望改变文本在屏幕上的位置,也可能需要缩放和旋转它。构造函数接收腰显示的文本和用于渲染的纹理数据(包括图像数据和行列数目)。 + +在构造函数中,我们加载纹理图像文件,并调用一个方法来创建一个`Mesh`实例用于模拟文本。让我们看看`buildMesh`方法。 + +```java +private Mesh buildMesh(Texture texture, int numCols, int numRows) { + byte[] chars = text.getBytes(Charset.forName("ISO-8859-1")); + int numChars = chars.length; + + List positions = new ArrayList(); + List textCoords = new ArrayList(); + float[] normals = new float[0]; + List indices = new ArrayList(); + + float tileWidth = (float)texture.getWidth() / (float)numCols; + float tileHeight = (float)texture.getHeight() / (float)numRows; +``` + +代码创建了用于储存Mesh的位置、纹理坐标、法线和索引的数据结构。现在我们不使用照明,因此法线数列是空的。我们要做的是构造一组字符片段,每个字符片段代表一个字符。我们还需要根据每个字符片段对应的字符来分配适当的纹理坐标。下图表示了文本矩形和字符片段的关系。 + +![文本矩形](_static/12/text_quad.png) + +因此,对于每个字符,我们需要创建由两个三角形构成的字符片段,这两个三角形可以用四个顶点(V1、V2、V3和V4)定义。第一个三角形(左下角的那个)的索引为(0, 1, 2),而第二个三角形(右上角的那个)的索引为(3, 0, 2)。纹理坐标是基于与纹理图像中每个字符相关连的行和列计算的,纹理坐标的范围为[0,1],所以我们只需要将当前行或当前列除以总行数和总列数就可以获得V1的坐标。对于其他顶点,我们只需要适当加上行宽或列宽就可以。 + +下面的循环语句块就创建了与显示文本的矩形相关的所有顶点、纹理坐标和索引。 + +```java +for(int i=0; i(); + + buildTexture(); +} +``` + +The first step is to handle the non latin issue, given a char set and a font we will build a `String` that contains all the characters that can be rendered. + +```java +private String getAllAvailableChars(String charsetName) { + CharsetEncoder ce = Charset.forName(charsetName).newEncoder(); + StringBuilder result = new StringBuilder(); + for (char c = 0; c < Character.MAX_VALUE; c++) { + if (ce.canEncode(c)) { + result.append(c); + } + } + return result.toString(); +} +``` + +Let’s now review the method that actually creates the texture, named `buildTexture`. + +```java +private void buildTexture() throws Exception { + // Get the font metrics for each character for the selected font by using image + BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2D = img.createGraphics(); + g2D.setFont(font); + FontMetrics fontMetrics = g2D.getFontMetrics(); + + String allChars = getAllAvailableChars(charSetName); + this.width = 0; + this.height = 0; + for (char c : allChars.toCharArray()) { + // Get the size for each character and update global image size + CharInfo charInfo = new CharInfo(width, fontMetrics.charWidth(c)); + charMap.put(c, charInfo); + width += charInfo.getWidth(); + height = Math.max(height, fontMetrics.getHeight()); + } + g2D.dispose(); +``` + +We first obtain the font metrics by creating a temporary image. Then we iterate over the `String` that contains all the available characters and get the width, with the help of the font metrics, of each of them. We store that information on a map, `charMap`, which will use as a key the character. With that process we determine the size of the image that will have the texture \(with a height equal to the maximum size of all the characters and its with equal to the sum of each character width\). `CharSet` is an inner class that holds the information about a character \(its width and where it starts, in the x coordinate, in the texture image\). + +```java + public static class CharInfo { + + private final int startX; + + private final int width; + + public CharInfo(int startX, int width) { + this.startX = startX; + this.width = width; + } + + public int getStartX() { + return startX; + } + + public int getWidth() { + return width; + } + } +``` + +Then we will create an image that will contain all the available characters. In order to do this, we just draw the string over a `BufferedImage`. + +```java + // Create the image associated to the charset + img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + g2D = img.createGraphics(); + g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2D.setFont(font); + fontMetrics = g2D.getFontMetrics(); + g2D.setColor(Color.WHITE); + g2D.drawString(allChars, 0, fontMetrics.getAscent()); + g2D.dispose(); +``` + +We are generating an image which contains all the characters in a single row \(we maybe are not fulfilling the premise that the texture should have a size of a power of two, but it should work on most modern cards. In any caseyou could always achieve that by adding some extra empty space\). You can even see the image that we are generating, if after that block of code, you put a line like this: + +```java +ImageIO.write(img, IMAGE_FORMAT, new java.io.File("Temp.png")); +``` + +The image will be written to a temporary file. That file will contain a long strip with all the available characters, drawn in white over transparent background using anti aliasing. + +![Font texture](_static/12/texture_font.png) + +Finally, we just need to create a `Texture` instance from that image, we just dump the image bytes using a PNG format \(which is what the `Texture` class expects\). + +```java + // Dump image to a byte buffer + InputStream is; + try ( + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ImageIO.write(img, IMAGE_FORMAT, out); + out.flush(); + is = new ByteArrayInputStream(out.toByteArray()); + } + + texture = new Texture(is); +} +``` + +You may notice that we have modified a little bit the `Texture` class to have another constructor that receives an `InputStream`. Now we just need to change the `TextItem` class to receive a `FontTexture` instance in its constructor. + +```java +public TextItem(String text, FontTexture fontTexture) throws Exception { + super(); + this.text = text; + this.fontTexture = fontTexture; + setMesh(buildMesh()); +} +``` + +The `buildMesh` method only needs to be changed a little bit when setting quad and texture coordinates, this is a sample for one of the vertices. + +```java + float startx = 0; + for(int i=0; i