回顾HUD - NanoVG(HUD Revisited - NanoVG)

在此前的章节中,我们讲解了如何使用正交投影在场景顶部创建一个HUD以渲染图形和纹理。在本章中,我们将学习如何使用NanoVG库来渲染抗锯齿矢量图形,从而以简单的方式创建更复杂的HUD。

你可以使用许多其他库来完成此事,例如Nifty GUINuklear等。在本章是,我们将重点介绍NanoVG,因为它使用起来非常简单,但是如果你希望开发可与按钮、菜单和窗口交互的复杂GUI,那你可能需要的是Nifty GUI

使用NanoVG首先是要在pom.xml文件中添加依赖项(一个是用于编译时所需的依赖项,另一个是用于运行时所需的本地代码):

  1. ...
  2. <dependency>
  3. <groupId>org.lwjgl</groupId>
  4. <artifactId>lwjgl-nanovg</artifactId>
  5. <version>${lwjgl.version}</version>
  6. </dependency>
  7. ...
  8. <dependency>
  9. <groupId>org.lwjgl</groupId>
  10. <artifactId>lwjgl-nanovg</artifactId>
  11. <version>${lwjgl.version}</version>
  12. <classifier>${native.target}</classifier>
  13. <scope>runtime</scope>
  14. </dependency>

在开始使用NanoVG之前,我们必须在OpenGL设置一些东西,以便示例能够正常工作。我们需要启用对模板测试(Stencil Test)的支持。到目前为止,我们已经讲解了颜色和深度缓冲区,但我们没有提到模板缓冲区。该缓冲区为用于控制应绘制哪些像素的每个像素储存一个值(整数),用于根据储存的值以屏蔽或放弃绘图区域。例如,它可以用来以一种简单的方式切割场景的某些部分。我们通过将此行添加到Window类中来启用模板测试(在启用深度测试之后):

  1. glEnable(GL_STENCIL_TEST);

因为我们使用的是另一个缓冲区,所以在每次渲染调用之前,我们还必须注意删除它的值。因此,我们需要修改Renderer类的clear方法:

  1. public void clear() {
  2. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
  3. }

我们还将添加一个新的窗口选项来激活抗锯齿(Anti-aliasing)。因此,在Window类中,我们将通过如下方式启用它:

  1. if (opts.antialiasing) {
  2. glfwWindowHint(GLFW_SAMPLES, 4);
  3. }

现在我们可以使用NanoVG库了。我们要做的第一件事就是删掉我们创建的HUD代码,即着色器,IHud接口,Renderer类中的HUD渲染方法等。你可以在源代码中查看。

在此情况下,新的Hud类将负责其渲染,因此我们不需要将其委托给Renderer类。让我们先定义这个类,它将有一个init方法来设置库和构建HUD所需要的资源。方法定义如下:

  1. public void init(Window window) throws Exception {
  2. this.vg = window.getOptions().antialiasing ? nvgCreate(NVG_ANTIALIAS | NVG_STENCIL_STROKES) : nvgCreate(NVG_STENCIL_STROKES);
  3. if (this.vg == NULL) {
  4. throw new Exception("Could not init nanovg");
  5. }
  6. fontBuffer = Utils.ioResourceToByteBuffer("/fonts/OpenSans-Bold.ttf", 150 * 1024);
  7. int font = nvgCreateFontMem(vg, FONT_NAME, fontBuffer, 0);
  8. if (font == -1) {
  9. throw new Exception("Could not add font");
  10. }
  11. colour = NVGColor.create();
  12. posx = MemoryUtil.memAllocDouble(1);
  13. posy = MemoryUtil.memAllocDouble(1);
  14. counter = 0;
  15. }

我们首先要做的是创建一个NanoVG上下文。在本例中,我们使用的是OpenGL3.0后端,因此我们引用的是org.lwjgl.nanovg.NanoVGGL3命名空间。如果抗锯齿被启用,我们将设置NVG_ANTIALIAS标志。

接下来,我们使用此前加载到ByteBuffer中的TrueType字体来创建字体。我们为它指定一个名词,以便稍后在渲染文本时使用它。关于这点,一件很重要的事情是用于加载字体的ByteBuffer必须在使用字体时储存在内存中。也就是说,它不能被回收,否则你将得到一个不错的核心崩溃。这就是将它储存为类属性的原因。

然后,我们创建一个颜色实例和一些有用的变量,这些变量将在渲染时使用。在初始化渲染之前,在游戏初始化方法中调用该方法:

  1. @Override
  2. public void init(Window window) throws Exception {
  3. hud.init(window);
  4. renderer.init(window);
  5. ...

Hud类还定义了一个渲染方法,该方法应在渲染场景后调用,以便在其上绘制Hud。

  1. @Override
  2. public void render(Window window) {
  3. renderer.render(window, camera, scene);
  4. hud.render(window);
  5. }

Hud类的render方法的开头如下所示:

  1. public void render(Window window) {
  2. nvgBeginFrame(vg, window.getWidth(), window.getHeight(), 1);

首先必须要做的第一件事是调用nvgBeginFrame方法。所有NanoVG渲染操作都必须保护在nvgBeginFramenvgEndFrame调用之间。nvgBeginFrame接受以下参数:

  • NanoVG环境
  • 要渲染的窗口的大小(宽度和高度)。
  • 像素比。如果需要支持Hi-DPI,可以修改此值。对于本例,我们只将其设置为1。

然后我们创建了几个占据整个屏幕的色带。第一条是这样绘制的:

  1. // 上色带
  2. nvgBeginPath(vg);
  3. nvgRect(vg, 0, window.getHeight() - 100, window.getWidth(), 50);
  4. nvgFillColor(vg, rgba(0x23, 0xa1, 0xf1, 200, colour));
  5. nvgFill(vg);

渲染图形时,应调用的第一个方法是nvgBeginPath,它指示NanoVG开始绘制新图形。然后定义要绘制的内容,一个矩形,填充颜色并通过调用nvgFill绘制它。

你可以查看源代码的其他部分,以了解其余图形是如何绘制的。当渲染文本是,不需要在渲染前调用nvgBeginPath

完成所有图形的绘制后,我们只需要调用nvgEndFrame来结束渲染,但在离开方法之前还有一件重要的事情要做。我们必须恢复OpenGL状态,NanoVG修改OpenGL以执行其操作,如果状态未正确还原,你可能会看到场景没有正确渲染,甚至被擦除。因此,我们需要恢复渲染所需的相关OpenGL状态。这是委派到Window类中的:

  1. // 还原状态
  2. window.restoreState();

方法的定义如下:

  1. public void restoreState() {
  2. glEnable(GL_DEPTH_TEST);
  3. glEnable(GL_STENCIL_TEST);
  4. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  5. if (opts.cullFace) {
  6. glEnable(GL_CULL_FACE);
  7. glCullFace(GL_BACK);
  8. }
  9. }

这就完事了(除了一些其它的清理方法),代码完成了。当你运行示例时,你将得到如下结果:

Hud