commit 306a444cc679f46e66da90c1dea47d12cac4035b Author: Mouse Date: Mon Sep 27 13:57:47 2021 +0800 Deployed 7ff35d0 with MkDocs version: 1.2.2 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/01-first-steps/index.html b/01-first-steps/index.html new file mode 100755 index 0000000..ef7bd53 --- /dev/null +++ b/01-first-steps/index.html @@ -0,0 +1,327 @@ + + + + + + + + + + + + 事前准备 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

事前准备(First Steps)

+

在本书中,我们将学习开发3D游戏所涉及的主要技术。本书将使用Java语言和Java轻量级游戏库(LWJGL)来编写示例代码。LWJGL库允许我们访问底层的API(应用程序接口),例如OpenGL。

+

LWJGL是一个底层的API,它像一个OpenGL包装库。如果你是想在短时间内开始制作一个3D游戏,那么你也许该考虑其它选择,比如使用引擎[JmonkeyEngine]。使用LWJGL这个底层API,在你收到成效之前,需要了解许多概念并且编写大量的代码。这样做的好处是你可以更好的理解3D图形渲染,并且可以更好的控制它。

+

在上文说到,在本书中我们将使用Java,更确切来说我们将使用Java 10。所以你需要从Oracle的页面下载对应版本的JDK。请选择适合你的操作系统的安装程序。本书假定你对Java语言有一定的了解。

+

如果你需要一个可以运行示例代码的Java IDE(集成开发环境),你可以下载为Java 10提供良好支持的IntelliJ IDEA。由于Java 10仅支持64位的平台,记得下载64位版本的IntelliJ。IntelliJ提供有一个免费且开源的社区版,你可以在这里下载它: https://www.jetbrains.com/idea/download/

+

IntelliJ

+

为了构建示例代码,我们将使用Maven。Maven已经集成在大多数IDE中,你可以在IDE中直接打开不同章节的示例代码。只要打开了示例代码所在的文件夹,IntelliJ就会检测到它是一个Maven项目。

+

Maven项目

+

Maven基于一个名为pom.xml(Project Object Model,项目对象模型)的XML文件来构建项目,它管理了项目的依赖(需要使用的库)和在构建过程中需要执行的步骤。Maven遵循约定高于配置的原则,即如果你遵守标准的项目结构和命名约定,就不需要在配置文件中明确地声明源文件在哪里或者应该在哪里编译类。

+

本书不是一个Maven教程,如果有需要,请在网上搜索Maven的相关资料。源代码文件夹定义了一个父项目,它声明需要使用的插件并且声明需要使用的库的版本。

+

LWJGL 3.1在项目构建上有一些变化。现在,它变得更加模块化,我们可以有选择地使用类库,而不是导入一个巨大的Jar文件。但这是有代价的:你需要仔细地逐个指定依赖关系。不过LWJGL下载页面提供了一个为您生成POM文件的脚本。在本例中,我们将只使用GLFW和OpenGL。你可以在源代码中查看我们的POM文件。

+

LWJGL平台依赖库已经可以为你的操作系统自动解压本地库,因此不需要使用其他插件(例如mavennatives)。我们只需要配置三个Profile来设置LWJGL所处的操作系统。Profile将会为Windows、Linux和Mac OS系列设置正确的值。

+
    <profiles>
+        <profile>
+            <id>windows-profile</id>
+            <activation>
+                <os>
+                    <family>Windows</family>
+                </os>
+            </activation>
+            <properties>
+                <native.target>natives-windows</native.target>
+            </properties>                
+        </profile>
+        <profile>
+            <id>linux-profile</id>
+            <activation>
+                <os>
+                    <family>Linux</family>
+                </os>
+            </activation>
+            <properties>
+                <native.target>natives-linux</native.target>
+            </properties>                
+        </profile>
+        <profile>
+            <id>OSX-profile</id>
+            <activation>
+                <os>
+                    <family>mac</family>
+                </os>
+            </activation>
+            <properties>
+                <native.target>natives-osx</native.target>
+            </properties>
+        </profile>
+    </profiles>
+
+

在每个项目中,LWJGL平台依赖项将使用配置文件为当前平台设置的正确的属性。

+
        <dependency>
+            <groupId>org.lwjgl</groupId>
+            <artifactId>lwjgl-platform</artifactId>
+            <version>${lwjgl.version}</version>
+            <classifier>${native.target}</classifier>
+        </dependency>
+
+

此外,每个项目可生成一个可运行的Jar(一种可以通过输入java -jar name_of_the_jar.jar就可运行的Jar),这是通过使用Maven的maven-jar-plugin插件实现的。该插件创建了一个含有MANIFEST.MF文件的Jar,并且文件内有指定的值,该文件最重要的属性就是Main-Class,它指明了程序的入口。此外,所有的依赖库都被设置在该文件的Class-Path属性中。要在另一台计算机上运行它,你只需要复制位于目标目录下的主Jar文件和Lib目录(包括其中所有的Jar文件)。

+

Jar文件储存着LWJGL类和本地库。LWJGL还将负责提取它们,并将它们添加到JVM的库路径中。

+

本章的源代码是LWJGL网站(https://www.lwjgl.org/guide)提供的的入门示例,你可以看到我们没有使用Swing或JavaFX作为我们的GUI库。我们使用的是GLFW,它是一个用于处理GUI组件(窗口等)和事件(按键按下、鼠标移动等),并且与OpenGL上下文进行简单连接的库。此前版本的LWJGL提供了一个自定义GUI API,但在LWJGL 3中,GLFW是首选的窗口API。

+

示例源码很简单并有着良好的文档,所以我们不会在书中再次说明。

+

如果你正确地配置了环境,你应该能够运行它并看到一个有红色背景的窗口。

+

Hello World

+

本书中源代码发布于GitHub

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + + Next » + + +
+ + + + + + + + + diff --git a/02-the-game-loop/index.html b/02-the-game-loop/index.html new file mode 100755 index 0000000..f6da4de --- /dev/null +++ b/02-the-game-loop/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + 游戏循环 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

游戏循环(The Game Loop)

+

在本章中,我们将通过创建游戏循环来开始开发游戏引擎。游戏循环是每个游戏的核心部分,它基本上是一个无休止的循环,负责周期地处理用户的输入、更新游戏状态和渲染图像到屏幕上。

+

下述代码片段展示了游戏循环的结构:

+
while (keepOnRunning) {
+    handleInput();
+    updateGameState();
+    render();
+}
+
+

那么,这就完了吗?我们已经完成游戏循环了吗?显然还没有,上述代码中有很多缺陷。首先,游戏循环运行的速度将取决于运行它的计算机。如果计算机足够快,用户甚至看不到游戏中发生了什么。此外,这个游戏循环将消耗所有的计算机资源。

+

因此,我们需要游戏循环独立于运行的计算机,尝试以恒定速率运行。假设我们希望游戏以每秒50帧(50 Frames Per Second,50 FPS)的恒定速率运行,那么游戏循环代码可能是这样的:

+
double secsPerFrame = 1.0d / 50.0d;
+
+while (keepOnRunning) {
+    double now = getTime();
+    handleInput();
+    updateGameState();
+    render();
+    sleep(now + secsPerFrame – getTime());
+}
+
+

这个游戏循环很简单,可以用于一些游戏,但是它也存在一些缺陷。首先,它假定我们的更新和渲染方法适合以50FPS(即secsPerFrame等于20毫秒)的速率更新。

+

此外,我们的计算机可能会优先考虑暂停游戏循环运行一段时间,以运行其他的任务。因此,我们可能会在非常不稳定的时间周期更新游戏状态,这是不符合游戏物理的要求的。

+

最后,线程休眠的时间精度仅仅只有0.1秒,所以即使我们的更新和渲染方法没有消耗时间,也不会以恒定的速率更新。所以,如你所见,问题没那么简单。

+

在网上你可以找到大量的游戏循环的变种。在本书中,我们将用一个不太复杂的,在大多数情况下都能正常工作的方法。我们将用的方法通常被称为定长游戏循环(Fixed Step Game Loop)。

+

首先,我们可能想要单独控制游戏状态被更新的周期和游戏被渲染到屏幕的周期。为什么要这么做?因为以恒定的速率更新游戏状态更为重要,特别是如果使用物理引擎。相反,如果渲染没有及时完成,在运行游戏循环时渲染旧帧也是没有意义的,我们可以灵活地跳过某些帧。

+

让我们看看现在的游戏循环是什么样的:

+
double secsPerUpdate = 1.0d / 30.0d;
+double previous = getTime();
+double steps = 0.0;
+while (true) {
+  double loopStartTime = getTime();
+  double elapsed = loopStartTime - previous;
+  previous = loopStartTime;
+  steps += elapsed;
+
+  handleInput();
+
+  while (steps >= secsPerUpdate) {
+    updateGameState();
+    steps -= secsPerUpdate;
+  }
+
+  render();
+  sync(loopStartTime);
+}
+
+

使用这个游戏循环,我们可以在固定的周期更新游戏状态。但是如何避免耗尽计算机资源,使它不连续渲染呢?这在sync方法中实现:

+
private void sync(double loopStartTime) {
+   float loopSlot = 1f / 50;
+   double endTime = loopStartTime + loopSlot; 
+   while(getTime() < endTime) {
+       try {
+           Thread.sleep(1);
+       } catch (InterruptedException ie) {}
+   }
+}
+
+

那么上述方法做了什么呢?简而言之,我们计算游戏循环迭代应该持续多长时间(它被储存在loopSlot变量中),休眠的时间取决于在循环中花费的时间。但我们不做一整段时间的休眠,而是进行一些小的休眠。这允许其他任务运行,并避免此前提到的休眠准确性问题。接下来我们要做的是: +1. 计算应该退出这个方法的时间(这个变量名为endTime),并开始游戏循环的另一次迭代。 +2. 比较当前时间和结束时间,如果没有到达结束时间,就休眠1毫秒。

+

现在是构建代码库以便开始编写游戏引擎的第一个版本的时候了。但在此之前,我们来讨论一下控制渲染速率的另一种方法。在上述代码中,我们做微休眠是为了控制需要等待的时间。但我们可以选择另一种方法来限制帧率。我们可以使用垂直同步(Vertical Synchronization),垂直同步的主要目的是避免画面撕裂。什么是画面撕裂?这是一种显示现象,当正在渲染时,我们更新图像储存区,导致屏幕的一部分显示先前的图像,而屏幕的另一部分显示正在渲染的图像。如果启用垂直同步,当GPU中的数据正被渲染到屏幕上时,我们不会向GPU发送数据。

+

当开启垂直同步时,我们将与显卡的刷新率同步,显卡将以恒定的帧率渲染。用下述一行代码启用它:

+
glfwSwapInterval(1);
+
+

有了上述代码,就意味着至少在一个屏幕更新被绘制到屏幕之前,我们必须等待。事实上我们不是直接绘制到屏幕上。相反,我们将数据储存在缓冲区中,然后用下面的方法交换它:

+
glfwSwapBuffers(windowHandle);
+
+

因此,如果启用垂直同步,我们就可以实现稳定的帧率,而不需要进行微休眠来检查更新时间。此外,帧率将与设备的显卡刷新率相匹配,也就是说,如果它设定为60Hz(60FPS),那么我们就有60FPS。我们可以通过在glfwSwapInterval方法中设置高于1的数字来降低这个速率(如果设置为2,将得到30FPS)。

+

让我们整理一下源代码。首先,我们将把所有的GLFW窗口初始化代码封装在一个名为Window的类中,传递一些基本的参数(如标题和大小)。Window类还提供一个方法以便在游戏循环中检测按下的按键:

+
public boolean isKeyPressed(int keyCode) {
+    return glfwGetKey(windowHandle, keyCode) == GLFW_PRESS;
+}
+
+

除了有初始化代码以外,Window类还需要知道窗口大小被调整。因此需要设置一个回调方法,在窗口大小被调整时调用它。回调方法将接收帧缓冲区(渲染区域,简单来说就是显示区域)的以像素为单位的宽度和高度。如果希望得到帧缓冲区的宽度和高度,你可以使用glfwSetWindowSizeCallback方法。屏幕坐标不一定对应像素(例如,具有视网膜显示屏(Retina Display)的Mac设备)。因为我们将在进行OpenGL调用时使用这些信息,所以要注意像素不在屏幕坐标中,你可以通过GLFW的文档了解更多信息。

+
// 设置调整大小回调
+glfwSetFramebufferSizeCallback(windowHandle, (window, width, height) -> {
+    Window.this.width = width;
+    Window.this.height = height;
+    Window.this.setResized(true);
+});
+
+

我们还将创建一个Renderer类,它将处理我们游戏的渲染。现在,它仅会有一个空的init方法,和另一个用预设颜色清空屏幕的方法:

+
public void init() throws Exception {
+}
+
+public void clear() {
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+

然后我们将创建一个名为IGameLogic的接口,它封装了我们的游戏逻辑。这样,我们就可以让游戏引擎在不同的游戏上重复使用。该接口将具有获取输入、更新游戏状态和渲染游戏内容的方法。

+
public interface IGameLogic {
+
+    void init() throws Exception;
+
+    void input(Window window);
+
+    void update(float interval);
+
+    void render(Window window);
+}
+
+

然后我们将创建一个名为GameEngine的类,它将包含我们游戏循环的代码,该类将实现储存游戏循环:

+
public class GameEngine implements Runnable {
+
+    //...
+
+    public GameEngine(String windowTitle, int width, int height, boolean vSync, IGameLogic gameLogic) throws Exception {
+        window = new Window(windowTitle, width, height, vSync);
+        this.gameLogic = gameLogic;
+        //...
+    }
+
+

vSync参数允许我们选择是否启用垂直同步。你可以看到我们实现了GameEngine类的run方法,其中包括游戏循环:

+
@Override
+public void run() {
+    try {
+        init();
+        gameLoop();
+    } catch (Exception excp) {
+        excp.printStackTrace();
+    }
+}
+
+

GameEngine类提供了一个run方法,该方法将执行初始化任务,并运行游戏循环,直到我们关闭窗口。关于线程需要注意的一点是,GLFW需要从主线程初始化,事件的轮询也应该在该线程中完成。因此,我们将在主线程中执行所有内容,而不是为游戏循环创建单独的线程。

+

在源代码中,你将看到我们创建了其他辅助类,例如Timer(它将提供用于计算已经过的时间的实用方法),并在游戏循环逻辑中使用它们。

+

GameEngine类只是将inputupdate方法委托给IGameLogic实例。在render方法中,它也委托给IGameLogic实例并更新窗口。

+
protected void input() {
+    gameLogic.input(window);
+}
+
+protected void update(float interval) {
+    gameLogic.update(interval);
+}
+
+protected void render() {
+    gameLogic.render(window);
+    window.update();
+}
+
+

在程序的入口,含有main方法的类只会创建一个GameEngine实例并运行它。

+
public class Main {
+
+    public static void main(String[] args) {
+        try {
+            boolean vSync = true;
+            IGameLogic gameLogic = new DummyGame();
+            GameEngine gameEng = new GameEngine("GAME",
+                600, 480, vSync, gameLogic);
+            gameEng.run();
+        } catch (Exception excp) {
+            excp.printStackTrace();
+            System.exit(-1);
+        }
+    }
+
+}
+
+

最后,在本章中我们只需要创建一个简单的游戏逻辑类。它只会在按下上或下键时,变亮或变暗窗口的颜色缓冲区的清空颜色,render方法将会用这个颜色清空窗口的颜色缓冲区。

+
public class DummyGame implements IGameLogic {
+
+    private int direction = 0;
+
+    private float color = 0.0f;
+
+    private final Renderer renderer;
+
+    public DummyGame() {
+        renderer = new Renderer();
+    }
+
+    @Override
+    public void init() throws Exception {
+        renderer.init();
+    }
+
+    @Override
+    public void input(Window window) {
+        if (window.isKeyPressed(GLFW_KEY_UP)) {
+            direction = 1;
+        } else if (window.isKeyPressed(GLFW_KEY_DOWN)) {
+            direction = -1;
+        } else {
+            direction = 0;
+        }
+    }
+
+    @Override
+    public void update(float interval) {
+        color += direction * 0.01f;
+        if (color > 1) {
+            color = 1.0f;
+        } else if ( color < 0 ) {
+            color = 0.0f;
+        }
+    }
+
+    @Override
+    public void render(Window window) {
+        if (window.isResized()) {
+            glViewport(0, 0, window.getWidth(), window.getHeight());
+            window.setResized(false);
+        }
+        window.setClearColor(color, color, color, 0.0f);
+        renderer.clear();
+    }    
+}
+
+

render方法中,当窗口大小被调整时,我们接收通知,以便更新视口将坐标中心定位到窗口的中心。

+

创建的类层次结构将帮助我们将游戏引擎代码与具体的游戏代码分开。虽然现在可能看起来没有必要,但我们已将每个游戏的通用代码,从具体的游戏的逻辑、美术作品和资源中分离出来,以便重用游戏引擎。在此后的章节中,我们需要重构这个类层次结构,因为我们的游戏引擎变得更加复杂。

+

平台差异(OSX)

+

你可以运行上面的代码在Windows或Linux上,但我们仍需要为OS X平台做一些修改。正如GLFW文档中所描述的:

+
+

目前OS X仅支持的OpenGL 3.x和4.x版本的环境是向上兼容的。OS X 10.7 Lion支持OpenGL 3.2版本和OS X 10.9 Mavericks支持OpenGL 3.3和4.1版本。在任何情况下,你的GPU需要支持指定版本的OpenGL,以成功创建环境。

+
+

因此,为了支持在此后章节中介绍的特性,我们需要将下述代码添加到Window类创建窗口代码之前:

+
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
+        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
+
+

这将使程序使用OpenGL 3.2到4.1之间的最高版本。如果没有上述代码,就会使用旧版本的OpenGL。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/03-a-brief-about-coordinates/index.html b/03-a-brief-about-coordinates/index.html new file mode 100755 index 0000000..7d92c6a --- /dev/null +++ b/03-a-brief-about-coordinates/index.html @@ -0,0 +1,291 @@ + + + + + + + + + + + + 坐标简介 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

坐标简介(A brief about coordinates)

+

本章中我们将讨论坐标和坐标系(Coordinate System),尝试以简单的方式介绍一些基本的数学概念,为后面章节将要介绍的技术和内容提供帮助。我们将会将一些内容简化,以易于学习,但牺牲了准确性。

+

我们通过指定坐标来确定物体在空间中的位置。联想地图,通过在地图上指定纬度和经度来确定一个点。只需一对数字,就可以精确地确认一个点,这对数字就是点坐标(实际上有些复杂,需要更多的数据才能在地图上确定一个点,因为地图是一个不完美的椭圆球体(地球)的投影,但这是一个很好的类比)。

+

坐标系是一个系统,它使用一个或多个数字,即一个或多个分量来唯一地确定一个点的位置。存在着多种不同的坐标系(如笛卡尔坐标系,极坐标系等),并且可以将坐标从一个坐标系转换到另一个坐标系。我们将使用笛卡尔坐标系。

+

在二维笛卡尔坐标系中,坐标由两个数字定义,它们表示到两个相互垂直的X轴和Y轴间距离。

+

笛卡尔坐标系

+

继续类比地图,坐标系定义一个原点。对于地理坐标来说,原点被设置为赤道和零度经线交叉的点。根据原点设置的位置,特定点的坐标是不同的。坐标系也可以定义轴的方向。在上图中,X坐标随着点向右移动而增加,Y坐标随着点向上移动而增加。但我们也可以定义一个与笛卡尔坐标系不同的,具有不同的轴取向的坐标系,我们将得到不同的坐标。

+

非传统笛卡尔坐标系

+

如你所见,我们需要定义一些参数,例如原点和轴方向,以便使构成坐标的数字对有恰当的含义。为了使用一组坐标,我们必须使用对应的坐标系,好消息是可以通过平移和旋转将坐标从一个坐标系转换到另一个坐标系。

+

如果要处理三维坐标,我们需要增加一个轴,即Z轴。三维坐标将由三个数字(x, y, z)构成。

+

三维笛卡尔坐标系

+

在二维笛卡尔坐标系中,只要轴相互垂直,我们就可以改变三维坐标系中的轴的方向。下图显示另一个三维坐标系。

+

非传统三维笛卡尔坐标系

+

三维坐标可分为左手系和右手系两种类型。你怎么知道它是什么类型的?用你的手在你的拇指和食指之间形成一个“L”,中指应指向垂直于其他两个手指的方向。拇指应该指向X轴的正方向,食指应该指向Y轴的正方向,而中指应该指向Z轴的正方向。如果你能用左手做到,那么它就是左手系,如果你需要用右手,那它就是右手系。

+

右手系vs左手系

+

二维坐标系是相同的,因为通过旋转,我们可以从一个坐标系转换到另一个坐标系。但是,三维坐标系并不都是相同的。如果它们可以使用相同的手来表示,换句话说,如果两者都是左手系或者右手系,那么就能通过旋转从一个坐标系转换到另一个坐标系。

+

现在已经明确了一些基本的概念,让我们来讲解一些在处理三维图形时常用的术语。当我们在此后的章节中解释如何渲染三维模型时,将看到我们将使用不同的三维坐标系,这是因为每个坐标系都有不同的设定,有不同的目的。一组坐标是没有意义的,除非明确它是某个坐标系的坐标。当你看到坐标(40.438031, -3.676626)时,你可能会有一个大胆的想法。但是如果我说它是几何坐标(经度和纬度)时,你就会发现它是马德里某个地方的坐标。

+

当我们加载三维物体时,我们将得到一组三维坐标。这些坐标在被称为物体坐标系(Object Coordinate Space)的三维坐标系中表达。当建模师在设计这些三维模型的时候,他们对该模型将显示的三维场景毫不知情,因此只能使用与模型相关的坐标系来定义坐标。

+

当要绘制一个三维场景时,所有的三维物体将与被称为世界空间坐标系的坐标系对应。我们需要将三维物体的坐标系转换到世界坐标系。一些物体需要旋转、拉伸、放大和转换,以便在三维场景中能够正确地显示。

+

我们还需要限制所显示的三维空间的范围,例如移动摄像机穿梭在三维空间中。然后我们需要将世界空间坐标系的坐标转换到摄像机或观察空间坐标系。最后,这些坐标需要转换为二维的屏幕坐标,所以我们需要将三维观察坐标投影到二维屏幕空间坐标系。

+

下图展示了OpenGL坐标系(Z轴垂直于屏幕),坐标在-1和+1之间。

+

OpenGL坐标系

+

如果你不能清楚地理解这些概念,别担心。在下一章中,它们将用实例表现出来。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/04-rendering/index.html b/04-rendering/index.html new file mode 100755 index 0000000..5ed46e8 --- /dev/null +++ b/04-rendering/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + 渲染 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

渲染(Rendering)

+

在本章中,我们将学习用OpenGL渲染场景时要做的事项。如果你已经习惯了OpenGL的旧版本,习惯了使用固定管线,你可能会跳过这一章,不想知道为什么它需要这么复杂。它其实更简单、更灵活,你只需要给它一个表现的机会。现代OpenGL使你只需考虑一个问题,这可以使你以更合理的方式组织代码和开发。

+

将三维表示映射到二维屏幕的一系列步骤被统称为图形管线(Graphics Pipeline)。OpenGL最初的版本使用了一个被称为固定管线(Fixed-function Pipeline)的模型。该模型在绘制过程中定义了一组固定的操作步骤,程序员被每一步骤可用的函数集约束,可以使用的效果和可进行的操作受到API(例如,“设置雾”或“添加光照”)的限制,但是这些功能的实现是固定的,并且不能修改。

+

图形管线由以下操作步骤组成:

+

图形管线

+

OpenGL 2.0 引入了可编程管线(Programmable Pipeline)的概念。在该模型中,组成图形管线的不同步骤可以通过使用一组叫做着色器(Shader)的特定程序来控制或编程。下图简单的展示了OpenGL可编程管线:

+

可编程管线

+

该渲染方式最初将以顶点缓冲区为形式的一系列顶点作为输入。但是,什么是顶点?顶点(Vertex)是描述二维或者三维空间中的点的数据结构。如何描述三维空间中的一个点呢?通过指定其X、Y和Z坐标。什么是顶点缓冲区?顶点缓冲区(Vertex Buffer)是使用顶点数组来包装所有需要渲染的顶点的另一种数据结构,并使这些数据能够在图形管线的着色器中使用。

+

这些顶点由顶点着色器(Vertex Shader)处理,顶点着色器的功能是计算每个顶点到屏幕空间中的投影位置。该着色器还可以生成与颜色或纹理相关的其他输出,但其主要目的还是将顶点投影到屏幕空间中,即生成点。

+

几何处理阶段(Geometry Processing)将由顶点着色器变换的顶点连接成三角形。它依照顶点储存的顺序,使用不同的模型对顶点进行分组。为什么是三角形?三角形就是显卡的基本工作单元,它是一个简单的几何形状,可以组合和变换,以构建复杂的三维场景。此阶段还可以使用特定的着色器来对顶点进行分组。

+

光栅化(Rasterization)阶段接收此前生成的三角形,剪辑它们,并将它们转换为像素大小的片元。

+

这些片元将在片元处理阶段(Fragment Processing)被片元着色器(Fragment Shader)使用,以生成写入到帧缓冲区的像素的最终颜色。帧缓冲区(Framebuffer)是图形管线的最终输出,它储存了每个像素应该被绘制到屏幕上的值。

+

注意,显卡被设计成并行处理上述所有操作,输入的数据可以并行处理以生成最终场景。

+

让我们开始编写第一个着色器程序。着色器是使用基于ANSI C的OpenGL着色器语言(GLSL)编写的。首先,在resources目录下创建一个名为“vertex.vs”(扩展名为顶点着色器英文简写)的文件,内容如下:

+
#version 330
+
+layout (location=0) in vec3 position;
+
+void main()
+{
+    gl_Position = vec4(position, 1.0);
+}
+
+

第一行是一个表示我们正使用的GLSL语言版本的标识符。下表是GLSL版本、与该版本匹配的OpenGL版本和使用方法(来自维基百科:https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GLSL版本OpenGL版本着色器标识符
1.10.592.0#version 110
1.20.82.1#version 120
1.30.103.0#version 130
1.40.083.1#version 140
1.50.113.2#version 150
3.30.63.3#version 330
4.00.94.0#version 400
4.10.64.1#version 410
4.20.114.2#version 420
4.30.84.3#version 430
4.404.4#version 440
4.504.5#version 450
+

第二行指定此着色器的输入格式。OpenGL缓冲区中的数据可以是我们想要的任何数据,也就是说,该语言不会强迫你传递预定义语言的任何指定数据结构。从着色器的角度来看,它期望接收一个存有数据的缓冲区。它可以是一个位置,一个有一些附加信息的位置,或者我们想要的任何数据。顶点着色器只接收浮点数组。当填充缓冲区时,我们定义要由着色器处理的缓冲区块。

+

首先,需要把这些块变成对我们有意义的数据。现在规定从位置0开始,我们期望接收由三个属性(X, Y, Z)组成的向量。

+

着色器有个main代码块,就像任何C语言程序一样,上述示例是非常简单的。它只是将接收到的坐标不经任何变换地返回到gl_Position。你现在可能想知道为什么三个属性的向量被转换成四个属性的向量(vec4)。这是因为gl_Position仅接收vec4类型的数据,因为它是齐次坐标(Homogeneous Coordinates)。也就是说,它希望接收到形似(X, Y, Z, W)的东西,其中W代表一个额外的维度。为什么还要添加另一个维度?在此后的章节中,你会看到我们需要做的大部分操作都是基于向量和矩阵的。如果没有额外的维度,一些操作不能组合。例如,不能把旋转和位移操作组合起来。(如果你想学习更多有关于这方面的知识,这个额外的维度允许我们组合仿射和线性变换。你可以通过阅读《3D Math Primer for Graphics and Game Development》(作者是Fletcher Dunn 和 Ian Parberry)来更多地了解这一点。)

+

现在来看看我们的第一个片元着色器。在resources目录下创建一个名为fragment.fs(扩展名片元着色器英文简写)的文件,内容如下:

+
#version 330
+
+out vec4 fragColor;
+
+void main()
+{
+    fragColor = vec4(0.0, 0.5, 0.5, 1.0);
+}
+
+

该结构与我们的顶点着色器非常相似。现在,它将为每个片元设置固定的颜色。输出值被定义为第二行的vec4类型的fragColor变量。

+

现在我们已经创建了着色器,该如何使用它们呢?以下是我们要做的一系列步骤: +1. 创建OpenGL程序。 +2. 载入顶点和片元着色器文件。 +3. 为每个着色器创建一个新的着色器程序并指定它的类型(顶点或片元)。 +4. 编译着色器。 +5. 将着色器绑定到OpenGL程序上。 +6. 连接程序。

+

最后,着色器将会被载入到显卡中,我们可以通过引用程序ID来使用它。

+
package org.lwjglb.engine.graph;
+
+import static org.lwjgl.opengl.GL20.*;
+
+public class ShaderProgram {
+
+    private final int programId;
+
+    private int vertexShaderId;
+
+    private int fragmentShaderId;
+
+    public ShaderProgram() throws Exception {
+        programId = glCreateProgram();
+        if (programId == 0) {
+            throw new Exception("Could not create Shader");
+        }
+    }
+
+    public void createVertexShader(String shaderCode) throws Exception {
+        vertexShaderId = createShader(shaderCode, GL_VERTEX_SHADER);
+    }
+
+    public void createFragmentShader(String shaderCode) throws Exception {
+        fragmentShaderId = createShader(shaderCode, GL_FRAGMENT_SHADER);
+    }
+
+    protected int createShader(String shaderCode, int shaderType) throws Exception {
+        int shaderId = glCreateShader(shaderType);
+        if (shaderId == 0) {
+            throw new Exception("Error creating shader. Type: " + shaderType);
+        }
+
+        glShaderSource(shaderId, shaderCode);
+        glCompileShader(shaderId);
+
+        if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) {
+            throw new Exception("Error compiling Shader code: " + glGetShaderInfoLog(shaderId, 1024));
+        }
+
+        glAttachShader(programId, shaderId);
+
+        return shaderId;
+    }
+
+    public void link() throws Exception {
+        glLinkProgram(programId);
+        if (glGetProgrami(programId, GL_LINK_STATUS) == 0) {
+            throw new Exception("Error linking Shader code: " + glGetProgramInfoLog(programId, 1024));
+        }
+
+        if (vertexShaderId != 0) {
+            glDetachShader(programId, vertexShaderId);
+        }
+        if (fragmentShaderId != 0) {
+            glDetachShader(programId, fragmentShaderId);
+        }
+
+        glValidateProgram(programId);
+        if (glGetProgrami(programId, GL_VALIDATE_STATUS) == 0) {
+            System.err.println("Warning validating Shader code: " + glGetProgramInfoLog(programId, 1024));
+        }
+
+    }
+
+    public void bind() {
+        glUseProgram(programId);
+    }
+
+    public void unbind() {
+        glUseProgram(0);
+    }
+
+    public void cleanup() {
+        unbind();
+        if (programId != 0) {
+            glDeleteProgram(programId);
+        }
+    }
+}
+
+

ShaderProgram类的构造函数在OpenGL中创建一个新的程序,并提供添加顶点和片元着色器的方法。这些着色器被编译并绑定到OpenGL程序中。当所有的着色器都被绑定时,应该调用link方法,来连接所有代码并验证所有操作都已正确地完成。

+

一旦着色器程序被连接,编译的顶点和片元着色器可以被释放(通过调用glDetachShader方法)。

+

验证是通过调用glValidateProgram方法完成的。此方法主要用于调试,当游戏到达生产阶段时,应将其删除。此方法将验证在当前OpenGL状态下着色器是否正确。这意味着,即使着色器是正确的,在某些情况下也可能验证失败,这是因为当前状态不够完整(一些数据可能尚未加载),无法运行着色器。因此,我们可以将错误信息输出到标准错误输出中。

+

ShaderProgram类还提供了在渲染时激活该程序(绑定)和停止使用它(解绑)的方法。最后,它提供了一个cleanup方法,用于在它不再被需要时,释放所有资源。

+

既然有一个清理方法,让我们更改IGameLogic接口来添加一个cleanup方法:

+
void cleanup();
+
+

该方法将在游戏循环结束时调用,所以需要修改GameEngine类的run方法:

+
@Override
+public void run() {
+    try {
+        init();
+        gameLoop();
+    } catch (Exception excp) {
+        excp.printStackTrace();
+    } finally {
+        cleanup();
+    }
+}
+
+

现在我们可以在Renderer类的init方法中使用着色器来显示一个三角形。首先,我们要创建着色器程序:

+
public void init() throws Exception {
+    shaderProgram = new ShaderProgram();
+    shaderProgram.createVertexShader(Utils.loadResource("/vertex.vs"));
+    shaderProgram.createFragmentShader(Utils.loadResource("/fragment.fs"));
+    shaderProgram.link();
+}
+
+

我们已经创建了一个工具类,它提供了一个从类路径中取得文件内容的方法,此方法用于取得我们的着色器代码。

+

现在我们可以把三角形定义为一组浮点数,创建一个一维浮点数组,它将定义三角形的顶点。如你所见,数组中没有数据结构。就目前而言,OpenGL无法知道该数组的结构,这只是一组浮点数:

+
float[] vertices = new float[]{
+     0.0f,  0.5f, 0.0f,
+    -0.5f, -0.5f, 0.0f,
+     0.5f, -0.5f, 0.0f
+};
+
+

下图体现了在坐标系中的三角形。

+

三角形

+

现在我们有了坐标,需要把它们储存到显卡中,并告诉OpenGL它的数据结构。现在将介绍两个重要的概念,顶点数组对象(Vertex Array Object,VAO)和顶点缓冲对象(Vertex Buffer Object,VBO)。如果你对接下来的代码感到困惑,请记住,现在所做的是把将要绘制的模型对象的数据传递到显存中。当储存它的时候,我们会得到一个ID,稍后绘制时会使用它。

+

先介绍顶点缓冲对象(VBO)吧,VBO只是显存中存储顶点的内存缓冲区。这是用来暂存一组用于建模三角形的浮点数的地方。如上所述,OpenGL对我们的数据结构一无所知。事实上,它不仅可以储存坐标,还可以储存其他信息,比如纹理、颜色等。

+

顶点数组对象(VAO)是一个对象,储存一个或多个通常被称为属性列表的VBO。每个属性列表可以保存一种类型的数据:位置、颜色、纹理等。在每个渲染间隔中,你可以自由地储存所需的任何数据。

+

一个VAO就像是一个包装,它按一组定义对储存在显卡中的数据分组。当创建一个VAO时,我们得到一个ID。我们使用此ID来渲染它和使用它在创建过程中定义的数据。

+

让我们继续编写示例代码。首先要做的事就是把浮点数储存在一个FloatBuffer中。这主要是因为我们必须使用基于C语言的OpenGL库的接口,所以必须把浮点数组转换成可以由库管理的东西。

+
FloatBuffer verticesBuffer = MemoryUtil.memAllocFloat(vertices.length);
+verticesBuffer.put(vertices).flip();
+
+

我们使用MemoryUtil类来在堆外内存中创建了一个缓冲区,以便OpenGL库访问它。在储存了数据(调用put方法)之后,我们需要调用flip方法将缓冲区的位置重置为0(也就是说,我们已经完成了对它的写入)。记住,Java中的对象,被分配在一个叫堆(Heap)的内存空间。堆是JVM内存中保留的一大堆内存,储存在堆中的对象不能通过本地代码访问(JNI,这种机制使得Java不能直接调用本地代码)。Java代码和本地代码直接共享内存数据的唯一方法是在Java中直接地分配内存。

+

如果你来自LWJGL的旧版本,强调一些要点是很重要的。你可能注意到了,我们不使用工具类BufferUtils,而使用MemoryUtil类来创建缓冲区。这是由于BufferUtils不是非常有效的,并且仅被用于向下兼容。LWJGL3提供了两种缓冲区的管理方法:

+
    +
  • 自动管理缓冲区,即由垃圾回收器自动回收的缓冲区。这些缓冲区适用于短暂的操作,或者用于传递到GPU的数据,并且不需要储存于进程内存中。这是通过使用org.lwjgl.system.MemoryStack实现的。
  • +
  • 手动管理缓冲区。此情况下,一旦完成操作,我们需要小心地释放它们。这些缓冲区适用于长时间的操作或者大量的数据。这是通过使用MemoryUtil类实现的。
  • +
+

你可以在此处查阅细节: +https://blog.lwjgl.org/memory-management-in-lwjgl-3/

+

在此情况下,我们的数据被发送到GPU,这样可以考虑使用自动管理的缓冲区。但稍后我们将使用它们来储存可能需要手动管理的大量数据,这就是使用MemoryUtil类的原因,因此,这就是为什么我们要在最后一个块中释放缓冲区资源。在下章中,我们将学习如何使用自动管理缓冲区。

+

现在需要创建VAO然后绑定它:

+
vaoId = glGenVertexArrays();
+glBindVertexArray(vaoId);
+
+

然后需要创建VBO,绑定它并将数据输入:

+
vboId = glGenBuffers();
+glBindBuffer(GL_ARRAY_BUFFER, vboId);
+glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);
+glEnableVertexAttribArray(0);
+
+

接下来是最重要的部分。我们需要定义数据结构,并将其储存在VAO的属性列表中,这是用下述代码完成的:

+
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
+
+

它的参数是:

+
    +
  • index: 指定着色器期望此数据的位置。
  • +
  • size: 指定每个顶点属性的数据数(从1到4)。现在,我们使用三维坐标,所以它应该是3。
  • +
  • type: 指定数组中每个数据的类型,现在是浮点数。
  • +
  • normalized: 指定值是否应归一化。
  • +
  • stride: 指定连续顶点数据之间的字节偏移量(稍后我们再解释)。
  • +
  • offset: 指定缓冲区中第一个数据的偏移量。
  • +
+

在完成了VBO操作之后,我们可以解除它和VAO的绑定(绑定到0)。

+
// 解绑VBO
+glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+// 解绑VAO
+glBindVertexArray(0);
+
+

一旦完成操作,我们必须通过手动调用memFree方法释放由FloatBuffer占用的堆外内存,因为Java垃圾回收不会清理分配的堆外内存。

+
if (verticesBuffer != null) {
+    MemoryUtil.memFree(verticesBuffer);
+}
+
+

这就是init方法应有的代码。我们的数据已经在显卡中准备使用了,现在只需要修改render方法在游戏循环中进行渲染。

+
public void render(Window window) {
+    clear();
+
+    if ( window.isResized() ) {
+        glViewport(0, 0, window.getWidth(), window.getHeight());
+        window.setResized(false);
+    }
+
+    shaderProgram.bind();
+
+    // 绑定VAO
+    glBindVertexArray(vaoId);
+
+    // 绘制顶点
+    glDrawArrays(GL_TRIANGLES, 0, 3);
+
+    // 还原状态
+    glBindVertexArray(0);
+
+    shaderProgram.unbind();
+}
+
+

如你所见,我们只需要清理窗口,绑定着色器程序,绑定VAO,绘制储存在VAO关联的VBO中的顶点,然后还原状态,仅此而已。

+

我们还在Renderer类中添加了一个cleanup方法用于释放资源。

+
public void cleanup() {
+    if (shaderProgram != null) {
+        shaderProgram.cleanup();
+    }
+
+    glDisableVertexAttribArray(0);
+
+    // 删除VBO
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDeleteBuffers(vboId);
+
+    // 删除VAO
+    glBindVertexArray(0);
+    glDeleteVertexArrays(vaoId);
+}
+
+

就这样!如果你小心地按着上述步骤做,你会看到类似的图像。

+

三角形游戏

+

这就是我们的第一个三角形!你也许会想这并不会使它成为前十名的游戏,你的想法是对的。你也可以认为这是一件无聊的事情来画一个无聊的三角形。但请记住,我们正在介绍关键的概念,并准备基于架构来做更复杂的事情,请耐心等待,继续阅读。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/05-more-on-rendering/index.html b/05-more-on-rendering/index.html new file mode 100755 index 0000000..e251f8c --- /dev/null +++ b/05-more-on-rendering/index.html @@ -0,0 +1,527 @@ + + + + + + + + + + + + 渲染补充 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

渲染补充(More on Rendering)

+

本章我们将继续讲述OpenGL如何渲染物体。为了整理代码,我们要创建一个名为Mesh的新类,把一个位置数组作为输入,为需要加载到显卡中的模型创建VBO和VAO对象。

+
package org.lwjglb.engine.graph;
+
+import java.nio.FloatBuffer;
+import org.lwjgl.system.MemoryUtil;
+
+import static org.lwjgl.opengl.GL30.*;
+
+public class Mesh {
+
+    private final int vaoId;
+
+    private final int vboId;
+
+    private final int vertexCount;
+
+    public Mesh(float[] positions) {
+        FloatBuffer verticesBuffer = null;
+        try {
+            verticesBuffer = MemoryUtil.memAllocFloat(positions.length);
+            vertexCount = positions.length / 3;
+            verticesBuffer.put(positions).flip();
+
+            vaoId = glGenVertexArrays();
+            glBindVertexArray(vaoId);
+
+            vboId = glGenBuffers();
+            glBindBuffer(GL_ARRAY_BUFFER, vboId);
+            glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);    
+            glEnableVertexAttribArray(0);        
+            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
+            glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+            glBindVertexArray(0);         
+        } finally {
+            if (verticesBuffer  != null) {
+                MemoryUtil.memFree(verticesBuffer);
+            }
+        }
+    }
+
+    public int getVaoId() {
+        return vaoId;
+    }
+
+    public int getVertexCount() {
+        return vertexCount;
+    }
+
+    public void cleanUp() {
+        glDisableVertexAttribArray(0);
+
+        // 删除VBO
+        glBindBuffer(GL_ARRAY_BUFFER, 0);
+        glDeleteBuffers(vboId);
+
+        // 删除VAO
+        glBindVertexArray(0);
+        glDeleteVertexArrays(vaoId);
+    }
+}
+
+

我们将在DummyGame类中实例化Mesh,然后将Rendererinit方法中的VAO和VBO代码删除。在Renderer类的渲染方法中将接收一个Mesh对象来渲染。cleanup方法也被简化,因为Mesh类已经提供了一个释放VAO和VBO资源的方法。

+
public void render(Mesh mesh) {
+    clear();
+
+    if ( window.isResized() ) {
+        glViewport(0, 0, window.getWidth(), window.getHeight());
+        window.setResized(false);
+    }
+
+    shaderProgram.bind();
+
+    // 绘制
+    glBindVertexArray(mesh.getVaoId());
+    glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
+
+    // 还原状态
+    glBindVertexArray(0);
+
+    shaderProgram.unbind();
+}
+
+public void cleanup() {
+    if (shaderProgram != null) {
+        shaderProgram.cleanup();
+    }
+}
+
+

值得注意的一点是:

+
glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
+
+

Mesh类通过将位置数组除以3来计算顶点的数目(因为我们使用X,Y和Z坐标)。现在,我们可以渲染更复杂的形状。来试试渲染一个正方形,一个正方形可以用两个三角形来组成,如图所示:

+

正方形坐标

+

如你所见,这两个三角形中的每一个都由三个顶点组成。第一个三角形由顶点V1、V2和V4(橙色的点)组成,第二个三角形由顶点V4,V2和V3(绿色的点)组成。顶点以逆时针顺序连接,因此要传递的浮点数数组应该是[V1, V2, V4, V4, V2, V3]。因此,DummyGameinit方法将是这样的:

+
@Override
+public void init() throws Exception {
+    renderer.init();
+    float[] positions = new float[]{
+        -0.5f,  0.5f, 0.0f,
+        -0.5f, -0.5f, 0.0f,
+         0.5f,  0.5f, 0.0f,
+         0.5f,  0.5f, 0.0f,
+        -0.5f, -0.5f, 0.0f,
+         0.5f, -0.5f, 0.0f,
+    };
+    mesh = new Mesh(positions);
+}
+
+

现在你应该可以看到这样的一个正方形:

+

正方形渲染

+

我们做完了吗?并没有,上述代码仍存在一些问题。我们使用了重复的坐标来表示正方形,传递了两次V2和V4坐标。这是个小图形,它可能不是什么大问题,但想象在一个更复杂的3D模型中,我们会多次重复传递坐标。记住,我们使用三个浮点数表示顶点的位置,但此后将需要更多的数据来表示纹理等。考虑到在更复杂的形状中,三角形直接共享的顶点数量甚至更高,如图所示(其顶点可以在六个三角形之间共享):

+

海豚

+

最后,我们需要更多的内存来储存重复的数据,这就是索引缓冲区(Index Buffer)大显身手的时候。为了绘制正方形,我们只需要以这样的方式指定每个顶点:V1, V2, V3, V4。每个顶点在数组中都有一个位置。V1在位置0上,V2在位置1上,等等:

+ + + + + + + + + + + + + + + + + +
V1V2V3V4
0123
+

然后,我们通过引用它们的位置来指定这些顶点的顺序:

+ + + + + + + + + + + + + + + + + + + + + +
013312
V1V2V4V4V2V3
+

因此,我们需要修改Mesh类来接收另一个参数,一个索引数组,现在绘制的顶点数量是该索引数组的长度。

+
public Mesh(float[] positions, int[] indices) {
+    vertexCount = indices.length;
+
+

在创建了储存位置的VBO之后,我们需要创建另一个VBO来储存索引。因此,重命名储存位置的VBO的ID的变量名,并为索引VBO(idxVboId)创建一个ID。创建VBO的过程相似,但现在的类型是GL_ELEMENT_ARRAY_BUFFER

+
idxVboId = glGenBuffers();
+indicesBuffer = MemoryUtil.memAllocInt(indices.length);
+indicesBuffer.put(indices).flip();
+glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxVboId);
+glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
+memFree(indicesBuffer);
+
+

因为我们是在处理整数,所以需要创建一个IntBuffer而不是一个FloatBuffer

+

就是这样。现在VAO包含两个VBO,一个储存位置,另一个储存索引。Mesh类的cleanUp方法也必须考虑到要释放另一个VBO。

+
public void cleanUp() {
+    glDisableVertexAttribArray(0);
+
+    // 删除 VBO
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    glDeleteBuffers(posVboId);
+    glDeleteBuffers(idxVboId);
+
+    // 删除 VAO
+    glBindVertexArray(0);
+    glDeleteVertexArrays(vaoId);
+}
+
+

最后,我们需要修改在绘制时调用的glDrawArrays方法:

+
glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
+
+

改为调用glDrawElements方法:

+
glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);
+
+

方法的参数如下: +* mode: 指定渲染的图元类型,现在是三角形,没有变化。 +* count: 指定要渲染的顶点数。 +* type: 指定索引数据的类型,现在是无符号整数型。 +* indices: 指定要开始使用索引渲染的数据偏移量。

+

现在可以使用全新和更有效的方法来绘制复杂的模型了,仅需指定索引。

+
public void init() throws Exception {
+    renderer.init();
+    float[] positions = new float[]{
+        -0.5f,  0.5f, 0.0f,
+        -0.5f, -0.5f, 0.0f,
+         0.5f, -0.5f, 0.0f,
+         0.5f,  0.5f, 0.0f,
+    };
+    int[] indices = new int[]{
+        0, 1, 3, 3, 1, 2,
+    };
+    mesh = new Mesh(positions, indices);
+}
+
+

现在为示例代码添加颜色吧。我们把另一组浮点数传递给Mesh类,它储存了正方形中每个顶点的颜色。

+
public Mesh(float[] positions, float[] colours, int[] indices) {
+
+

为了使用该数组,我们需要创建另一个VBO,它将与我们的VAO相关联。

+
// 颜色 VBO
+colourVboId = glGenBuffers();
+FloatBuffer colourBuffer = memAllocFloat(colours.length);
+colourBuffer.put(colours).flip();
+glBindBuffer(GL_ARRAY_BUFFER, colourVboId);
+glBufferData(GL_ARRAY_BUFFER, colourBuffer, GL_STATIC_DRAW);
+glEnableVertexAttribArray(1);
+glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);
+
+

请注意glVertexAttribPointer方法的调用,第一个参数现在是“1”,这是着色器期望数据的位置。(当然,由于增加了一个VBO,我们需要在cleanUp方法中释放它)。可以看到,我们需要在渲染期间启用位置1处的VAO属性。

+

接下来是修改着色器。顶点着色器现在需要两个参数,坐标(位置0)和颜色(位置1)。顶点着色器将只输出接收到的颜色,以便片元着色器可以对其进行处理。

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec3 inColour;
+
+out vec3 exColour;
+
+void main()
+{
+    gl_Position = vec4(position, 1.0);
+    exColour = inColour;
+}
+
+

现在,片元着色器接收由顶点着色器处理的颜色,并使用它来生成颜色。

+
#version 330
+
+in  vec3 exColour;
+out vec4 fragColor;
+
+void main()
+{
+    fragColor = vec4(exColour, 1.0);
+}
+
+

最后要做的是修改渲染代码以使用第二个数据数组:

+

我们现在可以将如下所示的颜色数组传递给Mesh类,为正方形添加一些颜色。

+
float[] colours = new float[]{
+    0.5f, 0.0f, 0.0f,
+    0.0f, 0.5f, 0.0f,
+    0.0f, 0.0f, 0.5f,
+    0.0f, 0.5f, 0.5f,
+};
+
+

然后会得到一个色彩鲜艳的正方形。

+

色彩鲜艳的正方形

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/06-transformations/index.html b/06-transformations/index.html new file mode 100755 index 0000000..2c3dc1b --- /dev/null +++ b/06-transformations/index.html @@ -0,0 +1,631 @@ + + + + + + + + + + + + 变换 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

变换(Transformations)

+

投影

+

让我们回看在前一章中创建的色彩鲜艳的正方形。如果仔细看,它更像一个矩形,你甚至可以将窗口的宽度从600像素改为900像素,失真就会更加明显。这发生了什么呢?

+

如果你查看顶点着色器的代码,我们只是直接地传递坐标。换句话说,当一个顶点的X坐标为0.5时,我们让OpenGL在屏幕的X坐标为0.5的位置绘制它。下图展示了OpenGL坐标系(仅含X和Y轴)。

+

坐标

+

将这些坐标投影到窗口坐标系(其原点位于上图的左上角),需要考虑到窗口的大小。因此,如果我们的窗口大小为900x480,OpenGL坐标(1, 0)将被投影到窗口坐标(900, 0),最终创建一个矩形而不是一个正方形。

+

矩形

+

但是,问题远比这更严重。将四边形的Z坐标从0.0修改为1.0和-1.0,你发现了什么?四边形完全是绘制在同一个地方,不管它是否沿着Z轴位移。为什么会发生这种情况?远处的物体应该比近处的物体绘制得更小,但是我们使用相同的X和Y坐标绘制它们。

+

但稍等一下,这不应该由Z坐标来处理吗?这半对半错。Z坐标告诉OpenGL一个物体的远近,但是OpenGL对你的物体的大小一无所知。你可以有两个不同大小的物体,一个更近更小,一个更远更大,而且可以以相同的大小正确地投影到屏幕上(有相同的屏幕X和Y坐标,但Z坐标不同)。OpenGL只使用正在传递的坐标,所以我们必须处理这个问题,我们需要正确地投影坐标。

+

既然已经确诊了这个问题,该怎么解决呢?答案是使用投影矩阵(Projection Matrix)或截锥体(Frustum)。投影矩阵将处理绘制区域的宽高比(大小与高度之间的关系),这样物体就不会变形。它还可以处理距离,所以较远的物体将会被绘制得更小。投影矩阵还将考虑我们的视野和应该显示的距离有多远。

+

对于不熟悉矩阵的人,矩阵(Matrix)是以行和列排列的二维数组。矩阵中的每个数字被称为元素。矩阵阶次是行和列的数量。例如,此处是一个2x2矩阵(有2行2列)。

+

2x2矩阵

+

矩阵有许多可以应用于它们的基本运算(如加法、乘法等),你可以在数学书中查阅,其中与三维图形相关的矩阵对空间中点的变换非常有用。

+

你可以把投影矩阵想象成一个摄像机,它有一个视野和最小和最大距离。该摄像机的可视区域是一个截断的金字塔,下图为该区域的俯视图。

+

投影矩阵概念

+

投影矩阵将正确地投影三维坐标,以便它们能够在二维屏幕上正确地显示。该矩阵的数学表示如下(不要害怕):

+

投影矩阵

+

其中屏幕宽高比(Aspect Ratio)指的是屏幕宽度与屏幕高度的关系()。为了获得给定点的投影坐标,只需要将投影矩阵乘以原始坐标,结果是投影后的另一个向量。

+

因此,我们需要处理一组数学实体,如向量、矩阵,并包括可以对它们进行的运算。我们可以选择从头开始编写所有的代码,或者使用已实现的库。当然我们会选择容易的方法,使用JOML(Java OpenGL Math Library,Java OpenGL 数学库)处理LWJGL内的数学运算。为了使用该库,我们只需要在pom.xml文件添加另一个依赖项。

+
        <dependency>
+            <groupId>org.joml</groupId>
+            <artifactId>joml</artifactId>
+            <version>${joml.version}</version>
+        </dependency>
+
+

然后设置要使用的库版本。

+
    <properties>
+        [...]
+        <joml.version>1.9.6</joml.version>
+        [...]
+    </properties>
+
+

现在一切都准备好了,来定义我们的投影矩阵。在Renderer类中创建Matrix4f类(由JOML库提供)的实例。Matrix4f类提供了一个perspective方法来创建投影矩阵,该方法需要以下参数:

+
    +
  • 视野:可视区域角的弧度大小,我们将定义一个储存该值的常数。
  • +
  • 屏幕宽高比。
  • +
  • 最近视距(z-near)。
  • +
  • 最远视距(z-far)。
  • +
+

我们将在init方法中实例化该矩阵,因此需要传递对Window实例的阴影以获取窗口大小(你可以查看源代码)。代码如下:

+
    /**
+     * 视野弧度
+     */
+    private static final float FOV = (float) Math.toRadians(60.0f);
+
+    private static final float Z_NEAR = 0.01f;
+
+    private static final float Z_FAR = 1000.f;
+
+    private Matrix4f projectionMatrix;
+
+

投影矩阵的创建如下所示:

+
float aspectRatio = (float) window.getWidth() / window.getHeight();
+projectionMatrix = new Matrix4f().perspective(FOV, aspectRatio,
+    Z_NEAR, Z_FAR);
+
+

现在我们省略宽高比可变的情况(通过调整窗口大小),这可以在render方法中检查并相应地改变投影矩阵。

+

现在有了矩阵,该如何使用它呢?我们需要在着色器中使用它,并且它应该被应用到所有顶点上。首先,你可能会想到把它捆绑在顶点输入中(就像坐标和颜色那样)。但这样,我们会浪费很多空间,因为投影矩阵在几次渲染期间都不会发生改变。你可能还想在Java代码中用矩阵处理所有顶点,但这样我们输入的VBO就是没用的了,这样就不能使用显卡中的处理器资源了。

+

答案是使用“uniform”。Uniform是着色器可以使用的全局的GLSL变量,我们将使用它与着色器交流。

+

所以我们需要修改顶点着色器的代码,并声明一个新的名为projectionMatrix的Uniform,并用它来计算投影后的位置。

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec3 inColour;
+
+out vec3 exColour;
+
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * vec4(position, 1.0);
+    exColour = inColour;
+}
+
+

如上所述,我们把projectionMatrix定义为一个4x4的矩阵,新的坐标是通过把它与原始坐标相乘得到的。现在我们需要把投影矩阵的值传递给着色器,首先需要确定Uniform的位置。

+

这是通过调用方法glGetUniformLocation实现的,它有两个参数:

+
    +
  • 着色器程序的ID
  • +
  • Uniform名(它应该与着色器里定义的名称相同)
  • +
+

此方法返回储存Uniform位置的ID。由于可能有一个以上的Uniform,我们将把这些ID储存在由变量名作为索引的Map中(此后我们需要那个ID)。因此,在ShaderProgram需要创建一个新的字段来保存这些ID:

+
private final Map<String, Integer> uniforms;
+
+

然后由构造方法初始化它:

+
uniforms = new HashMap<>();
+
+

最后,我们创建了一个方法来创建新的Uniform和储存获得的位置。

+
public void createUniform(String uniformName) throws Exception {
+    int uniformLocation = glGetUniformLocation(programId,
+        uniformName);
+    if (uniformLocation < 0) {
+        throw new Exception("Could not find uniform:" +
+            uniformName);
+    }
+    uniforms.put(uniformName, uniformLocation);
+}
+
+

现在,在着色器程序编译后,我们就可以在Renderer类中调用createUniform方法(本例中,我们将在投影矩阵实例化后调用它)。

+
shaderProgram.createUniform("projectionMatrix");
+
+

此时,我们已经准备好一个可以储存投影矩阵数据的储存器。由于投影矩阵在渲染期间不会变化,所以可以在创建Uniform后直接设置值,但我们将在render方法中做此事。稍后你可以看到,我们可以重用该Uniform来执行每次渲染调用中需要执行的其他操作。

+

我们将在ShaderProgram类中创建另一个名为setUniform的方法来设置数据,通过使用JOML库提供的实用方法将矩阵转换为4x4的FloatBuffer对象,并将它们发送到Uniform中。

+
public void setUniform(String uniformName, Matrix4f value) {
+    // 转储矩阵到FloatBuffer
+    try (MemoryStack stack = MemoryStack.stackPush()) {
+        FloatBuffer fb = stack.mallocFloat(16);
+        value.get(fb);
+        glUniformMatrix4fv(uniforms.get(uniformName), false, fb);
+    }
+}
+
+

如你所见,我们用与此前不同的方式创建缓冲区。我们使用的是自动管理的缓冲区,并将它们分配到堆栈上。这是因为这个缓冲区的大小很小,并且在该方法之外不会使用它。因此我们使用MemoryStack类。

+

现在,在着色器绑定之后,可以在Renderer类的render方法中调用该方法:

+
shaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+

我们就要完成了,现在可以正确地渲染四边形,所以现在可以启动程序,然后得到一个...黑色背景,没有任何彩色四边形。发生了什么?我们把什么弄坏了吗?实际上没有任何问题。记住我们正在模拟摄像机观察场景的效果。我们提供了两个距离,一个是最远视距(1000f)和一个最近视距(0.01f)。而我们的坐标是:

+
float[] positions = new float[]{
+    -0.5f,  0.5f, 0.0f,
+    -0.5f, -0.5f, 0.0f,
+     0.5f, -0.5f, 0.0f,
+     0.5f,  0.5f, 0.0f,
+};
+
+

也就是说,我们坐标中的Z坐标位于可视区域之外。将它们赋值为-0.05f,现在你会看到像这样的一个巨大的绿色正方形:

+

正方形1

+

这是因为,我们正绘制出离摄像机太近的正方形,实际上是在放大它。如果现在把一个-1.05f的值赋值给Z坐标,就可以看到彩色正方形了。

+

彩色正方形

+

如果继续向后移动四边形,我们会看到它变小了。还要注意到四边形不再像矩形了。

+

使用变换

+

回想一下到目前为止我们都做了什么。我们已经学会了如何将数据以有效的格式传递给显卡,以及如何使用顶点和片元着色器来投影这些顶点并设置它们的颜色。现在应该开始在三维空间中绘制更复杂的模型了,但为了做到它,我们必须能够加载模型,并在指定的位置以适当的大小和所需的旋转将它渲染在三维空间中。

+

现在为了实现这样的渲染,我们需要提供一些基本操作来操作模型:

+
    +
  • 位移(Translation): 在三个轴中的任意一个轴上移动一个物体。
  • +
  • 旋转(Rotation): 按任意一个轴旋转物体任意角度。
  • +
  • 缩放(Scale): 调整物体的大小。
  • +
+

变换

+

上面的操作统称为变换(Transformation)。你可能猜到要实现这一点的方法是把坐标乘以一组矩阵(一个用于移动,一个用于旋转,一个用于缩放)。这三个矩阵将被组合成一个称为“世界矩阵”的矩阵,并作为一个Uniform传递给顶点着色器。

+

之所以被称为世界矩阵,是因为我们正在将模型坐标转换为世界坐标。当学习加载3D模型时,你会发现这些模型是在它们自己的坐标系中定义的,它们不知道你的三维空间的大小,但它们需要在里面渲染。因此,当我们用矩阵乘以坐标时,实际上做的是从一个坐标系(模型坐标系)转换到另一个坐标系(三维世界坐标系)。

+

世界矩阵应该这样计算(顺序很重要,因为乘法交换律不适用于矩阵):

+

+ +

+

如果把投影矩阵包含在变换矩阵中,它会是这样的:

+

+ +

+

位移矩阵是这样定义的:

+

+ +

+

位移矩阵的参数如下:

+
    +
  • dx: 沿X轴位移。
  • +
  • dy: 沿Y轴位移。
  • +
  • dz: 沿Z轴位移。
  • +
+

缩放矩阵是这样定义的;

+

+ +

+

缩放矩阵的参数如下:

+
    +
  • sx: 沿着X轴缩放。
  • +
  • sy: 沿着Y轴缩放。
  • +
  • sz: 沿着Z轴缩放。
  • +
+

旋转矩阵要复杂得多,但请记住,它可以由每个绕单独的轴旋转的旋转矩阵相乘得到。

+

现在,为了实践这些理论,我们需要重构代码一点点。在游戏中,我们将加载一组模型,用来根据游戏逻辑在不同的位置渲染许多物体(想象一个FPS游戏,它载入了三个不同敌人的模型。确实只有三个模型,但使用这些模型,我们可以渲染想要的任意数量的敌人)。我们需要为每个对象创建一个VAO和一组VBO吗?答案是不需要,只需要每个模型加载一次就行。我们需要做的是根据它的位置,大小和旋转来独立地绘制它。当渲染这些模型时,我们需要对它们进行变换。

+

因此,我们将创建一个名为GameItem的新类,该类将模型加载到Mesh实例中。一个GameItem实例将由变量储存它的位置、旋转状态和缩放。如下是该类的定义。

+
package org.lwjglb.engine;
+
+import org.joml.Vector3f;
+import org.lwjglb.engine.graph.Mesh;
+
+public class GameItem {
+
+    private final Mesh mesh;
+
+    private final Vector3f position;
+
+    private float scale;
+
+    private final Vector3f rotation;
+
+    public GameItem(Mesh mesh) {
+        this.mesh = mesh;
+        position = new Vector3f(0, 0, 0);
+        scale = 1;
+        rotation = new Vector3f(0, 0, 0);
+    }
+
+    public Vector3f getPosition() {
+        return position;
+    }
+
+    public void setPosition(float x, float y, float z) {
+        this.position.x = x;
+        this.position.y = y;
+        this.position.z = z;
+    }
+
+    public float getScale() {
+        return scale;
+    }
+
+    public void setScale(float scale) {
+        this.scale = scale;
+    }
+
+    public Vector3f getRotation() {
+        return rotation;
+    }
+
+    public void setRotation(float x, float y, float z) {
+        this.rotation.x = x;
+        this.rotation.y = y;
+        this.rotation.z = z;
+    }
+
+    public Mesh getMesh() {
+        return mesh;
+    }
+}
+
+

我们将创建一个名为Transformation的类,让它来处理变换。

+
package org.lwjglb.engine.graph;
+
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+
+public class Transformation {
+
+    private final Matrix4f projectionMatrix;
+
+    private final Matrix4f worldMatrix;
+
+    public Transformation() {
+        worldMatrix = new Matrix4f();
+        projectionMatrix = new Matrix4f();
+    }
+
+    public final Matrix4f getProjectionMatrix(float fov, float width, float height, float zNear, float zFar) {
+        float aspectRatio = width / height;        
+        projectionMatrix.identity();
+        projectionMatrix.perspective(fov, aspectRatio, zNear, zFar);
+        return projectionMatrix;
+    }
+
+    public Matrix4f getWorldMatrix(Vector3f offset, Vector3f rotation, float scale) {
+        worldMatrix.identity().translate(offset).
+                rotateX((float)Math.toRadians(rotation.x)).
+                rotateY((float)Math.toRadians(rotation.y)).
+                rotateZ((float)Math.toRadians(rotation.z)).
+                scale(scale);
+        return worldMatrix;
+    }
+}
+
+

如你所见,这个类把投影矩阵和世界矩阵组合起来。给定一组参数来进行位移、旋转和缩放,然后返回世界矩阵。getWorldMatrix返回的结果将为每个GameItem实例变换坐标。该类还提供了获得投影矩阵的方法。

+

需要注意的一件事是,Matrix4f类的mul方法修改了该实例的内容。因此,如果直接将投影矩阵与变换矩阵相乘,我们会修改投影矩阵本身,这就是为什么总是在每次调用时将每个矩阵初始化为单位矩阵。

+

Renderer类的构造方法中,我们仅实例化了没有任何参数的Transformation类,而在init方法中,我们只创建了Uniform。

+
public Renderer() {
+    transformation = new Transformation();
+}
+
+public void init(Window window) throws Exception {
+    // ... 此前的一些代码 ...
+    // 为世界矩阵和投影矩阵创建Uniform
+    shaderProgram.createUniform("projectionMatrix");
+    shaderProgram.createUniform("worldMatrix");
+
+    window.setClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+}
+
+

Renderer类的渲染方法中,现在可以接收到一个GameItem的数组:

+
public void render(Window window, GameItem[] gameItems) {
+    clear();
+
+        if ( window.isResized() ) {
+            glViewport(0, 0, window.getWidth(), window.getHeight());
+            window.setResized(false);
+        }
+
+    shaderProgram.bind();
+
+    // 更新投影矩阵
+    Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOV, window.getWidth(), window.getHeight(), Z_NEAR, Z_FAR);
+    shaderProgram.setUniform("projectionMatrix", projectionMatrix);        
+
+    // 渲染每一个游戏项
+    for(GameItem gameItem : gameItems) {
+        // 为该项设置世界矩阵
+        Matrix4f worldMatrix =
+            transformation.getWorldMatrix(
+                gameItem.getPosition(),
+                gameItem.getRotation(),
+                gameItem.getScale());
+        shaderProgram.setUniform("worldMatrix", worldMatrix);
+        // 为该游戏项渲染网格
+        gameItem.getMesh().render();
+    }
+
+    shaderProgram.unbind();
+}
+
+

每次调用render时就更新投影矩阵一次,这样我们可以处理窗口大小的调整操作。然后我们遍历GameItem数组,并根据它们各自的位置、旋转和缩放创建变换矩阵,该矩阵将被传递到着色器并绘制Mesh。投影矩阵对于所有要渲染的项目都是相同的,这就是为什么它在Transformation类中是单独一个变量的原因。

+

我们将渲染代码移动到Mesh类中:

+
public void render() {
+    // 绘制Mesh
+    glBindVertexArray(getVaoId());
+
+    glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
+
+    // 重置状态
+    glBindVertexArray(0);
+}
+
+

顶点着色器只需简单地添加一个新的worldMatrix矩阵,然后用它与projectionMatrix一同计算坐标:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec3 inColour;
+
+out vec3 exColour;
+
+uniform mat4 worldMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * worldMatrix * vec4(position, 1.0);
+    exColour = inColour;
+}
+
+

如你所见,代码完全一致。我们使用Uniform来正确地投影坐标,并且考虑截锥、位置、缩放和旋转等。

+

另外一个重要的问题是,为什么不直接使用位移、旋转和缩放矩阵,而是把它们组合成一个世界矩阵呢?原因是我们应该尽量减少在着色器中使用的矩阵。还要记住,在着色器中所做的矩阵乘法是每个顶点一次,投影矩阵在渲染调用期间不会改变,而每一个GameItem实例的世界矩阵也不会改变。如果独立位移、旋转和缩放矩阵,我们要做更多的矩阵乘法运算。在一个有超多顶点的模型中,这是很多余的操作。

+

但你现在可能会想,如果每个GameItem中的世界矩阵都不会发生变化,为什么不在Java类中做矩阵乘法?我们将投影矩阵和世界矩阵与每个GameItem相乘,把它们作为一个Uniform,在此情况下,我们确实能省下更多的操作。但当我们向游戏引擎中添加更多的特性时,我们需要在着色器中使用世界坐标,所以最好独立地处理这两个矩阵。

+

最后只需要修改DummyGame类,创建一个GameItem实例,让其与Mesh关联,并添加一些逻辑来位移、旋转和缩放四边形。因为这只是个测试示例,没有添加太多内容,所以你可以在本书的源代码中找到相关代码。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/07-textures/index.html b/07-textures/index.html new file mode 100755 index 0000000..89eee70 --- /dev/null +++ b/07-textures/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + 纹理 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

纹理(Textures)

+

创建一个三维立方体

+

在本章中,我们将学习如何在渲染中加载纹理并使用它们。为了讲解与纹理相关的所有概念,我们将把此前章节中使用的正方形更改为三维立方体。为了绘制一个立方体,我们只需要正确地定义一个立方体的坐标,就能使用现有代码正确地绘制它。

+

为了绘制立方体,我们只需要定义八个顶点。

+

立方体坐标

+

因此,它的坐标数组将是这样的:

+
float[] positions = new float[] {
+    // VO
+    -0.5f,  0.5f,  0.5f,
+    // V1
+    -0.5f, -0.5f,  0.5f,
+    // V2
+    0.5f, -0.5f,  0.5f,
+    // V3
+     0.5f,  0.5f,  0.5f,
+    // V4
+    -0.5f,  0.5f, -0.5f,
+    // V5
+     0.5f,  0.5f, -0.5f,
+    // V6
+    -0.5f, -0.5f, -0.5f,
+    // V7
+     0.5f, -0.5f, -0.5f,
+};
+
+

当然,由于我们多了4个顶点,我们需要更改颜色数组,目前仅重复前四项的值。

+
float[] colours = new float[]{
+    0.5f, 0.0f, 0.0f,
+    0.0f, 0.5f, 0.0f,
+    0.0f, 0.0f, 0.5f,
+    0.0f, 0.5f, 0.5f,
+    0.5f, 0.0f, 0.0f,
+    0.0f, 0.5f, 0.0f,
+    0.0f, 0.0f, 0.5f,
+    0.0f, 0.5f, 0.5f,
+};
+
+

最后,由于立方体是由六个面构成的,需要绘制十二个三角形(每个面两个),因此我们需要修改索引数组。记住三角形必须按逆时针顺序定义,如果你直接去定义三角形,很容易犯错。一定要将你想定义的面摆在你的面前,确认顶点并以逆时针顺序绘制三角形。

+
int[] indices = new int[] {
+    // 前面
+    0, 1, 3, 3, 1, 2,
+    // 上面
+    4, 0, 3, 5, 4, 3,
+    // 右面
+    3, 2, 7, 5, 3, 7,
+    // 左面
+    6, 1, 0, 6, 0, 4,
+    // 下面
+    2, 1, 6, 2, 6, 7,
+    // 后面
+    7, 6, 4, 7, 4, 5,
+};
+
+

为了更好观察立方体,我们将修改DummyGame类中旋转模型的代码,使模型沿着三个轴旋转。

+
// 更新旋转角
+float rotation = gameItem.getRotation().x + 1.5f;
+if ( rotation > 360 ) {
+    rotation = 0;
+}
+gameItem.setRotation(rotation, rotation, rotation);
+
+

这就完了,现在能够显示一个旋转的三维立方体了,你可以编译和运行示例代码,会得到如下所示的东西。

+

没有开启深度测试的立方体

+

这个立方体有些奇怪,有些面没被正确地绘制,这发生了什么?立方体之所以出现这个现象,是因为组成立方体的三角形是以一种随机顺序绘制的。事实上距离较远的像素应该在距离较近的像素之前绘制,而不是现在这样。为了修复它,我们必须启用深度测试(Depth Test)。

+

这将在Window类的init方法中去做:

+
glEnable(GL_DEPTH_TEST);
+
+

现在立方体被正确地渲染了!

+

开启深度测试的立方体

+

如果你看了本章该小节的代码,你可能会看到Mesh类做了一下小规模的调整,VBO的ID现在被储存在一个List中,以便于迭代它们。

+

为立方体添加纹理

+

现在我们将把纹理应用到立方体上。纹理(Texture)是用来绘制某个模型的像素颜色的图像,可以认为纹理是包在三维模型上的皮肤。你要做的是将纹理图像中的点分配给模型中的顶点,这样做OpenGL就能根据纹理图像计算其他像素的颜色。

+

纹理映射

+

纹理图像不必与模型同样大小,它可以变大或变小。如果要处理的像素不能映射到纹理中的指定点,OpenGL将推断颜色。可在创建纹理时控制如何进行颜色推断。

+

因此,为了将纹理应用到模型上,我们必须做的是将纹理坐标分配给每个顶点。纹理坐标系有些不同于模型坐标系。首先,我们的纹理是二维纹理,所以坐标只有X和Y两个量。此外,原点是图像的左上角,X或Y的最大值都是1。

+

纹理坐标系

+

我们如何将纹理坐标与位置坐标联系起来呢?答案很简单,就像传递颜色信息,我们创建了一个VBO,为每个顶点储存其纹理坐标。

+

让我们开始修改代码,以便在三维立方体上使用纹理吧。首先是加载将被用作纹理的图像。对此在LWJGL的早期版本中,通常使用Slick2D库。在撰写本文时,该库似乎与LWJGL 3不兼容,因此我们需要使用另一种方法。我们将使用LWJGL为stb库提供的封装。为了使用它,首先需要在本地的pom.xml文件中声明依赖。

+
<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-stb</artifactId>
+    <version>${lwjgl.version}</version>
+</dependency>
+[...]
+<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-stb</artifactId>
+    <version>${lwjgl.version}</version>
+    <classifier>${native.target}</classifier>
+    <scope>runtime</scope>
+</dependency>
+
+

在一些教程中,你可能看到首先要做的事是调用glEnable(GL_TEXTURE_2D)来启用OpenGL环境中的纹理。如果使用固定管线这是对的,但我们使用GLSL着色器,因此不再需要了。

+

现在我们将创建一个新的Texture类,它将执行加载纹理所必须的步骤。首先,我们需要将图像载入到ByteBuffer中,代码如下:

+
private static int loadTexture(String fileName) throws Exception {
+    int width;
+    int height;
+    ByteBuffer buf;
+    // 加载纹理文件
+    try (MemoryStack stack = MemoryStack.stackPush()) {
+        IntBuffer w = stack.mallocInt(1);
+        IntBuffer h = stack.mallocInt(1);
+        IntBuffer channels = stack.mallocInt(1);
+
+        buf = stbi_load(fileName, w, h, channels, 4);
+        if (buf == null) {
+            throw new Exception("Image file [" + fileName  + "] not loaded: " + stbi_failure_reason());
+        }
+
+        /* 获得图像的高度与宽度 */
+        width = w.get();
+        height = h.get();
+     }
+     [... 接下来还有更多代码 ...]
+
+

首先我们要为库分配IntBuffer,以返回图像大小与通道数。然后,我们调用stbi_load方法将图像加载到ByteBuffer中,该方法需要如下参数:

+
    +
  • filePath:文件的绝对路径。stb库是本地的,不知道关于CLASSPATH的任何内容。因此,我们将使用常规的文件系统路径。
  • +
  • width:图像宽度,获取的图像宽度将被写入其中。
  • +
  • height:图像高度,获取的图像高度将被写入其中。
  • +
  • channels:图像通道。
  • +
  • desired_channels:所需的图像通道,我们传入4(RGBA)。
  • +
+

一件关于OpenGL的重要事项,由于历史原因,要求纹理图像的大小(每个轴的像素数)必须是二的指数(2, 4, 8, 16, ....)。一些驱动解除了这种限制,但最好还是保持以免出现问题。

+

下一步是将纹理上传到显存中。首先需要创建一个新的纹理ID,与该纹理相关的操作都要使用该ID,因此我们需要绑定它。

+
// 创建一个新的OpenGL纹理
+int textureId = glGenTextures();
+// 绑定纹理
+glBindTexture(GL_TEXTURE_2D, textureId);
+
+

然后需要告诉OpenGL如何解包RGBA字节,由于每个分量只有一个字节大小,所以我们需要添加以下代码:

+
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+

最后我们可以上传纹理数据:

+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height,
+    0, GL_RGBA, GL_UNSIGNED_BYTE, buf);
+
+

glTexImage2D的参数如下所示:

+
    +
  • target: 指定目标纹理(纹理类型),本例中是GL_TEXTURE_2D
  • +
  • level: 指定纹理细节的等级。0级是基本图像等级,第n级是第n个多级渐远纹理的图像,之后再谈论这个问题。
  • +
  • internal format: 指定纹理中颜色分量的数量。
  • +
  • width: 指定纹理图像的宽度。
  • +
  • height: 指定纹理图像的高度。
  • +
  • border: 此值必须为0。
  • +
  • format: 指定像素数据的格式,现在为RGBA。
  • +
  • type: 指定像素数据的类型。现在,我们使用的是无符号字节。
  • +
  • data: 储存数据的缓冲区。
  • +
+

在一些代码中,你可能会发现在调用glTexImage2D方法前设置了一些过滤参数。过滤是指在缩放时如何绘制图像,以及如何插值像素。如果未设置这些参数,纹理将不会显示。因此,在glTexImage2D方法调用之前,会看到以下代码:

+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+

这些参数基本上在说,当绘制一个像素时,如果没有直接一对一地关联到纹理坐标,它将选择最近的纹理坐标点。

+

到目前为止,我们不会设置这些参数。相反,我们将生成一个多级渐远纹理(Mipmap)。多级渐远纹理是由高细节纹理生成的逐级降低分辨率的纹理集合。当我们的物体缩放时,就将自动使用低分辨率的图像。

+

为了生成多级渐远纹理,只需要编写以下代码(目前我们把它放在glTextImage2D方法调用之后):

+
glGenerateMipmap(GL_TEXTURE_2D);
+
+

最后,我们可以释放原始图像数据本身的内存:

+
stbi_image_free(buf);
+
+

就这样,我们已经成功地加载了纹理,现在需要使用它。正如此前所说,我们需要把纹理坐标作为另一个VBO。因此,我们要修改Mesh类以接收储存纹理坐标的浮点数组,而不是颜色(我们可以同时有颜色和纹理,但为了简化它,我们将删除颜色),构造函数现在如下所示:

+
public Mesh(float[] positions, float[] textCoords, int[] indices,
+    Texture texture)
+
+

纹理坐标VBO与颜色VBO创建的方式相同。唯一的区别是它每个顶点属性只有两个分量而不是三个:

+
vboId = glGenBuffers();
+vboIdList.add(vboId);
+textCoordsBuffer = MemoryUtil.memAllocFloat(textCoords.length);
+textCoordsBuffer.put(textCoords).flip();
+glBindBuffer(GL_ARRAY_BUFFER, vboId);
+glBufferData(GL_ARRAY_BUFFER, textCoordsBuffer, GL_STATIC_DRAW);
+glEnableVertexAttribArray(1);
+glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);
+
+

现在我们需要在着色器中使用纹理。在顶点着色器中,我们修改了第二个输入参数,因为现在它是一个vec2(也顺便更改了名称)。顶点着色器就像此前一样,仅将纹理坐标传给片元着色器。

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+
+out vec2 outTexCoord;
+
+uniform mat4 worldMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * worldMatrix * vec4(position, 1.0);
+    outTexCoord = texCoord;
+}
+
+

在片元着色器中,我们使用那些纹理坐标来设置像素颜色:

+
#version 330
+
+in  vec2 outTexCoord;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+
+void main()
+{
+    fragColor = texture(texture_sampler, outTexCoord);
+}
+
+

在分析代码之前,我们先理清一些概念。显卡有几个空间或槽来储存纹理,每一个空间被称为纹理单元(Texture Unit)。当使用纹理时,我们必须设置想用的纹理。如你所见,我们有一个名为texture_sampler的新Uniform,该Uniform的类型是sampler2D,并储存有我们希望使用的纹理单元的值。

+

main函数中,我们使用名为texture的纹理采样函数,该函数有两个参数:取样器(Sampler)和纹理坐标,并返回正确的颜色。取样器Uniform允许使用多重纹理(Multi-texture),不过现在不是讨论这个话题的时候,但是我们会在稍后再尝试添加。

+

因此,在ShaderProgram类中,我们将创建一个新的方法,允许为整数型Uniform设置值:

+
public void setUniform(String uniformName, int value) {
+    glUniform1i(uniforms.get(uniformName), value);
+}
+
+

Renderer类的init方法中,我们将创建一个新的Uniform:

+
shaderProgram.createUniform("texture_sampler");
+
+

此外,在Renderer类的render方法中,我们将Uniform的值设置为0(我们现在不使用多个纹理,所以只使用单元0)。

+
shaderProgram.setUniform("texture_sampler", 0);
+
+

最好,我们只需修改Mesh类的render方法就可以使用纹理。在方法起始处,添加以下几行代码:

+
// 激活第一个纹理单元
+glActiveTexture(GL_TEXTURE0);
+// 绑定纹理
+glBindTexture(GL_TEXTURE_2D, texture.getId());
+
+

我们已经将texture.getId()所获得的纹理ID绑定到纹理单元0上。

+

我们刚刚修改了代码来支持纹理,现在需要为三维立方体设置纹理坐标,纹理图像文件是这样的:

+

立方体纹理

+

在我们的三维模型中,共有八个顶点。我们首先定义正面每个顶点的纹理坐标。

+

立方体纹理的正面

+ + + + + + + + + + + + + + + + + + + + + + + + + +
顶点纹理坐标
V0 + +
V1 + +
V2 + +
V3 + +
+

然后,定义顶面的纹理映射。

+

正方体纹理的顶面

+ + + + + + + + + + + + + + + + + + + + + + + + + +
顶点纹理坐标
V4 + +
V5 + +
V0 + +
V3 + +
+

如你所见,有一个问题,我们需要为同一个顶点(V0和V3)设置不同的纹理坐标。怎么样才能解决这个问题呢?解决这一问题的唯一方法是重复一些顶点并关联不同的纹理坐标。对于顶面,我们需要重复四个顶点并为它们分配正确的纹理坐标。

+

因为前面、后面和侧面都使用相同的纹理,所以我们不需要重复这些顶点。在源码中有完整的定义,但是我们需要从8个点上升到20个点了。最终的结果就像这样。

+

有纹理的立方体

+

在接下来的章节中,我们将学习如何加载由3D建模工具生成的模型,这样我们就不需要手动定义顶点和纹理坐标了(顺便一提,对于更复杂的模型,手动定义是不存在的)。

+

透明纹理简介

+

如你所见,当加载图像时,我们检索了四个RGBA组件,包括透明度等级。但如果加载一个透明的纹理,可能看不到任何东西。为了支持透明度,我们需要通过以下代码启用混合(Blend):

+
glEnable(GL_BLEND);
+
+

但仅启用混合,透明效果仍然不会显示,我们还需要指示OpenGL如何进行混合。这是通过调用glBlendFunc方法完成的:

+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+

你可以查看此处有关可使用的不同功能的详细说明。

+

即使启用了混合并设置了功能,也可能看不到正确的透明效果。其原因是深度测试,当使用深度值丢弃片元时,我们可能将具有透明度的片元与背景混合,而不是与它们后面的片元混合,这将得到错误的渲染结果。为了解决该问题,我们需要先绘制不透明物体,然后按深度递减顺序绘制具有透明度的物体(应先绘制较远物体)。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/08-camera/index.html b/08-camera/index.html new file mode 100755 index 0000000..ae193ce --- /dev/null +++ b/08-camera/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + 摄像机 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

摄像机(Camera)

+

本章中我们将学习如何在渲染的三维场景中移动。该功能就像是有一台可以在三维世界中运动的摄像机,然而事实上摄像机就是描述该功能的术语。

+

但如果你尝试在OpenGL寻找摄像机功能,你会发现没有摄像机这个概念,换句话说,摄像机一直是固定在屏幕中心的以(0, 0, 0)为中心点的位置。

+

因此,我们需要模拟出一个可以在三维空间中移动的摄像。但要怎么做呢?摄像机是不能移动的,因此我们必须要移动世界中的全部物体。换句话说,如果移动不了摄像机就移动整个世界。

+

假设摄像机从(Cx, Cy, Cz)沿着Z轴移动到(Cx, Cy, Cz+dz),从而靠近在(Ox, Oy, Oz)放置的物体。

+

摄像机运动

+

我们实际上做的是将物体(在三维空间中的所有物体)按摄像机应移动的相反方向移动。其实物体就像是放在跑步机上一样向后退。

+

实际的运动

+

摄像机可以沿着三个轴(X、Y和Z)移动,也可以绕着它们旋转(翻滚(Roll)、俯仰(Pitch)和偏航(Yaw))。

+

翻滚、俯仰和偏航

+

基本上要做的就是让移动和旋转应用于三维世界全部物体。那要怎么做呢?答案是应用另一种变换,该变换将所有物体的所有顶点按摄像机移动的相反方向平移,再根据摄像机的旋转来旋转它们。当然,这将用到另一个矩阵,即所谓的观察矩阵(View Matrix)来完成。这个矩阵首先进行平移,然后沿着轴线进行旋转。

+

来看看如何构造这个矩阵。如果你想起变换一章(第6章),其中的变换方程如下所示:

+

+ +

+

观察矩阵应在应用投影矩阵之前应用,因此我们的方程应如下所示:

+

+ +

+

现在有三个矩阵了,我们应稍微思考一下这些矩阵的生命周期。在游戏运行时,投影矩阵应该不会有太多的变化,最坏的情况下,每次渲染可能改变一次。如果摄像机移动,则观察矩阵在每次渲染时可能改变一次。每渲染一个GameItem实例世界矩阵都会改变一次,一次每次渲染调用都会改变多次。

+

因此我们应该将多少矩阵传递到顶点着色器呢?你可能会看到一些代码,为三个矩阵分别定义一个Uniform,但理论上最有效的方法是将投影矩阵和观察矩阵组合,将其称为pv矩阵,并传递worldpv矩阵到着色器。这样,我们可以使用世界坐标,并可以避免一些额外的运算。

+

但实际上,最方便的方法是将观察矩阵与世界矩阵组合。为什么这样?因为要记住整个摄像机概念就是个骗局,我们所做的是移动整个世界来模拟摄像机的位移,并仅显示一小部分的三维世界。因此,如果直接处理世界坐标,可能会让远离原点的世界坐标遇到一些精度问题。如果在所谓的摄像机空间中处理,我们将处理的点虽然远离世界原点,但也靠近摄像机。可以将观察矩阵和世界矩阵组合的矩阵称为模型观察矩阵(Model View Matrix)。

+

让我们开始修改代码以支持摄像机吧。先创建一个名为Camera的类,它将储存摄像机的位置与旋转状态。该类将提供设置位置或旋转状态的方法(setPositionsetRotation),或在当前状态下用偏移量更新这些值的方法(movePositionmoveRotation)。

+
package org.lwjglb.engine.graph;
+
+import org.joml.Vector3f;
+
+public class Camera {
+
+    private final Vector3f position;
+
+    private final Vector3f rotation;
+
+    public Camera() {
+        position = new Vector3f(0, 0, 0);
+        rotation = new Vector3f(0, 0, 0);
+    }
+
+    public Camera(Vector3f position, Vector3f rotation) {
+        this.position = position;
+        this.rotation = rotation;
+    }
+
+    public Vector3f getPosition() {
+        return position;
+    }
+
+    public void setPosition(float x, float y, float z) {
+        position.x = x;
+        position.y = y;
+        position.z = z;
+    }
+
+    public void movePosition(float offsetX, float offsetY, float offsetZ) {
+        if ( offsetZ != 0 ) {
+            position.x += (float)Math.sin(Math.toRadians(rotation.y)) * -1.0f * offsetZ;
+            position.z += (float)Math.cos(Math.toRadians(rotation.y)) * offsetZ;
+        }
+        if ( offsetX != 0) {
+            position.x += (float)Math.sin(Math.toRadians(rotation.y - 90)) * -1.0f * offsetX;
+            position.z += (float)Math.cos(Math.toRadians(rotation.y - 90)) * offsetX;
+        }
+        position.y += offsetY;
+    }
+
+    public Vector3f getRotation() {
+        return rotation;
+    }
+
+    public void setRotation(float x, float y, float z) {
+        rotation.x = x;
+        rotation.y = y;
+        rotation.z = z;
+    }
+
+    public void moveRotation(float offsetX, float offsetY, float offsetZ) {
+        rotation.x += offsetX;
+        rotation.y += offsetY;
+        rotation.z += offsetZ;
+    }
+}
+
+

接下来在Transformation中,将定义一个新矩阵来储存观察矩阵。

+
private final Matrix4f viewMatrix;
+
+

我们要提供一个更新这个值的方法。与投影矩阵相似,这个矩阵对于渲染周期中渲染的所有物体都是相同的。

+
public Matrix4f getViewMatrix(Camera camera) {
+    Vector3f cameraPos = camera.getPosition();
+    Vector3f rotation = camera.getRotation();
+
+    viewMatrix.identity();
+    // 首先进行旋转,使摄像机在其位置上旋转
+    viewMatrix.rotate((float)Math.toRadians(rotation.x), new Vector3f(1, 0, 0))
+        .rotate((float)Math.toRadians(rotation.y), new Vector3f(0, 1, 0));
+    // 然后做位移
+    viewMatrix.translate(-cameraPos.x, -cameraPos.y, -cameraPos.z);
+    return viewMatrix;
+}
+
+

如你所见,我们首先需要做旋转,然后位移。如果操作顺序相反,我们将不会沿着摄像机位置旋转,而是沿着坐标原点旋转。请注意,在Camera类的movePosition方法中,我们不只是简单地增加摄像机位置的偏移量。我们还考虑了沿Y轴的旋转,也就是偏航,以便计算最终位置。如果只是通过偏移来增加摄像机的位置,摄像机就不会朝着它所朝向的方向移动。

+

除了上述所说的,我们现在还没有得到一个可以完全自由移动的摄像机(例如,如果我们沿着X轴旋转,当向前移动时,摄像机不会在空间中向上或向下移动),这将在此后的章节中完成,因为这有点复杂。

+

最后,我们将移除之前的getWorldMatrix方法,并添加一个名为getModelViewMatrix的新方法。

+
public Matrix4f getModelViewMatrix(GameItem gameItem, Matrix4f viewMatrix) {
+    Vector3f rotation = gameItem.getRotation();
+    modelViewMatrix.set(viewMatrix).translate(gameItem.getPosition()).
+        rotateX((float)Math.toRadians(-rotation.x)).
+        rotateY((float)Math.toRadians(-rotation.y)).
+        rotateZ((float)Math.toRadians(-rotation.z)).
+            scale(gameItem.getScale());
+    return modelViewMatrix;
+}
+
+

每个GameItem实例都将调用一次getModelViewMatrix方法。

+

Renderer类的render方法中,我们只需要根据摄像机的值,在投影矩阵更新之后更新观察矩阵。

+
// 更新投影矩阵
+Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOV, window.getWidth(), window.getHeight(), Z_NEAR, Z_FAR);
+shaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+// 更新观察矩阵
+Matrix4f viewMatrix = transformation.getViewMatrix(camera);
+
+shaderProgram.setUniform("texture_sampler", 0);
+// 渲染每个游戏项
+for(GameItem gameItem : gameItems) {
+    // 为该项设置模型观察矩阵
+    Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix);
+    shaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+    // 渲染该游戏项的网格
+    gameItem.getMesh().render();
+}
+
+

这就是实现摄像机概念的基本代码,现在需要使用它。我们可以修改输入处理和更新摄像机的方式,将设置如下按键:

+
    +
  • “A”和“D”键使摄像机左右(X轴)移动。
  • +
  • “W”和“S”键使摄像机前后(Z轴)移动。
  • +
  • “Z”和“X”键使摄像机上下(Y轴)移动。
  • +
+

当鼠标按下右键时,我们将使用鼠标位置沿X和Y轴旋转摄像机。

+

如你所见,我们将首次使用鼠标,创建一个名为MouseInput的新类,该类将封装鼠标访问。该类代码如下所示:

+
package org.lwjglb.engine;
+
+import org.joml.Vector2d;
+import org.joml.Vector2f;
+import static org.lwjgl.glfw.GLFW.*;
+
+public class MouseInput {
+
+    private final Vector2d previousPos;
+
+    private final Vector2d currentPos;
+
+    private final Vector2f displVec;
+
+    private boolean inWindow = false;
+
+    private boolean leftButtonPressed = false;
+
+    private boolean rightButtonPressed = false;
+
+    public MouseInput() {
+        previousPos = new Vector2d(-1, -1);
+        currentPos = new Vector2d(0, 0);
+        displVec = new Vector2f();
+    }
+
+    public void init(Window window) {
+        glfwSetCursorPosCallback(window.getWindowHandle(), (windowHandle, xpos, ypos) -> {
+            currentPos.x = xpos;
+            currentPos.y = ypos;
+        });
+        glfwSetCursorEnterCallback(window.getWindowHandle(), (windowHandle, entered) -> {
+            inWindow = entered;
+        });
+        glfwSetMouseButtonCallback(window.getWindowHandle(), (windowHandle, button, action, mode) -> {
+            leftButtonPressed = button == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS;
+            rightButtonPressed = button == GLFW_MOUSE_BUTTON_2 && action == GLFW_PRESS;
+        });
+    }
+
+    public Vector2f getDisplVec() {
+        return displVec;
+    }
+
+    public void input(Window window) {
+        displVec.x = 0;
+        displVec.y = 0;
+        if (previousPos.x > 0 && previousPos.y > 0 && inWindow) {
+            double deltax = currentPos.x - previousPos.x;
+            double deltay = currentPos.y - previousPos.y;
+            boolean rotateX = deltax != 0;
+            boolean rotateY = deltay != 0;
+            if (rotateX) {
+                displVec.y = (float) deltax;
+            }
+            if (rotateY) {
+                displVec.x = (float) deltay;
+            }
+        }
+        previousPos.x = currentPos.x;
+        previousPos.y = currentPos.y;
+    }
+
+    public boolean isLeftButtonPressed() {
+        return leftButtonPressed;
+    }
+
+    public boolean isRightButtonPressed() {
+        return rightButtonPressed;
+    }
+}
+
+

MouseInput类提供了一个应在在初始化阶段调用的init方法,并注册一组回调以处理鼠标事件:

+
    +
  • glfwSetCursorPosCallback:注册一个回调,该回调将在鼠标移动时被调用。
  • +
  • glfwSetCursorEnterCallback:注册一个回调,该回调将在鼠标进入窗口时被调用。即使鼠标不在窗口内,我们也会收到鼠标事件。我们使用这个回调来确认鼠标进入窗口。
  • +
  • glfwSetMouseButtonCallback:注册一个回调,该回调在按下鼠标按钮时被调用。
  • +
+

MouseInput类提供了一个input方法,在处理游戏输入时应调用该方法。该方法计算鼠标从上一个位置的位移,并将其存储到Vector2f类型的displVec变量中,以便游戏使用它。

+

MouseInput类将在GameEngine类中实例化,并且将作为参数传递给游戏实现的initupdate方法(因此需要相应地更改IGameLogic接口)。

+
void input(Window window, MouseInput mouseInput);
+
+void update(float interval, MouseInput mouseInput);
+
+

鼠标输入将在GameEngine类的input方法中被处理,而最终的控制将交由游戏实现。

+
protected void input() {
+    mouseInput.input(window);
+    gameLogic.input(window, mouseInput);
+}
+
+

现在已经准备好修改DummyGame类来处理键盘和鼠标输入了。该类的输入方法如下所示:

+
@Override
+public void input(Window window, MouseInput mouseInput) {
+    cameraInc.set(0, 0, 0);
+    if (window.isKeyPressed(GLFW_KEY_W)) {
+        cameraInc.z = -1;
+    } else if (window.isKeyPressed(GLFW_KEY_S)) {
+        cameraInc.z = 1;
+    }
+    if (window.isKeyPressed(GLFW_KEY_A)) {
+        cameraInc.x = -1;
+    } else if (window.isKeyPressed(GLFW_KEY_D)) {
+        cameraInc.x = 1;
+    }
+    if (window.isKeyPressed(GLFW_KEY_Z)) {
+        cameraInc.y = -1;
+    } else if (window.isKeyPressed(GLFW_KEY_X)) {
+        cameraInc.y = 1;
+    }
+}
+
+

这只是更新一个名为cameraIncVector3f变量,它储存了摄像机应用的位移。

+

DummyGame类的update方法将根据处理的键盘和鼠标事件,修改摄像机的位置和旋转。

+
@Override
+public void update(float interval, MouseInput mouseInput) {
+    // 更新摄像机位置
+    camera.movePosition(cameraInc.x * CAMERA_POS_STEP,
+        cameraInc.y * CAMERA_POS_STEP,
+        cameraInc.z * CAMERA_POS_STEP);
+
+    // 基于鼠标更新摄像机           
+    if (mouseInput.isRightButtonPressed()) {
+        Vector2f rotVec = mouseInput.getDisplVec();
+        camera.moveRotation(rotVec.x * MOUSE_SENSITIVITY, rotVec.y * MOUSE_SENSITIVITY, 0);
+    }
+}
+
+

现在可以添加更多的立方体到世界中,缩放它们,将它们设置在特定位置,并使用新摄像机游玩。如你所见,所有的立方体共享相同的网格。

+
GameItem gameItem1 = new GameItem(mesh);
+gameItem1.setScale(0.5f);
+gameItem1.setPosition(0, 0, -2);
+
+GameItem gameItem2 = new GameItem(mesh);
+gameItem2.setScale(0.5f);
+gameItem2.setPosition(0.5f, 0.5f, -2);
+
+GameItem gameItem3 = new GameItem(mesh);
+gameItem3.setScale(0.5f);
+gameItem3.setPosition(0, 0, -2.5f);
+
+GameItem gameItem4 = new GameItem(mesh);
+gameItem4.setScale(0.5f);
+
+gameItem4.setPosition(0.5f, 0, -2.5f);
+gameItems = new GameItem[]{gameItem1, gameItem2, gameItem3, gameItem4};
+
+

你会得到如下所示的结果。

+

立方体们

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/09-loading-more-complex-models/index.html b/09-loading-more-complex-models/index.html new file mode 100755 index 0000000..32fc8de --- /dev/null +++ b/09-loading-more-complex-models/index.html @@ -0,0 +1,575 @@ + + + + + + + + + + + + 加载更复杂的模型 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

加载更复杂的模型(Loading more complex models)

+

本章中我们将学习加载在外部文件中定义的复杂模型。这些模型将使用3D建模工具(例如Blender)创建。到目前为止,我们已经通过直接编码定义其几何图形的数组来创建模型。但在本章中,我们将学习如何加载以OBJ格式定义的模型。

+

OBJ(或.obj)是Wavefront Technologies开发的一种几何定义开放文件格式,现已被广泛采用。OBJ文件定义构成三维模型的顶点、纹理坐标和多边形。这是一种相对容易解析的格式,因为它基于文本,每一行定义一个元素(顶点、纹理坐标等)。

+

在.obj文件中,每行以一个标记元素类型的标识符开头:

+
    +
  • 以"#"开始的行是注释。
  • +
  • 以"v"开始的行用坐标(x, y, z, w)定义一个几何顶点。例如:v 0.155 0.211 0.32 1.0
  • +
  • 以"vn"开始的行是用坐标(x, y, z)定义顶点法线(Normals)。例如:vn 0.71 0.21 0.82。之后再讨论这个东西。
  • +
  • 以"vt"开始的行定义纹理坐标。例如:vt 0.500 1
  • +
  • 以"f"开始的行定义了一个面。利用该行中的数据可以构造索引数组。我们只处理面导出为三角形的情况。它可以有几种定义方式:
      +
    • 它可以定义顶点位置(f v1 v2 v3)。例如:f 6 3 1。在这种情况下,这个三角形是由位置为6、3和1的几何顶点定义的(顶点索引总是从1开始)。
    • +
    • 它可以定义顶点位置、纹理坐标和法线(f v1/t1/n1 v2/t2/n2 v3/t3/n3)。例如:f 6/4/1 3/5/3 7/6/5。第一部分是v1/t1/n1,其定义了坐标、纹理坐标和顶点法线。看到该部分可以说出:选择几何顶点6、纹理坐标4和顶点法线1。
    • +
    +
  • +
+

OBJ格式有更多的元素类型(如一组多边形、定义材质等)。现在我们仅实现上述子集,我们的OBJ加载器将忽略其他元素类型。

+

但是什么是法线呢?让我们先定义它。一个平面的法线是一个垂直于该平面的长度为1的向量。

+

法线

+

如上所见,一个平面可以有两条法线,我们应该用哪一个呢?三维图形中的法线是用于光照的,所以我们应该选择面向光源的法线。换言之,我们应该选择指向模型外的法线。

+

我们有一个由多边形和三角形组成的3D模型,每个三角形由三个顶点组成,三角形的法线向量是垂直于三角形表面的长度为1的向量。

+

顶点法线与特定顶点相关联,并且是周围三角形的法线的组合(当然它的长度等于1)。在这里你可以看到一个3D网格的顶点模型(取自维基百科

+

顶点法线

+

现在我们开始创建OBJ加载器。首先,我们将修改Mesh类,因为现在必须使用纹理。我们可能加载一些没有定义纹理坐标的OBJ文件,因此必须能够使用颜色而不是使用纹理渲染它们。在此情况下,面的定义格式为:f v/n

+

Mesh类现在有一个名为colour的新属性。

+
private Vector3f colour;
+
+

并且构造函数不再需要Texture。取而代之的是,我们将为纹理和颜色属性提供getset方法。

+
public Mesh(float[] positions, float[] textCoords, float[] normals, int[] indices) {
+
+

当然,在renderclear方法中,在使用纹理之前,必须检查纹理是否为null。正如你在构造函数中看到的,现在需要传递一个名为normals的新浮点数组。如何使用法线渲染?答案很简单,它只是VAO中的另一个VBO,所以我们需要添加如下代码:

+
// 顶点法线VBO
+vboId = glGenBuffers();
+vboIdList.add(vboId);
+vecNormalsBuffer = MemoryUtil.memAllocFloat(normals.length);
+vecNormalsBuffer.put(normals).flip();
+glBindBuffer(GL_ARRAY_BUFFER, vboId);
+glBufferData(GL_ARRAY_BUFFER, vecNormalsBuffer, GL_STATIC_DRAW);
+glEnableVertexAttribArray(2);
+glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, 0);
+
+

现在我们已经完成了对Mesh类的修改,可以修改代码来使用纹理坐标或固定的颜色。因此,我们需要像这样修改片元着色器:

+
#version 330
+
+in  vec2 outTexCoord;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+uniform vec3 colour;
+uniform int useColour;
+
+void main()
+{
+    if ( useColour == 1 )
+    {
+        fragColor = vec4(colour, 1);
+    }
+    else
+    {
+        fragColor = texture(texture_sampler, outTexCoord);
+    }
+}
+
+

如你所见,我们创建了两个新Uniform:

+
    +
  • colour: 将储存基本颜色。
  • +
  • useColour: 这是个标志,当你不想使用纹理时,它将被设置为1。
  • +
+

Renderer类中,我们需要创建这两个Uniform。

+
// 为默认颜色与控制它的标志创建Uniform
+shaderProgram.createUniform("colour");
+shaderProgram.createUniform("useColour");
+
+

和其他Uniform一样,在Renderer类的render方法中,我们也需要为每个GameItem设置这些Uniform的值。

+
for(GameItem gameItem : gameItems) {
+    Mesh mesh = gameItem.getMesh();
+    // 为该游戏项设置模型观察矩阵
+    Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix);
+    shaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+    // 为该游戏项渲染网格
+    shaderProgram.setUniform("colour", mesh.getColour());
+    shaderProgram.setUniform("useColour", mesh.isTextured() ? 0 : 1);
+    mesh.render();
+}
+
+

现在我们可以创建一个名为OBJLoader的新类,该类解析OBJ文件,并用其中包含的数据创建一个Mesh实例。你可能会在网上发现一些其他实现可能比这更有效,但我认为该方案更容易理解。这是一个工具类,它将有如下静态方法:

+
public static Mesh loadMesh(String fileName) throws Exception {
+
+

参数fileName指定OBJ模型的文件名称,该文件必须包含在类路径中。

+

在该方法中我们首先要做的是读取文件内容并将所有行储存到一个数组中,然后创建几个列表来储存顶点、纹理坐标、法线和面。

+
List<String> lines = Utils.readAllLines(fileName);
+
+List<Vector3f> vertices = new ArrayList<>();
+List<Vector2f> textures = new ArrayList<>();
+List<Vector3f> normals = new ArrayList<>();
+List<Face> faces = new ArrayList<>();
+
+

然后解析每一行,并根据开头标识符得到顶点位置、纹理坐标、顶点法线或面定义,最后重新排列这些数据。

+
for (String line : lines) {
+    String[] tokens = line.split("\\s+");
+    switch (tokens[0]) {
+        case "v":
+            // 几何顶点
+            Vector3f vec3f = new Vector3f(
+                Float.parseFloat(tokens[1]),
+                Float.parseFloat(tokens[2]),
+                Float.parseFloat(tokens[3]));
+            vertices.add(vec3f);
+            break;
+        case "vt":
+            // 纹理坐标
+            Vector2f vec2f = new Vector2f(
+                Float.parseFloat(tokens[1]),
+                Float.parseFloat(tokens[2]));
+            textures.add(vec2f);
+            break;
+        case "vn":
+            // 顶点法线
+            Vector3f vec3fNorm = new Vector3f(
+                Float.parseFloat(tokens[1]),
+                Float.parseFloat(tokens[2]),
+                Float.parseFloat(tokens[3]));
+            normals.add(vec3fNorm);
+            break;
+        case "f":
+            Face face = new Face(tokens[1], tokens[2], tokens[3]);
+            faces.add(face);
+            break;
+        default:
+            // 忽略其他行
+            break;
+    }
+}
+return reorderLists(vertices, textures, normals, faces);
+
+

在讲解重新排序之前,让我们看看如何解析面的定义。我们已创建了一个名为Face的类,它负责解析一个面的定义。一个Face是由一个索引组列表组成的,在本例中,由于我们处理的是三角形,所以我们将有三个索引组。

+

面定义

+

我们将创建另一个名为IndexGroup的内部类,它将储存索引组的数据。

+
protected static class IdxGroup {
+
+    public static final int NO_VALUE = -1;
+
+    public int idxPos;
+
+    public int idxTextCoord;
+
+    public int idxVecNormal;
+
+    public IdxGroup() {
+        idxPos = NO_VALUE;
+        idxTextCoord = NO_VALUE;
+        idxVecNormal = NO_VALUE;
+        }
+}
+
+

Face类如下所示:

+
protected static class Face {
+
+    /**
+     * 面三角形的索引组列表(每个面三个顶点)。
+    */
+    private IdxGroup[] idxGroups = new IdxGroup[3];
+
+    public Face(String v1, String v2, String v3) {
+        idxGroups = new IdxGroup[3];
+        // 解析行
+        idxGroups[0] = parseLine(v1);
+        idxGroups[1] = parseLine(v2);
+        idxGroups[2] = parseLine(v3);
+    }
+
+    private IdxGroup parseLine(String line) {
+        IdxGroup idxGroup = new IdxGroup();
+
+        String[] lineTokens = line.split("/");
+        int length = lineTokens.length;
+        idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1;
+        if (length > 1) {
+            // 如果OBJ不定义纹理坐标,则可为null
+            String textCoord = lineTokens[1];
+            idxGroup.idxTextCoord = textCoord.length() > 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE;
+            if (length > 2) {
+                idxGroup.idxVecNormal = Integer.parseInt(lineTokens[2]) - 1;
+            }
+        }
+
+        return idxGroup;
+    }
+
+    public IdxGroup[] getFaceVertexIndices() {
+        return idxGroups;
+    }
+}
+
+

当解析面时,我们可以看到没有纹理但带有矢量法线的对象。在此情况下,面定义可能像f 11//1 17//1 13//1这样,所以我们需要检查这些情况。

+

最后,我们需要重新排列这些数据。Mesh类需要四个数组,分别用于位置坐标、纹理坐标、法线矢量和索引。前三个数组应该具有相同数量的元素,因为索引数组是唯一的(注意,相同数量的元素并不意味着相同的长度。顶点坐标是三维的,由三个浮点数组成。纹理坐标是二维的,由两个浮点数组成)。OpenGL不允许我们对每个元素类型定义不同的索引数组(如果可以的话,我们就不需要在应用纹理时重复顶点)。

+

当你打开一个OBJ文件时,你首先可能会看到储存顶点坐标的列表,比储存纹理坐标和顶点数量的列表的元素数量更多。这是我们需要解决的问题。举一个简单的例子,定义一个具有像素高度的正方形(只是为了演示),其OBJ文件可能是这样的(不要太关注法线坐标,因为它只是为了演示)。

+
v 0 0 0
+v 1 0 0
+v 1 1 0
+v 0 1 0
+
+vt 0 1
+vt 1 1
+
+vn 0 0 1
+
+f 1/2/1 2/1/1 3/2/1
+f 1/2/1 3/2/1 4/1/1
+
+

当完成对文件的解析时,我们得到如下所示列表(每个元素的数字是它在文件中的位置顺序,按出现顺序排列):

+

序列1

+

现在我们将使用面定义来创建包含索引的最终数组。需要考虑的是,纹理坐标与法线向量的定义顺序与顶点的定义顺序不对应。如果列表的大小是相同的并有序的,那么面定义就只需要每个顶点一个索引。

+

因此,我们需要排列数据,并根据需要进行相应的设置。首先要做的是创建三个数组(用于顶点、纹理坐标和法线)和一个索引列表。如上所述,三个数组元素数量相同(等于顶点数),顶点数组将是顶点列表的副本。

+

序列2

+

现在我们开始处理这些面。第一个面的第一个索引组是1/2/1。我们使用索引组中的第一个索引(定义几何顶点的索引)来构造索引列表,称之为posIndex

+

面指定我们应该把占据第一个位置的元素的索引添加到索引列表中。因此,我们将posIndex减去1后放到indicesList中(必须减1,因为数组的起始是0,而OBJ文件格式中起始是1)。

+

序列3

+

然后,我们使用索引组的其他索引来设置texturesArraynormalsArray。索引组中的第二个索引是2,所以我们必须将第二个纹理坐标放在与所占顶点指定的posIndex位置(V1)相同的位置上。

+

序列4

+

然后我们看到第三个索引,它为1,所以要做的是将第一个法线向量坐标放在与所占顶点指定的posIndex位置(V1)相同的位置上。

+

序列5

+

在处理了第一个面之后,数组和列表如下所示。

+

序列6

+

在处理了第二个面之后,数组和列表如下所示。

+

序列7

+

第二个面也定义了已经被赋值的顶点,但是它们有相同的值,所以处理这个问题上很简单。我觉得这个过程已经讲解得足够清晰了,不过在你明白之前可能会有些棘手。重新排列数据的方法如下所示。请记住,我们要的是浮点数组,所以必须把顶点、纹理和法线数组转换为浮点数组。因此,对于顶点和法线来说数组的长度是顶点列表的长度乘以3,而对于纹理坐标来说数组的长度是顶点列表的长度乘以2。

+
private static Mesh reorderLists(List<Vector3f> posList, List<Vector2f> textCoordList,
+    List<Vector3f> normList, List<Face> facesList) {
+
+    List<Integer> indices = new ArrayList<>();
+    // 按声明的顺序创建位置数组
+    float[] posArr = new float[posList.size() * 3];
+    int i = 0;
+    for (Vector3f pos : posList) {
+        posArr[i * 3] = pos.x;
+        posArr[i * 3 + 1] = pos.y;
+        posArr[i * 3 + 2] = pos.z;
+        i++;
+    }
+    float[] textCoordArr = new float[posList.size() * 2];
+    float[] normArr = new float[posList.size() * 3];
+
+    for (Face face : facesList) {
+        IdxGroup[] faceVertexIndices = face.getFaceVertexIndices();
+        for (IdxGroup indValue : faceVertexIndices) {
+            processFaceVertex(indValue, textCoordList, normList,
+                indices, textCoordArr, normArr);
+        }
+    }
+    int[] indicesArr = new int[indices.size()];
+    indicesArr = indices.stream().mapToInt((Integer v) -> v).toArray();
+    Mesh mesh = new Mesh(posArr, textCoordArr, normArr, indicesArr);
+    return mesh;
+}
+
+private static void processFaceVertex(IdxGroup indices, List<Vector2f> textCoordList,
+    List<Vector3f> normList, List<Integer> indicesList,
+    float[] texCoordArr, float[] normArr) {
+
+    // 设置顶点坐标的索引
+    int posIndex = indices.idxPos;
+    indicesList.add(posIndex);
+
+    // 对纹理坐标重新排序
+    if (indices.idxTextCoord >= 0) {
+        Vector2f textCoord = textCoordList.get(indices.idxTextCoord);
+        texCoordArr[posIndex * 2] = textCoord.x;
+        texCoordArr[posIndex * 2 + 1] = 1 - textCoord.y;
+    }
+    if (indices.idxVecNormal >= 0) {
+        // 对法线向量重新排序
+        Vector3f vecNorm = normList.get(indices.idxVecNormal);
+        normArr[posIndex * 3] = vecNorm.x;
+        normArr[posIndex * 3 + 1] = vecNorm.y;
+        normArr[posIndex * 3 + 2] = vecNorm.z;
+    }
+}
+
+

此外需要注意的是纹理坐标是UV格式,所以Y坐标为用一减去文件中取到的值。

+

现在,我们终于可以渲染OBJ模型。我准备了一个OBJ文件,其中是此前章节中使用过的具有纹理的立方体。为了在DummyGame类的init方法中使用它,我们需要创建一个GameItem实例。

+
Texture texture = new Texture("/textures/grassblock.png");
+mesh.setTexture(texture);
+GameItem gameItem = new GameItem(mesh);
+gameItem.setScale(0.5f);
+gameItem.setPosition(0, 0, -2);
+gameItems = new GameItem[]{gameItem};
+
+

然后将会得到一个熟悉的有纹理的立方体。

+

有纹理的立方体

+

我们可以尝试渲染其他模型,例如可以使用著名的Stanford Bunny模型(它可以免费下载),它放在resources文件夹中。这个模型没有纹理,所以我们可以这样做:

+
Mesh mesh = OBJLoader.loadMesh("/models/bunny.obj");
+GameItem gameItem = new GameItem(mesh);
+gameItem.setScale(1.5f);
+gameItem.setPosition(0, 0, -2);
+gameItems = new GameItem[]{gameItem};
+
+

Stanford Bunny

+

这个模型看起来有点奇怪,因为没有纹理也没有光,所以我们不能看到它的体积,但是你可以检查模型是否正确地加载。在Window类中,设置OpenGL参数时,添加这一行代码。

+
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
+
+

当你放大的时候,你会看到如下所示的东西。

+

Stanford Bunny的三角形

+

现在你可以看到构成模型的所有三角形。

+

有了这个OBJ载入类,你现在可以使用Blender创建模型。Blender是一个强大的工具,但刚开始使用它有点困难,它有很多选项,很多关节组合,在首次使用它时你需要花时间做很多最基本的事情。当使用Blender导出模型时,请确保包含法线并将面导出为三角形。

+

OBJ导出选项

+

导出时请记得分割边,因为我们不能将多个纹理坐标指定给同一个顶点。此外,我们需要为每个三角形定义法线,而不是指定给顶点。如果你在某些模型中遇到了光照问题(在下一章中),你应该验证一下法线。你可以在Blender中看到法线。

+

边分割

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/10-let-there-be-light/index.html b/10-let-there-be-light/index.html new file mode 100755 index 0000000..f90871c --- /dev/null +++ b/10-let-there-be-light/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + 要有光 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

要有光(Let there be light)

+

在本章中,我们将学习如何为3D游戏引擎添加光照。我们不会去实现一个完美的光照模型,因为抛开复杂性不谈,它还需要消耗大量的计算机资源,相反我们只需要一个近似的、像样的光照效果:我们将使用名为 Phong 的着色算法(由Bui Tuong Phong开发)。另一个需要注意的是,我们将只模拟光照,但不会模拟这些光照所产生的阴影(这将在后续章节中实现)。

+

在开始之前,先定义几个光照类型:

+
    +
  • 点光源(Point Light):这种光模拟的是一个由点向空间各个方向均匀发射的光源。
  • +
  • 聚光源(Spot Light):这种光模拟从空间中的点发射的光源,但不是向所有方向上发射,而是限定在了一个锥形方向上。
  • +
  • 平行光(Directional Light):这种光模拟了太阳光,3D场景中的所有物体都会受到来自特定方向的平行光线的照射。无论物体是近抑或是远,光线总是以相同角度照射在物体上。
  • +
  • 环境光(Ambient Light):这种光来自空间的任何方向,并以相同的强度照亮所有物体。
  • +
+

光照类型

+

因此,为了模拟光照,我们需要考虑光照的类型、位置和其他一些参数,如颜色。当然,我们还必须考虑物体如何受光照影响吸收和反射光。

+

Phong着色算法将模拟光线对我们模型中每个点的影响,即对每个顶点的影响。这就是为什么它被称为局部光照模型的原因,这也是该算法不能计算阴影的原因:它只会计算应用到每个顶点的光照,而不考虑顶点是否位于遮光物体之后。我们将在此后的章节中解决这个问题。但正因如此,它是一种非常简单快速的算法,并且可以提供非常好的效果。我们将在此实现一个没有深入考虑材质影响的简化版本。

+

Phong算法提供了三种光照分量:

+
    +
  • 环境光(Ambient Light):模拟来自任何方向的光照,它将照亮(需要对应强度值)未受任何光线照射的区域,就像背景光。
  • +
  • 漫反射(Diffuse Reflectance):考虑到面向光源的表面更亮。
  • +
  • 镜面反射(Specular Reflectance):模拟光线在抛光或金属表面上的反射。
  • +
+

最后,我们要得到一个因数,将它与指定片元的颜色相乘,根据它所受的光照将该颜色变得更亮或更暗。令为环境光、为漫反射光、为镜面反射光,目标因数将是上述分量的总和:

+

+ +

+

这些分量其实就是颜色,也就是每个光照分量所贡献的颜色分量。这是因为光照分量不仅会提供一定程度的亮度,还会改变模型的颜色。在我们的片元着色器中,只需将该光照的颜色与原始片元颜色(从纹理或基色获得的)相乘即可。

+

我们还可以为相同的材质指定不同的颜色,这些颜色将用于环境光、漫反射和镜面反射分量。因此,这些分量将由材质关联的颜色调整。如果材质有纹理,我们只需为每个分量使用一个纹理。

+

所以对于无纹理的材质的最终颜色将是:

+

+ +

+

对于有纹理的材质的最终颜色将是:

+

+ +

+

环境光分量

+

让我们来看看第一个分量,即环境光分量,它只是一个常量,会使我们的所有物体变得更亮或更暗。我们可以使用它来模拟一天中特定时间段内的光照(黎明、黄昏等),也可以用它来添加一些不直接被光线照射到,但可以以简单的方式被间接光线照射(比如反射)到的点的光照。

+

环境光是最容易计算的分量,我们只需要传递一种颜色,它将与基色相乘,以调整该基色。假如我们已经确定片元的颜色是,即红色。如果没有环境光时,它将显示为完全红色的片元。如果我们将环境光设置为,则最终颜色将为,其实就是变暗的红色。这种光照会以相同的方式使所有片元变暗(称其为使物体变暗的光似乎有点奇怪,事实上这就是我们得到的效果)。此外,如果光色的RGB分量不相同,它还可以为片元添加一些颜色,所以我们只需要一个矢量来调节环境光强度和颜色。

+

漫反射

+

现在我们来谈谈漫反射,它模拟了这样一个现象:与光线垂直的面看起来比以更接近与光线平行的角度接收光线的面更亮。一个物体接收到的光越多,其光密度(在此这样称呼)就越高。

+

漫反射光

+

但是,我们该如何计算它?你还记得上一章中我们介绍过的法线的概念吗?法线是垂直于平面并且长度为1的向量。因此,让我们为上图中的三个点的绘制法线。如你所见,每个点的法线将是垂直于每个点的切平面的向量。我们不用绘制来自光源的光线,而是绘制从每个点到光源(即相反的方向)的向量。

+

法线与光线的方向

+

如你所见,点的法线与指向光源的向量平行,该法线的方向与光线的方向相反(已被移动,以便你可以看到它,但这在数学上是等价的)。与指向光源的向量所成的夹角等于。因为它的切平面垂直于光源,所以将是最亮的点。

+

+点的法线与指向光源的向量所成的夹角约为30度,所以它应该比更暗。最后,的法线也与指向光源的向量平行,但两个向量的方向相反。与指向光源的向量的角度为180度,所以根本不应该接收到任何光线。

+

因此,我们似乎得到了一个计算某点的光照强度的好方法,光强与该点的法线和该点指向光源的向量之间的夹角大小有关。但我们要怎么计算它呢?

+

有一个我们可以使用的数学运算————数量积(又称为点积)。该运算需要两个向量并得到一个数字(标量),如果它们之间的角度较小,则得到一个正数;如果它们之间的角度很大,则得到一个负数。如果两个向量都被归一化,即两者的长度都等于1,那么数量积的结果将介于之间。如果两个向量的方向相同(即夹角为),则数量积为1;如果两个向量夹角为直角,则它的值为;如果两个向量的方向相反,则为

+

我们定义两个向量,,并以作为它们之间的夹角。数量积的定义如下:

+

数量积

+

如果两个向量都归一化,即它们的长度,或它们的模长等于1,它们的数量积即为夹角的余弦值。我们将使用该运算来计算漫反射分量。

+

所以我们需要计算指向光源的向量,该怎么做呢?假如我们有每个点的位置(即顶点位置)和光源的位置。首先,这两个坐标必须位于同一个坐标空间中。为了简化,我们假设它们都处于世界坐标系中,那么这些位置是指向顶点位置()和光源()的向量的坐标,如下图所示:

+

漫反射光照运算I

+

如果我们从中减去,就会得到我们所需的向量,称为

+

现在,我们可以在指向光源的矢量和法线之间做数量积,因为Johann Lambert是第一个提出这种关系来模拟平面亮度的,所以该乘积被称为兰伯特项。

+

让我们总结一下,定义以下变量:

+
    +
  • + :我们的顶点在模型观察空间坐标系中的位置。
  • +
  • +:观察空间坐标系中的光源位置。
  • +
  • +:光的强度(从0到1)。
  • +
  • +:光的颜色。
  • +
  • +:顶点法线。
  • +
+

首先我们需要计算从当前位置指向光源的向量:,该运算的结果需要归一化。

+

然后我们需要计算漫反射因数(标量):。计算两向量之间的数量积,我们希望值介于之间,所以两个向量都需要归一化。颜色需要介于之间,所以如果值低于,我们将其设为

+

最后,我们只需要通过漫反射因数和光的强度来调节光的颜色:

+

+ +

+

镜面反射

+

现在看到镜面反射,但首先我们需要知道光线是如何反射的。当光照射到一个表面时,它的一部分被吸收,另一部分被反射,如果你还记得你的物理课知识,反射就是光从物体反弹回来。

+

光反射

+

当然,物体表面不是完全抛光的,如果你仔细观察,你会看到很多不平整的地方。此外还有许多光线(实际上是光子),会撞击这个表面,并且会以不同的角度进行反射。因此,我们看到的就像是一束光照射表面并散射回来。也就是说,光在撞击表面时会发散,这就是我们之前讨论过的漫反射分量。

+

表面

+

但是,当光线照射抛光表面时,例如金属,光线会受到较低的扩散影响,并且在它撞到表面时,大部分会向反方向反射。

+

抛光平面

+

这就是镜面反射分量的模型,它取决于材质属性。对于镜面反射,重要的是只有当摄像机处于恰当的位置时,即反射光的反射区域内,反射光才可见。

+

镜面光照

+

既然已经解释了镜面反射之中的机理,我们接下来准备计算这个分量。首先我们需要一个从光源指向顶点的向量。当我们计算漫反射分量时,我们使用的是方向与之相反的向量,它指向的是光源,即。所以让我们将其计算为

+

然后我们需要考虑到表面的法线,来计算由射出的光线撞击表面所产生的反射光。有一个名为reflect的GLSL函数实现了该功能。所以,

+

我们还需要一个指向摄像机的向量,并将其命名为,然后计算出摄像机位置和顶点位置之间的差值:。摄像机位置向量和顶点位置需要处于相同的坐标系中,并且得到的向量需要归一化。下图概述了我们目前计算的主要分量:

+

镜面光照计算

+

现在我们需要计算光强,即。如果向量指向相同的方向,该值就越高,如果它们方向相反其值则越低。为了计算该值我们将再次运用数量积。。我们只希望该值位于之间,所以如果它低于,就将它设为0。

+

我们还需要考虑到,如果摄像机指向反射光锥,则该光更强烈。这可以通过计算次幂来实现,其中为给定的参数:

+

+ +

+

最后,我们需要对材质的反射率进行建模,如果光线反射,反射率将调节反射光的强度,这将用到一个名为reflectance的参数。所以镜面反射分量的颜色为:

+

+ +

+

衰减

+

我们现在知道如何计算这三个分量了,这些分量可以帮助我们用环境光模拟点光源。但是我们的光照模型还不完整,因为物体反射的光与光源的距离无关,也就是说,我们需要模拟光线衰减。

+

衰减是一个与距离和光有关的函数。光的强度与距离的平方成反比。这很容易理解,随着光线的传播,其能量沿着球体表面分布,其半径等于光线行进的距离,而球的表面积与其半径的平方成正比。我们可以用下式来计算衰减因子:

+

为了模拟衰减,我们只需要将衰减因数乘以最终颜色即可。

+

实现

+

现在我们可以开始编程实现上述的所有概念,我们将从着色器开始。大部分工作将在片元着色器中完成,但我们还需要将顶点着色器中的一些数据传递给它。在上一章中,片元着色器仅接收纹理坐标,现在我们还将传递两个参数:

+
    +
  • 已转换为模型观察空间坐标系并已归一化的顶点法线。
  • +
  • 已转换为模型观察空间坐标系的顶点位置。
  • +
+

顶点着色器的代码如下所示:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+out vec3 mvVertexNormal;
+out vec3 mvVertexPos;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPos;
+    outTexCoord = texCoord;
+    mvVertexNormal = normalize(modelViewMatrix * vec4(vertexNormal, 0.0)).xyz;
+    mvVertexPos = mvPos.xyz;
+}
+
+

在我们继续讲解片元着色器之前,必须强调一个非常重要的概念。从上述代码可以看到,mvVertexNormal,该变量包含已转换为模型观察空间坐标的顶点法线。这是通过将vertexNormal乘上modelViewMatrix来实现的,就像顶点位置一样。但有一个细微的区别,该顶点法线的w分量在乘以矩阵之前被设置为0:vec4(vertexNormal, 0.0)。我们为什么要这样做呢?因为我们希望法线被旋转和缩放,但我们不希望它被平移,所以我们只对它的方向感兴趣,而不是它的位置。而这是通过将w分量设置为0来实现的,这也是是使用齐次坐标的优点之一,通过设置w分量,我们可以控制应用的变换。你可以在纸上做矩阵乘法,看看为什么是这样。

+

现在我们可以开始在片元着色器中开工了,除了将来自顶点着色器的值声明为输入参数之外,我们将定义一些有用的结构体来建模光照和材质属性。首先我们定义用于建模光的结构体。

+
struct Attenuation
+{
+    float constant;
+    float linear;
+    float exponent;
+};
+
+struct PointLight
+{
+    vec3 colour;
+    // 光源位置是在观察坐标系中的
+    vec3 position;
+    float intensity;
+    Attenuation att;
+};
+
+

点光源由一个颜色,一个位置,以及一个介于之间的数字(用于模拟光照强度)和一组用于模拟衰减方程的参数定义。

+

建模材质属性的结构体如下:

+
struct Material
+{
+    vec4 ambient;
+    vec4 diffuse;
+    vec4 specular;
+    int hasTexture;
+    float reflectance;
+};
+
+

材质由一组颜色定义(假如我们不使用纹理为片元着色):

+
    +
  • 用于环境光分量的颜色。
  • +
  • 用于漫反射分量的颜色。
  • +
  • 用于镜面反射分量的颜色。
  • +
+

材质还由一个标志控制它是否拥有关联的纹理和反射率指数定义。我们将在片元着色器中使用以下Uniform。

+
uniform sampler2D texture_sampler;
+uniform vec3 ambientLight;
+uniform float specularPower;
+uniform Material material;
+uniform PointLight pointLight;
+uniform vec3 camera_pos;
+
+

我们用新建的Uniform设置以下变量:

+
    +
  • 环境光:储存颜色,以同样方式影响每个片元。
  • +
  • 镜面反射强度(在讨论镜面反射光时给出的方程式中使用的指数)。
  • +
  • 一个点光源。
  • +
  • 材质属性。
  • +
  • 摄像机在观察空间坐标系中的位置。
  • +
+

我们还将定义一些全局变量,它们将储存要在环境、漫反射和镜面反射中使用的材质颜色分量。我们使用这些变量是因为如果分量具有纹理,我们将对所有分量使用相同的颜色,并且我们不希望进行冗余的纹理查找。这些变量的定义如下:

+
vec4 ambientC;
+vec4 diffuseC;
+vec4 speculrC;
+
+

我们现在可以定义一个函数,来根据材质属性设置这些变量:

+
void setupColours(Material material, vec2 textCoord)
+{
+    if (material.hasTexture == 1)
+    {
+        ambientC = texture(texture_sampler, textCoord);
+        diffuseC = ambientC;
+        speculrC = ambientC;
+    }
+    else
+    {
+        ambientC = material.ambient;
+        diffuseC = material.diffuse;
+        speculrC = material.specular;
+    }
+}
+
+

现在我们要定义一个函数,它以点光源、顶点位置及其法线为输入并返回此前描述的漫反射和镜面反射分量计算的颜色。

+
vec4 calcPointLight(PointLight light, vec3 position, vec3 normal)
+{
+    vec4 diffuseColour = vec4(0, 0, 0, 0);
+    vec4 specColour = vec4(0, 0, 0, 0);
+
+    // 漫反射
+    vec3 light_direction = light.position - position;
+    vec3 to_light_source  = normalize(light_direction);
+    float diffuseFactor = max(dot(normal, to_light_source ), 0.0);
+    diffuseColour = diffuseC * vec4(light.colour, 1.0) * light.intensity * diffuseFactor;
+
+    // 镜面反射
+    vec3 camera_direction = normalize(-position);
+    vec3 from_light_source = -to_light_source;
+    vec3 reflected_light = normalize(reflect(from_light_source, normal));
+    float specularFactor = max( dot(camera_direction, reflected_light), 0.0);
+    specularFactor = pow(specularFactor, specularPower);
+    specColour = speculrC * specularFactor * material.reflectance * vec4(light.colour, 1.0);
+
+    // 衰减
+    float distance = length(light_direction);
+    float attenuationInv = light.att.constant + light.att.linear * distance +
+        light.att.exponent * distance * distance;
+    return (diffuseColour + specColour) / attenuationInv;
+}
+
+

上述代码相对比较直白简单,它只是计算了漫反射分量的颜色,另一个是计算镜面反射的颜色,并通过光线在到达我们正在处理的顶点时受到的衰减来调整它们。

+

请注意,顶点坐标是位于观察空间中的。在计算镜面反射时,我们得出到观察点(即摄像机位置)的方向,代码如下:

+
 vec3 camera_direction = normalize(camera_pos - position);
+
+

但是,由于position位于观察空间中,摄像机位置始终位于原点,即,所以我们按如下代码计算它:

+
 vec3 camera_direction = normalize(vec3(0, 0, 0) - position);
+
+

可以简化为:

+
 vec3 camera_direction = normalize(-position);
+
+

有了上述函数,顶点着色器的主函数就变得非常简单了。

+
void main()
+{
+    setupColours(material, outTexCoord);
+
+    vec4 diffuseSpecularComp = calcPointLight(pointLight, mvVertexPos, mvVertexNormal);
+
+    fragColor = ambientC * vec4(ambientLight, 1) + diffuseSpecularComp;
+}
+
+

调用setupColours函数将使用适当的颜色来设置变量ambientCdiffuseCspeculrC。然后,我们计算漫反射和镜面反射分量,并考虑到衰减。为了方便起见,我们使用单个函数调用来实现此操作,如上所述。最终的颜色是通过添加环境光分量来计算的(将ambientC乘以环境光)。如你所见,环境光不受衰减的影响。

+

在着色器中我们引入了一些需要进一步解释的新概念,我们正在定义结构体并将它们用作Uniform。但我们要怎么传递这些结构体?首先,我们将定义两个新类,它们建模点光源和材质属性,名为PointLightMaterial。它们只是普通的Java对象,所以你可以在本书附带的源代码中查看它们。然后,我们需要在ShaderProgram类中创建新方法,首先要能够为点光源和材质结构体创建Uniform。

+
public void createPointLightUniform(String uniformName) throws Exception {
+    createUniform(uniformName + ".colour");
+    createUniform(uniformName + ".position");
+    createUniform(uniformName + ".intensity");
+    createUniform(uniformName + ".att.constant");
+    createUniform(uniformName + ".att.linear");
+    createUniform(uniformName + ".att.exponent");
+}
+
+public void createMaterialUniform(String uniformName) throws Exception {
+    createUniform(uniformName + ".ambient");
+    createUniform(uniformName + ".diffuse");
+    createUniform(uniformName + ".specular");
+    createUniform(uniformName + ".hasTexture");
+    createUniform(uniformName + ".reflectance");
+}
+
+

如你所见,它非常简单,我们只为构成结构体的所有属性创建一个单独的Uniform。现在我们需要创建另外两个方法来设置这些Uniform的值,并将PointLightMaterial的实例作为参数。

+
public void setUniform(String uniformName, PointLight pointLight) {
+    setUniform(uniformName + ".colour", pointLight.getColor() );
+    setUniform(uniformName + ".position", pointLight.getPosition());
+    setUniform(uniformName + ".intensity", pointLight.getIntensity());
+    PointLight.Attenuation att = pointLight.getAttenuation();
+    setUniform(uniformName + ".att.constant", att.getConstant());
+    setUniform(uniformName + ".att.linear", att.getLinear());
+    setUniform(uniformName + ".att.exponent", att.getExponent());
+}
+
+public void setUniform(String uniformName, Material material) {
+    setUniform(uniformName + ".ambient", material.getAmbientColour());
+    setUniform(uniformName + ".diffuse", material.getDiffuseColour());
+    setUniform(uniformName + ".specular", material.getSpecularColour());
+    setUniform(uniformName + ".hasTexture", material.isTextured() ? 1 : 0);
+    setUniform(uniformName + ".reflectance", material.getReflectance());
+}
+
+

在本章源代码中,你还将看到我们还修改了Mesh类来储存材质实例,并且创建了一个简单的示例,在其中创建了一个可用“N”和“M”键控制移动的点光源,以显示点光源聚焦在反射率值高于0的网格上时是怎样的。

+

让我们回到片元着色器,如上所述,我们需要另一个储存摄像机位置camera_pos的Uniform。这些坐标必须位于观察空间中。通常我们将在世界空间坐标系中设置光源坐标,因此我们需要将它们乘以观察矩阵,以便能够在着色器中使用它们,所以需要在Transformation类中创建一个新方法,该方法返回观察矩阵以便变换光源坐标。

+
// 获得光源对象的副本并将它的坐标变换到观察空间坐标系
+PointLight currPointLight = new PointLight(pointLight);
+Vector3f lightPos = currPointLight.getPosition();
+Vector4f aux = new Vector4f(lightPos, 1);
+aux.mul(viewMatrix);
+lightPos.x = aux.x;
+lightPos.y = aux.y;
+lightPos.z = aux.z; 
+shaderProgram.setUniform("pointLight", currPointLight);
+
+

我们不会写上完整的源代码,因为如果这样这一章就太长了,且对于解释清楚概念没有太多的帮助,你可以在本书附带的源代码中查阅源代码。

+

光照效果

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/11-let-there-be-even-more-light/index.html b/11-let-there-be-even-more-light/index.html new file mode 100755 index 0000000..3018542 --- /dev/null +++ b/11-let-there-be-even-more-light/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + 要有更多的光 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

要有更多的光(Let there be even more light)

+

在本章中,我们将实现在此前章节中介绍的其他类型的光。我们先从平行光开始。

+

平行光

+

如果你回想一下,平行光从同一方向照射到所有物体上。它用来模拟遥远但光强很高的光源,比如太阳。

+

平行光

+

平行光的另一个特点是它不受衰减的影响,联想太阳光,所有被阳光照射的物体都以相同的光强被照射,因为离太阳的距离太大,以至于它们之间的相对位置都是无关紧要的。事实上,平行光被模拟为位于无穷远处的光源,如果它受到衰减的影响,那么它将对任何物体都没有影响(它对物体颜色的影响将等于0)。

+

此外,平行光也由漫反射和镜面反射分量组成,与点光源的区别在于它没有位置,但有方向,并且它不受衰减的影响。回到平行光的属性,想象我们正在模拟太阳在三维世界中运动,下图展示了黎明、正午和黄昏时的光线方向。

+

太阳像一个平行光

+

上图中的光线的方向为:

+
    +
  • 黎明: +
  • +
  • 正午: +
  • +
  • 黄昏: +
  • +
+

注意:你可能认为上述坐标是位置坐标,但它们只是一个矢量,一个方向,而不是一个位置。以数学的角度来看,矢量和位置是不可分辨的,但它们有着完全不同的含义。

+

但是,我们如何模拟这个光位于无穷远处呢?答案是使用分量,即使用齐次坐标并将分量设置为

+
    +
  • 黎明: +
  • +
  • 正午: +
  • +
  • 黄昏: +
  • +
+

这就如同我们在传递法线。对于法线,我们将其分量设置为,表示我们对其位移不感兴趣,只对方向感兴趣。此外,当我们处理平行光时,也需要这样做,摄像机的位移不应影响平行光的方向。

+

让我们开始编码实现和模拟平行光,首先要做的是创建一个类来储存它的属性。它只是另一个普通的Java对象,其具有复制构造函数,并储存光的方向、颜色和强度。

+
package org.lwjglb.engine.graph;
+
+import org.joml.Vector3f;
+
+public class DirectionalLight {
+
+    private Vector3f color;
+
+    private Vector3f direction;
+
+    private float intensity;
+
+    public DirectionalLight(Vector3f color, Vector3f direction, float intensity) {
+        this.color = color;
+        this.direction = direction;
+        this.intensity = intensity;
+    }
+
+    public DirectionalLight(DirectionalLight light) {
+        this(new Vector3f(light.getColor()), new Vector3f(light.getDirection()), light.getIntensity());
+    }
+
+    // 接下来是Getter和Setter...
+
+

如你所见,我们用Vector3f来储存方向。保持冷静,当将平行光传递到着色器时,我们将处理分量。顺便一提,接下来要做的就是更新ShaderProgram来创建和更新储存平行光的Uniform。

+

在片元着色器中,我们将定义一个结构体来模拟平行光。

+
struct DirectionalLight
+{
+    vec3 colour;
+    vec3 direction;
+    float intensity;
+};
+
+

有了上述定义,ShaderProgram类中的新方法就很简单了。

+
// ...
+public void createDirectionalLightUniform(String uniformName) throws Exception {
+    createUniform(uniformName + ".colour");
+    createUniform(uniformName + ".direction");
+    createUniform(uniformName + ".intensity");
+}
+// ...
+public void setUniform(String uniformName, DirectionalLight dirLight) {
+    setUniform(uniformName + ".colour", dirLight.getColor() );
+    setUniform(uniformName + ".direction", dirLight.getDirection());
+    setUniform(uniformName + ".intensity", dirLight.getIntensity());
+}
+
+

我们现在需要使用Uniform,通过DummyGame类控制太阳的角度来模拟它是如何在天上移动的。

+

太阳的移动

+

我们需要更新光的方向,所以太阳在黎明时(),光线在方向上,其分量从逐渐增加到分量逐渐从增加到。接下来,分量增加到分量减少到。这可以通过将分量设置为角的正弦和将分量设置为角的余弦来实现。

+

正弦和余弦

+

我们也会调节光强,当它远离黎明时强度将增强,当它临近黄昏时强度将减弱。我们通过将强度设置为来模拟夜晚。此外,我们还将调节颜色,使光在黎明和黄昏时变得更红。这将在DummyGame类的update方法中实现。

+
// 更新平行光的方向,强度和颜色
+lightAngle += 1.1f;
+if (lightAngle > 90) {
+    directionalLight.setIntensity(0);
+    if (lightAngle >= 360) {
+        lightAngle = -90;
+    }
+} else if (lightAngle <= -80 || lightAngle >= 80) {
+    float factor = 1 - (float)(Math.abs(lightAngle) - 80)/ 10.0f;
+    directionalLight.setIntensity(factor);
+    directionalLight.getColor().y = Math.max(factor, 0.9f);
+    directionalLight.getColor().z = Math.max(factor, 0.5f);
+} else {
+    directionalLight.setIntensity(1);
+    directionalLight.getColor().x = 1;
+    directionalLight.getColor().y = 1;
+    directionalLight.getColor().z = 1;
+}
+double angRad = Math.toRadians(lightAngle);
+directionalLight.getDirection().x = (float) Math.sin(angRad);
+directionalLight.getDirection().y = (float) Math.cos(angRad);
+
+

然后,我们需要在Renderer类中的render方法中将平行光传给着色器。

+
// 获取平行光对象的副本并将其坐标变换到观察坐标系
+DirectionalLight currDirLight = new DirectionalLight(directionalLight);
+Vector4f dir = new Vector4f(currDirLight.getDirection(), 0);
+dir.mul(viewMatrix);
+currDirLight.setDirection(new Vector3f(dir.x, dir.y, dir.z));
+shaderProgram.setUniform("directionalLight", currDirLight);
+
+

如你所见,我们需要变换光的方向到观察空间,但我们不想应用位移,所以将分量设置为

+

现在,我们已经准备好在片元着色器上完成剩下的工作了,因为顶点着色器不需要修改。此前已经说过,我们需要定义一个名为DirectionalLight的新结构体来模拟平行光,所以需要一个新的Uniform。

+
uniform DirectionalLight directionalLight;
+
+

我们需要重构一下代码,在上一章中,我们有一个名为calcPointLight的函数,它计算漫反射和镜面反射分量,并应用衰减。但如上所述,平行光使用漫反射和镜面反射分量,但不受衰减影响,所以我们将创建一个名为calcLightColour的新函数来计算那些分量。

+
vec4 calcLightColour(vec3 light_colour, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal)
+{
+    vec4 diffuseColour = vec4(0, 0, 0, 0);
+    vec4 specColour = vec4(0, 0, 0, 0);
+
+    // 漫反射光
+    float diffuseFactor = max(dot(normal, to_light_dir), 0.0);
+    diffuseColour = diffuseC * vec4(light_colour, 1.0) * light_intensity * diffuseFactor;
+
+    // 镜面反射光
+    vec3 camera_direction = normalize(camera_pos - position);
+    vec3 from_light_dir = -to_light_dir;
+    vec3 reflected_light = normalize(reflect(from_light_dir , normal));
+    float specularFactor = max( dot(camera_direction, reflected_light), 0.0);
+    specularFactor = pow(specularFactor, specularPower);
+    specColour = speculrC * light_intensity  * specularFactor * material.reflectance * vec4(light_colour, 1.0);
+
+    return (diffuseColour + specColour);
+}
+
+

然后,calcPointLight方法将衰减因数应用到上述函数计算的结果上。

+
vec4 calcPointLight(PointLight light, vec3 position, vec3 normal)
+{
+    vec3 light_direction = light.position - position;
+    vec3 to_light_dir  = normalize(light_direction);
+    vec4 light_colour = calcLightColour(light.colour, light.intensity, position, to_light_dir, normal);
+
+    // 应用衰减
+    float distance = length(light_direction);
+    float attenuationInv = light.att.constant + light.att.linear * distance +
+        light.att.exponent * distance * distance;
+    return light_colour / attenuationInv;
+}
+
+

我们还将创建一个新的函数来计算平行光的效果,它只调用仅需光照方向的calcLightColour方法。

+
vec4 calcDirectionalLight(DirectionalLight light, vec3 position, vec3 normal)
+{
+    return calcLightColour(light.colour, light.intensity, position, normalize(light.direction), normal);
+}
+
+

最后,main方法通过环境光和平行光的颜色分量综合起来计算片元颜色。

+
void main()
+{
+    setupColours(material, outTexCoord);
+
+    vec4 diffuseSpecularComp = calcDirectionalLight(directionalLight, mvVertexPos, mvVertexNormal);
+    diffuseSpecularComp += calcPointLight(pointLight, mvVertexPos, mvVertexNormal); 
+
+    fragColor = ambientC * vec4(ambientLight, 1) + diffuseSpecularComp;
+}
+
+

就这样,现在我们可以模拟太阳在天空中的运动,如下所示(在示例代码中运动速度加快,不用等待太久就可以看到)。

+

平行光效果

+

聚光源

+

现在我们将实现与点光源非常相似的聚光源,但是它发射的光仅限于三维圆锥体中。它模拟从焦点或任何其他不向所有方向发射光的光源。聚光源有着和点光源一样的属性,但它添加了两个新的参数,圆锥角和圆锥方向。

+

聚光源

+

聚光源与点光源的计算方法相同,但有一些不同。从顶点位置到光源的矢量不在光锥内的点不受光照的影响。

+

聚光源2

+

该如何计算它是否在光锥内呢?我们需要在光源和圆锥方向矢量(两者都归一化了)之间再做次数量积。

+

聚光源计算

+

+向量之间的数量积等于:。在聚光源的定义中,我们储存锥角的余弦值,如果数量积高于该值,我们就知道它位于光锥内部(想想余弦图,当角为时,其余弦值为。在0°~180°时,角度越小余弦值越大)。

+

第二个不同之处是远离光锥方向的点将受到更少的光照,换句话说,衰减影响将更强。有几种计算方法,我们将选择一种简单的方法,通过将衰减与下述公式相乘:

+

+ +

+

(在片元着色器中,我们没有传递角度,而是传递角度的余弦值。你可以检查上面的公式的结果是否位于0到1之间,当角度为0时,余弦值为1。)

+

实现非常类似于其他的光源,我们需要创建一个名为SpotLight的类,设置适当的Uniform,将其传递给着色器并修改片元着色器以获取它。你可以查看本章的源代码。

+

当传递Uniform时,另一件重要的事是位移不应该应用到光锥方向上,因为我们只对方向感兴趣。因此,和平行光的情况一样,当变换到观察空间坐标系时,必须将分量设置为

+

聚光源示例

+

多光源

+

我们终于实现了四种类型的光源,但是目前每种类型的光源只能使用一个实例。这对于环境光和平行光来说没问题,但是我们确实希望使用多个点光源和聚光源。我们需要修改片元着色器来接收光源列表,所以使用数组来储存这些数据。来看看怎么实现吧。

+

在开始之前要注意的是,在GLSL中,数组的长度必须在编译时设置,因此它必须足够大,以便在运行时能够储存所需的所有对象。首先是定义一些常量来设置要使用的最大点光源数和聚光源数。

+
const int MAX_POINT_LIGHTS = 5;
+const int MAX_SPOT_LIGHTS = 5;
+
+

然后我们需要修改此前只储存一个点光源和一个聚光源的Uniform,以便使用数组。

+
uniform PointLight pointLights[MAX_POINT_LIGHTS];
+uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
+
+

在main函数中,我们只需要对这些数组进行迭代,以使用现有函数计算每个对象对颜色的影响。我们可能不会像Uniform数组长度那样传递很多光源,所以需要控制它。有很多可行的方法,但这可能不适用于旧的显卡。最终我们选择检查光强(在数组中的空位,光强为0)。

+
for (int i=0; i<MAX_POINT_LIGHTS; i++)
+{
+    if ( pointLights[i].intensity > 0 )
+    {
+        diffuseSpecularComp += calcPointLight(pointLights[i], mvVertexPos, mvVertexNormal); 
+    }
+}
+
+for (int i=0; i<MAX_SPOT_LIGHTS; i++)
+{
+    if ( spotLights[i].pl.intensity > 0 )
+    {
+        diffuseSpecularComp += calcSpotLight(spotLights[i], mvVertexPos, mvVertexNormal);
+    }
+}
+
+

现在我们需要在Render类中创建这些Uniform。当使用数组时,我们需要为列表中的每个元素创建一个Uniform。例如,对于pointLights数组,我们需要创建名为pointLights[0]pointLights[1]之类的Uniform。当然,这也适用于结构体属性,所以我们将创建pointLights[0].colourpointLights[1].colour等等。创建这些Uniform的方法如下所示:

+
public void createPointLightListUniform(String uniformName, int size) throws Exception {
+    for (int i = 0; i < size; i++) {
+        createPointLightUniform(uniformName + "[" + i + "]");
+    }
+}
+
+public void createSpotLightListUniform(String uniformName, int size) throws Exception {
+    for (int i = 0; i < size; i++) {
+        createSpotLightUniform(uniformName + "[" + i + "]");
+    }
+}
+
+

我们也需要方法来设置这些Uniform的值:

+
public void setUniform(String uniformName, PointLight[] pointLights) {
+    int numLights = pointLights != null ? pointLights.length : 0;
+    for (int i = 0; i < numLights; i++) {
+        setUniform(uniformName, pointLights[i], i);
+    }
+}
+
+public void setUniform(String uniformName, PointLight pointLight, int pos) {
+    setUniform(uniformName + "[" + pos + "]", pointLight);
+}
+
+public void setUniform(String uniformName, SpotLight[] spotLights) {
+    int numLights = spotLights != null ? spotLights.length : 0;
+    for (int i = 0; i < numLights; i++) {
+        setUniform(uniformName, spotLights[i], i);
+    }
+}
+
+public void setUniform(String uniformName, SpotLight spotLight, int pos) {
+    setUniform(uniformName + "[" + pos + "]", spotLight);
+}
+
+

最后,我们只需要更新Render类来接收点光源和聚光源列表,并相应地修改DummyGame类以创建这些列表,最终效果如下所示。

+

多光源

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/12-game-hud/index.html b/12-game-hud/index.html new file mode 100755 index 0000000..3c265a9 --- /dev/null +++ b/12-game-hud/index.html @@ -0,0 +1,766 @@ + + + + + + + + + + + + 游戏HUD - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

游戏HUD(Game HUD)

+

在本章中,我们将为游戏创建一个HUD(Heads-Up Display,平视显示器)。换句话说,就是一组用于在三维场景上,随时显示相关信息的二维图形和文本。本例中将创建一个简单的HUD,这可为我们展现一些显示信息的基本技术。

+

当你查阅本章的源代码时,还将发现我们对源代码做了一些小的重构,特别是Renderer类,以便为HUD渲染做好准备。

+

文本渲染

+

创建HUD所要做的第一件事是渲染文本。为了实现它,我们将储存字符的纹理映射到一个方形中,该方形将被分割为一组表示每个字符的片段。之后,我们将使用该纹理在屏幕上绘制文本。所以首先创建含有所有字母的纹理,这项工作可以使用很多软件来做,例如CBFGF2IBuilder等等。本例使用Codehead的位图字体生成器(Codehead’s Bitmap Font Generator,CBFG)。

+

CBFG允许你配置很多选项,如纹理大小、字体类型、要使用的抗锯齿等等。下图是本例将用来生成纹理文件的配置。在本章中,我们将假设文本编码为ISO-8859-1,如果你需要处理其他的字符集,则需要稍微修改代码。

+

CBFG配置

+

当设置好CBFG的所有选项后,可以将其导出为多种图片格式。现在我们将其导出为BMP文件,然后再转换为PNG文件,以便将其作为纹理加载。当转换为PNG格式时,我们也可以将黑色背景设置为透明,也就是说,我们将黑色设为Alpha值等于0(可以使用GIMP这样的工具来实现)。最终你会看到类似下图所示的结果。

+

字体纹理

+

如你所见,图像中的所有字符都以行和列的形式排列。在本例中,图像由15列和17行字符组成。通过使用特定字符的编号,我们可以计算其对应储存在图像中的行和列。所在列的计算方法为:,其中是取余运算符,所在行的计算方法为:。在本例中我们将整数除以整数,以便忽略小数部分。

+

我们将创建一个名为TextItem的新类,它将储存渲染文本所需的图元。这是一个不考虑多行文本的简化实现,但是它能在HUD中显示文本信息。下列代码是该类的声明与构造函数:

+
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方法:

+
private Mesh buildMesh(Texture texture, int numCols, int numRows) {
+    byte[] chars = text.getBytes(Charset.forName("ISO-8859-1"));
+    int numChars = chars.length;
+
+    List<Float> positions = new ArrayList<>();
+    List<Float> textCoords = new ArrayList<>();
+    float[] normals   = new float[0];
+    List<Integer> indices   = new ArrayList<>();
+
+    float tileWidth = (float)texture.getWidth() / (float)numCols;
+    float tileHeight = (float)texture.getHeight() / (float)numRows;
+
+

代码创建了用于储存Mesh的位置、纹理坐标、法线和索引的数据结构。现在我们不使用光照,因此法线数列是空的。我们要做的是构造一组字符片段,每个字符片段代表一个字符。我们还需要根据每个片段对应的字符来分配对应的纹理坐标。下图展现了文本矩形和字符片段的关系:

+

文本矩形

+

因此,对于每个字符,我们需要创建由两个三角形构成的字符片段,这两个三角形可以用四个顶点(V1、V2、V3和V4)定义。第一个三角形(左下角的那个)的索引为(0, 1, 2),而第二个三角形(右上角的那个)的索引为(3, 0, 2)。纹理坐标是基于与纹理图像中每个字符相关的行列计算的,纹理坐标的范围为[0, 1],所以我们只需要将当前行或当前列除以总行数和总列数就可以获得V1的坐标。对于其他顶点,我们只需要适当加上行宽或列宽就可以得到对应坐标。

+

下述的循环语句块创建了与渲染文本矩形相关的所有顶点、纹理坐标和索引。

+
for(int i=0; i<numChars; i++) {
+    byte currChar = chars[i];
+    int col = currChar % numCols;
+    int row = currChar / numCols;
+
+    // 构造由两个三角形组成的字符片段
+
+    // 左上角的顶点
+    positions.add((float)i*tileWidth); // x
+    positions.add(0.0f); //y
+    positions.add(ZPOS); //z
+    textCoords.add((float)col / (float)numCols );
+    textCoords.add((float)row / (float)numRows );
+    indices.add(i*VERTICES_PER_QUAD);
+
+    // 左下角的顶点
+    positions.add((float)i*tileWidth); // x
+    positions.add(tileHeight); //y
+    positions.add(ZPOS); //z
+    textCoords.add((float)col / (float)numCols );
+    textCoords.add((float)(row + 1) / (float)numRows );
+    indices.add(i*VERTICES_PER_QUAD + 1);
+
+    // 右下角的顶点
+    positions.add((float)i*tileWidth + tileWidth); // x
+    positions.add(tileHeight); //y
+    positions.add(ZPOS); //z
+    textCoords.add((float)(col + 1)/ (float)numCols );
+    textCoords.add((float)(row + 1) / (float)numRows );
+    indices.add(i*VERTICES_PER_QUAD + 2);
+
+    // 右上角的顶点
+    positions.add((float)i*tileWidth + tileWidth); // x
+    positions.add(0.0f); //y
+    positions.add(ZPOS); //z
+    textCoords.add((float)(col + 1)/ (float)numCols );
+    textCoords.add((float)row / (float)numRows );
+    indices.add(i*VERTICES_PER_QUAD + 3);
+
+    // 添加左上角和右下角顶点的索引
+    indices.add(i*VERTICES_PER_QUAD);
+    indices.add(i*VERTICES_PER_QUAD + 2);
+}
+
+

上述代码中需要注意的一些事项:

+
    +
  • 我们将使用屏幕坐标来表示顶点(记住屏幕坐标系的原点位于屏幕左上角)。三角形上顶点的Y坐标小于三角形下顶点的Y坐标。
  • +
  • 我们不缩放图形,因此每个字符片段的X距离就等于字符宽度,三角形的高度就是每个字符的高度。这是因为本例希望尽可能地使渲染文本与原始纹理相似(不管怎样,我们可以稍后缩放它,因为TextItem类继承了GameItem类)。
  • +
  • Z坐标为固定值,因为它与绘制该图像无关。
  • +
+

下图展示了一些顶点的坐标:

+

文本矩形坐标

+

为什么我们使用屏幕坐标系?首先,因为本例将在HUD中渲染2D对象,并且通常这样使用它们更方便。其次,我们将使用正交投影(Orthographic Projection)绘制它们,稍后再解释什么是正交投影。

+

TextItem类最后还需添加一些方法,以获取文本并在运行时更改文本。每当文本被更改时,需要清理此前的VAO(储存在Mesh实例中)并创建一个新的VAO。我们不需要删除纹理,所以在Mesh类中添加了一个新方法来删除这些数据。

+
public String getText() {
+    return text;
+}
+
+public void setText(String text) {
+    this.text = text;
+    Texture texture = this.getMesh().getMaterial().getTexture();
+    this.getMesh().deleteBuffers();
+    this.setMesh(buildMesh(texture, numCols, numRows));
+}
+
+

既然我们已经建立了渲染文本所需要的基础结构,接下来该怎么做呢?首先如此前章节所述,渲染三维场景,然后在其上渲染二维的HUD。为了渲染HUD,我们将使用正交投影(也称为正交投影)。正交投影是三维物体的一种二维表示方式,你可能已经在三维模型的蓝图中看到了一些例子,它们用来表示某些物体从顶部或某些侧面看到的样子。下图是圆柱体从顶部和前面的正交投影:

+

正交投影

+

这种投影对于绘制二维物体是非常方便的,因为它“忽略”了Z坐标的值,也就是说,忽略了到屏幕的距离。使用这种投影,物体大小不会随着距离的增大而减小(不同于透视投影)。为了使用正交投影投影物体,我们需要使用另一个矩阵,即正交投影矩阵,正交投影矩阵的公式如下所示:

+

正交投影矩阵

+

这个矩阵还矫正了失真,因为我们的窗口并不总是完美的正方形,而是一个矩形。rightbottom是屏幕大小,而lefttop是原点坐标。正交投影矩阵用于将屏幕坐标转换为三维空间坐标。下图展现了该投影的映射过程:

+

正交投影示例

+

该矩阵将允许我们使用屏幕坐标。

+

我们现在可以继续实现HUD了。接下来要做的是创建另一组着色器,一个顶点着色器和一个片元着色器,来绘制HUD。顶点着色器很简单:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+
+uniform mat4 projModelMatrix;
+
+void main()
+{
+    gl_Position = projModelMatrix * vec4(position, 1.0);
+    outTexCoord = texCoord;
+}
+
+

它仅接收顶点坐标、纹理坐标、索引和法线,并将使用矩阵将它们转换为三维空间坐标,该矩阵是正交投影矩阵与模型矩阵的乘积,即。由于我们没有在世界坐标系中对坐标做任何处理,所以在Java代码中将两个矩阵相乘比在着色器中相乘更高效。这样,我们只需为每个项目做一次矩阵乘法运算,而不是为每个顶点做一次。还应记住顶点应该用屏幕坐标表示。

+

片元着色器也很简单:

+
#version 330
+
+in vec2 outTexCoord;
+in vec3 mvPos;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+uniform vec4 colour;
+
+void main()
+{
+    fragColor = colour * texture(texture_sampler, outTexCoord);
+}
+
+

它只是将基本颜色与纹理颜色相乘,这样可以改变渲染文本的颜色,而不需要创建多个纹理文件。现在既然已经创建了一组新的着色器,就可以在Renderer类中使用它们。但在此之前,我们要创建一个名为IHud的接口,该接口储存所有要在HUD上显示的元素,并提供一个默认的cleanup方法。

+
package org.lwjglb.engine;
+
+public interface IHud {
+
+    GameItem[] getGameItems();
+
+    default void cleanup() {
+        GameItem[] gameItems = getGameItems();
+        for (GameItem gameItem : gameItems) {
+            gameItem.getMesh().cleanUp();
+        }
+    }
+}
+
+

通过该接口,不同的游戏可以定义自定义的HUD,而不需要改变渲染机制。现在回到Renderer类,顺便一提,它已经被移动到engine.graph包下,因为现在它的通用性足以不依赖任何游戏的具体实现了。在Renderer类中,我们添加了一个新的方法来创建、连接和初始化一个新的ShaderProgram,以便使用上述所示的着色器。

+
private void setupHudShader() throws Exception {
+    hudShaderProgram = new ShaderProgram();
+    hudShaderProgram.createVertexShader(Utils.loadResource("/shaders/hud_vertex.vs"));
+    hudShaderProgram.createFragmentShader(Utils.loadResource("/shaders/hud_fragment.fs"));
+    hudShaderProgram.link();
+
+    // 为正交投影模型矩阵和颜色创建Uniform
+    hudShaderProgram.createUniform("projModelMatrix");
+    hudShaderProgram.createUniform("colour");
+}
+
+

render方法首先会调用renderScene方法,其中包含了此前章节所述的渲染三维场景的代码,然后调用一个名为renderHud的新方法,用于渲染HUD。

+
public void render(Window window, Camera camera, GameItem[] gameItems,
+    SceneLight sceneLight, IHud hud) {
+
+    clear();
+
+    if ( window.isResized() ) {
+        glViewport(0, 0, window.getWidth(), window.getHeight());
+        window.setResized(false);
+    }
+
+    renderScene(window, camera, gameItems, sceneLight);
+
+    renderHud(window, hud);
+}
+
+

renderHud方法实现如下:

+
private void renderHud(Window window, IHud hud) {
+
+    hudShaderProgram.bind();
+
+    Matrix4f ortho = transformation.getOrthoProjectionMatrix(0, window.getWidth(), window.getHeight(), 0);
+    for (GameItem gameItem : hud.getGameItems()) {
+        Mesh mesh = gameItem.getMesh();
+        // HUD元素的正交投影矩阵与模型矩阵相乘
+        Matrix4f projModelMatrix = transformation.getOrtoProjModelMatrix(gameItem, ortho);
+        hudShaderProgram.setUniform("projModelMatrix", projModelMatrix);
+        hudShaderProgram.setUniform("colour", gameItem.getMesh().getMaterial().getAmbientColour());
+
+        // 渲染此HUD项目的网格
+        mesh.render();
+    }
+
+    hudShaderProgram.unbind();
+}
+
+

上述代码中,我们遍历了HUD的所有元素,并将与每个元素关联的模型矩阵和正交投影矩阵相乘。正交投影矩阵在每次render调用时被刷新(因为屏幕大小可以被改变),并且通过如下方式计算:

+
public final Matrix4f getOrthoProjectionMatrix(float left, float right, float bottom, float top) {
+    orthoMatrix.identity();
+    orthoMatrix.setOrtho2D(left, right, bottom, top);
+    return orthoMatrix;
+}
+
+

我们将在game包中创建一个Hud类,它实现了IHud接口,并在构造函数接收一个文本,用于在其中创建TextItem实例。

+
package org.lwjglb.game;
+
+import org.joml.Vector4f;
+import org.lwjglb.engine.GameItem;
+import org.lwjglb.engine.IHud;
+import org.lwjglb.engine.TextItem;
+
+public class Hud implements IHud {
+
+    private static final int FONT_COLS = 15;
+
+    private static final int FONT_ROWS = 17;
+
+    private static final String FONT_TEXTURE = "textures/font_texture.png";
+
+    private final GameItem[] gameItems;
+
+    private final TextItem statusTextItem;
+
+    public Hud(String statusText) throws Exception {
+        this.statusTextItem = new TextItem(statusText, FONT_TEXTURE, FONT_COLS, FONT_ROWS);
+        this.statusTextItem.getMesh().getMaterial().setColour(new Vector4f(1, 1, 1, 1));
+        gameItems = new GameItem[]{statusTextItem};
+    }
+
+    public void setStatusText(String statusText) {
+        this.statusTextItem.setText(statusText);
+    }
+
+    @Override
+    public GameItem[] getGameItems() {
+        return gameItems;
+    }
+
+    public void updateSize(Window window) {
+        this.statusTextItem.setPosition(10f, window.getHeight() - 50f, 0);
+    }
+}
+
+

DummyGame类中我们创建该类的实例,并用默认文本初始化它,最后得到如下所示的结果:

+

文本渲染结果

+

Texture类中可以通过修改纹理的过滤方式来提升文本的可读性(如果需要文本缩放的话需要注意此事)。

+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+

但是本例还没有做完。如果用缩放使文本与立方体重叠时,就会看到如下效果:

+

背景不透明的文本

+

绘制的文本背景不透明。为了实现背景透明,我们必须明确启用混合(Blend),这样就可以使用Alpha分量。本例将在Window类中用下述代码设置其初始化参数:

+
// 支持透明
+glEnable(GL_BLEND);
+glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+

现在你可以看到文本以透明背景绘制了。

+

透明背景的文本

+

完善HUD

+

现在我们已经渲染了一些文本,但还可以向HUD添加更多的元素。本例将添加一个根据摄像机朝向旋转的指针。现在我们将向Hud类添加一个新的GameItem,它将包含一个指针的模型网格。

+

指针

+

指针的模型是.obj文件,但它不会关联任何纹理,相反,它只有一个基础色。所以需要修改HUD的片元着色器,以确认是否使用纹理。本例将通过设置一个名为hasTexture的新Uniform来实现它。

+
#version 330
+
+in vec2 outTexCoord;
+in vec3 mvPos;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+uniform vec4 colour;
+uniform int hasTexture;
+
+void main()
+{
+    if ( hasTexture == 1 )
+    {
+        fragColor = colour * texture(texture_sampler, outTexCoord);
+    }
+    else
+    {
+        fragColor = colour;
+    }
+}
+
+

要将指针添加到HUD上,只需在Hud类中创建一个新的GameItem实例。它加载指针模型,并将其添加到项目列表中。在本例中需要放大指针,因为它在屏幕坐标系中渲染,所以通常你需要放大它。

+
// 创建指针
+Mesh mesh = OBJLoader.loadMesh("/models/compass.obj");
+Material material = new Material();
+material.setAmbientColour(new Vector4f(1, 0, 0, 1));
+mesh.setMaterial(material);
+compassItem = new GameItem(mesh);
+compassItem.setScale(40.0f);
+// 旋转以将其变换到屏幕坐标系
+compassItem.setRotation(0f, 0f, 180f);
+
+// 创建一个数组,用于储存HUD项目
+gameItems = new GameItem[]{statusTextItem, compassItem};
+
+

还要注意,为了使指针指向上方,我们需要将其旋转180°,因为模型通常倾向于使用OpenGL空间坐标系。如果我们要使用屏幕坐标,它会指向下方。Hud类还提供一个方法来更新指针的指向,该方法也必须考虑到这一点。

+
public void rotateCompass(float angle) {
+    this.compassItem.setRotation(0, 0, 180 + angle);
+}
+
+

DummyGame类中,每当摄像机移动时,我们需要用Y角旋转更新角度。

+
// 根据鼠标更新摄像机            
+if (mouseInput.isRightButtonPressed()) {
+    Vector2f rotVec = mouseInput.getDisplVec();
+    camera.moveRotation(rotVec.x * MOUSE_SENSITIVITY, rotVec.y * MOUSE_SENSITIVITY, 0);
+
+    // 更新HUD指针
+    hud.rotateCompass(camera.getRotation().y);
+}
+
+

我们会看到这样的结果(记住这只是个示例,在实际的游戏中,你可能想使用一些纹理设置指针的外观)。

+

有指针的HUD

+

再谈文本渲染

+

在回顾其他主题之前,让我们再谈谈之前介绍的文本渲染方法。该方案非常简洁地介绍了渲染HUD所涉及的概念,但它存在一些问题:

+
    +
  • 它不支持非拉丁字符。
  • +
  • 如果你想使用多种字体,则需要为每种字体创建单独的纹理文件。此外,改变文本大小的唯一方法是缩放,这会导致渲染文本的质量较差,或者需要创建额外的纹理文件。
  • +
  • 最重要的是,大多数字体中的字符之间的大小并不同,而我们将字体纹理分割成同样大小的元素。我们使用了Monospaced风格(即所有字符具有相同的宽度)的“Consolas”字体,但如果使用非Monospaced的字体,就会看到字符之间恼人的空白。
  • +
+

我们需要更改方法,并提供一种更灵活的渲染文本方式。如果你仔细想想,整体想法是可行的,也就是通过单独渲染每个字符的矩形来渲染文本。这里的问题就是该如何生成纹理,我们需要通过系统中可用的字体动态地生成这些纹理。

+

这就需要java.awt.Font出手了,我们将通过指定字体系列和大小动态地绘制每一个字符来生成纹理。该纹理的使用方式与之前描述的相同,但它将完美地解决上述所有问题。我们将创建一个名为FontTexture的新类,该类将接收Font实例和字符集名称,并将动态地创建包含所有可用字符的纹理。构造函数如下所示:

+
public FontTexture(Font font, String charSetName) throws Exception {
+    this.font = font;
+    this.charSetName = charSetName;
+    charMap = new HashMap<>();
+
+    buildTexture();
+}
+
+

首先要处理非拉丁字符问题。给定字符集和字体,我们将构建一个包含所有可渲染字符的String

+
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();
+}
+
+

让我们来看看实际创建纹理的buildTexture方法:

+
private void buildTexture() throws Exception {
+    // 使用FontMetrics获取每个字符的信息
+    BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+    Graphics2D g2D = img.createGraphics();
+    g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+    g2D.setFont(font);
+    FontMetrics fontMetrics = g2D.getFontMetrics();
+
+    String allChars = getAllAvailableChars(charSetName);
+    this.width = 0;
+    this.height = fontMetrics.getHeight();
+    for (char c : allChars.toCharArray()) {
+        // 获取每个字符的大小,并更新图像的大小
+        CharInfo charInfo = new CharInfo(width, fontMetrics.charWidth(c));
+        charMap.put(c, charInfo);
+        width += charInfo.getWidth() + CHAR_PADDING;
+    }
+    g2D.dispose();
+
+

我们首先通过创建临时图像来获得FontMetrics,然后遍历包含所有可用字符的String,并在FontMetrics的帮助下获取每个字符的宽度。我们将这些信息储存在一个charMap映射上,以字符作为映射的键。这样,我们就确定了纹理图像的大小(图像的高度等于所有字符的最大高度,而宽度等于所有字符的宽度总和)。ChatSet是一个内部类,它储存有关字符的信息(它的宽度和它在纹理图像中的起点)。

+
    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;
+        }
+    }
+
+

然后,我们将创建一个储存所有可用字符的图像,只需在BufferedImage上绘制字符串即可。

+
    // 创建与字符集相关的图像
+    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);
+    int startX = 0;
+    for (char c : allChars.toCharArray()) {
+        CharInfo charInfo = charMap.get(c);
+        g2D.drawString("" + c, startX, fontMetrics.getAscent());
+        startX += charInfo.getWidth() + CHAR_PADDING;
+    }
+    g2D.dispose();
+
+

我们正在生成一个包含所有字符的单行图像(可能不满足纹理大小应该为二的幂的前提,但是它仍适用于大多数现代显卡。在任何情况下,你都可以通过增加额外的空白来解决这个问题)。你也可以查看生成的图像,在上述代码之后,添加下述的一行代码:

+
ImageIO.write(img, IMAGE_FORMAT, new java.io.File("Temp.png"));
+
+

图像将被写入一个临时文件。该文件将包含一个在白色背景下,使用抗锯齿绘制所有可用字符的长条。

+

字体纹理

+

最后只需要从该图像创建一个Texture实例,我们只需使用PNG格式(这就是Texture类所期望的)转储图像字节。

+
    ByteBuffer buf = null;
+    try ( ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+        ImageIO.write(img, IMAGE_FORMAT, out);
+        out.flush();
+        byte[] data = out.toByteArray();
+        buf = ByteBuffer.allocateDirect(data.length);
+        buf.put(data, 0, data.length);
+        buf.flip();
+    }
+
+    texture = new Texture(buf);
+}
+
+

你可能注意到,我们已经稍微修改了Texture类,使其具有一个接收ByteBuffer的构造函数。其中这个新的构造函数使用stbi_load_from_memory加载图片。现在我们只需更改TextItem类,以便在其构造函数中接收FontTexture实例。

+
public TextItem(String text, FontTexture fontTexture) throws Exception {
+    super();
+    this.text = text;
+    this.fontTexture = fontTexture;
+    setMesh(buildMesh());
+}
+
+

只需要在设置片段坐标和纹理坐标时稍稍修改buildMesh方法,下述代码是其中一个顶点的示例:

+
    float startx = 0;
+    for(int i=0; i<numChars; i++) {
+        FontTexture.CharInfo charInfo = fontTexture.getCharInfo(characters[i]);
+
+        // 构造由两个三角形组成的字符片段
+
+        // 左上角顶点
+        positions.add(startx); // x
+        positions.add(0.0f); // y
+        positions.add(ZPOS); // z
+        textCoords.add( (float)charInfo.getStartX() / (float)fontTexture.getWidth());
+        textCoords.add(0.0f);
+        indices.add(i*VERTICES_PER_QUAD);
+
+      // 更多代码...
+      startx += charInfo.getWidth();
+    }
+
+

你可以在源代码中查阅其他更改。下图是一个大小为20的Arial字体的渲染效果:

+

改进后的文本

+

如你所见文本渲染的质量已经有了很大的提升,你可以用不同的字体和大小来渲染。这仍然有很大的改进空间(如支持多行文本、特效等),但这将留给各位读者作为练习。

+

你可能还注意到,我们仍然能够缩放文本(通过着色器中的模型观察矩阵)。现在的文本可能不需要缩放,但对其他的HUD元素可能会有用。

+

我们已经确立了所有的基本数据结构,以便为游戏创建一个HUD。现在,只剩一个问题,那就是创建所有的元素,传递相关信息给用户,并给他们一个专业的外观。

+

OSX

+

如果你试图运行本章中的示例,以及下一个渲染文本的示例,则可能会发现应用程序阻塞,屏幕上不会显示任何内容。这是由于AWT和GLFW在OSX下相处得不太好,但这和AWT有什么关系呢?我们使用的是Font类,它属于AWT,如果要实例化它,AWT也需要初始化。在OSX中,AWT试图在主线程运行,但GLFW也需要在主线程运行,这就是造成此问题的原因。

+

为了能够使用Font类,GLFW必须在AWT之前初始化,并且示例需要以Headless模式运行。你需要在任何东西被初始化之前设置此属性:

+
System.setProperty("java.awt.headless", "true");
+
+

你也许会收到一个警告,但示例成功运行了。

+

一个更简洁的方法是使用stb库来渲染文本。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/13-sky-box-and-some-optimizations/index.html b/13-sky-box-and-some-optimizations/index.html new file mode 100755 index 0000000..67d29a2 --- /dev/null +++ b/13-sky-box-and-some-optimizations/index.html @@ -0,0 +1,530 @@ + + + + + + + + + + + + 天空盒与一些优化 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

天空盒与一些优化 (Sky Box and some optimizations)

+

天空盒

+

天空盒(Sky Box)将用于为三维世界设置背景,给人一种世界广阔的错觉。这个背景将摄像机所处位置包起来,罩住整个空间。我们要做的是构造一个渲染在三维场景周围的立方体,即摄像机的位置将位于立方体的中心。该立方体的面将包着一层纹理,纹理上有山、蓝天和云彩,它将以一种图像看起来像是一个连续的风景的方式进行映射。

+

下图展现了天空盒的概念。

+

天空盒

+

创建天空盒的过程可概括为以下几步:

+
    +
  • 创建一个大立方体。
  • +
  • 对它应用纹理,产生一种看到没有边缘的场景的错觉。
  • +
  • 渲染立方体,它的原点位于摄像机所处的位置,它的面距离原点很远。
  • +
+

让我们先从纹理开始。你会发现互联网上有很多预先生成的纹理可供使用,本章示例中使用的纹理从此处下载:http://www.custommapmakers.org/skyboxes.php。本章中具体使用的纹理是:http://www.custommapmakers.org/skyboxes/zips/ely_hills.zip,作者是Colin Lowndes。

+

该网站的纹理都是由单独的TGA文件组成,每个文件都是立方体的一面。我们的纹理加载器希望文件格式为PNG,所以需要将每一面的图像组合成一个PNG图像。我们可以使用其他方法,例如立方体映射(Cube Mapping),使其自动应用纹理。但为了使本章尽可能简洁易懂,你必须手动把它们排列为一张图片,最终图像如下所示:

+

天空盒纹理

+

接下来需要创建一个OBJ文件,其中含有一个立方体,正确地设置每一面的纹理坐标。下图展现了纹理与立方体各面的关系(你可以在本书源代码中找到本章使用的OBJ文件):

+

天空盒立方体的面

+

当相关资源准备就绪,就可以开始编写代码了。首先创建一个名为SkyBox的新类,它的构造函数接收OBJ模型路径和天空盒纹理文件路径。如同此前一章的HUD,这个类也继承GameItem类。为什么它要继承GameItem类?首先,为了方便我们重用大部分处理网格和纹理的代码;第二,因为天空盒不会移动,我们只想应用旋转和缩放。这样想想SkyBox确实是一个GameItemSkyBox类的实现如下:

+
package org.lwjglb.engine;
+
+import org.lwjglb.engine.graph.Material;
+import org.lwjglb.engine.graph.Mesh;
+import org.lwjglb.engine.graph.OBJLoader;
+import org.lwjglb.engine.graph.Texture;
+
+public class SkyBox extends GameItem {
+
+    public SkyBox(String objModel, String textureFile) throws Exception {
+        super();
+        Mesh skyBoxMesh = OBJLoader.loadMesh(objModel);
+        Texture skyBoxtexture = new Texture(textureFile);
+        skyBoxMesh.setMaterial(new Material(skyBoxtexture, 0.0f));
+        setMesh(skyBoxMesh);
+        setPosition(0, 0, 0);
+    }
+}
+
+

如果你查阅本章的源代码,你会发现我们做了一些重构。我们创建了一个名为Scene的类,它将整理与三维世界相关的所有数据。Scene类的定义和属性如下所示,其中包含SkyBox类的实例:

+
package org.lwjglb.engine;
+
+public class Scene {
+
+    private GameItem[] gameItems;
+
+    private SkyBox skyBox;
+
+    private SceneLight sceneLight;
+
+    public GameItem[] getGameItems() {
+        return gameItems;
+    }
+
+    // 更多代码...
+
+

接下来是为天空盒创建另一组顶点和片元着色器,但为什么不重用已有的场景着色器呢?实际上,我们所需的着色器是原有着色器的简化版,不需要让光照影响天空盒(更准确的说,我们不需要点光源,聚光源和平行光)。天空盒的顶点着色器如下所示:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    outTexCoord = texCoord;
+}
+
+

可以看到我们仍使用模型观察矩阵。如上所述,我们将缩放天空盒,所以需要变换矩阵。你可能会发现到一些其他实现,在初始化时就放大了立方体模型的大小,并且不需要将模型矩阵和观察矩阵相乘。但我们选择前一种方法,因为它更灵活,它允许在运行时改变天空盒的大小。但如果你想,你就可以轻松改换到另一种方法。

+

片元着色器也非常简单:

+
#version 330
+
+in vec2 outTexCoord;
+in vec3 mvPos;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+uniform vec3 ambientLight;
+
+void main()
+{
+    fragColor = vec4(ambientLight, 1) * texture(texture_sampler, outTexCoord);
+}
+
+

如你所见,我们为着色器添加了一个环境光,该Uniform的目的是修改天空盒的颜色以模拟昼夜(如果不这样做,在世界的其他地方都是黑暗的时候,天空盒看起来就像是在中午)。

+

Renderer类中,我们刚刚添加了新的方法来使用这些着色器并设置Uniform(这不是什么新的概念)。

+
private void setupSkyBoxShader() throws Exception {
+    skyBoxShaderProgram = new ShaderProgram();
+    skyBoxShaderProgram.createVertexShader(Utils.loadResource("/shaders/sb_vertex.vs"));
+    skyBoxShaderProgram.createFragmentShader(Utils.loadResource("/shaders/sb_fragment.fs"));
+    skyBoxShaderProgram.link();
+
+    skyBoxShaderProgram.createUniform("projectionMatrix");
+    skyBoxShaderProgram.createUniform("modelViewMatrix");
+    skyBoxShaderProgram.createUniform("texture_sampler");
+    skyBoxShaderProgram.createUniform("ambientLight");
+}
+
+

当然,我们需要在全局渲染中为渲染天空盒创建一个新的渲染方法。

+
private void renderSkyBox(Window window, Camera camera, Scene scene) {
+    skyBoxShaderProgram.bind();
+
+    skyBoxShaderProgram.setUniform("texture_sampler", 0);
+
+    // 更新投影矩阵
+    Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOV, window.getWidth(), window.getHeight(), Z_NEAR, Z_FAR);
+    skyBoxShaderProgram.setUniform("projectionMatrix", projectionMatrix);
+    SkyBox skyBox = scene.getSkyBox();
+    Matrix4f viewMatrix = transformation.getViewMatrix(camera);
+    viewMatrix.m30(0);
+    viewMatrix.m31(0);
+    viewMatrix.m32(0);
+    Matrix4f modelViewMatrix = transformation.getModelViewMatrix(skyBox, viewMatrix);
+    skyBoxShaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+    skyBoxShaderProgram.setUniform("ambientLight", scene.getSceneLight().getAmbientLight());
+
+    scene.getSkyBox().getMesh().render();
+
+    skyBoxShaderProgram.unbind();
+}
+
+

上述方法与其他渲染方法非常相似,但有一个不同之处需要解释。如你所见,我们像平常一样传递投影矩阵和模型观察矩阵。但当取得观察矩阵时,我们将其一些数值设置为0。为什么要这么做?其中的原因是我们不希望天空盒被移动。

+

请记住,当我们在移动摄像机时,实际上是在移动整个世界。因此,如果直接将观察矩阵与模型矩阵相乘,当摄像机移动时,天空盒也将移动。但是我们不想这样,我们想把它固定在坐标原点(0, 0, 0)。这是通过将观察矩阵的位移增量部分(m30, m31m32)设置为0来实现的。

+

你可能会认为可以避免使用观察矩阵,因为天空盒必须固定在原点。但在此情况下,你会看到天空盒不会随着摄像机旋转,这不是我们想要的。因此我们需要旋转它但不要移动它。

+

这就是全部内容,你可以查阅本章源代码,本例在DummyGame类中创建了更多的方块实例来模拟地面和天空盒。你也可以改变环境光来模拟光照和昼夜交替。最终得到如下所示的效果:

+

天空盒效果

+

天空盒是一个小立方体(在实际的游戏中,它应该大得多),所以你在世界中移动时,可以很容易地看到其效果。你还可以看到,构成地面的方块比天空盒大,所以当你移动时,你会看到从山中冒出的方块。这很明显,因为我们设置的天空盒相对较小。但无论如何,我们需要通过添加一个隐藏和模糊远处的物体的效果(例如使用雾效果)来减轻这种影响。

+

不创建更大的天空盒的另一个原因是我们需要几个优化来提升效率(稍后对此进行解释)。

+

可以注释渲染方法中防止天空盒移动的代码,然后你就可以在天空盒外看到如下所示图像:

+

移动的天空盒

+

虽然这不是天空盒该做的,但这可以帮助你理解天空盒的概念。请记住,这是一个简单的示例,你可以通过增加其他效果来改善它,比如太阳在天穹移动或云层移动。此外,为了创建更大的世界,你需要将世界分割成区域,只加载那些与玩家所处的区域相邻的区域。

+

另外值得一提的是:什么时候渲染天空盒,在渲染场景之前还是之后?在渲染场景后渲染天空盒更优,因为由于深度测试,大多数片元将被丢弃。当OpenGL尝试渲染它们,并且启用了深度测试时,将丢弃一些位于先前渲染的片元之后的片元,这些片元的深度值更低。所以答案很明显,对吧?渲染场景后,再渲染天空盒。

+

上述方法的问题是在处理透明纹理时,如果在场景中我们有应用透明纹理的物体,它们将使用“背景”色绘制,本例中是黑色。但如果我们先渲染天空盒,那么将会正确地应用透明效果。

+

那么,我们应该在渲染场景之前渲染天空盒吗?关键在于你的选择。如果你在渲染场景前渲染天空盒,可以解决透明纹理问题,但是会影响性能。实际上,就算没有天空盒,你也可能面临透明问题。假设你有一个透明的物体,它与远处的物体重叠,如果首先渲染透明对象,那么也会出现透明问题。因此,也许另一种方法是在所有其他项被渲染后,单独绘制透明的项。这是一些商业游戏使用的方法。不过现在我们在渲染场景之后渲染天空盒,以获得更高的性能。

+

一些优化

+

从上述示例来看,天空盒相对较小使得其效果有点奇怪(你可以看到物体神奇地从山体出现)。所以让我们增加天空盒的大小和世界的大小,将天空盒的大小放大50倍,这样世界由40,000个游戏项实例(方块)组成。

+

如果你更改缩放并重新运行这个示例,你会发现开始出现性能问题,并且在世界中移动不顺畅。现在是时候关注一下性能了(你可能听过一句老话:“过早的优化是万恶之源”,但是从本章开始,我希望没有人会说这是过早的)。

+

让我来介绍一个概念,它能减少正在渲染的数据数量,它叫做面剔除(Face Culling)。在示例中,我们渲染了成千上万个立方体,立方体是由六个面组成的。我们正在渲染每个立方体的六个面,即使它们有些是看不到的。进入一个立方体内部,你会看到它的内部如下图所示:

+

立方体内部

+

不能被看到的面应该立即舍弃,这就是面剔除的作用。实际上,对于一个立方体,你最多只能同时看到三个面,所以只能通过使用面剔除(如果你的游戏不要求你进入模型的内部,这样做才是高效的,稍后你就可以看到效果)来舍弃一半的面(40,000×3×2个三角形)。

+

面剔除检查每个三角形是否面向摄像机,丢弃那些不面向摄像机的三角形。但是,如何知道三角形是否面向摄像机呢?好吧,OpenGL实现它的方式是通过检查组成三角形顶点的环绕顺序。

+

记得从第一章开始,我们可以定义一个三角形的顶点以顺时针或逆时针顺序排列。在OpenGL中,默认情况下以逆时针顺序排列顶点的三角形面向摄像机,而以顺时针顺序排列顶点的三角形面向相反的方向。关键是在考虑摄像机的情况下,检查顶点的环绕顺序。因此,按照逆时针顺序定义的三角形可以渲染。

+

让我们来实现它,在Window类的init方法中添加下述代码:

+
glEnable(GL_CULL_FACE);
+glCullFace(GL_BACK);
+
+

第一行代码将启用面剔除,第二行代码设置背向面为需要剔除的面。如果向上看,你会看到如下图所示的情景:

+

应用面剔除的天空盒

+

发生了什么?如果你查看顶面的顶点顺序,将看到它是按逆时针定义的。但请记住,环绕顺序是以摄像机为视角。实际上,如果你让位移影响天空盒以便能从其上方观察它时,则当你在天空盒外时,将看到顶面再次渲染。

+

从外部看应用面剔除的天空盒

+

让我们来描述一下发生了什么。下图展示了天空盒立方体的顶面三角形中的一个三角形,它由逆时针顺序排列的三个顶点定义:

+

以逆时针顺序定义的顶点

+

但要记住,我们是在天空盒里,如果从内部观察立方体,会看到顶点是按顺时针顺序定义的。

+

从内部看立方体

+

这是因为天空盒被定义为从外部观察。因此,我们需要翻转一些面的定义,以便在启用面剔除时能正确地渲染它。

+

但这仍有更大的优化空间。回顾一下渲染流程,在Rendererrender方法中,我们要做的是遍历GameItem数组并渲染相关的Mesh。对每个GameItem,我们做了下述工作:

+
    +
  1. 设置模型观察矩阵(每个GameItem的唯一值)。
  2. +
  3. 获取GameItem储存的Mesh并绑定纹理,绑定VAO并启用其属性。
  4. +
  5. 执行调用以绘制三角形。
  6. +
  7. 停用纹理和VAO。
  8. +
+

但在现在的游戏中,40,000个GameItem都使用相同的Mesh,而我们一次又一次地重复第二项到第四项的操作。这不是很高效,请记住,对OpenGL函数的每次调用都是有性能开销的本地调用。此外,我们还应该尽量限制OpenGL中的状态变化(绑定和停用纹理、VAO都是状态变化)。

+

我们需要改变开发的方式,围绕网格组织代码结构,因为经常有许多游戏项使用相同的网格。现在我们有一个游戏项数组,每项都指向同一个网格,如下图所示:

+

游戏项数组

+

作为替代,我们将创建一个网格映射表,其中包括储存共享该网格的所有游戏项。

+

网格映射表

+

对于每一个Mesh,渲染步骤将会是:

+
    +
  1. 获取与GameItem相关联的Mesh并绑定Mesh纹理,绑定VAO并启用其属性。
  2. +
  3. 对于每个相关的GameItem: + a. 设置模型观察矩阵(每个GameItem唯一的)。 + b. 调用绘制三角形。
  4. +
  5. 解绑纹理和VAO。
  6. +
+

Scene类中储存下述Map

+
private Map<Mesh, List<GameItem>> meshMap;
+
+

我们仍有setGameItems方法,但我们不只是储存数组,而是构造网格映射表。

+
public void setGameItems(GameItem[] gameItems) {
+    int numGameItems = gameItems != null ? gameItems.length : 0;
+    for (int i=0; i<numGameItems; i++) {
+        GameItem gameItem = gameItems[i];
+        Mesh mesh = gameItem.getMesh();
+        List<GameItem> list = meshMap.get(mesh);
+        if ( list == null ) {
+            list = new ArrayList<>();
+            meshMap.put(mesh, list);
+        }
+        list.add(gameItem);
+    }
+}
+
+

Mesh类现在有一个方法来渲染与其相关的GameItem列表,然后将绑定和解绑代码分为不同的方法。

+
private void initRender() {
+    Texture texture = material.getTexture();
+    if (texture != null) {
+        // 激活第一个纹理库
+        glActiveTexture(GL_TEXTURE0);
+        // 绑定纹理
+        glBindTexture(GL_TEXTURE_2D, texture.getId());
+    }
+
+    // 绘制网格
+    glBindVertexArray(getVaoId());
+    glEnableVertexAttribArray(0);
+    glEnableVertexAttribArray(1);
+    glEnableVertexAttribArray(2);
+}
+
+private void endRender() {
+    // 恢复状态
+    glDisableVertexAttribArray(0);
+    glDisableVertexAttribArray(1);
+    glDisableVertexAttribArray(2);
+    glBindVertexArray(0);
+
+    glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+public void render() {
+    initRender();
+
+    glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
+
+    endRender();
+}
+
+public void renderList(List<GameItem> gameItems, Consumer<GameItem> consumer) {
+    initRender();
+
+    for (GameItem gameItem : gameItems) {
+        // 设置游戏项目所需的渲染数据
+        consumer.accept(gameItem);
+        // 渲染游戏项目
+        glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
+    }
+
+    endRender();
+}
+
+

如你所见,我们仍然保留了旧方法,它返回一个Mesh,这是考虑到如果只有一个GameItem的情况(这可能在其他情况下使用,这就是为什么不移除它)。新的方法渲染一个List<GameItem>,并接受一个Counsumer类型的参数(一个函数,使用了Java8引入的函数式编程),它将用于在绘制三角形之前为每个GameItem设置特定的内容,因为不希望Mesh类与Uniform的名称和设置这些参数时所涉及的参数相耦合,我们将使用它来设置模型观察矩阵。

+

Renderer类中的renderScene方法你可以看到,我们只需遍历网格映射表,并通过Lambda表达式设置模型观察矩阵的Uniform。

+
for (Mesh mesh : mapMeshes.keySet()) {
+    sceneShaderProgram.setUniform("material", mesh.getMaterial());
+    mesh.renderList(mapMeshes.get(mesh), (GameItem gameItem) -> {
+        Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(gameItem, viewMatrix);
+        sceneShaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+    }
+    );
+}
+
+

可以做的另一项优化是,我们在渲染周期中创建了大量对象。特别是,我们创建了太多的Matrix4f实例,它们为每个GameItem实例都保存了一个模型视图矩阵。我们应在Transformation类中创建特定的矩阵,并重用相同的实例。如果你查阅源代码,会看到我们已经更改了方法的名称,getXX方法只返回储存的矩阵实例,并且任何更改矩阵值的方法都称为buildXX,以阐明其用途。

+

我们也避免了每次为矩阵设置Uniform时构造新的FloatBuffer实例,并移除了其他一些无用的实例化操作。有了这些,你现在可以看到更流畅更灵活的渲染了。

+

你可以在源代码中查阅所有细节。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/14-height-maps/index.html b/14-height-maps/index.html new file mode 100755 index 0000000..61910a5 --- /dev/null +++ b/14-height-maps/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + 高度图 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

高度图(Height Maps)

+

本章中我们将学习如何使用高度图创建复杂的地形。在开始前,你会注意到我们做了一些重构。我们创建了一些新的包和移动了一些类以更好地组织它们。你可以在源代码中了解这些改变。

+

所以什么是高度图?高度图是用于生成三维地形的图像,它使用像素颜色来获取表面高度。高度图图像通常是灰度图,它可以由Terragen等软件生成。一张高度图图像看起来就像这样。

+

高度图

+

上图就像你俯视一片陆地一样。利用上图,我们将构建由顶点组成的三角形所组成的网格。每个顶点的高度将根据图像的每个像素的颜色来计算。黑色是最低,白色是最高。

+

我们将为图像的每个像素创建一组顶点,这些顶点将组成三角形,这些三角形将组成下图所示的网格。

+

高度图网格

+

网格将组成一个巨大的四边形,它将会在X和Z轴上渲染,并根据像素颜色来改变它的Y轴高度。

+

高度图坐标系

+

由高度图创建三维地形的过程可概括为以下步骤: +* 加载储存高度图的图像(我们将使用一个BufferedImage实例以获取每个像素)。 +* 为每个图像像素创建一个顶点,其高度基于像素颜色。 +* 将正确的纹理坐标分配给顶点。 +* 设置索引以绘制与顶点相关的三角形。

+

我们将创建一个名为HeightMapMesh的类,该类将基于高度图按以上步骤创建一个Mesh。让我们先看看该类定义的常量:

+
private static final int MAX_COLOUR = 255 * 255 * 255;
+
+

如上所述,我们将基于高度图图像的每个像素的颜色来计算每个顶点的高度。图像通常是灰度图,对于PNG图像来说,这意味着每个像素的每个RGB值可以在0到255之间变化,因此我们有256个值来表示不同的高度。这可能足够了,但如果精度不够,我们可以使用三个RGB值以有更多的值,在此情况下,高度计算范围为0到255^3。我们将使用第二种方法,因此我们不局限于灰度图。

+

接下来的常量是:

+
private static final float STARTX = -0.5f;
+
+private static final float STARTZ = -0.5f;
+
+

网格将由一组顶点(一个顶点对应一个像素)构成,其X和Z坐标的范围如下 +* X轴的范围为[-0.5, 0.5],即[STARTX, -STARTX]。 +* Z轴的范围为[-0.5, 0.5],即[STARTZ, -STARTZ]。

+

不用太过担心这些值,稍后得到的网格可以被缩放以适应世界的大小。关于Y轴,我们将设置minYmaxY两个参数,用于设置Y坐标的最低和最高值。这些参数并不是常数,因为我们可能希望在运行时更改它们,而不使用缩放。最后,地形将包含在范围为[STARTX, -STARTX][minY, maxY][STARTZ, -STARTZ]的立方体内。

+

网格将会在HeightMapMesh类的构造函数中创建,该类的定义如下。

+
public HeightMapMesh(float minY, float maxY, String heightMapFile, String textureFile, int textInc) throws Exception {
+
+

它接收Y轴的最小值和最大值,被用作高度图的图像文件名和要使用的纹理文件名。它还接受一个名为textInc的整数,这稍后再说明。

+

我们在构造函数中做的第一件事就是将高度图图像加载到BufferedImage实例中。

+
this.minY = minY;
+this.maxY = maxY;
+
+PNGDecoder decoder = new PNGDecoder(getClass().getResourceAsStream(heightMapFile));
+int height = decoder.getHeight();
+int width = decoder.getWidth();
+ByteBuffer buf = ByteBuffer.allocateDirect(
+        4 * decoder.getWidth() * decoder.getHeight());
+decoder.decode(buf, decoder.getWidth() * 4, PNGDecoder.Format.RGBA);
+buf.flip();
+
+

然后,我们将纹理文件载入到一个ByteBuffer中,并设置构造Mesh所需的变量。incxincz变量将储存每个顶点的X或Z坐标之间的最小间隔,因此Mesh包含在上述区域中。

+
Texture texture = new Texture(textureFile);
+
+float incx = getWidth() / (width - 1);
+float incz = Math.abs(STARTZ * 2) / (height - 1);
+
+List<Float> positions = new ArrayList();
+List<Float> textCoords = new ArrayList();
+List<Integer> indices = new ArrayList();
+
+

之后,我们将遍历图像,为每个像素创建一个顶点,设置其纹理坐标与索引,以正确地定义组成Mesh的三角形。

+
for (int row = 0; row < height; row++) {
+    for (int col = 0; col < width; col++) {
+        // 为当前位置创建顶点
+        positions.add(STARTX + col * incx); // x
+        positions.add(getHeight(col, row, width, buf)); // y
+        positions.add(STARTZ + row * incz); // z
+
+        // 设置纹理坐标
+        textCoords.add((float) textInc * (float) col / (float) width);
+        textCoords.add((float) textInc * (float) row / (float) height);
+
+        // 创建索引
+        if (col < width - 1 && row < height - 1) {
+            int leftTop = row * width + col;
+            int leftBottom = (row + 1) * width + col;
+            int rightBottom = (row + 1) * width + col + 1;
+            int rightTop = row * width + col + 1;
+
+            indices.add(rightTop);
+            indices.add(leftBottom);
+            indices.add(leftTop);
+
+            indices.add(rightBottom);
+            indices.add(leftBottom);
+            indices.add(rightTop);
+        }
+    }
+}
+
+

创建顶点坐标的过程是不需要解释的。现在先别管为什么我们用一个数字乘以纹理坐标以及如何计算高度。你可以看到,对于每个顶点,我们定义两个三角形的索引(除非现在是最后一行或最后一列)。让我们用一个3×3的图像来想象它们是如何构造的。一个3×3的图像包含9个顶点,每因此有2×4个三角形组成4个正方形。下图展示了网格,每个顶点被命名为Vrc(r:行,c:列)。

+

高度图顶点

+

当处理第一个顶点(V00)时,我们在红色阴影处定义了两个三角形的索引。

+

高度图索引I

+

当处理第二个顶点(V01)时,我们在红色阴影处又定义了两个三角形的索引。但当处理第三个顶点(V02)时,我们不需要定义更多的索引,该行的所有三角形都已被定义。

+

高度图索引II

+

你可以很容易地想到其他顶点的处理过程是如何进行的。现在,一旦创建了所有的顶点位置、纹理坐标和索引,我们就只需要用所有这些数据创建Mesh和相关的Material

+
float[] posArr = Utils.listToArray(positions);
+int[] indicesArr = indices.stream().mapToInt(i -> i).toArray();
+float[] textCoordsArr = Utils.listToArray(textCoords);
+float[] normalsArr = calcNormals(posArr, width, height);
+this.mesh = new Mesh(posArr, textCoordsArr, normalsArr, indicesArr);
+Material material = new Material(texture, 0.0f);
+mesh.setMaterial(material);
+
+

你可以看到,我们根据顶点位置计算法线。在看如何计算法线之前,来看看如何获取高度吧。我们已经创建了一个名为getHeight的方法,它负责计算顶点的高度。

+
private float getHeight(int x, int z, int width, ByteBuffer buffer) {
+    byte r = buffer.get(x * 4 + 0 + z * 4 * width);
+    byte g = buffer.get(x * 4 + 1 + z * 4 * width);
+    byte b = buffer.get(x * 4 + 2 + z * 4 * width);
+    byte a = buffer.get(x * 4 + 3 + z * 4 * width);
+    int argb = ((0xFF & a) << 24) | ((0xFF & r) << 16)
+            | ((0xFF & g) << 8) | (0xFF & b);
+    return this.minY + Math.abs(this.maxY - this.minY) * ((float) argb / (float) MAX_COLOUR);
+    }
+
+

该方法接受像素的X和Y坐标,图像的宽度以及与之相关的ByteBuffer,返回RGB颜色(R、G、B分量之和)并计算包含在minYmaxY之间的值(minY为黑色,maxY为白色)。

+

你可以使用BufferedImage来编写一个更简单的方法,它有更方便的方法来获得RGB值,但这将使用AWT。记住AWT不能很好的兼容OSX,所以尽量避免使用它的类。

+

现在来看看如何计算纹理坐标。第一个方法是将纹理覆盖整个网格,左上角的顶点纹理坐标为(0, 0),右下角的顶点纹理坐标为(1, 1)。这种方法的问题是,纹理必须是巨大的,以便获得良好的渲染效果,否则纹理将会被过度拉伸。

+

但我们仍然可以使用非常小的纹理,通过使用高效的技术来获得很好的效果。如果我们设置超出[1, 1]范围的纹理坐标,我们将回到原点并重新开始计算。下图表示在几个正方形中平铺相同的纹理,并超出了[1, 1]范围。

+

纹理坐标I

+

这是我们在设置纹理坐标时所要做的。我们将一个参数乘以纹理坐标(计算好像整个网格被纹理包裹的情况),即textInc参数,以增加在相邻顶点之间使用的纹理像素数。

+

纹理坐标II

+

目前唯一没有解决的是法线计算。记住我们需要法线,光照才能正确地应用于地形。没有法线,无论光照如何,地形将以相同的颜色渲染。我们在这里使用的方法不一定是最高效的,但它将帮助你理解如何自动计算法线。如果你搜索其他解决方案,可能会发现更有效的方法,只使用相邻点的高度而不需要做交叉相乘操作。尽管如此,这仅需要在启动时完成,这里的方法不会对性能造成太大的损害。

+

让我们用图解的方式解释如何计算一个法线值。假设我们有一个名为P0的顶点。我们首先计算其周围每个顶点(P1, P2, P3, P4)和与连接这些点的面相切的向量。这些向量(V1, V2, V3, V4)是通过将每个相邻点与P0相减(例如V1 = P1 - P0)得到的。

+

法线计算I

+

然后,我们计算连接每一个相邻点的平面的法线。这是与之前计算得到的向量交叉相乘计算的。例如,向量V1V2所在的平面(蓝色阴影部分)的法线是由V1V2交叉相乘得到的,即V12 = V1 × V2

+

法线计算II

+

如果我们计算完毕其他平面的法线(V23 = V2 × V3V34 = V3 × V4V41 = V4 × V1),则法线P0就是周围所有平面法线(归一化后)之和:N0 = V12 + V23 + V34 + V41

+

法线计算III

+

法线计算的方法实现如下所示。

+
private float[] calcNormals(float[] posArr, int width, int height) {
+    Vector3f v0 = new Vector3f();
+    Vector3f v1 = new Vector3f();
+    Vector3f v2 = new Vector3f();
+    Vector3f v3 = new Vector3f();
+    Vector3f v4 = new Vector3f();
+    Vector3f v12 = new Vector3f();
+    Vector3f v23 = new Vector3f();
+    Vector3f v34 = new Vector3f();
+    Vector3f v41 = new Vector3f();
+    List<Float> normals = new ArrayList<>();
+    Vector3f normal = new Vector3f();
+    for (int row = 0; row < height; row++) {
+        for (int col = 0; col < width; col++) {
+            if (row > 0 && row < height -1 && col > 0 && col < width -1) {
+                int i0 = row*width*3 + col*3;
+                v0.x = posArr[i0];
+                v0.y = posArr[i0 + 1];
+                v0.z = posArr[i0 + 2];
+
+                int i1 = row*width*3 + (col-1)*3;
+                v1.x = posArr[i1];
+                v1.y = posArr[i1 + 1];
+                v1.z = posArr[i1 + 2];                    
+                v1 = v1.sub(v0);
+
+                int i2 = (row+1)*width*3 + col*3;
+                v2.x = posArr[i2];
+                v2.y = posArr[i2 + 1];
+                v2.z = posArr[i2 + 2];
+                v2 = v2.sub(v0);
+
+                int i3 = (row)*width*3 + (col+1)*3;
+                v3.x = posArr[i3];
+                v3.y = posArr[i3 + 1];
+                v3.z = posArr[i3 + 2];
+                v3 = v3.sub(v0);
+
+                int i4 = (row-1)*width*3 + col*3;
+                v4.x = posArr[i4];
+                v4.y = posArr[i4 + 1];
+                v4.z = posArr[i4 + 2];
+                v4 = v4.sub(v0);
+
+                v1.cross(v2, v12);
+                v12.normalize();
+
+                v2.cross(v3, v23);
+                v23.normalize();
+
+                v3.cross(v4, v34);
+                v34.normalize();
+
+                v4.cross(v1, v41);
+                v41.normalize();
+
+                normal = v12.add(v23).add(v34).add(v41);
+                normal.normalize();
+            } else {
+                normal.x = 0;
+                normal.y = 1;
+                normal.z = 0;
+            }
+            normal.normalize();
+            normals.add(normal.x);
+            normals.add(normal.y);
+            normals.add(normal.z);
+        }
+    }
+    return Utils.listToArray(normals);
+}
+
+

最后,为了创建更大的地形,我们有两个选择: +* 创建更大的高度图 +* 重用高度图并将其平铺在三维空间中。高度图将像一个地形块,在世界上像瓷砖一样平移。为了做到这一点,高度图边缘的像素必须是相同的(左侧边缘必须与右侧相同,上侧边缘必须与下侧相同),以避免块之间的间隙。

+

我们将使用第二种方法(并选择适当的高度图)。为了做到它,我们将创建一个名为Terrain的类,该类将创建一个正方形的高度图块,定义如下。

+
package org.lwjglb.engine.items;
+
+import org.lwjglb.engine.graph.HeightMapMesh;
+
+public class Terrain {
+
+    private final GameItem[] gameItems;
+
+    public Terrain(int blocksPerRow, float scale, float minY, float maxY, String heightMap, String textureFile, int textInc) throws Exception {
+        gameItems = new GameItem[blocksPerRow * blocksPerRow];
+        HeightMapMesh heightMapMesh = new HeightMapMesh(minY, maxY, heightMap, textureFile, textInc);
+        for (int row = 0; row < blocksPerRow; row++) {
+            for (int col = 0; col < blocksPerRow; col++) {
+                float xDisplacement = (col - ((float) blocksPerRow - 1) / (float) 2) * scale * HeightMapMesh.getXLength();
+                float zDisplacement = (row - ((float) blocksPerRow - 1) / (float) 2) * scale * HeightMapMesh.getZLength();
+
+                GameItem terrainBlock = new GameItem(heightMapMesh.getMesh());
+                terrainBlock.setScale(scale);
+                terrainBlock.setPosition(xDisplacement, 0, zDisplacement);
+                gameItems[row * blocksPerRow + col] = terrainBlock;
+            }
+        }
+    }
+
+    public GameItem[] getGameItems() {
+        return gameItems;
+    }
+}
+
+

让我们详解整个过程,我们拥有由以下坐标定义的块(X和Z使用之前定义的常量)。

+

地形构建I

+

假设我们创建了一个由3×3块网格构成的地形。我们假设我们不会缩放地形块(也就是说,变量blocksPerRow3而变量scale将会是1)。我们希望网格的中央在坐标系的(0, 0)。

+

我们需要移动块,这样顶点就变成如下坐标。

+

地形构建II

+

移动是通过调用setPosition方法实现的,但记住,我们所设置的是一个位移而不是一个位置。如果你看到上图,你会发现中央块不需要任何移动,它已经定位在适当的坐标上。绘制绿色顶点需要在X轴上位移-1,而绘制蓝色顶点需要在X轴上位移+1。计算X位移的公式,要考虑到缩放和块的宽度,公式如下:

+

+ +

+

Z位移的公式为:

+

+ +

+

如果在DummyGame类中创建一个Terrain实例,我们可以得到如图所示的效果。

+

地形结果

+

你可以在地形周围移动相机,看看它是如何渲染的。由于还没有实现碰撞检测,你可以穿过它并从上面看它。由于我们已经启用了面剔除,当从下面观察时,地形的某些部分不会渲染。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/15-terrain-collisions/index.html b/15-terrain-collisions/index.html new file mode 100755 index 0000000..7e86ca9 --- /dev/null +++ b/15-terrain-collisions/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + 地形碰撞 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

地形碰撞(Terrain Collisions)

+

此前我们创建了一个地形,接下来就是检测碰撞以避免穿过它。回忆一下之前的内容,一个地形是由地形块组成的,每个地形块都是由高度图生成的,高度图用于设置构成地形的三角形的顶点高度。

+

为了检测碰撞,我们必须将当前所在位置的Y值与当前地形点的Y值进行比较。如果有碰撞,我们需要回到地形上方。很简单的想法,是吗?确实是这样,但在比较之前,我们需要进行几次计算。

+

我们首先要定义的是我们对“当前位置”这个词的理解。由于我们还没有一个球员的概念,答案很简单,当前的位置将是相机的位置。因此,我们已经有了比较的组成部分之一,因此,接下来要计算的是当前位置的地形高度。 +首先要定义的是“当前位置”这个词的概念。由于我们还没有一个“玩家”的概念,因此当前位置将是摄像机的位置。这样我们就有了比较的一方,因此接下来要计算当前位置的地形高度。

+

如上所是,地形由地形块组成,如下图所示。

+

地形网格

+

每个地形块都是由相同的高度图网格构成,但被精确地缩放和位移,以形成看起来像是连续的景观的地形网格。

+

所以首先要做的是确定当前位置(摄像机位置)在哪个地形块。为了得到它,我们将基于位移和缩放来计算每个地形块的包围盒(BoundingBox)。因为地形在运行时不会移动或缩放,所以我们可以在Terrain类的构造方法中计算。这样就可以在任何时候访问它们,而不需要在每个游戏循环周期中重复这些计算。

+

我们将创建一个新的方法来计算一个地形块的包围盒,名为getBoundingBox

+
private Box2D getBoundingBox(GameItem terrainBlock) {
+    float scale = terrainBlock.getScale();
+    Vector3f position = terrainBlock.getPosition();
+
+    float topLeftX = HeightMapMesh.STARTX * scale + position.x;
+    float topLeftZ = HeightMapMesh.STARTZ * scale + position.z;
+    float width = Math.abs(HeightMapMesh.STARTX * 2) * scale;
+    float height = Math.abs(HeightMapMesh.STARTZ * 2) * scale;
+    Box2D boundingBox = new Box2D(topLeftX, topLeftZ, width, height);
+    return boundingBox;
+}
+
+

Box2Djava.awt.Rectangle2D.Float类的简化版本,为了避免使用AWT而创建。

+

限制我们需要计算地形块的世界坐标。在上一章中,你看到所有的地形网格都是在一个正方形中创建的,它的原点设置为[STARTX, STARTZ]。因此,我们需要把这些坐标转换为世界坐标,这要考虑下图所示的位移与缩放。

+

模型坐标系到世界坐标系

+

如上所述,这可以在Terrain类构造方法中计算,因为它不会在运行时发生变化,所以我们要添加一个新的属性来保存包围盒:

+
private final Box2D[][] boundingBoxes;
+
+

Terrain类的构造方法中,当我们创建地形块时,只需调用计算包围盒的方法。

+
public Terrain(int terrainSize, float scale, float minY, float maxY, String heightMapFile, String textureFile, int textInc) throws Exception {
+    this.terrainSize = terrainSize;
+    gameItems = new GameItem[terrainSize * terrainSize];
+
+    PNGDecoder decoder = new PNGDecoder(getClass().getResourceAsStream(heightMapFile));
+    int height = decoder.getHeight();
+    int width = decoder.getWidth();
+    ByteBuffer buf = ByteBuffer.allocateDirect(
+            4 * decoder.getWidth() * decoder.getHeight());
+    decoder.decode(buf, decoder.getWidth() * 4, PNGDecoder.Format.RGBA);
+    buf.flip();
+
+    // 每行与每列的顶点数
+    verticesPerCol = heightMapImage.getWidth();
+    verticesPerRow = heightMapImage.getHeight();
+
+    heightMapMesh = new HeightMapMesh(minY, maxY, buf, width, textureFile, textInc);
+    boundingBoxes = new Box2D[terrainSize][terrainSize];
+    for (int row = 0; row < terrainSize; row++) {
+        for (int col = 0; col < terrainSize; col++) {
+            float xDisplacement = (col - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getXLength();
+            float zDisplacement = (row - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getZLength();
+
+            GameItem terrainBlock = new GameItem(heightMapMesh.getMesh());
+            terrainBlock.setScale(scale);
+            terrainBlock.setPosition(xDisplacement, 0, zDisplacement);
+            gameItems[row * terrainSize + col] = terrainBlock;
+
+            boundingBoxes[row][col] = getBoundingBox(terrainBlock);
+        }
+    }
+}
+
+

因此,有了所有预先计算的包围盒,我们将创建一个新的方法,这个方法将以当前位置为参数,返回对应地形高度。该方法名为getHeight,其定义如下。

+
public float getHeight(Vector3f position) {
+    float result = Float.MIN_VALUE;
+    // 对于每个地形块,我们获取包围盒,将其转换到观察坐标系
+    // 检查坐标是否包含在包围盒中
+    Box2D boundingBox = null;
+    boolean found = false;
+    GameItem terrainBlock = null;
+    for (int row = 0; row < terrainSize && !found; row++) {
+        for (int col = 0; col < terrainSize && !found; col++) {
+            terrainBlock = gameItems[row * terrainSize + col];
+            boundingBox = boundingBoxes[row][col];
+            found = boundingBox.contains(position.x, position.z);
+        }
+    }
+
+    // 如果我们找到了一个包含我们位置的地形块
+    // 计算该位置的地形高度
+    if (found) {
+        Vector3f[] triangle = getTriangle(position, boundingBox, terrainBlock);
+        result = interpolateHeight(triangle[0], triangle[1], triangle[2], position.x, position.z);
+    }
+
+    return result;
+}
+
+

在此方法中第一件事是确定我们所在的地形块。由于我们已经有了每个地形块的包围盒,所以算法很简单。我们只需要迭代包围盒数组,并检查当前位置是否位于其中(Box2D提供了该方法)。

+

一旦找到了地形块,我们需要计算所处的三角形,这是由之后的getTriangle方法计算的。之后,我们得到了所在三角形的坐标,包括它的高度。但是,我们需要的是一个点的高度,这个点不位于这些顶点中的任何一点,而位于它们之间的位置。这将在interpolateHeight方法中计算,我们也将解释这是如何计算的。

+

让我们先从确定所处的三角形开始。构成地形块的正方形可以看作一个网格,其中每个单元由两个三角形组成。首先我们定义一些变量:

+
    +
  • boundingBox.x是与包围盒相关联的地形块的原x坐标。
  • +
  • boundingBox.y是与包围盒相关联的地形块的原z坐标(即使你看到一个y,但它是在z轴的)。
  • +
  • boundingBox.width是地形块正方形的宽度。
  • +
  • boundingBox.height是地形块正方形的高度。
  • +
  • cellWidth是一个单元的宽度。
  • +
  • cellHeight是一个单元的高度。
  • +
+

上面定义的所有变量都用世界坐标来表示。为了计算单元的宽度,我们只需要将包围盒宽度除以每列的顶点数:

+

+ +

+

cellHeight的计算也相似:

+

+ +

+

一旦有了这些变量,我们就可以计算所在的单元格的行和列了:

+

+ +

+

+ +

+

下图在示例地形块展示了此前描述的所有变量。

+

地形块变量

+

有了这些信息,就可以计算单元格中包含的三角形顶点的位置。我们怎么才能做到呢?让我们来看看组成一个单元格的三角形。

+

单元格

+

你可以看到,单元格是被一个对角线分开为两个三角形的。确定与当前位置相关的三角形的方法,是检查z坐标在对角线的上方还是下方。在本例中,将对角线的x值设置为当前位置的x值,求出对应的对角线z值,如果当前位置的z值小于对角线的z值,那么我们在T1中。反之如果当前位置的z值大于对角线的z值,我们就在T2中。

+

我们可以通过计算与对角线相匹配的直线方程来确定。

+

如果你还记得学校的数学课,从两点通过的直线(在二维中)的方程为:

+

+ +

+

其中m是直线的斜率,也就是说,当沿x轴移动时,其高度会发生变化。请注意,在本例中,y坐标其实是一个z。还要注意的是,我们使用的是二维坐标,因为在这里不计算高度,只要x坐标和z坐标就足够了。因此,在本例中,直线方程应该是这样。

+

+ +

+

斜率可以按如下方式计算:

+

+ +

+

所以给定一个x坐标得到一个z值的对角线方程就像这样:

+

+ +

+

其中x1x2z1z2分别是顶点V1V2xz坐标。

+

因此,通过上述方式来获得当前位置所在的三角形的方法,名为getTriangle,其实现如下:

+
protected Vector3f[] getTriangle(Vector3f position, Box2D boundingBox, GameItem terrainBlock) {
+    // 获得与当前位置相关的高度图的行列
+    float cellWidth = boundingBox.width / (float) verticesPerCol;
+    float cellHeight = boundingBox.height / (float) verticesPerRow;
+    int col = (int) ((position.x - boundingBox.x) / cellWidth);
+    int row = (int) ((position.z - boundingBox.y) / cellHeight);
+
+    Vector3f[] triangle = new Vector3f[3];
+    triangle[1] = new Vector3f(
+        boundingBox.x + col * cellWidth,
+        getWorldHeight(row + 1, col, terrainBlock),
+        boundingBox.y + (row + 1) * cellHeight);
+    triangle[2] = new Vector3f(
+        boundingBox.x + (col + 1) * cellWidth,
+        getWorldHeight(row, col + 1, terrainBlock),
+        boundingBox.y + row * cellHeight);
+    if (position.z < getDiagonalZCoord(triangle[1].x, triangle[1].z, triangle[2].x, triangle[2].z, position.x)) {
+        triangle[0] = new Vector3f(
+            boundingBox.x + col * cellWidth,
+            getWorldHeight(row, col, terrainBlock),
+            boundingBox.y + row * cellHeight);
+    } else {
+        triangle[0] = new Vector3f(
+            boundingBox.x + (col + 1) * cellWidth,
+            getWorldHeight(row + 2, col + 1, terrainBlock),
+            boundingBox.y + (row + 1) * cellHeight);
+    }
+
+    return triangle;
+}
+
+protected float getDiagonalZCoord(float x1, float z1, float x2, float z2, float x) {
+    float z = ((z1 - z2) / (x1 - x2)) * (x - x1) + z1;
+    return z;
+}
+
+protected float getWorldHeight(int row, int col, GameItem gameItem) {
+    float y = heightMapMesh.getHeight(row, col);
+    return y * gameItem.getScale() + gameItem.getPosition().y;
+}
+
+

你可以看到我们有另外两个反复。第一个名为getDiagonalZCoord,给定x位置和两个顶点计算对角线的z坐标。另一个名为getWorldHeight,用来获得三角形顶点的高度(即y坐标)。当地形网格被创建时,每个顶点的高度都被预先计算和储存,我们只需将其转换为世界坐标。

+

好,我们有当前位置的三角形坐标。最后,我们准备在当前位置计算地形高度。怎么做呢?我们的三角形在一个平面上,一个平面可以由三个点定义,在本例中,三个顶点定义了一个三角形。

+

平面方程如下:

+

+ +

+

上述方程的常数值是:

+

+ +

+

+ +

+

+ +

+

其中ABC是定义平面所需的三个顶点。

+

然后,利用之前的方程以及当前位置的xz坐标值,我们能够计算y值,即当前位置的地形高度:

+

+ +

+

实现了如上运算的方法如下:

+
protected float interpolateHeight(Vector3f pA, Vector3f pB, Vector3f pC, float x, float z) {
+    // 平面方程 ax+by+cz+d=0
+    float a = (pB.y - pA.y) * (pC.z - pA.z) - (pC.y - pA.y) * (pB.z - pA.z);
+    float b = (pB.z - pA.z) * (pC.x - pA.x) - (pC.z - pA.z) * (pB.x - pA.x);
+    float c = (pB.x - pA.x) * (pC.y - pA.y) - (pC.x - pA.x) * (pB.y - pA.y);
+    float d = -(a * pA.x + b * pA.y + c * pA.z);
+    // y = (-d -ax -cz) / b
+    float y = (-d - a * x - c * z) / b;
+    return y;
+}
+
+

这就完了!现在我们能够检测碰撞,所以在DummyGame类中,在更新摄像机位置时,修改如下代码:

+
// 更新摄像机位置
+Vector3f prevPos = new Vector3f(camera.getPosition());
+camera.movePosition(cameraInc.x * CAMERA_POS_STEP, cameraInc.y * CAMERA_POS_STEP, cameraInc.z * CAMERA_POS_STEP);        
+// 检查是否发生碰撞。如果为true,将y坐标设置为
+// 最大高度
+float height = terrain.getHeight(camera.getPosition());
+if ( camera.getPosition().y <= height )  {
+    camera.setPosition(prevPos.x, prevPos.y, prevPos.z);
+}
+
+

如你所见,检测地形碰撞的概念很容易理解,但是我们需要仔细地进行计算并了解正处理的不同坐标系。

+

此外,虽然这里给出的算法在大多数情况下都是可用的,但仍存在需要仔细处理的情况。你可以发现的一个问题是隧道效应(Tunnelling)。设想一个情况,我们正以高速穿过地形,正因如此,位置增量值较高。这个值变得如此之高,以至于因为我们检测的是最终位置的碰撞,所以可能已经穿过了位于两点之间的障碍。

+

隧道效应

+

有许多可行的解决方案可以避免这个效应,最简单的解决方法是将要进行的计算分成增量较小的多份。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/16-fog/index.html b/16-fog/index.html new file mode 100755 index 0000000..ecd9eff --- /dev/null +++ b/16-fog/index.html @@ -0,0 +1,427 @@ + + + + + + + + + + + + 雾 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

雾(Fog)

+

在处理更复杂的问题之前,我们将学习如何在游戏引擎中创建雾特效。有了这个特效,就可以模拟遥远的物体变暗,似乎消失在浓雾中。

+

让我们来确定定义雾的属性是什么。第一个是雾的颜色。在现实世界中,雾是灰色的,但我们可以利用这个特效来模拟不同颜色的雾覆盖的区域。还有一个属性是雾的浓度。

+

因此,为了使用雾特效,我们需要找到一种方法,当3D场景的物体远离摄像机时,使它们褪色到雾的颜色。靠近摄像机的物体不会受到雾的影响,但远处的物体将无法分辨。因此,我们需要计算一个参数,可以用来混合雾的颜色与每个片元的颜色,以模拟雾特效。这个参数取决于与摄像机相距的距离。

+

让我们把这个参数命名为fogFactor,并设定它的范围为0到1。当fogFactor为1时,意味着物体完全不会受到雾的影响,也就是说,它是较近的物体。当fogFactor为0时,意味着物体完全隐藏在雾中。

+

然后,计算雾色的方程如下:

+

+ +

+
    +
  • finalColour 是使用雾特效的最终颜色。
  • +
  • fogFactor 是控制雾的颜色与片元的颜色如何混合的参数,它基本上控制物体的可见性。
  • +
  • fogColour 是雾的颜色。
  • +
  • fragmentColour 没有使用雾特效的片元颜色。
  • +
+

现在我们需要找到一种方法来基于距离计算fogFactor。我们可以选择不同的模型,首先使用线性模型。这是一个给定距离以线性方式改变fogFactor的模型。

+

线性模型由以下参数定义:

+
    +
  • fogStart: 开始使用雾特效的距离。
  • +
  • fogFinish: 雾特效达到最大值的距离。
  • +
  • distance: 到摄像机的距离。
  • +
+

有了这些参数,方程就会是这样的:

+

+ +

+

对于距离低于fogStart的物体我们简单地设置fogFactor1。下图表明了fogFactor是如何随着距离变化而变化的。

+

线性模型

+

线性模型易于计算,但不太真实,因为它不考虑雾气浓度。实际上雾往往以更平滑的方式增加。所以下一个合适的模型是指数模型。该模型的方程如下:

+

+ +

+

其中的新变量如下所述:

+
    +
  • fogDensity 是雾的厚度或浓度。
  • +
  • exponent 用来控制雾随着距离的增加增长的速度。
  • +
+

下图显示了两个图形,分别设置了不同的exponent值(蓝线为2,红线为4)。

+

指数模型

+

在代码中,我们将使用一个公式,让它可以为exponent设置不同的值(你可以很容易地修改示例以使用不同的值)。

+

既然已经解释过这个原理了,我们就可以实现它。我们将在场景的片元着色器中实现雾特效,因为这有我们需要的所有变量。我们将首先定义一个储存雾属性的结构体。

+
struct Fog
+{
+    int active;
+    vec3 colour;
+    float density;
+};
+
+

active属性用于激活或关闭雾特效。雾属性将通过另一个被称作fog的Uniform传递给着色器。

+
uniform Fog fog;
+
+

我们还将创建一个包含着雾属性的名为Fog的新类,它是另一个POJO(Plain Ordinary Java Object,简单的Java对象)。

+
package org.lwjglb.engine.graph.weather;
+
+import org.joml.Vector3f;
+
+public class Fog {
+
+    private boolean active;
+
+    private Vector3f colour;
+
+    private float density;
+
+    public static Fog NOFOG = new Fog();
+
+    public Fog() {
+        active = false;
+        this.colour = new Vector3f(0, 0, 0);
+        this.density = 0;
+    }
+
+    public Fog(boolean active, Vector3f colour, float density) {
+        this.colour = colour;
+        this.density = density;
+        this.active = active;
+    }
+
+   // Getters and setters here….
+
+

我们将在Scene类中添加一个Fog示例。默认情况下,Scene类将初始化一个Fog示例到常量NOFOG,用于模拟关闭雾特效的情况。

+

因为添加了一个新的Uniform类型,所以我们需要修改ShaderProgram类来创建和初始化雾的Uniform。

+
public void createFogUniform(String uniformName) throws Exception {
+    createUniform(uniformName + ".active");
+    createUniform(uniformName + ".colour");
+    createUniform(uniformName + ".density");
+}
+
+public void setUniform(String uniformName, Fog fog) {
+    setUniform(uniformName + ".activeFog", fog.isActive() ? 1 : 0);
+    setUniform(uniformName + ".colour", fog.getColour() );
+    setUniform(uniformName + ".density", fog.getDensity());
+}
+
+

Renderer类中,我们只需要在setupSceneShader方法中创建Uniform:

+
sceneShaderProgram.createFogUniform("fog");
+
+

然后在renderScene方法中使用它:

+
sceneShaderProgram.setUniform("fog", scene.getFog());
+
+

我们现在可以在游戏中定义雾特效,但是需要回到片元着色器中应用雾特效。我们将创建一个名为calcFog的函数,函数定义如下。

+
vec4 calcFog(vec3 pos, vec4 colour, Fog fog)
+{
+    float distance = length(pos);
+    float fogFactor = 1.0 / exp( (distance * fog.density)* (distance * fog.density));
+    fogFactor = clamp( fogFactor, 0.0, 1.0 );
+
+    vec3 resultColour = mix(fog.colour, colour.xyz, fogFactor);
+    return vec4(resultColour.xyz, colour.w);
+}
+
+

如你所见,我们首先计算到顶点的距离。顶点坐标定义在pos变量中,我们只需要计算长度。然后利用exponent为2的指数模型(相当于乘以两次)计算雾参数。我们得到的fogFactor的范围在01之间,并使用mix函数。在GLSL中,min函数被用于混合雾色和图元颜色(由颜色变量定义)。相当于使用如下方程:

+

+ +

+

我们还为颜色保留了w元素,即透明度。我们不希望这个元素受到影响,片元应该保持它的透明程度不变。

+

在应用了所有的光效之后,在片元着色器的最后,如果雾特效启用的话,我们将简单地把返回值设置为片元颜色。

+
if ( fog.activeFog == 1 ) 
+{
+    fragColor = calcFog(mvVertexPos, fragColor, fog);
+}
+
+

所有这些代码完成后,我们可以用下面的数据设置一个雾特效:

+
scene.setFog(new Fog(true, new Vector3f(0.5f, 0.5f, 0.5f), 0.15f));
+
+

然后我们将获得像这样的效果:

+

雾特效

+

你会看到远处的物体褪色,当你靠近它们时,雾开始消失。但有一个问题,天空盒看起来有点奇怪,地平线不受雾的影响。有几种方法可以解决这个问题:

+
    +
  • 使用只能看到天空的另一个不同的天空盒。
  • +
  • 删除天空盒,因为有浓雾,你不应该能够看到一个背景。
  • +
+

也可能这两个解决方案没有适合你的,你可以试着将雾色与天空盒的背景相匹配,但这样你会做复杂的计算,结果也许不会更好。

+

如果你运行这个示例,你会感到平行光变得暗淡,场景变暗,但雾看起来有问题,因为它不受光的影响,会看到如下图所示的结果。

+

发光的雾

+

远处的物体变为雾色,这是一个不受光影响的常数。这造成了一个在黑暗中发光的效果(这可能并不好)。我们需要修改计算雾的函数,让其考虑光照。该函数将接收环境光和平行光来调整雾色。

+
vec4 calcFog(vec3 pos, vec4 colour, Fog fog, vec3 ambientLight, DirectionalLight dirLight)
+{
+    vec3 fogColor = fog.colour * (ambientLight + dirLight.colour * dirLight.intensity);
+    float distance = length(pos);
+    float fogFactor = 1.0 / exp( (distance * fog.density)* (distance * fog.density));
+    fogFactor = clamp( fogFactor, 0.0, 1.0 );
+
+    vec3 resultColour = mix(fogColor, colour.xyz, fogFactor);
+    return vec4(resultColour.xyz, 1);
+}
+
+

如你所见,平行光我们仅使用了颜色和强度,我们不需要关注它的方向。这样,我们只需要稍微修改函数的调用:

+
if ( fog.active == 1 ) 
+{
+    fragColor = calcFog(mvVertexPos, fragColor, fog, ambientLight, directionalLight);
+}
+
+

在夜晚时,我们会看到这样的效果。

+

夜晚的雾

+

一个要强调的重要的事情是,我们必须聪明地选择雾色。这是很重要的,当我们没有天空盒,但有固定的颜色背景,应该把雾色设置为背景色。如果你删除了天空盒的代码并重新运行示例代码,你会得到这样的结果。

+

黑色背景

+

但如果我们把背景色修改为(0.5, 0.5, 0.5),最终结果看起来就是如下所示。

+

雾灰色背景

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/17-normal-mapping/index.html b/17-normal-mapping/index.html new file mode 100755 index 0000000..610407d --- /dev/null +++ b/17-normal-mapping/index.html @@ -0,0 +1,407 @@ + + + + + + + + + + + + 法线贴图 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

法线贴图(Normal Mapping)

+

本章中将讲解一项技术,它将极大地改善我们的3D模型的外观。到目前为止,我们已经能够将纹理使用到复杂的3D模型上,但这还离真实物体的样子很远。现实世界中的物体表面不是完全光滑的,它们有我们的3D模型目前所不具有的瑕疵。

+

为了渲染更真实的场景,我们将渲染法线贴图(Normal Mapping)。如果你在现实世界中看到一个平面,你会发现那些瑕疵甚至可以在很远的距离看到。在3D场景中,平面没有瑕疵,我们可以将纹理应用在它之上,但这不会改变光反射的方式。这就是为什么与现实有区别的原因。

+

我们可以考虑通过增加三角形数量来增加模型的细节并反映出这些瑕疵,但性能会下降。我们需要的是改变表面光反射的方式来增加真实感。这就是用法线贴图技术实现的。

+

让我们看看光滑平面的例子,一个平面由两个三角形组成为一个四边形。回忆之前的光照章节,模型的光反射的要素是平面法线。在此情况下,我们整个平面仅有单一的法线,当计算光如何影响片元时,每个片元都使用相同的法线。看起来就像下图那样。

+

平面法线

+

如果可以改变平面的每个片元的法线,我们就可以模拟平面的瑕疵,使它们更逼真。看起来就像下图那样。

+

片元法线

+

要做到这一点,我们要加载另一个纹理,它储存面的法线。法线纹理的每个像素将以RGB值储存法线的xyz坐标值。

+

让我们用下面的纹理绘制一个四边形。

+

纹理

+

上图的法线纹理如下所示。

+

法线纹理

+

如你所见,如果我们把颜色变换应用到原始纹理,每个像素使用颜色分量储存法线信息。在看到法线贴图时,你常常会看到主色调倾向于蓝色,这是由于大多数法线指向转换正z轴所致。在一个平面表面的矢量中,z分量通常比xy分量的值高得多。由于xyz坐标被映射到RGB,导致蓝色分量也有着更高的值。

+

因此,使用法线贴图渲染对象只需要一个额外的纹理,并同时使用它渲染片元以获得适当的法线值。

+

让我们开始修改代码,以支持法线贴图。我们将添加一个新的Texture实例到Material类,这样就可以把一个法线贴图纹理添加到游戏项目上。此实例将有自己的getset方法,并有方法可以检查Material是否有法线贴图。

+
public class Material {
+
+    private static final Vector4f DEFAULT_COLOUR = new Vector3f(1.0f, 1.0f, 1.0f, 10.f);
+
+    private Vector3f ambientColour;
+
+    private Vector3f diffuseColour;
+
+    private Vector3f specularColour;
+
+    private float reflectance;
+
+    private Texture texture;
+
+    private Texture normalMap;
+
+    // … Previous code here
+
+    public boolean hasNormalMap() {
+        return this.normalMap != null;
+    }
+
+    public Texture getNormalMap() {
+        return normalMap;
+    }
+
+    public void setNormalMap(Texture normalMap) {
+        this.normalMap = normalMap;
+    }
+}
+
+

我们将在场景的片元着色器中使用法线贴图纹理。但是,由于我们在观察坐标空间操作,所以需要通过模型观察矩阵来进行适当的变换。因此,我们需要修改场景的顶点着色器。

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+out vec3 mvVertexNormal;
+out vec3 mvVertexPos;
+out mat4 outModelViewMatrix;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPos;
+    outTexCoord = texCoord;
+    mvVertexNormal = normalize(modelViewMatrix * vec4(vertexNormal, 0.0)).xyz;
+    mvVertexPos = mvPos.xyz;
+    outModelViewMatrix = modelViewMatrix;
+}
+
+

在场景的片元着色器中,我们需要添加另一个输入参数。

+
in mat4 outModelViewMatrix;
+
+

在片元着色器中,我们需要为法线贴图纹理采样器传递一个新的Uniform:

+
uniform sampler2D texture_sampler;
+
+

此外,在片元着色器中,我们将创建一个新函数,该函数计算当前片元的法线。

+
vec3 calcNormal(Material material, vec3 normal, vec2 text_coord, mat4 modelViewMatrix)
+{
+    vec3 newNormal = normal;
+    if ( material.hasNormalMap == 1 )
+    {
+        newNormal = texture(normalMap, text_coord).rgb;
+        newNormal = normalize(newNormal * 2 - 1);
+        newNormal = normalize(modelViewMatrix * vec4(newNormal, 0.0)).xyz;
+    }
+    return newNormal;
+}
+
+

该函数有以下参数: +* Material实例。 +* 顶点法线。 +* 纹理坐标。 +* 模型视图矩阵。

+

我们在此函数中做的第一件事是检查这个Material实例是否有法线贴图。如果没有,我们就像平常一样简单地使用顶点法线。如果它有法线贴图,我们使用储存着法线数据的法线纹理关联到当前纹理坐标。

+

记住我们取得的颜色是法线坐标,但因为它们被储存为RGB值,所以它们的值在范围中。我们需要将其范围转换为,所以我们将其乘以2然后减去1。然后,我们将它归一化,并将其转换到模型观察坐标系(就像顶点法线那样)。

+

就这样,我们可以使用返回值作为片元所有光照计算中的法线。

+

Renderer类中,我们需要创建法线贴图,在renderScene方法中,我们做如下设置:

+
//...
+sceneShaderProgram.setUniform("fog", scene.getFog());
+sceneShaderProgram.setUniform("texture_sampler", 0);
+sceneShaderProgram.setUniform("normalMap", 1);
+//...
+
+

上述代码中你可能注意到一些有趣的事情。我们将材质纹理Uniform(texture_sampler)设置为0,然后将法线贴图纹理(normalMap)设置为1。如果你回想纹理章节,我们不止使用一个纹理,所以我们必须为每个单独的纹理设置纹理单元。

+

当我们渲染Mesh时,也需要考虑这一点。

+
private void initRender() {
+    Texture texture = material.getTexture();
+    if (texture != null) {
+        // 激活第一纹理库
+        glActiveTexture(GL_TEXTURE0);
+        // 绑定纹理
+        glBindTexture(GL_TEXTURE_2D, texture.getId());
+    }
+    Texture normalMap = material.getNormalMap();
+    if ( normalMap != null ) {
+        // 激活第二纹理库
+        glActiveTexture(GL_TEXTURE1);
+        // 绑定纹理
+        glBindTexture(GL_TEXTURE_2D, normalMap.getId());
+    }
+
+    // 绘制网格
+    glBindVertexArray(getVaoId());
+    glEnableVertexAttribArray(0);
+    glEnableVertexAttribArray(1);
+    glEnableVertexAttribArray(2);
+}
+
+

如你所见,我们需要绑定每个可用的纹理,并激活相关的纹理单元,以便多个纹理能够同时工作。在Renderer类的renderScene方法中,我们不需要显式设置纹理的Uniform,因为它已经包含在Material中。

+

为了展示法线贴图带来的提升,我已经创建了两个并排显示的四边形示例。右边的有一张法线贴图,而左边没有。我们还删除了地形、天空盒和HUD,并设置了平行光照,可以用鼠标左右键改变方向,这样你就可以看到效果了。我已经修改了基本源代码,以支持关闭天空盒和地形。我们还在片元着色器中设置了光效果的范围为,以避免图像过度曝光。

+

结果如下图所示。

+

法线贴图结果

+

如你所见,具有法线贴图的四边形感觉更大。虽然它本质上是一个像其他四边形一样的平面,但你可以看到光线是如何反射的。

+

尽管我们已经编写的代码完全符合这个示例,但你需要知道它的局限性。该代码仅适用与使用模型坐标空间创建的法线贴图纹理。如果是这种情况,我们可以使用模型视图矩阵来将法线坐标转换到观察空间。

+

但通常法线贴图不是这样定义的。它们通常被定义在所谓的切线空间(Tangent Space中。切线空间是一个坐标系,它定位在模型的每个三角形,其坐标系的z轴总是垂直于表面。这就是为什么当你看一个法线贴图时,它通常是蓝色的,甚至对于面相对复杂的模型也是如此。

+

我们现在仍使用这个简单的实现,但请记住,你必须总是使用在模型空间中定义的法线贴图。如果使用切线空间中定义的贴图,就会变得很奇怪。为了能够让它们一同工作,我们需要使用特定的矩阵来将坐标变换到切线空间。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/18-shadows/index.html b/18-shadows/index.html new file mode 100755 index 0000000..bbf474d --- /dev/null +++ b/18-shadows/index.html @@ -0,0 +1,563 @@ + + + + + + + + + + + + 阴影 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

阴影(Shadows)

+

阴影映射

+

目前,我们能够表现光线如何影响三维场景中的对象。接收到更多光的物体比没有接收光的物体更亮。然而,我们仍无法投射阴影。阴影能增加3D场景的真实度,因此我们将在本章中添加对它的支持。

+

我们将使用一种被称为阴影映射(Shadow Mapping)的技术,这种技术被广泛使用于游戏中,且不会严重影响引擎性能。阴影映射看起来很容易理解,但是很难正确地实现它。或者更准确地说,很难用一种通用的,涵盖了一切可能并产生一致的效果的方法去实现它。

+

我们将在此说明一种方法,它可以为你在大多数情况下添加阴影,但更重要的是,它将帮助你了解其局限性。这里介绍的代码远不是完美的,但我认为它很容易理解。它还被设计用于支持有向光(这我认为是更复杂的情况),但你将了解如何将其扩展以支持其他类型的光照(例如点光源)。如果想获得更高级的效果,你应该使用更高级的技术,例如级联阴影映射(Cascaded Shadow Maps)。在任何情况下,这里解释的概念都仅仅作为基础。

+

所以,让我们从思考如何检查特定区域(实际上是片元)是否在阴影中开始。在绘制这个区域的时候,我们可以发出射线投射到光源上,如果我们可以在不发生任何碰撞的情况下到达光源,那么像素就在光照中,反之,像素处于阴影中。

+

下图展示了点光源的情况,点PA可以到达光源,但点PB和PC不行,因此它们位于阴影中。

+

阴影概述I

+

那么,我们如何才能检查是否能以一种有效的方式发射出不发生碰撞的射线呢?理论上,光源可以投射出无限的光线,Name我们如何检查光线是否被遮挡?

+

我们能做的不是投射光线,而是从光线透视图中观察3D场景,并从该位置渲染场景。我们可以将相机设置在光源位置并渲染场景,以便储存每个片元的深度。这相当于计算每个片元到光源的距离。最后,我们要做的是将光照所及的最小距离储存为阴影图。

+

下图展示了一个悬浮在平面上并垂直于光线的立方体。

+

阴影概述II

+

从光源的角度看,情况是这样的(颜色越深,越接近光源)。

+

从光源的角度渲染

+

利用这些信息。我们可以像往常一样渲染3D场景,并以最小储存距离检查每个每个片元到光源的距离。如果距离小于阴影贴图中储存的值,则对象位于光照中,否则位于阴影中。我们可以让几个物体被同一光照照射,但我们储存最小距离。

+

因此,阴影映射分为两步:

+
    +
  • 首先,我们将场景从光照空间渲染为阴影图,以获得最小距离。
  • +
  • 其次,我们从摄像机的视角渲染场景。并使用深度图计算对象是否位于阴影中。
  • +
+

为了渲染深度图,我们需要说说深度缓冲区(Depth-buffer)。当我们渲染一个场景时,所有深度信息都储存在一个名为“深度缓冲区”(又称“Z缓冲区(Z-buffer)”)的缓冲区中。深度信息是渲染的每个片元的值。如果你从第一章回忆我们在渲染场景时,将正在渲染的场景从世界坐标转换为屏幕坐标。我们所绘制的坐标空间,对于轴来说,坐标的范围为。如果一个物体比其他对象原,我们必须通过透视投影矩阵计算它如何影响其坐标。这不是根据值自动计算的,它必须由我们来做。实际储存在坐标中的是它在片元的深度,仅此而已。

+

此外,在源代码中,我们启用了深度测试。在Window类中,我们添加如下行:

+
glEnable(GL_DEPTH_TEST);
+
+

通过添加这行,我们可以防止无法看到的片元被绘制出来,因为他们位于其他对象之后。在绘制片元之前,它的值将与Z缓冲区中的值进行比较。如果它的值(它的距离)大于缓冲区的值,则会被丢弃。请记住,这是在屏幕空间中完成的,因此,给定一对屏幕空间中范围为坐标,我们比较其片元的值。同样,值也在此范围内。

+

深度缓冲区的存在是我们在执行任何渲染操作之前需要清除屏幕的原因。我们不仅需要清除颜色,还要清除深度信息:

+
public void clear() {
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+

为了开始构建深度图,我们希望从光源的角度获得深度信息。我们需要在光源位置设置一个摄像头,渲染场景并将深度信息储存到纹理中,以便稍后访问它。

+

因此,我们首先需要做的是添加对创建这些纹理的支持。我们将修改Texture类,通过添加新的构造函数来支持创建空纹理。此构造函数需要纹理的尺寸以及它储存的像素的格式。

+
public Texture(int width, int height, int pixelFormat) throws Exception {
+    this.id = glGenTextures();
+    this.width = width;
+    this.height = height;
+    glBindTexture(GL_TEXTURE_2D, this.id);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, this.width, this.height, 0, pixelFormat, GL_FLOAT, (ByteBuffer) null);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
+

我们将纹理环绕方式设置为GL_CLAMP_TO_EDGE,因为我们不希望在超出范围时重复纹理。

+

所以,现在我们可以创建空的纹理,我们需要能够在其中渲染一个场景。为了做到它,我们需要使用帧缓冲区对象(Frame Buffers Objects,或称FBOs)。帧缓冲区是可以作为渲染终点的缓冲区集合。当我们渲染到屏幕上时,我们使用的是OpenGL的默认缓冲区。OpenGL允许我们使用FBO渲染到用户定义的缓冲区。我们将通过创建一个名为ShadowMap的新类,来隔离为阴影映射创建FBO过程的其余代码。如下就是那个类的定义。

+
package org.lwjglb.engine.graph;
+
+import static org.lwjgl.opengl.GL11.*;
+import static org.lwjgl.opengl.GL30.*;
+
+public class ShadowMap {
+
+    public static final int SHADOW_MAP_WIDTH = 1024;
+
+    public static final int SHADOW_MAP_HEIGHT = 1024;
+
+    private final int depthMapFBO;
+
+    private final Texture depthMap;
+
+    public ShadowMap() throws Exception {
+        // 创建FBO以渲染深度图
+        depthMapFBO = glGenFramebuffers();
+
+        // 创建深度图纹理
+        depthMap = new Texture(SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, GL_DEPTH_COMPONENT);
+
+        // 绑定深度图纹理到FBO
+        glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap.getId(), 0);
+        // 仅设置深度
+        glDrawBuffer(GL_NONE);
+        glReadBuffer(GL_NONE);
+
+        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+            throw new Exception("Could not create FrameBuffer");
+        }
+
+        // 解绑
+        glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    }
+
+    public Texture getDepthMapTexture() {
+        return depthMap;
+    }
+
+    public int getDepthMapFBO() {
+        return depthMapFBO;
+    }
+
+    public void cleanup() {
+        glDeleteFramebuffers(depthMapFBO);
+        depthMap.cleanup();
+    }
+}
+
+

ShadowMap类定义了两个常量,用于确定储存深度图的纹理的大小。它还定义了两个属性,一个用于FBO,一个用于纹理。在构造函数中,创建一个新的FBO和一个新的Texture。对于FBO,将使用常量GL_DEPTH_COMPONENT作为像素格式,因为我们只对储存深度值感兴趣,然后将FBO绑定到纹理实例。

+

以下几行代码显式地将FBO设置为不渲染任何颜色。FBO需要颜色缓冲区,但我们不需要,这就是为什么我们将颜色缓冲区设置为GL_NONE

+
glDrawBuffer(GL_NONE);
+glReadBuffer(GL_NONE);
+
+

现在,我们准备在Renderer类中将场景从灯光透视渲染为FBO。为了做到它,我们将创建一组特殊的顶点和片元着色器。

+

名为depth_vertex.vs的顶点着色器的定义如下:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+uniform mat4 modelLightViewMatrix;
+uniform mat4 orthoProjectionMatrix;
+
+void main()
+{
+    gl_Position = orthoProjectionMatrix * modelLightViewMatrix * vec4(position, 1.0f);
+}
+
+

我们希望接收与场景着色器相同的输入数据。但实际上,我们只需要坐标,但是为了尽可能多地重用代码,我们还是要传送其他数据。我们还需要一对矩阵。记住,我们必须以光源的角度渲染场景,所以我们需要将模型转换到光源的坐标空间。这是通过modelLightViewMatrix矩阵完成的,该矩阵类似于用于摄像机的模型观察矩阵。现在光源是我们的摄像机。

+

然后我们需要将这些坐标转换到屏幕空间,也就是说,需要投影它们。这是计算平行光与点光源的阴影图时的区别之一。对于地昂扬,我们将使用透视投影(Perspective Projection)矩阵,就像我们正常渲染场景一样。相反,平行光以相同方式影响所有对象,而与距离无关。平行光源位于无穷远的点上,没有位置,只有方向。正交投影(Orthographic Projection)不会使远处的物体变小,因此,正交投影最适合平行光。

+

片元着色器更简单。它只输出坐标作为深度值。

+
#version 330
+
+void main()
+{
+    gl_FragDepth = gl_FragCoord.z;
+}
+
+

实际上,你可以删掉该行,因为我们只生成深度值,深度值将自动返回。

+

一旦我们为深度渲染定义了新的着色器,就可以在Renderer类中使用它们。我们为初始化这些着色器定义了一个新方法,名为setupDepthShader,它将在其他着色器被初始化时调用。

+
private void setupDepthShader() throws Exception {
+    depthShaderProgram = new ShaderProgram();
+    depthShaderProgram.createVertexShader(Utils.loadResource("/shaders/depth_vertex.vs"));
+    depthShaderProgram.createFragmentShader(Utils.loadResource("/shaders/depth_fragment.fs"));
+    depthShaderProgram.link();
+
+    depthShaderProgram.createUniform("orthoProjectionMatrix");
+    depthShaderProgram.createUniform("modelLightViewMatrix");
+}
+
+

现在,我们需要创建一个新的方法,使用那些名为renderDepthMap的着色器。该方法将在主渲染方法中调用。

+
public void render(Window window, Camera camera, Scene scene, IHud hud) {
+    clear();
+
+    // 在设置视口之前渲染深度图
+    renderDepthMap(window, camera, scene);
+
+    glViewport(0, 0, window.getWidth(), window.getHeight());
+
+    // 其余的代码在这...
+
+

如果你浏览上述代码,将看到在设置视口之前,新方法就已经被调用。这是因为这个新方法将更高视口以匹配保存深度图的纹理的尺寸。因此,在完成renderDepthMap之后,我们将始终需要设置屏幕尺寸的视口(不检查窗口是否已调整大小)。

+

现在让我们定义一下renderDepthMap方法。第一件事是绑定在ShadowMap类中创建的FBO,并设置视口以匹配纹理尺寸。

+
glBindFramebuffer(GL_FRAMEBUFFER, shadowMap.getDepthMapFBO());
+glViewport(0, 0, ShadowMap.SHADOW_MAP_WIDTH, ShadowMap.SHADOW_MAP_HEIGHT);
+
+

然后,清除深度缓冲区内容并绑定深度着色器。因为我们只处理深度值,所以不需要清除颜色信息。

+
glClear(GL_DEPTH_BUFFER_BIT);
+
+depthShaderProgram.bind();
+
+

现在我们需要设置矩阵,接下来是棘手的部分。我们使用光源作为摄像机,所以需要创建一个需要一个坐标和三个角的观察矩阵。正如本章开头所说,我们只实现平行光,这种类型的光不定义位置,而是定义方向。如果我们使用点光源,这很容易,光源的位置就是观察矩阵的位置,但我们没有位置。

+

我们将采用一种简单的方法来计算光的位置。平行光是由一个向量定义的,通常是归一化的,它指向光源所在的方向。我们把这个方向向量乘以一个可配置的因子,在这样它就为要绘制的场景定义了一个合理距离的点。我们将使用该方向来计算该观察矩阵的旋转角度。

+

光源位置

+

这是计算灯光位置与旋转角度的代码片段:

+
float lightAngleX = (float)Math.toDegrees(Math.acos(lightDirection.z));
+float lightAngleY = (float)Math.toDegrees(Math.asin(lightDirection.x));
+float lightAngleZ = 0;
+Matrix4f lightViewMatrix = transformation.updateLightViewMatrix(new Vector3f(lightDirection).mul(light.getShadowPosMult()), new Vector3f(lightAngleX, lightAngleY, lightAngleZ));
+
+

接下我们需要计算正交投影矩阵:

+
Matrix4f orthoProjMatrix = transformation.updateOrthoProjectionMatrix(orthCoords.left, orthCoords.right, orthCoords.bottom, orthCoords.top, orthCoords.near, orthCoords.far);
+
+

我们已经修改了Transformation类,以囊括光照观察矩阵和正交投影矩阵。此们有一个正交的二维投影矩阵,所以我们重命名了此前的方法和属性,你可以直接查看源代码中的定义。

+

然后,我们按照renderScene方法渲染场景对象,但在光照空间坐标系中使用上述矩阵工作。

+
depthShaderProgram.setUniform("orthoProjectionMatrix", orthoProjMatrix);
+Map<Mesh, List<GameItem>> mapMeshes = scene.getGameMeshes();
+for (Mesh mesh : mapMeshes.keySet()) {
+    mesh.renderList(mapMeshes.get(mesh), (GameItem gameItem) -> {
+        Matrix4f modelLightViewMatrix = transformation.buildModelViewMatrix(gameItem, lightViewMatrix);
+        depthShaderProgram.setUniform("modelLightViewMatrix", modelLightViewMatrix);
+    }
+    );
+}
+
+// 解绑
+depthShaderProgram.unbind();
+glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+

正交投影矩阵的参数是在平行光中定义的。将正交投影矩阵想象为一个边界框,其中包含我们要渲染的所有对象。当只投影适合该边界框的对象时,对象将可视。该边界框由6个参数定义:左、右、上、下、近、远。由于光源位置现在是原点,因此这些参数定义为原点到左或右(x轴)或上或下(y轴)或远或近(z轴)的距离。

+

要使阴影图正常工作,最棘手的一点是确定灯光位置和正交投影矩阵的参数。这就是所有这些参数现在在DirectionalLight类中定义的方式,因此可以根据每个场景正确设置这些参数。

+

你可以实现一个更自动的方法,通过计算摄像机截锥(Frustum)的中心,回到光的方向,建立一个包含场景中所有对象的正交投影。下图展示了如上所述的三维场景、相机位置、截锥(蓝色)、最佳光源位置以及红色的边界框。

+

通用光源位置计算

+

上述方法的问题是很难计算,如果你有很小的物体,并且边界框很大,你可以会得到奇怪的结果。这里介绍的方法对于小场景更简单,你可以调整它以匹配你的模型(即使你可以选择显式设置灯光的位置,以避免相机远离原点移动时产生奇怪的效果)。如果你想要一个更通用的模板,可以应用到任何场景,你应该扩展它,以支持层叠阴影图。

+

让我们继续。在使用深度图实际计算阴影之前,可以使用生成的纹理渲染一个正方形,以观察深度图的实际外观。在有一个旋转立方体漂浮在一个有垂直平行光的平面上的场景中,你可以得到如下结果。

+

深度图

+

如上所述,颜色越深,离光源的位置越近。深度图中光源位置的影响是什么?你可以修改平行光照的倍增因子,将看到在纹理中渲染的对象的大小不会变小。记住,我们使用的是正交投影矩阵,物体不会随着距离增大而变小。你将看到的是,所有的颜色都会变得更亮,如下所示:

+

更远的距离的深度图

+

这是否意味着我们可以为光源选择一个较远的位置而不造成任何后果呢?答案是不行。如果光源离我们要渲染的对象太远,这些对象会超出正交投影矩阵定义的边界框。在此情况下,你会得到一个不错的白色纹理,但这是没用的阴影图。好的,那么我们只需增加边界框的大小,一切都会好的,对吗?答案也是不行。如果你为正交投影矩阵选用了巨大的尺寸,你的物体在纹理中会被绘制得很小,深度值甚至会重叠,造成奇怪的结果。好吧,所以你可以考虑增加纹理大小,但在此情况下,你是有限制的,纹理不能因使用巨大的编辑框而无限增大。

+

因此,可以看到,选择光源的位置和正交投影的参数是一个复杂的平衡,这使得使用阴影图很难得到正确的效果。

+

让我们回到渲染过程,一旦计算了深度图,我们就可以在渲染场景时使用它。首先,我们需要修改场景的顶点着色器。到目前为止,顶点着色器使用透视矩阵将顶点坐标从模型观察空间投影到屏幕空间。现在还需要使用投影矩阵从光照空间坐标投影顶点坐标,以用于片元着色器中计算阴影。

+

顶点着色器是这样修改的:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+out vec3 mvVertexNormal;
+out vec3 mvVertexPos;
+out vec4 mlightviewVertexPos;
+out mat4 outModelViewMatrix;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+uniform mat4 modelLightViewMatrix;
+uniform mat4 orthoProjectionMatrix;
+
+void main()
+{
+    vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPos;
+    outTexCoord = texCoord;
+    mvVertexNormal = normalize(modelViewMatrix * vec4(vertexNormal, 0.0)).xyz;
+    mvVertexPos = mvPos.xyz;
+    mlightviewVertexPos = orthoProjectionMatrix * modelLightViewMatrix * vec4(position, 1.0);
+    outModelViewMatrix = modelViewMatrix;
+}
+
+

我们为光照观察矩阵和正交投影矩阵使用了新的Uniform。

+

在片元着色器中,我们将创建一个新的函数来计算阴影,代码如下:

+
float calcShadow(vec4 position)
+{
+    float shadowFactor = 1.0;
+    vec3 projCoords = position.xyz;
+    // 从屏幕坐标变换到纹理坐标
+    projCoords = projCoords * 0.5 + 0.5;
+    if ( projCoords.z < texture(shadowMap, projCoords.xy).r ) 
+    {
+        // 当前片元不在阴影中
+        shadowFactor = 0;
+    }
+
+    return 1 - shadowFactor;
+}
+
+

该函数接收使用正交投影矩阵投影的光照观察空间的坐标。如果坐标在阴影中,则返回,如果不在阴影中,则返回。首先,将坐标转换为纹理坐标。屏幕坐标在范围内,但纹理坐标在范围内。我们通过坐标从纹理中获取深度值,并将其与片元坐标的值比较。如果值低于储存在纹理中的值,则表示片元不再阴影中。

+

在片元着色器中,calcShadow函数的返回值,用于调节点光源、聚光源和平行光对光照颜色的共享。环境光不受阴影的影响。

+
float shadow = calcShadow(mlightviewVertexPos);
+fragColor = clamp(ambientC * vec4(ambientLight, 1) + diffuseSpecularComp * shadow, 0, 1);
+
+

Renderer类的renderScene方法中,我们只需要传递正交投影和光照观察矩阵到Uniform(我们还需要修改着色器的初始化方法以创建新的Uniform)。你可以在本书的源代码中了解。

+

如果运行DummyGame类,该类已被修改为在带有平行光的平面上设置有悬浮的立方体,并可使用上下键修改角度,则应该看到如下情况。

+

阴影图结果

+

虽然阴影已经工作了(你可以通过移动光照方向来检查),但是实际会出现一些问题。首先,被照亮的物体中有奇怪的线条。这种情况被称为阴影失真(Shadow Acne),它是由储存深度图的纹理的分辨率有限造成的。第二个问题是,阴影的边界不平滑,看起来很粗糙。原因同样,纹理分辨率。我们将解决这些问题,以提高阴影质量。

+

改进阴影映射

+

既然我们已经有了阴影映射机制,那么让我们来解决现有的问题。我们先从失真问题开始。深度图纹理大小有限,因此,可以将多个片元映射到该纹理深度中的同一像素。纹理深度储存最小深度,因此到最后,我们有几个片元共享相同的深度,尽管它们的距离不同。

+

我们可以通过增加片元着色器中的深度比较来解决这个问题,我们添加了一个偏移。

+
float bias = 0.05;
+if ( projCoords.z - bias < texture(shadowMap, projCoords.xy).r ) 
+{
+    // 当前片元不在阴影中
+    shadowFactor = 0;
+}
+
+

现在,阴影失真消失了。

+

无阴影失真

+
+

译者注:使用偏移来消除阴影失真又会造成悬浮(Peter Panning)问题,另请参阅LearnOpenGL阴影映射一文。

+
+

现在我们要解决的是去阴影边缘问题,这也是由纹理分辨率引起的。对于每个片元,我们将使用片元的坐标值和周围的值对深度图进行采样。然后我们将计算平均值并将该值指定为阴影值。在此情况下,它的值不会是但可以在两者间取值,以获得更平滑的边缘。

+

深度平均值

+

在纹理坐标中,周围值必须与当前片元位置保持一个像素距离。所以我们需要计算纹理坐标中一个像素的增量,它等于

+

在片元着色器中,我们只需要修改阴影银子的计算来得到一个平均值。

+
float shadowFactor = 0.0;
+vec2 inc = 1.0 / textureSize(shadowMap, 0);
+for(int row = -1; row <= 1; ++row)
+{
+    for(int col = -1; col <= 1; ++col)
+    {
+        float textDepth = texture(shadowMap, projCoords.xy + vec2(row, col) * inc).r; 
+        shadowFactor += projCoords.z - bias > textDepth ? 1.0 : 0.0;        
+    }    
+}
+shadowFactor /= 9.0;
+
+

现在结果看起来更平滑了。

+

最终结果

+

现在我们的示例看起来好多了。尽管如此,这里介绍的阴影映射技术仍有很大的改进空间。你可以查看如何解决悬浮(Peter Panning)效果(因偏移引起)和其他改进阴影边缘的计算。无论如何,有了这里所讲解的概念,你就有了开始修改示例的良好基础。

+

为了渲染多个光源,你只需要为每个光源渲染一个深度图。在渲染场景时,你需要采样所有的深度图来计算合适的阴影系数。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/19-animations/index.html b/19-animations/index.html new file mode 100755 index 0000000..59a86cc --- /dev/null +++ b/19-animations/index.html @@ -0,0 +1,1004 @@ + + + + + + + + + + + + 动画 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

动画(Animations)

+

引言

+

到目前为止,我们已经加载了静态3D模型,在本章中,我们讲学习如何为它们设置动画。在思考动画时,首先想到的方法是为每个模型状态创建不同的网格,将它们加载到GPU中,然后按顺序绘制,以此造成动画的假象。虽然这种方法对于某些游戏来说是完美的,但是它的效率不是很高(就内存消耗来说)。

+

这就是骨骼动画(Skeletal Animation)的用武之地。在骨骼动画中,模型的动画方式由其底层骨架(Skeletal)定义,骨架是由被称为关节(Joints)的特殊点的层次结构定义的,这些关节又由它们的位置和旋转来定义。我们也说过这是一个层次结构,这意味着每个关键的最终位置都收到它们的父层次的影响。以手腕为例,如果角色移动肘部和肩膀,手腕的位置就会发生改变。

+

关节不需要表示一个合乎现实的骨骼或关节,它们是人工设置的,允许创作者建模动画。除了关节,我们还有顶点,这些顶点定义了构成3D模型的三角形。但在骨骼动画中,顶点是根据与之相关的关节的位置绘制的。

+

在本章中,我们将使用MD5格式来加载有动画的模型。MD5格式是由《毁灭战士》的开发商ID Software制定的,它基本上是一种基于文本的文件格式,易于理解。另一种方法是使用Collada格式,这是许多工具支持的公共标准。Collada是一种基于XML的文件格式,但它的缺点是非常复杂(1.5版本的规范就有500多页)。因此,我们将坚持使用一种更简单的格式,MD5,它使我们专注于骨骼动画的概念并创建一个可工作的示例。

+

你还可以通过在互联网上找到特定插件将一些模型从Blender导出为MD5格式。

+

在本章中,我参考了许多不同的资料,但是我发现有两个资料可以很好地解释如何使用MD5文件创建动画模型。这些资料的来源如下:

+ +

让我们从编写解析MD5文件的代码开始,MD5格式定义了两种类型的文件:

+
    +
  • 网格定义文件:它定义了构成3D模型的网格集的关节、顶点和纹理。这个文件通常有一个名为.md5mesh的扩展名。
  • +
  • 动画定义文件:定义可使用于模型的动画。这个文件通常有一个名为.md5anim的扩展名。
  • +
+

MD5文件由头(Header)和大括号之间包含的不同部分组成。让我们开始查看网格定义文件。在参考资料文件夹中,你将发现几种MD5格式的模型。如果你打开其中一个,你会看到类似这样的结构。

+

MD5结构

+

在网格定义文件中可以看到的第一个结构是头。你可以在下述提供的示例中,看到头的内容:

+
MD5Version 10
+commandline ""
+
+numJoints 33
+numMeshes 6
+
+

头定义了如下属性:

+
    +
  • 它所遵循的MD5规范的版本。
  • +
  • 用于(从3D建模工具)生成此文件的命令。
  • +
  • 在关节部分定义的关节数。
  • +
  • 网格数(需要的网格节数)。
  • +
+

关键部分定义关节的名称、状态、位置及其关系。下面展示了一个示例模型的关节部分的片段:

+
joints {
+    "origin"    -1 ( -0.000000 0.016430 -0.006044 ) ( 0.707107 0.000000 0.707107 )        // 
+    "sheath"    0 ( 11.004813 -3.177138 31.702473 ) ( 0.307041 -0.578614 0.354181 )        // origin
+    "sword"    1 ( 9.809593 -9.361549 40.753730 ) ( 0.305557 -0.578155 0.353505 )        // sheath
+    "pubis"    0 ( 0.014076 2.064442 26.144581 ) ( -0.466932 -0.531013 -0.466932 )        // origin
+              ……
+}
+
+

关节由以下属性定义:

+
    +
  • 关节名称,引号中的文本属性。
  • +
  • 关节的父关节,使用索引,该索引使用父关节在列表中的位置指向父关节。根关节的父关节等于-1。
  • +
  • 关节位置,在模型空间坐标系中定义。
  • +
  • 关节方向,也在模型空间坐标系中定义,方向实际上是一个四元数,但其w分量不包括在此。
  • +
+

在继续解释文件的其余部分之前,我们先来谈谈四元数(Quaternions)。四元数是用于表示旋转的四个构成元素。到目前为止,我们一直在使用欧拉角(偏航、俯仰和滚转)来定义旋转,这基本上定义了围绕x、y和z叫的旋转。但是,欧拉角在处理旋转时会出现一些问题,特别是你必须知道正确的旋转顺序,并且一些操作会变得非常复杂。

+

四元数有助于解决这种复杂情况。正如之前所说,四元数被定义为4个数字(x,y,z,w)一组。四元数定义旋转轴和围绕该轴的旋转角度。

+

四元数

+

你可以在网络中确认每个元素的数学定义,但好消息是我们使用的数学库JOML为其提供了支持。我们可以基于四元数构造旋转矩阵,并用它们对向量进行变换。

+

让我们回到关节的定义,其缺少元素,但可以在其他值的帮助下轻松地计算它。你可以查看源代码,看看这是如何做到的。

+

在关节定义之后,可以找到组成模型的不同网格的定义。接下来你可以从其中一个示例中看到网格定义的片段:

+
mesh {
+    shader "/textures/bob/guard1_body.png"
+
+    numverts 494
+    vert 0 ( 0.394531 0.513672 ) 0 1
+    vert 1 ( 0.447266 0.449219 ) 1 2
+    ...
+    vert 493 ( 0.683594 0.455078 ) 864 3
+
+    numtris 628
+    tri 0 0 2 1
+    tri 1 0 1 3
+    ...
+    tri 627 471 479 493
+
+    numweights 867
+    weight 0 5 1.000000 ( 6.175774 8.105262 -0.023020 )
+    weight 1 5 0.500000 ( 4.880173 12.805251 4.196980 )
+    ...
+    weight 866 6 0.333333 ( 1.266308 -0.302701 8.949338 )
+}
+
+

让我们看看上述展现的结构:

+
    +
  • 网格从定义纹理文件开始。请记住,你在此处找到的路径是创建该模型的工具所使用的路径。该路径可能与用于加载这些文件的路径不匹配。这里有两种方法解决,要么动态修改基本路径,要么手动修改该路径。我选择了后者,比较简单的一种。
  • +
  • 接下来可以找到顶点定义。顶点由以下属性定义:
  • +
  • 顶点索引。
  • +
  • 纹理坐标。
  • +
  • 影响此顶点的第一个权重定义的索引。
  • +
  • 要考虑的权重数。
  • +
  • 在顶点之后,将定义构成此网格的三角形。三角形定义了使用顶点索引组织顶点的方式。
  • +
  • 最后,定义了权重。权重定义由以下部分组成:
  • +
  • 权重指数。
  • +
  • 关节指数,指与该权重相关的关节
  • +
  • 偏倚系数,用于调节该权重的影响。
  • +
  • 该权重的位置。
  • +
+

下图用示例数据说明了上述组分之间的关系。

+

网格元素

+

好了,现在了解了网格模型文件,我们可以解析它了。如果你看了源代码,将看到已经创建了新的包来容纳模型格式的解析器。在org.lwjglb.engine.loaders.obj包下有一个解析OBJ文件的代码,而解析MD5文件的代码在org.lwjglb.engine.loaders.md5包下。

+

所有的解析代码都基于正则表达式从MD5文本文件中提取信息。解析器将创建一个层次结构对象,以模拟MD5文件中包含的信息组件的结构。它可能不是世界上最高效的解析器,但我认为它将有助于更好地理解这个过程。

+

解析MD5模型文件的起始类是MD5Model类,该类在其解析方法中作为参数被接收。MD5文件的内容是创建一个包含头、节点列表和所有子元素的网格列表的层次结构。代码非常简单,所以不包含在本文中了。

+

关于解析代码的一些注释:

+
    +
  • 网格的子元素被定义为MD5Mesh类的内部类。
  • +
  • 你可以查看如何在MD5Utils类的calculateQuaternion方法中计算关节方向的第四个分量。
  • +
+

既然我们已经解析了一个文件,我们必须讲这个对象层次结构转换成可以由游戏引擎处理的东西,我们必须创建一个GameItem实例。为了实现它,我们将创建一个名为MD5Loader的新类,该类将使用一个MD5Model实例来构造一个GameItem

+

在开始之前,如你所注意到的,MD5模型有好几个网格,但我们的GameItem类只支持单个网格。所以首先我们要修改它,GameItem类现在看起来是这样的:

+
package org.lwjglb.engine.items;
+
+import org.joml.Vector3f;
+import org.lwjglb.engine.graph.Mesh;
+
+public class GameItem {
+
+    private Mesh[] meshes;
+
+    private final Vector3f position;
+
+    private float scale;
+
+    private final Vector3f rotation;
+
+    public GameItem() {
+        position = new Vector3f(0, 0, 0);
+        scale = 1;
+        rotation = new Vector3f(0, 0, 0);
+    }
+
+    public GameItem(Mesh mesh) {
+        this();
+        this.meshes = new Mesh[]{mesh};
+    }
+
+    public GameItem(Mesh[] meshes) {
+        this();
+        this.meshes = meshes;
+    }
+
+    public Vector3f getPosition() {
+        return position;
+    }
+
+    public void setPosition(float x, float y, float z) {
+        this.position.x = x;
+        this.position.y = y;
+        this.position.z = z;
+    }
+
+    public float getScale() {
+        return scale;
+    }
+
+    public void setScale(float scale) {
+        this.scale = scale;
+    }
+
+    public Vector3f getRotation() {
+        return rotation;
+    }
+
+    public void setRotation(float x, float y, float z) {
+        this.rotation.x = x;
+        this.rotation.y = y;
+        this.rotation.z = z;
+    }
+
+    public Mesh getMesh() {
+        return meshes[0];
+    }
+
+    public Mesh[] getMeshes() {
+        return meshes;
+    }
+
+    public void setMeshes(Mesh[] meshes) {
+        this.meshes = meshes;
+    }
+
+    public void setMesh(Mesh mesh) {
+        if (this.meshes != null) {
+            for (Mesh currMesh : meshes) {
+                currMesh.cleanUp();
+            }
+        }
+        this.meshes = new Mesh[]{mesh};
+    }
+}
+
+

通过上述修改,我们现在可以编写MD5Loader类的内容。该类将有一个名为process的方法,该方法将接受一个MD5Model实例和一个默认颜色(对于不定义纹理的网格),并返回一个GameItem实例。该方法的方法体如下:

+
public static GameItem process(MD5Model md5Model, Vector4f defaultColour) throws Exception {
+    List<MD5Mesh> md5MeshList = md5Model.getMeshes();
+
+    List<Mesh> list = new ArrayList<>();
+    for (MD5Mesh md5Mesh : md5Model.getMeshes()) {
+        Mesh mesh = generateMesh(md5Model, md5Mesh, defaultColour);
+        handleTexture(mesh, md5Mesh, defaultColour);
+        list.add(mesh);
+    }
+    Mesh[] meshes = new Mesh[list.size()];
+    meshes = list.toArray(meshes);
+    GameItem gameItem = new GameItem(meshes);
+
+    return gameItem;
+}
+
+

如你所见,我们只需将定义在MD5Model类之内的网格进行遍历,并通过使用generateMesh方法,将其转换为org.lwjglb.engine.graph.Mesh类的实例。在查看该方法之前,我们将创建一个内部类,它将为我们构建坐标和法线数组。

+
private static class VertexInfo {
+
+    public Vector3f position;
+
+    public Vector3f normal;
+
+    public VertexInfo(Vector3f position) {
+        this.position = position;
+        normal = new Vector3f(0, 0, 0);
+    }
+
+    public VertexInfo() {
+        position = new Vector3f();
+        normal = new Vector3f();
+    }
+
+    public static float[] toPositionsArr(List<VertexInfo> list) {
+        int length = list != null ? list.size() * 3 : 0;
+        float[] result = new float[length];
+        int i = 0;
+        for (VertexInfo v : list) {
+            result[i] = v.position.x;
+            result[i + 1] = v.position.y;
+            result[i + 2] = v.position.z;
+            i += 3;
+        }
+        return result;
+    }
+
+    public static float[] toNormalArr(List<VertexInfo> list) {
+        int length = list != null ? list.size() * 3 : 0;
+        float[] result = new float[length];
+        int i = 0;
+        for (VertexInfo v : list) {
+            result[i] = v.normal.x;
+            result[i + 1] = v.normal.y;
+            result[i + 2] = v.normal.z;
+            i += 3;
+        }
+        return result;
+    }
+}
+
+

回到generateMesh方法,首先我们得到关节的网格顶点信息、权重和结构。

+
private static Mesh generateMesh(MD5Model md5Model, MD5Mesh md5Mesh, Vector4f defaultColour) throws Exception {
+    List<VertexInfo> vertexInfoList = new ArrayList<>();
+    List<Float> textCoords = new ArrayList<>();
+    List<Integer> indices = new ArrayList<>();
+
+    List<MD5Mesh.MD5Vertex> vertices = md5Mesh.getVertices();
+    List<MD5Mesh.MD5Weight> weights = md5Mesh.getWeights();
+    List<MD5JointInfo.MD5JointData> joints = md5Model.getJointInfo().getJoints();
+
+

接下来我们需要根据包含在权重和关节中的信息来计算顶点位置。这是在下述代码块中完成的:

+
    for (MD5Mesh.MD5Vertex vertex : vertices) {
+        Vector3f vertexPos = new Vector3f();
+        Vector2f vertexTextCoords = vertex.getTextCoords();
+        textCoords.add(vertexTextCoords.x);
+        textCoords.add(vertexTextCoords.y);
+
+        int startWeight = vertex.getStartWeight();
+        int numWeights = vertex.getWeightCount();
+
+        for (int i = startWeight; i < startWeight + numWeights; i++) {
+            MD5Mesh.MD5Weight weight = weights.get(i);
+            MD5JointInfo.MD5JointData joint = joints.get(weight.getJointIndex());
+            Vector3f rotatedPos = new Vector3f(weight.getPosition()).rotate(joint.getOrientation());
+            Vector3f acumPos = new Vector3f(joint.getPosition()).add(rotatedPos);
+            acumPos.mul(weight.getBias());
+            vertexPos.add(acumPos);
+        }
+
+       vertexInfoList.add(new VertexInfo(vertexPos));
+    }
+
+

让我们来看看在这里做了什么。我们遍历了顶点信息并将纹理坐标储存在列表中,不需要在这里应用任何变换。然后我们得到了计算顶点坐标所需考虑的起始权重和总权重。

+

顶点坐标是通过使用所有与之相关的权重来计算的。每个权重都有一个坐标和一个偏倚。与每个顶点相关的权重的所有偏倚之和必须为1.0。每个权重也有一个在关节的局部空间中定义的坐标,因此我们需要使用关节的方向和坐标(就像它是一个转换矩阵那样)将其转换为它所引用的模型空间坐标。

+

综上所述,顶点坐标可以用如下公式表示:

+

+ +

+

参数:

+
    +
  • (起始权重)到(权重数)权重总和。
  • +
  • +是与权重相关的关节的变换矩阵。
  • +
  • +是权重坐标。
  • +
  • +是权重偏倚。
  • +
+

该方程是在循环体中实现的(我们没有变换矩阵,但结果是相同的,因为我们有单独的关节坐标和旋转)。

+

使用上述代码,我们就能够构造坐标和纹理坐标数据,但是仍然需要建立索引和法线。索引可以通过使用三角形的信息计算,只需遍历包含这些三角形的列表即可。

+

法线也可以用三角形信息来计算,令为三角形顶点(在物体的模型空间中)。三角形的法线可以根据如下公式计算:

+

+ +

+

其中N应该归一化。下图是上述公式的几何解释:

+

法线计算

+

对于每个顶点,我们通过它所属的三角形的所有法线的归一化之和来计算它的法线。进行计算的代码如下所示:

+
    for (MD5Mesh.MD5Triangle tri : md5Mesh.getTriangles()) {
+        indices.add(tri.getVertex0());
+        indices.add(tri.getVertex1());
+        indices.add(tri.getVertex2());
+
+        // 法线
+        VertexInfo v0 = vertexInfoList.get(tri.getVertex0());
+        VertexInfo v1 = vertexInfoList.get(tri.getVertex1());
+        VertexInfo v2 = vertexInfoList.get(tri.getVertex2());
+        Vector3f pos0 = v0.position;
+        Vector3f pos1 = v1.position;
+        Vector3f pos2 = v2.position;
+
+        Vector3f normal = (new Vector3f(pos2).sub(pos0)).cross(new Vector3f(pos1).sub(pos0));
+
+        v0.normal.add(normal);
+        v1.normal.add(normal);
+        v2.normal.add(normal);
+     }
+
+     // 一旦完成了计算,就将结果归一化
+     for(VertexInfo v : vertexInfoList) {
+        v.normal.normalize();
+    }
+
+

然后我们只需要将列表转换为数组并处理纹理信息。

+
     float[] positionsArr = VertexInfo.toPositionsArr(vertexInfoList);
+     float[] textCoordsArr = Utils.listToArray(textCoords);
+     float[] normalsArr = VertexInfo.toNormalArr(vertexInfoList);
+     int[] indicesArr = indices.stream().mapToInt(i -> i).toArray();
+     Mesh mesh = new Mesh(positionsArr, textCoordsArr, normalsArr, indicesArr);
+
+     return mesh;
+}
+
+

回到process方法,你可以看到有个名为handleTexture的方法,它负责加载纹理。这就是该方法的定义:

+
private static void handleTexture(Mesh mesh, MD5Mesh md5Mesh, Vector4f defaultColour) throws Exception {
+    String texturePath = md5Mesh.getTexture();
+    if (texturePath != null && texturePath.length() > 0) {
+        Texture texture = new Texture(texturePath);
+        Material material = new Material(texture);
+
+        // 处理法线图
+        int pos = texturePath.lastIndexOf(".");
+        if (pos > 0) {
+            String basePath = texturePath.substring(0, pos);
+            String extension = texturePath.substring(pos, texturePath.length());
+            String normalMapFileName = basePath + NORMAL_FILE_SUFFIX + extension;
+            if (Utils.existsResourceFile(normalMapFileName)) {
+                Texture normalMap = new Texture(normalMapFileName);
+                material.setNormalMap(normalMap);
+            }
+        }
+        mesh.setMaterial(material);
+    } else {
+        mesh.setMaterial(new Material(defaultColour, 1));
+    }
+}
+
+

非常直接的实现。唯一的独特之处在于如果一个网格定义了一个名为“texture.png”的纹理,它的法线纹理图将在文件“texture_normal.png”中定义。我们需要检查该文件是否存在并相应地加载它。

+

我们现在可以加载一个MD5文件并像渲染其他游戏项一样渲染它,但在此之前,我们需要禁用面剔除来正确渲染它,因为不是所有三角形都将绘制在正确的方向上。我们将向Window类添加支持,以便在运行时设置这些参数(你可以在源代码中查看其变更)。

+

如果加载一些实例模型,就会得到类似这样的结果:

+

绑定的姿势

+

你在此处看到的是绑定的姿势,它是MD5模型的静态展示,使动画师轻松地对它们建模。为了让动画工作,我们必须处理动画定义文件。

+

模型动画

+

MD5动画定义文件,就像模型定义文件那样,由一个头和一个包含在大括号之间的不同部分组成。如果打开其中一个文件,可以看到类似的结构。

+

MD5动画结构

+

可以在动画文件中找到的第一个结构(就像是网格定义文件一样)是头。你可以从接下来提供的一个例子中看到头的内容:

+
MD5Version 10
+commandline ""
+
+numFrames 140
+numJoints 33
+frameRate 24
+numAnimatedComponents 198
+
+

头定义了以下属性:

+
    +
  • 所符合的MD5规范的版本。
  • +
  • 用于生成此文件的命令(来自3D建模工具)
  • +
  • 文件中定义的帧数。
  • +
  • 层次结构部分中定义的关节数量。
  • +
  • 帧速率,每秒帧数,用于创建动画。这个参数可以用来计算帧与帧之间的时间。
  • +
  • 每个帧定义的分量数量。(译注:通常情况下等于关节数乘以六)
  • +
+

首先出现的是层次结构(Hierarchy)部分,它定义了该动画的关节。你可以看到以下片段:

+
hierarchy {
+    "origin"    -1 0 0    //
+    "body"    0 63 0    // origin ( Tx Ty Tz Qx Qy Qz )
+    "body2"    1 0 0    // body
+    "SPINNER"    2 56 6    // body2 ( Qx Qy Qz )
+    ....
+}
+
+

一个关节,在层次结构部分中,由以下属性定义:

+
    +
  • 关节名,引号之间的一个文本属性
  • +
  • 关节的父关节,使用一个索引,该索引使用其在关节列表中的位置指向父关节。根关节的父节点等于-1。
  • +
  • 关节标志,根据每个动画帧中定义的数据,设置该关节的位置和方向将如何改变。
  • +
  • 起始索引,当应用标志时,用于每帧的动画数据内。
  • +
+

下一节是边界(Bounds)。本节定义了每个动画帧的模型的边界框。它将为每一帧动画储存一行数据,看起来就像是这样:

+
bounds {
+    ( -24.3102264404 -44.2608566284 -0.181215778 ) ( 31.0861988068 38.7131576538 117.7417449951 )
+    ( -24.3102283478 -44.1887664795 -0.1794649214 ) ( 31.1800289154 38.7173080444 117.7729110718 )
+    ( -24.3102359772 -44.1144447327 -0.1794776917 ) ( 31.2042789459 38.7091217041 117.8352737427 )
+    ....
+}
+
+

每个边界框由模型空间坐标中的两个三分量向量定义。第一个向量定义了最小值,第二个向量定义了最大值。

+

下一节是基本帧(Base Frame)数据。在本节中,在应用每个动画帧的形变之前,设置每个关节的位置和方向。你可以看到下面的片段:

+
baseframe {
+    ( 0 0 0 ) ( -0.5 -0.5 -0.5 )
+    ( -0.8947336078 70.7142486572 -6.5027675629 ) ( -0.3258574307 -0.0083037354 0.0313780755 )
+    ( 0.0000001462 0.0539700091 -0.0137935728 ) ( 0 0 0 )
+    ....
+}
+
+

每一行都与一个关节相关联,并定义了以下属性:

+
    +
  • 关节的坐标,是一个三分量向量。
  • +
  • 关节的方向,是一个四元数的三个分量(正如模型文件里的那样)。
  • +
+

在此之后,你将发现几个帧定义,以及分配给numFrames头属性的值。每个帧的节就像是一个巨大的浮点数组,当对每个帧应用变换时,节点将使用这个浮点数组。你可以在接下来看到一个片段:

+
frame 1 {
+     -0.9279100895 70.682762146 -6.3709330559 -0.3259022534 -0.0100501738 0.0320306309
+     0.3259022534 0.0100501738 -0.0320306309
+     -0.1038384438 -0.1639953405 -0.0152553488 0.0299418624
+     ....
+}
+
+

解析MD5动画文件的基类名为MD5AnimModel。该类创建由该文件内容映射的所有对象层次结构,你可以查看源代码以获得详细信息。结构类似于MD5模型定义文件。现在我们能够加载这些数据,并将使用它来生成动画。

+

我们将在着色器中生成动画,所以不是预先计算每个帧的所有坐标,我们需要准备所需的数据,这样在顶点着色器中就可以计算最终坐标。

+

让我们回到MD5Loader类中的process方法,需要修改它以考虑动画数据。新方法的定义如下:

+
public static AnimGameItem process(MD5Model md5Model, MD5AnimModel animModel, Vector4f defaultColour) throws Exception {
+    List<Matrix4f> invJointMatrices = calcInJointMatrices(md5Model);
+    List<AnimatedFrame> animatedFrames = processAnimationFrames(md5Model, animModel, invJointMatrices);
+
+    List<Mesh> list = new ArrayList<>();
+    for (MD5Mesh md5Mesh : md5Model.getMeshes()) {
+        Mesh mesh = generateMesh(md5Model, md5Mesh);
+        handleTexture(mesh, md5Mesh, defaultColour);
+        list.add(mesh);
+    }
+
+    Mesh[] meshes = new Mesh[list.size()];
+    meshes = list.toArray(meshes);
+
+    AnimGameItem result = new AnimGameItem(meshes, animatedFrames, invJointMatrices);
+    return result;
+}
+
+

这里有一些变化,最明显的是该方法现在接收一个MD5AnimModel实例。此外,我们不返回GameItem实例,而是返回AnimGameItem实例。该类继承自GameItem类,但添加了对动画的支持。稍后我们将看到为什么这样做。

+

如果我们继续阅读该处理方法,首先要做的是调用calcInJointMatrices方法,其定义如下:

+
private static List<Matrix4f> calcInJointMatrices(MD5Model md5Model) {
+    List<Matrix4f> result = new ArrayList<>();
+
+    List<MD5JointInfo.MD5JointData> joints = md5Model.getJointInfo().getJoints();
+    for(MD5JointInfo.MD5JointData joint : joints) {
+        Matrix4f translateMat = new Matrix4f().translate(joint.getPosition());
+        Matrix4f rotationMat = new Matrix4f().rotate(joint.getOrientation());
+        Matrix4f mat = translateMat.mul(rotationMat);
+        mat.invert();
+        result.add(mat);
+    } 
+    return result;
+}
+
+

该方法遍历MD5模型定义文件中包含的节点,计算与每个节点相关联的转换矩阵,然后得到这些矩阵的逆矩阵。此数据用于构造AnimGameItem实例。

+

让我们继续阅读process方法,接下来要做的是调用processAnimationFrames方法来处理动画帧:

+
private static List<AnimatedFrame> processAnimationFrames(MD5Model md5Model, MD5AnimModel animModel, List<Matrix4f> invJointMatrices) {
+    List<AnimatedFrame> animatedFrames = new ArrayList<>();
+    List<MD5Frame> frames = animModel.getFrames();
+    for(MD5Frame frame : frames) {
+        AnimatedFrame data = processAnimationFrame(md5Model, animModel, frame, invJointMatrices);
+        animatedFrames.add(data);
+    }
+    return animatedFrames;
+}
+
+

该方法处理MD5动画定义文件中定义的每个动画帧,并返回一个AnimatedFrame实例的列表。真正的工作是在processAnimationFrame方法中完成的。让我来解释一下这个方法的作用。

+

首先,遍历MD5动画文件的层次结构部分中定义的关节。

+
private static AnimatedFrame processAnimationFrame(MD5Model md5Model, MD5AnimModel animModel, MD5Frame frame, List<Matrix4f> invJointMatrices) {
+    AnimatedFrame result = new AnimatedFrame();
+
+    MD5BaseFrame baseFrame = animModel.getBaseFrame();
+    List<MD5Hierarchy.MD5HierarchyData> hierarchyList = animModel.getHierarchy().getHierarchyDataList();
+
+    List<MD5JointInfo.MD5JointData> joints = md5Model.getJointInfo().getJoints();
+    int numJoints = joints.size();
+    float[] frameData = frame.getFrameData();
+    for (int i = 0; i < numJoints; i++) {
+        MD5JointInfo.MD5JointData joint = joints.get(i);
+
+

我们得到与每个关节相关联的基本帧元素的位置和方向。

+
        MD5BaseFrame.MD5BaseFrameData baseFrameData = baseFrame.getFrameDataList().get(i);
+        Vector3f position = baseFrameData.getPosition();
+        Quaternionf orientation = baseFrameData.getOrientation();
+
+

原则上,该数据应分配给关节的位置和方向,但它需要根据关节的标志进行转换。如果你还记得,在展示动画文件的结构时,层次结构部分中的每个节点都定义了一个标志。该标志根据每个动画帧中定义的信息决定建模位置和方向应该如何更改。

+

如果标志字段的第一个位等于1,我们应该使用正在处理的动画帧中包含的数据更改基本帧坐标的x分量。动画帧定义了一个浮点数组,所以我们应该取哪个元素呢?答案也在关节定义中,其中包含startIndex属性。如果标志的第二个位等于1,我们应该用的值更改基本帧坐标的y分量,以此类推,接下来的是坐标的z分量,以及方向的x、y和z分量。

+
        int flags = hierarchyList.get(i).getFlags();
+        int startIndex = hierarchyList.get(i).getStartIndex();
+
+        if ( (flags & 1 ) > 0) {
+            position.x = frameData[startIndex++];
+        }
+        if ( (flags & 2) > 0) {
+            position.y = frameData[startIndex++];
+        }
+        if ( (flags & 4) > 0) {
+            position.z = frameData[startIndex++];
+        }
+        if ( (flags & 8) > 0) {
+            orientation.x = frameData[startIndex++];
+        }
+        if ( (flags & 16) > 0) {
+            orientation.y = frameData[startIndex++];
+        }
+        if ( (flags & 32) > 0) {
+            orientation.z = frameData[startIndex++];
+        }
+        // 更新四元数的w分量
+        orientation = MD5Utils.calculateQuaternion(orientation.x, orientation.y, orientation.z);
+
+

现在我们有了计算变换矩阵所需的所有数据,从而得到当前动画帧的每个关节的最终位置。但是我们必须考虑另一件事,每个关节的位置是相对于它的父关节的位置的,所以我们需要得到与每个父关节相关的变换矩阵并用它来得到模型空间坐标中的变换矩阵。

+
        // 计算这个关节的平移和旋转矩阵
+        Matrix4f translateMat = new Matrix4f().translate(position);
+        Matrix4f rotationMat = new Matrix4f().rotate(orientation);
+        Matrix4f jointMat = translateMat.mul(rotationMat);
+
+        // 关节位置是相对于关节的父索引的位置。
+        // 使用父矩阵将其转换为模型空间。
+        if ( joint.getParentIndex() > -1 ) {
+            Matrix4f parentMatrix = result.getLocalJointMatrices()[joint.getParentIndex()];
+            jointMat = new Matrix4f(parentMatrix).mul(jointMat);
+        }
+
+        result.setMatrix(i, jointMat, invJointMatrices.get(i));
+    }
+
+    return result;
+}
+
+

你可以看到,我们创建了AnimatedFrame类的一个实例,该类包含将在动画期间使用的数据。这个类也使用逆矩阵,稍后我们会知道为什么这样做。需要注意的一点是,AnimatedFramesetMatrix方法是这样定义的:

+
public void setMatrix(int pos, Matrix4f localJointMatrix, Matrix4f invJointMatrix) {
+    localJointMatrices[pos] = localJointMatrix;
+    Matrix4f mat = new Matrix4f(localJointMatrix);
+    mat.mul(invJointMatrix);
+    jointMatrices[pos] = mat;
+}
+
+

变量localJointMatrix储存当前帧中占据位置“i”的关节的旋转矩阵,invJointMatrix持有占据绑定姿势位置“i”位置的关节的逆变换矩阵。我们储存了localJointMatrixinvJointMatrix矩阵相乘的结果。这个结果将在稍后用于计算最终坐标。我们还储存了原始的关节变换矩阵,变量localJointMatrix,所以我们可以用它来计算子关节的变换矩阵。

+

让我们回到MD5Loader类,generateMesh方法也发生了变化,如我们之前说明的那样计算绑定姿势的坐标,但对于每个顶点,我们储存两个数组:

+
    +
  • 一个数组,储存着与该顶点相关的权重偏倚。
  • +
  • 一个输出,储存这与该顶点相关的关节索引(通过权重)。
  • +
+

我们将这些数组的大小限制为4。Mesh类也被修改为接收这些参数,并将其包含在着色器处理的VAO数据中。你可以在源代码中查看详细内容,但来回顾一下我们所做的:

+
    +
  • 我们仍在加载绑定姿势,通过权重数据计算出它们的最终位置,即关节坐标和方向的总和。
  • +
  • 这些数据以VBO的形式加载到着色器中,但是它由与每个顶点相关的权重的偏倚和影响它的关节的索引来补充。这个数据对所有动画帧都是通用的,因为它是在MD5定义文件中定义的。这就是我们限制偏倚和关节索引数组大小的原因,当模型被发送到GPU时,它们将被加载为VBO。
  • +
  • 对于每个动画帧,我们根据基础帧中定义的位置和方向,储存应用于每个关节的变换矩阵。
  • +
  • 我们还计算了定义绑定姿势的关节相关的变换矩阵的逆矩阵。也就是说,我们知道如何撤销绑定姿势中完成的变换,稍后将看到如何应用它。
  • +
+

静态VAO对比动态VAO

+

现在我们已经有了拼图的所有碎片,只需要在着色器中使用它们。我首先需要修改输入数据来接收权重和关节索引。

+
#version 330
+
+const int MAX_WEIGHTS = 4;
+const int MAX_JOINTS = 150;
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+layout (location=3) in vec4 jointWeights;
+layout (location=4) in ivec4 jointIndices;
+
+

我们定义了两个常量:

+
    +
  • MAX_WEIGHTS,定义权重VBO(一个单独的关节索引)中的权重的最大数量。
  • +
  • MAX_JOINTS,定义了我们将支持的最大关节数量(稍后将详细介绍)。
  • +
+

然后我们定义输出数据和Uniform。

+
out vec2 outTexCoord;
+out vec3 mvVertexNormal;
+out vec3 mvVertexPos;
+out vec4 mlightviewVertexPos;
+out mat4 outModelViewMatrix;
+
+uniform mat4 jointsMatrix[MAX_JOINTS];
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+uniform mat4 modelLightViewMatrix;
+uniform mat4 orthoProjectionMatrix;
+
+

你可以看到,我们有一个名为jointsMatrix的新Uniform,它是一个矩阵数组(最大长度由MAX_JOINTS常量设置)。该矩阵数组包含当前帧中所有关节的关节矩阵,并在处理帧时在MD5Loader类中计算。因此,该数组包含需要应用于当前动画帧中所有关节的变换,并将作为计算顶点最终坐标的基础。

+

使用VBO中的新数据和该Uniform,我们将变换绑定姿势的坐标。这将在下述代码块中完成:

+
    vec4 initPos = vec4(0, 0, 0, 0);
+    int count = 0;
+    for(int i = 0; i < MAX_WEIGHTS; i++)
+    {
+        float weight = jointWeights[i];
+        if(weight > 0) {
+            count++;
+            int jointIndex = jointIndices[i];
+            vec4 tmpPos = jointsMatrix[jointIndex] * vec4(position, 1.0);
+            initPos += weight * tmpPos;
+        }
+    }
+    if (count == 0)
+    {
+        initPos = vec4(position, 1.0);
+    }
+
+

首先,我们得到绑定姿势的坐标,遍历与这个顶点关联的权重,并通过使用储存在输入中的索引,使用该帧(储存在jointsMatrixUniform中)的权重和关节矩阵修改坐标。

+

关于jointsMatrix

+

因此,给定一个顶点坐标,我们计算它的帧坐标:

+

+ +

+

参数:

+
    +
  • +是顶点的最终坐标。
  • +
  • +是顶点的权重。
  • +
  • +是这个坐标系的关节变换矩阵。
  • +
  • +是绑定姿势的关节变换矩阵的逆矩阵。这个矩阵与的成绩储存在jointsMatrixUniform中。
  • +
  • +是绑定姿势中的顶点坐标。
  • +
+

+矩阵计算,这是绑定姿势的关节变换矩阵的矩阵。所以,最后我们要撤销绑定姿势的修改来应用该坐标系的变换。这就是我们需要逆绑定姿势矩阵的原因。

+

着色器支持权重可变的顶点,最多可达4个,还可以渲染非动画项。在此情况下,权重等于0我们将得到原始坐标。

+

着色器的其余部分或多或少保持不变,我们只是使用更新后的坐标并传递片元着色器要使用的正确值。

+
    vec4 mvPos = modelViewMatrix * initPos;
+    gl_Position = projectionMatrix * mvPos;
+    outTexCoord = texCoord;
+    mvVertexNormal = normalize(modelViewMatrix * vec4(vertexNormal, 0.0)).xyz;
+    mvVertexPos = mvPos.xyz;
+    mlightviewVertexPos = orthoProjectionMatrix * modelLightViewMatrix * vec4(position, 1.0);
+    outModelViewMatrix = modelViewMatrix;
+}
+
+

所以,为了测试动画,我们只需要将jointsMatrix传递给着色器。由于此信息仅储存在AnimGameItem实例中,因此代码非常简单。在渲染网格的循环中,我们添加了如下代码片段:

+
if ( gameItem instanceof AnimGameItem ) {
+    AnimGameItem animGameItem = (AnimGameItem)gameItem;
+    AnimatedFrame frame = animGameItem.getCurrentFrame();
+    sceneShaderProgram.setUniform("jointsMatrix", frame.getJointMatrices());
+}
+
+

当然,在使用它之前,你需要创建Uniform,你可以查看该类的源代码。如果运行这个示例,你将能够通过按下空格键来查看模型是如何动起来的(每次按下这个键,都会设置一个新的帧,并且jointsMatrixUniform会发生变化)。

+

你将看到如下所示的东西:

+

第一帧动画

+

虽然动画很流畅,但示例还是存在一些问题。首先,光照没有正常的工作,阴影表现的是绑定姿势,而不是当前帧。我们现在将解决所有这些问题。

+

修正动画问题

+

第一个要解决的问题是光照问题。你可能已经注意到这种情况了,这是因为我们没有变换法线。因此,片元着色器中使用的法线与绑定姿势相对应。我们需要像变换位置一样变换它们。

+

这个问题很好解决,我们只需要在循环中将法线也囊括到顶点着色器中的权重遍历。

+
    vec4 initPos = vec4(0, 0, 0, 0);
+    vec4 initNormal = vec4(0, 0, 0, 0);
+    int count = 0;
+    for(int i = 0; i < MAX_WEIGHTS; i++)
+    {
+        float weight = jointWeights[i];
+        if(weight > 0) {
+            count++;
+            int jointIndex = jointIndices[i];
+            vec4 tmpPos = jointsMatrix[jointIndex] * vec4(position, 1.0);
+            initPos += weight * tmpPos;
+
+            vec4 tmpNormal = jointsMatrix[jointIndex] * vec4(vertexNormal, 0.0);
+            initNormal += weight * tmpNormal;
+        }
+    }
+    if (count == 0)
+    {
+        initPos = vec4(position, 1.0);
+        initNormal = vec4(vertexNormal, 0.0);
+    }
+
+

然后我们像往常一样计算输出顶点法线向量:

+
mvVertexNormal = normalize(modelViewMatrix * initNormal).xyz;
+
+

接下来的问题是阴影问题。如果你记得在阴影一章中,我们使用阴影图绘制阴影。我们现在正从光照透视渲染场景,以便创建一个深度图,它告诉我们一个点是否在阴影中。但是,在法线的情况下,我们只是通过绑定姿势的坐标,而不是根据当前帧来改变它们。这就是阴影与当前坐标不一致的原因。

+

解决方法也很简单,我们只需要修改深度顶点着色器使用jointsMatrix、权重和关节索引来变换坐标。这就是深度顶点着色器:

+
#version 330
+
+const int MAX_WEIGHTS = 4;
+const int MAX_JOINTS = 150;
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+layout (location=3) in vec4 jointWeights;
+layout (location=4) in ivec4 jointIndices;
+
+uniform mat4 jointsMatrix[MAX_JOINTS];
+uniform mat4 modelLightViewMatrix;
+uniform mat4 orthoProjectionMatrix;
+
+void main()
+{
+    vec4 initPos = vec4(0, 0, 0, 0);
+    int count = 0;
+    for(int i = 0; i < MAX_WEIGHTS; i++)
+    {
+        float weight = jointWeights[i];
+        if(weight > 0) {
+            count++;
+            int jointIndex = jointIndices[i];
+            vec4 tmpPos = jointsMatrix[jointIndex] * vec4(position, 1.0);
+            initPos += weight * tmpPos;
+        }
+    }
+    if (count == 0)
+    {
+        initPos = vec4(position, 1.0);
+    }
+    gl_Position = orthoProjectionMatrix * modelLightViewMatrix * initPos;
+}
+
+

你需要修改Renderer类来为这个着色器设置新的Uniform,最终的效果会更好。光照将被正确的应用,阴影将随每个动画帧改变,如下图所示。

+

动画修复

+

这就是全部内容了,现在你已经有了一个用于动画MD5模型的可工作示例。源代码仍能改进,你可以修改在每个渲染周期中加载的矩阵,以便在帧之间插入。你可以查看本章中使用的资源,了解如何实现该功能。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/20-particles/index.html b/20-particles/index.html new file mode 100755 index 0000000..e0056d8 --- /dev/null +++ b/20-particles/index.html @@ -0,0 +1,894 @@ + + + + + + + + + + + + 粒子 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

粒子(Particles)

+

基础

+

在本章中,我们将添加粒子效果到游戏引擎中。有了这种效果,我们就能模拟关系、火、灰尘和云。这是一种简单的效果,将改善对任何游戏的图形方面。

+

在此之前值得一提的是,有很多方法可以实现不同效果的粒子效果。当前情况下,我们将使用面板粒子(Billboard Particle)。该技术使用移动的纹理四边形来表示一个粒子,它们总是面向观察者,在本例中,就是摄像机。你还可以使用面板技术在游戏项上显示信息面板,比如迷你HUD。

+

让我们开始定义粒子,粒子可以通过以下属性定义:

+
    +
  1. 一个用于表示四边形顶点的网格。
  2. +
  3. 一张纹理。
  4. +
  5. 某一时刻的坐标。
  6. +
  7. 缩放系数。
  8. +
  9. 速度。
  10. +
  11. 移动方向。
  12. +
  13. 生存时间或存活时间。一旦该时间过去,粒子就不再存在。
  14. +
+

前四项是GameItem类的一部分,但后三项不是。因此,我们要创建一个名为Particle的新类,它继承了GameItem类,其定义如下:

+
package org.lwjglb.engine.graph.particles;
+
+import org.joml.Vector3f;
+import org.lwjglb.engine.graph.Mesh;
+import org.lwjglb.engine.items.GameItem;
+
+public class Particle extends GameItem {
+
+    private Vector3f speed;
+
+    /**
+     * 粒子存活的时间,以毫秒为单位
+     */
+    private long ttl;
+
+    public Particle(Mesh mesh, Vector3f speed, long ttl) {
+        super(mesh);
+        this.speed = new Vector3f(speed);
+        this.ttl = ttl;
+    }
+
+    public Particle(Particle baseParticle) {
+        super(baseParticle.getMesh());
+        Vector3f aux = baseParticle.getPosition();
+        setPosition(aux.x, aux.y, aux.z);
+        aux = baseParticle.getRotation();
+        setRotation(aux.x, aux.y, aux.z);
+        setScale(baseParticle.getScale());
+        this.speed = new Vector3f(baseParticle.speed);
+        this.ttl = baseParticle.geTtl();
+    }
+
+    public Vector3f getSpeed() {
+        return speed;
+    }
+
+    public void setSpeed(Vector3f speed) {
+        this.speed = speed;
+    }
+
+    public long geTtl() {
+        return ttl;
+    }
+
+    public void setTtl(long ttl) {
+        this.ttl = ttl;
+    }
+
+    /**
+     * 更新粒子的存活时间
+     * @param elapsedTime 经过的时间(毫秒)
+     * @return 粒子的存活时间
+     */
+    public long updateTtl(long elapsedTime) {
+        this.ttl -= elapsedTime;
+        return this.ttl;
+    }
+}
+
+

从上述代码可以看出,粒子的速度和运动方向可以表示为一个向量。该向量的方向决定了粒子的运动方向和速度。粒子存活时间(TTL)被设定为毫秒计数器,每当更新游戏状态时,它都会减少。该类还有一个复制构造函数,也就是说,一个构造函数接收另一个粒子实例来进行复制。

+

现在,我们需要创建一个粒子生成器或粒子发射器,即一个动态生成粒子、控制其生命周期并根据特定的模式更新其位置的类。我们可以创建很多实现,它们在粒子的创建方式和位置的更新方式(例如,是否考虑重力)方面各不相同。因此,为了保持游戏引擎的通用性,我们将创建一个所有粒子发射器必须要实现的接口。这个名为IParticleEmitter的接口定义如下:

+
package org.lwjglb.engine.graph.particles;
+
+import java.util.List;
+import org.lwjglb.engine.items.GameItem;
+
+public interface IParticleEmitter {
+
+    void cleanup();
+
+    Particle getBaseParticle();
+
+    List<GameItem> getParticles();
+}
+
+

IParticleEmitter接口有一个清理资源的方法,名为cleanup,还有一个获取粒子列表的方法,名为getParticles。还有一个名为getBaseParticle的方法,但是这个方法是做什么的呢?一个粒子发射器将动态地产生许多例子。每当一个粒子过期,就会创建新的粒子。该粒子更新周期将使用基础粒子作为模板创建新的势力。这就是基础粒子的用途,这也是为什么Particle类定义了一个复制构造函数。

+

在游戏引擎的代码中,我们将只引用IParticleEmitter接口,因此基础代码将不依赖于特定的实现。不过,我们可以创建一个实现来模拟不受重力影响的粒子流。这个实现可以用来模拟光线或火焰,名为FlowParticleEmitter

+

这个类的行为可以通过以下属性进行调整:

+
    +
  • 一次能存在的最大粒子数量
  • +
  • 创建粒子的最短周期。粒子将在最短的时间内一个接一个地创建,以避免粒子爆发性创建。
  • +
  • 一组范围,以随机粒子速度和位置。新粒子将使用基础粒子的位置和速度,可以在相应范围内取值,以分散光线。
  • +
+

该类的实现如下:

+
package org.lwjglb.engine.graph.particles;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.joml.Vector3f;
+import org.lwjglb.engine.items.GameItem;
+
+public class FlowParticleEmitter implements IParticleEmitter {
+
+    private int maxParticles;
+
+    private boolean active;
+
+    private final List<GameItem> particles;
+
+    private final Particle baseParticle;
+
+    private long creationPeriodMillis;
+
+    private long lastCreationTime;
+
+    private float speedRndRange;
+
+    private float positionRndRange;
+
+    private float scaleRndRange;
+
+    public FlowParticleEmitter(Particle baseParticle, int maxParticles, long creationPeriodMillis) {
+        particles = new ArrayList<>();
+        this.baseParticle = baseParticle;
+        this.maxParticles = maxParticles;
+        this.active = false;
+        this.lastCreationTime = 0;
+        this.creationPeriodMillis = creationPeriodMillis;
+    }
+
+    @Override
+    public Particle getBaseParticle() {
+        return baseParticle;
+    }
+
+    public long getCreationPeriodMillis() {
+        return creationPeriodMillis;
+    }
+
+    public int getMaxParticles() {
+        return maxParticles;
+    }
+
+    @Override
+    public List<GameItem> getParticles() {
+        return particles;
+    }
+
+    public float getPositionRndRange() {
+        return positionRndRange;
+    }
+
+    public float getScaleRndRange() {
+        return scaleRndRange;
+    }
+
+    public float getSpeedRndRange() {
+        return speedRndRange;
+    }
+
+    public void setCreationPeriodMillis(long creationPeriodMillis) {
+        this.creationPeriodMillis = creationPeriodMillis;
+    }
+
+    public void setMaxParticles(int maxParticles) {
+        this.maxParticles = maxParticles;
+    }
+
+    public void setPositionRndRange(float positionRndRange) {
+        this.positionRndRange = positionRndRange;
+    }
+
+    public void setScaleRndRange(float scaleRndRange) {
+        this.scaleRndRange = scaleRndRange;
+    }
+
+    public boolean isActive() {
+        return active;
+    }
+
+    public void setActive(boolean active) {
+        this.active = active;
+    }
+
+    public void setSpeedRndRange(float speedRndRange) {
+        this.speedRndRange = speedRndRange;
+    }
+
+    public void update(long ellapsedTime) {
+        long now = System.currentTimeMillis();
+        if (lastCreationTime == 0) {
+            lastCreationTime = now;
+        }
+        Iterator<? extends GameItem> it = particles.iterator();
+        while (it.hasNext()) {
+            Particle particle = (Particle) it.next();
+            if (particle.updateTtl(ellapsedTime) < 0) {
+                it.remove();
+            } else {
+                updatePosition(particle, ellapsedTime);
+            }
+        }
+
+        int length = this.getParticles().size();
+        if (now - lastCreationTime >= this.creationPeriodMillis && length < maxParticles) {
+            createParticle();
+            this.lastCreationTime = now;
+        }
+    }
+
+    private void createParticle() {
+        Particle particle = new Particle(this.getBaseParticle());
+        // 添加一些随机的粒子
+        float sign = Math.random() > 0.5d ? -1.0f : 1.0f;
+        float speedInc = sign * (float)Math.random() * this.speedRndRange;
+        float posInc = sign * (float)Math.random() * this.positionRndRange;        
+        float scaleInc = sign * (float)Math.random() * this.scaleRndRange;        
+        particle.getPosition().add(posInc, posInc, posInc);
+        particle.getSpeed().add(speedInc, speedInc, speedInc);
+        particle.setScale(particle.getScale() + scaleInc);
+        particles.add(particle);
+    }
+
+    /**
+     * 更新一个粒子的位置
+     * @param particle 需要更新的粒子
+     * @param elapsedTime 已经过的时间(毫秒)
+     */
+    public void updatePosition(Particle particle, long elapsedTime) {
+        Vector3f speed = particle.getSpeed();
+        float delta = elapsedTime / 1000.0f;
+        float dx = speed.x * delta;
+        float dy = speed.y * delta;
+        float dz = speed.z * delta;
+        Vector3f pos = particle.getPosition();
+        particle.setPosition(pos.x + dx, pos.y + dy, pos.z + dz);
+    }
+
+    @Override
+    public void cleanup() {
+        for (GameItem particle : getParticles()) {
+            particle.cleanup();
+        }
+    }
+}
+
+

现在,我们可以拓展Scene类中包含的数据,使其包含一个ParticleEmitter的实例数组。

+
package org.lwjglb.engine;
+
+// 这是导入……
+
+public class Scene {
+
+    // 这有更多属性……
+
+    private IParticleEmitter[] particleEmitters;
+
+

在该阶段,我们可以开始渲染粒子。粒子不会受到光的影响,也不会产生任何音乐。它们不会有任何骨骼动画,所以用特定的着色器渲染它们是没有意义的。着色器非常简单,它们只会使用投影和模型观察矩阵渲染顶点,并使用纹理设置颜色。

+

顶点着色器的定义如下:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    outTexCoord = texCoord;
+}
+
+

片元着色器的定义如下:

+
#version 330
+
+in vec2 outTexCoord;
+in vec3 mvPos;
+out vec4 fragColor;
+
+uniform sampler2D texture_sampler;
+
+void main()
+{
+    fragColor = texture(texture_sampler, outTexCoord);
+}
+
+

如你所见,它们非常简单,就像渲染一章中使用的着色器。现在,和其他章节一样,我们需要在Renderer类中设置和使用这些着色器。着色器的设置将在一个名为setupParticlesShader的方法中完成,其定义如下:

+
private void setupParticlesShader() throws Exception {
+    particlesShaderProgram = new ShaderProgram();
+    particlesShaderProgram.createVertexShader(Utils.loadResource("/shaders/particles_vertex.vs"));
+    particlesShaderProgram.createFragmentShader(Utils.loadResource("/shaders/particles_fragment.fs"));
+    particlesShaderProgram.link();
+
+    particlesShaderProgram.createUniform("projectionMatrix");
+    particlesShaderProgram.createUniform("modelViewMatrix");
+    particlesShaderProgram.createUniform("texture_sampler");
+}
+
+

现在我们可以在Renderer类中创建渲染方法renderParticles,定义如下:

+
private void renderParticles(Window window, Camera camera, Scene scene) {
+    particlesShaderProgram.bind();
+
+    particlesShaderProgram.setUniform("texture_sampler", 0);
+    Matrix4f projectionMatrix = transformation.getProjectionMatrix();
+    particlesShaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+    Matrix4f viewMatrix = transformation.getViewMatrix();
+    IParticleEmitter[] emitters = scene.getParticleEmitters();
+    int numEmitters = emitters != null ? emitters.length : 0;
+
+    for (int i = 0; i < numEmitters; i++) {
+        IParticleEmitter emitter = emitters[i];
+        Mesh mesh = emitter.getBaseParticle().getMesh();
+
+        mesh.renderList((emitter.getParticles()), (GameItem gameItem) -> {
+            Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(gameItem, viewMatrix);
+            particlesShaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+        }
+        );
+    }
+    particlesShaderProgram.unbind();
+}
+
+

如果你努力阅读,上述代码应该是不言自明的,它只是设置必要的Uniform,并渲染每个粒子。现在,我们已经创建了测试粒子效果实现所需的所有方法,只需要修改DummyGame类,我们就可以创建粒子发射器和基本粒子的特性。

+
Vector3f particleSpeed = new Vector3f(0, 1, 0);
+particleSpeed.mul(2.5f);
+long ttl = 4000;
+int maxParticles = 200;
+long creationPeriodMillis = 300;
+float range = 0.2f;
+float scale = 0.5f;
+Mesh partMesh = OBJLoader.loadMesh("/models/particle.obj");
+Texture texture = new Texture("/textures/particle_tmp.png");
+Material partMaterial = new Material(texture, reflectance);
+partMesh.setMaterial(partMaterial);
+Particle particle = new Particle(partMesh, particleSpeed, ttl);
+particle.setScale(scale);
+particleEmitter = new FlowParticleEmitter(particle, maxParticles, creationPeriodMillis);
+particleEmitter.setActive(true);
+particleEmitter.setPositionRndRange(range);
+particleEmitter.setSpeedRndRange(range);
+this.scene.setParticleEmitters(new FlowParticleEmitter[] {particleEmitter});
+
+

我们现在使用一个普通填充圆作为粒子的纹理,以便更好地理解发生了什么。如果你运行它,你会看到如下所示的东西:

+

粒子I

+

为什么一些粒子似乎被切断了?为什么透明的背景不能解决这个问题?原因是深度测试。粒子的一些片元被丢弃,因为它们具有比该区域的深度缓冲的当前值高的深度值。我们可以通过将其与摄像机之间的距离来排序粒子以解决这个问题,或者我们可以禁用深度写入。

+

在绘制粒子之前我们需要插入这一行代码:

+
glDepthMask(false);
+
+

然后在我们完成渲染之后还原为先前值:

+
glDepthMask(true);
+
+

然后我们会得到如下所示的东西:

+

粒子II

+

好了,问题解决。然而,我们仍想应用另一种效果,我们希望颜色被混合,因此颜色将被添加,以达成更好的效果。这是在渲染前增加如下一行代码来实现的:

+
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+

与深度的情况一样,渲染完所有粒子后,我们将混合函数恢复为:

+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+

现在我们得到这样的效果:

+

粒子III

+

但我们还没有完成它。如果你把摄像机移到蓝色正方形的上方往下看,你可能会得到这样的东西:

+

粒子IV

+

这些粒子看起来不太好,它们应该是圆的,但现在看起来像一张纸。在此之上,我们应该应用面板技术。用于渲染粒子的四边形应该始终面向摄像机,与摄像机方向完全垂直,就好像根本没有旋转一样。摄像机的矩阵将位移和旋转应用于场景中的每一个对象,我们想跳过将要应用的旋转。

+

警告:在讲数学知识时,如果你觉得不舒服,你可以跳过它。让我们再次回顾那个观察矩阵。该矩阵可以像这样表示(没有应用任何缩放)。

+

+ + +

+

红色的元素代表摄像机的旋转,蓝色的元素代表位移。我们需要取消观察矩阵中的左上角3x3矩阵的旋转效果,所以它会变成这样:

+

+ + +

+

在左上角的红色部分,我们有一个3x3矩阵,把它命名为并且我们想把它转换成单位矩阵:。任何矩阵乘以它的逆矩阵都会得到单位矩阵:。我们只需要从观察矩阵中取左上角的3x3矩阵,然后乘以它的逆矩阵,但是还可以优化他。一个旋转矩阵有一个有趣的定理,它的逆矩阵与其转置矩阵相等。即:。转置矩阵比逆矩阵更容易计算。矩阵的转置就像将其反转过来,将每一列与每一行替换。

+

+^{T} += + + +

+

好的,让我们总结一下。我们有该变换:,其中是观察矩阵,是模型矩阵。我们可以这样表达:

+

+ +\times + + +

+

我们想要取消观察矩阵的旋转,得到这样的结果:

+

+ + +

+

所以我们只需要将模型矩阵的左上3x3矩阵设为观察矩阵上3x3部分的转置矩阵。

+

+ +\times + + +

+

但在这之后,我们去掉了缩放,实际上真正想要达到的结果是这样:

+

+ + +

+

其中sx,sy和sz就是缩放系数。因此,当我们将模型矩阵的左上3x3矩阵设置为观察矩阵的转置矩阵后,我们需要再次应用缩放。

+

就这些,我们只需要在renderParticlesMethod中像这样修改:

+
        for (int i = 0; i < numEmitters; i++) {
+            IParticleEmitter emitter = emitters[i];
+            Mesh mesh = emitter.getBaseParticle().getMesh();
+
+            mesh.renderList((emitter.getParticles()), (GameItem gameItem) -> {
+                Matrix4f modelMatrix = transformation.buildModelMatrix(gameItem);
+
+                viewMatrix.transpose3x3(modelMatrix);
+
+                Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(modelMatrix, viewMatrix);
+                modelViewMatrix.scale(gameItem.getScale());
+                particlesShaderProgram.setUniform("modelViewMatrix", modelViewMatrix);
+            }
+            );
+        }
+
+

我们还在Transformation类中添加了另一种方法,使用两个矩阵来构造模型观察矩阵,而不是使用GameItem和观察矩阵。

+

有了如上更改,当从上方观察粒子时,我们就得到如下结果:

+

粒子V

+

现在集齐了创建一个更真实的粒子效果所需要的所有要素,所以让我们将其改为更精细的纹理。我们将使用如下图片(它是由GIMP创作的,带有光照和阴影过滤器):

+

粒子纹理

+

有了如上纹理,我们会得到如下所示的粒子:

+

粒子VI

+

现在更好了!你可能会注意到我们需要调整缩放,因为粒子现在总是对着摄像机,显示的面积总是最大的。

+

最后,再提一点,为了得到可以在任何场景使用的完美的效果,你需要实现粒子排序和启用深度缓冲区。无论如何,这里有一个示例可以将这种效果囊括到你的游戏中。

+

纹理集(Texture Atlas)

+

现在我们已经做好了粒子效果的基础建设,现在可以为它添加一些动画效果了。为了实现它,我们将支持纹理集。纹理集(Texture Atlas)是一个包含所有将要使用的纹理的大型图片。使用纹理集,我们就只需要加载一个大的图片,然后再绘制游戏项时,选择该图像的一部分作为纹理。例如,当我们想用不同的纹理多次渲染相同的模型时(例如树或岩石),可以使用这种技术。我们可以使用相同的纹理集并选择适当的坐标,而不是使用很多纹理实例并在它们之间切换(记住,切换状态总是很慢的)。

+

在此情况下,我们将使用纹理坐标来添加粒子动画。我们遍历不同的纹理来为粒子动画建模,所有这些纹理将被分到一个像这样的纹理集:

+

纹理集

+

纹理集可以被划分为多个方形片段。我们将一个方形片段坐标分配到一个粒子上,并随着时间推移改变它以表示动画。让我们开始吧。我们要做的第一件事是修改Texture类来指定纹理集可以拥有的行数和列数。

+
package org.lwjglb.engine.graph;
+
+// .. 这里是导入
+
+public class Texture {
+
+    // 无关属性省略
+    private int numRows = 1;
+
+    private int numCols = 1;
+
+   // 无关代码省略
+
+    public Texture(String fileName, int numCols, int numRows) throws Exception  {
+        this(fileName);
+        this.numCols = numCols;
+        this.numRows = numRows;
+    }
+
+

默认情况下,我们处理的纹理的列数和行数等于1。我们还添加了另一个构造函数来指定行和列。

+

然后,我们需要追踪一个GameItem在纹理集中的坐标,因此只需向该类添加另一个属性,默认值为0。

+
package org.lwjglb.engine.items;
+
+import org.joml.Vector3f;
+import org.lwjglb.engine.graph.Mesh;
+
+public class GameItem {
+
+    // 更多属性省略
+
+    private int textPos;
+
+

然后我们修改Particle类,以便能够通过纹理集自动迭代。

+
package org.lwjglb.engine.graph.particles;
+
+import org.joml.Vector3f;
+import org.lwjglb.engine.graph.Mesh;
+import org.lwjglb.engine.graph.Texture;
+import org.lwjglb.engine.items.GameItem;
+
+public class Particle extends GameItem {
+
+    private long updateTextureMillis;
+
+    private long currentAnimTimeMillis;
+
+

属性updateTextureMillis定义移动到纹理集中下一个坐标的时间(以毫秒为单位)。数值月底,粒子在纹理上变化的速度就越快。属性currentAnimTimeMillis只是跟踪纹理持续当前纹理坐标的时间。

+

因此,我们需要修改Particle类构造函数来设置这些值。我们还计算了纹理集的片段数量,它是由属性animFrames定义的。

+
public Particle(Mesh mesh, Vector3f speed, long ttl, long updateTextureMillis) {
+    super(mesh);
+    this.speed = new Vector3f(speed);
+    this.ttl = ttl;
+    this.updateTextureMills = updateTextureMills;
+    this.currentAnimTimeMillis = 0;
+    Texture texture = this.getMesh().getMaterial().getTexture();
+    this.animFrames = texture.getNumCols() * texture.getNumRows();
+}
+
+

现在,我们只需要修改检查粒子是否已经过期的方法,来检查是否需要更新纹理坐标。

+
public long updateTtl(long elapsedTime) {
+    this.ttl -= elapsedTime;
+    this.currentAnimTimeMillis += elapsedTime;
+    if ( this.currentAnimTimeMillis >= this.getUpdateTextureMillis() && this.animFrames > 0 ) {
+        this.currentAnimTimeMillis = 0;
+        int pos = this.getTextPos();
+        pos++;
+        if ( pos < this.animFrames ) {
+            this.setTextPos(pos);
+        } else {
+            this.setTextPos(0);
+        }
+    }
+    return this.ttl;
+}
+
+

除此之外,我们还修改了FlowRangeEmitter类,在应该改变粒子纹理坐标的时间周期上增加了一些随机性。你可以在源代码上查看它。

+

现在,我们可以使用这些数据来设置合适的纹理坐标。我们将在顶点着色器中进行这一操作,因为它输出了要在片元着色器中使用的那些值。这个新着色器的定义如下:

+
#version 330
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+
+out vec2 outTexCoord;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+uniform float texXOffset;
+uniform float texYOffset;
+uniform int numCols;
+uniform int numRows;
+
+void main()
+{
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+
+    // Support for texture atlas, update texture coordinates
+    float x = (texCoord.x / numCols + texXOffset);
+    float y = (texCoord.y / numRows + texYOffset);
+
+    outTexCoord = vec2(x, y);
+}
+
+

如你所见,我们现在有三个新Uniform。UniformnumColsnumRows只储存纹理集的列和行数。为了计算纹理坐标,首先必须缩小这些参数。每个片段的宽度为,高度为,如下图所示。

+

纹理坐标

+

然后我们只需要根据行和列应用和偏移,这是由texXOffsettexYOffsetUniform定义的。

+

我们将在Renderer类中计算这些偏移量,如下述代码所示。我们根据每个粒子的坐标计算它们所处的行和列,并将偏移量计算为片段高度和宽度的倍数。

+
mesh.renderList((emitter.getParticles()), (GameItem gameItem) -> {
+    int col = gameItem.getTextPos() % text.getNumCols();
+    int row = gameItem.getTextPos() / text.getNumCols();
+    float textXOffset = (float) col / text.getNumCols();
+    float textYOffset = (float) row / text.getNumRows();
+    particlesShaderProgram.setUniform("texXOffset", textXOffset);
+    particlesShaderProgram.setUniform("texYOffset", textYOffset);
+
+

注意,如果你只需要支持正方形纹理集,你只需要两个Uniform。最终的效果是这样的:

+

粒子动画

+

现在,我们有了粒子动画。在下章中,我们讲学习如何优化渲染流程。我们正在渲染具有相同网格的多个元素,并为每个元素进行绘制调用。在下章中,我们讲学习如何在单个调用中渲染它们。这种技术不仅适用于粒子,也适用于渲染共享同一模型,但被放在不同位置或具有不同纹理的多个元素的场景。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/21-instanced-rendering/index.html b/21-instanced-rendering/index.html new file mode 100755 index 0000000..9f4a917 --- /dev/null +++ b/21-instanced-rendering/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + 实例化渲染 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

实例化渲染(Instanced Rendering)

+

很多实例

+

在绘制三维场景时,经常会有许多模型用相同的网格表示,但它们具有不同的变换。在此情况下,尽管它们可能是只有几个三角形的简单物体,但性能仍可能会受到影响,这背后的原因就是我们渲染它们的方式。

+

我们基本上是在循环中遍历所有游戏项,并对函数glDrawElements进行调用。如前几章所说的,对OpenGL库的调用应该最小化。对函数glDrawElements的每次调用都会造成一定开销。这对于每个GameItem实例会反复产生开销。

+

在处理大量相似物体时,使用一次调用渲染所有这些物体会更有效。这种技术被称为实例化渲染。为了实现同时渲染一组元素,OpenGL提供了一组名为glDrawXXXInstanced的函数。在本例中,由于我们正在绘制元素,所以将使用名为glDrawElementsInstanced的函数。该函数接收与glDrawElements相同的参数,外加一个额外的参数,用于设置要绘制的实例数。

+

这是一个如何使用glDrawElements的示例:

+
glDrawElements(GL_TRIANGLES, numVertices, GL_UNSIGNED_INT, 0)
+
+

以下是实例化版本的用法:

+
glDrawElementsInstanced(GL_TRIANGLES, numVertices, GL_UNSIGNED_INT, 0, numInstances);
+
+

但是你现在可能想知道,如何为每个实例设置不同的变换。现在,在绘制每个实例之前,我们将使用Uniform来传递不同的变换和实例相关的数据。在进行渲染调用之前,我们需要为每项设置特定的数据。当它们开始渲染时,我们如何做到这一点呢?

+

当使用实例化渲染时,在顶点着色器中我们可以使用一个输入变量来储存当前绘制实例的索引。例如,使用这个内置变量,我们可以传递一个包含要应用到每个实例的变换的Uniform数组,并仅做一次渲染调用。

+

这种方法的问题是,它仍然会带来太多的开销。除此之外,我们能够传递的Uniform数量是有限的。因此,需要使用另一种方法,而不是使用Uniform数组,我们将使用实例化数组。

+

如果你还记得前几章,每个网格的数据都是由一组名为VBO的数据数组定义的。这些VBO中的数据储存在每个唯一的Mesh实例中。

+

VBO

+

使用标准的VBO,在着色器中,我们可以访问与每个顶点(其位置、颜色、纹理等)相关的数据。无论何时运行着色器,输入变量都被设置为指向与每个顶点相关的指定数据。使用实例化数组,我们可以设置每个实例而不是每个顶点所更改的数据。如果我们将这两种类型结合起来,就可以使用常规的VBO来储存每个顶点的信息(坐标,纹理坐标)和用VBO储存每个实例的数据(如模型观察矩阵)。

+

下图展示了由三个顶点组成的网格,每个顶点定义了坐标、纹理与法线。每个元素的第一个索引是它所属的实例(蓝色)。第二个索引表示实例中的顶点位置。

+

网格也由两个实例VBO定义。一个用于模型观察矩阵,另一个用于光照观察矩阵。当为第一个实例渲染顶点(第1X个)时,模型观察矩阵和光照观察矩阵是相同的(第1个)。当渲染第二个实例的顶点时,将使用第二个模型观察矩阵和光照观察矩阵。

+

有实例属性的VBO

+

因此,在渲染第一个实例的第一个顶点时,将使用V11、T11和N11作为坐标、纹理和法线数据,并使用MV1作为模型观察矩阵。当渲染同一个实例的第二个顶点时,将使用V12、T12和N12作为坐标、纹理和法线数据,仍使用MV1作为模型观察矩阵。在渲染第二个实例之前,不会使用MV2和LV2。

+

为了定义每个实例数据,我们需要在顶点属性之后调用函数glVertexAttribDivisor。该函数接收两个参数:

+
    +
  • +

    index:顶点属性的索引(在glVertexAttribPointer函数中设置的)。

    +
  • +
  • +

    divisor: 如果这个值为0,那么在渲染时,每个顶点的数据都会改变。如果将其设置为1,则每渲染一个实例,数据更改一次。如果它被设置为2,则每渲染两个实例就会更改一次,以此类推。

    +
  • +
+

因此,为了设置实例的数据,我们需要在每个属性定义之后进行如下调用:

+
glVertexAttribDivisor(index, 1);
+
+

让我们开始修改代码库,以支持实例化渲染。第一步是创建一个名为InstancedMesh的新类,该类继承自Mesh类。该类的构造函数类似于Mesh的构造函数,但有一个额外的参数,即实例数。

+

在构造函数中,除了依赖超类的构造函数外,我们还将创建两个新的VBO,一个用于模型观察矩阵,另一个用于光照观察矩阵。创建模型观察矩阵的代码如下所示:

+
modelViewVBO = glGenBuffers();
+vboIdList.add(modelViewVBO);
+this.modelViewBuffer = MemoryUtil.memAllocFloat(numInstances * MATRIX_SIZE_FLOATS);
+glBindBuffer(GL_ARRAY_BUFFER, modelViewVBO);
+int start = 5;
+for (int i = 0; i < 4; i++) {
+    glVertexAttribPointer(start, 4, GL_FLOAT, false, MATRIX_SIZE_BYTES, i * VECTOR4F_SIZE_BYTES);
+    glVertexAttribDivisor(start, 1);
+    start++;
+}
+
+

我们首先要做的事是创建一个新的VBO和一个新的FloatBuffer以在其内储存数据。缓冲区的大小是用浮点数数量计算的,所以它等于实例数乘以4x4矩阵的浮点数大小(等于16)。

+

一旦VBO被绑定,我们就开始为它定义属性。你可以看到这是在for循环中完成的,循环进行了四次。每轮循环定义一个矩阵向量。为什么不简单地为整个矩阵定义一个属性呢?原因是顶点属性不能储存超过四个浮点数。因此,我们需要把矩阵定义分为四部分。让我们重新认识一下glVertexAttribPointer的参数:

+
    +
  • Index: 要定义的元素的索引。
  • +
  • Size: 该属性的分量数。在本例中,它是四个浮点数,这是可接受的最大值。
  • +
  • Type: 数据类型(在本例中为浮点型)。
  • +
  • Normalize: 是否应该归一化指定数据。
  • +
  • Stride(步长): 理解这里的概念很重要,它设置了连续属性之间的字节偏移量。在本例中,我们需要将其设置为整个矩阵的字节大小。这就像一个用于包装数据的标记,从而可以在顶点或实例之间进行更改。
  • +
  • Pointer: 此属性定义应用的偏移量。在本例中,我们需要将矩阵定义拆分为四次调用。依矩阵的每个向量增加偏移量。
  • +
+

定义了顶点属性之后,我们需要使用相同的索引调用glVertexAttribDivisor

+

光照观察矩阵的定义与上述过程类似,你可以在源代码中查看它。继续进行InstancedMesh类的定义,重写方法以在渲染之前启用顶点属性(以及在渲染之后要禁用它们)是很重要的。

+
@Override
+protected void initRender() {
+    super.initRender();
+    int start = 5;
+    int numElements = 4 * 2;
+    for (int i = 0; i < numElements; i++) {
+        glEnableVertexAttribArray(start + i);
+    }
+}
+
+@Override
+protected void endRender() {
+    int start = 5;
+    int numElements = 4 * 2;
+    for (int i = 0; i < numElements; i++) {
+        glDisableVertexAttribArray(start + i);
+    }
+    super.endRender();
+}
+
+

InstancedMesh类定义了一个名为renderListInstanced的公共方法,它渲染一系列的游戏项,这个方法将游戏项列表分割为大小与创建InstancedMesh所设实例数量相等的块。真正的渲染方法是renderChunkInstanced,定义如下:

+
private void renderChunkInstanced(List<GameItem> gameItems, boolean depthMap, Transformation transformation, Matrix4f viewMatrix, Matrix4f lightViewMatrix) {
+    this.modelViewBuffer.clear();
+    this.modelLightViewBuffer.clear();
+    int i = 0;
+    for (GameItem gameItem : gameItems) {
+        Matrix4f modelMatrix = transformation.buildModelMatrix(gameItem);
+        if (!depthMap) {
+            Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(modelMatrix, viewMatrix);
+            modelViewMatrix.get(MATRIX_SIZE_FLOATS * i, modelViewBuffer);
+        }
+        Matrix4f modelLightViewMatrix = transformation.buildModelLightViewMatrix(modelMatrix, lightViewMatrix);
+        modelLightViewMatrix.get(MATRIX_SIZE_FLOATS * i, this.modelLightViewBuffer);
+        i++;
+    }
+    glBindBuffer(GL_ARRAY_BUFFER, modelViewVBO);
+    glBufferData(GL_ARRAY_BUFFER, modelViewBuffer, GL_DYNAMIC_DRAW);
+    glBindBuffer(GL_ARRAY_BUFFER, modelLightViewVBO);
+    glBufferData(GL_ARRAY_BUFFER, modelLightViewBuffer, GL_DYNAMIC_DRAW);
+    glDrawElementsInstanced(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0, gameItems.size());
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
+}
+
+

该方法很简单,我们遍历游戏项,计算模型观察矩阵和光照观察矩阵。这些矩阵被转储到它们各自的缓冲区中。这些缓冲区的内容被发送到GPU,最后通过对方法glDrawElementsInstanced的调用来渲染它们。

+

回到着色器,我们需要修改顶点着色器以支持实例化渲染。首先我们为模型观察矩阵添加新的输入参数,这些参数将在使用实例化渲染时传递。

+
layout (location=5) in mat4 modelViewInstancedMatrix;
+layout (location=9) in mat4 modelLightViewInstancedMatrix;
+
+

如你所见,模型观察矩阵从位置5开始。由于矩阵是由一组共四个属性(每个属性储存一个向量)定义的,所以光照观察矩阵从位置9开始。因为我们想在非实例化渲染和实例化渲染中使用同一着色器,所以我们将维护模型观察矩阵和光照观察矩阵的Uniform。我们只需更改它们的名字。

+
uniform int isInstanced;
+uniform mat4 modelViewNonInstancedMatrix;
+…
+uniform mat4 modelLightViewNonInstancedMatrix;
+
+

我们创建了另一个Uniform来指定是否使用实例化渲染。在使用实例化渲染的情况下代码非常简单,我们只使用输入参数中的矩阵。

+
void main()
+{
+    vec4 initPos = vec4(0, 0, 0, 0);
+    vec4 initNormal = vec4(0, 0, 0, 0);
+    mat4 modelViewMatrix;
+    mat4 lightViewMatrix;
+    if ( isInstanced > 0 )
+    {
+        modelViewMatrix = modelViewInstancedMatrix;
+        lightViewMatrix = modelLightViewInstancedMatrix;
+        initPos = vec4(position, 1.0);
+        initNormal = vec4(vertexNormal, 0.0);
+    }
+
+

我们暂时不支持动画实例化渲染,以简化示例,但是该技术可以完美地用于此处。

+

最后,着色器像往常一样设置恰当的值。

+
    vec4 mvPos = modelViewMatrix * initPos;
+    gl_Position = projectionMatrix * mvPos;
+    outTexCoord = texCoord;
+    mvVertexNormal = normalize(modelViewMatrix * initNormal).xyz;
+    mvVertexPos = mvPos.xyz;
+    mlightviewVertexPos = orthoProjectionMatrix * lightViewMatrix * initPos;
+    outModelViewMatrix = modelViewMatrix;
+}
+
+

当然,Renderer类已经被修改,以支持Uniform的修改,并将非实例化网格的渲染从实例化网格中分离出来。你可以查看源代码中的修改。

+

此外,JOML的作者Kai Burjack还向源代码添加了一些优化。这些优化已经用于Transformation类,并总结为如下几条:

+
    +
  • 删除冗余调用,以设置具有单位值的矩阵。
  • +
  • 使用四元数进行更有效的旋转。
  • +
  • 使用特定的旋转和平移矩阵的方法,这些方法是针对这些操作优化的。
  • +
+

实例化渲染

+

回顾粒子

+

在实例化渲染的支持下,我们还可以提高粒子渲染的性能。粒子就是最好的用例。

+

为了将实例化渲染应用到粒子上我们必须提供对纹理集的支持。这可以通过添加一个带有纹理偏移量的VBO来实现。纹理偏移量可以由两个浮点数组成的单个向量定义,因此不需要像矩阵那样拆分定义。

+
// 纹理偏移量
+glVertexAttribPointer(start, 2, GL_FLOAT, false, INSTANCE_SIZE_BYTES, strideStart);
+glVertexAttribDivisor(start, 1);
+
+

但是,我们将在单个VBO中设置所有实例属性,而不是添加一个新的VBO。下图展示了这个概念。我们将所有属性打包在一个VBO中,每个实例的值都会发生变化。

+

单个VBO

+

为了使用单个VBO,我们需要修改实例中所有属性的属性大小。从上述代码中可以看到,纹理偏移量的定义使用了一个名为INSTANCE_SIZE_BYTES的常量。这个常量等于两个矩阵(一个用于模型观察矩阵定义,另一个用于光照观察矩阵定义)的字节大小再加上两个浮点数(纹理偏移量)的字节大小,总共是136。步长也需要适当地调整。

+

你可以在源代码中查看修改。

+

Renderer类也需要修改,以实用实例化渲染粒子和支持纹理集在场景中渲染。在本例中,支持这两种类型(非实例化和实例化)的渲染是没有意义的,所以修改更简单。

+

粒子的顶点着色器如下所示:

+
#version 330
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+layout (location=5) in mat4 modelViewMatrix;
+layout (location=13) in vec2 texOffset;
+out vec2 outTexCoord;
+uniform mat4 projectionMatrix;
+uniform int numCols;
+uniform int numRows;
+void main()
+{
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+
+    // 支持纹理集,更新纹理坐标
+    float x = (texCoord.x / numCols + texOffset.x);
+    float y = (texCoord.y / numRows + texOffset.y);
+    outTexCoord = vec2(x, y);}
+
+

上述修改的效果,看起来和非实例化粒子渲染时完全一样,但性能更高。FPS计数器已作为选项添加到窗口标题中。你可以使用实例化渲染和非实例化渲染来看看自身的性能提升。

+

粒子

+

扩展

+

结合我们现在所拥有的所有基础构造,我已经基于使用高度图修改了渲染方块数据的代码,还使用了纹理集以使用不同的纹理。它还融合了粒子渲染。看起来是这样的:

+

使用高度图的方块

+

请记住,这还有很大的优化空间,但这本书的目的是指导你学习LWJGL和OpenGL的概念和技术。我们的目标不是创建一个完整的游戏引擎(绝对不是一个体素引擎,它需要不同的方法和更多的优化)。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/22-audio/index.html b/22-audio/index.html new file mode 100755 index 0000000..0c31b06 --- /dev/null +++ b/22-audio/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + 音效 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

音频(Audio)

+

在此之前我们一直在处理图像,但每个游戏的另一个关键面是音频。本章将在OpenAL(Open Audio Library,开放音频库)的帮助下实现这个功能。OpenAL就像是OpenGL在音频的相似物,它允许我们通过抽象层播放声音。该层将我们与音频子系统的复杂底层隔离开来。此外,它还允许我们可以三维场景中特定的位置设置声音,“渲染”声音,随着距离衰减并根据它们的速度进行修改(模拟多普勒效应)。

+

LWJGL支持OpenGL,不需要任何额外的下载,它就已经可以使用了。但是在开始写代码之前,我们需要介绍处理OpenGL时所涉及的主要元素,它们是:

+
    +
  • 缓冲区(Buffer)。
  • +
  • 声源(Source)。
  • +
  • 侦听者(Listener)。
  • +
+

缓冲区储存音频数据,即音乐或音效。它们类似于OpenGL中的纹理。OpenAL希望音频数据采用PCM(Pulse Coded Modulation,脉冲编码调制)格式(单声道或多声道),因此我们不能只转储MP3或OGG文件而不首先将它们转换为PCM。

+

下一个元素是声源,它表示发出声音的三维空间中的位置(一个点)。声源与缓冲区关联(一次只能有一个),可以通过以下属性定义:

+
    +
  • 位置,声源的位置(坐标)。顺便一提,OpenAL和OpenGL一样使用右手笛卡尔坐标系,所以你可以假设(为了简化)你的世界坐标等于声音空间坐标系中的坐标。
  • +
  • 速度,它指定声源移动的速度。这是用来模拟多普勒效应的。
  • +
  • 增益,用来改变声音的大小(就像是一个放大因数)。
  • +
+

源代码中有额外的属性,稍后在讲解源代码时将描述这些属性。

+

最后但并不重要的是,侦听者是是产生的声音应该被听到的地方。侦听器就像是被设置在三维音频场景中用来接收声音的麦克风。现在只有一个侦听器。因此,人们常说的音频渲染是以听众的角度完成的。侦听器共享一些属性,但它还有一些附加属性,比如方向。方向表示侦听器所面朝的位置。

+

因此,三维音频场景有一组发出声音的声源和接收声音的侦听器组成。最终听到的声音取决于听者到不同声源的距离、相对速度和选择的传播模型(Propagation Model)。下图展示了一个包含不同元素类型的三维场景。

+

OpenAL概念

+

那么,让我们开始编写代码,我们将创建一个名为org.lwjglb.engine.sound的新包,它将负责管理所有负责处理音频的类。我们将首先从一个名为SoundBuffer的类开始,它将表示一个OpenAL缓冲区。该类的定义如下的一个代码片段所示:

+
package org.lwjglb.engine.sound;
+
+// import ...
+
+public class SoundBuffer {
+
+    private final int bufferId;
+
+    public SoundBuffer(String file) throws Exception {
+        this.bufferId = alGenBuffers();
+        try (STBVorbisInfo info = STBVorbisInfo.malloc()) {
+            ShortBuffer pcm = readVorbis(file, 32 * 1024, info);
+
+            // 复制到缓冲区
+            alBufferData(buffer, info.channels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, pcm, info.sample_rate());
+        }
+    }
+
+    public int getBufferId() {
+        return this.bufferId;
+    }
+
+    public void cleanup() {
+        alDeleteBuffers(this.bufferId);
+    }
+
+    // ...
+}
+
+

该类的构造函数需要一个声音文件(它可能与其他资源一样位于类路径中),并从中创建一个新的缓冲区。我们要做的第一件事是调用alGenBuffers创建一个OpenAL缓冲区。最后,我们的声音缓冲区将用一个整数来表示,就像一个指向它所持有的数据的指针。一旦创建了缓冲区,我们就将音频数据转储到其中。构造函数需要OGG格式的文件,因此我们需要将其转换为PCM格式。你可以查看这如何在源代码中完成的,无论如何,源代码是从LWJGL的OpenAL测试中提取的。

+

此前版本的LWJGL有一个名为WaveData的帮助类,用于加载WAV格式的音频文件。这个类不再出现在LWJGL3中。不过,你可以从该类获得源代码并在游戏中使用它(可能不需要任何修改)。

+

SoundBuffer类还提供了cleanup方法来释放资源。

+

让我们继续为OpenAL建模,它将由一个名为SoundSource的类实现。类定义如下:

+
package org.lwjglb.engine.sound;
+
+import org.joml.Vector3f;
+
+import static org.lwjgl.openal.AL10.*;
+
+public class SoundSource {
+
+    private final int sourceId;
+
+    public SoundSource(boolean loop, boolean relative) {
+        this.sourceId = alGenSources();
+        if (loop) {
+            alSourcei(sourceId, AL_LOOPING, AL_TRUE);
+        }
+        if (relative) {
+            alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);
+        }
+    }
+
+    public void setBuffer(int bufferId) {
+        stop();
+        alSourcei(sourceId, AL_BUFFER, bufferId);
+    }
+
+    public void setPosition(Vector3f position) {
+        alSource3f(sourceId, AL_POSITION, position.x, position.y, position.z);
+    }
+
+    public void setSpeed(Vector3f speed) {
+        alSource3f(sourceId, AL_VELOCITY, speed.x, speed.y, speed.z);
+    }
+
+    public void setGain(float gain) {
+        alSourcef(sourceId, AL_GAIN, gain);
+    }
+
+    public void setProperty(int param, float value) {
+        alSourcef(sourceId, param, value);
+    }
+
+    public void play() {
+        alSourcePlay(sourceId);
+    }
+
+    public boolean isPlaying() {
+        return alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PLAYING;
+    }
+
+    public void pause() {
+        alSourcePause(sourceId);
+    }
+
+    public void stop() {
+        alSourceStop(sourceId);
+    }
+
+    public void cleanup() {
+        stop();
+        alDeleteSources(sourceId);
+    }
+}
+
+

声源类提供了一些方法来设置它的位置、增益和控制方法来停止和暂停播放。请记住,声音控制操作是对一个声源(而不是对缓冲区)执行的。请记住,多个源可以共享同一个缓冲区。与SoundBuffer类中一样,SoundBuffer由一个标识符标记,该标识符在每个操作中使用。该类还提供了一个cleanup方法来释放保留的资源。但是让我们看看构造函数。我们要做的第一件事是调用alGenSources创建声源,然后使用构造函数参数设置一些有趣的属性。

+

第一个参数loop,表示要播放的声音是否应该处于循环模式。默认情况下,当通过声源调用播放操作时,当声音播放到最后时将停止。这对于一些声音来说是可以的,但是对于其他一些声音,比如背景音乐,就需要反复播放。当声音停止时不需要手动控制并重新播放声音,我们就只用将循环属性设置为truealSourcei(sourceId, AL_LOOPING, AL_TRUE);

+

另一个参数relative,控制声源的位置是否相对于侦听器。在本例中,当为声源设置位置时,我们基本上是在定义到侦听器的距离(使用想想),而不是OpenAL三维场景中的坐标,也不是世界坐标。这是通过调用alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);”启用的。但是,我们能用它做什么呢?这个属性很有趣,例如,用于不应该受到侦听器距离影响(减弱)的背景声音。例如,在背景音乐或与播放器控件相关的音效。如果我们将这些声源设置为相对的,并将它们的位置设置为,它们将不会被减弱。

+

现在轮到侦听器了,它是由一个名为SoundListener定义的。以下是该类的定义:

+
package org.lwjglb.engine.sound;
+
+import org.joml.Vector3f;
+
+import static org.lwjgl.openal.AL10.*;
+
+public class SoundListener {
+
+    public SoundListener() {
+        this(new Vector3f(0, 0, 0));
+    }
+
+    public SoundListener(Vector3f position) {
+        alListener3f(AL_POSITION, position.x, position.y, position.z);
+        alListener3f(AL_VELOCITY, 0, 0, 0);
+    }
+
+    public void setSpeed(Vector3f speed) {
+        alListener3f(AL_VELOCITY, speed.x, speed.y, speed.z);
+    }
+
+    public void setPosition(Vector3f position) {
+        alListener3f(AL_POSITION, position.x, position.y, position.z);
+    }
+
+    public void setOrientation(Vector3f at, Vector3f up) {
+        float[] data = new float[6];
+        data[0] = at.x;
+        data[1] = at.y;
+        data[2] = at.z;
+        data[3] = up.x;
+        data[4] = up.y;
+        data[5] = up.z;
+        alListenerfv(AL_ORIENTATION, data);
+    }
+}
+
+

与前面的类不同,你将注意到不需要创建侦听器。总会有一个侦听器,所以不需要创建一个,它已经为我们准备好了。因此,在构造函数中,我们只是简单地设置它的初始位置。基于同样的原因,没有必要使用cleanup方法。类也有设置侦听器位置和速度的方法,就像在SoundSource类中一样,但是我们有一个额外的方法来修改侦听器的方向。让我们回顾一下方向是什么。侦听器方向由两个向量定义,at向量和up向量,如下图所示:

+

侦听器的at和up向量

+

at向量基本上指向侦听器所朝向的位置,默认情况下它的值为up向量确定侦听器向上的方向,默认情况下它指向。这两个向量的三个分量都是在alListenerfv方法调用中设置的。此方法用于将一组浮点数(浮点数变量)传递到属性(在本例中为方向)。

+

在继续讲解之前,有必要强调一些与声音和侦听器速度相关的概念。声源与侦听器之间的相对速度会导致OpenAL模拟多普勒效应。如果你不知道多普勒效应是什么,多普勒效应将导致一个离你越来越近的物体发出的频率似乎比它离开时发出的频率要高的效应。问题是,仅仅通过设置声音和侦听器速度,OpenAL不会为你更新它们的位置。它将使用相对速度来计算多普勒效应,但位置不会改变。因此,如果你想要模拟一个移动的声源或者侦听器,你必须注意在游戏循环中更新它们的位置。

+

现在我们已经定义了关键元素,为了让它们工作,需要初始化OpenAL库,因此将创建一个名为SoundManager的新类来处理这个问题。下面是定义该类的代码片段:

+
package org.lwjglb.engine.sound;
+
+// import ...
+
+public class SoundManager {
+
+    private long device;
+
+    private long context;
+
+    private SoundListener listener;
+
+    private final List<SoundBuffer> soundBufferList;
+
+    private final Map<String, SoundSource> soundSourceMap;
+
+    private final Matrix4f cameraMatrix;
+
+    public SoundManager() {
+        soundBufferList = new ArrayList<>();
+        soundSourceMap = new HashMap<>();
+        cameraMatrix = new Matrix4f();
+    }
+
+    public void init() throws Exception {
+        this.device = alcOpenDevice((ByteBuffer) null);
+        if (device == NULL) {
+            throw new IllegalStateException("Failed to open the default OpenAL device.");
+        }
+        ALCCapabilities deviceCaps = ALC.createCapabilities(device);
+        this.context = alcCreateContext(device, (IntBuffer) null);
+        if (context == NULL) {
+            throw new IllegalStateException("Failed to create OpenAL context.");
+        }
+        alcMakeContextCurrent(context);
+        AL.createCapabilities(deviceCaps);
+    }
+
+

该类保存对SoundBufferSoundSource的实例的引用,以便跟踪和在此之后正确地清理它们。SoundBuffer储存在一个列表中,但SoundSource储存在一个Map中,因此可以通过名称搜索它们。init方法初始化OpenAL子系统:

+
    +
  • 开启默认设备。
  • +
  • 为该设备创建功能。
  • +
  • 创建一个声音环境,就像是OpenGL那样,并将其设置为当前环境。
  • +
+

SoundManager类还具有更新给定摄像机位置的侦听器朝向的方法。在本例中,侦听器将被设置在摄像机所在的位置。那么,给定摄像机的位置和旋转信息,我们如何计算atup向量呢?答案是使用与摄像机相关联的观察矩阵。我们需要将atup向量转换为考虑摄像机旋转的向量。让cameraMatrix为与摄像机关联的观察矩阵。实现的代码如下:

+
Matrix4f invCam = new Matrix4f(cameraMatrix).invert();
+Vector3f at = new Vector3f(0, 0, -1);
+invCam.transformDirection(at);
+Vector3f up = new Vector3f(0, 1, 0);
+invCam.transformDirection(up);
+
+

我们要做的第一件事是逆转摄像机观察矩阵。为什么要这么做?这样想,观察矩阵从世界空间坐标变换到观察空间。我们想要的正好相反,我们想要从观察空间坐标(观察矩阵)转换到世界空间坐标,这是侦听器应该被放置的位置。对于矩阵,反比通常就意味着逆转。一旦我们有了这个矩阵,我们只需要转换atup向量,使用这个矩阵计算新的方向。

+

但是,如果你查看源代码,你会看到实现略有不同,我们所做的是:

+
Vector3f at = new Vector3f();
+cameraMatrix.positiveZ(at).negate();
+Vector3f up = new Vector3f();
+cameraMatrix.positiveY(up);
+listener.setOrientation(at, up);
+
+

上述代码等价于第一种方法,它只是一种更高效的方法。它使用了一种更快的方法,可以在JOML库中找到,这种方法不需要计算完整的逆矩阵,但是可以得到相同的结果。这方法是由LWJGL论坛中的JOML作者提供的,因此你可以在[那里]((http://forum.lwjgl.org/index.php?topic=6080.0)查看更多细节。如果查看源代码,你将看到SoundManager类计算它自己的观察矩阵副本。这已经在Renderer类中完成了。为了保持代码简单,并避免重构,我倾向于使用这种方式。

+

这就完了。我们拥有播放声音所需的所有基础结构。你可以查看源代码,了解如何使用所有代码。你可以看到音乐是如何播放的,以及不同效果的声音(这些文件是从Freesound中获得的,贡献者都储存在名为CREDITS.txt的一个文件中)。如果你获得一些其他文件,你可以会注意到声音衰减与距离或侦听器的方向无关。请检查这些声音是否是单声道,是否不是立体声。OpenGL仅能使用单声道声音进行计算。

+

后记,OpenAL还允许你通过使用alDistanceModel并传递你想使用的模型(AL11.AL_EXPONENT_DISTANCEAL_EXPONENT_DISTANCE_CLAMP,等等)。你可以用它们来播放并检查效果。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/23-3d-object-picking/index.html b/23-3d-object-picking/index.html new file mode 100755 index 0000000..e94b0f2 --- /dev/null +++ b/23-3d-object-picking/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + + 三维物体选取 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

三维物体选取(3D Object Picking)

+

摄像机选取

+

每一个游戏的关键之一是能与环境交互,该功能要求能够在三维场景中选取物体。在本章中,我们将探讨如何实现这一功能。

+

但是,在开始讲述选取物体的步骤之前,我们需要一种表示所选物体的方法。因此,我们必须做的第一件事是添加一个属性到GameItem类,这允许我们标记选定的对象:

+
private boolean selected;
+
+

然后,我们需要能够在场景着色器中使用该值。让我们从片元着色器(scene_fragment.fs)开始。在本例中,我们将假设从顶点着色器接收一个标记,该标记将确定要渲染的片元是否是选定的物体。

+
in float outSelected;
+
+

然后,在片元着色器的结尾,我们将修改最终的片元颜色,如果选中,则将蓝色分量设置为.

+
if ( outSelected > 0 ) {
+    fragColor = vec4(fragColor.x, fragColor.y, 1, 1);
+}
+
+

然后,我们需要能够为每个GameItem设置该值。如果你回想一下前面的章节,我们有两种情况:

+
    +
  • 渲染非实例化网格。
  • +
  • 渲染实例化网格。
  • +
+

在第一种情况下,每个GameItem的数据通过Uniform传递,因此我们只需要在顶点着色器中为它添加一个新的Uniform。在第二种情况下,我们需要创建一个新的实例化属性。可以看到下述代码中集成了两种情况到顶点着色器。

+
layout (location=14) in float selectedInstanced;
+...
+uniform float selectedNonInstanced;
+...
+    if ( isInstanced > 0 )
+    {
+        outSelected = selectedInstanced;
+...
+    }
+    else
+    {
+    outSelected = selectedNonInstanced;
+...
+
+

既然已经做好了基本准备,我们只需要定义如何选择对象。在继续之前,如果你查阅源代码,你可能会注意到观察矩阵现在储存在Camera类中。这是因为我们在源代码中的几个类重新计算了观察矩阵,此前它储存在TransformationSoundManager类中。为了计算交点,我们就需要创建另一个副本。与其创建另一个副本,我们不如把它集中到Camera类中。这一更改还需要观察矩阵在游戏主循环中更新。

+

让我们继续物体选取的讨论。在本例中,我们将基于一个简单的方法,选取将由摄像机自动地完成,将选择摄像机所面对的最近的物体。让我们讨论一下如何做到它。

+

下图是我们需要解决的情况:

+

物体选取

+

我们把摄像机放在世界空间的某个坐标中,面朝一个特定方向。任何一个物体,如果它与摄像机的位置和前向的光线投射(Ray Cast)相交,那么它就是候选物体。在所有候选物体中,我们只需要选择最近的一个。

+

在本例中,游戏项是立方体,因此需要计算摄像机的前向向量与立方体的交点。这似乎是一个非常特殊的情况,但实际上是非常常见的。在许多游戏中,游戏项都与所谓的边界框(Bounding Box)相关连。边界框是一个矩形框,它囊括了该物体的所有顶点。例如,边界框也用于碰撞检测。实际上,在动画章节中,你看到的每个动画帧都定义了一个边界框,这有助于在任意给定时间设置边界。

+

接下来让我们开始编码。我们将创建一个名为CameraBoxSelectionDetector的新类,它有一个名为selectGameItem的方法,该方法将接收游戏项列表和摄像机。方法的定义如下:

+
public void selectGameItem(GameItem[] gameItems, Camera camera) {
+    GameItem selectedGameItem = null;
+    float closestDistance = Float.POSITIVE_INFINITY;
+
+    dir = camera.getViewMatrix().positiveZ(dir).negate();
+    for (GameItem gameItem : gameItems) {
+        gameItem.setSelected(false);
+        min.set(gameItem.getPosition());
+        max.set(gameItem.getPosition());
+        min.add(-gameItem.getScale(), -gameItem.getScale(), -gameItem.getScale());
+        max.add(gameItem.getScale(), gameItem.getScale(), gameItem.getScale());
+        if (Intersectionf.intersectRayAab(camera.getPosition(), dir, min, max, nearFar) && nearFar.x < closestDistance) {
+            closestDistance = nearFar.x;
+            selectedGameItem = gameItem;
+        }
+    }
+
+    if (selectedGameItem != null) {
+        selectedGameItem.setSelected(true);
+    }
+}
+
+

该方法将迭代游戏项,尝试从中获取与摄像机光线投射相交的项。它首先定义一个名为closestDistance的变量,该变量将储存最近的距离。对于相交的游戏项,将计算摄像机到交点的距离,如果该距离小于储存在closestDistance中的值,则该项将成为新的候选项。

+

在进入循环之前,我们需要摄像机所面向的方向向量。这很简单,只需使用视图矩阵去获得考虑相机旋转的Z方向。记住,正Z指向屏幕外,所以需要相反的方向向量,这就是为什么要反方向(negate)。

+

摄像机

+

在游戏循环中,每个GameItem都要进行相交计算。但是,我们该怎么做呢?这就是JOML库来帮忙的地方了。我们使用的是JOMLIntersectionf类,它提供了几种计算二维和三维交点的方法。具体来说,我们使用的是intersectRayAab方法。

+

该方法实现了轴对齐边界框(Axis Aligned Bounding Box,简称AABB)交点检测算法。你可以查看JOML文档中指出的详细信息

+

该方法测试由原点和方向定义的射线是否与由最小和最大坐标定义的边界框相交。这个算法是有效的,因为我们的立方体是轴对齐的,如果旋转它们,这个方法就不起作用。因此,该方法接收以下参数:

+
    +
  • 一个原点:在本例中,这是摄像机的位置。
  • +
  • 一个方向:在这里是摄像机的朝向,前向向量。
  • +
  • 边界框的最小坐标。在本例中,立方体以GameItem坐标为中心,最小坐标是该坐标减去比例。(在其原始大小中,立方体的长度为2,比例为1)。
  • +
  • 边界框的最大坐标。不言而喻。
  • +
  • 一个结果向量。它将储存到远近交点的距离(对于一个轴对齐边界框和一条射线,最多有两个交点)。
  • +
+

如果有交点,该方法将返回true。如果为true,我们将检查最近距离并在必要时更新它,并储存所选候选GameItem的引用。下图展示了该方法中涉及的所有要素。

+

交点

+

一旦循环完成,候选GamItem将被标记为已选定。

+

这就是全部了。selectGameItem将在DummyGame类的更新方法中调用,同时调用的还有观察矩阵更新。

+
// 更新观察矩阵
+camera.updateViewMatrix();
+
+// 更新声音监听器位置
+soundMgr.updateListenerPosition(camera);
+
+this.selectDetector.selectGameItem(gameItems, camera);
+
+

此外,一个十字瞄准线(Cross-hair)已添加到渲染中,以检查一切工作正常。结果如下图所示:

+

物体选取结果

+

显然,这里给出的方法远远不是最佳的,但它将为你提供基础知识,是你能够自己开发更复杂的方法。场景的某些部分很容易被丢弃,比如摄像机后面的物体,因为它们不会相交。除此之外,你换可以根据摄像机的距离来确定物体,以加快计算速度。此外,只有在摄像机移动或旋转后,才需要进行计算。

+

鼠标选取

+

用摄像机选取物体完成了,但当我们想用鼠标自由选择物体怎么办?在此情况下,我们希望每当用户单击屏幕时,自动选择最近的对象。

+

实现它的方法类似于上述方法。在之前的方法中,我们得到了摄像机的位置,并根据摄像机当前的方向使用“前向”方向从摄像机生成射线。在此情况下,我们仍需要投射广西,但方向指向一个远离摄像机的点,也就是点击的点。在本例中,我们需要使用点击的坐标来计算方向向量。

+

但是,我们如何将视口空间中的坐标变换到世界空间呢?让我们回顾一下如何从模型空间坐标变换到观察空间。为了达到这个目的,所应用的不同坐标变换是:

+
    +
  • 我们使用模型矩阵将模型坐标变换到世界坐标。
  • +
  • 我们使用观察矩阵(提供摄像机功能)将世界坐标转换到观察空间坐标。
  • +
  • 我们通过应用透视投影矩阵将观察坐标变换到齐次裁剪空间(Homogeneous Clip Space)。
  • +
  • 最终的屏幕坐标由OpenGL为我们自动计算。在此之前,它传递到归一化的终端空间(通过将坐标除以分量),然后传递到屏幕坐标。
  • +
+

所以我们只需要从屏幕坐标到世界坐标,逆路径遍历。

+

第一步是将屏幕坐标转换为归一化的终端空间。视口空间中的坐标的范围是 +。屏幕左上角的坐标为。我们需要将其转换为范围内的坐标。

+

屏幕坐标到归一化终端空间

+

很简单的数学:

+

+ +

+

+ +

+

但是,我们如何计算分量呢?答案很简单,我们只需给它分配值,这样广西就可以指向最远的可见距离(请记住,在OpenGL中,指向频幕)。现在我们有了归一化终端空间中的坐标。

+

为了继续变换,我们需要将它们转换为齐次剪切空间。我们需要有一个分量,它使用齐次坐标。虽然这个概念在前几章已经介绍过了,但让我们再回顾它。为了表示一个三维点,我们需要分量,但是我们一直在处理一个额外的分量。我们需要这个额外的分量来使用矩阵执行不同的变换。有些变化不需要这个额外的分量,但有些变换需要。例如,如果我们只有分量,那么变换矩阵就不能工作。因此,我们添加了分量,并为它们赋值为,这样我们就可以处理4x4矩阵了。

+

此外,大多数变换,或者更精确地说,大多数变换矩阵都不会更改分量。投影矩阵是一个例外,该矩阵将分量更改为与分量成比例。

+

通过将分量处以,就可以实现从齐次裁剪空间到归一化的终端坐标的转换。由于这个分量与分量成比例,意味着远处的物体被画得更小。在本例中,需要反其道而行之,我们可以忽略这一步,只需要将分量设置为,并保留其它组件的原始值。

+

我们现在需要回到观察空间。这很简单,我们只需要计算投影矩阵的逆矩阵并将它与4个分量向量相乘。完成之后,我们就需要把它们转换成世界空间。同样,我们只需要使用观察矩阵,计算它的逆矩阵然后乘以向量。

+

记住,我们只对方向感兴趣,因此,在本例中,我们将分量设置为。我们还可以将组件再次设置为,因为我们希望它指向频幕。一旦这样做并应用逆矩阵,我们就得到了世界空间中的向量。我们计算了射线,可以使用与摄像机选取相同的算法。

+

我们创建了一个名为MouseBoxSelectionDetector的新类,它实现了上述步骤。此外,我们将投影矩阵移动到Window类,所以我们可以在几个地方使用它们。我们还重构了一点CameraBoxSelectionDetector,所以MouseBoxSelectionDetector可以继承和使用碰撞检测方法。你可以直接查看源代码,因为实现非常简单。

+

结果如下所示:

+

鼠标选取

+

你只需用鼠标单击该方块即可进行选取。

+

之后你可以参考一篇优秀文章中背完所解释的步骤的细节,其中包含了涉及不同方案的非常详细的说明。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/24-hud-revisited/index.html b/24-hud-revisited/index.html new file mode 100755 index 0000000..b7cee7e --- /dev/null +++ b/24-hud-revisited/index.html @@ -0,0 +1,373 @@ + + + + + + + + + + + + 回顾HUD - NanoVG - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

回顾HUD - NanoVG(HUD Revisited - NanoVG)

+

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

+

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

+

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

+
...
+<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-nanovg</artifactId>
+    <version>${lwjgl.version}</version>
+</dependency>
+...
+<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-nanovg</artifactId>
+    <version>${lwjgl.version}</version>
+    <classifier>${native.target}</classifier>
+    <scope>runtime</scope>
+</dependency>
+
+

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

+
glEnable(GL_STENCIL_TEST);
+
+

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

+
public void clear() {
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+}
+
+

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

+
if (opts.antialiasing) {
+    glfwWindowHint(GLFW_SAMPLES, 4);
+}
+
+

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

+

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

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

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

+

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

+

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

+
@Override
+public void init(Window window) throws Exception {
+    hud.init(window);
+    renderer.init(window);
+    ...
+
+

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

+
@Override
+public void render(Window window) {
+    renderer.render(window, camera, scene);
+    hud.render(window);
+}
+
+

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

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

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

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

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

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

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

+

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

+

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

+
// 还原状态
+window.restoreState();
+
+

方法的定义如下:

+
public void restoreState() {
+    glEnable(GL_DEPTH_TEST);
+    glEnable(GL_STENCIL_TEST);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    if (opts.cullFace) {
+        glEnable(GL_CULL_FACE);
+        glCullFace(GL_BACK);
+    }
+}
+
+

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

+

Hud

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/25-optimizations-frustum-culling/index.html b/25-optimizations-frustum-culling/index.html new file mode 100755 index 0000000..3071c6b --- /dev/null +++ b/25-optimizations-frustum-culling/index.html @@ -0,0 +1,408 @@ + + + + + + + + + + + + 优化 - 截锥剔除 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

优化 - 截锥剔除(Optimizations - Frustum Culling)

+

优化 - 截锥剔除一

+

现在我们使用了许多不同的图形效果,例如光照、粒子等。此外,我们还学习了如何实例化渲染,以减少绘制许多相似对象的开销。然而,我们仍有足够的空间进行一些简单的优化,这将增加可以达到的帧率(FPS)。

+

你可能想知道为什么我们会在每一帧中绘制整个游戏项列表,即使其中一些项不可见(因为它们在摄像机后面或距离摄像机太远)。你甚至可能认为这是由OpenGL自动处理的,这在某种程度上是正确的。OpenGL将放弃位于可见区域之外的顶点的渲染,这称作裁剪(Clipping)。裁剪的问题是,在执行顶点着色之后,按顶点进行处理的。因此,即使此操作节省了资源,我们也可以通过不尝试渲染不可见的对象来提高效率。我们不会通过将数据发送到GPU以及对这些对象的每个顶点进行变换来浪费资源。我们需要移除不包含在视锥体(View Frustum)中的对象,也就是说,我们需要进行截锥剔除。

+

但是,首先让我们回顾一下什么是视锥体。视锥体是一个结合摄像机的位置和旋转以及使用的投影,包含所有可见物体的体积。通常,视锥体是一个四棱台,如下图所示:

+

视锥体I

+

如你所见,视锥体由六个平面定义,位于视锥体之外的任何内容都不会渲染。因此,截锥剔除是移除视锥体之外的对象的过程。

+

因此,为了进行截锥剔除,我们需要:

+
    +
  • +

    使用观察和投影矩阵中包含的数据计算截锥平面。

    +
  • +
  • +

    对每个游戏项检查它是否包含在视锥体中,换句话说,在大小截锥平面之间。并从渲染流程中删除那些不包含在其中的。

    +
  • +
+

视锥体II

+

那么让我们从计算截锥平面开始。平面由包含在其中的点和与该平面正交的向量定义,如下图所示:

+

平面

+

平面方程的定义如下:

+

+ +

+

因此,我们需要计算视锥体的六个侧面的六个平面方程。为了达成这个目标,你基本上有两个选项。你可以进行繁琐的计算,得到六个平面方程的来自上述方程的四个常数(A、B、C和D)。另一个选项是让JOML库为你计算这个值。通常情况下,我们选择后一个选项。

+

让我们开始编码吧。我们将创建一个名为FrustumCullingFilter的新类,跟它的名字相同,它将根据视锥体执行筛选操作。

+
public class FrustumCullingFilter {
+
+    private static final int NUM_PLANES = 6;
+
+    private final Matrix4f prjViewMatrix;
+
+    private final Vector4f[] frustumPlanes;
+
+    public FrustumCullingFilter() {
+        prjViewMatrix = new Matrix4f();
+        frustumPlanes = new Vector4f[NUM_PLANES];
+        for (int i = 0; i < NUM_PLANES; i++) {
+            frustumPlanes[i] = new Vector4f();
+        }
+    }
+
+

FrustumCullingFilter类也将有一个方法来计算平面方程,名为updateFrustum,它将在渲染之前调用。方法定义如下:

+
public void updateFrustum(Matrix4f projMatrix, Matrix4f viewMatrix) {
+    // 计算投影观察矩阵
+    prjViewMatrix.set(projMatrix);
+    prjViewMatrix.mul(viewMatrix);
+    // 获取视锥体平面
+    for (int i = 0; i < NUM_PLANES; i++) {
+        prjViewMatrix.frustumPlane(i, frustumPlanes[i]);
+    }
+}
+
+

首先,我们储存投影矩阵的副本,并将其与观察矩阵相乘,得到投影观察矩阵。然后,使用这个变换矩阵,我们只需要为每个截锥平面调用frustumPlane方法。需要注意的是,这些平面方程是用世界坐标表示的,所以所有的计算都要在这个空间中进行。

+

现在我们已经计算了所有平面,我们只需要检查GameItem实例是否在截锥体中。该怎么做?让我们首先确认一下如何检查一个点是否在截锥体内,可以通过计算点到每个平面的有符号的距离来实现这一点。如果点到平面的距离是正的,这意味着点在平面的前面(根据其法线)。如果是负的,则意味着点在平面的后面。

+

到平面的距离

+

因此,如果到截锥的所有平面的距离为正,则一个点位于视锥体的内部。点到平面的距离定义如下:

+

+,其中是点的坐标。

+

因此,如果则点在平面的后面。

+

但是,我们没有点,只有复杂的网格,我们不能仅仅用点来检查一个物品是否在截锥体内。你可以考虑检查GameItem的每个顶点,看看它是否在截锥体内。如果任何一个点在里面,游戏项应该被绘制出来。但这就是OpenGL在裁剪时所做的,也是我们要避免的。记住,网格越复杂,截锥剔除的好处越明显。

+

我们需要把每一个GameItem放到一个简单的体中,这个体很容易检查。这里我们有两个选项:

+
    +
  • +

    边界盒(Bounding Box)。

    +
  • +
  • +

    边界球(Bounding Sphere)。

    +
  • +
+

在本例中,我们将使用球体,因为这是最简单的方法。我们将把每一个游戏项放在一个球体中,并检查球体是否位于视锥体中。为了做到它,我们只需要球体的中心和半径。检查它几乎等同于检查点,但是我们需要考虑板甲。如果满足以下条件,则球体将位于截锥之外:

+

边界球

+

因此,我们将在FrustumCullingFilter类中添加一个新方法来检查球体是否在截锥中。方法的定义如下:

+
public boolean insideFrustum(float x0, float y0, float z0, float boundingRadius) {
+    boolean result = true;
+    for (int i = 0; i < NUM_PLANES; i++) {
+        Vector4f plane = frustumPlanes[i];
+        if (plane.x * x0 + plane.y * y0 + plane.z * z0 + plane.w <= -boundingRadius) {
+            result = false; return result;
+        }
+    }
+    return result;
+}
+
+

然后,我们将添加过滤视锥体外的游戏项的方法:

+
public void filter(List<GameItem> gameItems, float meshBoundingRadius) {
+    float boundingRadius;
+    Vector3f pos;
+    for (GameItem gameItem : gameItems) {
+        boundingRadius = gameItem.getScale() * meshBoundingRadius;
+        pos = gameItem.getPosition();
+        gameItem.setInsideFrustum(insideFrustum(pos.x, pos.y, pos.z, boundingRadius));
+    }
+}
+
+

我们在GameItem类中添加了一个新的属性insideFrustum来跟踪可见性。如你所见,边界球的板甲作为参数传递。这是由于边界球与Mesh管理,它不是GameItem的属性。但是,请记住,我们必须在世界坐标中操作,并且边界球的半径将在模型空间震。我们将应用为GameItem设置的比例将其转换为世界空间,我们还假设GameItem的位置是球体的中心(在世界空间坐标系中)。

+

最后一个方法只是一个实用方法,它接受网格表并过滤其中包含的所有GameItem实例:

+
public void filter(Map<? extends Mesh, List<GameItem>> mapMesh) {
+    for (Map.Entry<? extends Mesh, List<GameItem>> entry : mapMesh.entrySet()) {
+        List<GameItem> gameItems = entry.getValue();
+        filter(gameItems, entry.getKey().getBoundingRadius());
+    }
+}
+
+

就这样。我们可以在渲染流程中使用该类,只需要更新截锥平面,计算出哪些游戏项是可见的,并在绘制实例网格和非实例网格时过滤掉它们:

+
frustumFilter.updateFrustum(window.getProjectionMatrix(), camera.getViewMatrix());
+frustumFilter.filter(scene.getGameMeshes());
+frustumFilter.filter(scene.getGameInstancedMeshes());
+
+

你可以启用或禁用过滤功能,并可以检查你可以达到的FPS的增加和减少。在过滤时不考虑粒子,但是添加它是很简单的。对于粒子,在任何情况下最好检查发射器的位置,而不是检查每个粒子。

+

优化 - 截锥剔除二

+

解释了截锥剔除的基础,我们可以使用JOML库中提供的更精细的方法。它特别地提供了一个名为FrustumIntersection的类,该类以按此文章所述的一种更有效的方式获取视锥体的平面。除此之外,该类还提供了测试边界盒、点和球体的方法。

+

那么,让我们修改FrustumCullingFilter类。属性和构造函数简化如下:

+
public class FrustumCullingFilter {
+
+    private final Matrix4f prjViewMatrix;
+
+    private FrustumIntersection frustumInt;
+
+    public FrustumCullingFilter() {
+        prjViewMatrix = new Matrix4f();
+        frustumInt = new FrustumIntersection();
+    }
+
+

updateFrustum方法只是将平面获取委托给FrustumIntersection实例:

+
public void updateFrustum(Matrix4f projMatrix, Matrix4f viewMatrix) {
+    // 计算投影识图矩阵
+    prjViewMatrix.set(projMatrix);
+    prjViewMatrix.mul(viewMatrix);
+    // 更新截锥相交类
+    frustumInt.set(prjViewMatrix);
+}
+
+

insideFrustum方法更简单:

+
public boolean insideFrustum(float x0, float y0, float z0, float boundingRadius) {
+    return frustumInt.testSphere(x0, y0, z0, boundingRadius);
+}
+
+

使用该方法,你甚至可以达到更高的FPS。此外,还向Window类中添加了一个全局标记,以启用或禁用截锥剔除。GameItem类也有启用或禁用过滤的标记,因为对于某些项,截锥剔除过滤可能没有意义。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/26-cascaded-shadow-maps/index.html b/26-cascaded-shadow-maps/index.html new file mode 100755 index 0000000..3e8ecc9 --- /dev/null +++ b/26-cascaded-shadow-maps/index.html @@ -0,0 +1,449 @@ + + + + + + + + + + + + 级联阴影映射 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

级联阴影映射(Cascaded Shadow Maps)

+

在阴影一章中,我们介绍了阴影映射技术,以便在渲染三维场景时使用平行光显示阴影。此前介绍的方案中要求你手动调整一些参数以改进效果。在本章中,我们将修改该技术,以自动化所有流程,并改进在开放空间中的效果。为了达到目的,我们将使用一种称为级联阴影映射(CMS,Cascaded Shadow Map)的技术。

+

让我们首先看一下如何自动构造用于渲染阴影的光照视图矩阵和正交投影矩阵。如果你回想阴影一章,我们需要从光线的角度来绘制场景。这意味着创建一个光照视图矩阵,它就像一个作用于光源的摄像机和一个投影矩阵。由于光是定向的,而且应该位于无穷远处,所以我们选择了正交投影。

+

我们希望所有可见的物体都适用于光照视图投影矩阵。因此,我们需要将视截锥放入光截锥中。下图是我们最终想要实现的效果:

+

视截锥

+

该如何构造它?首先是计算观察投影矩阵的截锥顶点。我们得到世界空间的坐标,然后计算这个截锥的中心,这可以通过将所有的顶点的坐标相加并将结果除以顶点的数量来计算。

+

截锥中心

+

有了这些数据,我们就可以设置光源的位置。其位置及其方向将用于构建光照视图矩阵。为了计算位置,我们从此前得到的视锥体的中心开始,逆着光的方向,取相当于视锥体的近Z平面和远Z平面的距离的一点。

+

光源位置

+

构建完成光照视图矩阵,我们需要设置正交投影矩阵。为了计算它们,我们将截锥的顶点转换到光照视图空间,只需要将它们乘以刚刚构建的光照视图矩阵。投影矩阵的尺寸是最大和最小的X和Y值,近Z平面可以设置为与标准投影矩阵相同的值,远Z平面则是光照视图空间中截锥顶点的最大和最小Z值之间的距离。

+

但是,如果在阴影示例代码的基础上实现上述算法,则可能会对阴影质量感到失望。

+

低质量阴影

+

原因是阴影分辨率受纹理大小的限制。我们现在正覆盖一个潜在的巨大区域,并且我们用来储存深度信息的纹理没有足够的分辨率来取得良好的效果。你可能认为解决方法只是提高纹理分辨率,但这并不足以完全解决问题,你需要巨大的纹理。

+

有一个更聪明的解决方案。其关键是,靠近摄像机的物体需要比远处物体的阴影有更高的质量。一种方法是只渲染靠近摄像机的对象的阴影,但这会导致阴影在场景中移动时出现或消失。

+

级联阴影映射(CSM)使用的方法是将视锥体分割为多个部分。离摄像机较近的部分会覆盖较小的区域,而距离较远的部分会覆盖更广的区域。下图显示了把一个视锥体分为三个部分。

+

级联分割

+

对于每个部分,将渲染深度纹理,调整光照视图和投影矩阵以合适地覆盖每个分割的部分。因此,储存深度映射的纹理覆盖视锥体的区域缩小了。而且,由于离摄像机最近的部分会占用较少的空间,因此深度分辨率会提高。

+

从上述解释可以看出,我们需要尽可能多的将深度图进行分割,我们还将更改每个光照视图和投影矩阵。因此,要使用CSM需要做的事情是:

+
    +
  • 将视锥体分为n个部分。
  • +
  • 渲染深度纹理时,对于每个部分:
  • +
  • 计算光照视图和投影矩阵。
  • +
  • 从光源的角度将场景渲染为单独的深度图。
  • +
  • 渲染场景时:
  • +
  • 使用此前计算的深度图。
  • +
  • 确定要绘制的片段所属的部分。
  • +
  • 计算阴影因子,如阴影映射中所述。
  • +
+

如你所见,CSM的主要缺点是我们需要从灯光的角度为每个部分渲染场景。这就是为什么通常只用于开放空间的原因。不管怎么说,我们将可以看到如何轻松地减少开销。

+

所以,让我们看看代码。但是在继续之前,有一个小小的提醒,我不会在这里写上完整的源代码,因为这会非常枯燥。相反,我将介绍主要类、它们的职责和可能需要进一步解释的片段,以便更好地理解。所有与着色相关的类都已移动到一个名为org.lwjglb.engine.graph.shadow的新包中。

+

渲染阴影的代码,换句话说,从光影透视的场景已经移动到了ShadowRenderer类中。(该代码以前在Renderer类中)。

+

类定义了以下常量:

+
public static final int NUM_CASCADES = 3;
+public static final float[] CASCADE_SPLITS = new float[]{Window.Z_FAR / 20.0f, Window.Z_FAR / 10.0f, Window.Z_FAR};
+
+

首先是层级或拆分的数量。第二个定义了每个拆分的部分的远Z平面的位置。如你所见,它们的间距并不相等。离摄像机较近的部分在Z平面上的距离最短。

+

类还储存了用于渲染深度图的着色器程序的引用,一个列表,其中包含与每个部分相关联的信息,其由ShadowCascade类定义,以及对储存深度图数据(纹理)的对象的引用,由ShadowBuffer类定义。

+

ShadowRenderer类具有用于设置着色器和所需属性的方法以及一个渲染方法。render方法的定义如下所示:

+
public void render(Window window, Scene scene, Camera camera, Transformation transformation, Renderer renderer) {
+    update(window, camera.getViewMatrix(), scene);
+
+    // 设置视口以匹配纹理大小
+    glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer.getDepthMapFBO());
+    glViewport(0, 0, ShadowBuffer.SHADOW_MAP_WIDTH, ShadowBuffer.SHADOW_MAP_HEIGHT);
+    glClear(GL_DEPTH_BUFFER_BIT);
+
+    depthShaderProgram.bind();
+
+    // 为每层图渲染场景
+    for (int i = 0; i < NUM_CASCADES; i++) {
+        ShadowCascade shadowCascade = shadowCascades.get(i);
+
+        depthShaderProgram.setUniform("orthoProjectionMatrix", shadowCascade.getOrthoProjMatrix());
+        depthShaderProgram.setUniform("lightViewMatrix", shadowCascade.getLightViewMatrix());
+
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
+        glClear(GL_DEPTH_BUFFER_BIT);
+
+        renderNonInstancedMeshes(scene, transformation);
+
+        renderInstancedMeshes(scene, transformation);
+    }
+
+    // 解绑
+    depthShaderProgram.unbind();
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+

如你所见,我们为每个部分执行的几个渲染过程,类似于此前阴影图的渲染方法。在每次执行中,我们都会使用相关的ShadowCascade实例中保函的信息更改光照视图矩阵和正交投影矩阵。

+

此外,在每次执行中,我们都需要更改正在使用的纹理。每次都会将深度信息渲染为不同的纹理。此信息储存在ShadowBuffer类中,并设置为供FBO使用,代码如下:

+
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
+
+

正如刚刚提到的,ShadowBuffer类储存与用于储存深度信息的纹理相关的信息。代码与阴影章节中使用的代码非常相似,只是我们使用的是纹理数组。因此,我们创建了一个新的类ArrTexture,它创建了一个具有相同属性的纹理数组。此类还提供了一个bind方法,用于绑定所有纹理数组,以便在场景着色器中使用它们。该方法接收一个参数,从某个纹理单元开始绑定。

+
public void bindTextures(int start) {
+    for (int i = 0; i < ShadowRenderer.NUM_CASCADES; i++) {
+        glActiveTexture(start + i);
+        glBindTexture(GL_TEXTURE_2D, depthMap.getIds()[i]);
+    }
+}
+
+

ShadowCascade类储存与一个部分关联的光照视图和正交投影矩阵。每个分割的部分由一个近Z平面距离和一个远Z平面距离定义,并根据该信息计算矩阵。

+

该类提供并更新了以观察矩阵和光照方向为输入的方法,该方法首先计算世界空间中的视锥顶点,然后计算出光源的位置。根据光的方向,从截锥的中心到相当于远Z平面到近Z平面之间的距离的距离,计算出该位置。

+
public void update(Window window, Matrix4f viewMatrix, DirectionalLight light) {
+    // 为此层生成投影识图矩阵
+    float aspectRatio = (float) window.getWidth() / (float) window.getHeight();
+    projViewMatrix.setPerspective(Window.FOV, aspectRatio, zNear, zFar);
+    projViewMatrix.mul(viewMatrix);
+
+    // 计算世界空间中的截锥顶点
+    float maxZ = -Float.MAX_VALUE;
+    float minZ =  Float.MAX_VALUE;
+    for (int i = 0; i < FRUSTUM_CORNERS; i++) {
+        Vector3f corner = frustumCorners[i];
+        corner.set(0, 0, 0);
+        projViewMatrix.frustumCorner(i, corner);
+        centroid.add(corner);
+        centroid.div(8.0f);
+        minZ = Math.min(minZ, corner.z);
+        maxZ = Math.max(maxZ, corner.z);
+    }
+
+    // 从质心逆着光的方向上max.z-min.z的距离
+    Vector3f lightDirection = light.getDirection();
+    Vector3f lightPosInc = new Vector3f().set(lightDirection);
+    float distance = maxZ - minZ;
+    lightPosInc.mul(distance);
+    Vector3f lightPosition = new Vector3f();
+    lightPosition.set(centroid);
+    lightPosition.add(lightPosInc);
+
+    updateLightViewMatrix(lightDirection, lightPosition);
+
+    updateLightProjectionMatrix();
+}
+
+

根据光源的位置和方向,我们可以构造光照视图矩阵。这是在updateLightViewMatrix方法中完成的:

+
private void updateLightViewMatrix(Vector3f lightDirection, Vector3f lightPosition) {
+    float lightAngleX = (float) Math.toDegrees(Math.acos(lightDirection.z));
+    float lightAngleY = (float) Math.toDegrees(Math.asin(lightDirection.x));
+    float lightAngleZ = 0;
+    Transformation.updateGenericViewMatrix(lightPosition, new Vector3f(lightAngleX, lightAngleY, lightAngleZ), lightViewMatrix);
+}
+
+

最后,我们需要构造正交投影矩阵。这是在updateLightProjectionMatrix方法中完成的。该方法将视锥体坐标转换到光照空间。然后我们得到x、y坐标的最小值和最大值来构造包围视锥体的边界框。近Z平面可以设置为0,远Z平面可以设置为坐标的最大值和最小值之间的距离。

+
private void updateLightProjectionMatrix() {
+    // 现在计算光照空间中的截锥大小。
+    float minX =  Float.MAX_VALUE;
+    float maxX = -Float.MAX_VALUE;
+    float minY =  Float.MAX_VALUE;
+    float maxY = -Float.MAX_VALUE;
+    float minZ =  Float.MAX_VALUE;
+    float maxZ = -Float.MAX_VALUE;
+    for (int i = 0; i < FRUSTUM_CORNERS; i++) {
+        Vector3f corner = frustumCorners[i];
+        tmpVec.set(corner, 1);
+        tmpVec.mul(lightViewMatrix);
+        minX = Math.min(tmpVec.x, minX);
+        maxX = Math.max(tmpVec.x, maxX);
+        minY = Math.min(tmpVec.y, minY);
+        maxY = Math.max(tmpVec.y, maxY);
+        minZ = Math.min(tmpVec.z, minZ);
+        maxZ = Math.max(tmpVec.z, maxZ);
+    }
+    float distz = maxZ - minZ;
+
+    orthoProjMatrix.setOrtho(minX, maxX, minY, maxY, 0, distz);
+}
+
+

记住,正交投影就像一个边界框,应该包含所有将要渲染的对象。该边界框以光照视图坐标空间表示。因此,我们要做的是计算包围视锥体的最小边界框,轴与光源位置对齐。

+

Renderer类已被修改为使用视图包中的类以及修改传递给渲染器的信息。在渲染器中,我们需要处理模型、模型观察和光照视图矩阵。在此前的章节中,我们使用模型观察或光照视图矩阵来减少操作的数量。在本例中,我们选择简化要传递的元素的数量,现在只将模型、观察和光照矩阵传递给着色器。此外,对于粒子,我们需要保留比例,因为我们不再传递模型观察矩阵,所以现在信息将丢失。我们重用用于标记所选项的属性来设置比例信息。在粒子着色中,我们将使用该值再次设置缩放。

+

在场景的顶点着色器中,我们为每个部分计算模型光照视图矩阵,并将其作为输出传递给片元着色器。

+
mvVertexPos = mvPos.xyz;
+for (int i = 0 ; i < NUM_CASCADES ; i++) {
+    mlightviewVertexPos[i] = orthoProjectionMatrix[i] * lightViewMatrix[i] * modelMatrix * vec4(position, 1.0);
+}
+
+

在片元着色器中,我们使用这些值根据片元所处的部分来查询适当的深度图。这需要在片元着色器中完成,因为对于特定项目,它们的片元可能位于不同的部分中。

+

此外,在片元着色器中,我们必须决定要分成哪个部分。为了实现它,我们使用片元的Z值,并将其与每个部分的最大Z值进行比较,也就是远Z平面值。这些数据作为一个新的Uniform传递:

+
uniform float cascadeFarPlanes[NUM_CASCADES];
+
+

如下所示,我们计算分割的部分,变量idx将储存要使用的部分:

+
int idx;
+for (int i=0; i<NUM_CASCADES; i++)
+{
+    if ( abs(mvVertexPos.z) < cascadeFarPlanes[i] )
+    {
+        idx = i;
+        break;
+    }
+}
+
+

此外,在场景着色器中,我们需要传递一个纹理数组,一个sampler2D数组,用于深度图,即与我们分割部分相关联的纹理。源代码用一个Uniform列表(而不是使用数组)储存用于引用与每个部分关联的深度图的纹理单元。

+
uniform sampler2D normalMap;
+uniform sampler2D shadowMap_0;
+uniform sampler2D shadowMap_1;
+uniform sampler2D shadowMap_2;
+
+

将其更改为一组Uniform会导致难以追踪此示例的其他纹理出现的问题。在任何情况下,你都可以尝试在代码中修改它。

+

源代码中的其余修改和着色器只是基于上述更改所需的调整,你可以直接在源代码上查看它。

+

最后,当引入这些更改时,你可能会发现性能下降,这是因为我们绘制的深度图是此前的三倍。如果摄像机未被移动或场景项未发生更改,则不需要反复渲染深度图。深度图储存在纹理中,因此不需要每次渲染调用清除它们。因此,我们在render方法中添加了一个新变量,用于指示是否更新深度图,以避免更新深度图,使其保持不变,这会显著提高FPS。最终,你会得到这样的效果:

+

最终效果

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/27-assimp/index.html b/27-assimp/index.html new file mode 100755 index 0000000..0058e6f --- /dev/null +++ b/27-assimp/index.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + Assimp库 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

Assimp库(Assimp)

+

静态网格

+

加载不同格式的复杂三维模型的功能对于编写游戏至关重要,为其中一些编写解析器需要大量的工作,即便只支持一种格式也可能很耗时。例如,在第九章中描述的wavefront加载器只解析了规范中的一小部分(根本不处理材质)。

+

幸运的是,已经可以使用Assimp库来解析许多常见的3D格式。它是一个C++库,可以以各种格式加载静态和动画模型。LWJGL提供了绑定以便从Java代码中使用它们。在本章中,我们将学习如何使用它。

+

首先是将Assimp的Maven依赖项添加到pom.xml文件中。我们需要添加编译时和运行时依赖项。

+
<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-assimp</artifactId>
+    <version>${lwjgl.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.lwjgl</groupId>
+    <artifactId>lwjgl-assimp</artifactId>
+    <version>${lwjgl.version}</version>
+    <classifier>${native.target}</classifier>
+    <scope>runtime</scope>
+</dependency>
+
+

一旦设置了依赖项,我们将创建一个名为StaticMeshesLoader的新类,该类将用于加载不带动画的网格,该类定义了两个静态公共方法:

+
public static Mesh[] load(String resourcePath, String texturesDir) throws Exception {
+    return load(resourcePath, texturesDir, aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FixInfacingNormals);
+}
+
+public static Mesh[] load(String resourcePath, String texturesDir, int flags) throws Exception {
+    // ....
+
+

两种方法都有以下参数: +* resourcePath:模型文件所在的文件路径。这是一个绝对路径,因为Assimp可能需要加载其他文件(例如wavefront、OBJ等文件的材质文件),并且可能使用与资源路径相同的基路径。如果将资源嵌入JAR文件中,那么assimp将无法导入它,因此它必须是文件系统路径。 +* texturesDir:保存此模型的文件夹路径。这是CLASSPATH的相对路径。例如,wavefront材质文件可以定义多个纹理文件。代码希望此文件位于texturesDir目录下。如果发现纹理加载错误,可能需要在模型文件中手动调整这些路径。

+

第二个方法有一个名为flags的额外参数。此参数允许调整加载过程。第一个方法调用第二个方法,并传递一些在大多数情况下都有用的值: +* aiProcess_JoinIdenticalVertices:此标记将减少使用的顶点数,识别可在面之间重用的顶点。 +* aiProcess_Triangulate:模型可以使用四边形或其他几何图形来定义它们的元素。由于我们只处理三角形,因此必须使用此标记将所有的面拆分为三角形(如果有必要的话)。 +* aiProcess_FixInfacingNormals:此标记将尝试反转可能指向内部的法线。

+

还有许多其他标记可以使用,你可以在LWJGL的Javadoc文档中查阅它们。

+

回看到第二个载入方法,我们要做的第一件事是调用aiImportFile方法来加载带有所选标记的模型。

+
AIScene aiScene = aiImportFile(resourcePath, flags);
+if (aiScene == null) {
+    throw new Exception("Error loading model");
+}
+
+

载入方法的其余代码如下所示:

+
int numMaterials = aiScene.mNumMaterials();
+PointerBuffer aiMaterials = aiScene.mMaterials();
+List<Material> materials = new ArrayList<>();
+for (int i = 0; i < numMaterials; i++) {
+    AIMaterial aiMaterial = AIMaterial.create(aiMaterials.get(i));
+    processMaterial(aiMaterial, materials, texturesDir);
+}
+
+int numMeshes = aiScene.mNumMeshes();
+PointerBuffer aiMeshes = aiScene.mMeshes();
+Mesh[] meshes = new Mesh[numMeshes];
+for (int i = 0; i < numMeshes; i++) {
+    AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
+    Mesh mesh = processMesh(aiMesh, materials);
+    meshes[i] = mesh;
+}
+
+return meshes;
+
+

我们处理模型中包含的材质,材质定义组成模型的网格使用的颜色和纹理。然后我们处理不同的网格,模型可以定义多个网格,每个网格都可以使用为模型定义的一种材质。

+

如果你看到上面的代码,你可能会看到很多对Assimp库的调用返回的PointerBuffer实例。你可以用C指针那样的方式看待它们,它们只是指向储存数据的内存区域。你需要提前知道它们储存的数据类型,以便处理它们。对于材质,我们迭代该缓冲区,创建AIMaterial类的实例。在第二种情况下,我们迭代储存网格数据的缓冲区,创建AIMesh类的实例。

+

让我们看看processMaterial方法:

+
private static void processMaterial(AIMaterial aiMaterial, List<Material> materials, String texturesDir) throws Exception {
+    AIColor4D colour = AIColor4D.create();
+
+    AIString path = AIString.calloc();
+    Assimp.aiGetMaterialTexture(aiMaterial, aiTextureType_DIFFUSE, 0, path, (IntBuffer) null, null, null, null, null, null);
+    String textPath = path.dataString();
+    Texture texture = null;
+    if (textPath != null && textPath.length() > 0) {
+        TextureCache textCache = TextureCache.getInstance();
+        texture = textCache.getTexture(texturesDir + "/" + textPath);
+    }
+
+    Vector4f ambient = Material.DEFAULT_COLOUR;
+    int result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_AMBIENT, aiTextureType_NONE, 0, colour);
+    if (result == 0) {
+        ambient = new Vector4f(colour.r(), colour.g(), colour.b(), colour.a());
+    }
+
+    Vector4f diffuse = Material.DEFAULT_COLOUR;
+    result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_DIFFUSE, aiTextureType_NONE, 0, colour);
+    if (result == 0) {
+        diffuse = new Vector4f(colour.r(), colour.g(), colour.b(), colour.a());
+    }
+
+    Vector4f specular = Material.DEFAULT_COLOUR;
+    result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_SPECULAR, aiTextureType_NONE, 0, colour);
+    if (result == 0) {
+        specular = new Vector4f(colour.r(), colour.g(), colour.b(), colour.a());
+    }
+
+    Material material = new Material(ambient, diffuse, specular, 1.0f);
+    material.setTexture(texture);
+    materials.add(material);
+}
+
+

我们检查材质是否定义了纹理。如果有,我们加载纹理。我们创建了一个名为TextureCache的新类,用于缓存纹理。这是因为几个网格可能共享相同的纹理,我们不想浪费空间一次又一次加载相同的数据。然后我们尝试获得环境、漫反射和镜面反射的材质颜色分量。幸运的是,我们对材质的定义已经包含了这些信息。

+

TextureCache的定义非常简单,它只是一个映射,通过纹理文件的路径对不同的纹理进行索引(你可以直接查看源代码)。由于现在纹理可能使用不同的图像格式(PNG、JPEG等),我们已经修改了纹理的加载方式,现在使用STB库来加载更多的格式,而不是使用PNG库。

+

让我们看到StaticMeshesLoader类。processMesh的定义如下:

+
private static Mesh processMesh(AIMesh aiMesh, List<Material> materials) {
+    List<Float> vertices = new ArrayList<>();
+    List<Float> textures = new ArrayList<>();
+    List<Float> normals = new ArrayList<>();
+    List<Integer> indices = new ArrayList();
+
+    processVertices(aiMesh, vertices);
+    processNormals(aiMesh, normals);
+    processTextCoords(aiMesh, textures);
+    processIndices(aiMesh, indices);
+
+    Mesh mesh = new Mesh(Utils.listToArray(vertices),
+        Utils.listToArray(textures),
+        Utils.listToArray(normals),
+        Utils.listIntToArray(indices)
+    );
+    Material material;
+    int materialIdx = aiMesh.mMaterialIndex();
+    if (materialIdx >= 0 && materialIdx < materials.size()) {
+        material = materials.get(materialIdx);
+    } else {
+        material = new Material();
+    }
+    mesh.setMaterial(material);
+
+    return mesh;
+}
+
+

Mesh由一组顶点位置、法线方向、纹理坐标和索引定义。每个元素都在processVerticesprocessNormalsprocessTextCoordsprocessIndices方法中处理,网格也可以使用其索引指向材质。如果索引与之前处理的材质相对应,我们只需将它们与Mesh相关联。

+

processXXX方法非常简单,它们只是在返回所需数据的AIMesh实例上调用相应的方法。例如,processVertices的定义如下:

+
private static void processVertices(AIMesh aiMesh, List<Float> vertices) {
+    AIVector3D.Buffer aiVertices = aiMesh.mVertices();
+    while (aiVertices.remaining() > 0) {
+        AIVector3D aiVertex = aiVertices.get();
+        vertices.add(aiVertex.x());
+        vertices.add(aiVertex.y());
+        vertices.add(aiVertex.z());
+    }
+}
+
+

你可以看到,我们只是通过调用mVertices方法来获取顶点的缓冲区,简单地处理它们来创建一个储存顶点位置的浮点数列表。因为,该方法只返回一个缓冲区,你可以将该数据直接传给创建顶点的OpenGL方法。但我们不这样做,原因有两个。第一个原因是尽量减少对代码库的修改,第二个原因是,通过加载数据到中间层中,你可以执行一些专门的处理任务,甚至调试加载过程。

+

如果你想要一个更有效的示例,即直接将缓冲区传给OpenGL,可以查看这个例子

+

StaticMeshesLoader类让OBJLoader类过时了,因此它已经从源代码中删除。一个更复杂的OBJ文件已经作为示例提供,如果运行它,你将看到如下内容:

+

模型

+

动画

+

现在我们已经使用Assimp加载了静态网格,可以继续讲解动画。如果你回想动画一章,与网格关联的VAO包含顶点位置、纹理坐标、索引和应应用于关节位置以调整最终顶点位置的权重列表。

+

VAO动画

+

每个顶点位置都关联了一个改变最终位置的四个权重的列表,引用了将组合以确定顶点最终位置的骨骼索引。在每一帧,一个变换矩阵列表将被作为Uniform加载到每个关节。根据这些数据,算出最终位置。

+

在动画一章中,我们开发了一个MD5解析器来加载动画网格。在本章中,我们将使用Assimp库。这将允许我们加载MD5之外的更多格式,例如COLLADAFBX等。

+

在开始编码之前,让我们理清一些术语。在本章中,我们将不加区分地提到骨骼和关节。骨骼或者关节都只是影响顶点的元素,并且具有形成层次的父级。MD5格式使用术语关节,而Assimp使用术语骨骼。

+

让我们先看一下由Assimp处理的储存着动画信息的结构。我们将首先从骨骼和权重数据开始。对于每个网格,我们可以访问顶点位置、纹理坐标和索引。网格还储存着骨骼列表,每个骨骼由以下属性定义:

+
    +
  • 一个名字。
  • +
  • 一个偏移矩阵:稍后将用它来计算每个骨骼应该使用的最终变换。
  • +
+

骨骼还指向一个权重列表,每个权重由以下属性定义:

+
    +
  • 一个权重因子,即用于调节与每个顶点相关的骨骼变换影响的数字。
  • +
  • 一个顶点标识符,即与当前骨骼相关联的顶点。
  • +
+

下图展现了所有这些元素之间的关系。

+

网格、骨骼、权重和顶点之间的关系

+

因此,我们首先要做的事是从上述的结构构造顶点位置、骨骼/关节/索引和相关权重的列表。完成后,我们需要为模型中定一个所有动画帧预先计算每个骨骼/关节的变换矩阵。

+

Assimp场景对象是定义节点的层次结构,每个节点都由一个名词和一个子节点列表定义。动画使用这些节点来定义应该应用的变换,这个层次结构实际上定义了骨骼的层次结构。每个骨骼都是一个节点,并且有一个父节点(根节点除外),可能还有一组子节点。有一些特殊的节点不是骨骼,它们用于对变换进行分组,在计算变换时进行处理。另一个问题是,这些节点的层次结构是基于整个模型定义的,对于每个网格,我们没有单独的层次结构。

+

场景还定义了一组动画。一个模型可以有多个动画,可以对模型做行走、跑动等动画。每个动画定义了不同的变换。动画具有以下属性:

+
    +
  • 一个名字。
  • +
  • 持续时间。即动画的持续时间,由于动画是应用于每个不同帧的每个节点的变换列表,因此名称可能看起来很混乱。
  • +
  • 一个动画通道(Animation Channel)的列表。动画通道储存应应用于每个节点的特定时间点的位移、旋转和缩放数据,模型中储存动画通道数据的类是AINodeAnim
  • +
+

下图展示了上述所有元素之间的关系:

+

节点动画

+

对于特定的时刻,或者说对于帧,要应用到骨骼的变换是在动画通道中为该时刻定义的变换,乘以所有父节点到根节点的变换。因此,我们需要对场景中存储的信息进行重新排序,流程如下:

+
    +
  • 构造节点层次结构。
  • +
  • 对每个动画,迭代每个动画通道(对每个动画节点):为所有帧构造变换矩阵。变换矩阵是位移、旋转和缩放矩阵的组合。
  • +
  • 重新排列每一帧的信息:构造要应用于网格中每个骨骼的最终变换。这是通过将骨骼的变换矩阵(相关节点的变换矩阵)乘以所有父节点的变换矩阵直到根节点来实现的。
  • +
+

让我们开始编码吧。首先将创建一个名为AnimMeshesLoader的类,它由StaticMeshesLoader扩展,但它不返回网格数组,而是返回一个AnimGameItem实例。它定义了两个公共方法:

+
public static AnimGameItem loadAnimGameItem(String resourcePath, String texturesDir)
+        throws Exception {
+    return loadAnimGameItem(resourcePath, texturesDir,
+            aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate
+            | aiProcess_FixInfacingNormals | aiProcess_LimitBoneWeights);
+}
+
+public static AnimGameItem loadAnimGameItem(String resourcePath, String texturesDir, int flags)
+        throws Exception {
+    AIScene aiScene = aiImportFile(resourcePath, flags);
+    if (aiScene == null) {
+        throw new Exception("Error loading model");
+    }
+
+    int numMaterials = aiScene.mNumMaterials();
+    PointerBuffer aiMaterials = aiScene.mMaterials();
+    List<Material> materials = new ArrayList<>();
+    for (int i = 0; i < numMaterials; i++) {
+        AIMaterial aiMaterial = AIMaterial.create(aiMaterials.get(i));
+        processMaterial(aiMaterial, materials, texturesDir);
+    }
+
+    List<Bone> boneList = new ArrayList<>();
+    int numMeshes = aiScene.mNumMeshes();
+    PointerBuffer aiMeshes = aiScene.mMeshes();
+    Mesh[] meshes = new Mesh[numMeshes];
+    for (int i = 0; i < numMeshes; i++) {
+        AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
+        Mesh mesh = processMesh(aiMesh, materials, boneList);
+        meshes[i] = mesh;
+    }
+
+    AINode aiRootNode = aiScene.mRootNode();
+    Matrix4f rootTransfromation = AnimMeshesLoader.toMatrix(aiRootNode.mTransformation());
+    Node rootNode = processNodesHierarchy(aiRootNode, null);
+    Map<String, Animation> animations = processAnimations(aiScene, boneList, rootNode, rootTransfromation);
+    AnimGameItem item = new AnimGameItem(meshes, animations);
+
+    return item;
+}
+
+

这些方法与StaticMeshesLoader中定义的方法非常相似,但有如下区别:

+
    +
  • 传递默认加载标记的方法使用了新参数:aiProcess_LimitBoneWeights。这将会将影响顶点的最大权重数限制为4(这也是我们当前在动画章节中支持的最大值)。
  • +
  • 实际加载模型的方法只加载不同的网格,但它首先计算节点层次结构,然后在结尾调用processAnimations以生成AnimGameItem实例。
  • +
+

processMesh方法与StaticMeshesLoader类中的方法非常相似,只是它创建的网格将关节索引和权重作为参数传递:

+
processBones(aiMesh, boneList, boneIds, weights);
+
+Mesh mesh = new Mesh(Utils.listToArray(vertices), Utils.listToArray(textures),
+    Utils.listToArray(normals), Utils.listIntToArray(indices),
+    Utils.listIntToArray(boneIds), Utils.listToArray(weights));
+
+

关节索引和权重按processBones方法计算:

+
private static void processBones(AIMesh aiMesh, List<Bone> boneList, List<Integer> boneIds,
+        List<Float> weights) {
+    Map<Integer, List<VertexWeight>> weightSet = new HashMap<>();
+    int numBones = aiMesh.mNumBones();
+    PointerBuffer aiBones = aiMesh.mBones();
+    for (int i = 0; i < numBones; i++) {
+        AIBone aiBone = AIBone.create(aiBones.get(i));
+        int id = boneList.size();
+        Bone bone = new Bone(id, aiBone.mName().dataString(), toMatrix(aiBone.mOffsetMatrix()));
+        boneList.add(bone);
+        int numWeights = aiBone.mNumWeights();
+        AIVertexWeight.Buffer aiWeights = aiBone.mWeights();
+        for (int j = 0; j < numWeights; j++) {
+            AIVertexWeight aiWeight = aiWeights.get(j);
+            VertexWeight vw = new VertexWeight(bone.getBoneId(), aiWeight.mVertexId(),
+                    aiWeight.mWeight());
+            List<VertexWeight> vertexWeightList = weightSet.get(vw.getVertexId());
+            if (vertexWeightList == null) {
+                vertexWeightList = new ArrayList<>();
+                weightSet.put(vw.getVertexId(), vertexWeightList);
+            }
+            vertexWeightList.add(vw);
+        }
+    }
+
+    int numVertices = aiMesh.mNumVertices();
+    for (int i = 0; i < numVertices; i++) {
+        List<VertexWeight> vertexWeightList = weightSet.get(i);
+        int size = vertexWeightList != null ? vertexWeightList.size() : 0;
+        for (int j = 0; j < Mesh.MAX_WEIGHTS; j++) {
+            if (j < size) {
+                VertexWeight vw = vertexWeightList.get(j);
+                weights.add(vw.getWeight());
+                boneIds.add(vw.getBoneId());
+            } else {
+                weights.add(0.0f);
+                boneIds.add(0);
+            }
+        }
+    }
+}
+
+

此方法遍历特定网格的骨骼定义,获取其权重并生成三个列表:

+
    +
  • boneList:储存节点及其偏移矩阵的列表。稍后将使用它来计算节点变换。已创建一个名为Bone的新类来储存该信息。此列表将储存所有网格的骨骼。
  • +
  • boneIds:只储存包含Mesh的每个顶点的骨骼标识。骨骼在渲染时根据其位置进行标识,此列表仅包含特定网格的骨骼。
  • +
  • weights:储存要应用于关联骨骼的Mesh的每个顶点的权重。
  • +
+

weightsboneIds中储存的数据用于构造Mesh数据。boneList中储存的数据将在稍后计算动画数据时使用。

+

让我们回到loadAnimGameItem方法。一旦我们创建了网格,还得到了应用于根节点的变换,该变换也将用于计算最终的变换。之后,我们需要处理节点的层次结构,这是在processNodesHierarchy方法中完成的。这个方法非常简单,它只是从根节点开始遍历节点层次结构,构造一个节点树。

+
private static Node processNodesHierarchy(AINode aiNode, Node parentNode) {
+    String nodeName = aiNode.mName().dataString();
+    Node node = new Node(nodeName, parentNode);
+
+    int numChildren = aiNode.mNumChildren();
+    PointerBuffer aiChildren = aiNode.mChildren();
+    for (int i = 0; i < numChildren; i++) {
+        AINode aiChildNode = AINode.create(aiChildren.get(i));
+        Node childNode = processNodesHierarchy(aiChildNode, node);
+        node.addChild(childNode);
+    }
+
+    return node;
+}
+
+

我们已经创建了一个新的Node类,该类将储存AINode实例的相关信息,并提供了查找方法来定位节点层次结构,以便按名称查找节点。回到loadAnimGameItem方法,我们只使用该数据计算processAnimations方法中的动画。该方法返回Animation实例的Map。请记住,一个模型可以有多个动画,因此它们按名称储存索引。有了这些数据,我们终于可以构建一个AnimAgameItem实例。

+

processAnimations方法的定义如下所示:

+
private static Map<String, Animation> processAnimations(AIScene aiScene, List<Bone> boneList,
+        Node rootNode, Matrix4f rootTransformation) {
+    Map<String, Animation> animations = new HashMap<>();
+
+    // 处理所有动画
+    int numAnimations = aiScene.mNumAnimations();
+    PointerBuffer aiAnimations = aiScene.mAnimations();
+    for (int i = 0; i < numAnimations; i++) {
+        AIAnimation aiAnimation = AIAnimation.create(aiAnimations.get(i));
+
+        // 为每个节点计算变换矩阵
+        int numChanels = aiAnimation.mNumChannels();
+        PointerBuffer aiChannels = aiAnimation.mChannels();
+        for (int j = 0; j < numChanels; j++) {
+            AINodeAnim aiNodeAnim = AINodeAnim.create(aiChannels.get(j));
+            String nodeName = aiNodeAnim.mNodeName().dataString();
+            Node node = rootNode.findByName(nodeName);
+            buildTransFormationMatrices(aiNodeAnim, node);
+        }
+
+        List<AnimatedFrame> frames = buildAnimationFrames(boneList, rootNode, rootTransformation);
+        Animation animation = new Animation(aiAnimation.mName().dataString(), frames, aiAnimation.mDuration());
+        animations.put(animation.getName(), animation);
+    }
+    return animations;
+}
+
+

将为每个动画处理其动画通道,每个通道定义了不同的变换,这些变化应该随着时间的推移应用于一个节点。为每个节点定义的变换在buildTransFormationMatrices方法中定义,这些矩阵被每个节点储存。一旦节点层次结构中储存完这些信息,我们就可以构建动画帧。

+

让我们先回顾一下buildTransFormationMatrices方法:

+
private static void buildTransFormationMatrices(AINodeAnim aiNodeAnim, Node node) {
+    int numFrames = aiNodeAnim.mNumPositionKeys();
+    AIVectorKey.Buffer positionKeys = aiNodeAnim.mPositionKeys();
+    AIVectorKey.Buffer scalingKeys = aiNodeAnim.mScalingKeys();
+    AIQuatKey.Buffer rotationKeys = aiNodeAnim.mRotationKeys();
+
+    for (int i = 0; i < numFrames; i++) {
+        AIVectorKey aiVecKey = positionKeys.get(i);
+        AIVector3D vec = aiVecKey.mValue();
+
+        Matrix4f transfMat = new Matrix4f().translate(vec.x(), vec.y(), vec.z());
+
+        AIQuatKey quatKey = rotationKeys.get(i);
+        AIQuaternion aiQuat = quatKey.mValue();
+        Quaternionf quat = new Quaternionf(aiQuat.x(), aiQuat.y(), aiQuat.z(), aiQuat.w());
+        transfMat.rotate(quat);
+
+        if (i < aiNodeAnim.mNumScalingKeys()) {
+            aiVecKey = scalingKeys.get(i);
+            vec = aiVecKey.mValue();
+            transfMat.scale(vec.x(), vec.y(), vec.z());
+        }
+
+        node.addTransformation(transfMat);
+    }
+}
+
+

如你所见,AINodeAnim实例定义了一组包含位移、旋转和缩放信息的键,这些键是指特定的时刻。我们假设数据是按时间顺序排列的,并构建一个储存要应用于每个帧的变换的矩阵列表。最后的计算是在buildAnimationFrames方法中完成的:

+
private static List<AnimatedFrame> buildAnimationFrames(List<Bone> boneList, Node rootNode,
+        Matrix4f rootTransformation) {
+
+    int numFrames = rootNode.getAnimationFrames();
+    List<AnimatedFrame> frameList = new ArrayList<>();
+    for (int i = 0; i < numFrames; i++) {
+        AnimatedFrame frame = new AnimatedFrame();
+        frameList.add(frame);
+
+        int numBones = boneList.size();
+        for (int j = 0; j < numBones; j++) {
+            Bone bone = boneList.get(j);
+            Node node = rootNode.findByName(bone.getBoneName());
+            Matrix4f boneMatrix = Node.getParentTransforms(node, i);
+            boneMatrix.mul(bone.getOffsetMatrix());
+            boneMatrix = new Matrix4f(rootTransformation).mul(boneMatrix);
+            frame.setMatrix(j, boneMatrix);
+        }
+    }
+
+    return frameList;
+}
+
+

此方法返回AnimatedFrame实例的列表,每个AnimatedFrame实例将储存要应用于特定帧的每个骨骼的变换列表。这个方法只是迭代储存所有骨骼的列表。对于每个骨骼:

+
    +
  • 获取相关联的节点。
  • +
  • 通过将相关联的Node的变换与其父节点的所有变换相乘,生成变换矩阵,直至根节点。这是在Node.getParentTransforms方法中完成的。
  • +
  • 它将该矩阵与骨骼的偏倚矩阵相乘。
  • +
  • 最后的变换是通过将根节点的变换与上述步骤中计算的矩阵相乘来计算的。
  • +
+

源代码中的其他修改是为了适应某些结构而进行的微小修改。最后,你能够加载类似于下图的动画(你需要按空格改变帧):

+

动画效果

+

这个示例的复杂之处在于Assimp结构的调整,使其适应本书中使用的引擎,并预先计算每个帧的数据。此外,这些概念与动画一章中的概念类似。你可以尝试修改源代码以在帧之间插入,以获得更平滑的动画。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/28-deferred-shading/index.html b/28-deferred-shading/index.html new file mode 100755 index 0000000..b1d69cc --- /dev/null +++ b/28-deferred-shading/index.html @@ -0,0 +1,1050 @@ + + + + + + + + + + + + 延迟着色法 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

延迟着色法(Deferred Shading)

+

到目前为止,我们渲染三维场景的方式称为正向渲染法(Forward Rendering)。我们首先渲染3D对象,并在片元着色器中应用纹理和光照效果。如果我们有一个具有多种光照和复杂效果的复杂片元着色器处理阶段,这种方法就不是很高效。此外,我们最终可能会将这些效果应用到稍后可能由于深度测试而被丢弃的片元上(尽管这并不完全正确,如果我们启用早期片元测试的话)。

+

为了缓解上述问题,我们可以改变方式,使用一种称为延迟着色法的技术来渲染场景。通过延迟着色法,我们先将后期(在片元着色器中)所需的几何信息渲染到缓冲区。当使用储存在这些缓冲区中的信息时,片元着色器所需的复杂运算被推迟(Postpone)、延迟(Defer)到之后的阶段。

+

因此,使用延迟着色法,我们进行两个渲染阶段(Pass)。第一个是几何处理阶段,我们将场景渲染到储存以下数据的缓冲区中:

+
    +
  • 位置(在本例中,在光照视图坐标系中,尽管你可以看到使用世界坐标的其他示例)。
  • +
  • 每个位置的漫反射颜色。
  • +
  • 每个位置的镜面反射分量。
  • +
  • 每个位置的法线(也在光照视图坐标系中)。
  • +
  • 有向光的阴影图(你可能会发现这一步在其他实现中是单独完成的)。
  • +
+

所有这些数据都存储在一个称为G缓冲区(G-Buffer)的缓冲区中。

+

第二个阶段称为光照处理阶段,该阶段需要一个充满整个屏幕的矩形,并使用G缓冲区中储存的信息为每个片元生成颜色信息。当我们进行光照处理阶段时,深度测试将删除所有看不到的场景数据。因此,要执行的操作数量仅限于将在屏幕上显示的。

+

流程图

+

你可能会问进行额外的渲染阶段能否提高性能,答案是视情况而定。延迟着色法通常在有许多不同的光照处理阶段时使用,在此情况下,额外的渲染阶段将通过减少将在片元着色器中执行的操作来补偿。

+

让我们开始编码吧,首先要做的是为G缓冲区创建一个新类,名为GBuffer,其定义如下所示:

+
package org.lwjglb.engine.graph;
+
+import org.lwjgl.system.MemoryStack;
+import org.lwjglb.engine.Window;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import static org.lwjgl.opengl.GL11.*;
+import static org.lwjgl.opengl.GL20.*;
+import static org.lwjgl.opengl.GL30.*;
+
+public class GBuffer {
+
+    private static final int TOTAL_TEXTURES = 6;
+
+    private int gBufferId;
+
+    private int[] textureIds;
+
+    private int width;
+
+    private int height;
+
+

类定义了一个常量,该常量为要使用的最大缓冲区数建模。与G缓冲区本身关联的ID,以及单个缓冲区的数组,屏幕的大小也会被储存。

+

让我们看一下构造函数:

+
public GBuffer(Window window) throws Exception {
+    // 创建G缓冲区
+    gBufferId = glGenFramebuffers();
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBufferId);
+
+    textureIds = new int[TOTAL_TEXTURES];
+    glGenTextures(textureIds);
+
+    this.width = window.getWidth();
+    this.height = window.getHeight();
+
+    // 为位置、漫反射颜色、镜面反射颜色、法线、阴影因子和深度创建纹理
+    // 所有坐标都在世界坐标系中
+    for(int i=0; i<TOTAL_TEXTURES; i++) {
+        glBindTexture(GL_TEXTURE_2D, textureIds[i]);
+        int attachmentType;
+        switch(i) {
+            case TOTAL_TEXTURES - 1:
+                // 深度分量
+                glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
+                        (ByteBuffer) null);
+                attachmentType = GL_DEPTH_ATTACHMENT;
+                break;
+            default:
+                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, width, height, 0, GL_RGB, GL_FLOAT, (ByteBuffer) null);
+                attachmentType = GL_COLOR_ATTACHMENT0 + i;
+                break;
+        }
+        // 用于采样
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+        // 将纹理绑定到G缓冲区
+        glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, GL_TEXTURE_2D, textureIds[i], 0);
+    }
+
+    try (MemoryStack stack = MemoryStack.stackPush()) {
+        IntBuffer intBuff = stack.mallocInt(TOTAL_TEXTURES);
+        int values[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT5};
+        for(int i = 0; i < values.length; i++) {
+            intBuff.put(values[i]);
+        }
+        intBuff.flip();
+        glDrawBuffers(intBuff);
+    }
+
+    // 解绑
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
+
+

首先我们要做的事是创建一个帧缓冲区。记住,帧缓冲区只是一个OpenGL对象,可用于渲染操作,而不是渲染到屏幕。然后我们生成一组纹理(6个纹理),这些纹理将与帧缓冲区关联。

+

之后,我们使用for循环来初始化纹理,有如下类型:

+
    +
  • “常规纹理”,将储存位置、法线和漫反射分量等。
  • +
  • 用于储存深度缓冲区的纹理,这将是最后的纹理。
  • +
+

纹理初始化后,我们为它们启用采样并将它们绑定到帧缓冲区。每个纹理使用以GL_COLOR_ATTACHMENT0为开头的ID进行绑定。每个纹理都以该ID递增一位,因此坐标使用GL_COLOR_ATTACHMENT0绑定,漫反射分量使用GL_COLOR_ATTACHMENT1(即GL_COLOR_ATTACHMENT0 + 1)附加,以此类推。

+

在创建完所有纹理之后,我们需要让片元着色器使用它们进行渲染。这是通过调用glDrawBuffers完成的。我们只传递带有所用颜色绑定的ID的数组(GL_COLOR_ ATTACHMENT0GL_COLOR_ATTACHMENT5)。

+

类的其他部分只是getter方法和cleanup方法。

+
public int getWidth() {
+    return width;
+}
+
+public int getHeight() {
+    return height;
+}
+
+public int getGBufferId() {
+    return gBufferId;
+}
+
+public int[] getTextureIds() {
+    return textureIds;
+}
+
+public int getPositionTexture() {
+    return textureIds[0];
+}
+
+public int getDepthTexture() {
+    return textureIds[TOTAL_TEXTURES-1];
+}
+
+public void cleanUp() {
+    glDeleteFramebuffers(gBufferId);
+
+    if (textureIds != null) {
+        for (int i=0; i<TOTAL_TEXTURES; i++) {
+            glDeleteTextures(textureIds[i]);
+        }
+    }
+}
+
+

我们将创建一个名为SceneBuffer的新类,它只是另一个帧缓冲区,将在执行光照处理阶段时使用它。我们不直接渲染到屏幕,而是渲染到这个帧缓冲区。这样做,我们可以应用其他效果(如雾、天空盒等)。类的定义如下:

+
package org.lwjglb.engine.graph;
+
+import org.lwjglb.engine.Window;
+
+import java.nio.ByteBuffer;
+
+import static org.lwjgl.opengl.GL11.*;
+import static org.lwjgl.opengl.GL30.*;
+
+public class SceneBuffer {
+
+    private int bufferId;
+
+    private int textureId;
+
+    public SceneBuffer(Window window) throws Exception {
+        // 创建缓冲区
+        bufferId = glGenFramebuffers();
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bufferId);
+
+        // 创建纹理
+        int[] textureIds = new int[1];
+        glGenTextures(textureIds);
+        textureId = textureIds[0];
+        glBindTexture(GL_TEXTURE_2D, textureId);
+        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, window.getWidth(), window.getHeight(), 0, GL_RGB, GL_FLOAT, (ByteBuffer) null);
+
+        // 用于采样
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+        // 附加纹理到G缓冲区
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);
+
+        // 解绑
+        glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    }
+
+    public int getBufferId() {
+        return bufferId;
+    }
+
+    public int getTextureId() {
+        return textureId;
+    }
+
+    public void cleanup() {
+        glDeleteFramebuffers(bufferId);
+
+        glDeleteTextures(textureId);
+    }
+}
+
+

如你所见,它类似于GBuffer类,但这里只使用一个纹理来储存生成的颜色。既然已经创建了这些新类,我们就可以开始使用它们了。在Renderer类中,我们将不再使用用于渲染场景的正向渲染着色器(名为scene_vertex.vsscene_fragment.fs)。

+

Renderer类的init方法中,你会看到创建了一个GBuffer实现,我们分别为几何处理阶段(通过调用setupGeometryShader方法)和光照处理阶段(通过调用setupDirLightShadersetupPointLightShader方法)初始化了另一组着色器。你换可以看到我们创建了一个名为sceneBufferSceneBuffer类的实例,如上所述,它将在渲染光照时使用。名为bufferPassModelMatrix的实用矩阵也被实例化(在执行几何处理阶段时将使用它)。你可以看到我们在init方法的末尾创建了一个新的Mesh,它将用于光照处理阶段。关于这方面的详情将在之后介绍。

+
public void init(Window window) throws Exception {
+    shadowRenderer.init(window);
+    gBuffer = new GBuffer(window);
+    sceneBuffer = new SceneBuffer(window);
+    setupSkyBoxShader();
+    setupParticlesShader();
+    setupGeometryShader();
+    setupDirLightShader();
+    setupPointLightShader();
+    setupFogShader();
+
+    bufferPassModelMatrix =  new Matrix4f();
+    bufferPassMesh = StaticMeshesLoader.load("src/main/resources/models/buffer_pass_mess.obj", "src/main/resources/models")[0];
+}
+
+

几何处理阶段和光照处理阶段使用的着色器定义与通常一致(你可以直接查看源代码),让我们关注它们的内容。我们将从几何处理阶段开始,接下来是顶点着色器代码(gbuffer_vertex.vs):

+
#version 330
+
+const int MAX_WEIGHTS = 4;
+const int MAX_JOINTS = 150;
+const int NUM_CASCADES = 3;
+
+layout (location=0) in vec3 position;
+layout (location=1) in vec2 texCoord;
+layout (location=2) in vec3 vertexNormal;
+layout (location=3) in vec4 jointWeights;
+layout (location=4) in ivec4 jointIndices;
+layout (location=5) in mat4 modelInstancedMatrix;
+layout (location=9) in vec2 texOffset;
+layout (location=10) in float selectedInstanced;
+
+uniform int isInstanced;
+uniform mat4 viewMatrix;
+uniform mat4 projectionMatrix;
+uniform mat4 modelNonInstancedMatrix;
+uniform mat4 jointsMatrix[MAX_JOINTS];
+uniform mat4 lightViewMatrix[NUM_CASCADES];
+uniform mat4 orthoProjectionMatrix[NUM_CASCADES];
+uniform int numCols;
+uniform int numRows;
+uniform float selectedNonInstanced;
+
+out vec2  vs_textcoord;
+out vec3  vs_normal;
+out vec4  vs_mvVertexPos;
+out vec4  vs_mlightviewVertexPos[NUM_CASCADES];
+out mat4  vs_modelMatrix;
+out float vs_selected;
+
+void main()
+{
+    vec4 initPos = vec4(0, 0, 0, 0);
+    vec4 initNormal = vec4(0, 0, 0, 0);
+    mat4 modelMatrix;
+    if ( isInstanced > 0 )
+    {
+        vs_selected = selectedInstanced;
+        modelMatrix = modelInstancedMatrix;
+
+        initPos = vec4(position, 1.0);
+        initNormal = vec4(vertexNormal, 0.0);
+    }
+    else
+    {
+        vs_selected = selectedNonInstanced;
+        modelMatrix = modelNonInstancedMatrix;
+
+        int count = 0;
+        for(int i = 0; i < MAX_WEIGHTS; i++)
+        {
+            float weight = jointWeights[i];
+            if(weight > 0) {
+                count++;
+                int jointIndex = jointIndices[i];
+                vec4 tmpPos = jointsMatrix[jointIndex] * vec4(position, 1.0);
+                initPos += weight * tmpPos;
+
+                vec4 tmpNormal = jointsMatrix[jointIndex] * vec4(vertexNormal, 0.0);
+                initNormal += weight * tmpNormal;
+            }
+        }
+        if (count == 0)
+        {
+            initPos = vec4(position, 1.0);
+            initNormal = vec4(vertexNormal, 0.0);
+        }
+    }
+    mat4 modelViewMatrix = viewMatrix * modelMatrix;
+    vs_mvVertexPos = modelViewMatrix * initPos;
+    gl_Position = projectionMatrix * vs_mvVertexPos;
+
+    // Support for texture atlas, update texture coordinates
+    float x = (texCoord.x / numCols + texOffset.x);
+    float y = (texCoord.y / numRows + texOffset.y);
+
+    vs_textcoord = vec2(x, y);
+    vs_normal = normalize(modelViewMatrix * initNormal).xyz;
+
+    for (int i = 0 ; i < NUM_CASCADES ; i++) {
+        vs_mlightviewVertexPos[i] = orthoProjectionMatrix[i] * lightViewMatrix[i] * modelMatrix * initPos;
+    }
+
+    vs_modelMatrix = modelMatrix;
+}
+
+

此着色器与上一章中用于渲染场景的顶点着色器非常相似,输出变量的名称有一些变化,但本质上是相同的着色器。事实上,它应该是几近一致的,我们渲染顶点的方式不应该改变,主要的更改是在片元着色器中,它是这样定义的(gbuffer_fragment.fs):

+
#version 330
+
+const int NUM_CASCADES = 3;
+
+in vec2  vs_textcoord;
+in vec3  vs_normal;
+in vec4  vs_mvVertexPos;
+in vec4  vs_mlightviewVertexPos[NUM_CASCADES];
+in mat4  vs_modelMatrix;
+in float vs_selected;
+
+layout (location = 0) out vec3 fs_worldpos;
+layout (location = 1) out vec3 fs_diffuse;
+layout (location = 2) out vec3 fs_specular;
+layout (location = 3) out vec3 fs_normal;
+layout (location = 4) out vec2 fs_shadow;
+
+uniform mat4 viewMatrix;
+
+struct Material
+{
+    vec4 diffuse;
+    vec4 specular;
+    int hasTexture;
+    int hasNormalMap;
+    float reflectance;
+};
+
+uniform sampler2D texture_sampler;
+uniform sampler2D normalMap;
+uniform Material  material;
+
+uniform sampler2D shadowMap_0;
+uniform sampler2D shadowMap_1;
+uniform sampler2D shadowMap_2;
+uniform float cascadeFarPlanes[NUM_CASCADES];
+uniform mat4 orthoProjectionMatrix[NUM_CASCADES];
+uniform int renderShadow;
+
+vec4 diffuseC;
+vec4 speculrC;
+
+void getColour(Material material, vec2 textCoord)
+{
+    if (material.hasTexture == 1)
+    {
+        diffuseC = texture(texture_sampler, textCoord);
+        speculrC = diffuseC;
+    }
+    else
+    {
+        diffuseC = material.diffuse;
+        speculrC = material.specular;
+    }
+}
+
+vec3 calcNormal(Material material, vec3 normal, vec2 text_coord, mat4 modelMatrix)
+{
+    vec3 newNormal = normal;
+    if ( material.hasNormalMap == 1 )
+    {
+        newNormal = texture(normalMap, text_coord).rgb;
+        newNormal = normalize(newNormal * 2 - 1);
+        newNormal = normalize(viewMatrix * modelMatrix * vec4(newNormal, 0.0)).xyz;
+    }
+    return newNormal;
+}
+
+float calcShadow(vec4 position, int idx)
+{
+    if ( renderShadow == 0 )
+    {
+        return 1.0;
+    }
+
+    vec3 projCoords = position.xyz;
+    // Transform from screen coordinates to texture coordinates
+    projCoords = projCoords * 0.5 + 0.5;
+    float bias = 0.005;
+
+    float shadowFactor = 0.0;
+    vec2 inc;
+    if (idx == 0)
+    {
+        inc = 1.0 / textureSize(shadowMap_0, 0);
+    }
+    else if (idx == 1)
+    {
+        inc = 1.0 / textureSize(shadowMap_1, 0);
+    }
+    else
+    {
+        inc = 1.0 / textureSize(shadowMap_2, 0);
+    }
+    for(int row = -1; row <= 1; ++row)
+    {
+        for(int col = -1; col <= 1; ++col)
+        {
+            float textDepth;
+            if (idx == 0)
+            {
+                textDepth = texture(shadowMap_0, projCoords.xy + vec2(row, col) * inc).r; 
+            }
+            else if (idx == 1)
+            {
+                textDepth = texture(shadowMap_1, projCoords.xy + vec2(row, col) * inc).r; 
+            }
+            else
+            {
+                textDepth = texture(shadowMap_2, projCoords.xy + vec2(row, col) * inc).r; 
+            }
+            shadowFactor += projCoords.z - bias > textDepth ? 1.0 : 0.0;        
+        }    
+    }
+    shadowFactor /= 9.0;
+
+    if(projCoords.z > 1.0)
+    {
+        shadowFactor = 1.0;
+    }
+
+    return 1 - shadowFactor;
+} 
+
+void main()
+{
+    getColour(material, vs_textcoord);
+
+    fs_worldpos   = vs_mvVertexPos.xyz;
+    fs_diffuse    = diffuseC.xyz;
+    fs_specular   = speculrC.xyz;
+    fs_normal     = normalize(calcNormal(material, vs_normal, vs_textcoord, vs_modelMatrix));
+
+    int idx;
+    for (int i=0; i<NUM_CASCADES; i++)
+    {
+        if ( abs(vs_mvVertexPos.z) < cascadeFarPlanes[i] )
+        {
+            idx = i;
+            break;
+        }
+    }
+    fs_shadow  = vec2(calcShadow(vs_mlightviewVertexPos[idx], idx), material.reflectance);
+
+    if ( vs_selected > 0 ) {
+        fs_diffuse = vec3(fs_diffuse.x, fs_diffuse.y, 1);
+    }
+}
+
+

最关键的行是:

+
layout (location = 0) out vec3 fs_worldpos;
+layout (location = 1) out vec3 fs_diffuse;
+layout (location = 2) out vec3 fs_specular;
+layout (location = 3) out vec3 fs_normal;
+layout (location = 4) out vec2 fs_shadow;
+
+

这就是我们要引用该片元着色器将写入的纹理的地方。如你所见,我们只是转储位置(在光照视图坐标系中)、漫反射颜色(可以是材质相关的纹理的颜色分量)、镜面反射分量、法线和阴影图的深度值。

+

旁注:我们已经简化了Material类的定义,删除了环境颜色分量。

+

回到Renderer类,render方法的定义如下:

+
public void render(Window window, Camera camera, Scene scene, boolean sceneChanged) {
+    clear();
+
+    if (window.getOptions().frustumCulling) {
+        frustumFilter.updateFrustum(window.getProjectionMatrix(), camera.getViewMatrix());
+        frustumFilter.filter(scene.getGameMeshes());
+        frustumFilter.filter(scene.getGameInstancedMeshes());
+    }
+
+    // 在设置视口之前渲染深度图
+    if (scene.isRenderShadows() && sceneChanged) {
+        shadowRenderer.render(window, scene, camera, transformation, this);
+    }
+
+    glViewport(0, 0, window.getWidth(), window.getHeight());
+
+    // 每个渲染周期更新投影矩阵一次
+    window.updateProjectionMatrix();
+
+    renderGeometry(window, camera, scene);
+
+    initLightRendering();
+    renderPointLights(window, camera, scene);
+    renderDirectionalLight(window, camera, scene);
+    endLightRendering();
+
+    renderFog(window, camera, scene);
+    renderSkyBox(window, camera, scene);
+    renderParticles(window, camera, scene);
+}
+
+

几何处理阶段是在renderGeometry方法中完成的(你可以看到我们不再有renderScene)。光照处理阶段分几个步骤完成,首先设置要使用的缓冲区和其他参数(在initLightRendering中),然后渲染点光源(在renderPointLights中)和平行光(在renderDirectionalLight中),最后恢复状态(在endLightRendering中)。

+

让我们从几何处理阶段开始。renderGeometry方法几乎等同于在前几章中使用的renderScene方法:

+
private void renderGeometry(Window window, Camera camera, Scene scene) {
+    // 渲染G缓冲区以便写入
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId());
+
+    clear();
+
+    glDisable(GL_BLEND);
+
+    gBufferShaderProgram.bind();
+
+    Matrix4f viewMatrix = camera.getViewMatrix();
+    Matrix4f projectionMatrix = window.getProjectionMatrix();
+    gBufferShaderProgram.setUniform("viewMatrix", viewMatrix);
+    gBufferShaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+    gBufferShaderProgram.setUniform("texture_sampler", 0);
+    gBufferShaderProgram.setUniform("normalMap", 1);
+
+    List<ShadowCascade> shadowCascades = shadowRenderer.getShadowCascades();
+    for (int i = 0; i < ShadowRenderer.NUM_CASCADES; i++) {
+        ShadowCascade shadowCascade = shadowCascades.get(i);
+        gBufferShaderProgram.setUniform("orthoProjectionMatrix", shadowCascade.getOrthoProjMatrix(), i);
+        gBufferShaderProgram.setUniform("cascadeFarPlanes", ShadowRenderer.CASCADE_SPLITS[i], i);
+        gBufferShaderProgram.setUniform("lightViewMatrix", shadowCascade.getLightViewMatrix(), i);
+    }
+    shadowRenderer.bindTextures(GL_TEXTURE2);
+    int start = 2;
+    for (int i = 0; i < ShadowRenderer.NUM_CASCADES; i++) {
+        gBufferShaderProgram.setUniform("shadowMap_" + i, start + i);
+    }
+    gBufferShaderProgram.setUniform("renderShadow", scene.isRenderShadows() ? 1 : 0);
+
+    renderNonInstancedMeshes(scene);
+
+    renderInstancedMeshes(scene, viewMatrix);
+
+    gBufferShaderProgram.unbind();
+
+    glEnable(GL_BLEND);
+}
+
+

仅有的区别是:

+
    +
  • 我们绑定到G缓冲区而不是屏幕。
  • +
  • 禁用混合,因为我们只想使用最接近摄像机的值(最小深度值),所以不需要混合。
  • +
+

如果使用OpenGL调试器(如RenderDoc)调试示例,则可以看到几何处理阶段期间生成的纹理。位置纹理将如下所示:

+

位置纹理

+

储存漫反射分量值的纹理如下所示:

+

漫反射纹理

+

储存法线值的纹理如下所示:

+

法线纹理

+

现在轮到光照处理阶段了。我们首先需要在渲染之前设置一些东西,这是在initLightRendering方法中完成的:

+
private void initLightRendering() {
+    // 绑定场景缓冲区
+    glBindFramebuffer(GL_FRAMEBUFFER, sceneBuffer.getBufferId());
+
+    // 清理G缓冲区
+    clear();
+
+    // 禁用深度测试以允许绘制具有相同深度的多个图层
+    glDisable(GL_DEPTH_TEST);
+
+    glEnable(GL_BLEND);
+    glBlendEquation(GL_FUNC_ADD);
+    glBlendFunc(GL_ONE, GL_ONE);
+
+    // 绑定G缓冲区以便读取
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer.getGBufferId());
+}
+
+

因为我们不会渲染到屏幕上,所以首先需要绑定到纹理上,该纹理将保存光照处理阶段的结果。然后清除缓冲区并禁用深度测试。因为已经在几何处理阶段进行了深度测试,这就不再需要了。另一重要步骤是启用混合。最后一个操作是启用G缓冲区进行读取,它将在光照处理阶段期间使用。

+

在分析不同光照的渲染方法之前,让我们稍微考虑一下如何渲染光照。我们需要使用G缓冲区的内容,但为了使用它们,需要先渲染一些东西。但是,我们已经绘制了场景,现在要渲染什么?答案很简单,我们只需要渲染一个充满了整个屏幕的矩形。对于该矩形的每个片元,我们将使用G缓冲区中储存的数据并生成正确的输出颜色。你还记得我们在Renderer类的init方法中加载的Mesh吗?它被命名为bufferPassMesh,它只储存一个充满整个屏幕的矩形。

+

那么,光照处理阶段的顶点着色器看起来是怎样的?

+
#version 330
+
+layout (location=0) in vec3 position;
+uniform mat4 projectionMatrix;
+uniform mat4 modelMatrix;
+
+void main()
+{
+    gl_Position = projectionMatrix * modelMatrix * vec4(position, 1.0);
+}
+
+

上述代码是渲染点光源和平行光时使用的顶点着色器(light_vertex.vs)。它只是使用模型矩阵和投影矩阵来转储顶点。不需要使用观察矩阵,因为这里不需要摄像机。

+

点光源的片元着色器(point_light_fragment.fs)的定义如下:

+
#version 330
+
+out vec4 fragColor;
+
+struct Attenuation
+{
+    float constant;
+    float linear;
+    float exponent;
+};
+
+struct PointLight
+{
+    vec3 colour;
+    // Light position is assumed to be in view coordinates
+    vec3 position;
+    float intensity;
+    Attenuation att;
+};
+
+uniform sampler2D positionsText;
+uniform sampler2D diffuseText;
+uniform sampler2D specularText;
+uniform sampler2D normalsText;
+uniform sampler2D shadowText;
+uniform sampler2D depthText;
+
+uniform vec2 screenSize;
+
+uniform float specularPower;
+uniform PointLight pointLight;
+
+vec2 getTextCoord()
+{
+    return gl_FragCoord.xy / screenSize;
+}
+
+vec4 calcLightColour(vec4 diffuseC, vec4 speculrC, float reflectance, vec3 light_colour, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal)
+{
+    vec4 diffuseColour = vec4(0, 0, 0, 1);
+    vec4 specColour = vec4(0, 0, 0, 1);
+
+    // Diffuse Light
+    float diffuseFactor = max(dot(normal, to_light_dir), 0.0);
+    diffuseColour = diffuseC * vec4(light_colour, 1.0) * light_intensity * diffuseFactor;
+
+    // Specular Light
+    vec3 camera_direction = normalize(-position);
+    vec3 from_light_dir = -to_light_dir;
+    vec3 reflected_light = normalize(reflect(from_light_dir , normal));
+    float specularFactor = max( dot(camera_direction, reflected_light), 0.0);
+    specularFactor = pow(specularFactor, specularPower);
+    specColour = speculrC * light_intensity  * specularFactor * reflectance * vec4(light_colour, 1.0);
+
+    return (diffuseColour + specColour);
+}
+
+vec4 calcPointLight(vec4 diffuseC, vec4 speculrC, float reflectance, PointLight light, vec3 position, vec3 normal)
+{
+    vec3 light_direction = light.position - position;
+    vec3 to_light_dir  = normalize(light_direction);
+    vec4 light_colour = calcLightColour(diffuseC, speculrC, reflectance, light.colour, light.intensity, position, to_light_dir, normal);
+
+    // Apply Attenuation
+    float distance = length(light_direction);
+    float attenuationInv = light.att.constant + light.att.linear * distance +
+        light.att.exponent * distance * distance;
+    return light_colour / attenuationInv;
+}
+
+void main()
+{
+    vec2 textCoord = getTextCoord();
+    float depth = texture(depthText, textCoord).r;
+    vec3 worldPos = texture(positionsText, textCoord).xyz;
+    vec4 diffuseC = texture(diffuseText, textCoord);
+    vec4 speculrC = texture(specularText, textCoord);
+    vec3 normal  = texture(normalsText, textCoord).xyz;
+    float shadowFactor = texture(shadowText, textCoord).r;
+    float reflectance = texture(shadowText, textCoord).g;
+
+    fragColor = calcPointLight(diffuseC, speculrC, reflectance, pointLight, worldPos.xyz, normal.xyz) * shadowFactor;
+}
+
+

如你所见,它包含你应该熟悉的函数,它们在前几章的场景片元着色器中使用过。这里需要注意的重要事项如下:

+
uniform sampler2D positionsText;
+uniform sampler2D diffuseText;
+uniform sampler2D specularText;
+uniform sampler2D normalsText;
+uniform sampler2D shadowText;
+uniform sampler2D depthText;
+
+

这些Uniform建模构成了G缓冲区的不同纹理。我们将使用它们来访问数据。你现在可能会问,当我们渲染一个片元时,如何知道要从这些纹理中获取哪个像素?答案是使用gl_FragCoord输入变量,此变量储存当前片元与窗口的相对坐标。因为要从坐标系转换为纹理,我们使用以下函数:

+
vec2 getTextCoord()
+{
+    return gl_FragCoord.xy / screenSize;
+}
+
+

平行光的片元着色器也很相似,可以查看源代码。现在已经有了着色器,我们回到Renderer类。对于点光源,我们将做尽可能多的光照处理,我们只是绑定用于此类型光源的着色器,并为每个着色器绘制矩形。

+
private void renderPointLights(Window window, Camera camera, Scene scene) {
+    pointLightShaderProgram.bind();
+
+    Matrix4f viewMatrix = camera.getViewMatrix();
+    Matrix4f projectionMatrix = window.getProjectionMatrix();
+    pointLightShaderProgram.setUniform("modelMatrix", bufferPassModelMatrix);
+    pointLightShaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+    // 镜面反射系数
+    pointLightShaderProgram.setUniform("specularPower", specularPower);
+
+    // 绑定G缓冲区纹理
+    int[] textureIds = this.gBuffer.getTextureIds();
+    int numTextures = textureIds != null ? textureIds.length : 0;
+    for (int i=0; i<numTextures; i++) {
+        glActiveTexture(GL_TEXTURE0 + i);
+        glBindTexture(GL_TEXTURE_2D, textureIds[i]);
+    }
+
+    pointLightShaderProgram.setUniform("positionsText", 0);
+    pointLightShaderProgram.setUniform("diffuseText", 1);
+    pointLightShaderProgram.setUniform("specularText", 2);
+    pointLightShaderProgram.setUniform("normalsText", 3);
+    pointLightShaderProgram.setUniform("shadowText", 4);
+
+    pointLightShaderProgram.setUniform("screenSize", (float) gBuffer.getWidth(), (float)gBuffer.getHeight());
+
+    SceneLight sceneLight = scene.getSceneLight();
+    PointLight[] pointLights = sceneLight.getPointLightList();
+    int numPointLights = pointLights != null ? pointLights.length : 0;
+    for(int i=0; i<numPointLights; i++) {
+        // 获取点光源对象的副本并将其位置转换到观察坐标系
+        PointLight currPointLight = new PointLight(pointLights[i]);
+        Vector3f lightPos = currPointLight.getPosition();
+        tmpVec.set(lightPos, 1);
+        tmpVec.mul(viewMatrix);
+        lightPos.x = tmpVec.x;
+        lightPos.y = tmpVec.y;
+        lightPos.z = tmpVec.z;
+        pointLightShaderProgram.setUniform("pointLight", currPointLight);
+
+        bufferPassMesh.render();
+    }
+
+    pointLightShaderProgram.unbind();
+}
+
+

对于平行光来说,方法非常相似。在此情况下,我们只进行一次处理:

+
private void renderDirectionalLight(Window window, Camera camera, Scene scene) {
+    dirLightShaderProgram.bind();
+
+    Matrix4f viewMatrix = camera.getViewMatrix();
+    Matrix4f projectionMatrix = window.getProjectionMatrix();
+    dirLightShaderProgram.setUniform("modelMatrix", bufferPassModelMatrix);
+    dirLightShaderProgram.setUniform("projectionMatrix", projectionMatrix);
+
+    // 镜面反射系数
+    dirLightShaderProgram.setUniform("specularPower", specularPower);
+
+    // 绑定G缓冲区纹理
+    int[] textureIds = this.gBuffer.getTextureIds();
+    int numTextures = textureIds != null ? textureIds.length : 0;
+    for (int i=0; i<numTextures; i++) {
+        glActiveTexture(GL_TEXTURE0 + i);
+        glBindTexture(GL_TEXTURE_2D, textureIds[i]);
+    }
+
+    dirLightShaderProgram.setUniform("positionsText", 0);
+    dirLightShaderProgram.setUniform("diffuseText", 1);
+    dirLightShaderProgram.setUniform("specularText", 2);
+    dirLightShaderProgram.setUniform("normalsText", 3);
+    dirLightShaderProgram.setUniform("shadowText", 4);
+
+    dirLightShaderProgram.setUniform("screenSize", (float) gBuffer.getWidth(), (float)gBuffer.getHeight());
+
+    // 环境光照
+    SceneLight sceneLight = scene.getSceneLight();
+    dirLightShaderProgram.setUniform("ambientLight", sceneLight.getAmbientLight());
+
+    // 平行光
+    // 获取平行光对象的副本并将其位置转换到观察坐标系
+    DirectionalLight currDirLight = new DirectionalLight(sceneLight.getDirectionalLight());
+    tmpVec.set(currDirLight.getDirection(), 0);
+    tmpVec.mul(viewMatrix);
+    currDirLight.setDirection(new Vector3f(tmpVec.x, tmpVec.y, tmpVec.z));
+    dirLightShaderProgram.setUniform("directionalLight", currDirLight);
+
+    bufferPassMesh.render();
+
+    dirLightShaderProgram.unbind();
+}
+
+

endLightRendering只简单地恢复状态。

+
private void endLightRendering() {
+    // 绑定屏幕以便写入
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    glEnable(GL_DEPTH_TEST);
+    glDisable(GL_BLEND);
+}
+
+

如果运行示例,你将看到如下内容:

+

结果

+

本章比预期时间长,但有几个要点需要澄清:

+
    +
  • 为了简化本章,已经删除了聚光源的相关内容。
  • +
  • 延迟着色法中使用点光源的一种常见技术是计算受该光源影响的场景区域。在此情况下,你可以使用较小的四边形,圆形等,而不是渲染充满屏幕的矩形。请牢记,最好是好的敌人,进行复杂的微积分以确定所需的最小形状可能比使用其他粗糙方法慢。
  • +
  • 如果没有许多光源,此方法比正向着色法慢。
  • +
+

最后要注意的一点是,如果你想了解这些技术在现实中的应用,可以查看这个极好的说明,了解GTA V的帧是如何渲染的。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/404.html b/404.html new file mode 100755 index 0000000..96431a7 --- /dev/null +++ b/404.html @@ -0,0 +1,246 @@ + + + + + + + + + + + + Lwjglbook中文翻译 + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+
    +
  • Docs »
  • + + +
  • + +
  • +
+ +
+
+ +
+
+ + +

404

+ +

Page not found

+ + +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + + +
+ + + + + + + + + diff --git a/_static/01/hello_world.png b/_static/01/hello_world.png new file mode 100755 index 0000000..7f33aa4 Binary files /dev/null and b/_static/01/hello_world.png differ diff --git a/_static/01/intellij.png b/_static/01/intellij.png new file mode 100755 index 0000000..8419fe9 Binary files /dev/null and b/_static/01/intellij.png differ diff --git a/_static/01/maven_project.png b/_static/01/maven_project.png new file mode 100755 index 0000000..2e05491 Binary files /dev/null and b/_static/01/maven_project.png differ diff --git a/_static/03/3d_cartesian_coordinate_system.png b/_static/03/3d_cartesian_coordinate_system.png new file mode 100755 index 0000000..a407039 Binary files /dev/null and b/_static/03/3d_cartesian_coordinate_system.png differ diff --git a/_static/03/alt_3d_cartesian_coordinate_system.png b/_static/03/alt_3d_cartesian_coordinate_system.png new file mode 100755 index 0000000..3436ef5 Binary files /dev/null and b/_static/03/alt_3d_cartesian_coordinate_system.png differ diff --git a/_static/03/alt_cartesian_coordinate_system.png b/_static/03/alt_cartesian_coordinate_system.png new file mode 100755 index 0000000..ffcb065 Binary files /dev/null and b/_static/03/alt_cartesian_coordinate_system.png differ diff --git a/_static/03/cartesian_coordinate_system.png b/_static/03/cartesian_coordinate_system.png new file mode 100755 index 0000000..62ce9e9 Binary files /dev/null and b/_static/03/cartesian_coordinate_system.png differ diff --git a/_static/03/opengl_coordinates.png b/_static/03/opengl_coordinates.png new file mode 100755 index 0000000..cb3e747 Binary files /dev/null and b/_static/03/opengl_coordinates.png differ diff --git a/_static/03/righthanded_lefthanded.png b/_static/03/righthanded_lefthanded.png new file mode 100755 index 0000000..2c9a389 Binary files /dev/null and b/_static/03/righthanded_lefthanded.png differ diff --git a/_static/04/rendering_pipeline.png b/_static/04/rendering_pipeline.png new file mode 100755 index 0000000..c6da42a Binary files /dev/null and b/_static/04/rendering_pipeline.png differ diff --git a/_static/04/rendering_pipeline_2.png b/_static/04/rendering_pipeline_2.png new file mode 100755 index 0000000..27ebbad Binary files /dev/null and b/_static/04/rendering_pipeline_2.png differ diff --git a/_static/04/triangle_coordinates.png b/_static/04/triangle_coordinates.png new file mode 100755 index 0000000..8876d63 Binary files /dev/null and b/_static/04/triangle_coordinates.png differ diff --git a/_static/04/triangle_window.png b/_static/04/triangle_window.png new file mode 100755 index 0000000..db8e6e0 Binary files /dev/null and b/_static/04/triangle_window.png differ diff --git a/_static/05/coloured_quad.png b/_static/05/coloured_quad.png new file mode 100755 index 0000000..cebeb3e Binary files /dev/null and b/_static/05/coloured_quad.png differ diff --git a/_static/05/coordinates.png b/_static/05/coordinates.png new file mode 100755 index 0000000..3c4c5cc Binary files /dev/null and b/_static/05/coordinates.png differ diff --git a/_static/05/dolphin.png b/_static/05/dolphin.png new file mode 100755 index 0000000..f096591 Binary files /dev/null and b/_static/05/dolphin.png differ diff --git a/_static/05/quad_coordinates.png b/_static/05/quad_coordinates.png new file mode 100755 index 0000000..c000e3a Binary files /dev/null and b/_static/05/quad_coordinates.png differ diff --git a/_static/05/quad_rendered.png b/_static/05/quad_rendered.png new file mode 100755 index 0000000..6b5caf9 Binary files /dev/null and b/_static/05/quad_rendered.png differ diff --git a/_static/06/2_2_matrix.png b/_static/06/2_2_matrix.png new file mode 100755 index 0000000..d859a43 Binary files /dev/null and b/_static/06/2_2_matrix.png differ diff --git a/_static/06/coordinates.png b/_static/06/coordinates.png new file mode 100755 index 0000000..5de890b Binary files /dev/null and b/_static/06/coordinates.png differ diff --git a/_static/06/projection_matrix.png b/_static/06/projection_matrix.png new file mode 100755 index 0000000..7a5ee29 Binary files /dev/null and b/_static/06/projection_matrix.png differ diff --git a/_static/06/projection_matrix_eq.png b/_static/06/projection_matrix_eq.png new file mode 100755 index 0000000..ed1ac63 Binary files /dev/null and b/_static/06/projection_matrix_eq.png differ diff --git a/_static/06/rectangle.png b/_static/06/rectangle.png new file mode 100755 index 0000000..f3607f5 Binary files /dev/null and b/_static/06/rectangle.png differ diff --git a/_static/06/square_1.png b/_static/06/square_1.png new file mode 100755 index 0000000..3947f78 Binary files /dev/null and b/_static/06/square_1.png differ diff --git a/_static/06/square_coloured.png b/_static/06/square_coloured.png new file mode 100755 index 0000000..ed4cbfb Binary files /dev/null and b/_static/06/square_coloured.png differ diff --git a/_static/06/transformations.png b/_static/06/transformations.png new file mode 100755 index 0000000..3f8e7c4 Binary files /dev/null and b/_static/06/transformations.png differ diff --git a/_static/07/cube_coords.png b/_static/07/cube_coords.png new file mode 100755 index 0000000..35788ab Binary files /dev/null and b/_static/07/cube_coords.png differ diff --git a/_static/07/cube_depth_test.png b/_static/07/cube_depth_test.png new file mode 100755 index 0000000..b326c21 Binary files /dev/null and b/_static/07/cube_depth_test.png differ diff --git a/_static/07/cube_no_depth_test.png b/_static/07/cube_no_depth_test.png new file mode 100755 index 0000000..6f78e8b Binary files /dev/null and b/_static/07/cube_no_depth_test.png differ diff --git a/_static/07/cube_texture.png b/_static/07/cube_texture.png new file mode 100755 index 0000000..a106c9f Binary files /dev/null and b/_static/07/cube_texture.png differ diff --git a/_static/07/cube_texture_front_face.png b/_static/07/cube_texture_front_face.png new file mode 100755 index 0000000..c441d63 Binary files /dev/null and b/_static/07/cube_texture_front_face.png differ diff --git a/_static/07/cube_texture_top_face.png b/_static/07/cube_texture_top_face.png new file mode 100755 index 0000000..8dbabc7 Binary files /dev/null and b/_static/07/cube_texture_top_face.png differ diff --git a/_static/07/cube_with_texture.png b/_static/07/cube_with_texture.png new file mode 100755 index 0000000..752e7e3 Binary files /dev/null and b/_static/07/cube_with_texture.png differ diff --git a/_static/07/texture_coordinates.png b/_static/07/texture_coordinates.png new file mode 100755 index 0000000..cb704ee Binary files /dev/null and b/_static/07/texture_coordinates.png differ diff --git a/_static/07/texture_mapping.png b/_static/07/texture_mapping.png new file mode 100755 index 0000000..1dcd7e0 Binary files /dev/null and b/_static/07/texture_mapping.png differ diff --git a/_static/08/actual_movement.png b/_static/08/actual_movement.png new file mode 100755 index 0000000..d23e177 Binary files /dev/null and b/_static/08/actual_movement.png differ diff --git a/_static/08/camera_movement.png b/_static/08/camera_movement.png new file mode 100755 index 0000000..1be3f3e Binary files /dev/null and b/_static/08/camera_movement.png differ diff --git a/_static/08/cubes.png b/_static/08/cubes.png new file mode 100755 index 0000000..095ad90 Binary files /dev/null and b/_static/08/cubes.png differ diff --git a/_static/08/new_transf_eq.png b/_static/08/new_transf_eq.png new file mode 100755 index 0000000..904ca4f Binary files /dev/null and b/_static/08/new_transf_eq.png differ diff --git a/_static/08/prev_transformation_eq.png b/_static/08/prev_transformation_eq.png new file mode 100755 index 0000000..a51942c Binary files /dev/null and b/_static/08/prev_transformation_eq.png differ diff --git a/_static/08/roll_pitch_yaw.png b/_static/08/roll_pitch_yaw.png new file mode 100755 index 0000000..37bbb82 Binary files /dev/null and b/_static/08/roll_pitch_yaw.png differ diff --git a/_static/09/edge_split.png b/_static/09/edge_split.png new file mode 100755 index 0000000..730a0d7 Binary files /dev/null and b/_static/09/edge_split.png differ diff --git a/_static/09/face_definition.png b/_static/09/face_definition.png new file mode 100755 index 0000000..102458a Binary files /dev/null and b/_static/09/face_definition.png differ diff --git a/_static/09/normals.png b/_static/09/normals.png new file mode 100755 index 0000000..39ac3ce Binary files /dev/null and b/_static/09/normals.png differ diff --git a/_static/09/obj_export_options.png b/_static/09/obj_export_options.png new file mode 100755 index 0000000..b83caeb Binary files /dev/null and b/_static/09/obj_export_options.png differ diff --git a/_static/09/ordering_i.png b/_static/09/ordering_i.png new file mode 100755 index 0000000..fa85765 Binary files /dev/null and b/_static/09/ordering_i.png differ diff --git a/_static/09/ordering_ii.png b/_static/09/ordering_ii.png new file mode 100755 index 0000000..801777d Binary files /dev/null and b/_static/09/ordering_ii.png differ diff --git a/_static/09/ordering_iii.png b/_static/09/ordering_iii.png new file mode 100755 index 0000000..80ca2b9 Binary files /dev/null and b/_static/09/ordering_iii.png differ diff --git a/_static/09/ordering_iv.png b/_static/09/ordering_iv.png new file mode 100755 index 0000000..dff8ef9 Binary files /dev/null and b/_static/09/ordering_iv.png differ diff --git a/_static/09/ordering_v.png b/_static/09/ordering_v.png new file mode 100755 index 0000000..4706af6 Binary files /dev/null and b/_static/09/ordering_v.png differ diff --git a/_static/09/ordering_vi.png b/_static/09/ordering_vi.png new file mode 100755 index 0000000..979e17b Binary files /dev/null and b/_static/09/ordering_vi.png differ diff --git a/_static/09/ordering_vii.png b/_static/09/ordering_vii.png new file mode 100755 index 0000000..3893022 Binary files /dev/null and b/_static/09/ordering_vii.png differ diff --git a/_static/09/standford_bunny.png b/_static/09/standford_bunny.png new file mode 100755 index 0000000..e10620e Binary files /dev/null and b/_static/09/standford_bunny.png differ diff --git a/_static/09/standford_bunny_triangles.png b/_static/09/standford_bunny_triangles.png new file mode 100755 index 0000000..8f4f241 Binary files /dev/null and b/_static/09/standford_bunny_triangles.png differ diff --git a/_static/09/textured_cube.png b/_static/09/textured_cube.png new file mode 100755 index 0000000..cab06ca Binary files /dev/null and b/_static/09/textured_cube.png differ diff --git a/_static/09/vertex_normals.png b/_static/09/vertex_normals.png new file mode 100755 index 0000000..6cd69ef Binary files /dev/null and b/_static/09/vertex_normals.png differ diff --git a/_static/10/diffuse_calc_i.png b/_static/10/diffuse_calc_i.png new file mode 100755 index 0000000..0f3d0e8 Binary files /dev/null and b/_static/10/diffuse_calc_i.png differ diff --git a/_static/10/diffuse_light.png b/_static/10/diffuse_light.png new file mode 100755 index 0000000..38fbfe7 Binary files /dev/null and b/_static/10/diffuse_light.png differ diff --git a/_static/10/diffuse_light_normals.png b/_static/10/diffuse_light_normals.png new file mode 100755 index 0000000..45ad986 Binary files /dev/null and b/_static/10/diffuse_light_normals.png differ diff --git a/_static/10/dot_product.png b/_static/10/dot_product.png new file mode 100755 index 0000000..e18ef5f Binary files /dev/null and b/_static/10/dot_product.png differ diff --git a/_static/10/light_reflection.png b/_static/10/light_reflection.png new file mode 100755 index 0000000..579aea0 Binary files /dev/null and b/_static/10/light_reflection.png differ diff --git a/_static/10/light_types.png b/_static/10/light_types.png new file mode 100755 index 0000000..b92f71f Binary files /dev/null and b/_static/10/light_types.png differ diff --git a/_static/10/lightning_result.png b/_static/10/lightning_result.png new file mode 100755 index 0000000..3c13003 Binary files /dev/null and b/_static/10/lightning_result.png differ diff --git a/_static/10/polished_surface.png b/_static/10/polished_surface.png new file mode 100755 index 0000000..473b22c Binary files /dev/null and b/_static/10/polished_surface.png differ diff --git a/_static/10/specular_lightining.png b/_static/10/specular_lightining.png new file mode 100755 index 0000000..a574b99 Binary files /dev/null and b/_static/10/specular_lightining.png differ diff --git a/_static/10/specular_lightining_calc.png b/_static/10/specular_lightining_calc.png new file mode 100755 index 0000000..08e03ff Binary files /dev/null and b/_static/10/specular_lightining_calc.png differ diff --git a/_static/10/surface.png b/_static/10/surface.png new file mode 100755 index 0000000..e4c492c Binary files /dev/null and b/_static/10/surface.png differ diff --git a/_static/11/directional_light.png b/_static/11/directional_light.png new file mode 100755 index 0000000..5eed51e Binary files /dev/null and b/_static/11/directional_light.png differ diff --git a/_static/11/directional_light_result.png b/_static/11/directional_light_result.png new file mode 100755 index 0000000..95e5d42 Binary files /dev/null and b/_static/11/directional_light_result.png differ diff --git a/_static/11/multiple_lights.png b/_static/11/multiple_lights.png new file mode 100755 index 0000000..a3769cf Binary files /dev/null and b/_static/11/multiple_lights.png differ diff --git a/_static/11/sine_cosine.png b/_static/11/sine_cosine.png new file mode 100755 index 0000000..fe437d8 Binary files /dev/null and b/_static/11/sine_cosine.png differ diff --git a/_static/11/spot_light.png b/_static/11/spot_light.png new file mode 100755 index 0000000..4541ba8 Binary files /dev/null and b/_static/11/spot_light.png differ diff --git a/_static/11/spot_light_calc.png b/_static/11/spot_light_calc.png new file mode 100755 index 0000000..bffd6f6 Binary files /dev/null and b/_static/11/spot_light_calc.png differ diff --git a/_static/11/spot_light_ii.png b/_static/11/spot_light_ii.png new file mode 100755 index 0000000..7d40c97 Binary files /dev/null and b/_static/11/spot_light_ii.png differ diff --git a/_static/11/spot_light_sample.png b/_static/11/spot_light_sample.png new file mode 100755 index 0000000..2c688ac Binary files /dev/null and b/_static/11/spot_light_sample.png differ diff --git a/_static/11/sun_directional_light.png b/_static/11/sun_directional_light.png new file mode 100755 index 0000000..f4bd0ba Binary files /dev/null and b/_static/11/sun_directional_light.png differ diff --git a/_static/11/sun_movement.png b/_static/11/sun_movement.png new file mode 100755 index 0000000..7e59fc5 Binary files /dev/null and b/_static/11/sun_movement.png differ diff --git a/_static/12/CBG.png b/_static/12/CBG.png new file mode 100755 index 0000000..3b0e910 Binary files /dev/null and b/_static/12/CBG.png differ diff --git a/_static/12/compass.png b/_static/12/compass.png new file mode 100755 index 0000000..da1671c Binary files /dev/null and b/_static/12/compass.png differ diff --git a/_static/12/font_texture.png b/_static/12/font_texture.png new file mode 100755 index 0000000..383cd34 Binary files /dev/null and b/_static/12/font_texture.png differ diff --git a/_static/12/hud_compass.png b/_static/12/hud_compass.png new file mode 100755 index 0000000..fb7cc53 Binary files /dev/null and b/_static/12/hud_compass.png differ diff --git a/_static/12/orthographic_matrix.png b/_static/12/orthographic_matrix.png new file mode 100755 index 0000000..806b137 Binary files /dev/null and b/_static/12/orthographic_matrix.png differ diff --git a/_static/12/orthographic_projection_sample.png b/_static/12/orthographic_projection_sample.png new file mode 100755 index 0000000..c451445 Binary files /dev/null and b/_static/12/orthographic_projection_sample.png differ diff --git a/_static/12/orthographic_projections.png b/_static/12/orthographic_projections.png new file mode 100755 index 0000000..fb858f1 Binary files /dev/null and b/_static/12/orthographic_projections.png differ diff --git a/_static/12/text_opaque.png b/_static/12/text_opaque.png new file mode 100755 index 0000000..124ab83 Binary files /dev/null and b/_static/12/text_opaque.png differ diff --git a/_static/12/text_quad.png b/_static/12/text_quad.png new file mode 100755 index 0000000..5ecef62 Binary files /dev/null and b/_static/12/text_quad.png differ diff --git a/_static/12/text_quad_coords.png b/_static/12/text_quad_coords.png new file mode 100755 index 0000000..ea9f763 Binary files /dev/null and b/_static/12/text_quad_coords.png differ diff --git a/_static/12/text_rendered_improved.png b/_static/12/text_rendered_improved.png new file mode 100755 index 0000000..516f760 Binary files /dev/null and b/_static/12/text_rendered_improved.png differ diff --git a/_static/12/text_result.png b/_static/12/text_result.png new file mode 100755 index 0000000..c67eade Binary files /dev/null and b/_static/12/text_result.png differ diff --git a/_static/12/text_transparent.png b/_static/12/text_transparent.png new file mode 100755 index 0000000..bcf5b26 Binary files /dev/null and b/_static/12/text_transparent.png differ diff --git a/_static/12/texture_font.png b/_static/12/texture_font.png new file mode 100755 index 0000000..672d3c8 Binary files /dev/null and b/_static/12/texture_font.png differ diff --git a/_static/13/cube_clockwise.png b/_static/13/cube_clockwise.png new file mode 100755 index 0000000..10e3355 Binary files /dev/null and b/_static/13/cube_clockwise.png differ diff --git a/_static/13/cube_counter_clockwise.png b/_static/13/cube_counter_clockwise.png new file mode 100755 index 0000000..1ad89e2 Binary files /dev/null and b/_static/13/cube_counter_clockwise.png differ diff --git a/_static/13/cube_interior.png b/_static/13/cube_interior.png new file mode 100755 index 0000000..6a011bb Binary files /dev/null and b/_static/13/cube_interior.png differ diff --git a/_static/13/game_item_list.png b/_static/13/game_item_list.png new file mode 100755 index 0000000..a4e833e Binary files /dev/null and b/_static/13/game_item_list.png differ diff --git a/_static/13/mesh_map.png b/_static/13/mesh_map.png new file mode 100755 index 0000000..016f44d Binary files /dev/null and b/_static/13/mesh_map.png differ diff --git a/_static/13/skybox.png b/_static/13/skybox.png new file mode 100755 index 0000000..a88c7a5 Binary files /dev/null and b/_static/13/skybox.png differ diff --git a/_static/13/skybox_cube_faces.png b/_static/13/skybox_cube_faces.png new file mode 100755 index 0000000..db29981 Binary files /dev/null and b/_static/13/skybox_cube_faces.png differ diff --git a/_static/13/skybox_displaced.png b/_static/13/skybox_displaced.png new file mode 100755 index 0000000..19c2b2f Binary files /dev/null and b/_static/13/skybox_displaced.png differ diff --git a/_static/13/skybox_face_culling.png b/_static/13/skybox_face_culling.png new file mode 100755 index 0000000..a464e80 Binary files /dev/null and b/_static/13/skybox_face_culling.png differ diff --git a/_static/13/skybox_face_culling_exterior.png b/_static/13/skybox_face_culling_exterior.png new file mode 100755 index 0000000..c2dc68a Binary files /dev/null and b/_static/13/skybox_face_culling_exterior.png differ diff --git a/_static/13/skybox_result.png b/_static/13/skybox_result.png new file mode 100755 index 0000000..6a65d20 Binary files /dev/null and b/_static/13/skybox_result.png differ diff --git a/_static/13/skybox_texture.png b/_static/13/skybox_texture.png new file mode 100755 index 0000000..b842217 Binary files /dev/null and b/_static/13/skybox_texture.png differ diff --git a/_static/14/heightmap.png b/_static/14/heightmap.png new file mode 100755 index 0000000..95d98a1 Binary files /dev/null and b/_static/14/heightmap.png differ diff --git a/_static/14/heightmap_coordinates.png b/_static/14/heightmap_coordinates.png new file mode 100755 index 0000000..cb968d1 Binary files /dev/null and b/_static/14/heightmap_coordinates.png differ diff --git a/_static/14/heightmap_grid.png b/_static/14/heightmap_grid.png new file mode 100755 index 0000000..b9d141d Binary files /dev/null and b/_static/14/heightmap_grid.png differ diff --git a/_static/14/heightmap_indices_i.png b/_static/14/heightmap_indices_i.png new file mode 100755 index 0000000..8977c33 Binary files /dev/null and b/_static/14/heightmap_indices_i.png differ diff --git a/_static/14/heightmap_indices_ii.png b/_static/14/heightmap_indices_ii.png new file mode 100755 index 0000000..18c23c8 Binary files /dev/null and b/_static/14/heightmap_indices_ii.png differ diff --git a/_static/14/heightmap_vertices.png b/_static/14/heightmap_vertices.png new file mode 100755 index 0000000..cb73766 Binary files /dev/null and b/_static/14/heightmap_vertices.png differ diff --git a/_static/14/normals_calc_i.png b/_static/14/normals_calc_i.png new file mode 100755 index 0000000..e6d43d3 Binary files /dev/null and b/_static/14/normals_calc_i.png differ diff --git a/_static/14/normals_calc_ii.png b/_static/14/normals_calc_ii.png new file mode 100755 index 0000000..ee15fce Binary files /dev/null and b/_static/14/normals_calc_ii.png differ diff --git a/_static/14/normals_calc_iii.png b/_static/14/normals_calc_iii.png new file mode 100755 index 0000000..c8bc7e0 Binary files /dev/null and b/_static/14/normals_calc_iii.png differ diff --git a/_static/14/terrain_construction_1.png b/_static/14/terrain_construction_1.png new file mode 100755 index 0000000..fa3ab92 Binary files /dev/null and b/_static/14/terrain_construction_1.png differ diff --git a/_static/14/terrain_construction_2.png b/_static/14/terrain_construction_2.png new file mode 100755 index 0000000..138b1da Binary files /dev/null and b/_static/14/terrain_construction_2.png differ diff --git a/_static/14/terrain_result.png b/_static/14/terrain_result.png new file mode 100755 index 0000000..b67da4b Binary files /dev/null and b/_static/14/terrain_result.png differ diff --git a/_static/14/texture_coordinates_i.png b/_static/14/texture_coordinates_i.png new file mode 100755 index 0000000..9bd9141 Binary files /dev/null and b/_static/14/texture_coordinates_i.png differ diff --git a/_static/14/texture_coordinates_ii.png b/_static/14/texture_coordinates_ii.png new file mode 100755 index 0000000..87bb15e Binary files /dev/null and b/_static/14/texture_coordinates_ii.png differ diff --git a/_static/15/cell.png b/_static/15/cell.png new file mode 100755 index 0000000..e047389 Binary files /dev/null and b/_static/15/cell.png differ diff --git a/_static/15/model_to_world_coordinates.png b/_static/15/model_to_world_coordinates.png new file mode 100755 index 0000000..a7c007b Binary files /dev/null and b/_static/15/model_to_world_coordinates.png differ diff --git a/_static/15/terrain_block_variables_n.png b/_static/15/terrain_block_variables_n.png new file mode 100755 index 0000000..514b620 Binary files /dev/null and b/_static/15/terrain_block_variables_n.png differ diff --git a/_static/15/terrain_grid.png b/_static/15/terrain_grid.png new file mode 100755 index 0000000..606fcf5 Binary files /dev/null and b/_static/15/terrain_grid.png differ diff --git a/_static/15/tunnelling.png b/_static/15/tunnelling.png new file mode 100755 index 0000000..54a5ea6 Binary files /dev/null and b/_static/15/tunnelling.png differ diff --git a/_static/16/exponential_model.png b/_static/16/exponential_model.png new file mode 100755 index 0000000..d27706a Binary files /dev/null and b/_static/16/exponential_model.png differ diff --git a/_static/16/fog_at_night.png b/_static/16/fog_at_night.png new file mode 100755 index 0000000..59110d9 Binary files /dev/null and b/_static/16/fog_at_night.png differ diff --git a/_static/16/fog_clear_colour_black.png b/_static/16/fog_clear_colour_black.png new file mode 100755 index 0000000..5980793 Binary files /dev/null and b/_static/16/fog_clear_colour_black.png differ diff --git a/_static/16/fog_clear_colour_grey.png b/_static/16/fog_clear_colour_grey.png new file mode 100755 index 0000000..9053499 Binary files /dev/null and b/_static/16/fog_clear_colour_grey.png differ diff --git a/_static/16/fog_effect.png b/_static/16/fog_effect.png new file mode 100755 index 0000000..a94eb24 Binary files /dev/null and b/_static/16/fog_effect.png differ diff --git a/_static/16/glowing_fog.png b/_static/16/glowing_fog.png new file mode 100755 index 0000000..dcc7098 Binary files /dev/null and b/_static/16/glowing_fog.png differ diff --git a/_static/16/linear_model.png b/_static/16/linear_model.png new file mode 100755 index 0000000..0a96e5c Binary files /dev/null and b/_static/16/linear_model.png differ diff --git a/_static/17/fragment_normals.png b/_static/17/fragment_normals.png new file mode 100755 index 0000000..be25baf Binary files /dev/null and b/_static/17/fragment_normals.png differ diff --git a/_static/17/normal_mapping_result.png b/_static/17/normal_mapping_result.png new file mode 100755 index 0000000..88f3497 Binary files /dev/null and b/_static/17/normal_mapping_result.png differ diff --git a/_static/17/rock.png b/_static/17/rock.png new file mode 100755 index 0000000..1f683b4 Binary files /dev/null and b/_static/17/rock.png differ diff --git a/_static/17/rock_normals.png b/_static/17/rock_normals.png new file mode 100755 index 0000000..b91467e Binary files /dev/null and b/_static/17/rock_normals.png differ diff --git a/_static/17/surface_normals.png b/_static/17/surface_normals.png new file mode 100755 index 0000000..20e654a Binary files /dev/null and b/_static/17/surface_normals.png differ diff --git a/_static/18/depth_average_value.png b/_static/18/depth_average_value.png new file mode 100755 index 0000000..bb03ac7 Binary files /dev/null and b/_static/18/depth_average_value.png differ diff --git a/_static/18/depth_map.png b/_static/18/depth_map.png new file mode 100755 index 0000000..6d387f3 Binary files /dev/null and b/_static/18/depth_map.png differ diff --git a/_static/18/depth_map_higher_distance.png b/_static/18/depth_map_higher_distance.png new file mode 100755 index 0000000..3f9ad09 Binary files /dev/null and b/_static/18/depth_map_higher_distance.png differ diff --git a/_static/18/final_result.png b/_static/18/final_result.png new file mode 100755 index 0000000..6e7d108 Binary files /dev/null and b/_static/18/final_result.png differ diff --git a/_static/18/generic_light_pos_calculation.png b/_static/18/generic_light_pos_calculation.png new file mode 100755 index 0000000..3bbb5e7 Binary files /dev/null and b/_static/18/generic_light_pos_calculation.png differ diff --git a/_static/18/light_position.png b/_static/18/light_position.png new file mode 100755 index 0000000..cddbabf Binary files /dev/null and b/_static/18/light_position.png differ diff --git a/_static/18/render_light_perspective.png b/_static/18/render_light_perspective.png new file mode 100755 index 0000000..537f2a6 Binary files /dev/null and b/_static/18/render_light_perspective.png differ diff --git a/_static/18/shadow_concepts_I.png b/_static/18/shadow_concepts_I.png new file mode 100755 index 0000000..9f2a945 Binary files /dev/null and b/_static/18/shadow_concepts_I.png differ diff --git a/_static/18/shadow_concepts_II.png b/_static/18/shadow_concepts_II.png new file mode 100755 index 0000000..0737d60 Binary files /dev/null and b/_static/18/shadow_concepts_II.png differ diff --git a/_static/18/shadow_map_result.png b/_static/18/shadow_map_result.png new file mode 100755 index 0000000..325c4b5 Binary files /dev/null and b/_static/18/shadow_map_result.png differ diff --git a/_static/18/shadow_no_acne.png b/_static/18/shadow_no_acne.png new file mode 100755 index 0000000..53fbd5e Binary files /dev/null and b/_static/18/shadow_no_acne.png differ diff --git a/_static/19/Animation.png b/_static/19/Animation.png new file mode 100755 index 0000000..fb1b39d Binary files /dev/null and b/_static/19/Animation.png differ diff --git a/_static/19/animation_refined.png b/_static/19/animation_refined.png new file mode 100755 index 0000000..8d318c1 Binary files /dev/null and b/_static/19/animation_refined.png differ diff --git a/_static/19/binding_pose.png b/_static/19/binding_pose.png new file mode 100755 index 0000000..047e711 Binary files /dev/null and b/_static/19/binding_pose.png differ diff --git a/_static/19/md5_animation_structure.png b/_static/19/md5_animation_structure.png new file mode 100755 index 0000000..1e58c72 Binary files /dev/null and b/_static/19/md5_animation_structure.png differ diff --git a/_static/19/md5_model_structure.png b/_static/19/md5_model_structure.png new file mode 100755 index 0000000..043921b Binary files /dev/null and b/_static/19/md5_model_structure.png differ diff --git a/_static/19/mesh_elements.png b/_static/19/mesh_elements.png new file mode 100755 index 0000000..0deabd8 Binary files /dev/null and b/_static/19/mesh_elements.png differ diff --git a/_static/19/nomal_calculation.png b/_static/19/nomal_calculation.png new file mode 100755 index 0000000..8bbc4a3 Binary files /dev/null and b/_static/19/nomal_calculation.png differ diff --git a/_static/19/quaternion.png b/_static/19/quaternion.png new file mode 100755 index 0000000..428b119 Binary files /dev/null and b/_static/19/quaternion.png differ diff --git a/_static/19/relation_to_joints_matrix.png b/_static/19/relation_to_joints_matrix.png new file mode 100755 index 0000000..e3d153a Binary files /dev/null and b/_static/19/relation_to_joints_matrix.png differ diff --git a/_static/19/static_vao_vs_animation_vao.png b/_static/19/static_vao_vs_animation_vao.png new file mode 100755 index 0000000..60c2cbb Binary files /dev/null and b/_static/19/static_vao_vs_animation_vao.png differ diff --git a/_static/20/animated_particles.png b/_static/20/animated_particles.png new file mode 100755 index 0000000..c53ebbf Binary files /dev/null and b/_static/20/animated_particles.png differ diff --git a/_static/20/particle_texture.png b/_static/20/particle_texture.png new file mode 100755 index 0000000..1370aa0 Binary files /dev/null and b/_static/20/particle_texture.png differ diff --git a/_static/20/particles_i.png b/_static/20/particles_i.png new file mode 100755 index 0000000..7caa5bd Binary files /dev/null and b/_static/20/particles_i.png differ diff --git a/_static/20/particles_ii.png b/_static/20/particles_ii.png new file mode 100755 index 0000000..53d8c3e Binary files /dev/null and b/_static/20/particles_ii.png differ diff --git a/_static/20/particles_iii.png b/_static/20/particles_iii.png new file mode 100755 index 0000000..a4832af Binary files /dev/null and b/_static/20/particles_iii.png differ diff --git a/_static/20/particles_iv.png b/_static/20/particles_iv.png new file mode 100755 index 0000000..f8dc000 Binary files /dev/null and b/_static/20/particles_iv.png differ diff --git a/_static/20/particles_v.png b/_static/20/particles_v.png new file mode 100755 index 0000000..f1f6123 Binary files /dev/null and b/_static/20/particles_v.png differ diff --git a/_static/20/particles_vi.png b/_static/20/particles_vi.png new file mode 100755 index 0000000..4d89767 Binary files /dev/null and b/_static/20/particles_vi.png differ diff --git a/_static/20/texture_atlas.png b/_static/20/texture_atlas.png new file mode 100755 index 0000000..a4ffb0b Binary files /dev/null and b/_static/20/texture_atlas.png differ diff --git a/_static/20/texture_coordinates.png b/_static/20/texture_coordinates.png new file mode 100755 index 0000000..55b6fa2 Binary files /dev/null and b/_static/20/texture_coordinates.png differ diff --git a/_static/21/cubes_height_map.png b/_static/21/cubes_height_map.png new file mode 100755 index 0000000..0427852 Binary files /dev/null and b/_static/21/cubes_height_map.png differ diff --git a/_static/21/instanced_rendering.png b/_static/21/instanced_rendering.png new file mode 100755 index 0000000..e17fed2 Binary files /dev/null and b/_static/21/instanced_rendering.png differ diff --git a/_static/21/particles.png b/_static/21/particles.png new file mode 100755 index 0000000..c150224 Binary files /dev/null and b/_static/21/particles.png differ diff --git a/_static/21/single_vbo.png b/_static/21/single_vbo.png new file mode 100755 index 0000000..b7a43d1 Binary files /dev/null and b/_static/21/single_vbo.png differ diff --git a/_static/21/vao_1.png b/_static/21/vao_1.png new file mode 100755 index 0000000..224c3dd Binary files /dev/null and b/_static/21/vao_1.png differ diff --git a/_static/21/vao_2.png b/_static/21/vao_2.png new file mode 100755 index 0000000..cd3e2f5 Binary files /dev/null and b/_static/21/vao_2.png differ diff --git a/_static/22/listener_at_up.png b/_static/22/listener_at_up.png new file mode 100755 index 0000000..503f337 Binary files /dev/null and b/_static/22/listener_at_up.png differ diff --git a/_static/22/openal_concepts.png b/_static/22/openal_concepts.png new file mode 100755 index 0000000..70919db Binary files /dev/null and b/_static/22/openal_concepts.png differ diff --git a/_static/23/camera.png b/_static/23/camera.png new file mode 100755 index 0000000..6ffd07c Binary files /dev/null and b/_static/23/camera.png differ diff --git a/_static/23/intersection.png b/_static/23/intersection.png new file mode 100755 index 0000000..85a66e8 Binary files /dev/null and b/_static/23/intersection.png differ diff --git a/_static/23/mouse_selection.png b/_static/23/mouse_selection.png new file mode 100755 index 0000000..fb91c8b Binary files /dev/null and b/_static/23/mouse_selection.png differ diff --git a/_static/23/object_picking.png b/_static/23/object_picking.png new file mode 100755 index 0000000..550beb2 Binary files /dev/null and b/_static/23/object_picking.png differ diff --git a/_static/23/object_picking_result.png b/_static/23/object_picking_result.png new file mode 100755 index 0000000..9fbfd95 Binary files /dev/null and b/_static/23/object_picking_result.png differ diff --git a/_static/23/screen_coordinates.png b/_static/23/screen_coordinates.png new file mode 100755 index 0000000..2bf4e52 Binary files /dev/null and b/_static/23/screen_coordinates.png differ diff --git a/_static/24/hud.png b/_static/24/hud.png new file mode 100755 index 0000000..cb7a02a Binary files /dev/null and b/_static/24/hud.png differ diff --git a/_static/25/bounding_sphere.png b/_static/25/bounding_sphere.png new file mode 100755 index 0000000..27cc21e Binary files /dev/null and b/_static/25/bounding_sphere.png differ diff --git a/_static/25/distance_to_plane.png b/_static/25/distance_to_plane.png new file mode 100755 index 0000000..e846a08 Binary files /dev/null and b/_static/25/distance_to_plane.png differ diff --git a/_static/25/plane.png b/_static/25/plane.png new file mode 100755 index 0000000..ae7a47d Binary files /dev/null and b/_static/25/plane.png differ diff --git a/_static/25/view_frustum_i.png b/_static/25/view_frustum_i.png new file mode 100755 index 0000000..9956632 Binary files /dev/null and b/_static/25/view_frustum_i.png differ diff --git a/_static/25/view_frustum_ii.png b/_static/25/view_frustum_ii.png new file mode 100755 index 0000000..f74291f Binary files /dev/null and b/_static/25/view_frustum_ii.png differ diff --git a/_static/26/cascade_splits.png b/_static/26/cascade_splits.png new file mode 100755 index 0000000..001f8da Binary files /dev/null and b/_static/26/cascade_splits.png differ diff --git a/_static/26/csmpng.png b/_static/26/csmpng.png new file mode 100755 index 0000000..3a6ce47 Binary files /dev/null and b/_static/26/csmpng.png differ diff --git a/_static/26/frustum_center.png b/_static/26/frustum_center.png new file mode 100755 index 0000000..fa470e7 Binary files /dev/null and b/_static/26/frustum_center.png differ diff --git a/_static/26/light_position.png b/_static/26/light_position.png new file mode 100755 index 0000000..29f50ff Binary files /dev/null and b/_static/26/light_position.png differ diff --git a/_static/26/low_quality_shadows.png b/_static/26/low_quality_shadows.png new file mode 100755 index 0000000..2060047 Binary files /dev/null and b/_static/26/low_quality_shadows.png differ diff --git a/_static/26/view_frustum.png b/_static/26/view_frustum.png new file mode 100755 index 0000000..7b66c98 Binary files /dev/null and b/_static/26/view_frustum.png differ diff --git a/_static/27/animation_result.png b/_static/27/animation_result.png new file mode 100755 index 0000000..595e366 Binary files /dev/null and b/_static/27/animation_result.png differ diff --git a/_static/27/mesh_bones_weights_vertices.png b/_static/27/mesh_bones_weights_vertices.png new file mode 100755 index 0000000..dd6eeb0 Binary files /dev/null and b/_static/27/mesh_bones_weights_vertices.png differ diff --git a/_static/27/model.png b/_static/27/model.png new file mode 100755 index 0000000..f1745ce Binary files /dev/null and b/_static/27/model.png differ diff --git a/_static/27/node_animations.png b/_static/27/node_animations.png new file mode 100755 index 0000000..d80446e Binary files /dev/null and b/_static/27/node_animations.png differ diff --git a/_static/27/vao_animation.png b/_static/27/vao_animation.png new file mode 100755 index 0000000..5bedc4b Binary files /dev/null and b/_static/27/vao_animation.png differ diff --git a/_static/28/result.png b/_static/28/result.png new file mode 100755 index 0000000..5328577 Binary files /dev/null and b/_static/28/result.png differ diff --git a/_static/28/schema.png b/_static/28/schema.png new file mode 100755 index 0000000..e8c218d Binary files /dev/null and b/_static/28/schema.png differ diff --git a/_static/28/text_diffuse.png b/_static/28/text_diffuse.png new file mode 100755 index 0000000..dd329ff Binary files /dev/null and b/_static/28/text_diffuse.png differ diff --git a/_static/28/text_normals.png b/_static/28/text_normals.png new file mode 100755 index 0000000..9c4f41a Binary files /dev/null and b/_static/28/text_normals.png differ diff --git a/_static/28/text_positions.png b/_static/28/text_positions.png new file mode 100755 index 0000000..15ad31e Binary files /dev/null and b/_static/28/text_positions.png differ diff --git a/_static/a01/capture.png b/_static/a01/capture.png new file mode 100755 index 0000000..0da5840 Binary files /dev/null and b/_static/a01/capture.png differ diff --git a/_static/a01/event_browser.png b/_static/a01/event_browser.png new file mode 100755 index 0000000..f93c15a Binary files /dev/null and b/_static/a01/event_browser.png differ diff --git a/_static/a01/exec_arguments.png b/_static/a01/exec_arguments.png new file mode 100755 index 0000000..c45cde9 Binary files /dev/null and b/_static/a01/exec_arguments.png differ diff --git a/_static/a01/java_process.png b/_static/a01/java_process.png new file mode 100755 index 0000000..8607bc9 Binary files /dev/null and b/_static/a01/java_process.png differ diff --git a/_static/a01/mesh.png b/_static/a01/mesh.png new file mode 100755 index 0000000..ca81bd9 Binary files /dev/null and b/_static/a01/mesh.png differ diff --git a/_static/a01/pipeline_state.png b/_static/a01/pipeline_state.png new file mode 100755 index 0000000..fc6405b Binary files /dev/null and b/_static/a01/pipeline_state.png differ diff --git a/_static/a01/renderdoc.png b/_static/a01/renderdoc.png new file mode 100755 index 0000000..d116151 Binary files /dev/null and b/_static/a01/renderdoc.png differ diff --git a/_static/a01/sample.png b/_static/a01/sample.png new file mode 100755 index 0000000..b5207a9 Binary files /dev/null and b/_static/a01/sample.png differ diff --git a/_static/a01/texture_inputs.png b/_static/a01/texture_inputs.png new file mode 100755 index 0000000..3bd51f4 Binary files /dev/null and b/_static/a01/texture_inputs.png differ diff --git a/_static/a01/texture_outputs.png b/_static/a01/texture_outputs.png new file mode 100755 index 0000000..dc3233b Binary files /dev/null and b/_static/a01/texture_outputs.png differ diff --git a/a01-opengl-debugging/index.html b/a01-opengl-debugging/index.html new file mode 100755 index 0000000..55a9f00 --- /dev/null +++ b/a01-opengl-debugging/index.html @@ -0,0 +1,296 @@ + + + + + + + + + + + + 附录 A - OpenGL调试 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

附录 A - OpenGL调试(OpenGL Debugging)

+

调试OpenGL程序可能是一项艰巨的任务。在大多数情况下,你会看到一个黑屏,你无法知道到底发生了什么。为了缓解这个窘况,我们可以使用一些现有的工具来提供关于渲染流程的更多信息。

+

在本附录中,我们将学习如何使用RenderDoc工具调试LWJGL程序。RenderDoc是一个图形调试工程,可以与Direct3D、Vulkan和OpenGL一起使用。对于OpenGL,它仅支持从3.2到4.5的核心模式。

+

让我们开始吧,你需要下载并安装对应操作系统的RenderDoc。安装后,当你启动它时,会看到类似于下图的东西:

+

RenderDoc

+

第一步是配置RenderDoc来运行和监视我们的示例。在“Capture Executable”选项卡中,需要设置以下参数:

+
    +
  • Executable path:在本例中,这应该指向JVM启动程序(例如,“C:\Program Files\Java\jdk-9\bin\java.exe”)。
  • +
  • Working Directory:这将为你的程序设置工作目录。在本例中,应该将其设置为Maven转储结果的target目录。这样设置后,可以找到依赖项。
  • +
  • Command line arguments:这将包含JVM运行示例所需的参数。在本例中,只需要传递要执行的Jar(例如,“-java game-c28-1.0.jar”)。
  • +
+

运行参数

+

你要记住3D模型现已由Assimp库加载,我们需要的是实际的文件路径(而不是CLASSPATH的相对路径),因此你需要在你设置的工作目录检查路径。在此情况下,最简单快速的测试方法是将src文件夹复制到target目录中。

+

此选项卡中还有许多其他选项用于配置捕获选项。你可以在RenderDoc文档中查阅它们的用途。一切就绪后,你可以通过点击“Launch”按钮来开始运行程序。你将看到如下内容:

+

示例

+

你可能会看到一个警告,因为RenderDoc只能使用OpenGL核心模式。在本例中,我们已经启用了兼容性模式,但它会忽略警告继续工作。程序运行后,你就可以触发它的快照。你将看到添加了一个新的选项卡,名为“java [PID XXXX]”(其中XXXX数字表示Java进程的PID、进程标识号)

+

Java进程

+

从该选项卡中,你可以通过按下“Trigger capture”按钮来捕获程序的状态。生成了一个捕获结果后,你将在同一个选项卡中看到一个小快照。

+

捕获

+

如果双击该捕获结果,则将加载收集的所有数据,并可以开始查看它。“Event Browser”面板将显示所有在一个渲染周期内执行的与OpenGL相关的调用。

+

事件浏览器

+

对于第一个渲染流程,你可以看到地面是如何在模型房屋的网格之后绘制的。如果单击一个glDrawELements事件,并选择“Mesh”选项卡,则可以查看绘制的网格,事件的顶点着色器的输入与输出。

+

你换可以查看用于该绘制操作的输入纹理(通过单击“Texture Viewer”选项卡)。

+

纹理输入

+

在中央面板中,你可以看到输出,在右侧面板上可以看到用于输入的纹理列表。还可以逐个查看输出纹理。这很好地说明了延迟着色是如何工作的。

+

纹理输出

+

如你所见,该工具提供了有关渲染时发生的情况的宝贵数据,它可以在调试渲染问题时节省宝贵的时间。它甚至可以显示有关渲染管线中使用的着色器的信息。

+

管线状态

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/css/theme.css b/css/theme.css new file mode 100755 index 0000000..bb00f2f --- /dev/null +++ b/css/theme.css @@ -0,0 +1,14 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/rtfd/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 0.4.1 | MIT license */ + /* Built 20180727 10:07 */ + *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.3576515979%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.3576515979%;width:48.821174201%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.3576515979%;width:31.7615656014%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{position:absolute;content:"";display:block;left:0;top:0;width:36px;height:12px;border-radius:4px;background:#ccc;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27AE60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:.3em;display:block}.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:before,.wy-breadcrumbs:after{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin-bottom:0;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a{color:#404040}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980B9;text-align:center;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:gray}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:gray}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{width:100%}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1100px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;display:block;overflow:auto}.rst-content pre.literal-block,.rst-content div[class^='highlight']{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px 0}.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{padding:0px;border:none;margin:0}.rst-content div[class^='highlight'] td.code{width:100%}.rst-content .linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;display:block;overflow:auto}.rst-content div[class^='highlight'] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:12px;line-height:1.4}@media print{.rst-content .codeblock,.rst-content div[class^='highlight'],.rst-content div[class^='highlight'] pre{white-space:pre-wrap}}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .section ol p:last-child,.rst-content .section ul p:last-child{margin-bottom:24px}.rst-content .line-block{margin-left:0px;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink{visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after{content:"";font-family:FontAwesome}.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after{visibility:visible}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:baseline;position:relative;top:-0.4em;line-height:0;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:gray}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}.rst-content table.docutils td .last,.rst-content table.docutils td .last :last-child{margin-bottom:0}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content tt,.rst-content tt,.rst-content code{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content pre,.rst-content kbd,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold;margin-bottom:12px}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-regular.eot");src:url("../fonts/Lato/lato-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-regular.woff2") format("woff2"),url("../fonts/Lato/lato-regular.woff") format("woff"),url("../fonts/Lato/lato-regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bold.eot");src:url("../fonts/Lato/lato-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bold.woff2") format("woff2"),url("../fonts/Lato/lato-bold.woff") format("woff"),url("../fonts/Lato/lato-bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bolditalic.eot");src:url("../fonts/Lato/lato-bolditalic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bolditalic.woff2") format("woff2"),url("../fonts/Lato/lato-bolditalic.woff") format("woff"),url("../fonts/Lato/lato-bolditalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-italic.eot");src:url("../fonts/Lato/lato-italic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-italic.woff2") format("woff2"),url("../fonts/Lato/lato-italic.woff") format("woff"),url("../fonts/Lato/lato-italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:url("../fonts/RobotoSlab/roboto-slab.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.ttf") format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.ttf") format("truetype")} diff --git a/css/theme_extra.css b/css/theme_extra.css new file mode 100755 index 0000000..ec7002a --- /dev/null +++ b/css/theme_extra.css @@ -0,0 +1,191 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; /* csslint allow: box-model */ + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid wierdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; /* csslint allow: important */ + border-collapse: collapse; +} + +/* + * Without the following amendments, the navigation in the theme will be + * slightly cut off. This is due to the fact that the .wy-nav-side has a + * padding-bottom of 2em, which must not necessarily align with the font-size of + * 90 % on the .rst-current-version container, combined with the padding of 12px + * above and below. These amendments fix this in two steps: First, make sure the + * .rst-current-version container has a fixed height of 40px, achieved using + * line-height, and then applying a padding-bottom of 40px to this container. In + * a second step, the items within that container are re-aligned using flexbox. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ + .wy-nav-side { + padding-bottom: 40px; +} + +/* + * The second step of above amendment: Here we make sure the items are aligned + * correctly within the .rst-current-version container. Using flexbox, we + * achieve it in such a way that it will look like the following: + * + * [No repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * [With repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-versions .rst-current-version { + padding: 0 12px; + display: flex; + font-size: initial; + justify-content: space-between; + align-items: center; + line-height: 40px; +} + +/* + * Please note that this amendment also involves removing certain inline-styles + * from the file ./mkdocs/themes/readthedocs/versions.html. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-current-version span { + flex: 1; + text-align: center; +} diff --git a/fonts/Lato/lato-bold.eot b/fonts/Lato/lato-bold.eot new file mode 100755 index 0000000..3361183 Binary files /dev/null and b/fonts/Lato/lato-bold.eot differ diff --git a/fonts/Lato/lato-bold.ttf b/fonts/Lato/lato-bold.ttf new file mode 100755 index 0000000..29f691d Binary files /dev/null and b/fonts/Lato/lato-bold.ttf differ diff --git a/fonts/Lato/lato-bold.woff b/fonts/Lato/lato-bold.woff new file mode 100755 index 0000000..c6dff51 Binary files /dev/null and b/fonts/Lato/lato-bold.woff differ diff --git a/fonts/Lato/lato-bold.woff2 b/fonts/Lato/lato-bold.woff2 new file mode 100755 index 0000000..bb19504 Binary files /dev/null and b/fonts/Lato/lato-bold.woff2 differ diff --git a/fonts/Lato/lato-bolditalic.eot b/fonts/Lato/lato-bolditalic.eot new file mode 100755 index 0000000..3d41549 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.eot differ diff --git a/fonts/Lato/lato-bolditalic.ttf b/fonts/Lato/lato-bolditalic.ttf new file mode 100755 index 0000000..f402040 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.ttf differ diff --git a/fonts/Lato/lato-bolditalic.woff b/fonts/Lato/lato-bolditalic.woff new file mode 100755 index 0000000..88ad05b Binary files /dev/null and b/fonts/Lato/lato-bolditalic.woff differ diff --git a/fonts/Lato/lato-bolditalic.woff2 b/fonts/Lato/lato-bolditalic.woff2 new file mode 100755 index 0000000..c4e3d80 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/fonts/Lato/lato-italic.eot b/fonts/Lato/lato-italic.eot new file mode 100755 index 0000000..3f82642 Binary files /dev/null and b/fonts/Lato/lato-italic.eot differ diff --git a/fonts/Lato/lato-italic.ttf b/fonts/Lato/lato-italic.ttf new file mode 100755 index 0000000..b4bfc9b Binary files /dev/null and b/fonts/Lato/lato-italic.ttf differ diff --git a/fonts/Lato/lato-italic.woff b/fonts/Lato/lato-italic.woff new file mode 100755 index 0000000..76114bc Binary files /dev/null and b/fonts/Lato/lato-italic.woff differ diff --git a/fonts/Lato/lato-italic.woff2 b/fonts/Lato/lato-italic.woff2 new file mode 100755 index 0000000..3404f37 Binary files /dev/null and b/fonts/Lato/lato-italic.woff2 differ diff --git a/fonts/Lato/lato-regular.eot b/fonts/Lato/lato-regular.eot new file mode 100755 index 0000000..11e3f2a Binary files /dev/null and b/fonts/Lato/lato-regular.eot differ diff --git a/fonts/Lato/lato-regular.ttf b/fonts/Lato/lato-regular.ttf new file mode 100755 index 0000000..74decd9 Binary files /dev/null and b/fonts/Lato/lato-regular.ttf differ diff --git a/fonts/Lato/lato-regular.woff b/fonts/Lato/lato-regular.woff new file mode 100755 index 0000000..ae1307f Binary files /dev/null and b/fonts/Lato/lato-regular.woff differ diff --git a/fonts/Lato/lato-regular.woff2 b/fonts/Lato/lato-regular.woff2 new file mode 100755 index 0000000..3bf9843 Binary files /dev/null and b/fonts/Lato/lato-regular.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100755 index 0000000..79dc8ef Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100755 index 0000000..df5d1df Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100755 index 0000000..6cb6000 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100755 index 0000000..7059e23 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100755 index 0000000..2f7ca78 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100755 index 0000000..eb52a79 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100755 index 0000000..f815f63 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100755 index 0000000..f2c76e5 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab.eot b/fonts/RobotoSlab/roboto-slab.eot new file mode 100755 index 0000000..2f7ca78 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab.eot differ diff --git a/fonts/fontawesome-webfont.eot b/fonts/fontawesome-webfont.eot new file mode 100755 index 0000000..e9f60ca Binary files /dev/null and b/fonts/fontawesome-webfont.eot differ diff --git a/fonts/fontawesome-webfont.svg b/fonts/fontawesome-webfont.svg new file mode 100755 index 0000000..855c845 --- /dev/null +++ b/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/fontawesome-webfont.ttf b/fonts/fontawesome-webfont.ttf new file mode 100755 index 0000000..35acda2 Binary files /dev/null and b/fonts/fontawesome-webfont.ttf differ diff --git a/fonts/fontawesome-webfont.woff b/fonts/fontawesome-webfont.woff new file mode 100755 index 0000000..400014a Binary files /dev/null and b/fonts/fontawesome-webfont.woff differ diff --git a/fonts/fontawesome-webfont.woff2 b/fonts/fontawesome-webfont.woff2 new file mode 100755 index 0000000..4d13fc6 Binary files /dev/null and b/fonts/fontawesome-webfont.woff2 differ diff --git a/glossary/index.html b/glossary/index.html new file mode 100755 index 0000000..13011b1 --- /dev/null +++ b/glossary/index.html @@ -0,0 +1,337 @@ + + + + + + + + + + + + 术语表 - Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ +

术语表

+
+

本书术语表按术语首次介绍章节排序。

+
+

Java轻量级游戏库(Lightweight Java Game Library,LWJGL): OpenGL、OpenCL、OpenAL和Vulkan对Java平台的原生绑定,常用于开发游戏。

+

GLFW: 为OpenGL、OpenGL ES和Vulkan提供的跨平台窗口与输入库。

+

帧率(Frames Per Second,FPS): 以帧称为单位的位图图像连续出现在显示器上的频率(速率),单位为Hz或FPS,通俗来讲就是每秒出现在屏幕上的画面数。

+

定长游戏循环(Fixed Step Game Loop): 以固定时间周期更新的游戏循环。

+

垂直同步(Vertical Synchronization): 避免因为游戏运行速度过快导致的画面撕裂现象。

+

图形管线(Graphics Pipeline): 又称渲染管线(Rendering Pipeline),是将三维表示映射到二维屏幕的一系列步骤。

+

固定管线(Fixed-function Pipeline): 固定管线在绘制过程中定义了一组固定的操作步骤,程序员被每一步骤可用的函数集约束,可以使用的效果和可进行的操作受到API的限制,但是这些功能的实现是固定的,并且不能修改。

+

可编程管线(Programmable Pipeline): 可编程管线使组成图形管线的不同步骤可以通过使用一组叫做着色器的特定程序来控制或编程。

+

着色器(Shader): 用于控制图形管线不同阶段的特定程序。

+

顶点(Vertex): 用于描述二维或者三维空间中的点的数据结构。

+

顶点缓冲区(Vertex Buffer): 使用顶点数组来包装所有需要渲染的顶点的数据结构,并使这些数据能够在图形管线的着色器中使用。

+

顶点着色器(Vertex Shader): 着色器之一,用于计算每个顶点到屏幕空间中的投影位置。

+

几何处理阶段(Geometry Processing): 图形管线阶段之一,此阶段将由顶点着色器变换的顶点连接成三角形。

+

三角形(Triangle): 显卡的基本工作单元之一,最简单的二维几何形状。

+

光栅化(Rasterization): 图形管线阶段之一,此阶段将几何处理阶段生成的三角形剪辑并将其转换为像素大小的片元。

+

片元处理阶段(Fragment Processing): 图形管线阶段之一,生成写入到帧缓冲区的像素的最终颜色。

+

片元(Fragment): 组成帧的最小单位,通常相当于一个像素的大小,可认为其即是像素(Pixel)。

+

片元着色器(Fragment Shader): 着色器之一,用于生成写入到帧缓冲区的像素的最终颜色。

+

帧缓冲区(Framebuffer): 用于储存图形管线的输出,由多个像素组成的数据结构。

+

OpenGL着色器语言(GLSL): 用于编写OpenGL着色器的类C语言。

+

齐次坐标(Homogeneous Coordinates): 齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示。

+

顶点缓冲对象(Vertex Buffer Object,VBO): 显存中存储顶点或其他数据的内存缓冲区。

+

顶点数组对象(Vertex Array Object,VAO): 用于储存一个或多个顶点缓冲对象的对象,便于使用显卡中的储存的数据。

+

四边形(Quad): 可由两个三角形组成的几何形状,由四个顶点组成。

+

网格(Mesh): 游戏开发中常见概念之一,通常用于包装顶点数组对象(VAO),储存模型的位置、顶点索引、颜色等数据。

+

索引缓冲区(Index Buffer): 用于指定顶点绘制顺序的缓冲区。

+

投影矩阵(Projection Matrix): 将顶点从三维世界空间投影到二维屏幕空间的矩阵。

+

截锥体(Frustum): 三维几何体的一种,又称为棱台。在游戏引擎中通常为四棱台。

+

矩阵(Matrix): 矩阵是以行和列排列的二维数组,在游戏引擎中通常用于变换三维空间中的点。

+

宽高比(Aspect Ratio): 高度与宽度之比。

+

JOML(Java OpenGL Math Library): Java OpenGL 数学库,提供了对OpenGL的一系列常用数学实体的封装。

+

Uniform: Uniform是着色器可以使用的全局的GLSL变量。

+

位移(Translation): 在三个轴中的任意一个轴上移动一个物体。

+

旋转(Rotation): 按任意一个轴旋转物体任意角度。

+

缩放(Scale): 调整物体的大小。

+

变换(Transformation): 位移、旋转和缩放三者的统称。

+

世界矩阵(World Matrix): 将物体或模型从模型坐标空间变换到世界坐标空间的矩阵,由位移矩阵、旋转矩阵和缩放矩阵相乘所得。

+

位移矩阵(Translation Matrix): 可对三维坐标进行位移变换的矩阵。

+

旋转矩阵(Rotation Matrix): 可对三维坐标进行旋转变换的矩阵。

+

缩放矩阵(Scale Matrix): 可对三维坐标进行缩放变换的矩阵。

+

深度测试(Depth Test): 对片元进行深度测试,丢弃距离屏幕较远的片元。

+

纹理(Texture): 用于绘制模型的像素的图像。

+

多级渐远纹理(Mipmap): 对于距离屏幕较远的像素,将使用细节较低的纹理进行绘制。多级渐远纹理可解决由于距离问题而导致的远处像素闪烁问题。

+

纹理坐标(Texture Coordinate): 通常只有两个分量,用于指定片元在纹理上的位置。

+

纹理单元(Texture Unit): 用于储存纹理的空间。

+

取样器(Sampler): Uniform类型之一,用于指定被取样的纹理。

+

混合(Blend): OpenGL功能之一,用于渲染透明或半透明物体,将其片元与其后片元的颜色进行混合。

+

摄像机(Camera): 用于自由观察三维世界的功能。OpenGL中不存在此功能和概念,移动摄像机实际上是将场景中的所有物体向相反的方向移动。

+

翻滚(Roll): 欧拉角(Euler Angles)之一,用于描述物体沿其纵轴旋转的角度。

+

俯仰(Pitch): 欧拉角之一,用于描述物体沿其横轴旋转的角度。

+

偏航(Yaw): 欧拉角之一,用于描述物体沿其竖轴旋转的角度。

+

观察矩阵(View Matrix): 用于将顶点向摄像机所在位置的相反方向和旋转状态变换的矩阵。

+

模型观察矩阵(Model View Matrix): 是观察矩阵与世界矩阵组合,可将模型空间的坐标变换到摄像机空间中的坐标,用于简化计算。

+

OBJ(.obj): Wavefront Technologies开发的一种几何定义开放文件格式,现已被广泛采用。

+

法线(Normal): 一个平面的法线是一个垂直于该平面的向量。

+

归一化(Normalize): 使向量的长度(或模长)为1。

+

顶点法线(Vertex Normal): 一个顶点的法线是其参与组成的三角形的面的法线的组合,其长度仍为1。

+

点光源(Point Light): 这种光模拟的是一个由点向空间各个方向均匀发射的光源。

+

聚光源(Spot Light): 这种光模拟从空间中的点发射的光源,但不是向所有方向上发射,而是限定在了一个锥形方向上。

+

平行光(Directional Light): 这种光模拟了太阳光,3D场景中的所有物体都会受到来自特定方向的平行光线的照射。无论物体是近抑或是远,光线总是以相同角度照射在物体上。

+

环境光(Ambient Light): 这种光来自空间的任何方向,并以相同的强度照亮所有物体。

+

漫反射(Diffuse Reflectance): 考虑到面向光源的表面更亮。

+

镜面反射(Specular Reflectance): 模拟光线在抛光或金属表面上的反射。

+

HUD(Heads-Up Display): 平视显示器,常用于飞机上将重要信息投影于挡风玻璃或头盔上,以便飞行员同时查看前方情况和飞行信息。现用于游戏中,将游戏中的重要信息显示于三维场景前。

+

正交投影(Orthographic Projection,Orthogonal Projection): 也称为正投影,正射投影。指不受近大远小影响的将三维物体投影到二维平面上的投影方式。

+

混合(Blend): 实现3D渲染中物体透明的一个功能,启用透明将允许使用Alpha分量混合颜色。

+

天空盒(Sky Box): 包裹3D场景并以摄像机位置为原点的巨大立方体,将纹理渲染在该立方体内部,以产生一种看到没有边缘的场景的错觉。

+

立方体映射(Cube Mapping): 将纹理分为六个部分,将各部分映射到立方体上。

+

面剔除(Face Culling): 剔除无用或看不到的面,以提升性能的OpenGL功能。

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + « Previous + + + +
+ + + + + + + + + diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100755 index 0000000..e85006a Binary files /dev/null and b/img/favicon.ico differ diff --git a/index.html b/index.html new file mode 100755 index 0000000..1cd12a1 --- /dev/null +++ b/index.html @@ -0,0 +1,280 @@ + + + + + + + + + + + + Lwjglbook中文翻译 + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+
    +
  • Docs »
  • + + + +
  • 《用LWJGL 3开发3D游戏(3D Game Development with LWJGL 3)》中文翻译
  • +
  • + + Edit on GitHub + +
  • +
+ +
+
+ +
+
+ +

《用LWJGL 3开发3D游戏(3D Game Development with LWJGL 3)》中文翻译

+

本书原介绍

+

这本在线电子书主要讲解使用LWJGL 3库开发3D游戏。

+

LWJGL是一个Java库,提供了访问OpenGL、OpenAL和OpenCL的接口。这个库可以在使用高性能的OpenGL的同时,使用Java语言开发。

+

我最初的目标是学习使用OpenGL开发3D游戏所涉及的技术。所需资料都可在网上找到,但是没有人将其整理,有时很难找到,甚至有些资料不完整或存在误导。

+

我开始收集一些资料,编写一些示例,并决定以电子书的形式整理这些资料。

+

源代码

+

本书中所有的源码示例均上传至GitHub.

+

本书英文原文地址GitBook GitHub.

+

许可证

+

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

+

所有的源代码的许可协议均为 Apache v2.0

+

原作者

+

Antonio Hernández Bejarano

+

支持作者

+

如果你喜欢这本书,请Star并且分享它。如果你想捐助作者,你可以通过下面的按钮捐助:

+

Paypal

+

特别感谢

+

感谢所有提供了指正、意见和建议的读者。

+

译者的话

+

本书是译者为了学习LWJGL3和OpenGL而翻译的。限于译者的能力,本译本中如有不妥之处欢迎各位读者指正。

+

如果你喜欢本译本,请Star并分享它。如果你想捐助译者,请通过下面的链接捐助:爱发电

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + + GitHub + + + + + +
+ + + + + + + + + + + diff --git a/js/jquery-2.1.1.min.js b/js/jquery-2.1.1.min.js new file mode 100755 index 0000000..e5ace11 --- /dev/null +++ b/js/jquery-2.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("