Deployed 7ff35d0 with MkDocs version: 1.2.2

This commit is contained in:
Mouse
2021-09-27 13:57:47 +08:00
commit 306a444cc6
288 changed files with 23754 additions and 0 deletions

0
.nojekyll Normal file
View File

327
01-first-steps/index.html Executable file
View File

@@ -0,0 +1,327 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>事前准备 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u4e8b\u524d\u51c6\u5907";
var mkdocs_page_input_path = "01-first-steps.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">事前准备</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>事前准备</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/01-first-steps.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="first-steps">事前准备First Steps</h1>
<p>在本书中我们将学习开发3D游戏所涉及的主要技术。本书将使用Java语言和Java轻量级游戏库(<a href="http://www.lwjgl.org/">LWJGL</a>)来编写示例代码。LWJGL库允许我们访问底层的API应用程序接口例如OpenGL。</p>
<p>LWJGL是一个底层的API它像一个OpenGL包装库。如果你是想在短时间内开始制作一个3D游戏那么你也许该考虑其它选择比如使用引擎[JmonkeyEngine]。使用LWJGL这个底层API在你收到成效之前需要了解许多概念并且编写大量的代码。这样做的好处是你可以更好的理解3D图形渲染并且可以更好的控制它。</p>
<p>在上文说到在本书中我们将使用Java更确切来说我们将使用Java 10。所以你需要从Oracle的页面下载对应版本的JDK。请选择适合你的操作系统的安装程序。本书假定你对Java语言有一定的了解。</p>
<p>如果你需要一个可以运行示例代码的Java IDE集成开发环境你可以下载为Java 10提供良好支持的IntelliJ IDEA。由于Java 10仅支持64位的平台记得下载64位版本的IntelliJ。IntelliJ提供有一个免费且开源的社区版你可以在这里下载它 <a href="https://www.jetbrains.com/idea/download/" title="Intellij">https://www.jetbrains.com/idea/download/</a></p>
<p><img alt="IntelliJ" src="../_static/01/intellij.png" /></p>
<p>为了构建示例代码,我们将使用<a href="https://maven.apache.org/">Maven</a>。Maven已经集成在大多数IDE中你可以在IDE中直接打开不同章节的示例代码。只要打开了示例代码所在的文件夹IntelliJ就会检测到它是一个Maven项目。</p>
<p><img alt="Maven项目" src="../_static/01/maven_project.png" /></p>
<p>Maven基于一个名为<code>pom.xml</code>Project Object Model项目对象模型的XML文件来构建项目它管理了项目的依赖需要使用的库和在构建过程中需要执行的步骤。Maven遵循约定高于配置的原则即如果你遵守标准的项目结构和命名约定就不需要在配置文件中明确地声明源文件在哪里或者应该在哪里编译类。</p>
<p>本书不是一个Maven教程如果有需要请在网上搜索Maven的相关资料。源代码文件夹定义了一个父项目它声明需要使用的插件并且声明需要使用的库的版本。</p>
<p>LWJGL 3.1在项目构建上有一些变化。现在它变得更加模块化我们可以有选择地使用类库而不是导入一个巨大的Jar文件。但这是有代价的你需要仔细地逐个指定依赖关系。不过<a href="https://www.lwjgl.org/download">LWJGL下载</a>页面提供了一个为您生成POM文件的脚本。在本例中我们将只使用GLFW和OpenGL。你可以在源代码中查看我们的POM文件。</p>
<p>LWJGL平台依赖库已经可以为你的操作系统自动解压本地库因此不需要使用其他插件例如<code>mavennatives</code>。我们只需要配置三个Profile来设置LWJGL所处的操作系统。Profile将会为Windows、Linux和Mac OS系列设置正确的值。</p>
<pre><code class="language-xml"> &lt;profiles&gt;
&lt;profile&gt;
&lt;id&gt;windows-profile&lt;/id&gt;
&lt;activation&gt;
&lt;os&gt;
&lt;family&gt;Windows&lt;/family&gt;
&lt;/os&gt;
&lt;/activation&gt;
&lt;properties&gt;
&lt;native.target&gt;natives-windows&lt;/native.target&gt;
&lt;/properties&gt;
&lt;/profile&gt;
&lt;profile&gt;
&lt;id&gt;linux-profile&lt;/id&gt;
&lt;activation&gt;
&lt;os&gt;
&lt;family&gt;Linux&lt;/family&gt;
&lt;/os&gt;
&lt;/activation&gt;
&lt;properties&gt;
&lt;native.target&gt;natives-linux&lt;/native.target&gt;
&lt;/properties&gt;
&lt;/profile&gt;
&lt;profile&gt;
&lt;id&gt;OSX-profile&lt;/id&gt;
&lt;activation&gt;
&lt;os&gt;
&lt;family&gt;mac&lt;/family&gt;
&lt;/os&gt;
&lt;/activation&gt;
&lt;properties&gt;
&lt;native.target&gt;natives-osx&lt;/native.target&gt;
&lt;/properties&gt;
&lt;/profile&gt;
&lt;/profiles&gt;
</code></pre>
<p>在每个项目中LWJGL平台依赖项将使用配置文件为当前平台设置的正确的属性。</p>
<pre><code class="language-xml"> &lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-platform&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;classifier&gt;${native.target}&lt;/classifier&gt;
&lt;/dependency&gt;
</code></pre>
<p>此外每个项目可生成一个可运行的Jar一种可以通过输入<code>java -jar name_of_the_jar.jar</code>就可运行的Jar这是通过使用Maven的<code>maven-jar-plugin</code>插件实现的。该插件创建了一个含有<code>MANIFEST.MF</code>文件的Jar并且文件内有指定的值该文件最重要的属性就是<code>Main-Class</code>,它指明了程序的入口。此外,所有的依赖库都被设置在该文件的<code>Class-Path</code>属性中。要在另一台计算机上运行它你只需要复制位于目标目录下的主Jar文件和Lib目录包括其中所有的Jar文件</p>
<p>Jar文件储存着LWJGL类和本地库。LWJGL还将负责提取它们并将它们添加到JVM的库路径中。</p>
<p>本章的源代码是LWJGL网站(<a href="https://www.lwjgl.org/guide">https://www.lwjgl.org/guide</a>)提供的的入门示例你可以看到我们没有使用Swing或JavaFX作为我们的GUI库。我们使用的是<a href="https://www.glfw.org/">GLFW</a>它是一个用于处理GUI组件窗口等和事件按键按下、鼠标移动等并且与OpenGL上下文进行简单连接的库。此前版本的LWJGL提供了一个自定义GUI API但在LWJGL 3中GLFW是首选的窗口API。</p>
<p>示例源码很简单并有着良好的文档,所以我们不会在书中再次说明。</p>
<p>如果你正确地配置了环境,你应该能够运行它并看到一个有红色背景的窗口。</p>
<p><img alt="Hello World" src="../_static/01/hello_world.png" /></p>
<p><strong>本书中源代码发布于<a href="https://github.com/lwjglgamedev/lwjglbook"><strong>GitHub</strong></a></strong></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../02-the-game-loop/" class="btn btn-neutral float-right" title="游戏循环">Next <span class="icon icon-circle-arrow-right"></span></a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../02-the-game-loop/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

492
02-the-game-loop/index.html Executable file
View File

@@ -0,0 +1,492 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>游戏循环 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6e38\u620f\u5faa\u73af";
var mkdocs_page_input_path = "02-the-game-loop.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">游戏循环</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#osx">平台差异OSX</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>游戏循环</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/02-the-game-loop.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="the-game-loop">游戏循环The Game Loop</h1>
<p>在本章中,我们将通过创建游戏循环来开始开发游戏引擎。游戏循环是每个游戏的核心部分,它基本上是一个无休止的循环,负责周期地处理用户的输入、更新游戏状态和渲染图像到屏幕上。</p>
<p>下述代码片段展示了游戏循环的结构:</p>
<pre><code class="language-java">while (keepOnRunning) {
handleInput();
updateGameState();
render();
}
</code></pre>
<p>那么,这就完了吗?我们已经完成游戏循环了吗?显然还没有,上述代码中有很多缺陷。首先,游戏循环运行的速度将取决于运行它的计算机。如果计算机足够快,用户甚至看不到游戏中发生了什么。此外,这个游戏循环将消耗所有的计算机资源。</p>
<p>因此我们需要游戏循环独立于运行的计算机尝试以恒定速率运行。假设我们希望游戏以每秒50帧50 Frames Per Second50 FPS的恒定速率运行那么游戏循环代码可能是这样的</p>
<pre><code class="language-java">double secsPerFrame = 1.0d / 50.0d;
while (keepOnRunning) {
double now = getTime();
handleInput();
updateGameState();
render();
sleep(now + secsPerFrame getTime());
}
</code></pre>
<p>这个游戏循环很简单可以用于一些游戏但是它也存在一些缺陷。首先它假定我们的更新和渲染方法适合以50FPS<code>secsPerFrame</code>等于20毫秒的速率更新。</p>
<p>此外,我们的计算机可能会优先考虑暂停游戏循环运行一段时间,以运行其他的任务。因此,我们可能会在非常不稳定的时间周期更新游戏状态,这是不符合游戏物理的要求的。</p>
<p>最后线程休眠的时间精度仅仅只有0.1秒,所以即使我们的更新和渲染方法没有消耗时间,也不会以恒定的速率更新。所以,如你所见,问题没那么简单。</p>
<p>在网上你可以找到大量的游戏循环的变种。在本书中,我们将用一个不太复杂的,在大多数情况下都能正常工作的方法。我们将用的方法通常被称为<strong>定长游戏循环</strong>Fixed Step Game Loop</p>
<p>首先,我们可能想要单独控制游戏状态被更新的周期和游戏被渲染到屏幕的周期。为什么要这么做?因为以恒定的速率更新游戏状态更为重要,特别是如果使用物理引擎。相反,如果渲染没有及时完成,在运行游戏循环时渲染旧帧也是没有意义的,我们可以灵活地跳过某些帧。</p>
<p>让我们看看现在的游戏循环是什么样的:</p>
<pre><code class="language-java">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 &gt;= secsPerUpdate) {
updateGameState();
steps -= secsPerUpdate;
}
render();
sync(loopStartTime);
}
</code></pre>
<p>使用这个游戏循环,我们可以在固定的周期更新游戏状态。但是如何避免耗尽计算机资源,使它不连续渲染呢?这在<code>sync</code>方法中实现:</p>
<pre><code class="language-java">private void sync(double loopStartTime) {
float loopSlot = 1f / 50;
double endTime = loopStartTime + loopSlot;
while(getTime() &lt; endTime) {
try {
Thread.sleep(1);
} catch (InterruptedException ie) {}
}
}
</code></pre>
<p>那么上述方法做了什么呢?简而言之,我们计算游戏循环迭代应该持续多长时间(它被储存在<code>loopSlot</code>变量中),休眠的时间取决于在循环中花费的时间。但我们不做一整段时间的休眠,而是进行一些小的休眠。这允许其他任务运行,并避免此前提到的休眠准确性问题。接下来我们要做的是:
1. 计算应该退出这个方法的时间(这个变量名为<code>endTime</code>),并开始游戏循环的另一次迭代。
2. 比较当前时间和结束时间如果没有到达结束时间就休眠1毫秒。</p>
<p>现在是构建代码库以便开始编写游戏引擎的第一个版本的时候了。但在此之前,我们来讨论一下控制渲染速率的另一种方法。在上述代码中,我们做微休眠是为了控制需要等待的时间。但我们可以选择另一种方法来限制帧率。我们可以使用<strong>垂直同步</strong>Vertical Synchronization垂直同步的主要目的是避免画面撕裂。什么是画面撕裂这是一种显示现象当正在渲染时我们更新图像储存区导致屏幕的一部分显示先前的图像而屏幕的另一部分显示正在渲染的图像。如果启用垂直同步当GPU中的数据正被渲染到屏幕上时我们不会向GPU发送数据。</p>
<p>当开启垂直同步时,我们将与显卡的刷新率同步,显卡将以恒定的帧率渲染。用下述一行代码启用它:</p>
<pre><code class="language-java">glfwSwapInterval(1);
</code></pre>
<p>有了上述代码,就意味着至少在一个屏幕更新被绘制到屏幕之前,我们必须等待。事实上我们不是直接绘制到屏幕上。相反,我们将数据储存在缓冲区中,然后用下面的方法交换它:</p>
<pre><code class="language-java">glfwSwapBuffers(windowHandle);
</code></pre>
<p>因此如果启用垂直同步我们就可以实现稳定的帧率而不需要进行微休眠来检查更新时间。此外帧率将与设备的显卡刷新率相匹配也就是说如果它设定为60Hz60FPS那么我们就有60FPS。我们可以通过在<code>glfwSwapInterval</code>方法中设置高于1的数字来降低这个速率如果设置为2将得到30FPS</p>
<p>让我们整理一下源代码。首先我们将把所有的GLFW窗口初始化代码封装在一个名为<code>Window</code>的类中,传递一些基本的参数(如标题和大小)。<code>Window</code>类还提供一个方法以便在游戏循环中检测按下的按键:</p>
<pre><code class="language-java">public boolean isKeyPressed(int keyCode) {
return glfwGetKey(windowHandle, keyCode) == GLFW_PRESS;
}
</code></pre>
<p>除了有初始化代码以外,<code>Window</code>类还需要知道窗口大小被调整。因此需要设置一个回调方法,在窗口大小被调整时调用它。回调方法将接收帧缓冲区(渲染区域,简单来说就是显示区域)的以像素为单位的宽度和高度。如果希望得到帧缓冲区的宽度和高度,你可以使用<code>glfwSetWindowSizeCallback</code>方法。屏幕坐标不一定对应像素例如具有视网膜显示屏Retina Display的Mac设备。因为我们将在进行OpenGL调用时使用这些信息所以要注意像素不在屏幕坐标中你可以通过GLFW的文档了解更多信息。</p>
<pre><code class="language-java">// 设置调整大小回调
glfwSetFramebufferSizeCallback(windowHandle, (window, width, height) -&gt; {
Window.this.width = width;
Window.this.height = height;
Window.this.setResized(true);
});
</code></pre>
<p>我们还将创建一个<code>Renderer</code>类,它将处理我们游戏的渲染。现在,它仅会有一个空的<code>init</code>方法,和另一个用预设颜色清空屏幕的方法:</p>
<pre><code class="language-java">public void init() throws Exception {
}
public void clear() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
</code></pre>
<p>然后我们将创建一个名为<code>IGameLogic</code>的接口,它封装了我们的游戏逻辑。这样,我们就可以让游戏引擎在不同的游戏上重复使用。该接口将具有获取输入、更新游戏状态和渲染游戏内容的方法。</p>
<pre><code class="language-java">public interface IGameLogic {
void init() throws Exception;
void input(Window window);
void update(float interval);
void render(Window window);
}
</code></pre>
<p>然后我们将创建一个名为<code>GameEngine</code>的类,它将包含我们游戏循环的代码,该类将实现储存游戏循环:</p>
<pre><code class="language-java">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;
//...
}
</code></pre>
<p><code>vSync</code>参数允许我们选择是否启用垂直同步。你可以看到我们实现了<code>GameEngine</code>类的<code>run</code>方法,其中包括游戏循环:</p>
<pre><code class="language-java">@Override
public void run() {
try {
init();
gameLoop();
} catch (Exception excp) {
excp.printStackTrace();
}
}
</code></pre>
<p><code>GameEngine</code>类提供了一个<code>run</code>方法该方法将执行初始化任务并运行游戏循环直到我们关闭窗口。关于线程需要注意的一点是GLFW需要从主线程初始化事件的轮询也应该在该线程中完成。因此我们将在主线程中执行所有内容而不是为游戏循环创建单独的线程。</p>
<p>在源代码中,你将看到我们创建了其他辅助类,例如<code>Timer</code>(它将提供用于计算已经过的时间的实用方法),并在游戏循环逻辑中使用它们。</p>
<p><code>GameEngine</code>类只是将<code>input</code><code>update</code>方法委托给<code>IGameLogic</code>实例。在<code>render</code>方法中,它也委托给<code>IGameLogic</code>实例并更新窗口。</p>
<pre><code class="language-java">protected void input() {
gameLogic.input(window);
}
protected void update(float interval) {
gameLogic.update(interval);
}
protected void render() {
gameLogic.render(window);
window.update();
}
</code></pre>
<p>在程序的入口,含有<code>main</code>方法的类只会创建一个<code>GameEngine</code>实例并运行它。</p>
<pre><code class="language-java">public class Main {
public static void main(String[] args) {
try {
boolean vSync = true;
IGameLogic gameLogic = new DummyGame();
GameEngine gameEng = new GameEngine(&quot;GAME&quot;,
600, 480, vSync, gameLogic);
gameEng.run();
} catch (Exception excp) {
excp.printStackTrace();
System.exit(-1);
}
}
}
</code></pre>
<p>最后,在本章中我们只需要创建一个简单的游戏逻辑类。它只会在按下上或下键时,变亮或变暗窗口的颜色缓冲区的清空颜色,<code>render</code>方法将会用这个颜色清空窗口的颜色缓冲区。</p>
<pre><code class="language-java">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 &gt; 1) {
color = 1.0f;
} else if ( color &lt; 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();
}
}
</code></pre>
<p><code>render</code>方法中,当窗口大小被调整时,我们接收通知,以便更新视口将坐标中心定位到窗口的中心。</p>
<p>创建的类层次结构将帮助我们将游戏引擎代码与具体的游戏代码分开。虽然现在可能看起来没有必要,但我们已将每个游戏的通用代码,从具体的游戏的逻辑、美术作品和资源中分离出来,以便重用游戏引擎。在此后的章节中,我们需要重构这个类层次结构,因为我们的游戏引擎变得更加复杂。</p>
<h2 id="osx">平台差异OSX</h2>
<p>你可以运行上面的代码在Windows或Linux上但我们仍需要为OS X平台做一些修改。正如GLFW文档中所描述的</p>
<blockquote>
<p>目前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以成功创建环境。</p>
</blockquote>
<p>因此,为了支持在此后章节中介绍的特性,我们需要将下述代码添加到<code>Window</code>类创建窗口代码之前:</p>
<pre><code class="language-java"> 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);
</code></pre>
<p>这将使程序使用OpenGL 3.2到4.1之间的最高版本。如果没有上述代码就会使用旧版本的OpenGL。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../03-a-brief-about-coordinates/" class="btn btn-neutral float-right" title="坐标简介">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../01-first-steps/" class="btn btn-neutral" title="事前准备"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../01-first-steps/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../03-a-brief-about-coordinates/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>坐标简介 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u5750\u6807\u7b80\u4ecb";
var mkdocs_page_input_path = "03-a-brief-about-coordinates.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">坐标简介</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>坐标简介</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/03-a-brief-about-coordinates.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="a-brief-about-coordinates">坐标简介A brief about coordinates</h1>
<p>本章中我们将讨论坐标和坐标系Coordinate System尝试以简单的方式介绍一些基本的数学概念为后面章节将要介绍的技术和内容提供帮助。我们将会将一些内容简化以易于学习但牺牲了准确性。</p>
<p>我们通过指定坐标来确定物体在空间中的位置。联想地图,通过在地图上指定纬度和经度来确定一个点。只需一对数字,就可以精确地确认一个点,这对数字就是点坐标(实际上有些复杂,需要更多的数据才能在地图上确定一个点,因为地图是一个不完美的椭圆球体(地球)的投影,但这是一个很好的类比)。</p>
<p>坐标系是一个系统,它使用一个或多个数字,即一个或多个分量来唯一地确定一个点的位置。存在着多种不同的坐标系(如笛卡尔坐标系,极坐标系等),并且可以将坐标从一个坐标系转换到另一个坐标系。我们将使用笛卡尔坐标系。</p>
<p>在二维笛卡尔坐标系中坐标由两个数字定义它们表示到两个相互垂直的X轴和Y轴间距离。</p>
<p><img alt="笛卡尔坐标系" src="../_static/03/cartesian_coordinate_system.png" /> </p>
<p>继续类比地图坐标系定义一个原点。对于地理坐标来说原点被设置为赤道和零度经线交叉的点。根据原点设置的位置特定点的坐标是不同的。坐标系也可以定义轴的方向。在上图中X坐标随着点向右移动而增加Y坐标随着点向上移动而增加。但我们也可以定义一个与笛卡尔坐标系不同的具有不同的轴取向的坐标系我们将得到不同的坐标。</p>
<p><img alt="非传统笛卡尔坐标系" src="../_static/03/alt_cartesian_coordinate_system.png" /></p>
<p>如你所见,我们需要定义一些参数,例如原点和轴方向,以便使构成坐标的数字对有恰当的含义。为了使用一组坐标,我们必须使用对应的坐标系,好消息是可以通过平移和旋转将坐标从一个坐标系转换到另一个坐标系。</p>
<p>如果要处理三维坐标我们需要增加一个轴即Z轴。三维坐标将由三个数字(x, y, z)构成。</p>
<p><img alt="三维笛卡尔坐标系" src="../_static/03/3d_cartesian_coordinate_system.png" /></p>
<p>在二维笛卡尔坐标系中,只要轴相互垂直,我们就可以改变三维坐标系中的轴的方向。下图显示另一个三维坐标系。</p>
<p><img alt="非传统三维笛卡尔坐标系" src="../_static/03/alt_3d_cartesian_coordinate_system.png" /></p>
<p>三维坐标可分为左手系和右手系两种类型。你怎么知道它是什么类型的用你的手在你的拇指和食指之间形成一个“L”中指应指向垂直于其他两个手指的方向。拇指应该指向X轴的正方向食指应该指向Y轴的正方向而中指应该指向Z轴的正方向。如果你能用左手做到那么它就是左手系如果你需要用右手那它就是右手系。</p>
<p><img alt="右手系vs左手系" src="../_static/03/righthanded_lefthanded.png" /> </p>
<p>二维坐标系是相同的,因为通过旋转,我们可以从一个坐标系转换到另一个坐标系。但是,三维坐标系并不都是相同的。如果它们可以使用相同的手来表示,换句话说,如果两者都是左手系或者右手系,那么就能通过旋转从一个坐标系转换到另一个坐标系。</p>
<p>现在已经明确了一些基本的概念,让我们来讲解一些在处理三维图形时常用的术语。当我们在此后的章节中解释如何渲染三维模型时,将看到我们将使用不同的三维坐标系,这是因为每个坐标系都有不同的设定,有不同的目的。一组坐标是没有意义的,除非明确它是某个坐标系的坐标。当你看到坐标(40.438031, -3.676626)时,你可能会有一个大胆的想法。但是如果我说它是几何坐标(经度和纬度)时,你就会发现它是马德里某个地方的坐标。</p>
<p>当我们加载三维物体时我们将得到一组三维坐标。这些坐标在被称为物体坐标系Object Coordinate Space的三维坐标系中表达。当建模师在设计这些三维模型的时候他们对该模型将显示的三维场景毫不知情因此只能使用与模型相关的坐标系来定义坐标。</p>
<p>当要绘制一个三维场景时,所有的三维物体将与被称为世界空间坐标系的坐标系对应。我们需要将三维物体的坐标系转换到世界坐标系。一些物体需要旋转、拉伸、放大和转换,以便在三维场景中能够正确地显示。</p>
<p>我们还需要限制所显示的三维空间的范围,例如移动摄像机穿梭在三维空间中。然后我们需要将世界空间坐标系的坐标转换到摄像机或观察空间坐标系。最后,这些坐标需要转换为二维的屏幕坐标,所以我们需要将三维观察坐标投影到二维屏幕空间坐标系。</p>
<p>下图展示了OpenGL坐标系Z轴垂直于屏幕坐标在-1和+1之间。</p>
<p><img alt="OpenGL坐标系" src="../_static/03/opengl_coordinates.png" /> </p>
<p>如果你不能清楚地理解这些概念,别担心。在下一章中,它们将用实例表现出来。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../04-rendering/" class="btn btn-neutral float-right" title="渲染">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../02-the-game-loop/" class="btn btn-neutral" title="游戏循环"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../02-the-game-loop/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../04-rendering/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

599
04-rendering/index.html Executable file
View File

@@ -0,0 +1,599 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>渲染 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6e32\u67d3";
var mkdocs_page_input_path = "04-rendering.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">渲染</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>渲染</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/04-rendering.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="rendering">渲染Rendering</h1>
<p>在本章中我们将学习用OpenGL渲染场景时要做的事项。如果你已经习惯了OpenGL的旧版本习惯了使用固定管线你可能会跳过这一章不想知道为什么它需要这么复杂。它其实更简单、更灵活你只需要给它一个表现的机会。现代OpenGL使你只需考虑一个问题这可以使你以更合理的方式组织代码和开发。</p>
<p>将三维表示映射到二维屏幕的一系列步骤被统称为图形管线Graphics Pipeline。OpenGL最初的版本使用了一个被称为固定管线Fixed-function Pipeline的模型。该模型在绘制过程中定义了一组固定的操作步骤程序员被每一步骤可用的函数集约束可以使用的效果和可进行的操作受到API例如“设置雾”或“添加光照”的限制但是这些功能的实现是固定的并且不能修改。</p>
<p>图形管线由以下操作步骤组成:</p>
<p><img alt="图形管线" src="../_static/04/rendering_pipeline.png" /></p>
<p>OpenGL 2.0 引入了可编程管线Programmable Pipeline的概念。在该模型中组成图形管线的不同步骤可以通过使用一组叫做着色器Shader的特定程序来控制或编程。下图简单的展示了OpenGL可编程管线</p>
<p><img alt="可编程管线" src="../_static/04/rendering_pipeline_2.png" /></p>
<p>该渲染方式最初将以顶点缓冲区为形式的一系列顶点作为输入。但是什么是顶点顶点Vertex是描述二维或者三维空间中的点的数据结构。如何描述三维空间中的一个点呢通过指定其X、Y和Z坐标。什么是顶点缓冲区顶点缓冲区Vertex Buffer是使用顶点数组来包装所有需要渲染的顶点的另一种数据结构并使这些数据能够在图形管线的着色器中使用。</p>
<p>这些顶点由顶点着色器Vertex Shader处理顶点着色器的功能是计算每个顶点到屏幕空间中的投影位置。该着色器还可以生成与颜色或纹理相关的其他输出但其主要目的还是将顶点投影到屏幕空间中即生成点。</p>
<p>几何处理阶段Geometry Processing将由顶点着色器变换的顶点连接成三角形。它依照顶点储存的顺序使用不同的模型对顶点进行分组。为什么是三角形三角形就是显卡的基本工作单元它是一个简单的几何形状可以组合和变换以构建复杂的三维场景。此阶段还可以使用特定的着色器来对顶点进行分组。</p>
<p>光栅化Rasterization阶段接收此前生成的三角形剪辑它们并将它们转换为像素大小的片元。</p>
<p>这些片元将在片元处理阶段Fragment Processing被片元着色器Fragment Shader使用以生成写入到帧缓冲区的像素的最终颜色。帧缓冲区Framebuffer是图形管线的最终输出它储存了每个像素应该被绘制到屏幕上的值。</p>
<p>注意,显卡被设计成并行处理上述所有操作,输入的数据可以并行处理以生成最终场景。</p>
<p>让我们开始编写第一个着色器程序。着色器是使用基于ANSI C的OpenGL着色器语言GLSL编写的。首先<code>resources</code>目录下创建一个名为“<code>vertex.vs</code>”(扩展名为顶点着色器英文简写)的文件,内容如下:</p>
<pre><code class="language-glsl">#version 330
layout (location=0) in vec3 position;
void main()
{
gl_Position = vec4(position, 1.0);
}
</code></pre>
<p>第一行是一个表示我们正使用的GLSL语言版本的标识符。下表是GLSL版本、与该版本匹配的OpenGL版本和使用方法来自维基百科<a href="https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions">https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions</a></p>
<table>
<thead>
<tr>
<th>GLSL版本</th>
<th>OpenGL版本</th>
<th>着色器标识符</th>
</tr>
</thead>
<tbody>
<tr>
<td>1.10.59</td>
<td>2.0</td>
<td>#version 110</td>
</tr>
<tr>
<td>1.20.8</td>
<td>2.1</td>
<td>#version 120</td>
</tr>
<tr>
<td>1.30.10</td>
<td>3.0</td>
<td>#version 130</td>
</tr>
<tr>
<td>1.40.08</td>
<td>3.1</td>
<td>#version 140</td>
</tr>
<tr>
<td>1.50.11</td>
<td>3.2</td>
<td>#version 150</td>
</tr>
<tr>
<td>3.30.6</td>
<td>3.3</td>
<td>#version 330</td>
</tr>
<tr>
<td>4.00.9</td>
<td>4.0</td>
<td>#version 400</td>
</tr>
<tr>
<td>4.10.6</td>
<td>4.1</td>
<td>#version 410</td>
</tr>
<tr>
<td>4.20.11</td>
<td>4.2</td>
<td>#version 420</td>
</tr>
<tr>
<td>4.30.8</td>
<td>4.3</td>
<td>#version 430</td>
</tr>
<tr>
<td>4.40</td>
<td>4.4</td>
<td>#version 440</td>
</tr>
<tr>
<td>4.50</td>
<td>4.5</td>
<td>#version 450</td>
</tr>
</tbody>
</table>
<p>第二行指定此着色器的输入格式。OpenGL缓冲区中的数据可以是我们想要的任何数据也就是说该语言不会强迫你传递预定义语言的任何指定数据结构。从着色器的角度来看它期望接收一个存有数据的缓冲区。它可以是一个位置一个有一些附加信息的位置或者我们想要的任何数据。顶点着色器只接收浮点数组。当填充缓冲区时我们定义要由着色器处理的缓冲区块。</p>
<p>首先需要把这些块变成对我们有意义的数据。现在规定从位置0开始我们期望接收由三个属性(X, Y, Z)组成的向量。</p>
<p>着色器有个<code>main</code>代码块就像任何C语言程序一样上述示例是非常简单的。它只是将接收到的坐标不经任何变换地返回到<code>gl_Position</code>。你现在可能想知道为什么三个属性的向量被转换成四个属性的向量(<code>vec4</code>)。这是因为<code>gl_Position</code>仅接收<code>vec4</code>类型的数据因为它是齐次坐标Homogeneous Coordinates。也就是说它希望接收到形似(X, Y, Z, W)的东西其中W代表一个额外的维度。为什么还要添加另一个维度在此后的章节中你会看到我们需要做的大部分操作都是基于向量和矩阵的。如果没有额外的维度一些操作不能组合。例如不能把旋转和位移操作组合起来。如果你想学习更多有关于这方面的知识这个额外的维度允许我们组合仿射和线性变换。你可以通过阅读《3D Math Primer for Graphics and Game Development》作者是Fletcher Dunn 和 Ian Parberry来更多地了解这一点。</p>
<p>现在来看看我们的第一个片元着色器。在<code>resources</code>目录下创建一个名为<code>fragment.fs</code>(扩展名片元着色器英文简写)的文件,内容如下:</p>
<pre><code class="language-glsl">#version 330
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 0.5, 0.5, 1.0);
}
</code></pre>
<p>该结构与我们的顶点着色器非常相似。现在,它将为每个片元设置固定的颜色。输出值被定义为第二行的<code>vec4</code>类型的<code>fragColor</code>变量。</p>
<p>现在我们已经创建了着色器,该如何使用它们呢?以下是我们要做的一系列步骤:
1. 创建OpenGL程序。
2. 载入顶点和片元着色器文件。
3. 为每个着色器创建一个新的着色器程序并指定它的类型(顶点或片元)。
4. 编译着色器。
5. 将着色器绑定到OpenGL程序上。
6. 连接程序。</p>
<p>最后着色器将会被载入到显卡中我们可以通过引用程序ID来使用它。</p>
<pre><code class="language-java">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(&quot;Could not create Shader&quot;);
}
}
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(&quot;Error creating shader. Type: &quot; + shaderType);
}
glShaderSource(shaderId, shaderCode);
glCompileShader(shaderId);
if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) {
throw new Exception(&quot;Error compiling Shader code: &quot; + 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(&quot;Error linking Shader code: &quot; + 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(&quot;Warning validating Shader code: &quot; + glGetProgramInfoLog(programId, 1024));
}
}
public void bind() {
glUseProgram(programId);
}
public void unbind() {
glUseProgram(0);
}
public void cleanup() {
unbind();
if (programId != 0) {
glDeleteProgram(programId);
}
}
}
</code></pre>
<p><code>ShaderProgram</code>类的构造函数在OpenGL中创建一个新的程序并提供添加顶点和片元着色器的方法。这些着色器被编译并绑定到OpenGL程序中。当所有的着色器都被绑定时应该调用<code>link</code>方法,来连接所有代码并验证所有操作都已正确地完成。</p>
<p>一旦着色器程序被连接,编译的顶点和片元着色器可以被释放(通过调用<code>glDetachShader</code>方法)。</p>
<p>验证是通过调用<code>glValidateProgram</code>方法完成的。此方法主要用于调试,当游戏到达生产阶段时,应将其删除。此方法将验证在<strong>当前OpenGL状态</strong>下着色器是否正确。这意味着,即使着色器是正确的,在某些情况下也可能验证失败,这是因为当前状态不够完整(一些数据可能尚未加载),无法运行着色器。因此,我们可以将错误信息输出到标准错误输出中。</p>
<p><code>ShaderProgram</code>类还提供了在渲染时激活该程序(绑定)和停止使用它(解绑)的方法。最后,它提供了一个<code>cleanup</code>方法,用于在它不再被需要时,释放所有资源。</p>
<p>既然有一个清理方法,让我们更改<code>IGameLogic</code>接口来添加一个<code>cleanup</code>方法:</p>
<pre><code class="language-java">void cleanup();
</code></pre>
<p>该方法将在游戏循环结束时调用,所以需要修改<code>GameEngine</code>类的<code>run</code>方法:</p>
<pre><code class="language-java">@Override
public void run() {
try {
init();
gameLoop();
} catch (Exception excp) {
excp.printStackTrace();
} finally {
cleanup();
}
}
</code></pre>
<p>现在我们可以在<code>Renderer</code>类的<code>init</code>方法中使用着色器来显示一个三角形。首先,我们要创建着色器程序:</p>
<pre><code class="language-java">public void init() throws Exception {
shaderProgram = new ShaderProgram();
shaderProgram.createVertexShader(Utils.loadResource(&quot;/vertex.vs&quot;));
shaderProgram.createFragmentShader(Utils.loadResource(&quot;/fragment.fs&quot;));
shaderProgram.link();
}
</code></pre>
<p>我们已经创建了一个工具类,它提供了一个从类路径中取得文件内容的方法,此方法用于取得我们的着色器代码。</p>
<p>现在我们可以把三角形定义为一组浮点数创建一个一维浮点数组它将定义三角形的顶点。如你所见数组中没有数据结构。就目前而言OpenGL无法知道该数组的结构这只是一组浮点数</p>
<pre><code class="language-java">float[] vertices = new float[]{
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
</code></pre>
<p>下图体现了在坐标系中的三角形。</p>
<p><img alt="三角形" src="../_static/04/triangle_coordinates.png" /></p>
<p>现在我们有了坐标需要把它们储存到显卡中并告诉OpenGL它的数据结构。现在将介绍两个重要的概念顶点数组对象Vertex Array ObjectVAO和顶点缓冲对象Vertex Buffer ObjectVBO。如果你对接下来的代码感到困惑请记住现在所做的是把将要绘制的模型对象的数据传递到显存中。当储存它的时候我们会得到一个ID稍后绘制时会使用它。</p>
<p>先介绍顶点缓冲对象VBOVBO只是显存中存储顶点的内存缓冲区。这是用来暂存一组用于建模三角形的浮点数的地方。如上所述OpenGL对我们的数据结构一无所知。事实上它不仅可以储存坐标还可以储存其他信息比如纹理、颜色等。</p>
<p>顶点数组对象VAO是一个对象储存一个或多个通常被称为属性列表的VBO。每个属性列表可以保存一种类型的数据位置、颜色、纹理等。在每个渲染间隔中你可以自由地储存所需的任何数据。</p>
<p>一个VAO就像是一个包装它按一组定义对储存在显卡中的数据分组。当创建一个VAO时我们得到一个ID。我们使用此ID来渲染它和使用它在创建过程中定义的数据。</p>
<p>让我们继续编写示例代码。首先要做的事就是把浮点数储存在一个<code>FloatBuffer</code>中。这主要是因为我们必须使用基于C语言的OpenGL库的接口所以必须把浮点数组转换成可以由库管理的东西。</p>
<pre><code class="language-java">FloatBuffer verticesBuffer = MemoryUtil.memAllocFloat(vertices.length);
verticesBuffer.put(vertices).flip();
</code></pre>
<p>我们使用<code>MemoryUtil</code>类来在堆外内存中创建了一个缓冲区以便OpenGL库访问它。在储存了数据调用<code>put</code>方法)之后,我们需要调用<code>flip</code>方法将缓冲区的位置重置为0也就是说我们已经完成了对它的写入。记住Java中的对象被分配在一个叫堆Heap的内存空间。堆是JVM内存中保留的一大堆内存储存在堆中的对象不能通过本地代码访问JNI这种机制使得Java不能直接调用本地代码。Java代码和本地代码直接共享内存数据的唯一方法是在Java中直接地分配内存。</p>
<p>如果你来自LWJGL的旧版本强调一些要点是很重要的。你可能注意到了我们不使用工具类<code>BufferUtils</code>,而使用<code>MemoryUtil</code>类来创建缓冲区。这是由于<code>BufferUtils</code>不是非常有效的并且仅被用于向下兼容。LWJGL3提供了两种缓冲区的管理方法</p>
<ul>
<li>自动管理缓冲区即由垃圾回收器自动回收的缓冲区。这些缓冲区适用于短暂的操作或者用于传递到GPU的数据并且不需要储存于进程内存中。这是通过使用<code>org.lwjgl.system.MemoryStack</code>实现的。</li>
<li>手动管理缓冲区。此情况下,一旦完成操作,我们需要小心地释放它们。这些缓冲区适用于长时间的操作或者大量的数据。这是通过使用<code>MemoryUtil</code>类实现的。</li>
</ul>
<p>你可以在此处查阅细节:
<a href="https://blog.lwjgl.org/memory-management-in-lwjgl-3/" title="here">https://blog.lwjgl.org/memory-management-in-lwjgl-3/</a></p>
<p>在此情况下我们的数据被发送到GPU这样可以考虑使用自动管理的缓冲区。但稍后我们将使用它们来储存可能需要手动管理的大量数据这就是使用<code>MemoryUtil</code>类的原因,因此,这就是为什么我们要在最后一个块中释放缓冲区资源。在下章中,我们将学习如何使用自动管理缓冲区。</p>
<p>现在需要创建VAO然后绑定它</p>
<pre><code class="language-java">vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
</code></pre>
<p>然后需要创建VBO绑定它并将数据输入</p>
<pre><code class="language-java">vboId = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
</code></pre>
<p>接下来是最重要的部分。我们需要定义数据结构并将其储存在VAO的属性列表中这是用下述代码完成的</p>
<pre><code class="language-java">glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
</code></pre>
<p>它的参数是:</p>
<ul>
<li>index: 指定着色器期望此数据的位置。</li>
<li>size: 指定每个顶点属性的数据数从1到4。现在我们使用三维坐标所以它应该是3。</li>
<li>type: 指定数组中每个数据的类型,现在是浮点数。</li>
<li>normalized: 指定值是否应归一化。</li>
<li>stride: 指定连续顶点数据之间的字节偏移量(稍后我们再解释)。</li>
<li>offset: 指定缓冲区中第一个数据的偏移量。</li>
</ul>
<p>在完成了VBO操作之后我们可以解除它和VAO的绑定绑定到0</p>
<pre><code class="language-java">// 解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑VAO
glBindVertexArray(0);
</code></pre>
<p>一旦完成操作,我们<strong>必须</strong>通过手动调用<code>memFree</code>方法释放由<code>FloatBuffer</code>占用的堆外内存因为Java垃圾回收不会清理分配的堆外内存。</p>
<pre><code class="language-java">if (verticesBuffer != null) {
MemoryUtil.memFree(verticesBuffer);
}
</code></pre>
<p>这就是<code>init</code>方法应有的代码。我们的数据已经在显卡中准备使用了,现在只需要修改<code>render</code>方法在游戏循环中进行渲染。</p>
<pre><code class="language-java">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();
}
</code></pre>
<p>如你所见我们只需要清理窗口绑定着色器程序绑定VAO绘制储存在VAO关联的VBO中的顶点然后还原状态仅此而已。</p>
<p>我们还在<code>Renderer</code>类中添加了一个<code>cleanup</code>方法用于释放资源。</p>
<pre><code class="language-java">public void cleanup() {
if (shaderProgram != null) {
shaderProgram.cleanup();
}
glDisableVertexAttribArray(0);
// 删除VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(vboId);
// 删除VAO
glBindVertexArray(0);
glDeleteVertexArrays(vaoId);
}
</code></pre>
<p>就这样!如果你小心地按着上述步骤做,你会看到类似的图像。</p>
<p><img alt="三角形游戏" src="../_static/04/triangle_window.png" /></p>
<p>这就是我们的第一个三角形!你也许会想这并不会使它成为前十名的游戏,你的想法是对的。你也可以认为这是一件无聊的事情来画一个无聊的三角形。但请记住,我们正在介绍关键的概念,并准备基于架构来做更复杂的事情,请耐心等待,继续阅读。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../05-more-on-rendering/" class="btn btn-neutral float-right" title="渲染补充">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../03-a-brief-about-coordinates/" class="btn btn-neutral" title="坐标简介"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../03-a-brief-about-coordinates/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../05-more-on-rendering/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

527
05-more-on-rendering/index.html Executable file
View File

@@ -0,0 +1,527 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>渲染补充 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6e32\u67d3\u8865\u5145";
var mkdocs_page_input_path = "05-more-on-rendering.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">渲染补充</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>渲染补充</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/05-more-on-rendering.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="more-on-rendering">渲染补充More on Rendering</h1>
<p>本章我们将继续讲述OpenGL如何渲染物体。为了整理代码我们要创建一个名为<code>Mesh</code>的新类把一个位置数组作为输入为需要加载到显卡中的模型创建VBO和VAO对象。</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>我们将在<code>DummyGame</code>类中实例化<code>Mesh</code>,然后将<code>Renderer</code><code>init</code>方法中的VAO和VBO代码删除。在<code>Renderer</code>类的渲染方法中将接收一个<code>Mesh</code>对象来渲染。<code>cleanup</code>方法也被简化,因为<code>Mesh</code>类已经提供了一个释放VAO和VBO资源的方法。</p>
<pre><code class="language-java">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();
}
}
</code></pre>
<p>值得注意的一点是:</p>
<pre><code class="language-java">glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
</code></pre>
<p><code>Mesh</code>类通过将位置数组除以3来计算顶点的数目因为我们使用XY和Z坐标。现在我们可以渲染更复杂的形状。来试试渲染一个正方形一个正方形可以用两个三角形来组成如图所示</p>
<p><img alt="正方形坐标" src="../_static/05/quad_coordinates.png" /></p>
<p>如你所见这两个三角形中的每一个都由三个顶点组成。第一个三角形由顶点V1、V2和V4橙色的点组成第二个三角形由顶点V4V2和V3绿色的点组成。顶点以逆时针顺序连接因此要传递的浮点数数组应该是[V1, V2, V4, V4, V2, V3]。因此,<code>DummyGame</code><code>init</code>方法将是这样的:</p>
<pre><code class="language-java">@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);
}
</code></pre>
<p>现在你应该可以看到这样的一个正方形:</p>
<p><img alt="正方形渲染" src="../_static/05/quad_rendered.png" /></p>
<p>我们做完了吗并没有上述代码仍存在一些问题。我们使用了重复的坐标来表示正方形传递了两次V2和V4坐标。这是个小图形它可能不是什么大问题但想象在一个更复杂的3D模型中我们会多次重复传递坐标。记住我们使用三个浮点数表示顶点的位置但此后将需要更多的数据来表示纹理等。考虑到在更复杂的形状中三角形直接共享的顶点数量甚至更高如图所示其顶点可以在六个三角形之间共享</p>
<p><img alt="海豚" src="../_static/05/dolphin.png" /></p>
<p>最后我们需要更多的内存来储存重复的数据这就是索引缓冲区Index Buffer大显身手的时候。为了绘制正方形我们只需要以这样的方式指定每个顶点V1, V2, V3, V4。每个顶点在数组中都有一个位置。V1在位置0上V2在位置1上等等</p>
<table>
<thead>
<tr>
<th>V1</th>
<th>V2</th>
<th>V3</th>
<th>V4</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</tbody>
</table>
<p>然后,我们通过引用它们的位置来指定这些顶点的顺序:</p>
<table>
<thead>
<tr>
<th>0</th>
<th>1</th>
<th>3</th>
<th>3</th>
<th>1</th>
<th>2</th>
</tr>
</thead>
<tbody>
<tr>
<td>V1</td>
<td>V2</td>
<td>V4</td>
<td>V4</td>
<td>V2</td>
<td>V3</td>
</tr>
</tbody>
</table>
<p>因此,我们需要修改<code>Mesh</code>类来接收另一个参数,一个索引数组,现在绘制的顶点数量是该索引数组的长度。</p>
<pre><code class="language-java">public Mesh(float[] positions, int[] indices) {
vertexCount = indices.length;
</code></pre>
<p>在创建了储存位置的VBO之后我们需要创建另一个VBO来储存索引。因此重命名储存位置的VBO的ID的变量名并为索引VBO<code>idxVboId</code>创建一个ID。创建VBO的过程相似但现在的类型是<code>GL_ELEMENT_ARRAY_BUFFER</code></p>
<pre><code class="language-java">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);
</code></pre>
<p>因为我们是在处理整数,所以需要创建一个<code>IntBuffer</code>而不是一个<code>FloatBuffer</code></p>
<p>就是这样。现在VAO包含两个VBO一个储存位置另一个储存索引。<code>Mesh</code>类的<code>cleanUp</code>方法也必须考虑到要释放另一个VBO。</p>
<pre><code class="language-java">public void cleanUp() {
glDisableVertexAttribArray(0);
// 删除 VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(posVboId);
glDeleteBuffers(idxVboId);
// 删除 VAO
glBindVertexArray(0);
glDeleteVertexArrays(vaoId);
}
</code></pre>
<p>最后,我们需要修改在绘制时调用的<code>glDrawArrays</code>方法:</p>
<pre><code class="language-java">glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());
</code></pre>
<p>改为调用<code>glDrawElements</code>方法:</p>
<pre><code class="language-java">glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);
</code></pre>
<p>方法的参数如下:
* mode: 指定渲染的图元类型,现在是三角形,没有变化。
* count: 指定要渲染的顶点数。
* type: 指定索引数据的类型,现在是无符号整数型。
* indices: 指定要开始使用索引渲染的数据偏移量。</p>
<p>现在可以使用全新和更有效的方法来绘制复杂的模型了,仅需指定索引。</p>
<pre><code class="language-java">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);
}
</code></pre>
<p>现在为示例代码添加颜色吧。我们把另一组浮点数传递给<code>Mesh</code>类,它储存了正方形中每个顶点的颜色。</p>
<pre><code class="language-java">public Mesh(float[] positions, float[] colours, int[] indices) {
</code></pre>
<p>为了使用该数组我们需要创建另一个VBO它将与我们的VAO相关联。</p>
<pre><code class="language-java">// 颜色 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);
</code></pre>
<p>请注意<code>glVertexAttribPointer</code>方法的调用第一个参数现在是“1”这是着色器期望数据的位置。当然由于增加了一个VBO我们需要在<code>cleanUp</code>方法中释放它。可以看到我们需要在渲染期间启用位置1处的VAO属性。</p>
<p>接下来是修改着色器。顶点着色器现在需要两个参数坐标位置0和颜色位置1。顶点着色器将只输出接收到的颜色以便片元着色器可以对其进行处理。</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>现在,片元着色器接收由顶点着色器处理的颜色,并使用它来生成颜色。</p>
<pre><code class="language-glsl">#version 330
in vec3 exColour;
out vec4 fragColor;
void main()
{
fragColor = vec4(exColour, 1.0);
}
</code></pre>
<p>最后要做的是修改渲染代码以使用第二个数据数组:</p>
<p>我们现在可以将如下所示的颜色数组传递给<code>Mesh</code>类,为正方形添加一些颜色。</p>
<pre><code class="language-java">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,
};
</code></pre>
<p>然后会得到一个色彩鲜艳的正方形。</p>
<p><img alt="色彩鲜艳的正方形" src="../_static/05/coloured_quad.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../06-transformations/" class="btn btn-neutral float-right" title="变换">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../04-rendering/" class="btn btn-neutral" title="渲染"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../04-rendering/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../06-transformations/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

631
06-transformations/index.html Executable file
View File

@@ -0,0 +1,631 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>变换 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u53d8\u6362";
var mkdocs_page_input_path = "06-transformations.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">变换</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">投影</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">使用变换</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>变换</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/06-transformations.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="transformations">变换Transformations</h1>
<h2 id="_1">投影</h2>
<p>让我们回看在前一章中创建的色彩鲜艳的正方形。如果仔细看它更像一个矩形你甚至可以将窗口的宽度从600像素改为900像素失真就会更加明显。这发生了什么呢</p>
<p>如果你查看顶点着色器的代码我们只是直接地传递坐标。换句话说当一个顶点的X坐标为0.5时我们让OpenGL在屏幕的X坐标为0.5的位置绘制它。下图展示了OpenGL坐标系仅含X和Y轴</p>
<p><img alt="坐标" src="../_static/06/coordinates.png" /></p>
<p>将这些坐标投影到窗口坐标系其原点位于上图的左上角需要考虑到窗口的大小。因此如果我们的窗口大小为900x480OpenGL坐标(1, 0)将被投影到窗口坐标(900, 0),最终创建一个矩形而不是一个正方形。</p>
<p><img alt="矩形" src="../_static/06/rectangle.png" /></p>
<p>但是问题远比这更严重。将四边形的Z坐标从0.0修改为1.0和-1.0你发现了什么四边形完全是绘制在同一个地方不管它是否沿着Z轴位移。为什么会发生这种情况远处的物体应该比近处的物体绘制得更小但是我们使用相同的X和Y坐标绘制它们。</p>
<p>但稍等一下这不应该由Z坐标来处理吗这半对半错。Z坐标告诉OpenGL一个物体的远近但是OpenGL对你的物体的大小一无所知。你可以有两个不同大小的物体一个更近更小一个更远更大而且可以以相同的大小正确地投影到屏幕上有相同的屏幕X和Y坐标但Z坐标不同。OpenGL只使用正在传递的坐标所以我们必须处理这个问题我们需要正确地投影坐标。</p>
<p>既然已经确诊了这个问题该怎么解决呢答案是使用投影矩阵Projection Matrix或截锥体Frustum。投影矩阵将处理绘制区域的宽高比大小与高度之间的关系这样物体就不会变形。它还可以处理距离所以较远的物体将会被绘制得更小。投影矩阵还将考虑我们的视野和应该显示的距离有多远。</p>
<p>对于不熟悉矩阵的人矩阵Matrix是以行和列排列的二维数组。矩阵中的每个数字被称为元素。矩阵阶次是行和列的数量。例如此处是一个2x2矩阵有2行2列</p>
<p><img alt="2x2矩阵" src="../_static/06/2_2_matrix.png" /></p>
<p>矩阵有许多可以应用于它们的基本运算(如加法、乘法等),你可以在数学书中查阅,其中与三维图形相关的矩阵对空间中点的变换非常有用。</p>
<p>你可以把投影矩阵想象成一个摄像机,它有一个视野和最小和最大距离。该摄像机的可视区域是一个截断的金字塔,下图为该区域的俯视图。</p>
<p><img alt="投影矩阵概念" src="../_static/06/projection_matrix.png" /></p>
<p>投影矩阵将正确地投影三维坐标,以便它们能够在二维屏幕上正确地显示。该矩阵的数学表示如下(不要害怕):</p>
<p><img alt="投影矩阵" src="../_static/06/projection_matrix_eq.png" /></p>
<p>其中屏幕宽高比Aspect Ratio指的是屏幕宽度与屏幕高度的关系<script type="math/tex">屏幕宽高比=宽度/高度</script>)。为了获得给定点的投影坐标,只需要将投影矩阵乘以原始坐标,结果是投影后的另一个向量。</p>
<p>因此我们需要处理一组数学实体如向量、矩阵并包括可以对它们进行的运算。我们可以选择从头开始编写所有的代码或者使用已实现的库。当然我们会选择容易的方法使用JOMLJava OpenGL Math LibraryJava OpenGL 数学库处理LWJGL内的数学运算。为了使用该库我们只需要在<code>pom.xml</code>文件添加另一个依赖项。</p>
<pre><code class="language-xml"> &lt;dependency&gt;
&lt;groupId&gt;org.joml&lt;/groupId&gt;
&lt;artifactId&gt;joml&lt;/artifactId&gt;
&lt;version&gt;${joml.version}&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>然后设置要使用的库版本。</p>
<pre><code class="language-xml"> &lt;properties&gt;
[...]
&lt;joml.version&gt;1.9.6&lt;/joml.version&gt;
[...]
&lt;/properties&gt;
</code></pre>
<p>现在一切都准备好了,来定义我们的投影矩阵。在<code>Renderer</code>类中创建<code>Matrix4f</code>由JOML库提供的实例。<code>Matrix4f</code>类提供了一个<code>perspective</code>方法来创建投影矩阵,该方法需要以下参数:</p>
<ul>
<li>视野:可视区域角的弧度大小,我们将定义一个储存该值的常数。</li>
<li>屏幕宽高比。</li>
<li>最近视距z-near</li>
<li>最远视距z-far</li>
</ul>
<p>我们将在<code>init</code>方法中实例化该矩阵,因此需要传递对<code>Window</code>实例的阴影以获取窗口大小(你可以查看源代码)。代码如下:</p>
<pre><code class="language-java"> /**
* 视野弧度
*/
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;
</code></pre>
<p>投影矩阵的创建如下所示:</p>
<pre><code class="language-java">float aspectRatio = (float) window.getWidth() / window.getHeight();
projectionMatrix = new Matrix4f().perspective(FOV, aspectRatio,
Z_NEAR, Z_FAR);
</code></pre>
<p>现在我们省略宽高比可变的情况(通过调整窗口大小),这可以在<code>render</code>方法中检查并相应地改变投影矩阵。</p>
<p>现在有了矩阵该如何使用它呢我们需要在着色器中使用它并且它应该被应用到所有顶点上。首先你可能会想到把它捆绑在顶点输入中就像坐标和颜色那样。但这样我们会浪费很多空间因为投影矩阵在几次渲染期间都不会发生改变。你可能还想在Java代码中用矩阵处理所有顶点但这样我们输入的VBO就是没用的了这样就不能使用显卡中的处理器资源了。</p>
<p>答案是使用“<code>uniform</code>”。Uniform是着色器可以使用的全局的GLSL变量我们将使用它与着色器交流。</p>
<p>所以我们需要修改顶点着色器的代码,并声明一个新的名为<code>projectionMatrix</code>的Uniform并用它来计算投影后的位置。</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>如上所述,我们把<code>projectionMatrix</code>定义为一个4x4的矩阵新的坐标是通过把它与原始坐标相乘得到的。现在我们需要把投影矩阵的值传递给着色器首先需要确定Uniform的位置。</p>
<p>这是通过调用方法<code>glGetUniformLocation</code>实现的,它有两个参数:</p>
<ul>
<li>着色器程序的ID</li>
<li>Uniform名它应该与着色器里定义的名称相同</li>
</ul>
<p>此方法返回储存Uniform位置的ID。由于可能有一个以上的Uniform我们将把这些ID储存在由变量名作为索引的Map中此后我们需要那个ID。因此<code>ShaderProgram</code>需要创建一个新的字段来保存这些ID</p>
<pre><code class="language-java">private final Map&lt;String, Integer&gt; uniforms;
</code></pre>
<p>然后由构造方法初始化它:</p>
<pre><code class="language-java">uniforms = new HashMap&lt;&gt;();
</code></pre>
<p>最后我们创建了一个方法来创建新的Uniform和储存获得的位置。</p>
<pre><code class="language-java">public void createUniform(String uniformName) throws Exception {
int uniformLocation = glGetUniformLocation(programId,
uniformName);
if (uniformLocation &lt; 0) {
throw new Exception(&quot;Could not find uniform:&quot; +
uniformName);
}
uniforms.put(uniformName, uniformLocation);
}
</code></pre>
<p>现在,在着色器程序编译后,我们就可以在<code>Renderer</code>类中调用<code>createUniform</code>方法(本例中,我们将在投影矩阵实例化后调用它)。</p>
<pre><code class="language-java">shaderProgram.createUniform(&quot;projectionMatrix&quot;);
</code></pre>
<p>此时我们已经准备好一个可以储存投影矩阵数据的储存器。由于投影矩阵在渲染期间不会变化所以可以在创建Uniform后直接设置值但我们将在<code>render</code>方法中做此事。稍后你可以看到我们可以重用该Uniform来执行每次渲染调用中需要执行的其他操作。</p>
<p>我们将在<code>ShaderProgram</code>类中创建另一个名为<code>setUniform</code>的方法来设置数据通过使用JOML库提供的实用方法将矩阵转换为4x4的<code>FloatBuffer</code>对象并将它们发送到Uniform中。</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>如你所见,我们用与此前不同的方式创建缓冲区。我们使用的是自动管理的缓冲区,并将它们分配到堆栈上。这是因为这个缓冲区的大小很小,并且在该方法之外不会使用它。因此我们使用<code>MemoryStack</code>类。</p>
<p>现在,在着色器绑定之后,可以在<code>Renderer</code>类的<code>render</code>方法中调用该方法:</p>
<pre><code class="language-java">shaderProgram.setUniform(&quot;projectionMatrix&quot;, projectionMatrix);
</code></pre>
<p>我们就要完成了,现在可以正确地渲染四边形,所以现在可以启动程序,然后得到一个...黑色背景没有任何彩色四边形。发生了什么我们把什么弄坏了吗实际上没有任何问题。记住我们正在模拟摄像机观察场景的效果。我们提供了两个距离一个是最远视距1000f和一个最近视距0.01f)。而我们的坐标是:</p>
<pre><code class="language-java">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,
};
</code></pre>
<p>也就是说我们坐标中的Z坐标位于可视区域之外。将它们赋值为-0.05f,现在你会看到像这样的一个巨大的绿色正方形:</p>
<p><img alt="正方形1" src="../_static/06/square_1.png" /></p>
<p>这是因为,我们正绘制出离摄像机太近的正方形,实际上是在放大它。如果现在把一个<code>-1.05f</code>的值赋值给Z坐标就可以看到彩色正方形了。</p>
<p><img alt="彩色正方形" src="../_static/06/square_coloured.png" /></p>
<p>如果继续向后移动四边形,我们会看到它变小了。还要注意到四边形不再像矩形了。</p>
<h2 id="_2">使用变换</h2>
<p>回想一下到目前为止我们都做了什么。我们已经学会了如何将数据以有效的格式传递给显卡,以及如何使用顶点和片元着色器来投影这些顶点并设置它们的颜色。现在应该开始在三维空间中绘制更复杂的模型了,但为了做到它,我们必须能够加载模型,并在指定的位置以适当的大小和所需的旋转将它渲染在三维空间中。</p>
<p>现在为了实现这样的渲染,我们需要提供一些基本操作来操作模型:</p>
<ul>
<li>位移Translation: 在三个轴中的任意一个轴上移动一个物体。</li>
<li>旋转Rotation: 按任意一个轴旋转物体任意角度。</li>
<li>缩放Scale: 调整物体的大小。</li>
</ul>
<p><img alt="变换" src="../_static/06/transformations.png" /></p>
<p>上面的操作统称为变换Transformation。你可能猜到要实现这一点的方法是把坐标乘以一组矩阵一个用于移动一个用于旋转一个用于缩放。这三个矩阵将被组合成一个称为“世界矩阵”的矩阵并作为一个Uniform传递给顶点着色器。</p>
<p>之所以被称为世界矩阵是因为我们正在将模型坐标转换为世界坐标。当学习加载3D模型时你会发现这些模型是在它们自己的坐标系中定义的它们不知道你的三维空间的大小但它们需要在里面渲染。因此当我们用矩阵乘以坐标时实际上做的是从一个坐标系模型坐标系转换到另一个坐标系三维世界坐标系</p>
<p>世界矩阵应该这样计算(顺序很重要,因为乘法交换律不适用于矩阵):</p>
<p>
<script type="math/tex; mode=display">世界矩阵=\left[位移矩阵\right]\left[旋转矩阵\right]\left[缩放矩阵\right]</script>
</p>
<p>如果把投影矩阵包含在变换矩阵中,它会是这样的:</p>
<p>
<script type="math/tex; mode=display">
\begin{array}{lcl}
Transf & = & \left[投影矩阵\right]\left[位移矩阵\right]\left[旋转矩阵\right]\left[缩放矩阵\right] \\
& = & \left[投影矩阵\right]\left[世界矩阵\right]
\end{array}
</script>
</p>
<p>位移矩阵是这样定义的:</p>
<p>
<script type="math/tex; mode=display">
\begin{bmatrix}
1 & 0 & 0 & dx \\
0 & 1 & 0 & dy \\
0 & 0 & 1 & dz \\
0 & 0 & 0 & 1
\end{bmatrix}
</script>
</p>
<p>位移矩阵的参数如下:</p>
<ul>
<li>dx: 沿X轴位移。</li>
<li>dy: 沿Y轴位移。</li>
<li>dz: 沿Z轴位移。</li>
</ul>
<p>缩放矩阵是这样定义的;</p>
<p>
<script type="math/tex; mode=display">
\begin{bmatrix}
sx & 0 & 0 & 0 \\
0 & sy & 0 & 0 \\
0 & 0 & sz & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
</script>
</p>
<p>缩放矩阵的参数如下:</p>
<ul>
<li>sx: 沿着X轴缩放。</li>
<li>sy: 沿着Y轴缩放。</li>
<li>sz: 沿着Z轴缩放。</li>
</ul>
<p>旋转矩阵要复杂得多,但请记住,它可以由每个绕单独的轴旋转的旋转矩阵相乘得到。</p>
<p>现在为了实践这些理论我们需要重构代码一点点。在游戏中我们将加载一组模型用来根据游戏逻辑在不同的位置渲染许多物体想象一个FPS游戏它载入了三个不同敌人的模型。确实只有三个模型但使用这些模型我们可以渲染想要的任意数量的敌人。我们需要为每个对象创建一个VAO和一组VBO吗答案是不需要只需要每个模型加载一次就行。我们需要做的是根据它的位置大小和旋转来独立地绘制它。当渲染这些模型时我们需要对它们进行变换。</p>
<p>因此,我们将创建一个名为<code>GameItem</code>的新类,该类将模型加载到<code>Mesh</code>实例中。一个<code>GameItem</code>实例将由变量储存它的位置、旋转状态和缩放。如下是该类的定义。</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p>我们将创建一个名为<code>Transformation</code>的类,让它来处理变换。</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p>如你所见,这个类把投影矩阵和世界矩阵组合起来。给定一组参数来进行位移、旋转和缩放,然后返回世界矩阵。<code>getWorldMatrix</code>返回的结果将为每个<code>GameItem</code>实例变换坐标。该类还提供了获得投影矩阵的方法。</p>
<p>需要注意的一件事是,<code>Matrix4f</code>类的<code>mul</code>方法修改了该实例的内容。因此,如果直接将投影矩阵与变换矩阵相乘,我们会修改投影矩阵本身,这就是为什么总是在每次调用时将每个矩阵初始化为单位矩阵。</p>
<p><code>Renderer</code>类的构造方法中,我们仅实例化了没有任何参数的<code>Transformation</code>类,而在<code>init</code>方法中我们只创建了Uniform。</p>
<pre><code class="language-java">public Renderer() {
transformation = new Transformation();
}
public void init(Window window) throws Exception {
// ... 此前的一些代码 ...
// 为世界矩阵和投影矩阵创建Uniform
shaderProgram.createUniform(&quot;projectionMatrix&quot;);
shaderProgram.createUniform(&quot;worldMatrix&quot;);
window.setClearColor(0.0f, 0.0f, 0.0f, 0.0f);
}
</code></pre>
<p><code>Renderer</code>类的渲染方法中,现在可以接收到一个<code>GameItem</code>的数组:</p>
<pre><code class="language-java">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(&quot;projectionMatrix&quot;, projectionMatrix);
// 渲染每一个游戏项
for(GameItem gameItem : gameItems) {
// 为该项设置世界矩阵
Matrix4f worldMatrix =
transformation.getWorldMatrix(
gameItem.getPosition(),
gameItem.getRotation(),
gameItem.getScale());
shaderProgram.setUniform(&quot;worldMatrix&quot;, worldMatrix);
// 为该游戏项渲染网格
gameItem.getMesh().render();
}
shaderProgram.unbind();
}
</code></pre>
<p>每次调用<code>render</code>时就更新投影矩阵一次,这样我们可以处理窗口大小的调整操作。然后我们遍历<code>GameItem</code>数组,并根据它们各自的位置、旋转和缩放创建变换矩阵,该矩阵将被传递到着色器并绘制<code>Mesh</code>。投影矩阵对于所有要渲染的项目都是相同的,这就是为什么它在<code>Transformation</code>类中是单独一个变量的原因。</p>
<p>我们将渲染代码移动到<code>Mesh</code>类中:</p>
<pre><code class="language-java">public void render() {
// 绘制Mesh
glBindVertexArray(getVaoId());
glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
// 重置状态
glBindVertexArray(0);
}
</code></pre>
<p>顶点着色器只需简单地添加一个新的<code>worldMatrix</code>矩阵,然后用它与<code>projectionMatrix</code>一同计算坐标:</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>如你所见代码完全一致。我们使用Uniform来正确地投影坐标并且考虑截锥、位置、缩放和旋转等。</p>
<p>另外一个重要的问题是,为什么不直接使用位移、旋转和缩放矩阵,而是把它们组合成一个世界矩阵呢?原因是我们应该尽量减少在着色器中使用的矩阵。还要记住,在着色器中所做的矩阵乘法是每个顶点一次,投影矩阵在渲染调用期间不会改变,而每一个<code>GameItem</code>实例的世界矩阵也不会改变。如果独立位移、旋转和缩放矩阵,我们要做更多的矩阵乘法运算。在一个有超多顶点的模型中,这是很多余的操作。</p>
<p>但你现在可能会想,如果每个<code>GameItem</code>中的世界矩阵都不会发生变化为什么不在Java类中做矩阵乘法我们将投影矩阵和世界矩阵与每个<code>GameItem</code>相乘把它们作为一个Uniform在此情况下我们确实能省下更多的操作。但当我们向游戏引擎中添加更多的特性时我们需要在着色器中使用世界坐标所以最好独立地处理这两个矩阵。</p>
<p>最后只需要修改<code>DummyGame</code>类,创建一个<code>GameItem</code>实例,让其与<code>Mesh</code>关联,并添加一些逻辑来位移、旋转和缩放四边形。因为这只是个测试示例,没有添加太多内容,所以你可以在本书的源代码中找到相关代码。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../07-textures/" class="btn btn-neutral float-right" title="纹理">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../05-more-on-rendering/" class="btn btn-neutral" title="渲染补充"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../05-more-on-rendering/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../07-textures/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

586
07-textures/index.html Executable file
View File

@@ -0,0 +1,586 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>纹理 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u7eb9\u7406";
var mkdocs_page_input_path = "07-textures.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">纹理</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">创建一个三维立方体</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">为立方体添加纹理</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_3">透明纹理简介</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>纹理</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/07-textures.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="textures">纹理Textures</h1>
<h2 id="_1">创建一个三维立方体</h2>
<p>在本章中,我们将学习如何在渲染中加载纹理并使用它们。为了讲解与纹理相关的所有概念,我们将把此前章节中使用的正方形更改为三维立方体。为了绘制一个立方体,我们只需要正确地定义一个立方体的坐标,就能使用现有代码正确地绘制它。</p>
<p>为了绘制立方体,我们只需要定义八个顶点。</p>
<p><img alt="立方体坐标" src="../_static/07/cube_coords.png" /></p>
<p>因此,它的坐标数组将是这样的:</p>
<pre><code class="language-java">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,
};
</code></pre>
<p>当然由于我们多了4个顶点我们需要更改颜色数组目前仅重复前四项的值。</p>
<pre><code class="language-java">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,
};
</code></pre>
<p>最后,由于立方体是由六个面构成的,需要绘制十二个三角形(每个面两个),因此我们需要修改索引数组。记住三角形必须按逆时针顺序定义,如果你直接去定义三角形,很容易犯错。一定要将你想定义的面摆在你的面前,确认顶点并以逆时针顺序绘制三角形。</p>
<pre><code class="language-java">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,
};
</code></pre>
<p>为了更好观察立方体,我们将修改<code>DummyGame</code>类中旋转模型的代码,使模型沿着三个轴旋转。</p>
<pre><code class="language-java">// 更新旋转角
float rotation = gameItem.getRotation().x + 1.5f;
if ( rotation &gt; 360 ) {
rotation = 0;
}
gameItem.setRotation(rotation, rotation, rotation);
</code></pre>
<p>这就完了,现在能够显示一个旋转的三维立方体了,你可以编译和运行示例代码,会得到如下所示的东西。</p>
<p><img alt="没有开启深度测试的立方体" src="../_static/07/cube_no_depth_test.png" /></p>
<p>这个立方体有些奇怪有些面没被正确地绘制这发生了什么立方体之所以出现这个现象是因为组成立方体的三角形是以一种随机顺序绘制的。事实上距离较远的像素应该在距离较近的像素之前绘制而不是现在这样。为了修复它我们必须启用深度测试Depth Test</p>
<p>这将在<code>Window</code>类的<code>init</code>方法中去做:</p>
<pre><code class="language-java">glEnable(GL_DEPTH_TEST);
</code></pre>
<p>现在立方体被正确地渲染了!</p>
<p><img alt="开启深度测试的立方体" src="../_static/07/cube_depth_test.png" /></p>
<p>如果你看了本章该小节的代码,你可能会看到<code>Mesh</code>类做了一下小规模的调整VBO的ID现在被储存在一个List中以便于迭代它们。</p>
<h2 id="_2">为立方体添加纹理</h2>
<p>现在我们将把纹理应用到立方体上。纹理Texture是用来绘制某个模型的像素颜色的图像可以认为纹理是包在三维模型上的皮肤。你要做的是将纹理图像中的点分配给模型中的顶点这样做OpenGL就能根据纹理图像计算其他像素的颜色。</p>
<p><img alt="纹理映射" src="../_static/07/texture_mapping.png" /></p>
<p>纹理图像不必与模型同样大小它可以变大或变小。如果要处理的像素不能映射到纹理中的指定点OpenGL将推断颜色。可在创建纹理时控制如何进行颜色推断。</p>
<p>因此为了将纹理应用到模型上我们必须做的是将纹理坐标分配给每个顶点。纹理坐标系有些不同于模型坐标系。首先我们的纹理是二维纹理所以坐标只有X和Y两个量。此外原点是图像的左上角X或Y的最大值都是1。</p>
<p><img alt="纹理坐标系" src="../_static/07/texture_coordinates.png" /></p>
<p>我们如何将纹理坐标与位置坐标联系起来呢答案很简单就像传递颜色信息我们创建了一个VBO为每个顶点储存其纹理坐标。</p>
<p>让我们开始修改代码以便在三维立方体上使用纹理吧。首先是加载将被用作纹理的图像。对此在LWJGL的早期版本中通常使用Slick2D库。在撰写本文时该库似乎与LWJGL 3不兼容因此我们需要使用另一种方法。我们将使用LWJGL为<a href="https://github.com/nothings/stb">stb</a>库提供的封装。为了使用它,首先需要在本地的<code>pom.xml</code>文件中声明依赖。</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-stb&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;/dependency&gt;
[...]
&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-stb&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;classifier&gt;${native.target}&lt;/classifier&gt;
&lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;
</code></pre>
<p>在一些教程中,你可能看到首先要做的事是调用<code>glEnable(GL_TEXTURE_2D)</code>来启用OpenGL环境中的纹理。如果使用固定管线这是对的但我们使用GLSL着色器因此不再需要了。</p>
<p>现在我们将创建一个新的<code>Texture</code>类,它将执行加载纹理所必须的步骤。首先,我们需要将图像载入到<code>ByteBuffer</code>中,代码如下:</p>
<pre><code class="language-java">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(&quot;Image file [&quot; + fileName + &quot;] not loaded: &quot; + stbi_failure_reason());
}
/* 获得图像的高度与宽度 */
width = w.get();
height = h.get();
}
[... 接下来还有更多代码 ...]
</code></pre>
<p>首先我们要为库分配<code>IntBuffer</code>,以返回图像大小与通道数。然后,我们调用<code>stbi_load</code>方法将图像加载到<code>ByteBuffer</code>中,该方法需要如下参数:</p>
<ul>
<li><code>filePath</code>文件的绝对路径。stb库是本地的不知道关于<code>CLASSPATH</code>的任何内容。因此,我们将使用常规的文件系统路径。</li>
<li><code>width</code>:图像宽度,获取的图像宽度将被写入其中。</li>
<li><code>height</code>:图像高度,获取的图像高度将被写入其中。</li>
<li><code>channels</code>:图像通道。</li>
<li><code>desired_channels</code>所需的图像通道我们传入4RGBA</li>
</ul>
<p>一件关于OpenGL的重要事项由于历史原因要求纹理图像的大小每个轴的像素数必须是二的指数2, 4, 8, 16, ....)。一些驱动解除了这种限制,但最好还是保持以免出现问题。</p>
<p>下一步是将纹理上传到显存中。首先需要创建一个新的纹理ID与该纹理相关的操作都要使用该ID因此我们需要绑定它。</p>
<pre><code class="language-java">// 创建一个新的OpenGL纹理
int textureId = glGenTextures();
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
</code></pre>
<p>然后需要告诉OpenGL如何解包RGBA字节由于每个分量只有一个字节大小所以我们需要添加以下代码</p>
<pre><code class="language-java">glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
</code></pre>
<p>最后我们可以上传纹理数据:</p>
<pre><code class="language-java">glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height,
0, GL_RGBA, GL_UNSIGNED_BYTE, buf);
</code></pre>
<p><code>glTexImage2D</code>的参数如下所示:</p>
<ul>
<li><code>target</code>: 指定目标纹理(纹理类型),本例中是<code>GL_TEXTURE_2D</code></li>
<li><code>level</code>: 指定纹理细节的等级。0级是基本图像等级第n级是第n个多级渐远纹理的图像之后再谈论这个问题。</li>
<li><code>internal format</code>: 指定纹理中颜色分量的数量。</li>
<li><code>width</code>: 指定纹理图像的宽度。</li>
<li><code>height</code>: 指定纹理图像的高度。</li>
<li><code>border</code>: 此值必须为0。</li>
<li><code>format</code>: 指定像素数据的格式现在为RGBA。</li>
<li><code>type</code>: 指定像素数据的类型。现在,我们使用的是无符号字节。</li>
<li><code>data</code>: 储存数据的缓冲区。</li>
</ul>
<p>在一些代码中,你可能会发现在调用<code>glTexImage2D</code>方法前设置了一些过滤参数。过滤是指在缩放时如何绘制图像,以及如何插值像素。如果未设置这些参数,纹理将不会显示。因此,在<code>glTexImage2D</code>方法调用之前,会看到以下代码:</p>
<pre><code class="language-java">glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
</code></pre>
<p>这些参数基本上在说,当绘制一个像素时,如果没有直接一对一地关联到纹理坐标,它将选择最近的纹理坐标点。</p>
<p>到目前为止我们不会设置这些参数。相反我们将生成一个多级渐远纹理Mipmap。多级渐远纹理是由高细节纹理生成的逐级降低分辨率的纹理集合。当我们的物体缩放时就将自动使用低分辨率的图像。</p>
<p>为了生成多级渐远纹理,只需要编写以下代码(目前我们把它放在<code>glTextImage2D</code>方法调用之后):</p>
<pre><code class="language-java">glGenerateMipmap(GL_TEXTURE_2D);
</code></pre>
<p>最后,我们可以释放原始图像数据本身的内存:</p>
<pre><code class="language-java">stbi_image_free(buf);
</code></pre>
<p>就这样我们已经成功地加载了纹理现在需要使用它。正如此前所说我们需要把纹理坐标作为另一个VBO。因此我们要修改<code>Mesh</code>类以接收储存纹理坐标的浮点数组,而不是颜色(我们可以同时有颜色和纹理,但为了简化它,我们将删除颜色),构造函数现在如下所示:</p>
<pre><code class="language-java">public Mesh(float[] positions, float[] textCoords, int[] indices,
Texture texture)
</code></pre>
<p>纹理坐标VBO与颜色VBO创建的方式相同。唯一的区别是它每个顶点属性只有两个分量而不是三个</p>
<pre><code class="language-java">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);
</code></pre>
<p>现在我们需要在着色器中使用纹理。在顶点着色器中,我们修改了第二个输入参数,因为现在它是一个<code>vec2</code>(也顺便更改了名称)。顶点着色器就像此前一样,仅将纹理坐标传给片元着色器。</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>在片元着色器中,我们使用那些纹理坐标来设置像素颜色:</p>
<pre><code class="language-glsl">#version 330
in vec2 outTexCoord;
out vec4 fragColor;
uniform sampler2D texture_sampler;
void main()
{
fragColor = texture(texture_sampler, outTexCoord);
}
</code></pre>
<p>在分析代码之前我们先理清一些概念。显卡有几个空间或槽来储存纹理每一个空间被称为纹理单元Texture Unit。当使用纹理时我们必须设置想用的纹理。如你所见我们有一个名为<code>texture_sampler</code>的新Uniform该Uniform的类型是<code>sampler2D</code>,并储存有我们希望使用的纹理单元的值。</p>
<p><code>main</code>函数中,我们使用名为<code>texture</code>的纹理采样函数该函数有两个参数取样器Sampler和纹理坐标并返回正确的颜色。取样器Uniform允许使用多重纹理Multi-texture不过现在不是讨论这个话题的时候但是我们会在稍后再尝试添加。</p>
<p>因此,在<code>ShaderProgram</code>类中我们将创建一个新的方法允许为整数型Uniform设置值</p>
<pre><code class="language-java">public void setUniform(String uniformName, int value) {
glUniform1i(uniforms.get(uniformName), value);
}
</code></pre>
<p><code>Renderer</code>类的<code>init</code>方法中我们将创建一个新的Uniform</p>
<pre><code class="language-java">shaderProgram.createUniform(&quot;texture_sampler&quot;);
</code></pre>
<p>此外,在<code>Renderer</code>类的<code>render</code>方法中我们将Uniform的值设置为0我们现在不使用多个纹理所以只使用单元0</p>
<pre><code class="language-java">shaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
</code></pre>
<p>最好,我们只需修改<code>Mesh</code>类的<code>render</code>方法就可以使用纹理。在方法起始处,添加以下几行代码:</p>
<pre><code class="language-java">// 激活第一个纹理单元
glActiveTexture(GL_TEXTURE0);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture.getId());
</code></pre>
<p>我们已经将<code>texture.getId()</code>所获得的纹理ID绑定到纹理单元0上。</p>
<p>我们刚刚修改了代码来支持纹理,现在需要为三维立方体设置纹理坐标,纹理图像文件是这样的:</p>
<p><img alt="立方体纹理" src="../_static/07/cube_texture.png" /></p>
<p>在我们的三维模型中,共有八个顶点。我们首先定义正面每个顶点的纹理坐标。</p>
<p><img alt="立方体纹理的正面" src="../_static/07/cube_texture_front_face.png" /></p>
<table>
<thead>
<tr>
<th>顶点</th>
<th>纹理坐标</th>
</tr>
</thead>
<tbody>
<tr>
<td>V0</td>
<td>
<script type="math/tex">0.0, 0.0</script>
</td>
</tr>
<tr>
<td>V1</td>
<td>
<script type="math/tex">0.0, 0.5</script>
</td>
</tr>
<tr>
<td>V2</td>
<td>
<script type="math/tex">0.5, 0.5</script>
</td>
</tr>
<tr>
<td>V3</td>
<td>
<script type="math/tex">0.5, 0.0</script>
</td>
</tr>
</tbody>
</table>
<p>然后,定义顶面的纹理映射。</p>
<p><img alt="正方体纹理的顶面" src="../_static/07/cube_texture_top_face.png" /></p>
<table>
<thead>
<tr>
<th>顶点</th>
<th>纹理坐标</th>
</tr>
</thead>
<tbody>
<tr>
<td>V4</td>
<td>
<script type="math/tex">0.0, 0.5</script>
</td>
</tr>
<tr>
<td>V5</td>
<td>
<script type="math/tex">0.5, 0.5</script>
</td>
</tr>
<tr>
<td>V0</td>
<td>
<script type="math/tex">0.0, 1.0</script>
</td>
</tr>
<tr>
<td>V3</td>
<td>
<script type="math/tex">0.5, 1.0</script>
</td>
</tr>
</tbody>
</table>
<p>如你所见有一个问题我们需要为同一个顶点V0和V3设置不同的纹理坐标。怎么样才能解决这个问题呢解决这一问题的唯一方法是重复一些顶点并关联不同的纹理坐标。对于顶面我们需要重复四个顶点并为它们分配正确的纹理坐标。</p>
<p>因为前面、后面和侧面都使用相同的纹理所以我们不需要重复这些顶点。在源码中有完整的定义但是我们需要从8个点上升到20个点了。最终的结果就像这样。</p>
<p><img alt="有纹理的立方体" src="../_static/07/cube_with_texture.png" /></p>
<p>在接下来的章节中我们将学习如何加载由3D建模工具生成的模型这样我们就不需要手动定义顶点和纹理坐标了顺便一提对于更复杂的模型手动定义是不存在的</p>
<h2 id="_3">透明纹理简介</h2>
<p>如你所见当加载图像时我们检索了四个RGBA组件包括透明度等级。但如果加载一个透明的纹理可能看不到任何东西。为了支持透明度我们需要通过以下代码启用混合Blend</p>
<pre><code class="language-java">glEnable(GL_BLEND);
</code></pre>
<p>但仅启用混合透明效果仍然不会显示我们还需要指示OpenGL如何进行混合。这是通过调用<code>glBlendFunc</code>方法完成的:</p>
<pre><code class="language-java">glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
</code></pre>
<p>你可以查看<a href="https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/03%20Blending/">此处</a>有关可使用的不同功能的详细说明。</p>
<p>即使启用了混合并设置了功能,也可能看不到正确的透明效果。其原因是深度测试,当使用深度值丢弃片元时,我们可能将具有透明度的片元与背景混合,而不是与它们后面的片元混合,这将得到错误的渲染结果。为了解决该问题,我们需要先绘制不透明物体,然后按深度递减顺序绘制具有透明度的物体(应先绘制较远物体)。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../08-camera/" class="btn btn-neutral float-right" title="摄像机">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../06-transformations/" class="btn btn-neutral" title="变换"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../06-transformations/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../08-camera/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

565
08-camera/index.html Executable file
View File

@@ -0,0 +1,565 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>摄像机 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6444\u50cf\u673a";
var mkdocs_page_input_path = "08-camera.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">摄像机</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>摄像机</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/08-camera.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="camera">摄像机Camera</h1>
<p>本章中我们将学习如何在渲染的三维场景中移动。该功能就像是有一台可以在三维世界中运动的摄像机,然而事实上摄像机就是描述该功能的术语。</p>
<p>但如果你尝试在OpenGL寻找摄像机功能你会发现没有摄像机这个概念换句话说摄像机一直是固定在屏幕中心的以(0, 0, 0)为中心点的位置。</p>
<p>因此,我们需要模拟出一个可以在三维空间中移动的摄像。但要怎么做呢?摄像机是不能移动的,因此我们必须要移动世界中的全部物体。换句话说,如果移动不了摄像机就移动整个世界。</p>
<p>假设摄像机从(Cx, Cy, Cz)沿着Z轴移动到(Cx, Cy, Cz+dz),从而靠近在(Ox, Oy, Oz)放置的物体。</p>
<p><img alt="摄像机运动" src="../_static/08/camera_movement.png" /></p>
<p>我们实际上做的是将物体(在三维空间中的所有物体)按摄像机应移动的相反方向移动。其实物体就像是放在跑步机上一样向后退。</p>
<p><img alt="实际的运动" src="../_static/08/actual_movement.png" /></p>
<p>摄像机可以沿着三个轴X、Y和Z移动也可以绕着它们旋转翻滚Roll、俯仰Pitch和偏航Yaw</p>
<p><img alt="翻滚、俯仰和偏航" src="../_static/08/roll_pitch_yaw.png" /></p>
<p>基本上要做的就是让移动和旋转应用于三维世界全部物体。那要怎么做呢答案是应用另一种变换该变换将所有物体的所有顶点按摄像机移动的相反方向平移再根据摄像机的旋转来旋转它们。当然这将用到另一个矩阵即所谓的观察矩阵View Matrix来完成。这个矩阵首先进行平移然后沿着轴线进行旋转。</p>
<p>来看看如何构造这个矩阵。如果你想起变换一章第6章其中的变换方程如下所示</p>
<p>
<script type="math/tex; mode=display">
\begin{array}{lcl}
Transf & = & \lbrack 投影矩阵 \rbrack \cdot \lbrack 位移矩阵 \rbrack \cdot \lbrack 旋转矩阵 \rbrack \cdot \lbrack 缩放矩阵 \rbrack \\
& = & \lbrack 投影矩阵 \rbrack \cdot \lbrack 世界矩阵 \rbrack
\end{array}
</script>
</p>
<p>观察矩阵应在应用投影矩阵之前应用,因此我们的方程应如下所示:</p>
<p>
<script type="math/tex; mode=display">
\begin{array}{lcl}
Transf & = & \lbrack 投影矩阵 \rbrack \cdot \lbrack 观察矩阵 \rbrack \cdot \lbrack 位移矩阵 \rbrack \cdot \lbrack 旋转矩阵 \rbrack \cdot \lbrack 缩放矩阵 \rbrack \\
& = & \lbrack 投影矩阵 \rbrack \cdot \lbrack 观察矩阵 \rbrack \cdot \lbrack 世界矩阵 \rbrack
\end{array}
</script>
</p>
<p>现在有三个矩阵了,我们应稍微思考一下这些矩阵的生命周期。在游戏运行时,投影矩阵应该不会有太多的变化,最坏的情况下,每次渲染可能改变一次。如果摄像机移动,则观察矩阵在每次渲染时可能改变一次。每渲染一个<code>GameItem</code>实例世界矩阵都会改变一次,一次每次渲染调用都会改变多次。</p>
<p>因此我们应该将多少矩阵传递到顶点着色器呢你可能会看到一些代码为三个矩阵分别定义一个Uniform但理论上最有效的方法是将投影矩阵和观察矩阵组合将其称为<code>pv</code>矩阵,并传递<code>world</code><code>pv</code>矩阵到着色器。这样,我们可以使用世界坐标,并可以避免一些额外的运算。</p>
<p>但实际上最方便的方法是将观察矩阵与世界矩阵组合。为什么这样因为要记住整个摄像机概念就是个骗局我们所做的是移动整个世界来模拟摄像机的位移并仅显示一小部分的三维世界。因此如果直接处理世界坐标可能会让远离原点的世界坐标遇到一些精度问题。如果在所谓的摄像机空间中处理我们将处理的点虽然远离世界原点但也靠近摄像机。可以将观察矩阵和世界矩阵组合的矩阵称为模型观察矩阵Model View Matrix</p>
<p>让我们开始修改代码以支持摄像机吧。先创建一个名为<code>Camera</code>的类,它将储存摄像机的位置与旋转状态。该类将提供设置位置或旋转状态的方法(<code>setPosition</code><code>setRotation</code>),或在当前状态下用偏移量更新这些值的方法(<code>movePosition</code><code>moveRotation</code>)。</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p>接下来在<code>Transformation</code>中,将定义一个新矩阵来储存观察矩阵。</p>
<pre><code class="language-java">private final Matrix4f viewMatrix;
</code></pre>
<p>我们要提供一个更新这个值的方法。与投影矩阵相似,这个矩阵对于渲染周期中渲染的所有物体都是相同的。</p>
<pre><code class="language-java">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;
}
</code></pre>
<p>如你所见,我们首先需要做旋转,然后位移。如果操作顺序相反,我们将不会沿着摄像机位置旋转,而是沿着坐标原点旋转。请注意,在<code>Camera</code>类的<code>movePosition</code>方法中我们不只是简单地增加摄像机位置的偏移量。我们还考虑了沿Y轴的旋转也就是偏航以便计算最终位置。如果只是通过偏移来增加摄像机的位置摄像机就不会朝着它所朝向的方向移动。</p>
<p>除了上述所说的我们现在还没有得到一个可以完全自由移动的摄像机例如如果我们沿着X轴旋转当向前移动时摄像机不会在空间中向上或向下移动这将在此后的章节中完成因为这有点复杂。 </p>
<p>最后,我们将移除之前的<code>getWorldMatrix</code>方法,并添加一个名为<code>getModelViewMatrix</code>的新方法。 </p>
<pre><code class="language-java">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;
}
</code></pre>
<p>每个<code>GameItem</code>实例都将调用一次<code>getModelViewMatrix</code>方法。</p>
<p><code>Renderer</code>类的<code>render</code>方法中,我们只需要根据摄像机的值,在投影矩阵更新之后更新观察矩阵。</p>
<pre><code class="language-java">// 更新投影矩阵
Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOV, window.getWidth(), window.getHeight(), Z_NEAR, Z_FAR);
shaderProgram.setUniform(&quot;projectionMatrix&quot;, projectionMatrix);
// 更新观察矩阵
Matrix4f viewMatrix = transformation.getViewMatrix(camera);
shaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
// 渲染每个游戏项
for(GameItem gameItem : gameItems) {
// 为该项设置模型观察矩阵
Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix);
shaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
// 渲染该游戏项的网格
gameItem.getMesh().render();
}
</code></pre>
<p>这就是实现摄像机概念的基本代码,现在需要使用它。我们可以修改输入处理和更新摄像机的方式,将设置如下按键:</p>
<ul>
<li>“A”和“D”键使摄像机左右X轴移动。</li>
<li>“W”和“S”键使摄像机前后Z轴移动。</li>
<li>“Z”和“X”键使摄像机上下Y轴移动。</li>
</ul>
<p>当鼠标按下右键时我们将使用鼠标位置沿X和Y轴旋转摄像机。 </p>
<p>如你所见,我们将首次使用鼠标,创建一个名为<code>MouseInput</code>的新类,该类将封装鼠标访问。该类代码如下所示:</p>
<pre><code class="language-java">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) -&gt; {
currentPos.x = xpos;
currentPos.y = ypos;
});
glfwSetCursorEnterCallback(window.getWindowHandle(), (windowHandle, entered) -&gt; {
inWindow = entered;
});
glfwSetMouseButtonCallback(window.getWindowHandle(), (windowHandle, button, action, mode) -&gt; {
leftButtonPressed = button == GLFW_MOUSE_BUTTON_1 &amp;&amp; action == GLFW_PRESS;
rightButtonPressed = button == GLFW_MOUSE_BUTTON_2 &amp;&amp; action == GLFW_PRESS;
});
}
public Vector2f getDisplVec() {
return displVec;
}
public void input(Window window) {
displVec.x = 0;
displVec.y = 0;
if (previousPos.x &gt; 0 &amp;&amp; previousPos.y &gt; 0 &amp;&amp; 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;
}
}
</code></pre>
<p><code>MouseInput</code>类提供了一个应在在初始化阶段调用的<code>init</code>方法,并注册一组回调以处理鼠标事件:</p>
<ul>
<li><code>glfwSetCursorPosCallback</code>:注册一个回调,该回调将在鼠标移动时被调用。</li>
<li><code>glfwSetCursorEnterCallback</code>:注册一个回调,该回调将在鼠标进入窗口时被调用。即使鼠标不在窗口内,我们也会收到鼠标事件。我们使用这个回调来确认鼠标进入窗口。</li>
<li><code>glfwSetMouseButtonCallback</code>:注册一个回调,该回调在按下鼠标按钮时被调用。</li>
</ul>
<p><code>MouseInput</code>类提供了一个<code>input</code>方法,在处理游戏输入时应调用该方法。该方法计算鼠标从上一个位置的位移,并将其存储到<code>Vector2f</code>类型的<code>displVec</code>变量中,以便游戏使用它。</p>
<p><code>MouseInput</code>类将在<code>GameEngine</code>类中实例化,并且将作为参数传递给游戏实现的<code>init</code><code>update</code>方法(因此需要相应地更改<code>IGameLogic</code>接口)。 </p>
<pre><code class="language-java">void input(Window window, MouseInput mouseInput);
void update(float interval, MouseInput mouseInput);
</code></pre>
<p>鼠标输入将在<code>GameEngine</code>类的<code>input</code>方法中被处理,而最终的控制将交由游戏实现。</p>
<pre><code class="language-java">protected void input() {
mouseInput.input(window);
gameLogic.input(window, mouseInput);
}
</code></pre>
<p>现在已经准备好修改<code>DummyGame</code>类来处理键盘和鼠标输入了。该类的输入方法如下所示:</p>
<pre><code class="language-java">@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;
}
}
</code></pre>
<p>这只是更新一个名为<code>cameraInc</code><code>Vector3f</code>变量,它储存了摄像机应用的位移。</p>
<p><code>DummyGame</code>类的<code>update</code>方法将根据处理的键盘和鼠标事件,修改摄像机的位置和旋转。</p>
<pre><code class="language-java">@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);
}
}
</code></pre>
<p>现在可以添加更多的立方体到世界中,缩放它们,将它们设置在特定位置,并使用新摄像机游玩。如你所见,所有的立方体共享相同的网格。</p>
<pre><code class="language-java">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};
</code></pre>
<p>你会得到如下所示的结果。</p>
<p><img alt="立方体们" src="../_static/08/cubes.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../09-loading-more-complex-models/" class="btn btn-neutral float-right" title="加载更复杂的模型">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../07-textures/" class="btn btn-neutral" title="纹理"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../07-textures/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../09-loading-more-complex-models/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,575 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>加载更复杂的模型 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u52a0\u8f7d\u66f4\u590d\u6742\u7684\u6a21\u578b";
var mkdocs_page_input_path = "09-loading-more-complex-models.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">加载更复杂的模型</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>加载更复杂的模型</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/09-loading-more-complex-models.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="loading-more-complex-models">加载更复杂的模型Loading more complex models</h1>
<p>本章中我们将学习加载在外部文件中定义的复杂模型。这些模型将使用3D建模工具(例如<a href="https://www.blender.org/">Blender</a>)创建。到目前为止我们已经通过直接编码定义其几何图形的数组来创建模型。但在本章中我们将学习如何加载以OBJ格式定义的模型。</p>
<p>OBJ或.obj是Wavefront Technologies开发的一种几何定义开放文件格式现已被广泛采用。OBJ文件定义构成三维模型的顶点、纹理坐标和多边形。这是一种相对容易解析的格式因为它基于文本每一行定义一个元素顶点、纹理坐标等</p>
<p>在.obj文件中每行以一个标记元素类型的标识符开头</p>
<ul>
<li>以"#"开始的行是注释。</li>
<li>以"v"开始的行用坐标(x, y, z, w)定义一个几何顶点。例如:<code>v 0.155 0.211 0.32 1.0</code></li>
<li>以"vn"开始的行是用坐标(x, y, z)定义顶点法线Normals。例如<code>vn 0.71 0.21 0.82</code>。之后再讨论这个东西。</li>
<li>以"vt"开始的行定义纹理坐标。例如:<code>vt 0.500 1</code></li>
<li>以"f"开始的行定义了一个面。利用该行中的数据可以构造索引数组。我们只处理面导出为三角形的情况。它可以有几种定义方式:<ul>
<li>它可以定义顶点位置(<code>f v1 v2 v3</code>)。例如:<code>f 6 3 1</code>。在这种情况下这个三角形是由位置为6、3和1的几何顶点定义的顶点索引总是从1开始</li>
<li>它可以定义顶点位置、纹理坐标和法线(<code>f v1/t1/n1 v2/t2/n2 v3/t3/n3</code>)。例如:<code>f 6/4/1 3/5/3 7/6/5</code>。第一部分是<code>v1/t1/n1</code>其定义了坐标、纹理坐标和顶点法线。看到该部分可以说出选择几何顶点6、纹理坐标4和顶点法线1。</li>
</ul>
</li>
</ul>
<p>OBJ格式有更多的元素类型如一组多边形、定义材质等。现在我们仅实现上述子集我们的OBJ加载器将忽略其他元素类型。</p>
<p>但是什么是法线呢让我们先定义它。一个平面的法线是一个垂直于该平面的长度为1的向量。</p>
<p><img alt="法线" src="../_static/09/normals.png" /></p>
<p>如上所见,一个平面可以有两条法线,我们应该用哪一个呢?三维图形中的法线是用于光照的,所以我们应该选择面向光源的法线。换言之,我们应该选择指向模型外的法线。</p>
<p>我们有一个由多边形和三角形组成的3D模型每个三角形由三个顶点组成三角形的法线向量是垂直于三角形表面的长度为1的向量。</p>
<p>顶点法线与特定顶点相关联并且是周围三角形的法线的组合当然它的长度等于1。在这里你可以看到一个3D网格的顶点模型取自<a href="https://en.wikipedia.org/wiki/Vertex_normal#/media/File:Vertex_normals.png">维基百科</a></p>
<p><img alt="顶点法线" src="../_static/09/vertex_normals.png" /></p>
<p>现在我们开始创建OBJ加载器。首先我们将修改<code>Mesh</code>因为现在必须使用纹理。我们可能加载一些没有定义纹理坐标的OBJ文件因此必须能够使用颜色而不是使用纹理渲染它们。在此情况下面的定义格式为<code>f v/n</code></p>
<p><code>Mesh</code>类现在有一个名为<code>colour</code>的新属性。</p>
<pre><code class="language-java">private Vector3f colour;
</code></pre>
<p>并且构造函数不再需要<code>Texture</code>。取而代之的是,我们将为纹理和颜色属性提供<code>get</code><code>set</code>方法。</p>
<pre><code class="language-java">public Mesh(float[] positions, float[] textCoords, float[] normals, int[] indices) {
</code></pre>
<p>当然,在<code>render</code><code>clear</code>方法中,在使用纹理之前,必须检查纹理是否为<code>null</code>。正如你在构造函数中看到的,现在需要传递一个名为<code>normals</code>的新浮点数组。如何使用法线渲染答案很简单它只是VAO中的另一个VBO所以我们需要添加如下代码</p>
<pre><code class="language-java">// 顶点法线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);
</code></pre>
<p>现在我们已经完成了对<code>Mesh</code>类的修改,可以修改代码来使用纹理坐标或固定的颜色。因此,我们需要像这样修改片元着色器:</p>
<pre><code class="language-glsl">#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);
}
}
</code></pre>
<p>如你所见我们创建了两个新Uniform</p>
<ul>
<li><code>colour</code>: 将储存基本颜色。</li>
<li><code>useColour</code>: 这是个标志当你不想使用纹理时它将被设置为1。</li>
</ul>
<p><code>Renderer</code>类中我们需要创建这两个Uniform。</p>
<pre><code class="language-java">// 为默认颜色与控制它的标志创建Uniform
shaderProgram.createUniform(&quot;colour&quot;);
shaderProgram.createUniform(&quot;useColour&quot;);
</code></pre>
<p>和其他Uniform一样<code>Renderer</code>类的<code>render</code>方法中,我们也需要为每个<code>GameItem</code>设置这些Uniform的值。</p>
<pre><code class="language-java">for(GameItem gameItem : gameItems) {
Mesh mesh = gameItem.getMesh();
// 为该游戏项设置模型观察矩阵
Matrix4f modelViewMatrix = transformation.getModelViewMatrix(gameItem, viewMatrix);
shaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
// 为该游戏项渲染网格
shaderProgram.setUniform(&quot;colour&quot;, mesh.getColour());
shaderProgram.setUniform(&quot;useColour&quot;, mesh.isTextured() ? 0 : 1);
mesh.render();
}
</code></pre>
<p>现在我们可以创建一个名为<code>OBJLoader</code>的新类该类解析OBJ文件并用其中包含的数据创建一个<code>Mesh</code>实例。你可能会在网上发现一些其他实现可能比这更有效,但我认为该方案更容易理解。这是一个工具类,它将有如下静态方法:</p>
<pre><code class="language-java">public static Mesh loadMesh(String fileName) throws Exception {
</code></pre>
<p>参数<code>fileName</code>指定OBJ模型的文件名称该文件必须包含在类路径中。</p>
<p>在该方法中我们首先要做的是读取文件内容并将所有行储存到一个数组中,然后创建几个列表来储存顶点、纹理坐标、法线和面。</p>
<pre><code class="language-java">List&lt;String&gt; lines = Utils.readAllLines(fileName);
List&lt;Vector3f&gt; vertices = new ArrayList&lt;&gt;();
List&lt;Vector2f&gt; textures = new ArrayList&lt;&gt;();
List&lt;Vector3f&gt; normals = new ArrayList&lt;&gt;();
List&lt;Face&gt; faces = new ArrayList&lt;&gt;();
</code></pre>
<p>然后解析每一行,并根据开头标识符得到顶点位置、纹理坐标、顶点法线或面定义,最后重新排列这些数据。</p>
<pre><code class="language-java">for (String line : lines) {
String[] tokens = line.split(&quot;\\s+&quot;);
switch (tokens[0]) {
case &quot;v&quot;:
// 几何顶点
Vector3f vec3f = new Vector3f(
Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2]),
Float.parseFloat(tokens[3]));
vertices.add(vec3f);
break;
case &quot;vt&quot;:
// 纹理坐标
Vector2f vec2f = new Vector2f(
Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2]));
textures.add(vec2f);
break;
case &quot;vn&quot;:
// 顶点法线
Vector3f vec3fNorm = new Vector3f(
Float.parseFloat(tokens[1]),
Float.parseFloat(tokens[2]),
Float.parseFloat(tokens[3]));
normals.add(vec3fNorm);
break;
case &quot;f&quot;:
Face face = new Face(tokens[1], tokens[2], tokens[3]);
faces.add(face);
break;
default:
// 忽略其他行
break;
}
}
return reorderLists(vertices, textures, normals, faces);
</code></pre>
<p>在讲解重新排序之前,让我们看看如何解析面的定义。我们已创建了一个名为<code>Face</code>的类,它负责解析一个面的定义。一个<code>Face</code>是由一个索引组列表组成的,在本例中,由于我们处理的是三角形,所以我们将有三个索引组。</p>
<p><img alt="面定义" src="../_static/09/face_definition.png" /></p>
<p>我们将创建另一个名为<code>IndexGroup</code>的内部类,它将储存索引组的数据。</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p><code>Face</code>类如下所示:</p>
<pre><code class="language-java">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(&quot;/&quot;);
int length = lineTokens.length;
idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1;
if (length &gt; 1) {
// 如果OBJ不定义纹理坐标则可为null
String textCoord = lineTokens[1];
idxGroup.idxTextCoord = textCoord.length() &gt; 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE;
if (length &gt; 2) {
idxGroup.idxVecNormal = Integer.parseInt(lineTokens[2]) - 1;
}
}
return idxGroup;
}
public IdxGroup[] getFaceVertexIndices() {
return idxGroups;
}
}
</code></pre>
<p>当解析面时,我们可以看到没有纹理但带有矢量法线的对象。在此情况下,面定义可能像<code>f 11//1 17//1 13//1</code>这样,所以我们需要检查这些情况。</p>
<p>最后,我们需要重新排列这些数据。<code>Mesh</code>类需要四个数组分别用于位置坐标、纹理坐标、法线矢量和索引。前三个数组应该具有相同数量的元素因为索引数组是唯一的注意相同数量的元素并不意味着相同的长度。顶点坐标是三维的由三个浮点数组成。纹理坐标是二维的由两个浮点数组成。OpenGL不允许我们对每个元素类型定义不同的索引数组如果可以的话我们就不需要在应用纹理时重复顶点</p>
<p>当你打开一个OBJ文件时你首先可能会看到储存顶点坐标的列表比储存纹理坐标和顶点数量的列表的元素数量更多。这是我们需要解决的问题。举一个简单的例子定义一个具有像素高度的正方形只是为了演示其OBJ文件可能是这样的不要太关注法线坐标因为它只是为了演示</p>
<pre><code class="language-java">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
</code></pre>
<p>当完成对文件的解析时,我们得到如下所示列表(每个元素的数字是它在文件中的位置顺序,按出现顺序排列):</p>
<p><img alt="序列1" src="../_static/09/ordering_i.png" /></p>
<p>现在我们将使用面定义来创建包含索引的最终数组。需要考虑的是,纹理坐标与法线向量的定义顺序与顶点的定义顺序不对应。如果列表的大小是相同的并有序的,那么面定义就只需要每个顶点一个索引。</p>
<p>因此,我们需要排列数据,并根据需要进行相应的设置。首先要做的是创建三个数组(用于顶点、纹理坐标和法线)和一个索引列表。如上所述,三个数组元素数量相同(等于顶点数),顶点数组将是顶点列表的副本。</p>
<p><img alt="序列2" src="../_static/09/ordering_ii.png" /></p>
<p>现在我们开始处理这些面。第一个面的第一个索引组是1/2/1。我们使用索引组中的第一个索引定义几何顶点的索引来构造索引列表称之为<code>posIndex</code></p>
<p>面指定我们应该把占据第一个位置的元素的索引添加到索引列表中。因此,我们将<code>posIndex</code>减去1后放到<code>indicesList</code>必须减1因为数组的起始是0而OBJ文件格式中起始是1</p>
<p><img alt="序列3" src="../_static/09/ordering_iii.png" /></p>
<p>然后,我们使用索引组的其他索引来设置<code>texturesArray</code><code>normalsArray</code>。索引组中的第二个索引是2所以我们必须将第二个纹理坐标放在与所占顶点指定的<code>posIndex</code>位置V1相同的位置上。</p>
<p><img alt="序列4" src="../_static/09/ordering_iv.png" /></p>
<p>然后我们看到第三个索引它为1所以要做的是将第一个法线向量坐标放在与所占顶点指定的<code>posIndex</code>位置V1相同的位置上。</p>
<p><img alt="序列5" src="../_static/09/ordering_v.png" /></p>
<p>在处理了第一个面之后,数组和列表如下所示。</p>
<p><img alt="序列6" src="../_static/09/ordering_vi.png" /></p>
<p>在处理了第二个面之后,数组和列表如下所示。</p>
<p><img alt="序列7" src="../_static/09/ordering_vii.png" /></p>
<p>第二个面也定义了已经被赋值的顶点但是它们有相同的值所以处理这个问题上很简单。我觉得这个过程已经讲解得足够清晰了不过在你明白之前可能会有些棘手。重新排列数据的方法如下所示。请记住我们要的是浮点数组所以必须把顶点、纹理和法线数组转换为浮点数组。因此对于顶点和法线来说数组的长度是顶点列表的长度乘以3而对于纹理坐标来说数组的长度是顶点列表的长度乘以2。</p>
<pre><code class="language-java">private static Mesh reorderLists(List&lt;Vector3f&gt; posList, List&lt;Vector2f&gt; textCoordList,
List&lt;Vector3f&gt; normList, List&lt;Face&gt; facesList) {
List&lt;Integer&gt; indices = new ArrayList&lt;&gt;();
// 按声明的顺序创建位置数组
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) -&gt; v).toArray();
Mesh mesh = new Mesh(posArr, textCoordArr, normArr, indicesArr);
return mesh;
}
private static void processFaceVertex(IdxGroup indices, List&lt;Vector2f&gt; textCoordList,
List&lt;Vector3f&gt; normList, List&lt;Integer&gt; indicesList,
float[] texCoordArr, float[] normArr) {
// 设置顶点坐标的索引
int posIndex = indices.idxPos;
indicesList.add(posIndex);
// 对纹理坐标重新排序
if (indices.idxTextCoord &gt;= 0) {
Vector2f textCoord = textCoordList.get(indices.idxTextCoord);
texCoordArr[posIndex * 2] = textCoord.x;
texCoordArr[posIndex * 2 + 1] = 1 - textCoord.y;
}
if (indices.idxVecNormal &gt;= 0) {
// 对法线向量重新排序
Vector3f vecNorm = normList.get(indices.idxVecNormal);
normArr[posIndex * 3] = vecNorm.x;
normArr[posIndex * 3 + 1] = vecNorm.y;
normArr[posIndex * 3 + 2] = vecNorm.z;
}
}
</code></pre>
<p>此外需要注意的是纹理坐标是UV格式所以Y坐标为用一减去文件中取到的值。</p>
<p>现在我们终于可以渲染OBJ模型。我准备了一个OBJ文件其中是此前章节中使用过的具有纹理的立方体。为了在<code>DummyGame</code>类的<code>init</code>方法中使用它,我们需要创建一个<code>GameItem</code>实例。</p>
<pre><code class="language-java">Texture texture = new Texture(&quot;/textures/grassblock.png&quot;);
mesh.setTexture(texture);
GameItem gameItem = new GameItem(mesh);
gameItem.setScale(0.5f);
gameItem.setPosition(0, 0, -2);
gameItems = new GameItem[]{gameItem};
</code></pre>
<p>然后将会得到一个熟悉的有纹理的立方体。</p>
<p><img alt="有纹理的立方体" src="../_static/09/textured_cube.png" /></p>
<p>我们可以尝试渲染其他模型例如可以使用著名的Stanford Bunny模型它可以免费下载它放在resources文件夹中。这个模型没有纹理所以我们可以这样做</p>
<pre><code class="language-java">Mesh mesh = OBJLoader.loadMesh(&quot;/models/bunny.obj&quot;);
GameItem gameItem = new GameItem(mesh);
gameItem.setScale(1.5f);
gameItem.setPosition(0, 0, -2);
gameItems = new GameItem[]{gameItem};
</code></pre>
<p><img alt="Stanford Bunny" src="../_static/09/standford_bunny.png" /></p>
<p>这个模型看起来有点奇怪,因为没有纹理也没有光,所以我们不能看到它的体积,但是你可以检查模型是否正确地加载。在<code>Window</code>类中设置OpenGL参数时添加这一行代码。</p>
<pre><code class="language-java">glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
</code></pre>
<p>当你放大的时候,你会看到如下所示的东西。</p>
<p><img alt="Stanford Bunny的三角形" src="../_static/09/standford_bunny_triangles.png" /></p>
<p>现在你可以看到构成模型的所有三角形。</p>
<p>有了这个OBJ载入类你现在可以使用Blender创建模型。Blender是一个强大的工具但刚开始使用它有点困难它有很多选项很多关节组合在首次使用它时你需要花时间做很多最基本的事情。当使用Blender导出模型时请确保包含法线并将面导出为三角形。</p>
<p><img alt="OBJ导出选项" src="../_static/09/obj_export_options.png" /></p>
<p>导出时请记得分割边因为我们不能将多个纹理坐标指定给同一个顶点。此外我们需要为每个三角形定义法线而不是指定给顶点。如果你在某些模型中遇到了光照问题在下一章中你应该验证一下法线。你可以在Blender中看到法线。</p>
<p><img alt="边分割" src="../_static/09/edge_split.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../10-let-there-be-light/" class="btn btn-neutral float-right" title="要有光">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../08-camera/" class="btn btn-neutral" title="摄像机"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../08-camera/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../10-let-there-be-light/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

576
10-let-there-be-light/index.html Executable file
View File

@@ -0,0 +1,576 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>要有光 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u8981\u6709\u5149";
var mkdocs_page_input_path = "10-let-there-be-light.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">要有光</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">环境光分量</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">漫反射</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_3">镜面反射</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_4">衰减</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_5">实现</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>要有光</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/10-let-there-be-light.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="let-there-be-light">要有光Let there be light</h1>
<p>在本章中我们将学习如何为3D游戏引擎添加光照。我们不会去实现一个完美的光照模型因为抛开复杂性不谈它还需要消耗大量的计算机资源相反我们只需要一个近似的、像样的光照效果我们将使用名为 <strong>Phong</strong> 的着色算法由Bui Tuong Phong开发。另一个需要注意的是我们将只模拟光照但不会模拟这些光照所产生的阴影这将在后续章节中实现</p>
<p>在开始之前,先定义几个光照类型:</p>
<ul>
<li><strong>点光源Point Light</strong>:这种光模拟的是一个由点向空间各个方向均匀发射的光源。</li>
<li><strong>聚光源Spot Light</strong>:这种光模拟从空间中的点发射的光源,但不是向所有方向上发射,而是限定在了一个锥形方向上。</li>
<li><strong>平行光Directional Light</strong>这种光模拟了太阳光3D场景中的所有物体都会受到来自特定方向的平行光线的照射。无论物体是近抑或是远光线总是以相同角度照射在物体上。</li>
<li><strong>环境光Ambient Light</strong>:这种光来自空间的任何方向,并以相同的强度照亮所有物体。</li>
</ul>
<p><img alt="光照类型" src="../_static/10/light_types.png" /></p>
<p>因此,为了模拟光照,我们需要考虑光照的类型、位置和其他一些参数,如颜色。当然,我们还必须考虑物体如何受光照影响吸收和反射光。</p>
<p>Phong着色算法将模拟光线对我们模型中每个点的影响即对每个顶点的影响。这就是为什么它被称为局部光照模型的原因这也是该算法不能计算阴影的原因它只会计算应用到每个顶点的光照而不考虑顶点是否位于遮光物体之后。我们将在此后的章节中解决这个问题。但正因如此它是一种非常简单快速的算法并且可以提供非常好的效果。我们将在此实现一个没有深入考虑材质影响的简化版本。</p>
<p>Phong算法提供了三种光照分量</p>
<ul>
<li><strong>环境光Ambient Light</strong>:模拟来自任何方向的光照,它将照亮(需要对应强度值)未受任何光线照射的区域,就像背景光。</li>
<li><strong>漫反射Diffuse Reflectance</strong>:考虑到面向光源的表面更亮。</li>
<li><strong>镜面反射Specular Reflectance</strong>:模拟光线在抛光或金属表面上的反射。</li>
</ul>
<p>最后,我们要得到一个因数,将它与指定片元的颜色相乘,根据它所受的光照将该颜色变得更亮或更暗。令<script type="math/tex">A</script>为环境光、<script type="math/tex">D</script>为漫反射光、<script type="math/tex">S</script>为镜面反射光,目标因数将是上述分量的总和:</p>
<p>
<script type="math/tex; mode=display">L = A + D + S</script>
</p>
<p>这些分量其实就是颜色,也就是每个光照分量所贡献的颜色分量。这是因为光照分量不仅会提供一定程度的亮度,还会改变模型的颜色。在我们的片元着色器中,只需将该光照的颜色与原始片元颜色(从纹理或基色获得的)相乘即可。</p>
<p>我们还可以为相同的材质指定不同的颜色,这些颜色将用于环境光、漫反射和镜面反射分量。因此,这些分量将由材质关联的颜色调整。如果材质有纹理,我们只需为每个分量使用一个纹理。</p>
<p>所以对于无纹理的材质的最终颜色将是:</p>
<p>
<script type="math/tex; mode=display">L = A * 环境光的颜色 + D * 漫反射的颜色 + S * 镜面反射的颜色</script>
</p>
<p>对于有纹理的材质的最终颜色将是:</p>
<p>
<script type="math/tex; mode=display">L = A * 纹理颜色 + D * 纹理颜色 + S * 纹理颜色</script>
</p>
<h2 id="_1">环境光分量</h2>
<p>让我们来看看第一个分量,即环境光分量,它只是一个常量,会使我们的所有物体变得更亮或更暗。我们可以使用它来模拟一天中特定时间段内的光照(黎明、黄昏等),也可以用它来添加一些不直接被光线照射到,但可以以简单的方式被间接光线照射(比如反射)到的点的光照。</p>
<p>环境光是最容易计算的分量,我们只需要传递一种颜色,它将与基色相乘,以调整该基色。假如我们已经确定片元的颜色是<script type="math/tex">(1.0, 0.0, 0.0)</script>,即红色。如果没有环境光时,它将显示为完全红色的片元。如果我们将环境光设置为<script type="math/tex">(0.5, 0.5, 0.5)</script>,则最终颜色将为<script type="math/tex">(0.5, 0.0, 0.0)</script>其实就是变暗的红色。这种光照会以相同的方式使所有片元变暗称其为使物体变暗的光似乎有点奇怪事实上这就是我们得到的效果。此外如果光色的RGB分量不相同它还可以为片元添加一些颜色所以我们只需要一个矢量来调节环境光强度和颜色。</p>
<h2 id="_2">漫反射</h2>
<p>现在我们来谈谈漫反射,它模拟了这样一个现象:与光线垂直的面看起来比以更接近与光线平行的角度接收光线的面更亮。一个物体接收到的光越多,其光密度(在此这样称呼)就越高。</p>
<p><img alt="漫反射光" src="../_static/10/diffuse_light.png" /></p>
<p>但是我们该如何计算它你还记得上一章中我们介绍过的法线的概念吗法线是垂直于平面并且长度为1的向量。因此让我们为上图中的三个点的绘制法线。如你所见每个点的法线将是垂直于每个点的切平面的向量。我们不用绘制来自光源的光线而是绘制从每个点到光源即相反的方向的向量。</p>
<p><img alt="法线与光线的方向" src="../_static/10/diffuse_light_normals.png" /></p>
<p>如你所见,<script type="math/tex">P1</script>点的法线<script type="math/tex">N1</script>与指向光源的向量平行,该法线的方向与光线的方向相反(<script type="math/tex">N1</script>已被移动,以便你可以看到它,但这在数学上是等价的)。<script type="math/tex">P1</script>与指向光源的向量所成的夹角等于<script type="math/tex">0</script>。因为它的切平面垂直于光源,所以<script type="math/tex">P1</script>将是最亮的点。</p>
<p>
<script type="math/tex">P2</script>点的法线<script type="math/tex">N2</script>与指向光源的向量所成的夹角约为30度所以它应该比<script type="math/tex">P1</script>更暗。最后,<script type="math/tex">P3</script>的法线<script type="math/tex">N3</script>也与指向光源的向量平行,但两个向量的方向相反。<script type="math/tex">P3</script>与指向光源的向量的角度为180度所以根本不应该接收到任何光线。</p>
<p>因此,我们似乎得到了一个计算某点的光照强度的好方法,光强与该点的法线和该点指向光源的向量之间的夹角大小有关。但我们要怎么计算它呢?</p>
<p>有一个我们可以使用的数学运算————数量积又称为点积。该运算需要两个向量并得到一个数字标量如果它们之间的角度较小则得到一个正数如果它们之间的角度很大则得到一个负数。如果两个向量都被归一化即两者的长度都等于1那么数量积的结果将介于<script type="math/tex">-1</script><script type="math/tex">1</script>之间。如果两个向量的方向相同(即夹角为<script type="math/tex">0</script>则数量积为1如果两个向量夹角为直角则它的值为<script type="math/tex">0</script>;如果两个向量的方向相反,则为<script type="math/tex">-1</script></p>
<p>我们定义两个向量,<script type="math/tex">v1</script><script type="math/tex">v2</script>,并以<script type="math/tex">α</script>作为它们之间的夹角。数量积的定义如下:</p>
<p><img alt="数量积" src="../_static/10/dot_product.png" /></p>
<p>如果两个向量都归一化即它们的长度或它们的模长等于1它们的数量积即为夹角的余弦值。我们将使用该运算来计算漫反射分量。</p>
<p>所以我们需要计算指向光源的向量,该怎么做呢?假如我们有每个点的位置(即顶点位置)和光源的位置。首先,这两个坐标必须位于同一个坐标空间中。为了简化,我们假设它们都处于世界坐标系中,那么这些位置是指向顶点位置(<script type="math/tex">VP</script>)和光源(<script type="math/tex">VS</script>)的向量的坐标,如下图所示:</p>
<p><img alt="漫反射光照运算I" src="../_static/10/diffuse_calc_i.png" /></p>
<p>如果我们从<script type="math/tex">VP</script>中减去<script type="math/tex">VS</script>,就会得到我们所需的向量,称为<script type="math/tex">L</script></p>
<p>现在我们可以在指向光源的矢量和法线之间做数量积因为Johann Lambert是第一个提出这种关系来模拟平面亮度的所以该乘积被称为兰伯特项。</p>
<p>让我们总结一下,定义以下变量:</p>
<ul>
<li>
<script type="math/tex">vPos</script> :我们的顶点在模型观察空间坐标系中的位置。</li>
<li>
<script type="math/tex">lPos</script>:观察空间坐标系中的光源位置。</li>
<li>
<script type="math/tex">intensity</script>光的强度从0到1</li>
<li>
<script type="math/tex">lCourour</script>:光的颜色。</li>
<li>
<script type="math/tex">normal</script>:顶点法线。</li>
</ul>
<p>首先我们需要计算从当前位置指向光源的向量:<script type="math/tex">toLightDirection = lPos - vPos</script>,该运算的结果需要归一化。</p>
<p>然后我们需要计算漫反射因数(标量):<script type="math/tex">diffuseFactor = normal \cdot toLightDirection</script>。计算两向量之间的数量积,我们希望值介于<script type="math/tex">-1</script><script type="math/tex">1</script>之间,所以两个向量都需要归一化。颜色需要介于<script type="math/tex">0</script><script type="math/tex">1</script>之间,所以如果值低于<script type="math/tex">0</script>,我们将其设为<script type="math/tex">0</script></p>
<p>最后,我们只需要通过漫反射因数和光的强度来调节光的颜色:</p>
<p>
<script type="math/tex; mode=display"> color = diffuseColour * lColour * diffuseFactor * intensity</script>
</p>
<h2 id="_3">镜面反射</h2>
<p>现在看到镜面反射,但首先我们需要知道光线是如何反射的。当光照射到一个表面时,它的一部分被吸收,另一部分被反射,如果你还记得你的物理课知识,反射就是光从物体反弹回来。</p>
<p><img alt="光反射" src="../_static/10/light_reflection.png" /></p>
<p>当然,物体表面不是完全抛光的,如果你仔细观察,你会看到很多不平整的地方。此外还有许多光线(实际上是光子),会撞击这个表面,并且会以不同的角度进行反射。因此,我们看到的就像是一束光照射表面并散射回来。也就是说,光在撞击表面时会发散,这就是我们之前讨论过的漫反射分量。</p>
<p><img alt="表面" src="../_static/10/surface.png" /></p>
<p>但是,当光线照射抛光表面时,例如金属,光线会受到较低的扩散影响,并且在它撞到表面时,大部分会向反方向反射。</p>
<p><img alt="抛光平面" src="../_static/10/polished_surface.png" /></p>
<p>这就是镜面反射分量的模型,它取决于材质属性。对于镜面反射,重要的是只有当摄像机处于恰当的位置时,即反射光的反射区域内,反射光才可见。</p>
<p><img alt="镜面光照" src="../_static/10/specular_lightining.png" /></p>
<p>既然已经解释了镜面反射之中的机理,我们接下来准备计算这个分量。首先我们需要一个从光源指向顶点的向量。当我们计算漫反射分量时,我们使用的是方向与之相反的向量,它指向的是光源,即<script type="math/tex">toLightDirection</script>。所以让我们将其计算为<script type="math/tex">fromLightDirection = -(toLightDirection)</script></p>
<p>然后我们需要考虑到表面的法线,来计算由<script type="math/tex">fromLightDirection</script>射出的光线撞击表面所产生的反射光。有一个名为<code>reflect</code>的GLSL函数实现了该功能。所以<script type="math/tex">reflectLight = reflect(fromLightSource, normal)</script></p>
<p>我们还需要一个指向摄像机的向量,并将其命名为<script type="math/tex">cameraDirection</script>,然后计算出摄像机位置和顶点位置之间的差值:<script type="math/tex">cameraDirection = cameraPos - vPos</script>。摄像机位置向量和顶点位置需要处于相同的坐标系中,并且得到的向量需要归一化。下图概述了我们目前计算的主要分量:</p>
<p><img alt="镜面光照计算" src="../_static/10/specular_lightining_calc.png" /></p>
<p>现在我们需要计算光强,即<script type="math/tex">specularFactor</script>。如果<script type="math/tex">cameraDirection</script><script type="math/tex">reflectLight</script>向量指向相同的方向,该值就越高,如果它们方向相反其值则越低。为了计算该值我们将再次运用数量积。<script type="math/tex">specularFactor = cameraDirection \cdot reflectLight</script>。我们只希望该值位于<script type="math/tex">0</script><script type="math/tex">1</script>之间,所以如果它低于<script type="math/tex">0</script>就将它设为0。</p>
<p>我们还需要考虑到,如果摄像机指向反射光锥,则该光更强烈。这可以通过计算<script type="math/tex">specularFactor</script><script type="math/tex">specularPower</script>次幂来实现,其中<script type="math/tex">specularPower</script>为给定的参数:</p>
<p>
<script type="math/tex; mode=display">specularFactor = specularFactor^{specularPower}</script>
</p>
<p>最后,我们需要对材质的反射率进行建模,如果光线反射,反射率将调节反射光的强度,这将用到一个名为<code>reflectance</code>的参数。所以镜面反射分量的颜色为:</p>
<p>
<script type="math/tex; mode=display">specularColour * lColour * reflectance * specularFactor * intensity</script>
</p>
<h2 id="_4">衰减</h2>
<p>我们现在知道如何计算这三个分量了,这些分量可以帮助我们用环境光模拟点光源。但是我们的光照模型还不完整,因为物体反射的光与光源的距离无关,也就是说,我们需要模拟光线衰减。</p>
<p>衰减是一个与距离和光有关的函数。光的强度与距离的平方成反比。这很容易理解,随着光线的传播,其能量沿着球体表面分布,其半径等于光线行进的距离,而球的表面积与其半径的平方成正比。我们可以用下式来计算衰减因子:<script type="math/tex">1.0 /(atConstant + atLineardist + atExponentdist ^ {2})</script></p>
<p>为了模拟衰减,我们只需要将衰减因数乘以最终颜色即可。</p>
<h2 id="_5">实现</h2>
<p>现在我们可以开始编程实现上述的所有概念,我们将从着色器开始。大部分工作将在片元着色器中完成,但我们还需要将顶点着色器中的一些数据传递给它。在上一章中,片元着色器仅接收纹理坐标,现在我们还将传递两个参数:</p>
<ul>
<li>已转换为模型观察空间坐标系并已归一化的顶点法线。</li>
<li>已转换为模型观察空间坐标系的顶点位置。</li>
</ul>
<p>顶点着色器的代码如下所示:</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>在我们继续讲解片元着色器之前,必须强调一个非常重要的概念。从上述代码可以看到,<code>mvVertexNormal</code>,该变量包含已转换为模型观察空间坐标的顶点法线。这是通过将<code>vertexNormal</code>乘上<code>modelViewMatrix</code>来实现的就像顶点位置一样。但有一个细微的区别该顶点法线的w分量在乘以矩阵之前被设置为0<code>vec4(vertexNormal, 0.0)</code>。我们为什么要这样做呢?因为我们希望法线被旋转和缩放但我们不希望它被平移所以我们只对它的方向感兴趣而不是它的位置。而这是通过将w分量设置为0来实现的这也是是使用齐次坐标的优点之一通过设置w分量我们可以控制应用的变换。你可以在纸上做矩阵乘法看看为什么是这样。</p>
<p>现在我们可以开始在片元着色器中开工了,除了将来自顶点着色器的值声明为输入参数之外,我们将定义一些有用的结构体来建模光照和材质属性。首先我们定义用于建模光的结构体。</p>
<pre><code class="language-glsl">struct Attenuation
{
float constant;
float linear;
float exponent;
};
struct PointLight
{
vec3 colour;
// 光源位置是在观察坐标系中的
vec3 position;
float intensity;
Attenuation att;
};
</code></pre>
<p>点光源由一个颜色,一个位置,以及一个介于<script type="math/tex">0</script><script type="math/tex">1</script>之间的数字(用于模拟光照强度)和一组用于模拟衰减方程的参数定义。</p>
<p>建模材质属性的结构体如下:</p>
<pre><code class="language-glsl">struct Material
{
vec4 ambient;
vec4 diffuse;
vec4 specular;
int hasTexture;
float reflectance;
};
</code></pre>
<p>材质由一组颜色定义(假如我们不使用纹理为片元着色):</p>
<ul>
<li>用于环境光分量的颜色。</li>
<li>用于漫反射分量的颜色。</li>
<li>用于镜面反射分量的颜色。</li>
</ul>
<p>材质还由一个标志控制它是否拥有关联的纹理和反射率指数定义。我们将在片元着色器中使用以下Uniform。</p>
<pre><code class="language-glsl">uniform sampler2D texture_sampler;
uniform vec3 ambientLight;
uniform float specularPower;
uniform Material material;
uniform PointLight pointLight;
uniform vec3 camera_pos;
</code></pre>
<p>我们用新建的Uniform设置以下变量</p>
<ul>
<li>环境光:储存颜色,以同样方式影响每个片元。</li>
<li>镜面反射强度(在讨论镜面反射光时给出的方程式中使用的指数)。</li>
<li>一个点光源。</li>
<li>材质属性。</li>
<li>摄像机在观察空间坐标系中的位置。</li>
</ul>
<p>我们还将定义一些全局变量,它们将储存要在环境、漫反射和镜面反射中使用的材质颜色分量。我们使用这些变量是因为如果分量具有纹理,我们将对所有分量使用相同的颜色,并且我们不希望进行冗余的纹理查找。这些变量的定义如下:</p>
<pre><code class="language-glsl">vec4 ambientC;
vec4 diffuseC;
vec4 speculrC;
</code></pre>
<p>我们现在可以定义一个函数,来根据材质属性设置这些变量:</p>
<pre><code class="language-glsl">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;
}
}
</code></pre>
<p>现在我们要定义一个函数,它以点光源、顶点位置及其法线为输入并返回此前描述的漫反射和镜面反射分量计算的颜色。</p>
<pre><code class="language-glsl">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;
}
</code></pre>
<p>上述代码相对比较直白简单,它只是计算了漫反射分量的颜色,另一个是计算镜面反射的颜色,并通过光线在到达我们正在处理的顶点时受到的衰减来调整它们。</p>
<p>请注意,顶点坐标是位于观察空间中的。在计算镜面反射时,我们得出到观察点(即摄像机位置)的方向,代码如下:</p>
<pre><code class="language-glsl"> vec3 camera_direction = normalize(camera_pos - position);
</code></pre>
<p>但是,由于<code>position</code>位于观察空间中,摄像机位置始终位于原点,即<script type="math/tex">(0, 0, 0)</script>,所以我们按如下代码计算它:</p>
<pre><code class="language-glsl"> vec3 camera_direction = normalize(vec3(0, 0, 0) - position);
</code></pre>
<p>可以简化为:</p>
<pre><code class="language-glsl"> vec3 camera_direction = normalize(-position);
</code></pre>
<p>有了上述函数,顶点着色器的主函数就变得非常简单了。</p>
<pre><code class="language-glsl">void main()
{
setupColours(material, outTexCoord);
vec4 diffuseSpecularComp = calcPointLight(pointLight, mvVertexPos, mvVertexNormal);
fragColor = ambientC * vec4(ambientLight, 1) + diffuseSpecularComp;
}
</code></pre>
<p>调用<code>setupColours</code>函数将使用适当的颜色来设置变量<code>ambientC</code><code>diffuseC</code><code>speculrC</code>。然后,我们计算漫反射和镜面反射分量,并考虑到衰减。为了方便起见,我们使用单个函数调用来实现此操作,如上所述。最终的颜色是通过添加环境光分量来计算的(将<code>ambientC</code>乘以环境光)。如你所见,环境光不受衰减的影响。</p>
<p>在着色器中我们引入了一些需要进一步解释的新概念我们正在定义结构体并将它们用作Uniform。但我们要怎么传递这些结构体首先我们将定义两个新类它们建模点光源和材质属性名为<code>PointLight</code><code>Material</code>。它们只是普通的Java对象所以你可以在本书附带的源代码中查看它们。然后我们需要在<code>ShaderProgram</code>类中创建新方法首先要能够为点光源和材质结构体创建Uniform。</p>
<pre><code class="language-java">public void createPointLightUniform(String uniformName) throws Exception {
createUniform(uniformName + &quot;.colour&quot;);
createUniform(uniformName + &quot;.position&quot;);
createUniform(uniformName + &quot;.intensity&quot;);
createUniform(uniformName + &quot;.att.constant&quot;);
createUniform(uniformName + &quot;.att.linear&quot;);
createUniform(uniformName + &quot;.att.exponent&quot;);
}
public void createMaterialUniform(String uniformName) throws Exception {
createUniform(uniformName + &quot;.ambient&quot;);
createUniform(uniformName + &quot;.diffuse&quot;);
createUniform(uniformName + &quot;.specular&quot;);
createUniform(uniformName + &quot;.hasTexture&quot;);
createUniform(uniformName + &quot;.reflectance&quot;);
}
</code></pre>
<p>如你所见它非常简单我们只为构成结构体的所有属性创建一个单独的Uniform。现在我们需要创建另外两个方法来设置这些Uniform的值并将<code>PointLight</code><code>Material</code>的实例作为参数。</p>
<pre><code class="language-java">public void setUniform(String uniformName, PointLight pointLight) {
setUniform(uniformName + &quot;.colour&quot;, pointLight.getColor() );
setUniform(uniformName + &quot;.position&quot;, pointLight.getPosition());
setUniform(uniformName + &quot;.intensity&quot;, pointLight.getIntensity());
PointLight.Attenuation att = pointLight.getAttenuation();
setUniform(uniformName + &quot;.att.constant&quot;, att.getConstant());
setUniform(uniformName + &quot;.att.linear&quot;, att.getLinear());
setUniform(uniformName + &quot;.att.exponent&quot;, att.getExponent());
}
public void setUniform(String uniformName, Material material) {
setUniform(uniformName + &quot;.ambient&quot;, material.getAmbientColour());
setUniform(uniformName + &quot;.diffuse&quot;, material.getDiffuseColour());
setUniform(uniformName + &quot;.specular&quot;, material.getSpecularColour());
setUniform(uniformName + &quot;.hasTexture&quot;, material.isTextured() ? 1 : 0);
setUniform(uniformName + &quot;.reflectance&quot;, material.getReflectance());
}
</code></pre>
<p>在本章源代码中,你还将看到我们还修改了<code>Mesh</code>类来储存材质实例并且创建了一个简单的示例在其中创建了一个可用“N”和“M”键控制移动的点光源以显示点光源聚焦在反射率值高于0的网格上时是怎样的。</p>
<p>让我们回到片元着色器,如上所述,我们需要另一个储存摄像机位置<code>camera_pos</code>的Uniform。这些坐标必须位于观察空间中。通常我们将在世界空间坐标系中设置光源坐标因此我们需要将它们乘以观察矩阵以便能够在着色器中使用它们所以需要在<code>Transformation</code>类中创建一个新方法,该方法返回观察矩阵以便变换光源坐标。</p>
<pre><code class="language-java">// 获得光源对象的副本并将它的坐标变换到观察空间坐标系
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(&quot;pointLight&quot;, currPointLight);
</code></pre>
<p>我们不会写上完整的源代码,因为如果这样这一章就太长了,且对于解释清楚概念没有太多的帮助,你可以在本书附带的源代码中查阅源代码。</p>
<p><img alt="光照效果" src="../_static/10/lightning_result.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../11-let-there-be-even-more-light/" class="btn btn-neutral float-right" title="要有更多的光">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../09-loading-more-complex-models/" class="btn btn-neutral" title="加载更复杂的模型"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../09-loading-more-complex-models/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../11-let-there-be-even-more-light/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,525 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>要有更多的光 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u8981\u6709\u66f4\u591a\u7684\u5149";
var mkdocs_page_input_path = "11-let-there-be-even-more-light.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">要有更多的光</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">平行光</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">聚光源</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_3">多光源</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>要有更多的光</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/11-let-there-be-even-more-light.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="let-there-be-even-more-light">要有更多的光Let there be even more light</h1>
<p>在本章中,我们将实现在此前章节中介绍的其他类型的光。我们先从平行光开始。</p>
<h2 id="_1">平行光</h2>
<p>如果你回想一下,平行光从同一方向照射到所有物体上。它用来模拟遥远但光强很高的光源,比如太阳。</p>
<p><img alt="平行光" src="../_static/11/directional_light.png" /></p>
<p>平行光的另一个特点是它不受衰减的影响联想太阳光所有被阳光照射的物体都以相同的光强被照射因为离太阳的距离太大以至于它们之间的相对位置都是无关紧要的。事实上平行光被模拟为位于无穷远处的光源如果它受到衰减的影响那么它将对任何物体都没有影响它对物体颜色的影响将等于0</p>
<p>此外,平行光也由漫反射和镜面反射分量组成,与点光源的区别在于它没有位置,但有方向,并且它不受衰减的影响。回到平行光的属性,想象我们正在模拟太阳在三维世界中运动,下图展示了黎明、正午和黄昏时的光线方向。</p>
<p><img alt="太阳像一个平行光" src="../_static/11/sun_directional_light.png" /></p>
<p>上图中的光线的方向为:</p>
<ul>
<li>黎明: <script type="math/tex">-1, 0, 0</script>
</li>
<li>正午: <script type="math/tex">0, 1, 0</script>
</li>
<li>黄昏: <script type="math/tex">1, 0, 0</script>
</li>
</ul>
<p>注意:你可能认为上述坐标是位置坐标,但它们只是一个矢量,一个方向,而不是一个位置。以数学的角度来看,矢量和位置是不可分辨的,但它们有着完全不同的含义。</p>
<p>但是,我们如何模拟这个光位于无穷远处呢?答案是使用<script type="math/tex">w</script>分量,即使用齐次坐标并将<script type="math/tex">w</script>分量设置为<script type="math/tex">0</script></p>
<ul>
<li>黎明: <script type="math/tex">-1, 0, 0, 0</script>
</li>
<li>正午: <script type="math/tex">0, 1, 0, 0</script>
</li>
<li>黄昏: <script type="math/tex">1, 0, 0, 0</script>
</li>
</ul>
<p>这就如同我们在传递法线。对于法线,我们将其<script type="math/tex">w</script>分量设置为<script type="math/tex">0</script>,表示我们对其位移不感兴趣,只对方向感兴趣。此外,当我们处理平行光时,也需要这样做,摄像机的位移不应影响平行光的方向。</p>
<p>让我们开始编码实现和模拟平行光首先要做的是创建一个类来储存它的属性。它只是另一个普通的Java对象其具有复制构造函数并储存光的方向、颜色和强度。</p>
<pre><code class="language-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...
</code></pre>
<p>如你所见,我们用<code>Vector3f</code>来储存方向。保持冷静,当将平行光传递到着色器时,我们将处理<script type="math/tex">w</script>分量。顺便一提,接下来要做的就是更新<code>ShaderProgram</code>来创建和更新储存平行光的Uniform。</p>
<p>在片元着色器中,我们将定义一个结构体来模拟平行光。</p>
<pre><code class="language-glsl">struct DirectionalLight
{
vec3 colour;
vec3 direction;
float intensity;
};
</code></pre>
<p>有了上述定义,<code>ShaderProgram</code>类中的新方法就很简单了。</p>
<pre><code class="language-java">// ...
public void createDirectionalLightUniform(String uniformName) throws Exception {
createUniform(uniformName + &quot;.colour&quot;);
createUniform(uniformName + &quot;.direction&quot;);
createUniform(uniformName + &quot;.intensity&quot;);
}
// ...
public void setUniform(String uniformName, DirectionalLight dirLight) {
setUniform(uniformName + &quot;.colour&quot;, dirLight.getColor() );
setUniform(uniformName + &quot;.direction&quot;, dirLight.getDirection());
setUniform(uniformName + &quot;.intensity&quot;, dirLight.getIntensity());
}
</code></pre>
<p>我们现在需要使用Uniform通过<code>DummyGame</code>类控制太阳的角度来模拟它是如何在天上移动的。</p>
<p><img alt="太阳的移动" src="../_static/11/sun_movement.png" /></p>
<p>我们需要更新光的方向,所以太阳在黎明时(<script type="math/tex">-90°</script>),光线在<script type="math/tex">(-1, 0, 0)</script>方向上,其<script type="math/tex">x</script>分量从<script type="math/tex">-1</script>逐渐增加到<script type="math/tex">0</script><script type="math/tex">y</script>分量逐渐从<script type="math/tex">0</script>增加到<script type="math/tex">1</script>。接下来,<script type="math/tex">x</script>分量增加到<script type="math/tex">1</script><script type="math/tex">y</script>分量减少到<script type="math/tex">0</script>。这可以通过将<script type="math/tex">x</script>分量设置为角的正弦和将<script type="math/tex">y</script>分量设置为角的余弦来实现。</p>
<p><img alt="正弦和余弦" src="../_static/11/sine_cosine.png" /></p>
<p>我们也会调节光强,当它远离黎明时强度将增强,当它临近黄昏时强度将减弱。我们通过将强度设置为<script type="math/tex">0</script>来模拟夜晚。此外,我们还将调节颜色,使光在黎明和黄昏时变得更红。这将在<code>DummyGame</code>类的<code>update</code>方法中实现。</p>
<pre><code class="language-java">// 更新平行光的方向,强度和颜色
lightAngle += 1.1f;
if (lightAngle &gt; 90) {
directionalLight.setIntensity(0);
if (lightAngle &gt;= 360) {
lightAngle = -90;
}
} else if (lightAngle &lt;= -80 || lightAngle &gt;= 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);
</code></pre>
<p>然后,我们需要在<code>Renderer</code>类中的<code>render</code>方法中将平行光传给着色器。</p>
<pre><code class="language-java">// 获取平行光对象的副本并将其坐标变换到观察坐标系
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(&quot;directionalLight&quot;, currDirLight);
</code></pre>
<p>如你所见,我们需要变换光的方向到观察空间,但我们不想应用位移,所以将<script type="math/tex">w</script>分量设置为<script type="math/tex">0</script></p>
<p>现在,我们已经准备好在片元着色器上完成剩下的工作了,因为顶点着色器不需要修改。此前已经说过,我们需要定义一个名为<code>DirectionalLight</code>的新结构体来模拟平行光所以需要一个新的Uniform。</p>
<pre><code class="language-glsl">uniform DirectionalLight directionalLight;
</code></pre>
<p>我们需要重构一下代码,在上一章中,我们有一个名为<code>calcPointLight</code>的函数,它计算漫反射和镜面反射分量,并应用衰减。但如上所述,平行光使用漫反射和镜面反射分量,但不受衰减影响,所以我们将创建一个名为<code>calcLightColour</code>的新函数来计算那些分量。</p>
<pre><code class="language-glsl">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);
}
</code></pre>
<p>然后,<code>calcPointLight</code>方法将衰减因数应用到上述函数计算的结果上。</p>
<pre><code class="language-glsl">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;
}
</code></pre>
<p>我们还将创建一个新的函数来计算平行光的效果,它只调用仅需光照方向的<code>calcLightColour</code>方法。</p>
<pre><code class="language-glsl">vec4 calcDirectionalLight(DirectionalLight light, vec3 position, vec3 normal)
{
return calcLightColour(light.colour, light.intensity, position, normalize(light.direction), normal);
}
</code></pre>
<p>最后,<code>main</code>方法通过环境光和平行光的颜色分量综合起来计算片元颜色。</p>
<pre><code class="language-glsl">void main()
{
setupColours(material, outTexCoord);
vec4 diffuseSpecularComp = calcDirectionalLight(directionalLight, mvVertexPos, mvVertexNormal);
diffuseSpecularComp += calcPointLight(pointLight, mvVertexPos, mvVertexNormal);
fragColor = ambientC * vec4(ambientLight, 1) + diffuseSpecularComp;
}
</code></pre>
<p>就这样,现在我们可以模拟太阳在天空中的运动,如下所示(在示例代码中运动速度加快,不用等待太久就可以看到)。</p>
<p><img alt="平行光效果" src="../_static/11/directional_light_result.png" /></p>
<h2 id="_2">聚光源</h2>
<p>现在我们将实现与点光源非常相似的聚光源,但是它发射的光仅限于三维圆锥体中。它模拟从焦点或任何其他不向所有方向发射光的光源。聚光源有着和点光源一样的属性,但它添加了两个新的参数,圆锥角和圆锥方向。</p>
<p><img alt="聚光源" src="../_static/11/spot_light.png" /></p>
<p>聚光源与点光源的计算方法相同,但有一些不同。从顶点位置到光源的矢量不在光锥内的点不受光照的影响。</p>
<p><img alt="聚光源2" src="../_static/11/spot_light_ii.png" /></p>
<p>该如何计算它是否在光锥内呢?我们需要在光源和圆锥方向矢量(两者都归一化了)之间再做次数量积。</p>
<p><img alt="聚光源计算" src="../_static/11/spot_light_calc.png" /></p>
<p>
<script type="math/tex">L</script><script type="math/tex">C</script>向量之间的数量积等于:<script type="math/tex">\vec{L}\cdot\vec{C}=|\vec{L}|\cdot|\vec{C}|\cdot Cos(\alpha)</script>。在聚光源的定义中,我们储存锥角的余弦值,如果数量积高于该值,我们就知道它位于光锥内部(想想余弦图,当<script type="math/tex">α</script>角为<script type="math/tex">0</script>时,其余弦值为<script type="math/tex">1</script>。在0°~180°时角度越小余弦值越大</p>
<p>第二个不同之处是远离光锥方向的点将受到更少的光照,换句话说,衰减影响将更强。有几种计算方法,我们将选择一种简单的方法,通过将衰减与下述公式相乘:</p>
<p>
<script type="math/tex; mode=display">1 - (1-Cos(\alpha))/(1-Cos(cutOffAngle)</script>
</p>
<p>(在片元着色器中我们没有传递角度而是传递角度的余弦值。你可以检查上面的公式的结果是否位于0到1之间当角度为0时余弦值为1。)</p>
<p>实现非常类似于其他的光源,我们需要创建一个名为<code>SpotLight</code>的类设置适当的Uniform将其传递给着色器并修改片元着色器以获取它。你可以查看本章的源代码。</p>
<p>当传递Uniform时另一件重要的事是位移不应该应用到光锥方向上因为我们只对方向感兴趣。因此和平行光的情况一样当变换到观察空间坐标系时必须将<script type="math/tex">w</script>分量设置为<script type="math/tex">0</script></p>
<p><img alt="聚光源示例" src="../_static/11/spot_light_sample.png" /></p>
<h2 id="_3">多光源</h2>
<p>我们终于实现了四种类型的光源,但是目前每种类型的光源只能使用一个实例。这对于环境光和平行光来说没问题,但是我们确实希望使用多个点光源和聚光源。我们需要修改片元着色器来接收光源列表,所以使用数组来储存这些数据。来看看怎么实现吧。</p>
<p>在开始之前要注意的是在GLSL中数组的长度必须在编译时设置因此它必须足够大以便在运行时能够储存所需的所有对象。首先是定义一些常量来设置要使用的最大点光源数和聚光源数。</p>
<pre><code class="language-glsl">const int MAX_POINT_LIGHTS = 5;
const int MAX_SPOT_LIGHTS = 5;
</code></pre>
<p>然后我们需要修改此前只储存一个点光源和一个聚光源的Uniform以便使用数组。</p>
<pre><code class="language-glsl">uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
</code></pre>
<p>在main函数中我们只需要对这些数组进行迭代以使用现有函数计算每个对象对颜色的影响。我们可能不会像Uniform数组长度那样传递很多光源所以需要控制它。有很多可行的方法但这可能不适用于旧的显卡。最终我们选择检查光强在数组中的空位光强为0</p>
<pre><code class="language-glsl">for (int i=0; i&lt;MAX_POINT_LIGHTS; i++)
{
if ( pointLights[i].intensity &gt; 0 )
{
diffuseSpecularComp += calcPointLight(pointLights[i], mvVertexPos, mvVertexNormal);
}
}
for (int i=0; i&lt;MAX_SPOT_LIGHTS; i++)
{
if ( spotLights[i].pl.intensity &gt; 0 )
{
diffuseSpecularComp += calcSpotLight(spotLights[i], mvVertexPos, mvVertexNormal);
}
}
</code></pre>
<p>现在我们需要在<code>Render</code>类中创建这些Uniform。当使用数组时我们需要为列表中的每个元素创建一个Uniform。例如对于<code>pointLights</code>数组,我们需要创建名为<code>pointLights[0]</code><code>pointLights[1]</code>之类的Uniform。当然这也适用于结构体属性所以我们将创建<code>pointLights[0].colour</code><code>pointLights[1].colour</code>等等。创建这些Uniform的方法如下所示</p>
<pre><code class="language-java">public void createPointLightListUniform(String uniformName, int size) throws Exception {
for (int i = 0; i &lt; size; i++) {
createPointLightUniform(uniformName + &quot;[&quot; + i + &quot;]&quot;);
}
}
public void createSpotLightListUniform(String uniformName, int size) throws Exception {
for (int i = 0; i &lt; size; i++) {
createSpotLightUniform(uniformName + &quot;[&quot; + i + &quot;]&quot;);
}
}
</code></pre>
<p>我们也需要方法来设置这些Uniform的值</p>
<pre><code class="language-java">public void setUniform(String uniformName, PointLight[] pointLights) {
int numLights = pointLights != null ? pointLights.length : 0;
for (int i = 0; i &lt; numLights; i++) {
setUniform(uniformName, pointLights[i], i);
}
}
public void setUniform(String uniformName, PointLight pointLight, int pos) {
setUniform(uniformName + &quot;[&quot; + pos + &quot;]&quot;, pointLight);
}
public void setUniform(String uniformName, SpotLight[] spotLights) {
int numLights = spotLights != null ? spotLights.length : 0;
for (int i = 0; i &lt; numLights; i++) {
setUniform(uniformName, spotLights[i], i);
}
}
public void setUniform(String uniformName, SpotLight spotLight, int pos) {
setUniform(uniformName + &quot;[&quot; + pos + &quot;]&quot;, spotLight);
}
</code></pre>
<p>最后,我们只需要更新<code>Render</code>类来接收点光源和聚光源列表,并相应地修改<code>DummyGame</code>类以创建这些列表,最终效果如下所示。</p>
<p><img alt="多光源" src="../_static/11/multiple_lights.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../12-game-hud/" class="btn btn-neutral float-right" title="游戏HUD">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../10-let-there-be-light/" class="btn btn-neutral" title="要有光"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../10-let-there-be-light/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../12-game-hud/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

766
12-game-hud/index.html Executable file
View File

@@ -0,0 +1,766 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>游戏HUD - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6e38\u620fHUD";
var mkdocs_page_input_path = "12-game-hud.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">游戏HUD</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">文本渲染</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#hud">完善HUD</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">再谈文本渲染</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#osx">OSX</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>游戏HUD</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/12-game-hud.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="hudgame-hud">游戏HUDGame HUD</h1>
<p>在本章中我们将为游戏创建一个HUDHeads-Up Display平视显示器。换句话说就是一组用于在三维场景上随时显示相关信息的二维图形和文本。本例中将创建一个简单的HUD这可为我们展现一些显示信息的基本技术。</p>
<p>当你查阅本章的源代码时,还将发现我们对源代码做了一些小的重构,特别是<code>Renderer</code>以便为HUD渲染做好准备。</p>
<h2 id="_1">文本渲染</h2>
<p>创建HUD所要做的第一件事是渲染文本。为了实现它我们将储存字符的纹理映射到一个方形中该方形将被分割为一组表示每个字符的片段。之后我们将使用该纹理在屏幕上绘制文本。所以首先创建含有所有字母的纹理这项工作可以使用很多软件来做例如<a href="http://www.codehead.co.uk/cbfg/">CBFG</a><a href="http://sourceforge.net/projects/f2ibuilder/">F2IBuilder</a>等等。本例使用Codehead的位图字体生成器Codeheads Bitmap Font GeneratorCBFG</p>
<p>CBFG允许你配置很多选项如纹理大小、字体类型、要使用的抗锯齿等等。下图是本例将用来生成纹理文件的配置。在本章中我们将假设文本编码为ISO-8859-1如果你需要处理其他的字符集则需要稍微修改代码。</p>
<p><img alt="CBFG配置" src="../_static/12/CBG.png" /></p>
<p>当设置好CBFG的所有选项后可以将其导出为多种图片格式。现在我们将其导出为BMP文件然后再转换为PNG文件以便将其作为纹理加载。当转换为PNG格式时我们也可以将黑色背景设置为透明也就是说我们将黑色设为Alpha值等于0可以使用GIMP这样的工具来实现。最终你会看到类似下图所示的结果。</p>
<p><img alt="字体纹理" src="../_static/12/font_texture.png" /></p>
<p>如你所见图像中的所有字符都以行和列的形式排列。在本例中图像由15列和17行字符组成。通过使用特定字符的编号我们可以计算其对应储存在图像中的行和列。所在列的计算方法为<script type="math/tex">列数 = 字符编号 \space mod \space 列总数</script>,其中<script type="math/tex">mod</script>是取余运算符,所在行的计算方法为:<script type="math/tex">所在行 = 字符编号 / 行总数</script>。在本例中我们将整数除以整数,以便忽略小数部分。</p>
<p>我们将创建一个名为<code>TextItem</code>的新类它将储存渲染文本所需的图元。这是一个不考虑多行文本的简化实现但是它能在HUD中显示文本信息。下列代码是该类的声明与构造函数</p>
<pre><code class="language-java">package org.lwjglb.engine;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.lwjglb.engine.graph.Material;
import org.lwjglb.engine.graph.Mesh;
import org.lwjglb.engine.graph.Texture;
public class TextItem extends GameItem {
private static final float ZPOS = 0.0f;
private static final int VERTICES_PER_QUAD = 4;
private String text;
private final int numCols;
private final int numRows;
public TextItem(String text, String fontFileName, int numCols, int numRows) throws Exception {
super();
this.text = text;
this.numCols = numCols;
this.numRows = numRows;
Texture texture = new Texture(fontFileName);
this.setMesh(buildMesh(texture, numCols, numRows));
}
</code></pre>
<p>这个类继承了<code>GameItem</code>类,这是因为本例希望改变在屏幕上文本的位置,也可能需要缩放和旋转它。构造函数接收要渲染的文本和用于渲染的纹理的文件和相关数据(储存图像的文件及行列数)。</p>
<p>在构造函数中,我们加载纹理图像文件,并调用一个方法来创建一个<code>Mesh</code>实例为文本建模。让我们看到<code>buildMesh</code>方法:</p>
<pre><code class="language-java">private Mesh buildMesh(Texture texture, int numCols, int numRows) {
byte[] chars = text.getBytes(Charset.forName(&quot;ISO-8859-1&quot;));
int numChars = chars.length;
List&lt;Float&gt; positions = new ArrayList&lt;&gt;();
List&lt;Float&gt; textCoords = new ArrayList&lt;&gt;();
float[] normals = new float[0];
List&lt;Integer&gt; indices = new ArrayList&lt;&gt;();
float tileWidth = (float)texture.getWidth() / (float)numCols;
float tileHeight = (float)texture.getHeight() / (float)numRows;
</code></pre>
<p>代码创建了用于储存Mesh的位置、纹理坐标、法线和索引的数据结构。现在我们不使用光照因此法线数列是空的。我们要做的是构造一组字符片段每个字符片段代表一个字符。我们还需要根据每个片段对应的字符来分配对应的纹理坐标。下图展现了文本矩形和字符片段的关系</p>
<p><img alt="文本矩形" src="../_static/12/text_quad.png" /></p>
<p>因此对于每个字符我们需要创建由两个三角形构成的字符片段这两个三角形可以用四个顶点V1、V2、V3和V4定义。第一个三角形左下角的那个的索引为(0, 1, 2),而第二个三角形(右上角的那个)的索引为(3, 0, 2)。纹理坐标是基于与纹理图像中每个字符相关的行列计算的,纹理坐标的范围为[0, 1]所以我们只需要将当前行或当前列除以总行数和总列数就可以获得V1的坐标。对于其他顶点我们只需要适当加上行宽或列宽就可以得到对应坐标。</p>
<p>下述的循环语句块创建了与渲染文本矩形相关的所有顶点、纹理坐标和索引。</p>
<pre><code class="language-java">for(int i=0; i&lt;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);
}
</code></pre>
<p>上述代码中需要注意的一些事项:</p>
<ul>
<li>我们将使用屏幕坐标来表示顶点记住屏幕坐标系的原点位于屏幕左上角。三角形上顶点的Y坐标小于三角形下顶点的Y坐标。</li>
<li>我们不缩放图形因此每个字符片段的X距离就等于字符宽度三角形的高度就是每个字符的高度。这是因为本例希望尽可能地使渲染文本与原始纹理相似不管怎样我们可以稍后缩放它因为<code>TextItem</code>类继承了<code>GameItem</code>类)。</li>
<li>Z坐标为固定值因为它与绘制该图像无关。</li>
</ul>
<p>下图展示了一些顶点的坐标:</p>
<p><img alt="文本矩形坐标" src="../_static/12/text_quad_coords.png" /></p>
<p>为什么我们使用屏幕坐标系首先因为本例将在HUD中渲染2D对象并且通常这样使用它们更方便。其次我们将使用正交投影Orthographic Projection绘制它们稍后再解释什么是正交投影。</p>
<p><code>TextItem</code>类最后还需添加一些方法以获取文本并在运行时更改文本。每当文本被更改时需要清理此前的VAO储存在<code>Mesh</code>实例中并创建一个新的VAO。我们不需要删除纹理所以在<code>Mesh</code>类中添加了一个新方法来删除这些数据。</p>
<pre><code class="language-java">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));
}
</code></pre>
<p>既然我们已经建立了渲染文本所需要的基础结构接下来该怎么做呢首先如此前章节所述渲染三维场景然后在其上渲染二维的HUD。为了渲染HUD我们将使用正交投影也称为正交投影。正交投影是三维物体的一种二维表示方式你可能已经在三维模型的蓝图中看到了一些例子它们用来表示某些物体从顶部或某些侧面看到的样子。下图是圆柱体从顶部和前面的正交投影</p>
<p><img alt="正交投影" src="../_static/12/orthographic_projections.png" /></p>
<p>这种投影对于绘制二维物体是非常方便的因为它“忽略”了Z坐标的值也就是说忽略了到屏幕的距离。使用这种投影物体大小不会随着距离的增大而减小不同于透视投影。为了使用正交投影投影物体我们需要使用另一个矩阵即正交投影矩阵正交投影矩阵的公式如下所示</p>
<p><img alt="正交投影矩阵" src="../_static/12/orthographic_matrix.png" /></p>
<p>这个矩阵还矫正了失真,因为我们的窗口并不总是完美的正方形,而是一个矩形。<code>right</code><code>bottom</code>是屏幕大小,而<code>left</code><code>top</code>是原点坐标。正交投影矩阵用于将屏幕坐标转换为三维空间坐标。下图展现了该投影的映射过程:</p>
<p><img alt="正交投影示例" src="../_static/12/orthographic_projection_sample.png" /></p>
<p>该矩阵将允许我们使用屏幕坐标。</p>
<p>我们现在可以继续实现HUD了。接下来要做的是创建另一组着色器一个顶点着色器和一个片元着色器来绘制HUD。顶点着色器很简单</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>它仅接收顶点坐标、纹理坐标、索引和法线,并将使用矩阵将它们转换为三维空间坐标,该矩阵是正交投影矩阵与模型矩阵的乘积,即<script type="math/tex">projModelMatrix = 正交投影矩阵 \cdot 模型矩阵</script>。由于我们没有在世界坐标系中对坐标做任何处理所以在Java代码中将两个矩阵相乘比在着色器中相乘更高效。这样我们只需为每个项目做一次矩阵乘法运算而不是为每个顶点做一次。还应记住顶点应该用屏幕坐标表示。</p>
<p>片元着色器也很简单:</p>
<pre><code class="language-glsl">#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);
}
</code></pre>
<p>它只是将基本颜色与纹理颜色相乘,这样可以改变渲染文本的颜色,而不需要创建多个纹理文件。现在既然已经创建了一组新的着色器,就可以在<code>Renderer</code>类中使用它们。但在此之前,我们要创建一个名为<code>IHud</code>的接口该接口储存所有要在HUD上显示的元素并提供一个默认的<code>cleanup</code>方法。</p>
<pre><code class="language-java">package org.lwjglb.engine;
public interface IHud {
GameItem[] getGameItems();
default void cleanup() {
GameItem[] gameItems = getGameItems();
for (GameItem gameItem : gameItems) {
gameItem.getMesh().cleanUp();
}
}
}
</code></pre>
<p>通过该接口不同的游戏可以定义自定义的HUD而不需要改变渲染机制。现在回到<code>Renderer</code>类,顺便一提,它已经被移动到<code>engine.graph</code>包下,因为现在它的通用性足以不依赖任何游戏的具体实现了。在<code>Renderer</code>类中,我们添加了一个新的方法来创建、连接和初始化一个新的<code>ShaderProgram</code>,以便使用上述所示的着色器。</p>
<pre><code class="language-java">private void setupHudShader() throws Exception {
hudShaderProgram = new ShaderProgram();
hudShaderProgram.createVertexShader(Utils.loadResource(&quot;/shaders/hud_vertex.vs&quot;));
hudShaderProgram.createFragmentShader(Utils.loadResource(&quot;/shaders/hud_fragment.fs&quot;));
hudShaderProgram.link();
// 为正交投影模型矩阵和颜色创建Uniform
hudShaderProgram.createUniform(&quot;projModelMatrix&quot;);
hudShaderProgram.createUniform(&quot;colour&quot;);
}
</code></pre>
<p><code>render</code>方法首先会调用<code>renderScene</code>方法,其中包含了此前章节所述的渲染三维场景的代码,然后调用一个名为<code>renderHud</code>的新方法用于渲染HUD。</p>
<pre><code class="language-java">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);
}
</code></pre>
<p><code>renderHud</code>方法实现如下:</p>
<pre><code class="language-java">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(&quot;projModelMatrix&quot;, projModelMatrix);
hudShaderProgram.setUniform(&quot;colour&quot;, gameItem.getMesh().getMaterial().getAmbientColour());
// 渲染此HUD项目的网格
mesh.render();
}
hudShaderProgram.unbind();
}
</code></pre>
<p>上述代码中我们遍历了HUD的所有元素并将与每个元素关联的模型矩阵和正交投影矩阵相乘。正交投影矩阵在每次<code>render</code>调用时被刷新(因为屏幕大小可以被改变),并且通过如下方式计算:</p>
<pre><code class="language-java">public final Matrix4f getOrthoProjectionMatrix(float left, float right, float bottom, float top) {
orthoMatrix.identity();
orthoMatrix.setOrtho2D(left, right, bottom, top);
return orthoMatrix;
}
</code></pre>
<p>我们将在<code>game</code>包中创建一个<code>Hud</code>类,它实现了<code>IHud</code>接口,并在构造函数接收一个文本,用于在其中创建<code>TextItem</code>实例。</p>
<pre><code class="language-java">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 = &quot;textures/font_texture.png&quot;;
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);
}
}
</code></pre>
<p><code>DummyGame</code>类中我们创建该类的实例,并用默认文本初始化它,最后得到如下所示的结果:</p>
<p><img alt="文本渲染结果" src="../_static/12/text_result.png" /></p>
<p><code>Texture</code>类中可以通过修改纹理的过滤方式来提升文本的可读性(如果需要文本缩放的话需要注意此事)。</p>
<pre><code class="language-java">glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
</code></pre>
<p>但是本例还没有做完。如果用缩放使文本与立方体重叠时,就会看到如下效果:</p>
<p><img alt="背景不透明的文本" src="../_static/12/text_opaque.png" /></p>
<p>绘制的文本背景不透明。为了实现背景透明我们必须明确启用混合Blend这样就可以使用Alpha分量。本例将在<code>Window</code>类中用下述代码设置其初始化参数:</p>
<pre><code class="language-java">// 支持透明
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
</code></pre>
<p>现在你可以看到文本以透明背景绘制了。</p>
<p><img alt="透明背景的文本" src="../_static/12/text_transparent.png" /></p>
<h2 id="hud">完善HUD</h2>
<p>现在我们已经渲染了一些文本但还可以向HUD添加更多的元素。本例将添加一个根据摄像机朝向旋转的指针。现在我们将向<code>Hud</code>类添加一个新的<code>GameItem</code>,它将包含一个指针的模型网格。</p>
<p><img alt="指针" src="../_static/12/compass.png" /></p>
<p>指针的模型是.obj文件但它不会关联任何纹理相反它只有一个基础色。所以需要修改HUD的片元着色器以确认是否使用纹理。本例将通过设置一个名为<code>hasTexture</code>的新Uniform来实现它。</p>
<pre><code class="language-glsl">#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;
}
}
</code></pre>
<p>要将指针添加到HUD上只需在<code>Hud</code>类中创建一个新的<code>GameItem</code>实例。它加载指针模型,并将其添加到项目列表中。在本例中需要放大指针,因为它在屏幕坐标系中渲染,所以通常你需要放大它。</p>
<pre><code class="language-java">// 创建指针
Mesh mesh = OBJLoader.loadMesh(&quot;/models/compass.obj&quot;);
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};
</code></pre>
<p>还要注意为了使指针指向上方我们需要将其旋转180°因为模型通常倾向于使用OpenGL空间坐标系。如果我们要使用屏幕坐标它会指向下方。<code>Hud</code>类还提供一个方法来更新指针的指向,该方法也必须考虑到这一点。</p>
<pre><code class="language-java">public void rotateCompass(float angle) {
this.compassItem.setRotation(0, 0, 180 + angle);
}
</code></pre>
<p><code>DummyGame</code>类中每当摄像机移动时我们需要用Y角旋转更新角度。</p>
<pre><code class="language-java">// 根据鼠标更新摄像机
if (mouseInput.isRightButtonPressed()) {
Vector2f rotVec = mouseInput.getDisplVec();
camera.moveRotation(rotVec.x * MOUSE_SENSITIVITY, rotVec.y * MOUSE_SENSITIVITY, 0);
// 更新HUD指针
hud.rotateCompass(camera.getRotation().y);
}
</code></pre>
<p>我们会看到这样的结果(记住这只是个示例,在实际的游戏中,你可能想使用一些纹理设置指针的外观)。</p>
<p><img alt="有指针的HUD" src="../_static/12/hud_compass.png" /></p>
<h2 id="_2">再谈文本渲染</h2>
<p>在回顾其他主题之前让我们再谈谈之前介绍的文本渲染方法。该方案非常简洁地介绍了渲染HUD所涉及的概念但它存在一些问题</p>
<ul>
<li>它不支持非拉丁字符。</li>
<li>如果你想使用多种字体,则需要为每种字体创建单独的纹理文件。此外,改变文本大小的唯一方法是缩放,这会导致渲染文本的质量较差,或者需要创建额外的纹理文件。</li>
<li>最重要的是,大多数字体中的字符之间的大小并不同,而我们将字体纹理分割成同样大小的元素。我们使用了<a href="https://en.wikipedia.org/wiki/Monospaced_font">Monospaced</a>风格即所有字符具有相同的宽度的“Consolas”字体但如果使用非Monospaced的字体就会看到字符之间恼人的空白。</li>
</ul>
<p>我们需要更改方法,并提供一种更灵活的渲染文本方式。如果你仔细想想,整体想法是可行的,也就是通过单独渲染每个字符的矩形来渲染文本。这里的问题就是该如何生成纹理,我们需要通过系统中可用的字体动态地生成这些纹理。</p>
<p>这就需要<code>java.awt.Font</code>出手了,我们将通过指定字体系列和大小动态地绘制每一个字符来生成纹理。该纹理的使用方式与之前描述的相同,但它将完美地解决上述所有问题。我们将创建一个名为<code>FontTexture</code>的新类,该类将接收<code>Font</code>实例和字符集名称,并将动态地创建包含所有可用字符的纹理。构造函数如下所示:</p>
<pre><code class="language-java">public FontTexture(Font font, String charSetName) throws Exception {
this.font = font;
this.charSetName = charSetName;
charMap = new HashMap&lt;&gt;();
buildTexture();
}
</code></pre>
<p>首先要处理非拉丁字符问题。给定字符集和字体,我们将构建一个包含所有可渲染字符的<code>String</code></p>
<pre><code class="language-java">private String getAllAvailableChars(String charsetName) {
CharsetEncoder ce = Charset.forName(charsetName).newEncoder();
StringBuilder result = new StringBuilder();
for (char c = 0; c &lt; Character.MAX_VALUE; c++) {
if (ce.canEncode(c)) {
result.append(c);
}
}
return result.toString();
}
</code></pre>
<p>让我们来看看实际创建纹理的<code>buildTexture</code>方法:</p>
<pre><code class="language-java">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();
</code></pre>
<p>我们首先通过创建临时图像来获得<code>FontMetrics</code>,然后遍历包含所有可用字符的<code>String</code>,并在<code>FontMetrics</code>的帮助下获取每个字符的宽度。我们将这些信息储存在一个<code>charMap</code>映射上,以字符作为映射的键。这样,我们就确定了纹理图像的大小(图像的高度等于所有字符的最大高度,而宽度等于所有字符的宽度总和)。<code>ChatSet</code>是一个内部类,它储存有关字符的信息(它的宽度和它在纹理图像中的起点)。</p>
<pre><code class="language-java"> public static class CharInfo {
private final int startX;
private final int width;
public CharInfo(int startX, int width) {
this.startX = startX;
this.width = width;
}
public int getStartX() {
return startX;
}
public int getWidth() {
return width;
}
}
</code></pre>
<p>然后,我们将创建一个储存所有可用字符的图像,只需在<code>BufferedImage</code>上绘制字符串即可。</p>
<pre><code class="language-java"> // 创建与字符集相关的图像
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(&quot;&quot; + c, startX, fontMetrics.getAscent());
startX += charInfo.getWidth() + CHAR_PADDING;
}
g2D.dispose();
</code></pre>
<p>我们正在生成一个包含所有字符的单行图像(可能不满足纹理大小应该为二的幂的前提,但是它仍适用于大多数现代显卡。在任何情况下,你都可以通过增加额外的空白来解决这个问题)。你也可以查看生成的图像,在上述代码之后,添加下述的一行代码:</p>
<pre><code class="language-java">ImageIO.write(img, IMAGE_FORMAT, new java.io.File(&quot;Temp.png&quot;));
</code></pre>
<p>图像将被写入一个临时文件。该文件将包含一个在白色背景下,使用抗锯齿绘制所有可用字符的长条。</p>
<p><img alt="字体纹理" src="../_static/12/texture_font.png" /></p>
<p>最后只需要从该图像创建一个<code>Texture</code>实例我们只需使用PNG格式这就是<code>Texture</code>类所期望的)转储图像字节。</p>
<pre><code class="language-java"> 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);
}
</code></pre>
<p>你可能注意到,我们已经稍微修改了<code>Texture</code>类,使其具有一个接收<code>ByteBuffer</code>的构造函数。其中这个新的构造函数使用<code>stbi_load_from_memory</code>加载图片。现在我们只需更改<code>TextItem</code>类,以便在其构造函数中接收<code>FontTexture</code>实例。</p>
<pre><code class="language-java">public TextItem(String text, FontTexture fontTexture) throws Exception {
super();
this.text = text;
this.fontTexture = fontTexture;
setMesh(buildMesh());
}
</code></pre>
<p>只需要在设置片段坐标和纹理坐标时稍稍修改<code>buildMesh</code>方法,下述代码是其中一个顶点的示例:</p>
<pre><code class="language-java"> float startx = 0;
for(int i=0; i&lt;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();
}
</code></pre>
<p>你可以在源代码中查阅其他更改。下图是一个大小为20的Arial字体的渲染效果</p>
<p><img alt="改进后的文本" src="../_static/12/text_rendered_improved.png" /></p>
<p>如你所见文本渲染的质量已经有了很大的提升,你可以用不同的字体和大小来渲染。这仍然有很大的改进空间(如支持多行文本、特效等),但这将留给各位读者作为练习。</p>
<p>你可能还注意到我们仍然能够缩放文本通过着色器中的模型观察矩阵。现在的文本可能不需要缩放但对其他的HUD元素可能会有用。</p>
<p>我们已经确立了所有的基本数据结构以便为游戏创建一个HUD。现在只剩一个问题那就是创建所有的元素传递相关信息给用户并给他们一个专业的外观。</p>
<h2 id="osx">OSX</h2>
<p>如果你试图运行本章中的示例以及下一个渲染文本的示例则可能会发现应用程序阻塞屏幕上不会显示任何内容。这是由于AWT和GLFW在OSX下相处得不太好但这和AWT有什么关系呢我们使用的是<code>Font</code>它属于AWT如果要实例化它AWT也需要初始化。在OSX中AWT试图在主线程运行但GLFW也需要在主线程运行这就是造成此问题的原因。</p>
<p>为了能够使用<code>Font</code>GLFW必须在AWT之前初始化并且示例需要以Headless模式运行。你需要在任何东西被初始化之前设置此属性</p>
<pre><code class="language-java">System.setProperty(&quot;java.awt.headless&quot;, &quot;true&quot;);
</code></pre>
<p>你也许会收到一个警告,但示例成功运行了。</p>
<p>一个更简洁的方法是使用<a href="https://github.com/nothings/stb/">stb</a>库来渲染文本。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../13-sky-box-and-some-optimizations/" class="btn btn-neutral float-right" title="天空盒与一些优化">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../11-let-there-be-even-more-light/" class="btn btn-neutral" title="要有更多的光"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../11-let-there-be-even-more-light/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../13-sky-box-and-some-optimizations/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,530 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>天空盒与一些优化 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u5929\u7a7a\u76d2\u4e0e\u4e00\u4e9b\u4f18\u5316";
var mkdocs_page_input_path = "13-sky-box-and-some-optimizations.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">天空盒与一些优化</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">天空盒</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">一些优化</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>天空盒与一些优化</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/13-sky-box-and-some-optimizations.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="sky-box-and-some-optimizations">天空盒与一些优化 (Sky Box and some optimizations)</h1>
<h2 id="_1">天空盒</h2>
<p>天空盒Sky Box将用于为三维世界设置背景给人一种世界广阔的错觉。这个背景将摄像机所处位置包起来罩住整个空间。我们要做的是构造一个渲染在三维场景周围的立方体即摄像机的位置将位于立方体的中心。该立方体的面将包着一层纹理纹理上有山、蓝天和云彩它将以一种图像看起来像是一个连续的风景的方式进行映射。</p>
<p>下图展现了天空盒的概念。</p>
<p><img alt="天空盒" src="../_static/13/skybox.png" /></p>
<p>创建天空盒的过程可概括为以下几步:</p>
<ul>
<li>创建一个大立方体。</li>
<li>对它应用纹理,产生一种看到没有边缘的场景的错觉。</li>
<li>渲染立方体,它的原点位于摄像机所处的位置,它的面距离原点很远。</li>
</ul>
<p>让我们先从纹理开始。你会发现互联网上有很多预先生成的纹理可供使用,本章示例中使用的纹理从此处下载:<a href="http://www.custommapmakers.org/skyboxes.php">http://www.custommapmakers.org/skyboxes.php</a>。本章中具体使用的纹理是:<a href="http://www.custommapmakers.org/skyboxes/zips/ely_hills.zip">http://www.custommapmakers.org/skyboxes/zips/ely_hills.zip</a>作者是Colin Lowndes。</p>
<p>该网站的纹理都是由单独的TGA文件组成每个文件都是立方体的一面。我们的纹理加载器希望文件格式为PNG所以需要将每一面的图像组合成一个PNG图像。我们可以使用其他方法例如立方体映射Cube Mapping使其自动应用纹理。但为了使本章尽可能简洁易懂你必须手动把它们排列为一张图片最终图像如下所示</p>
<p><img alt="天空盒纹理" src="../_static/13/skybox_texture.png" /></p>
<p>接下来需要创建一个OBJ文件其中含有一个立方体正确地设置每一面的纹理坐标。下图展现了纹理与立方体各面的关系你可以在本书源代码中找到本章使用的OBJ文件</p>
<p><img alt="天空盒立方体的面" src="../_static/13/skybox_cube_faces.png" /></p>
<p>当相关资源准备就绪,就可以开始编写代码了。首先创建一个名为<code>SkyBox</code>的新类它的构造函数接收OBJ模型路径和天空盒纹理文件路径。如同此前一章的HUD这个类也继承<code>GameItem</code>类。为什么它要继承<code>GameItem</code>类?首先,为了方便我们重用大部分处理网格和纹理的代码;第二,因为天空盒不会移动,我们只想应用旋转和缩放。这样想想<code>SkyBox</code>确实是一个<code>GameItem</code><code>SkyBox</code>类的实现如下:</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>如果你查阅本章的源代码,你会发现我们做了一些重构。我们创建了一个名为<code>Scene</code>的类,它将整理与三维世界相关的所有数据。<code>Scene</code>类的定义和属性如下所示,其中包含<code>SkyBox</code>类的实例:</p>
<pre><code class="language-java">package org.lwjglb.engine;
public class Scene {
private GameItem[] gameItems;
private SkyBox skyBox;
private SceneLight sceneLight;
public GameItem[] getGameItems() {
return gameItems;
}
// 更多代码...
</code></pre>
<p>接下来是为天空盒创建另一组顶点和片元着色器,但为什么不重用已有的场景着色器呢?实际上,我们所需的着色器是原有着色器的简化版,不需要让光照影响天空盒(更准确的说,我们不需要点光源,聚光源和平行光)。天空盒的顶点着色器如下所示:</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>可以看到我们仍使用模型观察矩阵。如上所述,我们将缩放天空盒,所以需要变换矩阵。你可能会发现到一些其他实现,在初始化时就放大了立方体模型的大小,并且不需要将模型矩阵和观察矩阵相乘。但我们选择前一种方法,因为它更灵活,它允许在运行时改变天空盒的大小。但如果你想,你就可以轻松改换到另一种方法。</p>
<p>片元着色器也非常简单:</p>
<pre><code class="language-glsl">#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);
}
</code></pre>
<p>如你所见我们为着色器添加了一个环境光该Uniform的目的是修改天空盒的颜色以模拟昼夜如果不这样做在世界的其他地方都是黑暗的时候天空盒看起来就像是在中午</p>
<p><code>Renderer</code>类中我们刚刚添加了新的方法来使用这些着色器并设置Uniform这不是什么新的概念</p>
<pre><code class="language-java">private void setupSkyBoxShader() throws Exception {
skyBoxShaderProgram = new ShaderProgram();
skyBoxShaderProgram.createVertexShader(Utils.loadResource(&quot;/shaders/sb_vertex.vs&quot;));
skyBoxShaderProgram.createFragmentShader(Utils.loadResource(&quot;/shaders/sb_fragment.fs&quot;));
skyBoxShaderProgram.link();
skyBoxShaderProgram.createUniform(&quot;projectionMatrix&quot;);
skyBoxShaderProgram.createUniform(&quot;modelViewMatrix&quot;);
skyBoxShaderProgram.createUniform(&quot;texture_sampler&quot;);
skyBoxShaderProgram.createUniform(&quot;ambientLight&quot;);
}
</code></pre>
<p>当然,我们需要在全局渲染中为渲染天空盒创建一个新的渲染方法。</p>
<pre><code class="language-java">private void renderSkyBox(Window window, Camera camera, Scene scene) {
skyBoxShaderProgram.bind();
skyBoxShaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
// 更新投影矩阵
Matrix4f projectionMatrix = transformation.getProjectionMatrix(FOV, window.getWidth(), window.getHeight(), Z_NEAR, Z_FAR);
skyBoxShaderProgram.setUniform(&quot;projectionMatrix&quot;, 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(&quot;modelViewMatrix&quot;, modelViewMatrix);
skyBoxShaderProgram.setUniform(&quot;ambientLight&quot;, scene.getSceneLight().getAmbientLight());
scene.getSkyBox().getMesh().render();
skyBoxShaderProgram.unbind();
}
</code></pre>
<p>上述方法与其他渲染方法非常相似但有一个不同之处需要解释。如你所见我们像平常一样传递投影矩阵和模型观察矩阵。但当取得观察矩阵时我们将其一些数值设置为0。为什么要这么做其中的原因是我们不希望天空盒被移动。</p>
<p>请记住,当我们在移动摄像机时,实际上是在移动整个世界。因此,如果直接将观察矩阵与模型矩阵相乘,当摄像机移动时,天空盒也将移动。但是我们不想这样,我们想把它固定在坐标原点(0, 0, 0)。这是通过将观察矩阵的位移增量部分(<code>m30</code>, <code>m31</code><code>m32</code>设置为0来实现的。</p>
<p>你可能会认为可以避免使用观察矩阵,因为天空盒必须固定在原点。但在此情况下,你会看到天空盒不会随着摄像机旋转,这不是我们想要的。因此我们需要旋转它但不要移动它。</p>
<p>这就是全部内容,你可以查阅本章源代码,本例在<code>DummyGame</code>类中创建了更多的方块实例来模拟地面和天空盒。你也可以改变环境光来模拟光照和昼夜交替。最终得到如下所示的效果:</p>
<p><img alt="天空盒效果" src="../_static/13/skybox_result.png" /></p>
<p>天空盒是一个小立方体(在实际的游戏中,它应该大得多),所以你在世界中移动时,可以很容易地看到其效果。你还可以看到,构成地面的方块比天空盒大,所以当你移动时,你会看到从山中冒出的方块。这很明显,因为我们设置的天空盒相对较小。但无论如何,我们需要通过添加一个隐藏和模糊远处的物体的效果(例如使用雾效果)来减轻这种影响。</p>
<p>不创建更大的天空盒的另一个原因是我们需要几个优化来提升效率(稍后对此进行解释)。</p>
<p>可以注释渲染方法中防止天空盒移动的代码,然后你就可以在天空盒外看到如下所示图像:</p>
<p><img alt="移动的天空盒" src="../_static/13/skybox_displaced.png" /></p>
<p>虽然这不是天空盒该做的,但这可以帮助你理解天空盒的概念。请记住,这是一个简单的示例,你可以通过增加其他效果来改善它,比如太阳在天穹移动或云层移动。此外,为了创建更大的世界,你需要将世界分割成区域,只加载那些与玩家所处的区域相邻的区域。</p>
<p>另外值得一提的是什么时候渲染天空盒在渲染场景之前还是之后在渲染场景后渲染天空盒更优因为由于深度测试大多数片元将被丢弃。当OpenGL尝试渲染它们并且启用了深度测试时将丢弃一些位于先前渲染的片元之后的片元这些片元的深度值更低。所以答案很明显对吧渲染场景后再渲染天空盒。</p>
<p>上述方法的问题是在处理透明纹理时,如果在场景中我们有应用透明纹理的物体,它们将使用“背景”色绘制,本例中是黑色。但如果我们先渲染天空盒,那么将会正确地应用透明效果。</p>
<p>那么,我们应该在渲染场景之前渲染天空盒吗?关键在于你的选择。如果你在渲染场景前渲染天空盒,可以解决透明纹理问题,但是会影响性能。实际上,就算没有天空盒,你也可能面临透明问题。假设你有一个透明的物体,它与远处的物体重叠,如果首先渲染透明对象,那么也会出现透明问题。因此,也许另一种方法是在所有其他项被渲染后,单独绘制透明的项。这是一些商业游戏使用的方法。不过现在我们在渲染场景之后渲染天空盒,以获得更高的性能。</p>
<h2 id="_2">一些优化</h2>
<p>从上述示例来看天空盒相对较小使得其效果有点奇怪你可以看到物体神奇地从山体出现。所以让我们增加天空盒的大小和世界的大小将天空盒的大小放大50倍这样世界由40,000个游戏项实例方块组成。</p>
<p>如果你更改缩放并重新运行这个示例,你会发现开始出现性能问题,并且在世界中移动不顺畅。现在是时候关注一下性能了(你可能听过一句老话:“过早的优化是万恶之源”,但是从本章开始,我希望没有人会说这是过早的)。</p>
<p>让我来介绍一个概念它能减少正在渲染的数据数量它叫做面剔除Face Culling。在示例中我们渲染了成千上万个立方体立方体是由六个面组成的。我们正在渲染每个立方体的六个面即使它们有些是看不到的。进入一个立方体内部你会看到它的内部如下图所示</p>
<p><img alt="立方体内部" src="../_static/13/cube_interior.png" /></p>
<p>不能被看到的面应该立即舍弃这就是面剔除的作用。实际上对于一个立方体你最多只能同时看到三个面所以只能通过使用面剔除如果你的游戏不要求你进入模型的内部这样做才是高效的稍后你就可以看到效果来舍弃一半的面40,000×3×2个三角形</p>
<p>面剔除检查每个三角形是否面向摄像机丢弃那些不面向摄像机的三角形。但是如何知道三角形是否面向摄像机呢好吧OpenGL实现它的方式是通过检查组成三角形顶点的环绕顺序。</p>
<p>记得从第一章开始我们可以定义一个三角形的顶点以顺时针或逆时针顺序排列。在OpenGL中默认情况下以逆时针顺序排列顶点的三角形面向摄像机而以顺时针顺序排列顶点的三角形面向相反的方向。关键是在考虑摄像机的情况下检查顶点的环绕顺序。因此按照逆时针顺序定义的三角形可以渲染。</p>
<p>让我们来实现它,在<code>Window</code>类的<code>init</code>方法中添加下述代码:</p>
<pre><code class="language-java">glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
</code></pre>
<p>第一行代码将启用面剔除,第二行代码设置背向面为需要剔除的面。如果向上看,你会看到如下图所示的情景:</p>
<p><img alt="应用面剔除的天空盒" src="../_static/13/skybox_face_culling.png" /></p>
<p>发生了什么?如果你查看顶面的顶点顺序,将看到它是按逆时针定义的。但请记住,环绕顺序是以摄像机为视角。实际上,如果你让位移影响天空盒以便能从其上方观察它时,则当你在天空盒外时,将看到顶面再次渲染。</p>
<p><img alt="从外部看应用面剔除的天空盒" src="../_static/13/skybox_face_culling_exterior.png" /></p>
<p>让我们来描述一下发生了什么。下图展示了天空盒立方体的顶面三角形中的一个三角形,它由逆时针顺序排列的三个顶点定义:</p>
<p><img alt="以逆时针顺序定义的顶点" src="../_static/13/cube_counter_clockwise.png" /></p>
<p>但要记住,我们是在天空盒里,如果从内部观察立方体,会看到顶点是按顺时针顺序定义的。</p>
<p><img alt="从内部看立方体" src="../_static/13/cube_clockwise.png" /></p>
<p>这是因为天空盒被定义为从外部观察。因此,我们需要翻转一些面的定义,以便在启用面剔除时能正确地渲染它。</p>
<p>但这仍有更大的优化空间。回顾一下渲染流程,在<code>Renderer</code><code>render</code>方法中,我们要做的是遍历<code>GameItem</code>数组并渲染相关的<code>Mesh</code>。对每个<code>GameItem</code>,我们做了下述工作:</p>
<ol>
<li>设置模型观察矩阵(每个<code>GameItem</code>的唯一值)。</li>
<li>获取<code>GameItem</code>储存的<code>Mesh</code>并绑定纹理绑定VAO并启用其属性。</li>
<li>执行调用以绘制三角形。</li>
<li>停用纹理和VAO。</li>
</ol>
<p>但在现在的游戏中40,000个<code>GameItem</code>都使用相同的<code>Mesh</code>而我们一次又一次地重复第二项到第四项的操作。这不是很高效请记住对OpenGL函数的每次调用都是有性能开销的本地调用。此外我们还应该尽量限制OpenGL中的状态变化绑定和停用纹理、VAO都是状态变化</p>
<p>我们需要改变开发的方式,围绕网格组织代码结构,因为经常有许多游戏项使用相同的网格。现在我们有一个游戏项数组,每项都指向同一个网格,如下图所示:</p>
<p><img alt="游戏项数组" src="../_static/13/game_item_list.png" /></p>
<p>作为替代,我们将创建一个网格映射表,其中包括储存共享该网格的所有游戏项。</p>
<p><img alt="网格映射表" src="../_static/13/mesh_map.png" /></p>
<p>对于每一个<code>Mesh</code>,渲染步骤将会是:</p>
<ol>
<li>获取与<code>GameItem</code>相关联的<code>Mesh</code>并绑定<code>Mesh</code>纹理绑定VAO并启用其属性。</li>
<li>对于每个相关的<code>GameItem</code>
a. 设置模型观察矩阵(每个<code>GameItem</code>唯一的)。
b. 调用绘制三角形。</li>
<li>解绑纹理和VAO。</li>
</ol>
<p><code>Scene</code>类中储存下述<code>Map</code></p>
<pre><code class="language-java">private Map&lt;Mesh, List&lt;GameItem&gt;&gt; meshMap;
</code></pre>
<p>我们仍有<code>setGameItems</code>方法,但我们不只是储存数组,而是构造网格映射表。</p>
<pre><code class="language-java">public void setGameItems(GameItem[] gameItems) {
int numGameItems = gameItems != null ? gameItems.length : 0;
for (int i=0; i&lt;numGameItems; i++) {
GameItem gameItem = gameItems[i];
Mesh mesh = gameItem.getMesh();
List&lt;GameItem&gt; list = meshMap.get(mesh);
if ( list == null ) {
list = new ArrayList&lt;&gt;();
meshMap.put(mesh, list);
}
list.add(gameItem);
}
}
</code></pre>
<p><code>Mesh</code>类现在有一个方法来渲染与其相关的<code>GameItem</code>列表,然后将绑定和解绑代码分为不同的方法。</p>
<pre><code class="language-java">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&lt;GameItem&gt; gameItems, Consumer&lt;GameItem&gt; consumer) {
initRender();
for (GameItem gameItem : gameItems) {
// 设置游戏项目所需的渲染数据
consumer.accept(gameItem);
// 渲染游戏项目
glDrawElements(GL_TRIANGLES, getVertexCount(), GL_UNSIGNED_INT, 0);
}
endRender();
}
</code></pre>
<p>如你所见,我们仍然保留了旧方法,它返回一个<code>Mesh</code>,这是考虑到如果只有一个<code>GameItem</code>的情况(这可能在其他情况下使用,这就是为什么不移除它)。新的方法渲染一个<code>List&lt;GameItem&gt;</code>,并接受一个<code>Counsumer</code>类型的参数一个函数使用了Java8引入的函数式编程它将用于在绘制三角形之前为每个<code>GameItem</code>设置特定的内容,因为不希望<code>Mesh</code>类与Uniform的名称和设置这些参数时所涉及的参数相耦合我们将使用它来设置模型观察矩阵。</p>
<p><code>Renderer</code>类中的<code>renderScene</code>方法你可以看到我们只需遍历网格映射表并通过Lambda表达式设置模型观察矩阵的Uniform。</p>
<pre><code class="language-java">for (Mesh mesh : mapMeshes.keySet()) {
sceneShaderProgram.setUniform(&quot;material&quot;, mesh.getMaterial());
mesh.renderList(mapMeshes.get(mesh), (GameItem gameItem) -&gt; {
Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(gameItem, viewMatrix);
sceneShaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
}
);
}
</code></pre>
<p>可以做的另一项优化是,我们在渲染周期中创建了大量对象。特别是,我们创建了太多的<code>Matrix4f</code>实例,它们为每个<code>GameItem</code>实例都保存了一个模型视图矩阵。我们应在<code>Transformation</code>类中创建特定的矩阵,并重用相同的实例。如果你查阅源代码,会看到我们已经更改了方法的名称,<code>getXX</code>方法只返回储存的矩阵实例,并且任何更改矩阵值的方法都称为<code>buildXX</code>,以阐明其用途。</p>
<p>我们也避免了每次为矩阵设置Uniform时构造新的<code>FloatBuffer</code>实例,并移除了其他一些无用的实例化操作。有了这些,你现在可以看到更流畅更灵活的渲染了。</p>
<p>你可以在源代码中查阅所有细节。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../14-height-maps/" class="btn btn-neutral float-right" title="高度图">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../12-game-hud/" class="btn btn-neutral" title="游戏HUD"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../12-game-hud/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../14-height-maps/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

513
14-height-maps/index.html Executable file
View File

@@ -0,0 +1,513 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>高度图 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u9ad8\u5ea6\u56fe";
var mkdocs_page_input_path = "14-height-maps.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">高度图</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>高度图</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/14-height-maps.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="height-maps">高度图Height Maps</h1>
<p>本章中我们将学习如何使用高度图创建复杂的地形。在开始前,你会注意到我们做了一些重构。我们创建了一些新的包和移动了一些类以更好地组织它们。你可以在源代码中了解这些改变。</p>
<p>所以什么是高度图?高度图是用于生成三维地形的图像,它使用像素颜色来获取表面高度。高度图图像通常是灰度图,它可以由<a href="http://planetside.co.uk/">Terragen</a>等软件生成。一张高度图图像看起来就像这样。</p>
<p><img alt="高度图" src="../_static/14/heightmap.png" /> </p>
<p>上图就像你俯视一片陆地一样。利用上图,我们将构建由顶点组成的三角形所组成的网格。每个顶点的高度将根据图像的每个像素的颜色来计算。黑色是最低,白色是最高。</p>
<p>我们将为图像的每个像素创建一组顶点,这些顶点将组成三角形,这些三角形将组成下图所示的网格。</p>
<p><img alt="高度图网格" src="../_static/14/heightmap_grid.png" /> </p>
<p>网格将组成一个巨大的四边形它将会在X和Z轴上渲染并根据像素颜色来改变它的Y轴高度。</p>
<p><img alt="高度图坐标系" src="../_static/14/heightmap_coordinates.png" /> </p>
<p>由高度图创建三维地形的过程可概括为以下步骤:
* 加载储存高度图的图像(我们将使用一个<code>BufferedImage</code>实例以获取每个像素)。
* 为每个图像像素创建一个顶点,其高度基于像素颜色。
* 将正确的纹理坐标分配给顶点。
* 设置索引以绘制与顶点相关的三角形。</p>
<p>我们将创建一个名为<code>HeightMapMesh</code>的类,该类将基于高度图按以上步骤创建一个<code>Mesh</code>。让我们先看看该类定义的常量:</p>
<pre><code class="language-java">private static final int MAX_COLOUR = 255 * 255 * 255;
</code></pre>
<p>如上所述我们将基于高度图图像的每个像素的颜色来计算每个顶点的高度。图像通常是灰度图对于PNG图像来说这意味着每个像素的每个RGB值可以在0到255之间变化因此我们有256个值来表示不同的高度。这可能足够了但如果精度不够我们可以使用三个RGB值以有更多的值在此情况下高度计算范围为0到255^3。我们将使用第二种方法因此我们不局限于灰度图。</p>
<p>接下来的常量是:</p>
<pre><code class="language-java">private static final float STARTX = -0.5f;
private static final float STARTZ = -0.5f;
</code></pre>
<p>网格将由一组顶点一个顶点对应一个像素构成其X和Z坐标的范围如下
* X轴的范围为[-0.5, 0.5],即[<code>STARTX</code>, <code>-STARTX</code>]。
* Z轴的范围为[-0.5, 0.5],即[<code>STARTZ</code>, <code>-STARTZ</code>]。</p>
<p>不用太过担心这些值稍后得到的网格可以被缩放以适应世界的大小。关于Y轴我们将设置<code>minY</code><code>maxY</code>两个参数用于设置Y坐标的最低和最高值。这些参数并不是常数因为我们可能希望在运行时更改它们而不使用缩放。最后地形将包含在范围为<code>[STARTX, -STARTX]</code><code>[minY, maxY]</code><code>[STARTZ, -STARTZ]</code>的立方体内。</p>
<p>网格将会在<code>HeightMapMesh</code>类的构造函数中创建,该类的定义如下。</p>
<pre><code class="language-java">public HeightMapMesh(float minY, float maxY, String heightMapFile, String textureFile, int textInc) throws Exception {
</code></pre>
<p>它接收Y轴的最小值和最大值被用作高度图的图像文件名和要使用的纹理文件名。它还接受一个名为<code>textInc</code>的整数,这稍后再说明。</p>
<p>我们在构造函数中做的第一件事就是将高度图图像加载到<code>BufferedImage</code>实例中。</p>
<pre><code class="language-java">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();
</code></pre>
<p>然后,我们将纹理文件载入到一个<code>ByteBuffer</code>中,并设置构造<code>Mesh</code>所需的变量。<code>incx</code><code>incz</code>变量将储存每个顶点的X或Z坐标之间的最小间隔因此<code>Mesh</code>包含在上述区域中。</p>
<pre><code class="language-java">Texture texture = new Texture(textureFile);
float incx = getWidth() / (width - 1);
float incz = Math.abs(STARTZ * 2) / (height - 1);
List&lt;Float&gt; positions = new ArrayList();
List&lt;Float&gt; textCoords = new ArrayList();
List&lt;Integer&gt; indices = new ArrayList();
</code></pre>
<p>之后,我们将遍历图像,为每个像素创建一个顶点,设置其纹理坐标与索引,以正确地定义组成<code>Mesh</code>的三角形。</p>
<pre><code class="language-java">for (int row = 0; row &lt; height; row++) {
for (int col = 0; col &lt; 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 &lt; width - 1 &amp;&amp; row &lt; 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);
}
}
}
</code></pre>
<p>创建顶点坐标的过程是不需要解释的。现在先别管为什么我们用一个数字乘以纹理坐标以及如何计算高度。你可以看到,对于每个顶点,我们定义两个三角形的索引(除非现在是最后一行或最后一列)。让我们用一个<strong>3×3</strong>的图像来想象它们是如何构造的。一个<strong>3×3</strong>的图像包含9个顶点每因此有<strong>2×4</strong>个三角形组成4个正方形。下图展示了网格每个顶点被命名为<code>Vrc</code>(rc列)。</p>
<p><img alt="高度图顶点" src="../_static/14/heightmap_vertices.png" /></p>
<p>当处理第一个顶点(V00)时,我们在红色阴影处定义了两个三角形的索引。</p>
<p><img alt="高度图索引I" src="../_static/14/heightmap_indices_i.png" /> </p>
<p>当处理第二个顶点(V01)时,我们在红色阴影处又定义了两个三角形的索引。但当处理第三个顶点(V02)时,我们不需要定义更多的索引,该行的所有三角形都已被定义。</p>
<p><img alt="高度图索引II" src="../_static/14/heightmap_indices_ii.png" /> </p>
<p>你可以很容易地想到其他顶点的处理过程是如何进行的。现在,一旦创建了所有的顶点位置、纹理坐标和索引,我们就只需要用所有这些数据创建<code>Mesh</code>和相关的<code>Material</code></p>
<pre><code class="language-java">float[] posArr = Utils.listToArray(positions);
int[] indicesArr = indices.stream().mapToInt(i -&gt; 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);
</code></pre>
<p>你可以看到,我们根据顶点位置计算法线。在看如何计算法线之前,来看看如何获取高度吧。我们已经创建了一个名为<code>getHeight</code>的方法,它负责计算顶点的高度。</p>
<pre><code class="language-java">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 &amp; a) &lt;&lt; 24) | ((0xFF &amp; r) &lt;&lt; 16)
| ((0xFF &amp; g) &lt;&lt; 8) | (0xFF &amp; b);
return this.minY + Math.abs(this.maxY - this.minY) * ((float) argb / (float) MAX_COLOUR);
}
</code></pre>
<p>该方法接受像素的X和Y坐标图像的宽度以及与之相关的<code>ByteBuffer</code>返回RGB颜色(R、G、B分量之和)并计算包含在<code>minY</code><code>maxY</code>之间的值(<code>minY</code>为黑色,<code>maxY</code>为白色)。</p>
<p>你可以使用<code>BufferedImage</code>来编写一个更简单的方法它有更方便的方法来获得RGB值但这将使用AWT。记住AWT不能很好的兼容OSX所以尽量避免使用它的类。</p>
<p>现在来看看如何计算纹理坐标。第一个方法是将纹理覆盖整个网格,左上角的顶点纹理坐标为(0, 0),右下角的顶点纹理坐标为(1, 1)。这种方法的问题是,纹理必须是巨大的,以便获得良好的渲染效果,否则纹理将会被过度拉伸。</p>
<p>但我们仍然可以使用非常小的纹理,通过使用高效的技术来获得很好的效果。如果我们设置超出[1, 1]范围的纹理坐标,我们将回到原点并重新开始计算。下图表示在几个正方形中平铺相同的纹理,并超出了[1, 1]范围。</p>
<p><img alt="纹理坐标I" src="../_static/14/texture_coordinates_i.png" /> </p>
<p>这是我们在设置纹理坐标时所要做的。我们将一个参数乘以纹理坐标(计算好像整个网格被纹理包裹的情况),即<code>textInc</code>参数,以增加在相邻顶点之间使用的纹理像素数。</p>
<p><img alt="纹理坐标II" src="../_static/14/texture_coordinates_ii.png" /> </p>
<p>目前唯一没有解决的是法线计算。记住我们需要法线,光照才能正确地应用于地形。没有法线,无论光照如何,地形将以相同的颜色渲染。我们在这里使用的方法不一定是最高效的,但它将帮助你理解如何自动计算法线。如果你搜索其他解决方案,可能会发现更有效的方法,只使用相邻点的高度而不需要做交叉相乘操作。尽管如此,这仅需要在启动时完成,这里的方法不会对性能造成太大的损害。</p>
<p>让我们用图解的方式解释如何计算一个法线值。假设我们有一个名为<strong>P0</strong>的顶点。我们首先计算其周围每个顶点(<strong>P1</strong>, <strong>P2</strong>, <strong>P3</strong>, <strong>P4</strong>)和与连接这些点的面相切的向量。这些向量(<strong>V1</strong>, <strong>V2</strong>, <strong>V3</strong>, <strong>V4</strong>)是通过将每个相邻点与<strong>P0</strong>相减(例如<strong>V1 = P1 - P0</strong>)得到的。</p>
<p><img alt="法线计算I" src="../_static/14/normals_calc_i.png" /> </p>
<p>然后,我们计算连接每一个相邻点的平面的法线。这是与之前计算得到的向量交叉相乘计算的。例如,向量<strong>V1</strong><strong>V2</strong>所在的平面(蓝色阴影部分)的法线是由<strong>V1</strong><strong>V2</strong>交叉相乘得到的,即<strong>V12 = V1 × V2</strong></p>
<p><img alt="法线计算II" src="../_static/14/normals_calc_ii.png" /> </p>
<p>如果我们计算完毕其他平面的法线(<strong>V23 = V2 × V3</strong><strong>V34 = V3 × V4</strong><strong>V41 = V4 × V1</strong>),则法线<strong>P0</strong>就是周围所有平面法线(归一化后)之和:<strong>N0 = V12 + V23 + V34 + V41</strong></p>
<p><img alt="法线计算III" src="../_static/14/normals_calc_iii.png" /></p>
<p>法线计算的方法实现如下所示。</p>
<pre><code class="language-java">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&lt;Float&gt; normals = new ArrayList&lt;&gt;();
Vector3f normal = new Vector3f();
for (int row = 0; row &lt; height; row++) {
for (int col = 0; col &lt; width; col++) {
if (row &gt; 0 &amp;&amp; row &lt; height -1 &amp;&amp; col &gt; 0 &amp;&amp; col &lt; 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);
}
</code></pre>
<p>最后,为了创建更大的地形,我们有两个选择:
* 创建更大的高度图
* 重用高度图并将其平铺在三维空间中。高度图将像一个地形块,在世界上像瓷砖一样平移。为了做到这一点,高度图边缘的像素必须是相同的(左侧边缘必须与右侧相同,上侧边缘必须与下侧相同),以避免块之间的间隙。</p>
<p>我们将使用第二种方法(并选择适当的高度图)。为了做到它,我们将创建一个名为<code>Terrain</code>的类,该类将创建一个正方形的高度图块,定义如下。</p>
<pre><code class="language-java">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 &lt; blocksPerRow; row++) {
for (int col = 0; col &lt; 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;
}
}
</code></pre>
<p>让我们详解整个过程,我们拥有由以下坐标定义的块(X和Z使用之前定义的常量)。</p>
<p><img alt="地形构建I" src="../_static/14/terrain_construction_1.png" /></p>
<p>假设我们创建了一个由3×3块网格构成的地形。我们假设我们不会缩放地形块(也就是说,变量<code>blocksPerRow</code><strong>3</strong>而变量<code>scale</code>将会是<strong>1</strong>)。我们希望网格的中央在坐标系的(0, 0)。</p>
<p>我们需要移动块,这样顶点就变成如下坐标。</p>
<p><img alt="地形构建II" src="../_static/14/terrain_construction_2.png" /></p>
<p>移动是通过调用<code>setPosition</code>方法实现的但记住我们所设置的是一个位移而不是一个位置。如果你看到上图你会发现中央块不需要任何移动它已经定位在适当的坐标上。绘制绿色顶点需要在X轴上位移<strong>-1</strong>而绘制蓝色顶点需要在X轴上位移<strong>+1</strong>。计算X位移的公式要考虑到缩放和块的宽度公式如下</p>
<p>
<script type="math/tex; mode=display">xDisplacement=(col - (blocksPerRow -1 ) / 2) \times scale \times width</script>
</p>
<p>Z位移的公式为</p>
<p>
<script type="math/tex; mode=display">zDisplacement=(row - (blocksPerRow -1 ) / 2) \times scale \times height</script>
</p>
<p>如果在<code>DummyGame</code>类中创建一个<code>Terrain</code>实例,我们可以得到如图所示的效果。</p>
<p><img alt="地形结果" src="../_static/14/terrain_result.png" /> </p>
<p>你可以在地形周围移动相机,看看它是如何渲染的。由于还没有实现碰撞检测,你可以穿过它并从上面看它。由于我们已经启用了面剔除,当从下面观察时,地形的某些部分不会渲染。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../15-terrain-collisions/" class="btn btn-neutral float-right" title="地形碰撞">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../13-sky-box-and-some-optimizations/" class="btn btn-neutral" title="天空盒与一些优化"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../13-sky-box-and-some-optimizations/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../15-terrain-collisions/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

495
15-terrain-collisions/index.html Executable file
View File

@@ -0,0 +1,495 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>地形碰撞 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u5730\u5f62\u78b0\u649e";
var mkdocs_page_input_path = "15-terrain-collisions.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">地形碰撞</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>地形碰撞</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/15-terrain-collisions.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="terrain-collisions">地形碰撞Terrain Collisions</h1>
<p>此前我们创建了一个地形,接下来就是检测碰撞以避免穿过它。回忆一下之前的内容,一个地形是由地形块组成的,每个地形块都是由高度图生成的,高度图用于设置构成地形的三角形的顶点高度。</p>
<p>为了检测碰撞,我们必须将当前所在位置的<strong>Y</strong>值与当前地形点的<strong>Y</strong>值进行比较。如果有碰撞,我们需要回到地形上方。很简单的想法,是吗?确实是这样,但在比较之前,我们需要进行几次计算。</p>
<p>我们首先要定义的是我们对“当前位置”这个词的理解。由于我们还没有一个球员的概念,答案很简单,当前的位置将是相机的位置。因此,我们已经有了比较的组成部分之一,因此,接下来要计算的是当前位置的地形高度。
首先要定义的是“当前位置”这个词的概念。由于我们还没有一个“玩家”的概念,因此当前位置将是摄像机的位置。这样我们就有了比较的一方,因此接下来要计算当前位置的地形高度。</p>
<p>如上所是,地形由地形块组成,如下图所示。</p>
<p><img alt="地形网格" src="../_static/15/terrain_grid.png" /></p>
<p>每个地形块都是由相同的高度图网格构成,但被精确地缩放和位移,以形成看起来像是连续的景观的地形网格。</p>
<p>所以首先要做的是确定当前位置(摄像机位置)在哪个地形块。为了得到它,我们将基于位移和缩放来计算每个地形块的包围盒(<strong>BoundingBox</strong>)。因为地形在运行时不会移动或缩放,所以我们可以在<code>Terrain</code>类的构造方法中计算。这样就可以在任何时候访问它们,而不需要在每个游戏循环周期中重复这些计算。</p>
<p>我们将创建一个新的方法来计算一个地形块的包围盒,名为<code>getBoundingBox</code></p>
<pre><code class="language-java">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;
}
</code></pre>
<p><code>Box2D</code><code>java.awt.Rectangle2D.Float</code>类的简化版本为了避免使用AWT而创建。</p>
<p>限制我们需要计算地形块的世界坐标。在上一章中,你看到所有的地形网格都是在一个正方形中创建的,它的原点设置为<code>[STARTX, STARTZ]</code>。因此,我们需要把这些坐标转换为世界坐标,这要考虑下图所示的位移与缩放。</p>
<p><img alt="模型坐标系到世界坐标系" src="../_static/15/model_to_world_coordinates.png" /></p>
<p>如上所述,这可以在<code>Terrain</code>类构造方法中计算,因为它不会在运行时发生变化,所以我们要添加一个新的属性来保存包围盒:</p>
<pre><code class="language-java">private final Box2D[][] boundingBoxes;
</code></pre>
<p><code>Terrain</code>类的构造方法中,当我们创建地形块时,只需调用计算包围盒的方法。</p>
<pre><code class="language-java">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 &lt; terrainSize; row++) {
for (int col = 0; col &lt; 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);
}
}
}
</code></pre>
<p>因此,有了所有预先计算的包围盒,我们将创建一个新的方法,这个方法将以当前位置为参数,返回对应地形高度。该方法名为<code>getHeight</code>,其定义如下。</p>
<pre><code class="language-java">public float getHeight(Vector3f position) {
float result = Float.MIN_VALUE;
// 对于每个地形块,我们获取包围盒,将其转换到观察坐标系
// 检查坐标是否包含在包围盒中
Box2D boundingBox = null;
boolean found = false;
GameItem terrainBlock = null;
for (int row = 0; row &lt; terrainSize &amp;&amp; !found; row++) {
for (int col = 0; col &lt; terrainSize &amp;&amp; !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;
}
</code></pre>
<p>在此方法中第一件事是确定我们所在的地形块。由于我们已经有了每个地形块的包围盒,所以算法很简单。我们只需要迭代包围盒数组,并检查当前位置是否位于其中(<code>Box2D</code>提供了该方法)。</p>
<p>一旦找到了地形块,我们需要计算所处的三角形,这是由之后的<code>getTriangle</code>方法计算的。之后,我们得到了所在三角形的坐标,包括它的高度。但是,我们需要的是一个点的高度,这个点不位于这些顶点中的任何一点,而位于它们之间的位置。这将在<code>interpolateHeight</code>方法中计算,我们也将解释这是如何计算的。</p>
<p>让我们先从确定所处的三角形开始。构成地形块的正方形可以看作一个网格,其中每个单元由两个三角形组成。首先我们定义一些变量:</p>
<ul>
<li><strong>boundingBox.x</strong>是与包围盒相关联的地形块的原<strong>x</strong>坐标。</li>
<li><strong>boundingBox.y</strong>是与包围盒相关联的地形块的原<strong>z</strong>坐标(即使你看到一个<strong>y</strong>,但它是在<strong>z</strong>轴的)。</li>
<li><strong>boundingBox.width</strong>是地形块正方形的宽度。</li>
<li><strong>boundingBox.height</strong>是地形块正方形的高度。</li>
<li><strong>cellWidth</strong>是一个单元的宽度。</li>
<li><strong>cellHeight</strong>是一个单元的高度。</li>
</ul>
<p>上面定义的所有变量都用世界坐标来表示。为了计算单元的宽度,我们只需要将包围盒宽度除以每列的顶点数:</p>
<p>
<script type="math/tex; mode=display">cellWidth = \frac{boundingBox.width}{verticesPerCol}</script>
</p>
<p><code>cellHeight</code>的计算也相似:</p>
<p>
<script type="math/tex; mode=display">cellHeight = \frac{boundingBox.height}{verticesPerRow}</script>
</p>
<p>一旦有了这些变量,我们就可以计算所在的单元格的行和列了:</p>
<p>
<script type="math/tex; mode=display">col = \frac{position.x - boundingBox.x}{boundingBox.width}</script>
</p>
<p>
<script type="math/tex; mode=display">row = \frac{position.z - boundingBox.y}{boundingBox.height}</script>
</p>
<p>下图在示例地形块展示了此前描述的所有变量。</p>
<p><img alt="地形块变量" src="../_static/15/terrain_block_variables_n.png" /></p>
<p>有了这些信息,就可以计算单元格中包含的三角形顶点的位置。我们怎么才能做到呢?让我们来看看组成一个单元格的三角形。</p>
<p><img alt="单元格" src="../_static/15/cell.png" /></p>
<p>你可以看到,单元格是被一个对角线分开为两个三角形的。确定与当前位置相关的三角形的方法,是检查<strong>z</strong>坐标在对角线的上方还是下方。在本例中,将对角线的<strong>x</strong>值设置为当前位置的<strong>x</strong>值,求出对应的对角线<strong>z</strong>值,如果当前位置的<strong>z</strong>值小于对角线的<strong>z</strong>值,那么我们在<strong>T1</strong>中。反之如果当前位置的<strong>z</strong>值大于对角线的<strong>z</strong>值,我们就在<strong>T2</strong>中。</p>
<p>我们可以通过计算与对角线相匹配的直线方程来确定。</p>
<p>如果你还记得学校的数学课,从两点通过的直线(在二维中)的方程为:</p>
<p>
<script type="math/tex; mode=display">y-y1=m\cdot(x-x1)</script>
</p>
<p>其中m是直线的斜率也就是说当沿<strong>x</strong>轴移动时,其高度会发生变化。请注意,在本例中,<strong>y</strong>坐标其实是一个<strong>z</strong>。还要注意的是,我们使用的是二维坐标,因为在这里不计算高度,只要<strong>x</strong>坐标和<strong>z</strong>坐标就足够了。因此,在本例中,直线方程应该是这样。</p>
<p>
<script type="math/tex; mode=display">z-z1=m\cdot(x-x1)</script>
</p>
<p>斜率可以按如下方式计算:</p>
<p>
<script type="math/tex; mode=display">m=\frac{z1-z2}{x1-x2}</script>
</p>
<p>所以给定一个<strong>x</strong>坐标得到一个<strong>z</strong>值的对角线方程就像这样:</p>
<p>
<script type="math/tex; mode=display">z=m\cdot(xpos-x1)+z1=\frac{z1-z2}{x1-x2}\cdot(zpos-x1)+z1</script>
</p>
<p>其中<strong>x1</strong><strong>x2</strong><strong>z1</strong><strong>z2</strong>分别是顶点<strong>V1</strong><strong>V2</strong><strong>x</strong><strong>z</strong>坐标。</p>
<p>因此,通过上述方式来获得当前位置所在的三角形的方法,名为<code>getTriangle</code>,其实现如下:</p>
<pre><code class="language-java">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 &lt; 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;
}
</code></pre>
<p>你可以看到我们有另外两个反复。第一个名为<code>getDiagonalZCoord</code>,给定<strong>x</strong>位置和两个顶点计算对角线的<strong>z</strong>坐标。另一个名为<code>getWorldHeight</code>,用来获得三角形顶点的高度(即<strong>y</strong>坐标)。当地形网格被创建时,每个顶点的高度都被预先计算和储存,我们只需将其转换为世界坐标。</p>
<p>好,我们有当前位置的三角形坐标。最后,我们准备在当前位置计算地形高度。怎么做呢?我们的三角形在一个平面上,一个平面可以由三个点定义,在本例中,三个顶点定义了一个三角形。</p>
<p>平面方程如下:</p>
<p>
<script type="math/tex; mode=display">a\cdot x+b\cdot y+c\cdot z+d=0</script>
</p>
<p>上述方程的常数值是:</p>
<p>
<script type="math/tex; mode=display">a=(B_{y}-A_{y}) \cdot (C_{z} - A_{z}) - (C_{y} - A_{y}) \cdot (B_{z}-A_{z})</script>
</p>
<p>
<script type="math/tex; mode=display">b=(B_{z}-A_{z}) \cdot (C_{x} - A_{x}) - (C_{z} - A_{z}) \cdot (B_{z}-A_{z})</script>
</p>
<p>
<script type="math/tex; mode=display">c=(B_{x}-A_{x}) \cdot (C_{y} - A_{y}) - (C_{x} - A_{x}) \cdot (B_{y}-A_{y})</script>
</p>
<p>其中<strong>A</strong><strong>B</strong><strong>C</strong>是定义平面所需的三个顶点。</p>
<p>然后,利用之前的方程以及当前位置的<strong>x</strong><strong>z</strong>坐标值,我们能够计算<strong>y</strong>值,即当前位置的地形高度:</p>
<p>
<script type="math/tex; mode=display">y = (-d - a \cdot x - c \cdot z) / b</script>
</p>
<p>实现了如上运算的方法如下:</p>
<pre><code class="language-java">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;
}
</code></pre>
<p>这就完了!现在我们能够检测碰撞,所以在<code>DummyGame</code>类中,在更新摄像机位置时,修改如下代码:</p>
<pre><code class="language-java">// 更新摄像机位置
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 &lt;= height ) {
camera.setPosition(prevPos.x, prevPos.y, prevPos.z);
}
</code></pre>
<p>如你所见,检测地形碰撞的概念很容易理解,但是我们需要仔细地进行计算并了解正处理的不同坐标系。</p>
<p>此外,虽然这里给出的算法在大多数情况下都是可用的,但仍存在需要仔细处理的情况。你可以发现的一个问题是隧道效应(<code>Tunnelling</code>)。设想一个情况,我们正以高速穿过地形,正因如此,位置增量值较高。这个值变得如此之高,以至于因为我们检测的是最终位置的碰撞,所以可能已经穿过了位于两点之间的障碍。</p>
<p><img alt="隧道效应" src="../_static/15/tunnelling.png" /></p>
<p>有许多可行的解决方案可以避免这个效应,最简单的解决方法是将要进行的计算分成增量较小的多份。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../16-fog/" class="btn btn-neutral float-right" title="雾">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../14-height-maps/" class="btn btn-neutral" title="高度图"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../14-height-maps/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../16-fog/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

427
16-fog/index.html Executable file
View File

@@ -0,0 +1,427 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>雾 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u96fe";
var mkdocs_page_input_path = "16-fog.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./"></a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li></li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/16-fog.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="fog">Fog</h1>
<p>在处理更复杂的问题之前,我们将学习如何在游戏引擎中创建雾特效。有了这个特效,就可以模拟遥远的物体变暗,似乎消失在浓雾中。</p>
<p>让我们来确定定义雾的属性是什么。第一个是雾的颜色。在现实世界中,雾是灰色的,但我们可以利用这个特效来模拟不同颜色的雾覆盖的区域。还有一个属性是雾的浓度。</p>
<p>因此为了使用雾特效我们需要找到一种方法当3D场景的物体远离摄像机时使它们褪色到雾的颜色。靠近摄像机的物体不会受到雾的影响但远处的物体将无法分辨。因此我们需要计算一个参数可以用来混合雾的颜色与每个片元的颜色以模拟雾特效。这个参数取决于与摄像机相距的距离。</p>
<p>让我们把这个参数命名为<code>fogFactor</code>并设定它的范围为0到1。当<code>fogFactor</code>为1时意味着物体完全不会受到雾的影响也就是说它是较近的物体。当<code>fogFactor</code>为0时意味着物体完全隐藏在雾中。</p>
<p>然后,计算雾色的方程如下:</p>
<p>
<script type="math/tex; mode=display">finalColour = (1 - fogFactor) \cdot fogColour + fogFactor \cdot framentColour</script>
</p>
<ul>
<li><strong>finalColour</strong> 是使用雾特效的最终颜色。</li>
<li><strong>fogFactor</strong> 是控制雾的颜色与片元的颜色如何混合的参数,它基本上控制物体的可见性。</li>
<li><strong>fogColour</strong> 是雾的颜色。</li>
<li><strong>fragmentColour</strong> 没有使用雾特效的片元颜色。</li>
</ul>
<p>现在我们需要找到一种方法来基于距离计算<strong>fogFactor</strong>。我们可以选择不同的模型,首先使用线性模型。这是一个给定距离以线性方式改变<strong>fogFactor</strong>的模型。</p>
<p>线性模型由以下参数定义:</p>
<ul>
<li><strong>fogStart</strong>: 开始使用雾特效的距离。</li>
<li><strong>fogFinish</strong>: 雾特效达到最大值的距离。</li>
<li><strong>distance</strong>: 到摄像机的距离。</li>
</ul>
<p>有了这些参数,方程就会是这样的:</p>
<p>
<script type="math/tex; mode=display">\displaystyle fogFactor = \frac{(fogFinish - distance)}{(fogFinish - fogStart)}</script>
</p>
<p>对于距离低于<strong>fogStart</strong>的物体我们简单地设置<strong>fogFactor</strong><strong>1</strong>。下图表明了<strong>fogFactor</strong>是如何随着距离变化而变化的。</p>
<p><img alt="线性模型" src="../_static/16/linear_model.png" /></p>
<p>线性模型易于计算,但不太真实,因为它不考虑雾气浓度。实际上雾往往以更平滑的方式增加。所以下一个合适的模型是指数模型。该模型的方程如下:</p>
<p>
<script type="math/tex; mode=display">\displaystyle focFactor = e^{-(distance \cdot fogDensity)^{exponent}} = \frac{1}{e^{(distance \cdot fogDensity)^{exponent}}}</script>
</p>
<p>其中的新变量如下所述:</p>
<ul>
<li><strong>fogDensity</strong> 是雾的厚度或浓度。</li>
<li><strong>exponent</strong> 用来控制雾随着距离的增加增长的速度。</li>
</ul>
<p>下图显示了两个图形,分别设置了不同的<strong>exponent</strong>值(蓝线为<strong>2</strong>,红线为<strong>4</strong>)。</p>
<p><img alt="指数模型" src="../_static/16/exponential_model.png" /></p>
<p>在代码中,我们将使用一个公式,让它可以为<strong>exponent</strong>设置不同的值(你可以很容易地修改示例以使用不同的值)。</p>
<p>既然已经解释过这个原理了,我们就可以实现它。我们将在场景的片元着色器中实现雾特效,因为这有我们需要的所有变量。我们将首先定义一个储存雾属性的结构体。</p>
<pre><code class="language-glsl">struct Fog
{
int active;
vec3 colour;
float density;
};
</code></pre>
<p><code>active</code>属性用于激活或关闭雾特效。雾属性将通过另一个被称作<code>fog</code>的Uniform传递给着色器。</p>
<pre><code class="language-glsl">uniform Fog fog;
</code></pre>
<p>我们还将创建一个包含着雾属性的名为<code>Fog</code>的新类它是另一个POJOPlain Ordinary Java Object简单的Java对象</p>
<pre><code class="language-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….
</code></pre>
<p>我们将在<code>Scene</code>类中添加一个<code>Fog</code>示例。默认情况下,<code>Scene</code>类将初始化一个<code>Fog</code>示例到常量<code>NOFOG</code>,用于模拟关闭雾特效的情况。</p>
<p>因为添加了一个新的Uniform类型所以我们需要修改<code>ShaderProgram</code>类来创建和初始化雾的Uniform。</p>
<pre><code class="language-java">public void createFogUniform(String uniformName) throws Exception {
createUniform(uniformName + &quot;.active&quot;);
createUniform(uniformName + &quot;.colour&quot;);
createUniform(uniformName + &quot;.density&quot;);
}
public void setUniform(String uniformName, Fog fog) {
setUniform(uniformName + &quot;.activeFog&quot;, fog.isActive() ? 1 : 0);
setUniform(uniformName + &quot;.colour&quot;, fog.getColour() );
setUniform(uniformName + &quot;.density&quot;, fog.getDensity());
}
</code></pre>
<p><code>Renderer</code>类中,我们只需要在<code>setupSceneShader</code>方法中创建Uniform</p>
<pre><code class="language-java">sceneShaderProgram.createFogUniform(&quot;fog&quot;);
</code></pre>
<p>然后在<code>renderScene</code>方法中使用它:</p>
<pre><code class="language-java">sceneShaderProgram.setUniform(&quot;fog&quot;, scene.getFog());
</code></pre>
<p>我们现在可以在游戏中定义雾特效,但是需要回到片元着色器中应用雾特效。我们将创建一个名为<code>calcFog</code>的函数,函数定义如下。</p>
<pre><code class="language-glsl">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);
}
</code></pre>
<p>如你所见,我们首先计算到顶点的距离。顶点坐标定义在<code>pos</code>变量中,我们只需要计算长度。然后利用<strong>exponent</strong>为2的指数模型相当于乘以两次计算雾参数。我们得到的<code>fogFactor</code>的范围在<strong>0</strong><strong>1</strong>之间,并使用<code>mix</code>函数。在GLSL中<code>min</code>函数被用于混合雾色和图元颜色(由颜色变量定义)。相当于使用如下方程:</p>
<p>
<script type="math/tex; mode=display">resultColour = (1 - fogFactor) \cdot fog.colour + fogFactor \cdot colour</script>
</p>
<p>我们还为颜色保留了<strong>w</strong>元素,即透明度。我们不希望这个元素受到影响,片元应该保持它的透明程度不变。</p>
<p>在应用了所有的光效之后,在片元着色器的最后,如果雾特效启用的话,我们将简单地把返回值设置为片元颜色。</p>
<pre><code class="language-glsl">if ( fog.activeFog == 1 )
{
fragColor = calcFog(mvVertexPos, fragColor, fog);
}
</code></pre>
<p>所有这些代码完成后,我们可以用下面的数据设置一个雾特效:</p>
<pre><code class="language-java">scene.setFog(new Fog(true, new Vector3f(0.5f, 0.5f, 0.5f), 0.15f));
</code></pre>
<p>然后我们将获得像这样的效果:</p>
<p><img alt="雾特效" src="../_static/16/fog_effect.png" /></p>
<p>你会看到远处的物体褪色,当你靠近它们时,雾开始消失。但有一个问题,天空盒看起来有点奇怪,地平线不受雾的影响。有几种方法可以解决这个问题:</p>
<ul>
<li>使用只能看到天空的另一个不同的天空盒。</li>
<li>删除天空盒,因为有浓雾,你不应该能够看到一个背景。</li>
</ul>
<p>也可能这两个解决方案没有适合你的,你可以试着将雾色与天空盒的背景相匹配,但这样你会做复杂的计算,结果也许不会更好。</p>
<p>如果你运行这个示例,你会感到平行光变得暗淡,场景变暗,但雾看起来有问题,因为它不受光的影响,会看到如下图所示的结果。</p>
<p><img alt="发光的雾" src="../_static/16/glowing_fog.png" /></p>
<p>远处的物体变为雾色,这是一个不受光影响的常数。这造成了一个在黑暗中发光的效果(这可能并不好)。我们需要修改计算雾的函数,让其考虑光照。该函数将接收环境光和平行光来调整雾色。</p>
<pre><code class="language-glsl">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);
}
</code></pre>
<p>如你所见,平行光我们仅使用了颜色和强度,我们不需要关注它的方向。这样,我们只需要稍微修改函数的调用:</p>
<pre><code class="language-glsl">if ( fog.active == 1 )
{
fragColor = calcFog(mvVertexPos, fragColor, fog, ambientLight, directionalLight);
}
</code></pre>
<p>在夜晚时,我们会看到这样的效果。</p>
<p><img alt="夜晚的雾" src="../_static/16/fog_at_night.png" /></p>
<p>一个要强调的重要的事情是,我们必须聪明地选择雾色。这是很重要的,当我们没有天空盒,但有固定的颜色背景,应该把雾色设置为背景色。如果你删除了天空盒的代码并重新运行示例代码,你会得到这样的结果。</p>
<p><img alt="黑色背景" src="../_static/16/fog_clear_colour_black.png" /></p>
<p>但如果我们把背景色修改为0.5, 0.5, 0.5),最终结果看起来就是如下所示。</p>
<p><img alt="雾灰色背景" src="../_static/16/fog_clear_colour_grey.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../17-normal-mapping/" class="btn btn-neutral float-right" title="法线贴图">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../15-terrain-collisions/" class="btn btn-neutral" title="地形碰撞"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../15-terrain-collisions/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../17-normal-mapping/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

407
17-normal-mapping/index.html Executable file
View File

@@ -0,0 +1,407 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>法线贴图 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u6cd5\u7ebf\u8d34\u56fe";
var mkdocs_page_input_path = "17-normal-mapping.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">法线贴图</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>法线贴图</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/17-normal-mapping.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="normal-mapping">法线贴图Normal Mapping</h1>
<p>本章中将讲解一项技术它将极大地改善我们的3D模型的外观。到目前为止我们已经能够将纹理使用到复杂的3D模型上但这还离真实物体的样子很远。现实世界中的物体表面不是完全光滑的它们有我们的3D模型目前所不具有的瑕疵。</p>
<p>为了渲染更真实的场景,我们将渲染<strong>法线贴图(Normal Mapping)</strong>。如果你在现实世界中看到一个平面你会发现那些瑕疵甚至可以在很远的距离看到。在3D场景中平面没有瑕疵我们可以将纹理应用在它之上但这不会改变光反射的方式。这就是为什么与现实有区别的原因。</p>
<p>我们可以考虑通过增加三角形数量来增加模型的细节并反映出这些瑕疵,但性能会下降。我们需要的是改变表面光反射的方式来增加真实感。这就是用法线贴图技术实现的。</p>
<p>让我们看看光滑平面的例子,一个平面由两个三角形组成为一个四边形。回忆之前的光照章节,模型的光反射的要素是平面法线。在此情况下,我们整个平面仅有单一的法线,当计算光如何影响片元时,每个片元都使用相同的法线。看起来就像下图那样。</p>
<p><img alt="平面法线" src="../_static/17/surface_normals.png" /></p>
<p>如果可以改变平面的每个片元的法线,我们就可以模拟平面的瑕疵,使它们更逼真。看起来就像下图那样。</p>
<p><img alt="片元法线" src="../_static/17/fragment_normals.png" /></p>
<p>要做到这一点我们要加载另一个纹理它储存面的法线。法线纹理的每个像素将以RGB值储存法线的<strong>x</strong><strong>y</strong><strong>z</strong>坐标值。</p>
<p>让我们用下面的纹理绘制一个四边形。</p>
<p><img alt="纹理" src="../_static/17/rock.png" /></p>
<p>上图的法线纹理如下所示。</p>
<p><img alt="法线纹理" src="../_static/17/rock_normals.png" /></p>
<p>如你所见,如果我们把颜色变换应用到原始纹理,每个像素使用颜色分量储存法线信息。在看到法线贴图时,你常常会看到主色调倾向于蓝色,这是由于大多数法线指向转换正<strong>z</strong>轴所致。在一个平面表面的矢量中,<strong>z</strong>分量通常比<strong>x</strong><strong>y</strong>分量的值高得多。由于<strong>x</strong><strong>y</strong><strong>z</strong>坐标被映射到RGB导致蓝色分量也有着更高的值。</p>
<p>因此,使用法线贴图渲染对象只需要一个额外的纹理,并同时使用它渲染片元以获得适当的法线值。</p>
<p>让我们开始修改代码,以支持法线贴图。我们将添加一个新的<code>Texture</code>实例到<code>Material</code>类,这样就可以把一个法线贴图纹理添加到游戏项目上。此实例将有自己的<code>get</code><code>set</code>方法,并有方法可以检查<code>Material</code>是否有法线贴图。</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p>我们将在场景的片元着色器中使用法线贴图纹理。但是,由于我们在观察坐标空间操作,所以需要通过模型观察矩阵来进行适当的变换。因此,我们需要修改场景的顶点着色器。</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>在场景的片元着色器中,我们需要添加另一个输入参数。</p>
<pre><code class="language-glsl">in mat4 outModelViewMatrix;
</code></pre>
<p>在片元着色器中我们需要为法线贴图纹理采样器传递一个新的Uniform</p>
<pre><code class="language-glsl">uniform sampler2D texture_sampler;
</code></pre>
<p>此外,在片元着色器中,我们将创建一个新函数,该函数计算当前片元的法线。</p>
<pre><code class="language-glsl">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;
}
</code></pre>
<p>该函数有以下参数:
* <code>Material</code>实例。
* 顶点法线。
* 纹理坐标。
* 模型视图矩阵。</p>
<p>我们在此函数中做的第一件事是检查这个<code>Material</code>实例是否有法线贴图。如果没有,我们就像平常一样简单地使用顶点法线。如果它有法线贴图,我们使用储存着法线数据的法线纹理关联到当前纹理坐标。</p>
<p>记住我们取得的颜色是法线坐标但因为它们被储存为RGB值所以它们的值在范围<script type="math/tex; mode=display">0, 1</script>中。我们需要将其范围转换为<script type="math/tex; mode=display">-1, 1</script>所以我们将其乘以2然后减去1。然后我们将它归一化并将其转换到模型观察坐标系就像顶点法线那样</p>
<p>就这样,我们可以使用返回值作为片元所有光照计算中的法线。</p>
<p><code>Renderer</code>类中,我们需要创建法线贴图,在<code>renderScene</code>方法中,我们做如下设置:</p>
<pre><code class="language-java">//...
sceneShaderProgram.setUniform(&quot;fog&quot;, scene.getFog());
sceneShaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
sceneShaderProgram.setUniform(&quot;normalMap&quot;, 1);
//...
</code></pre>
<p>上述代码中你可能注意到一些有趣的事情。我们将材质纹理Uniform<code>texture_sampler</code>)设置为<strong>0</strong>,然后将法线贴图纹理(<code>normalMap</code>)设置为<strong>1</strong>。如果你回想纹理章节,我们不止使用一个纹理,所以我们必须为每个单独的纹理设置纹理单元。</p>
<p>当我们渲染<code>Mesh</code>时,也需要考虑这一点。</p>
<pre><code class="language-java">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);
}
</code></pre>
<p>如你所见,我们需要绑定每个可用的纹理,并激活相关的纹理单元,以便多个纹理能够同时工作。在<code>Renderer</code>类的<code>renderScene</code>方法中我们不需要显式设置纹理的Uniform因为它已经包含在<code>Material</code>中。</p>
<p>为了展示法线贴图带来的提升我已经创建了两个并排显示的四边形示例。右边的有一张法线贴图而左边没有。我们还删除了地形、天空盒和HUD并设置了平行光照可以用鼠标左右键改变方向这样你就可以看到效果了。我已经修改了基本源代码以支持关闭天空盒和地形。我们还在片元着色器中设置了光效果的范围为<script type="math/tex; mode=display">0, 1</script>,以避免图像过度曝光。</p>
<p>结果如下图所示。</p>
<p><img alt="法线贴图结果" src="../_static/17/normal_mapping_result.png" /></p>
<p>如你所见,具有法线贴图的四边形感觉更大。虽然它本质上是一个像其他四边形一样的平面,但你可以看到光线是如何反射的。</p>
<p>尽管我们已经编写的代码完全符合这个示例,但你需要知道它的局限性。该代码仅适用与使用模型坐标空间创建的法线贴图纹理。如果是这种情况,我们可以使用模型视图矩阵来将法线坐标转换到观察空间。</p>
<p>但通常法线贴图不是这样定义的。它们通常被定义在所谓的<strong>切线空间(<code>Tangent Space</code></strong>中。切线空间是一个坐标系,它定位在模型的每个三角形,其坐标系的<strong>z</strong>轴总是垂直于表面。这就是为什么当你看一个法线贴图时,它通常是蓝色的,甚至对于面相对复杂的模型也是如此。</p>
<p>我们现在仍使用这个简单的实现,但请记住,你必须总是使用在模型空间中定义的法线贴图。如果使用切线空间中定义的贴图,就会变得很奇怪。为了能够让它们一同工作,我们需要使用特定的矩阵来将坐标变换到切线空间。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../18-shadows/" class="btn btn-neutral float-right" title="阴影">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../16-fog/" class="btn btn-neutral" title="雾"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../16-fog/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../18-shadows/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

563
18-shadows/index.html Executable file
View File

@@ -0,0 +1,563 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>阴影 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u9634\u5f71";
var mkdocs_page_input_path = "18-shadows.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">阴影</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">阴影映射</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">改进阴影映射</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>阴影</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/18-shadows.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="shadows">阴影Shadows</h1>
<h2 id="_1">阴影映射</h2>
<p>目前我们能够表现光线如何影响三维场景中的对象。接收到更多光的物体比没有接收光的物体更亮。然而我们仍无法投射阴影。阴影能增加3D场景的真实度因此我们将在本章中添加对它的支持。</p>
<p>我们将使用一种被称为阴影映射Shadow Mapping的技术这种技术被广泛使用于游戏中且不会严重影响引擎性能。阴影映射看起来很容易理解但是很难正确地实现它。或者更准确地说很难用一种通用的涵盖了一切可能并产生一致的效果的方法去实现它。</p>
<p>我们将在此说明一种方法它可以为你在大多数情况下添加阴影但更重要的是它将帮助你了解其局限性。这里介绍的代码远不是完美的但我认为它很容易理解。它还被设计用于支持有向光这我认为是更复杂的情况但你将了解如何将其扩展以支持其他类型的光照例如点光源。如果想获得更高级的效果你应该使用更高级的技术例如级联阴影映射Cascaded Shadow Maps。在任何情况下这里解释的概念都仅仅作为基础。</p>
<p>所以,让我们从思考如何检查特定区域(实际上是片元)是否在阴影中开始。在绘制这个区域的时候,我们可以发出射线投射到光源上,如果我们可以在不发生任何碰撞的情况下到达光源,那么像素就在光照中,反之,像素处于阴影中。</p>
<p>下图展示了点光源的情况点PA可以到达光源但点PB和PC不行因此它们位于阴影中。</p>
<p><img alt="阴影概述I" src="../_static/18/shadow_concepts_I.png" /></p>
<p>那么我们如何才能检查是否能以一种有效的方式发射出不发生碰撞的射线呢理论上光源可以投射出无限的光线Name我们如何检查光线是否被遮挡</p>
<p>我们能做的不是投射光线而是从光线透视图中观察3D场景并从该位置渲染场景。我们可以将相机设置在光源位置并渲染场景以便储存每个片元的深度。这相当于计算每个片元到光源的距离。最后我们要做的是将光照所及的最小距离储存为阴影图。</p>
<p>下图展示了一个悬浮在平面上并垂直于光线的立方体。</p>
<p><img alt="阴影概述II" src="../_static/18/shadow_concepts_II.png" /></p>
<p>从光源的角度看,情况是这样的(颜色越深,越接近光源)。</p>
<p><img alt="从光源的角度渲染" src="../_static/18/render_light_perspective.png" /></p>
<p>利用这些信息。我们可以像往常一样渲染3D场景并以最小储存距离检查每个每个片元到光源的距离。如果距离小于阴影贴图中储存的值则对象位于光照中否则位于阴影中。我们可以让几个物体被同一光照照射但我们储存最小距离。</p>
<p>因此,阴影映射分为两步:</p>
<ul>
<li>首先,我们将场景从光照空间渲染为阴影图,以获得最小距离。</li>
<li>其次,我们从摄像机的视角渲染场景。并使用深度图计算对象是否位于阴影中。</li>
</ul>
<p>为了渲染深度图我们需要说说深度缓冲区Depth-buffer。当我们渲染一个场景时所有深度信息都储存在一个名为“深度缓冲区”又称“Z缓冲区Z-buffer的缓冲区中。深度信息是渲染的每个片元的<script type="math/tex">z</script>值。如果你从第一章回忆我们在渲染场景时,将正在渲染的场景从世界坐标转换为屏幕坐标。我们所绘制的坐标空间,对于<script type="math/tex">x</script><script type="math/tex">y</script>轴来说,坐标的范围为<script type="math/tex">0</script><script type="math/tex">1</script>。如果一个物体比其他对象原,我们必须通过透视投影矩阵计算它如何影响其<script type="math/tex">x</script><script type="math/tex">y</script>坐标。这不是根据<script type="math/tex">z</script>值自动计算的,它必须由我们来做。实际储存在<script type="math/tex">z</script>坐标中的是它在片元的深度,仅此而已。</p>
<p>此外,在源代码中,我们启用了深度测试。在<code>Window</code>类中,我们添加如下行:</p>
<pre><code class="language-glsl">glEnable(GL_DEPTH_TEST);
</code></pre>
<p>通过添加这行,我们可以防止无法看到的片元被绘制出来,因为他们位于其他对象之后。在绘制片元之前,它的<script type="math/tex">z</script>值将与Z缓冲区中的<script type="math/tex">z</script>值进行比较。如果它的<script type="math/tex">z</script>值(它的距离)大于缓冲区的<script type="math/tex">z</script>值,则会被丢弃。请记住,这是在屏幕空间中完成的,因此,给定一对屏幕空间中范围为<script type="math/tex">[0, 1]</script><script type="math/tex">x</script><script type="math/tex">y</script>坐标,我们比较其片元的<script type="math/tex">z</script>值。同样,<script type="math/tex">z</script>值也在此范围内。</p>
<p>深度缓冲区的存在是我们在执行任何渲染操作之前需要清除屏幕的原因。我们不仅需要清除颜色,还要清除深度信息:</p>
<pre><code class="language-java">public void clear() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
</code></pre>
<p>为了开始构建深度图,我们希望从光源的角度获得深度信息。我们需要在光源位置设置一个摄像头,渲染场景并将深度信息储存到纹理中,以便稍后访问它。</p>
<p>因此,我们首先需要做的是添加对创建这些纹理的支持。我们将修改<code>Texture</code>类,通过添加新的构造函数来支持创建空纹理。此构造函数需要纹理的尺寸以及它储存的像素的格式。</p>
<pre><code class="language-java">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);
}
</code></pre>
<p>我们将纹理环绕方式设置为<code>GL_CLAMP_TO_EDGE</code>,因为我们不希望在超出<script type="math/tex">[0, 1]</script>范围时重复纹理。</p>
<p>所以现在我们可以创建空的纹理我们需要能够在其中渲染一个场景。为了做到它我们需要使用帧缓冲区对象Frame Buffers Objects或称FBOs。帧缓冲区是可以作为渲染终点的缓冲区集合。当我们渲染到屏幕上时我们使用的是OpenGL的默认缓冲区。OpenGL允许我们使用FBO渲染到用户定义的缓冲区。我们将通过创建一个名为<code>ShadowMap</code>的新类来隔离为阴影映射创建FBO过程的其余代码。如下就是那个类的定义。</p>
<pre><code class="language-java">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(&quot;Could not create FrameBuffer&quot;);
}
// 解绑
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
public Texture getDepthMapTexture() {
return depthMap;
}
public int getDepthMapFBO() {
return depthMapFBO;
}
public void cleanup() {
glDeleteFramebuffers(depthMapFBO);
depthMap.cleanup();
}
}
</code></pre>
<p><code>ShadowMap</code>类定义了两个常量用于确定储存深度图的纹理的大小。它还定义了两个属性一个用于FBO一个用于纹理。在构造函数中创建一个新的FBO和一个新的<code>Texture</code>。对于FBO将使用常量<code>GL_DEPTH_COMPONENT</code>作为像素格式因为我们只对储存深度值感兴趣然后将FBO绑定到纹理实例。</p>
<p>以下几行代码显式地将FBO设置为不渲染任何颜色。FBO需要颜色缓冲区但我们不需要这就是为什么我们将颜色缓冲区设置为<code>GL_NONE</code></p>
<pre><code class="language-java">glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
</code></pre>
<p>现在,我们准备在<code>Renderer</code>类中将场景从灯光透视渲染为FBO。为了做到它我们将创建一组特殊的顶点和片元着色器。</p>
<p>名为<code>depth_vertex.vs</code>的顶点着色器的定义如下:</p>
<pre><code class="language-glsl">#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);
}
</code></pre>
<p>我们希望接收与场景着色器相同的输入数据。但实际上,我们只需要坐标,但是为了尽可能多地重用代码,我们还是要传送其他数据。我们还需要一对矩阵。记住,我们必须以光源的角度渲染场景,所以我们需要将模型转换到光源的坐标空间。这是通过<code>modelLightViewMatrix</code>矩阵完成的,该矩阵类似于用于摄像机的模型观察矩阵。现在光源是我们的摄像机。</p>
<p>然后我们需要将这些坐标转换到屏幕空间也就是说需要投影它们。这是计算平行光与点光源的阴影图时的区别之一。对于地昂扬我们将使用透视投影Perspective Projection矩阵就像我们正常渲染场景一样。相反平行光以相同方式影响所有对象而与距离无关。平行光源位于无穷远的点上没有位置只有方向。正交投影Orthographic Projection不会使远处的物体变小因此正交投影最适合平行光。</p>
<p>片元着色器更简单。它只输出<script type="math/tex">z</script>坐标作为深度值。</p>
<pre><code class="language-glsl">#version 330
void main()
{
gl_FragDepth = gl_FragCoord.z;
}
</code></pre>
<p>实际上,你可以删掉该行,因为我们只生成深度值,深度值将自动返回。</p>
<p>一旦我们为深度渲染定义了新的着色器,就可以在<code>Renderer</code>类中使用它们。我们为初始化这些着色器定义了一个新方法,名为<code>setupDepthShader</code>,它将在其他着色器被初始化时调用。</p>
<pre><code class="language-java">private void setupDepthShader() throws Exception {
depthShaderProgram = new ShaderProgram();
depthShaderProgram.createVertexShader(Utils.loadResource(&quot;/shaders/depth_vertex.vs&quot;));
depthShaderProgram.createFragmentShader(Utils.loadResource(&quot;/shaders/depth_fragment.fs&quot;));
depthShaderProgram.link();
depthShaderProgram.createUniform(&quot;orthoProjectionMatrix&quot;);
depthShaderProgram.createUniform(&quot;modelLightViewMatrix&quot;);
}
</code></pre>
<p>现在,我们需要创建一个新的方法,使用那些名为<code>renderDepthMap</code>的着色器。该方法将在主渲染方法中调用。</p>
<pre><code class="language-java">public void render(Window window, Camera camera, Scene scene, IHud hud) {
clear();
// 在设置视口之前渲染深度图
renderDepthMap(window, camera, scene);
glViewport(0, 0, window.getWidth(), window.getHeight());
// 其余的代码在这...
</code></pre>
<p>如果你浏览上述代码,将看到在设置视口之前,新方法就已经被调用。这是因为这个新方法将更高视口以匹配保存深度图的纹理的尺寸。因此,在完成<code>renderDepthMap</code>之后,我们将始终需要设置屏幕尺寸的视口(不检查窗口是否已调整大小)。</p>
<p>现在让我们定义一下<code>renderDepthMap</code>方法。第一件事是绑定在<code>ShadowMap</code>类中创建的FBO并设置视口以匹配纹理尺寸。</p>
<pre><code class="language-java">glBindFramebuffer(GL_FRAMEBUFFER, shadowMap.getDepthMapFBO());
glViewport(0, 0, ShadowMap.SHADOW_MAP_WIDTH, ShadowMap.SHADOW_MAP_HEIGHT);
</code></pre>
<p>然后,清除深度缓冲区内容并绑定深度着色器。因为我们只处理深度值,所以不需要清除颜色信息。</p>
<pre><code class="language-java">glClear(GL_DEPTH_BUFFER_BIT);
depthShaderProgram.bind();
</code></pre>
<p>现在我们需要设置矩阵,接下来是棘手的部分。我们使用光源作为摄像机,所以需要创建一个需要一个坐标和三个角的观察矩阵。正如本章开头所说,我们只实现平行光,这种类型的光不定义位置,而是定义方向。如果我们使用点光源,这很容易,光源的位置就是观察矩阵的位置,但我们没有位置。</p>
<p>我们将采用一种简单的方法来计算光的位置。平行光是由一个向量定义的,通常是归一化的,它指向光源所在的方向。我们把这个方向向量乘以一个可配置的因子,在这样它就为要绘制的场景定义了一个合理距离的点。我们将使用该方向来计算该观察矩阵的旋转角度。</p>
<p><img alt="光源位置" src="../_static/18/light_position.png" /></p>
<p>这是计算灯光位置与旋转角度的代码片段:</p>
<pre><code class="language-java">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));
</code></pre>
<p>接下我们需要计算正交投影矩阵:</p>
<pre><code class="language-java">Matrix4f orthoProjMatrix = transformation.updateOrthoProjectionMatrix(orthCoords.left, orthCoords.right, orthCoords.bottom, orthCoords.top, orthCoords.near, orthCoords.far);
</code></pre>
<p>我们已经修改了<code>Transformation</code>类,以囊括光照观察矩阵和正交投影矩阵。此们有一个正交的二维投影矩阵,所以我们重命名了此前的方法和属性,你可以直接查看源代码中的定义。</p>
<p>然后,我们按照<code>renderScene</code>方法渲染场景对象,但在光照空间坐标系中使用上述矩阵工作。</p>
<pre><code class="language-java">depthShaderProgram.setUniform(&quot;orthoProjectionMatrix&quot;, orthoProjMatrix);
Map&lt;Mesh, List&lt;GameItem&gt;&gt; mapMeshes = scene.getGameMeshes();
for (Mesh mesh : mapMeshes.keySet()) {
mesh.renderList(mapMeshes.get(mesh), (GameItem gameItem) -&gt; {
Matrix4f modelLightViewMatrix = transformation.buildModelViewMatrix(gameItem, lightViewMatrix);
depthShaderProgram.setUniform(&quot;modelLightViewMatrix&quot;, modelLightViewMatrix);
}
);
}
// 解绑
depthShaderProgram.unbind();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
</code></pre>
<p>正交投影矩阵的参数是在平行光中定义的。将正交投影矩阵想象为一个边界框其中包含我们要渲染的所有对象。当只投影适合该边界框的对象时对象将可视。该边界框由6个参数定义左、右、上、下、近、远。由于光源位置现在是原点因此这些参数定义为原点到左或右x轴或上或下y轴或远或近z轴的距离。</p>
<p>要使阴影图正常工作,最棘手的一点是确定灯光位置和正交投影矩阵的参数。这就是所有这些参数现在在<code>DirectionalLight</code>类中定义的方式,因此可以根据每个场景正确设置这些参数。</p>
<p>你可以实现一个更自动的方法通过计算摄像机截锥Frustum的中心回到光的方向建立一个包含场景中所有对象的正交投影。下图展示了如上所述的三维场景、相机位置、截锥蓝色、最佳光源位置以及红色的边界框。</p>
<p><img alt="通用光源位置计算" src="../_static/18/generic_light_pos_calculation.png" /></p>
<p>上述方法的问题是很难计算,如果你有很小的物体,并且边界框很大,你可以会得到奇怪的结果。这里介绍的方法对于小场景更简单,你可以调整它以匹配你的模型(即使你可以选择显式设置灯光的位置,以避免相机远离原点移动时产生奇怪的效果)。如果你想要一个更通用的模板,可以应用到任何场景,你应该扩展它,以支持层叠阴影图。</p>
<p>让我们继续。在使用深度图实际计算阴影之前,可以使用生成的纹理渲染一个正方形,以观察深度图的实际外观。在有一个旋转立方体漂浮在一个有垂直平行光的平面上的场景中,你可以得到如下结果。</p>
<p><img alt="深度图" src="../_static/18/depth_map.png" /></p>
<p>如上所述,颜色越深,离光源的位置越近。深度图中光源位置的影响是什么?你可以修改平行光照的倍增因子,将看到在纹理中渲染的对象的大小不会变小。记住,我们使用的是正交投影矩阵,物体不会随着距离增大而变小。你将看到的是,所有的颜色都会变得更亮,如下所示:</p>
<p><img alt="更远的距离的深度图" src="../_static/18/depth_map_higher_distance.png" /> </p>
<p>这是否意味着我们可以为光源选择一个较远的位置而不造成任何后果呢?答案是不行。如果光源离我们要渲染的对象太远,这些对象会超出正交投影矩阵定义的边界框。在此情况下,你会得到一个不错的白色纹理,但这是没用的阴影图。好的,那么我们只需增加边界框的大小,一切都会好的,对吗?答案也是不行。如果你为正交投影矩阵选用了巨大的尺寸,你的物体在纹理中会被绘制得很小,深度值甚至会重叠,造成奇怪的结果。好吧,所以你可以考虑增加纹理大小,但在此情况下,你是有限制的,纹理不能因使用巨大的编辑框而无限增大。</p>
<p>因此,可以看到,选择光源的位置和正交投影的参数是一个复杂的平衡,这使得使用阴影图很难得到正确的效果。</p>
<p>让我们回到渲染过程,一旦计算了深度图,我们就可以在渲染场景时使用它。首先,我们需要修改场景的顶点着色器。到目前为止,顶点着色器使用透视矩阵将顶点坐标从模型观察空间投影到屏幕空间。现在还需要使用投影矩阵从光照空间坐标投影顶点坐标,以用于片元着色器中计算阴影。</p>
<p>顶点着色器是这样修改的:</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>我们为光照观察矩阵和正交投影矩阵使用了新的Uniform。</p>
<p>在片元着色器中,我们将创建一个新的函数来计算阴影,代码如下:</p>
<pre><code class="language-glsl">float calcShadow(vec4 position)
{
float shadowFactor = 1.0;
vec3 projCoords = position.xyz;
// 从屏幕坐标变换到纹理坐标
projCoords = projCoords * 0.5 + 0.5;
if ( projCoords.z &lt; texture(shadowMap, projCoords.xy).r )
{
// 当前片元不在阴影中
shadowFactor = 0;
}
return 1 - shadowFactor;
}
</code></pre>
<p>该函数接收使用正交投影矩阵投影的光照观察空间的坐标。如果坐标在阴影中,则返回<script type="math/tex">0</script>,如果不在阴影中,则返回<script type="math/tex">1</script>。首先,将坐标转换为纹理坐标。屏幕坐标在<script type="math/tex">[-1, 1]</script>范围内,但纹理坐标在<script type="math/tex">[0, 1]</script>范围内。我们通过坐标从纹理中获取深度值,并将其与片元坐标的<script type="math/tex">z</script>值比较。如果<script type="math/tex">z</script>值低于储存在纹理中的值,则表示片元不再阴影中。</p>
<p>在片元着色器中,<code>calcShadow</code>函数的返回值,用于调节点光源、聚光源和平行光对光照颜色的共享。环境光不受阴影的影响。</p>
<pre><code class="language-glsl">float shadow = calcShadow(mlightviewVertexPos);
fragColor = clamp(ambientC * vec4(ambientLight, 1) + diffuseSpecularComp * shadow, 0, 1);
</code></pre>
<p><code>Renderer</code>类的<code>renderScene</code>方法中我们只需要传递正交投影和光照观察矩阵到Uniform我们还需要修改着色器的初始化方法以创建新的Uniform。你可以在本书的源代码中了解。</p>
<p>如果运行<code>DummyGame</code>类,该类已被修改为在带有平行光的平面上设置有悬浮的立方体,并可使用上下键修改角度,则应该看到如下情况。</p>
<p><img alt="阴影图结果" src="../_static/18/shadow_map_result.png" /></p>
<p>虽然阴影已经工作了你可以通过移动光照方向来检查但是实际会出现一些问题。首先被照亮的物体中有奇怪的线条。这种情况被称为阴影失真Shadow Acne它是由储存深度图的纹理的分辨率有限造成的。第二个问题是阴影的边界不平滑看起来很粗糙。原因同样纹理分辨率。我们将解决这些问题以提高阴影质量。</p>
<h2 id="_2">改进阴影映射</h2>
<p>既然我们已经有了阴影映射机制,那么让我们来解决现有的问题。我们先从失真问题开始。深度图纹理大小有限,因此,可以将多个片元映射到该纹理深度中的同一像素。纹理深度储存最小深度,因此到最后,我们有几个片元共享相同的深度,尽管它们的距离不同。</p>
<p>我们可以通过增加片元着色器中的深度比较来解决这个问题,我们添加了一个偏移。</p>
<pre><code class="language-glsl">float bias = 0.05;
if ( projCoords.z - bias &lt; texture(shadowMap, projCoords.xy).r )
{
// 当前片元不在阴影中
shadowFactor = 0;
}
</code></pre>
<p>现在,阴影失真消失了。</p>
<p><img alt="无阴影失真" src="../_static/18/shadow_no_acne.png" /></p>
<blockquote>
<p>译者注使用偏移来消除阴影失真又会造成悬浮Peter Panning问题另请参阅<a href="https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/">LearnOpenGL阴影映射</a>一文。</p>
</blockquote>
<p>现在我们要解决的是去阴影边缘问题,这也是由纹理分辨率引起的。对于每个片元,我们将使用片元的坐标值和周围的值对深度图进行采样。然后我们将计算平均值并将该值指定为阴影值。在此情况下,它的值不会是<script type="math/tex">0</script><script type="math/tex">1</script>但可以在两者间取值,以获得更平滑的边缘。</p>
<p><img alt="深度平均值" src="../_static/18/depth_average_value.png" /></p>
<p>在纹理坐标中,周围值必须与当前片元位置保持一个像素距离。所以我们需要计算纹理坐标中一个像素的增量,它等于<script type="math/tex">1 / 纹理大小</script></p>
<p>在片元着色器中,我们只需要修改阴影银子的计算来得到一个平均值。</p>
<pre><code class="language-glsl">float shadowFactor = 0.0;
vec2 inc = 1.0 / textureSize(shadowMap, 0);
for(int row = -1; row &lt;= 1; ++row)
{
for(int col = -1; col &lt;= 1; ++col)
{
float textDepth = texture(shadowMap, projCoords.xy + vec2(row, col) * inc).r;
shadowFactor += projCoords.z - bias &gt; textDepth ? 1.0 : 0.0;
}
}
shadowFactor /= 9.0;
</code></pre>
<p>现在结果看起来更平滑了。</p>
<p><img alt="最终结果" src="../_static/18/final_result.png" /></p>
<p>现在我们的示例看起来好多了。尽管如此这里介绍的阴影映射技术仍有很大的改进空间。你可以查看如何解决悬浮Peter Panning效果因偏移引起和其他改进阴影边缘的计算。无论如何有了这里所讲解的概念你就有了开始修改示例的良好基础。</p>
<p>为了渲染多个光源,你只需要为每个光源渲染一个深度图。在渲染场景时,你需要采样所有的深度图来计算合适的阴影系数。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../19-animations/" class="btn btn-neutral float-right" title="动画">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../17-normal-mapping/" class="btn btn-neutral" title="法线贴图"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../17-normal-mapping/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../19-animations/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

1004
19-animations/index.html Executable file

File diff suppressed because it is too large Load Diff

894
20-particles/index.html Executable file
View File

@@ -0,0 +1,894 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>粒子 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u7c92\u5b50";
var mkdocs_page_input_path = "20-particles.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">粒子</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">基础</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#texture-atlas">纹理集Texture Atlas</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>粒子</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/20-particles.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="particles">粒子Particles</h1>
<h2 id="_1">基础</h2>
<p>在本章中,我们将添加粒子效果到游戏引擎中。有了这种效果,我们就能模拟关系、火、灰尘和云。这是一种简单的效果,将改善对任何游戏的图形方面。</p>
<p>在此之前值得一提的是有很多方法可以实现不同效果的粒子效果。当前情况下我们将使用面板粒子Billboard Particle。该技术使用移动的纹理四边形来表示一个粒子它们总是面向观察者在本例中就是摄像机。你还可以使用面板技术在游戏项上显示信息面板比如迷你HUD。</p>
<p>让我们开始定义粒子,粒子可以通过以下属性定义:</p>
<ol>
<li>一个用于表示四边形顶点的网格。</li>
<li>一张纹理。</li>
<li>某一时刻的坐标。</li>
<li>缩放系数。</li>
<li>速度。</li>
<li>移动方向。</li>
<li>生存时间或存活时间。一旦该时间过去,粒子就不再存在。</li>
</ol>
<p>前四项是<code>GameItem</code>类的一部分,但后三项不是。因此,我们要创建一个名为<code>Particle</code>的新类,它继承了<code>GameItem</code>类,其定义如下:</p>
<pre><code class="language-java">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;
}
}
</code></pre>
<p>从上述代码可以看出粒子的速度和运动方向可以表示为一个向量。该向量的方向决定了粒子的运动方向和速度。粒子存活时间TTL被设定为毫秒计数器每当更新游戏状态时它都会减少。该类还有一个复制构造函数也就是说一个构造函数接收另一个粒子实例来进行复制。</p>
<p>现在,我们需要创建一个粒子生成器或粒子发射器,即一个动态生成粒子、控制其生命周期并根据特定的模式更新其位置的类。我们可以创建很多实现,它们在粒子的创建方式和位置的更新方式(例如,是否考虑重力)方面各不相同。因此,为了保持游戏引擎的通用性,我们将创建一个所有粒子发射器必须要实现的接口。这个名为<code>IParticleEmitter</code>的接口定义如下:</p>
<pre><code class="language-java">package org.lwjglb.engine.graph.particles;
import java.util.List;
import org.lwjglb.engine.items.GameItem;
public interface IParticleEmitter {
void cleanup();
Particle getBaseParticle();
List&lt;GameItem&gt; getParticles();
}
</code></pre>
<p><code>IParticleEmitter</code>接口有一个清理资源的方法,名为<code>cleanup</code>,还有一个获取粒子列表的方法,名为<code>getParticles</code>。还有一个名为<code>getBaseParticle</code>的方法,但是这个方法是做什么的呢?一个粒子发射器将动态地产生许多例子。每当一个粒子过期,就会创建新的粒子。该粒子更新周期将使用基础粒子作为模板创建新的势力。这就是基础粒子的用途,这也是为什么<code>Particle</code>类定义了一个复制构造函数。</p>
<p>在游戏引擎的代码中,我们将只引用<code>IParticleEmitter</code>接口,因此基础代码将不依赖于特定的实现。不过,我们可以创建一个实现来模拟不受重力影响的粒子流。这个实现可以用来模拟光线或火焰,名为<code>FlowParticleEmitter</code></p>
<p>这个类的行为可以通过以下属性进行调整:</p>
<ul>
<li>一次能存在的最大粒子数量</li>
<li>创建粒子的最短周期。粒子将在最短的时间内一个接一个地创建,以避免粒子爆发性创建。</li>
<li>一组范围,以随机粒子速度和位置。新粒子将使用基础粒子的位置和速度,可以在相应范围内取值,以分散光线。</li>
</ul>
<p>该类的实现如下:</p>
<pre><code class="language-java">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&lt;GameItem&gt; 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&lt;&gt;();
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&lt;GameItem&gt; 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&lt;? extends GameItem&gt; it = particles.iterator();
while (it.hasNext()) {
Particle particle = (Particle) it.next();
if (particle.updateTtl(ellapsedTime) &lt; 0) {
it.remove();
} else {
updatePosition(particle, ellapsedTime);
}
}
int length = this.getParticles().size();
if (now - lastCreationTime &gt;= this.creationPeriodMillis &amp;&amp; length &lt; maxParticles) {
createParticle();
this.lastCreationTime = now;
}
}
private void createParticle() {
Particle particle = new Particle(this.getBaseParticle());
// 添加一些随机的粒子
float sign = Math.random() &gt; 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();
}
}
}
</code></pre>
<p>现在,我们可以拓展<code>Scene</code>类中包含的数据,使其包含一个<code>ParticleEmitter</code>的实例数组。</p>
<pre><code class="language-java">package org.lwjglb.engine;
// 这是导入……
public class Scene {
// 这有更多属性……
private IParticleEmitter[] particleEmitters;
</code></pre>
<p>在该阶段,我们可以开始渲染粒子。粒子不会受到光的影响,也不会产生任何音乐。它们不会有任何骨骼动画,所以用特定的着色器渲染它们是没有意义的。着色器非常简单,它们只会使用投影和模型观察矩阵渲染顶点,并使用纹理设置颜色。</p>
<p>顶点着色器的定义如下:</p>
<pre><code class="language-glsl">#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;
}
</code></pre>
<p>片元着色器的定义如下:</p>
<pre><code class="language-glsl">#version 330
in vec2 outTexCoord;
in vec3 mvPos;
out vec4 fragColor;
uniform sampler2D texture_sampler;
void main()
{
fragColor = texture(texture_sampler, outTexCoord);
}
</code></pre>
<p>如你所见,它们非常简单,就像渲染一章中使用的着色器。现在,和其他章节一样,我们需要在<code>Renderer</code>类中设置和使用这些着色器。着色器的设置将在一个名为<code>setupParticlesShader</code>的方法中完成,其定义如下:</p>
<pre><code class="language-java">private void setupParticlesShader() throws Exception {
particlesShaderProgram = new ShaderProgram();
particlesShaderProgram.createVertexShader(Utils.loadResource(&quot;/shaders/particles_vertex.vs&quot;));
particlesShaderProgram.createFragmentShader(Utils.loadResource(&quot;/shaders/particles_fragment.fs&quot;));
particlesShaderProgram.link();
particlesShaderProgram.createUniform(&quot;projectionMatrix&quot;);
particlesShaderProgram.createUniform(&quot;modelViewMatrix&quot;);
particlesShaderProgram.createUniform(&quot;texture_sampler&quot;);
}
</code></pre>
<p>现在我们可以在Renderer类中创建渲染方法<code>renderParticles</code>,定义如下:</p>
<pre><code class="language-java">private void renderParticles(Window window, Camera camera, Scene scene) {
particlesShaderProgram.bind();
particlesShaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
Matrix4f projectionMatrix = transformation.getProjectionMatrix();
particlesShaderProgram.setUniform(&quot;projectionMatrix&quot;, projectionMatrix);
Matrix4f viewMatrix = transformation.getViewMatrix();
IParticleEmitter[] emitters = scene.getParticleEmitters();
int numEmitters = emitters != null ? emitters.length : 0;
for (int i = 0; i &lt; numEmitters; i++) {
IParticleEmitter emitter = emitters[i];
Mesh mesh = emitter.getBaseParticle().getMesh();
mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(gameItem, viewMatrix);
particlesShaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
}
);
}
particlesShaderProgram.unbind();
}
</code></pre>
<p>如果你努力阅读上述代码应该是不言自明的它只是设置必要的Uniform并渲染每个粒子。现在我们已经创建了测试粒子效果实现所需的所有方法只需要修改<code>DummyGame</code>类,我们就可以创建粒子发射器和基本粒子的特性。</p>
<pre><code class="language-java">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(&quot;/models/particle.obj&quot;);
Texture texture = new Texture(&quot;/textures/particle_tmp.png&quot;);
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});
</code></pre>
<p>我们现在使用一个普通填充圆作为粒子的纹理,以便更好地理解发生了什么。如果你运行它,你会看到如下所示的东西:</p>
<p><img alt="粒子I" src="../_static/20/particles_i.png" /></p>
<p>为什么一些粒子似乎被切断了?为什么透明的背景不能解决这个问题?原因是深度测试。粒子的一些片元被丢弃,因为它们具有比该区域的深度缓冲的当前值高的深度值。我们可以通过将其与摄像机之间的距离来排序粒子以解决这个问题,或者我们可以禁用深度写入。</p>
<p>在绘制粒子之前我们需要插入这一行代码:</p>
<pre><code class="language-java">glDepthMask(false);
</code></pre>
<p>然后在我们完成渲染之后还原为先前值:</p>
<pre><code class="language-java">glDepthMask(true);
</code></pre>
<p>然后我们会得到如下所示的东西:</p>
<p><img alt="粒子II" src="../_static/20/particles_ii.png" /></p>
<p>好了,问题解决。然而,我们仍想应用另一种效果,我们希望颜色被混合,因此颜色将被添加,以达成更好的效果。这是在渲染前增加如下一行代码来实现的:</p>
<pre><code class="language-java">glBlendFunc(GL_SRC_ALPHA, GL_ONE);
</code></pre>
<p>与深度的情况一样,渲染完所有粒子后,我们将混合函数恢复为:</p>
<pre><code class="language-java">glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
</code></pre>
<p>现在我们得到这样的效果:</p>
<p><img alt="粒子III" src="../_static/20/particles_iii.png" /></p>
<p>但我们还没有完成它。如果你把摄像机移到蓝色正方形的上方往下看,你可能会得到这样的东西:</p>
<p><img alt="粒子IV" src="../_static/20/particles_iv.png" /></p>
<p>这些粒子看起来不太好,它们应该是圆的,但现在看起来像一张纸。在此之上,我们应该应用面板技术。用于渲染粒子的四边形应该始终面向摄像机,与摄像机方向完全垂直,就好像根本没有旋转一样。摄像机的矩阵将位移和旋转应用于场景中的每一个对象,我们想跳过将要应用的旋转。</p>
<p>警告:在讲数学知识时,如果你觉得不舒服,你可以跳过它。让我们再次回顾那个观察矩阵。该矩阵可以像这样表示(没有应用任何缩放)。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{r_{00}} & \color{red}{r_{10}} & \color{red}{r_{20}} & \color{blue}{dx} \\
\color{red}{r_{01}} & \color{red}{r_{11}} & \color{red}{r_{21}} & \color{blue}{dy} \\
\color{red}{r_{02}} & \color{red}{r_{12}} & \color{red}{r_{22}} & \color{blue}{dz} \\
0 & 0 & 0 & 1
\end{bmatrix}</script>
</script>
</p>
<p>红色的元素代表摄像机的旋转蓝色的元素代表位移。我们需要取消观察矩阵中的左上角3x3矩阵的旋转效果所以它会变成这样</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{1} & \color{red}{0} & \color{red}{0} & \color{blue}{dx} \\
\color{red}{0} & \color{red}{1} & \color{red}{0} & \color{blue}{dy} \\
\color{red}{0} & \color{red}{0} & \color{red}{1} & \color{blue}{dz} \\
0 & 0 & 0 & 1
\end{bmatrix}</script>
</script>
</p>
<p>在左上角的红色部分我们有一个3x3矩阵把它命名为<script type="math/tex">M_{r}</script>并且我们想把它转换成单位矩阵:<script type="math/tex">I</script>。任何矩阵乘以它的逆矩阵都会得到单位矩阵:<script type="math/tex">M_{r} \times M_{r}^{-1} = I</script>。我们只需要从观察矩阵中取左上角的3x3矩阵然后乘以它的逆矩阵但是还可以优化他。一个旋转矩阵有一个有趣的定理它的逆矩阵与其转置矩阵相等。即<script type="math/tex">M_{r} \times M_{r}^{-1} = M_{r} \times M_{r}^{T} = I</script>。转置矩阵比逆矩阵更容易计算。矩阵的转置就像将其反转过来,将每一列与每一行替换。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
r_{00} & r_{10} & r_{20} \\
r_{01} & r_{11} & r_{21} \\
r_{02} & r_{12} & r_{22}
\end{bmatrix}</script>^{T}
=
<script type="math/tex; mode=display">\begin{bmatrix}
r_{00} & r_{01} & r_{02} \\
r_{10} & r_{11} & r_{12} \\
r_{20} & r_{21} & r_{22}
\end{bmatrix}</script>
</script>
</p>
<p>好的,让我们总结一下。我们有该变换:<script type="math/tex">V \times M</script>,其中<script type="math/tex">V</script>是观察矩阵,<script type="math/tex">M</script>是模型矩阵。我们可以这样表达:</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{10}} & \color{red}{v_{20}} & v_{30} \\
\color{red}{v_{01}} & \color{red}{v_{11}} & \color{red}{v_{21}} & v_{31} \\
\color{red}{v_{02}} & \color{red}{v_{12}} & \color{red}{v_{22}} & v_{32} \\
v_{03} & v_{13} & v_{23} & v_{33}
\end{bmatrix}</script>
\times
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{m_{00}} & \color{red}{m_{10}} & \color{red}{m_{20}} & m_{30} \\
\color{red}{m_{01}} & \color{red}{m_{11}} & \color{red}{m_{21}} & m_{31} \\
\color{red}{m_{02}} & \color{red}{m_{12}} & \color{red}{m_{22}} & m_{32} \\
m_{03} & m_{13} & m_{23} & m_{33}
\end{bmatrix}</script>
</script>
</p>
<p>我们想要取消观察矩阵的旋转,得到这样的结果:</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{1} & \color{red}{0} & \color{red}{0} & mv_{30} \\
\color{red}{0} & \color{red}{1} & \color{red}{0} & mv_{31} \\
\color{red}{0} & \color{red}{0} & \color{red}{1} & mv_{32} \\
mv_{03} & mv_{13} & mv_{23} & mv_{33}
\end{bmatrix}</script>
</script>
</p>
<p>所以我们只需要将模型矩阵的左上3x3矩阵设为观察矩阵上3x3部分的转置矩阵。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{10}} & \color{red}{v_{20}} & v_{30} \\
\color{red}{v_{01}} & \color{red}{v_{11}} & \color{red}{v_{21}} & v_{31} \\
\color{red}{v_{02}} & \color{red}{v_{12}} & \color{red}{v_{22}} & v_{32} \\
v_{03} & v_{13} & v_{23} & v_{33}
\end{bmatrix}</script>
\times
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{01}} & \color{red}{v_{02}} & m_{30} \\
\color{red}{v_{10}} & \color{red}{v_{11}} & \color{red}{v_{12}} & m_{31} \\
\color{red}{v_{20}} & \color{red}{v_{21}} & \color{red}{v_{22}} & m_{32} \\
m_{03} & m_{13} & m_{23} & m_{33}
\end{bmatrix}</script>
</script>
</p>
<p>但在这之后,我们去掉了缩放,实际上真正想要达到的结果是这样:</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{sx} & \color{red}{0} & \color{red}{0} & mv_{30} \\
\color{red}{0} & \color{red}{sy} & \color{red}{0} & mv_{31} \\
\color{red}{0} & \color{red}{0} & \color{red}{sz} & mv_{32} \\
mv_{03} & mv_{13} & mv_{23} & mv_{33}
\end{bmatrix}</script>
</script>
</p>
<p>其中sxsy和sz就是缩放系数。因此当我们将模型矩阵的左上3x3矩阵设置为观察矩阵的转置矩阵后我们需要再次应用缩放。</p>
<p>就这些,我们只需要在<code>renderParticlesMethod</code>中像这样修改:</p>
<pre><code class="language-java"> for (int i = 0; i &lt; numEmitters; i++) {
IParticleEmitter emitter = emitters[i];
Mesh mesh = emitter.getBaseParticle().getMesh();
mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
Matrix4f modelMatrix = transformation.buildModelMatrix(gameItem);
viewMatrix.transpose3x3(modelMatrix);
Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(modelMatrix, viewMatrix);
modelViewMatrix.scale(gameItem.getScale());
particlesShaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
}
);
}
</code></pre>
<p>我们还在<code>Transformation</code>类中添加了另一种方法,使用两个矩阵来构造模型观察矩阵,而不是使用<code>GameItem</code>和观察矩阵。</p>
<p>有了如上更改,当从上方观察粒子时,我们就得到如下结果:</p>
<p><img alt="粒子V" src="../_static/20/particles_v.png" /></p>
<p>现在集齐了创建一个更真实的粒子效果所需要的所有要素,所以让我们将其改为更精细的纹理。我们将使用如下图片(它是由<a href="https://www.gimp.org/">GIMP</a>创作的,带有光照和阴影过滤器):</p>
<p><img alt="粒子纹理" src="../_static/20/particle_texture.png" /></p>
<p>有了如上纹理,我们会得到如下所示的粒子:</p>
<p><img alt="粒子VI" src="../_static/20/particles_vi.png" /></p>
<p>现在更好了!你可能会注意到我们需要调整缩放,因为粒子现在总是对着摄像机,显示的面积总是最大的。</p>
<p>最后,再提一点,为了得到可以在任何场景使用的完美的效果,你需要实现粒子排序和启用深度缓冲区。无论如何,这里有一个示例可以将这种效果囊括到你的游戏中。</p>
<h2 id="texture-atlas">纹理集Texture Atlas</h2>
<p>现在我们已经做好了粒子效果的基础建设现在可以为它添加一些动画效果了。为了实现它我们将支持纹理集。纹理集Texture Atlas是一个包含所有将要使用的纹理的大型图片。使用纹理集我们就只需要加载一个大的图片然后再绘制游戏项时选择该图像的一部分作为纹理。例如当我们想用不同的纹理多次渲染相同的模型时例如树或岩石可以使用这种技术。我们可以使用相同的纹理集并选择适当的坐标而不是使用很多纹理实例并在它们之间切换记住切换状态总是很慢的</p>
<p>在此情况下,我们将使用纹理坐标来添加粒子动画。我们遍历不同的纹理来为粒子动画建模,所有这些纹理将被分到一个像这样的纹理集:</p>
<p><img alt="纹理集" src="../_static/20/texture_atlas.png" /></p>
<p>纹理集可以被划分为多个方形片段。我们将一个方形片段坐标分配到一个粒子上,并随着时间推移改变它以表示动画。让我们开始吧。我们要做的第一件事是修改<code>Texture</code>类来指定纹理集可以拥有的行数和列数。</p>
<pre><code class="language-java">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;
}
</code></pre>
<p>默认情况下我们处理的纹理的列数和行数等于1。我们还添加了另一个构造函数来指定行和列。</p>
<p>然后,我们需要追踪一个<code>GameItem</code>在纹理集中的坐标因此只需向该类添加另一个属性默认值为0。</p>
<pre><code class="language-java">package org.lwjglb.engine.items;
import org.joml.Vector3f;
import org.lwjglb.engine.graph.Mesh;
public class GameItem {
// 更多属性省略
private int textPos;
</code></pre>
<p>然后我们修改<code>Particle</code>类,以便能够通过纹理集自动迭代。</p>
<pre><code class="language-java">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;
</code></pre>
<p>属性<code>updateTextureMillis</code>定义移动到纹理集中下一个坐标的时间(以毫秒为单位)。数值月底,粒子在纹理上变化的速度就越快。属性<code>currentAnimTimeMillis</code>只是跟踪纹理持续当前纹理坐标的时间。</p>
<p>因此,我们需要修改<code>Particle</code>类构造函数来设置这些值。我们还计算了纹理集的片段数量,它是由属性<code>animFrames</code>定义的。</p>
<pre><code class="language-java">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();
}
</code></pre>
<p>现在,我们只需要修改检查粒子是否已经过期的方法,来检查是否需要更新纹理坐标。</p>
<pre><code class="language-java">public long updateTtl(long elapsedTime) {
this.ttl -= elapsedTime;
this.currentAnimTimeMillis += elapsedTime;
if ( this.currentAnimTimeMillis &gt;= this.getUpdateTextureMillis() &amp;&amp; this.animFrames &gt; 0 ) {
this.currentAnimTimeMillis = 0;
int pos = this.getTextPos();
pos++;
if ( pos &lt; this.animFrames ) {
this.setTextPos(pos);
} else {
this.setTextPos(0);
}
}
return this.ttl;
}
</code></pre>
<p>除此之外,我们还修改了<code>FlowRangeEmitter</code>类,在应该改变粒子纹理坐标的时间周期上增加了一些随机性。你可以在源代码上查看它。</p>
<p>现在,我们可以使用这些数据来设置合适的纹理坐标。我们将在顶点着色器中进行这一操作,因为它输出了要在片元着色器中使用的那些值。这个新着色器的定义如下:</p>
<pre><code class="language-glsl">#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);
}
</code></pre>
<p>如你所见我们现在有三个新Uniform。Uniform<code>numCols</code><code>numRows</code>只储存纹理集的列和行数。为了计算纹理坐标,首先必须缩小这些参数。每个片段的宽度为<script type="math/tex">1 / numCols</script>,高度为<script type="math/tex">1 / numRows</script>,如下图所示。</p>
<p><img alt="纹理坐标" src="../_static/20/texture_coordinates.png" /></p>
<p>然后我们只需要根据行和列应用和偏移,这是由<code>texXOffset</code><code>texYOffset</code>Uniform定义的。</p>
<p>我们将在<code>Renderer</code>类中计算这些偏移量,如下述代码所示。我们根据每个粒子的坐标计算它们所处的行和列,并将偏移量计算为片段高度和宽度的倍数。</p>
<pre><code class="language-java">mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
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(&quot;texXOffset&quot;, textXOffset);
particlesShaderProgram.setUniform(&quot;texYOffset&quot;, textYOffset);
</code></pre>
<p>注意如果你只需要支持正方形纹理集你只需要两个Uniform。最终的效果是这样的</p>
<p><img alt="粒子动画" src="../_static/20/animated_particles.png" /></p>
<p>现在,我们有了粒子动画。在下章中,我们讲学习如何优化渲染流程。我们正在渲染具有相同网格的多个元素,并为每个元素进行绘制调用。在下章中,我们讲学习如何在单个调用中渲染它们。这种技术不仅适用于粒子,也适用于渲染共享同一模型,但被放在不同位置或具有不同纹理的多个元素的场景。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../21-instanced-rendering/" class="btn btn-neutral float-right" title="实例化渲染">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../19-animations/" class="btn btn-neutral" title="动画"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../19-animations/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../21-instanced-rendering/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

458
21-instanced-rendering/index.html Executable file
View File

@@ -0,0 +1,458 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>实例化渲染 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u5b9e\u4f8b\u5316\u6e32\u67d3";
var mkdocs_page_input_path = "21-instanced-rendering.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">实例化渲染</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">很多实例</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">回顾粒子</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_3">扩展</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>实例化渲染</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/21-instanced-rendering.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="instanced-rendering">实例化渲染Instanced Rendering</h1>
<h2 id="_1">很多实例</h2>
<p>在绘制三维场景时,经常会有许多模型用相同的网格表示,但它们具有不同的变换。在此情况下,尽管它们可能是只有几个三角形的简单物体,但性能仍可能会受到影响,这背后的原因就是我们渲染它们的方式。</p>
<p>我们基本上是在循环中遍历所有游戏项,并对函数<code>glDrawElements</code>进行调用。如前几章所说的对OpenGL库的调用应该最小化。对函数<code>glDrawElements</code>的每次调用都会造成一定开销。这对于每个<code>GameItem</code>实例会反复产生开销。</p>
<p>在处理大量相似物体时使用一次调用渲染所有这些物体会更有效。这种技术被称为实例化渲染。为了实现同时渲染一组元素OpenGL提供了一组名为<code>glDrawXXXInstanced</code>的函数。在本例中,由于我们正在绘制元素,所以将使用名为<code>glDrawElementsInstanced</code>的函数。该函数接收与<code>glDrawElements</code>相同的参数,外加一个额外的参数,用于设置要绘制的实例数。</p>
<p>这是一个如何使用<code>glDrawElements</code>的示例:</p>
<pre><code class="language-java">glDrawElements(GL_TRIANGLES, numVertices, GL_UNSIGNED_INT, 0)
</code></pre>
<p>以下是实例化版本的用法:</p>
<pre><code class="language-java">glDrawElementsInstanced(GL_TRIANGLES, numVertices, GL_UNSIGNED_INT, 0, numInstances);
</code></pre>
<p>但是你现在可能想知道如何为每个实例设置不同的变换。现在在绘制每个实例之前我们将使用Uniform来传递不同的变换和实例相关的数据。在进行渲染调用之前我们需要为每项设置特定的数据。当它们开始渲染时我们如何做到这一点呢</p>
<p>当使用实例化渲染时在顶点着色器中我们可以使用一个输入变量来储存当前绘制实例的索引。例如使用这个内置变量我们可以传递一个包含要应用到每个实例的变换的Uniform数组并仅做一次渲染调用。</p>
<p>这种方法的问题是它仍然会带来太多的开销。除此之外我们能够传递的Uniform数量是有限的。因此需要使用另一种方法而不是使用Uniform数组我们将使用实例化数组。</p>
<p>如果你还记得前几章每个网格的数据都是由一组名为VBO的数据数组定义的。这些VBO中的数据储存在每个唯一的<code>Mesh</code>实例中。</p>
<p><img alt="VBO" src="../_static/21/vao_1.png" /></p>
<p>使用标准的VBO在着色器中我们可以访问与每个顶点其位置、颜色、纹理等相关的数据。无论何时运行着色器输入变量都被设置为指向与每个顶点相关的指定数据。使用实例化数组我们可以设置每个实例而不是每个顶点所更改的数据。如果我们将这两种类型结合起来就可以使用常规的VBO来储存每个顶点的信息坐标纹理坐标和用VBO储存每个实例的数据如模型观察矩阵</p>
<p>下图展示了由三个顶点组成的网格,每个顶点定义了坐标、纹理与法线。每个元素的第一个索引是它所属的实例(蓝色)。第二个索引表示实例中的顶点位置。</p>
<p>网格也由两个实例VBO定义。一个用于模型观察矩阵另一个用于光照观察矩阵。当为第一个实例渲染顶点第1X个模型观察矩阵和光照观察矩阵是相同的第1个。当渲染第二个实例的顶点时将使用第二个模型观察矩阵和光照观察矩阵。</p>
<p><img alt="有实例属性的VBO" src="../_static/21/vao_2.png" /></p>
<p>因此在渲染第一个实例的第一个顶点时将使用V11、T11和N11作为坐标、纹理和法线数据并使用MV1作为模型观察矩阵。当渲染同一个实例的第二个顶点时将使用V12、T12和N12作为坐标、纹理和法线数据仍使用MV1作为模型观察矩阵。在渲染第二个实例之前不会使用MV2和LV2。</p>
<p>为了定义每个实例数据,我们需要在顶点属性之后调用函数<code>glVertexAttribDivisor</code>。该函数接收两个参数:</p>
<ul>
<li>
<p>index顶点属性的索引<code>glVertexAttribPointer</code>函数中设置的)。</p>
</li>
<li>
<p>divisor: 如果这个值为0那么在渲染时每个顶点的数据都会改变。如果将其设置为1则每渲染一个实例数据更改一次。如果它被设置为2则每渲染两个实例就会更改一次以此类推。</p>
</li>
</ul>
<p>因此,为了设置实例的数据,我们需要在每个属性定义之后进行如下调用:</p>
<pre><code class="language-java">glVertexAttribDivisor(index, 1);
</code></pre>
<p>让我们开始修改代码库,以支持实例化渲染。第一步是创建一个名为<code>InstancedMesh</code>的新类,该类继承自<code>Mesh</code>类。该类的构造函数类似于<code>Mesh</code>的构造函数,但有一个额外的参数,即实例数。</p>
<p>在构造函数中除了依赖超类的构造函数外我们还将创建两个新的VBO一个用于模型观察矩阵另一个用于光照观察矩阵。创建模型观察矩阵的代码如下所示</p>
<pre><code class="language-java">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 &lt; 4; i++) {
glVertexAttribPointer(start, 4, GL_FLOAT, false, MATRIX_SIZE_BYTES, i * VECTOR4F_SIZE_BYTES);
glVertexAttribDivisor(start, 1);
start++;
}
</code></pre>
<p>我们首先要做的事是创建一个新的VBO和一个新的<code>FloatBuffer</code>以在其内储存数据。缓冲区的大小是用浮点数数量计算的所以它等于实例数乘以4x4矩阵的浮点数大小等于16</p>
<p>一旦VBO被绑定我们就开始为它定义属性。你可以看到这是在<code>for</code>循环中完成的,循环进行了四次。每轮循环定义一个矩阵向量。为什么不简单地为整个矩阵定义一个属性呢?原因是顶点属性不能储存超过四个浮点数。因此,我们需要把矩阵定义分为四部分。让我们重新认识一下<code>glVertexAttribPointer</code>的参数:</p>
<ul>
<li>Index: 要定义的元素的索引。</li>
<li>Size: 该属性的分量数。在本例中,它是四个浮点数,这是可接受的最大值。</li>
<li>Type: 数据类型(在本例中为浮点型)。</li>
<li>Normalize: 是否应该归一化指定数据。</li>
<li>Stride步长: 理解这里的概念很重要,它设置了连续属性之间的字节偏移量。在本例中,我们需要将其设置为整个矩阵的字节大小。这就像一个用于包装数据的标记,从而可以在顶点或实例之间进行更改。</li>
<li>Pointer: 此属性定义应用的偏移量。在本例中,我们需要将矩阵定义拆分为四次调用。依矩阵的每个向量增加偏移量。</li>
</ul>
<p>定义了顶点属性之后,我们需要使用相同的索引调用<code>glVertexAttribDivisor</code></p>
<p>光照观察矩阵的定义与上述过程类似,你可以在源代码中查看它。继续进行<code>InstancedMesh</code>类的定义,重写方法以在渲染之前启用顶点属性(以及在渲染之后要禁用它们)是很重要的。</p>
<pre><code class="language-java">@Override
protected void initRender() {
super.initRender();
int start = 5;
int numElements = 4 * 2;
for (int i = 0; i &lt; numElements; i++) {
glEnableVertexAttribArray(start + i);
}
}
@Override
protected void endRender() {
int start = 5;
int numElements = 4 * 2;
for (int i = 0; i &lt; numElements; i++) {
glDisableVertexAttribArray(start + i);
}
super.endRender();
}
</code></pre>
<p><code>InstancedMesh</code>类定义了一个名为<code>renderListInstanced</code>的公共方法,它渲染一系列的游戏项,这个方法将游戏项列表分割为大小与创建<code>InstancedMesh</code>所设实例数量相等的块。真正的渲染方法是<code>renderChunkInstanced</code>,定义如下:</p>
<pre><code class="language-java">private void renderChunkInstanced(List&lt;GameItem&gt; 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);
}
</code></pre>
<p>该方法很简单我们遍历游戏项计算模型观察矩阵和光照观察矩阵。这些矩阵被转储到它们各自的缓冲区中。这些缓冲区的内容被发送到GPU最后通过对方法<code>glDrawElementsInstanced</code>的调用来渲染它们。</p>
<p>回到着色器,我们需要修改顶点着色器以支持实例化渲染。首先我们为模型观察矩阵添加新的输入参数,这些参数将在使用实例化渲染时传递。</p>
<pre><code class="language-glsl">layout (location=5) in mat4 modelViewInstancedMatrix;
layout (location=9) in mat4 modelLightViewInstancedMatrix;
</code></pre>
<p>如你所见模型观察矩阵从位置5开始。由于矩阵是由一组共四个属性每个属性储存一个向量定义的所以光照观察矩阵从位置9开始。因为我们想在非实例化渲染和实例化渲染中使用同一着色器所以我们将维护模型观察矩阵和光照观察矩阵的Uniform。我们只需更改它们的名字。</p>
<pre><code class="language-glsl">uniform int isInstanced;
uniform mat4 modelViewNonInstancedMatrix;
uniform mat4 modelLightViewNonInstancedMatrix;
</code></pre>
<p>我们创建了另一个Uniform来指定是否使用实例化渲染。在使用实例化渲染的情况下代码非常简单我们只使用输入参数中的矩阵。</p>
<pre><code class="language-glsl">void main()
{
vec4 initPos = vec4(0, 0, 0, 0);
vec4 initNormal = vec4(0, 0, 0, 0);
mat4 modelViewMatrix;
mat4 lightViewMatrix;
if ( isInstanced &gt; 0 )
{
modelViewMatrix = modelViewInstancedMatrix;
lightViewMatrix = modelLightViewInstancedMatrix;
initPos = vec4(position, 1.0);
initNormal = vec4(vertexNormal, 0.0);
}
</code></pre>
<p>我们暂时不支持动画实例化渲染,以简化示例,但是该技术可以完美地用于此处。</p>
<p>最后,着色器像往常一样设置恰当的值。</p>
<pre><code class="language-glsl"> vec4 mvPos = modelViewMatrix * initPos;
gl_Position = projectionMatrix * mvPos;
outTexCoord = texCoord;
mvVertexNormal = normalize(modelViewMatrix * initNormal).xyz;
mvVertexPos = mvPos.xyz;
mlightviewVertexPos = orthoProjectionMatrix * lightViewMatrix * initPos;
outModelViewMatrix = modelViewMatrix;
}
</code></pre>
<p>当然,<code>Renderer</code>类已经被修改以支持Uniform的修改并将非实例化网格的渲染从实例化网格中分离出来。你可以查看源代码中的修改。</p>
<p>此外JOML的作者<a href="https://github.com/httpdigest">Kai Burjack</a>还向源代码添加了一些优化。这些优化已经用于<code>Transformation</code>类,并总结为如下几条:</p>
<ul>
<li>删除冗余调用,以设置具有单位值的矩阵。</li>
<li>使用四元数进行更有效的旋转。</li>
<li>使用特定的旋转和平移矩阵的方法,这些方法是针对这些操作优化的。</li>
</ul>
<p><img alt="实例化渲染" src="../_static/21/instanced_rendering.png" /></p>
<h2 id="_2">回顾粒子</h2>
<p>在实例化渲染的支持下,我们还可以提高粒子渲染的性能。粒子就是最好的用例。</p>
<p>为了将实例化渲染应用到粒子上我们必须提供对纹理集的支持。这可以通过添加一个带有纹理偏移量的VBO来实现。纹理偏移量可以由两个浮点数组成的单个向量定义因此不需要像矩阵那样拆分定义。</p>
<pre><code class="language-java">// 纹理偏移量
glVertexAttribPointer(start, 2, GL_FLOAT, false, INSTANCE_SIZE_BYTES, strideStart);
glVertexAttribDivisor(start, 1);
</code></pre>
<p>但是我们将在单个VBO中设置所有实例属性而不是添加一个新的VBO。下图展示了这个概念。我们将所有属性打包在一个VBO中每个实例的值都会发生变化。</p>
<p><img alt="单个VBO" src="../_static/21/single_vbo.png" /></p>
<p>为了使用单个VBO我们需要修改实例中所有属性的属性大小。从上述代码中可以看到纹理偏移量的定义使用了一个名为<code>INSTANCE_SIZE_BYTES</code>的常量。这个常量等于两个矩阵一个用于模型观察矩阵定义另一个用于光照观察矩阵定义的字节大小再加上两个浮点数纹理偏移量的字节大小总共是136。步长也需要适当地调整。</p>
<p>你可以在源代码中查看修改。</p>
<p><code>Renderer</code>类也需要修改,以实用实例化渲染粒子和支持纹理集在场景中渲染。在本例中,支持这两种类型(非实例化和实例化)的渲染是没有意义的,所以修改更简单。</p>
<p>粒子的顶点着色器如下所示:</p>
<pre><code class="language-glsl">#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);}
</code></pre>
<p>上述修改的效果看起来和非实例化粒子渲染时完全一样但性能更高。FPS计数器已作为选项添加到窗口标题中。你可以使用实例化渲染和非实例化渲染来看看自身的性能提升。</p>
<p><img alt="粒子" src="../_static/21/particles.png" /></p>
<h2 id="_3">扩展</h2>
<p>结合我们现在所拥有的所有基础构造,我已经基于使用高度图修改了渲染方块数据的代码,还使用了纹理集以使用不同的纹理。它还融合了粒子渲染。看起来是这样的:</p>
<p><img alt="使用高度图的方块" src="../_static/21/cubes_height_map.png" /></p>
<p>请记住这还有很大的优化空间但这本书的目的是指导你学习LWJGL和OpenGL的概念和技术。我们的目标不是创建一个完整的游戏引擎绝对不是一个体素引擎它需要不同的方法和更多的优化</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../22-audio/" class="btn btn-neutral float-right" title="音效">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../20-particles/" class="btn btn-neutral" title="粒子"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../20-particles/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../22-audio/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

492
22-audio/index.html Executable file
View File

@@ -0,0 +1,492 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>音效 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u97f3\u6548";
var mkdocs_page_input_path = "22-audio.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">音效</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>音效</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/22-audio.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="audio">音频Audio</h1>
<p>在此之前我们一直在处理图像,但每个游戏的另一个关键面是音频。本章将在<a href="https://www.openal.org" title="OpenAL">OpenAL</a>Open Audio Library开放音频库的帮助下实现这个功能。OpenAL就像是OpenGL在音频的相似物它允许我们通过抽象层播放声音。该层将我们与音频子系统的复杂底层隔离开来。此外它还允许我们可以三维场景中特定的位置设置声音“渲染”声音随着距离衰减并根据它们的速度进行修改模拟<a href="https://en.wikipedia.org/wiki/Doppler_effect">多普勒效应</a>)。</p>
<p>LWJGL支持OpenGL不需要任何额外的下载它就已经可以使用了。但是在开始写代码之前我们需要介绍处理OpenGL时所涉及的主要元素它们是</p>
<ul>
<li>缓冲区Buffer</li>
<li>声源Source</li>
<li>侦听者Listener</li>
</ul>
<p>缓冲区储存音频数据即音乐或音效。它们类似于OpenGL中的纹理。OpenAL希望音频数据采用PCMPulse Coded Modulation脉冲编码调制格式单声道或多声道因此我们不能只转储MP3或OGG文件而不首先将它们转换为PCM。</p>
<p>下一个元素是声源,它表示发出声音的三维空间中的位置(一个点)。声源与缓冲区关联(一次只能有一个),可以通过以下属性定义:</p>
<ul>
<li>位置,声源的位置(<script type="math/tex">x</script><script type="math/tex">y</script><script type="math/tex">z</script>坐标。顺便一提OpenAL和OpenGL一样使用右手笛卡尔坐标系所以你可以假设为了简化你的世界坐标等于声音空间坐标系中的坐标。</li>
<li>速度,它指定声源移动的速度。这是用来模拟多普勒效应的。</li>
<li>增益,用来改变声音的大小(就像是一个放大因数)。</li>
</ul>
<p>源代码中有额外的属性,稍后在讲解源代码时将描述这些属性。</p>
<p>最后但并不重要的是,侦听者是是产生的声音应该被听到的地方。侦听器就像是被设置在三维音频场景中用来接收声音的麦克风。现在只有一个侦听器。因此,人们常说的音频渲染是以听众的角度完成的。侦听器共享一些属性,但它还有一些附加属性,比如方向。方向表示侦听器所面朝的位置。</p>
<p>因此三维音频场景有一组发出声音的声源和接收声音的侦听器组成。最终听到的声音取决于听者到不同声源的距离、相对速度和选择的传播模型Propagation Model。下图展示了一个包含不同元素类型的三维场景。</p>
<p><img alt="OpenAL概念" src="../_static/22/openal_concepts.png" /></p>
<p>那么,让我们开始编写代码,我们将创建一个名为<code>org.lwjglb.engine.sound</code>的新包,它将负责管理所有负责处理音频的类。我们将首先从一个名为<code>SoundBuffer</code>的类开始它将表示一个OpenAL缓冲区。该类的定义如下的一个代码片段所示</p>
<pre><code class="language-java">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);
}
// ...
}
</code></pre>
<p>该类的构造函数需要一个声音文件(它可能与其他资源一样位于类路径中),并从中创建一个新的缓冲区。我们要做的第一件事是调用<code>alGenBuffers</code>创建一个OpenAL缓冲区。最后我们的声音缓冲区将用一个整数来表示就像一个指向它所持有的数据的指针。一旦创建了缓冲区我们就将音频数据转储到其中。构造函数需要OGG格式的文件因此我们需要将其转换为PCM格式。你可以查看这如何在源代码中完成的无论如何源代码是从LWJGL的OpenAL测试中提取的。</p>
<p>此前版本的LWJGL有一个名为<code>WaveData</code>的帮助类用于加载WAV格式的音频文件。这个类不再出现在LWJGL3中。不过你可以从该类获得源代码并在游戏中使用它可能不需要任何修改</p>
<p><code>SoundBuffer</code>类还提供了<code>cleanup</code>方法来释放资源。</p>
<p>让我们继续为OpenAL建模它将由一个名为<code>SoundSource</code>的类实现。类定义如下:</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>声源类提供了一些方法来设置它的位置、增益和控制方法来停止和暂停播放。请记住,声音控制操作是对一个声源(而不是对缓冲区)执行的。请记住,多个源可以共享同一个缓冲区。与<code>SoundBuffer</code>类中一样,<code>SoundBuffer</code>由一个标识符标记,该标识符在每个操作中使用。该类还提供了一个<code>cleanup</code>方法来释放保留的资源。但是让我们看看构造函数。我们要做的第一件事是调用<code>alGenSources</code>创建声源,然后使用构造函数参数设置一些有趣的属性。</p>
<p>第一个参数<code>loop</code>,表示要播放的声音是否应该处于循环模式。默认情况下,当通过声源调用播放操作时,当声音播放到最后时将停止。这对于一些声音来说是可以的,但是对于其他一些声音,比如背景音乐,就需要反复播放。当声音停止时不需要手动控制并重新播放声音,我们就只用将循环属性设置为<code>true</code><code>alSourcei(sourceId, AL_LOOPING, AL_TRUE);</code></p>
<p>另一个参数<code>relative</code>控制声源的位置是否相对于侦听器。在本例中当为声源设置位置时我们基本上是在定义到侦听器的距离使用想想而不是OpenAL三维场景中的坐标也不是世界坐标。这是通过调用<code>alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);”</code>启用的。但是,我们能用它做什么呢?这个属性很有趣,例如,用于不应该受到侦听器距离影响(减弱)的背景声音。例如,在背景音乐或与播放器控件相关的音效。如果我们将这些声源设置为相对的,并将它们的位置设置为<script type="math/tex">(0, 0, 0)</script>,它们将不会被减弱。</p>
<p>现在轮到侦听器了,它是由一个名为<code>SoundListener</code>定义的。以下是该类的定义:</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>与前面的类不同,你将注意到不需要创建侦听器。总会有一个侦听器,所以不需要创建一个,它已经为我们准备好了。因此,在构造函数中,我们只是简单地设置它的初始位置。基于同样的原因,没有必要使用<code>cleanup</code>方法。类也有设置侦听器位置和速度的方法,就像在<code>SoundSource</code>类中一样,但是我们有一个额外的方法来修改侦听器的方向。让我们回顾一下方向是什么。侦听器方向由两个向量定义,<code>at</code>向量和<code>up</code>向量,如下图所示:</p>
<p><img alt="侦听器的at和up向量" src="../_static/22/listener_at_up.png" /></p>
<p><code>at</code>向量基本上指向侦听器所朝向的位置,默认情况下它的值为<script type="math/tex">(0, 0, -1)</script><code>up</code>向量确定侦听器向上的方向,默认情况下它指向<script type="math/tex">(0, 1, 0)</script>。这两个向量的三个分量都是在<code>alListenerfv</code>方法调用中设置的。此方法用于将一组浮点数(浮点数变量)传递到属性(在本例中为方向)。</p>
<p>在继续讲解之前有必要强调一些与声音和侦听器速度相关的概念。声源与侦听器之间的相对速度会导致OpenAL模拟多普勒效应。如果你不知道多普勒效应是什么多普勒效应将导致一个离你越来越近的物体发出的频率似乎比它离开时发出的频率要高的效应。问题是仅仅通过设置声音和侦听器速度OpenAL不会为你更新它们的位置。它将使用相对速度来计算多普勒效应但位置不会改变。因此如果你想要模拟一个移动的声源或者侦听器你必须注意在游戏循环中更新它们的位置。</p>
<p>现在我们已经定义了关键元素为了让它们工作需要初始化OpenAL库因此将创建一个名为<code>SoundManager</code>的新类来处理这个问题。下面是定义该类的代码片段:</p>
<pre><code class="language-java">package org.lwjglb.engine.sound;
// import ...
public class SoundManager {
private long device;
private long context;
private SoundListener listener;
private final List&lt;SoundBuffer&gt; soundBufferList;
private final Map&lt;String, SoundSource&gt; soundSourceMap;
private final Matrix4f cameraMatrix;
public SoundManager() {
soundBufferList = new ArrayList&lt;&gt;();
soundSourceMap = new HashMap&lt;&gt;();
cameraMatrix = new Matrix4f();
}
public void init() throws Exception {
this.device = alcOpenDevice((ByteBuffer) null);
if (device == NULL) {
throw new IllegalStateException(&quot;Failed to open the default OpenAL device.&quot;);
}
ALCCapabilities deviceCaps = ALC.createCapabilities(device);
this.context = alcCreateContext(device, (IntBuffer) null);
if (context == NULL) {
throw new IllegalStateException(&quot;Failed to create OpenAL context.&quot;);
}
alcMakeContextCurrent(context);
AL.createCapabilities(deviceCaps);
}
</code></pre>
<p>该类保存对<code>SoundBuffer</code><code>SoundSource</code>的实例的引用,以便跟踪和在此之后正确地清理它们。<code>SoundBuffer</code>储存在一个列表中,但<code>SoundSource</code>储存在一个<code>Map</code>中,因此可以通过名称搜索它们。<code>init</code>方法初始化OpenAL子系统</p>
<ul>
<li>开启默认设备。</li>
<li>为该设备创建功能。</li>
<li>创建一个声音环境就像是OpenGL那样并将其设置为当前环境。</li>
</ul>
<p><code>SoundManager</code>类还具有更新给定摄像机位置的侦听器朝向的方法。在本例中,侦听器将被设置在摄像机所在的位置。那么,给定摄像机的位置和旋转信息,我们如何计算<code>at</code><code>up</code>向量呢?答案是使用与摄像机相关联的观察矩阵。我们需要将<code>at</code><script type="math/tex">(0, 0, -1)</script><code>up</code><script type="math/tex">(0, 1, 0)</script>向量转换为考虑摄像机旋转的向量。让<code>cameraMatrix</code>为与摄像机关联的观察矩阵。实现的代码如下:</p>
<pre><code class="language-java">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);
</code></pre>
<p>我们要做的第一件事是逆转摄像机观察矩阵。为什么要这么做?这样想,观察矩阵从世界空间坐标变换到观察空间。我们想要的正好相反,我们想要从观察空间坐标(观察矩阵)转换到世界空间坐标,这是侦听器应该被放置的位置。对于矩阵,反比通常就意味着逆转。一旦我们有了这个矩阵,我们只需要转换<code>at</code><code>up</code>向量,使用这个矩阵计算新的方向。</p>
<p>但是,如果你查看源代码,你会看到实现略有不同,我们所做的是:</p>
<pre><code class="language-java">Vector3f at = new Vector3f();
cameraMatrix.positiveZ(at).negate();
Vector3f up = new Vector3f();
cameraMatrix.positiveY(up);
listener.setOrientation(at, up);
</code></pre>
<p>上述代码等价于第一种方法,它只是一种更高效的方法。它使用了一种更快的方法,可以在<a href="https://github.com/JOML-CI/JOML">JOML</a>库中找到这种方法不需要计算完整的逆矩阵但是可以得到相同的结果。这方法是由LWJGL论坛中的<a href="https://github.com/httpdigest">JOML作者</a>提供的,因此你可以在[那里]((http://forum.lwjgl.org/index.php?topic=6080.0)查看更多细节。如果查看源代码,你将看到<code>SoundManager</code>类计算它自己的观察矩阵副本。这已经在<code>Renderer</code>类中完成了。为了保持代码简单,并避免重构,我倾向于使用这种方式。</p>
<p>这就完了。我们拥有播放声音所需的所有基础结构。你可以查看源代码,了解如何使用所有代码。你可以看到音乐是如何播放的,以及不同效果的声音(这些文件是从<a href="https://www.freesound.org/">Freesound</a>中获得的贡献者都储存在名为CREDITS.txt的一个文件中。如果你获得一些其他文件你可以会注意到声音衰减与距离或侦听器的方向无关。请检查这些声音是否是单声道是否不是立体声。OpenGL仅能使用单声道声音进行计算。</p>
<p>后记OpenAL还允许你通过使用<code>alDistanceModel</code>并传递你想使用的模型(<code>AL11.AL_EXPONENT_DISTANCE</code><code>AL_EXPONENT_DISTANCE_CLAMP</code>,等等)。你可以用它们来播放并检查效果。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../23-3d-object-picking/" class="btn btn-neutral float-right" title="三维物体选取">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../21-instanced-rendering/" class="btn btn-neutral" title="实例化渲染"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../21-instanced-rendering/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../23-3d-object-picking/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

395
23-3d-object-picking/index.html Executable file
View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>三维物体选取 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u4e09\u7ef4\u7269\u4f53\u9009\u53d6";
var mkdocs_page_input_path = "23-3d-object-picking.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">三维物体选取</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">摄像机选取</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">鼠标选取</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>三维物体选取</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/23-3d-object-picking.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="3d-object-picking">三维物体选取3D Object Picking</h1>
<h2 id="_1">摄像机选取</h2>
<p>每一个游戏的关键之一是能与环境交互,该功能要求能够在三维场景中选取物体。在本章中,我们将探讨如何实现这一功能。</p>
<p>但是,在开始讲述选取物体的步骤之前,我们需要一种表示所选物体的方法。因此,我们必须做的第一件事是添加一个属性到<code>GameItem</code>类,这允许我们标记选定的对象:</p>
<pre><code class="language-java">private boolean selected;
</code></pre>
<p>然后,我们需要能够在场景着色器中使用该值。让我们从片元着色器(<code>scene_fragment.fs</code>)开始。在本例中,我们将假设从顶点着色器接收一个标记,该标记将确定要渲染的片元是否是选定的物体。</p>
<pre><code class="language-glsl">in float outSelected;
</code></pre>
<p>然后,在片元着色器的结尾,我们将修改最终的片元颜色,如果选中,则将蓝色分量设置为<script type="math/tex">1</script>.</p>
<pre><code class="language-glsl">if ( outSelected &gt; 0 ) {
fragColor = vec4(fragColor.x, fragColor.y, 1, 1);
}
</code></pre>
<p>然后,我们需要能够为每个<code>GameItem</code>设置该值。如果你回想一下前面的章节,我们有两种情况:</p>
<ul>
<li>渲染非实例化网格。</li>
<li>渲染实例化网格。</li>
</ul>
<p>在第一种情况下,每个<code>GameItem</code>的数据通过Uniform传递因此我们只需要在顶点着色器中为它添加一个新的Uniform。在第二种情况下我们需要创建一个新的实例化属性。可以看到下述代码中集成了两种情况到顶点着色器。</p>
<pre><code class="language-glsl">layout (location=14) in float selectedInstanced;
...
uniform float selectedNonInstanced;
...
if ( isInstanced &gt; 0 )
{
outSelected = selectedInstanced;
...
}
else
{
outSelected = selectedNonInstanced;
...
</code></pre>
<p>既然已经做好了基本准备,我们只需要定义如何选择对象。在继续之前,如果你查阅源代码,你可能会注意到观察矩阵现在储存在<code>Camera</code>类中。这是因为我们在源代码中的几个类重新计算了观察矩阵,此前它储存在<code>Transformation</code><code>SoundManager</code>类中。为了计算交点,我们就需要创建另一个副本。与其创建另一个副本,我们不如把它集中到<code>Camera</code>类中。这一更改还需要观察矩阵在游戏主循环中更新。</p>
<p>让我们继续物体选取的讨论。在本例中,我们将基于一个简单的方法,选取将由摄像机自动地完成,将选择摄像机所面对的最近的物体。让我们讨论一下如何做到它。</p>
<p>下图是我们需要解决的情况:</p>
<p><img alt="物体选取" src="../_static/23/object_picking.png" /></p>
<p>我们把摄像机放在世界空间的某个坐标中面朝一个特定方向。任何一个物体如果它与摄像机的位置和前向的光线投射Ray Cast相交那么它就是候选物体。在所有候选物体中我们只需要选择最近的一个。</p>
<p>在本例中游戏项是立方体因此需要计算摄像机的前向向量与立方体的交点。这似乎是一个非常特殊的情况但实际上是非常常见的。在许多游戏中游戏项都与所谓的边界框Bounding Box相关连。边界框是一个矩形框它囊括了该物体的所有顶点。例如边界框也用于碰撞检测。实际上在动画章节中你看到的每个动画帧都定义了一个边界框这有助于在任意给定时间设置边界。</p>
<p>接下来让我们开始编码。我们将创建一个名为<code>CameraBoxSelectionDetector</code>的新类,它有一个名为<code>selectGameItem</code>的方法,该方法将接收游戏项列表和摄像机。方法的定义如下:</p>
<pre><code class="language-java">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) &amp;&amp; nearFar.x &lt; closestDistance) {
closestDistance = nearFar.x;
selectedGameItem = gameItem;
}
}
if (selectedGameItem != null) {
selectedGameItem.setSelected(true);
}
}
</code></pre>
<p>该方法将迭代游戏项,尝试从中获取与摄像机光线投射相交的项。它首先定义一个名为<code>closestDistance</code>的变量,该变量将储存最近的距离。对于相交的游戏项,将计算摄像机到交点的距离,如果该距离小于储存在<code>closestDistance</code>中的值,则该项将成为新的候选项。</p>
<p>在进入循环之前我们需要摄像机所面向的方向向量。这很简单只需使用视图矩阵去获得考虑相机旋转的Z方向。记住正Z指向屏幕外所以需要相反的方向向量这就是为什么要反方向<code>negate</code>)。</p>
<p><img alt="摄像机" src="../_static/23/camera.png" /></p>
<p>在游戏循环中,每个<code>GameItem</code>都要进行相交计算。但是,我们该怎么做呢?这就是<a href="https://github.com/JOML-CI/JOML" title="JOML">JOML</a>库来帮忙的地方了。我们使用的是<a href="https://github.com/JOML-CI/JOML" title="JOML">JOML</a><code>Intersectionf</code>类,它提供了几种计算二维和三维交点的方法。具体来说,我们使用的是<code>intersectRayAab</code>方法。</p>
<p>该方法实现了轴对齐边界框Axis Aligned Bounding Box简称AABB交点检测算法。你可以查看JOML文档中指出的<a href="http://people.csail.mit.edu/amy/papers/box-jgt.pdf" title="这里">详细信息</a></p>
<p>该方法测试由原点和方向定义的射线是否与由最小和最大坐标定义的边界框相交。这个算法是有效的,因为我们的立方体是轴对齐的,如果旋转它们,这个方法就不起作用。因此,该方法接收以下参数:</p>
<ul>
<li>一个原点:在本例中,这是摄像机的位置。</li>
<li>一个方向:在这里是摄像机的朝向,前向向量。</li>
<li>边界框的最小坐标。在本例中,立方体以<code>GameItem</code>坐标为中心最小坐标是该坐标减去比例。在其原始大小中立方体的长度为2比例为1</li>
<li>边界框的最大坐标。不言而喻。</li>
<li>一个结果向量。它将储存到远近交点的距离(对于一个轴对齐边界框和一条射线,最多有两个交点)。</li>
</ul>
<p>如果有交点,该方法将返回<code>true</code>。如果为<code>true</code>,我们将检查最近距离并在必要时更新它,并储存所选候选<code>GameItem</code>的引用。下图展示了该方法中涉及的所有要素。</p>
<p><img alt="交点" src="../_static/23/intersection.png" /></p>
<p>一旦循环完成,候选<code>GamItem</code>将被标记为已选定。</p>
<p>这就是全部了。<code>selectGameItem</code>将在<code>DummyGame</code>类的更新方法中调用,同时调用的还有观察矩阵更新。</p>
<pre><code class="language-java">// 更新观察矩阵
camera.updateViewMatrix();
// 更新声音监听器位置
soundMgr.updateListenerPosition(camera);
this.selectDetector.selectGameItem(gameItems, camera);
</code></pre>
<p>此外一个十字瞄准线Cross-hair已添加到渲染中以检查一切工作正常。结果如下图所示</p>
<p><img alt="物体选取结果" src="../_static/23/object_picking_result.png" /></p>
<p>显然,这里给出的方法远远不是最佳的,但它将为你提供基础知识,是你能够自己开发更复杂的方法。场景的某些部分很容易被丢弃,比如摄像机后面的物体,因为它们不会相交。除此之外,你换可以根据摄像机的距离来确定物体,以加快计算速度。此外,只有在摄像机移动或旋转后,才需要进行计算。</p>
<h2 id="_2">鼠标选取</h2>
<p>用摄像机选取物体完成了,但当我们想用鼠标自由选择物体怎么办?在此情况下,我们希望每当用户单击屏幕时,自动选择最近的对象。</p>
<p>实现它的方法类似于上述方法。在之前的方法中,我们得到了摄像机的位置,并根据摄像机当前的方向使用“前向”方向从摄像机生成射线。在此情况下,我们仍需要投射广西,但方向指向一个远离摄像机的点,也就是点击的点。在本例中,我们需要使用点击的坐标来计算方向向量。</p>
<p>但是,我们如何将视口空间中的<script type="math/tex">(x,y)</script>坐标变换到世界空间呢?让我们回顾一下如何从模型空间坐标变换到观察空间。为了达到这个目的,所应用的不同坐标变换是:</p>
<ul>
<li>我们使用模型矩阵将模型坐标变换到世界坐标。</li>
<li>我们使用观察矩阵(提供摄像机功能)将世界坐标转换到观察空间坐标。</li>
<li>我们通过应用透视投影矩阵将观察坐标变换到齐次裁剪空间Homogeneous Clip Space</li>
<li>最终的屏幕坐标由OpenGL为我们自动计算。在此之前它传递到归一化的终端空间通过将<script type="math/tex">x,y,z</script>坐标除以<script type="math/tex">w</script>分量),然后传递到<script type="math/tex">x,y</script>屏幕坐标。</li>
</ul>
<p>所以我们只需要从屏幕坐标<script type="math/tex">(x,y)</script>到世界坐标,逆路径遍历。</p>
<p>第一步是将屏幕坐标转换为归一化的终端空间。视口空间中的<script type="math/tex">(x,y)</script>坐标的范围是<script type="math/tex">[0, 屏幕宽度]</script>
<script type="math/tex">[0, 屏幕高度]</script>。屏幕左上角的坐标为<script type="math/tex">(0, 0)</script>。我们需要将其转换为<script type="math/tex">[-1, 1]</script>范围内的坐标。</p>
<p><img alt="屏幕坐标到归一化终端空间" src="../_static/23/screen_coordinates.png" /></p>
<p>很简单的数学:</p>
<p>
<script type="math/tex; mode=display">x = 2 \cdot screen_x / screenwidth - 1</script>
</p>
<p>
<script type="math/tex; mode=display">y = 1 - 2 * screen_y / screenheight</script>
</p>
<p>但是,我们如何计算<script type="math/tex">z</script>分量呢?答案很简单,我们只需给它分配<script type="math/tex">-1</script>这样广西就可以指向最远的可见距离请记住在OpenGL中<script type="math/tex">-1</script>指向频幕)。现在我们有了归一化终端空间中的坐标。</p>
<p>为了继续变换,我们需要将它们转换为齐次剪切空间。我们需要有一个<script type="math/tex">w</script>分量,它使用齐次坐标。虽然这个概念在前几章已经介绍过了,但让我们再回顾它。为了表示一个三维点,我们需要<script type="math/tex">x</script><script type="math/tex">y</script><script type="math/tex">z</script>分量,但是我们一直在处理一个额外的<script type="math/tex">w</script>分量。我们需要这个额外的分量来使用矩阵执行不同的变换。有些变化不需要这个额外的分量,但有些变换需要。例如,如果我们只有<script type="math/tex">x</script><script type="math/tex">y</script><script type="math/tex">z</script>分量,那么变换矩阵就不能工作。因此,我们添加了<script type="math/tex">w</script>分量,并为它们赋值为<script type="math/tex">1</script>这样我们就可以处理4x4矩阵了。</p>
<p>此外,大多数变换,或者更精确地说,大多数变换矩阵都不会更改<script type="math/tex">w</script>分量。投影矩阵是一个例外,该矩阵将<script type="math/tex">w</script>分量更改为与<script type="math/tex">z</script>分量成比例。</p>
<p>通过将<script type="math/tex">x</script><script type="math/tex">y</script><script type="math/tex">z</script>分量处以<script type="math/tex">w</script>,就可以实现从齐次裁剪空间到归一化的终端坐标的转换。由于这个分量与<script type="math/tex">z</script>分量成比例,意味着远处的物体被画得更小。在本例中,需要反其道而行之,我们可以忽略这一步,只需要将<script type="math/tex">w</script>分量设置为<script type="math/tex">1</script>,并保留其它组件的原始值。</p>
<p>我们现在需要回到观察空间。这很简单我们只需要计算投影矩阵的逆矩阵并将它与4个分量向量相乘。完成之后我们就需要把它们转换成世界空间。同样我们只需要使用观察矩阵计算它的逆矩阵然后乘以向量。</p>
<p>记住,我们只对方向感兴趣,因此,在本例中,我们将<script type="math/tex">w</script>分量设置为<script type="math/tex">0</script>。我们还可以将<script type="math/tex">z</script>组件再次设置为<script type="math/tex">-1</script>,因为我们希望它指向频幕。一旦这样做并应用逆矩阵,我们就得到了世界空间中的向量。我们计算了射线,可以使用与摄像机选取相同的算法。</p>
<p>我们创建了一个名为<code>MouseBoxSelectionDetector</code>的新类,它实现了上述步骤。此外,我们将投影矩阵移动到<code>Window</code>类,所以我们可以在几个地方使用它们。我们还重构了一点<code>CameraBoxSelectionDetector</code>,所以<code>MouseBoxSelectionDetector</code>可以继承和使用碰撞检测方法。你可以直接查看源代码,因为实现非常简单。</p>
<p>结果如下所示:</p>
<p><img alt="鼠标选取" src="../_static/23/mouse_selection.png" /></p>
<p>你只需用鼠标单击该方块即可进行选取。</p>
<p>之后你可以参考一篇<a href="https://capnramses.github.io/opengl/raycasting.html" title="优秀文章">优秀文章</a>中背完所解释的步骤的细节,其中包含了涉及不同方案的非常详细的说明。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../24-hud-revisited/" class="btn btn-neutral float-right" title="回顾HUD - NanoVG">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../22-audio/" class="btn btn-neutral" title="音效"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../22-audio/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../24-hud-revisited/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

373
24-hud-revisited/index.html Executable file
View File

@@ -0,0 +1,373 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>回顾HUD - NanoVG - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u56de\u987eHUD - NanoVG";
var mkdocs_page_input_path = "24-hud-revisited.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">回顾HUD - NanoVG</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>回顾HUD - NanoVG</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/24-hud-revisited.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="hud-nanovghud-revisited-nanovg">回顾HUD - NanoVGHUD Revisited - NanoVG</h1>
<p>在此前的章节中我们讲解了如何使用正交投影在场景顶部创建一个HUD以渲染图形和纹理。在本章中我们将学习如何使用<a href="https://github.com/memononen/nanovg">NanoVG</a>库来渲染抗锯齿矢量图形从而以简单的方式创建更复杂的HUD。</p>
<p>你可以使用许多其他库来完成此事,例如<a href="https://github.com/nifty-gui/nifty-gui">Nifty GUI</a><a href="https://github.com/vurtun/nuklear">Nuklear</a>等。在本章是我们将重点介绍NanoVG因为它使用起来非常简单但是如果你希望开发可与按钮、菜单和窗口交互的复杂GUI那你可能需要的是<a href="https://github.com/nifty-gui/nifty-gui">Nifty GUI</a></p>
<p>使用<a href="https://github.com/memononen/nanovg">NanoVG</a>首先是要在<code>pom.xml</code>文件中添加依赖项(一个是用于编译时所需的依赖项,另一个是用于运行时所需的本地代码):</p>
<pre><code class="language-xml">...
&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-nanovg&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;/dependency&gt;
...
&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-nanovg&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;classifier&gt;${native.target}&lt;/classifier&gt;
&lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;
</code></pre>
<p>在开始使用<a href="https://github.com/memononen/nanovg">NanoVG</a>之前我们必须在OpenGL设置一些东西以便示例能够正常工作。我们需要启用对模板测试Stencil Test的支持。到目前为止我们已经讲解了颜色和深度缓冲区但我们没有提到模板缓冲区。该缓冲区为用于控制应绘制哪些像素的每个像素储存一个值整数用于根据储存的值以屏蔽或放弃绘图区域。例如它可以用来以一种简单的方式切割场景的某些部分。我们通过将此行添加到<code>Window</code>类中来启用模板测试(在启用深度测试之后):</p>
<pre><code class="language-java">glEnable(GL_STENCIL_TEST);
</code></pre>
<p>因为我们使用的是另一个缓冲区,所以在每次渲染调用之前,我们还必须注意删除它的值。因此,我们需要修改<code>Renderer</code>类的<code>clear</code>方法:</p>
<pre><code class="language-java">public void clear() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}
</code></pre>
<p>我们还将添加一个新的窗口选项来激活抗锯齿Anti-aliasing。因此<code>Window</code>类中,我们将通过如下方式启用它:</p>
<pre><code class="language-java">if (opts.antialiasing) {
glfwWindowHint(GLFW_SAMPLES, 4);
}
</code></pre>
<p>现在我们可以使用<a href="https://github.com/memononen/nanovg">NanoVG</a>库了。我们要做的第一件事就是删掉我们创建的HUD代码即着色器<code>IHud</code>接口,<code>Renderer</code>类中的HUD渲染方法等。你可以在源代码中查看。</p>
<p>在此情况下,新的<code>Hud</code>类将负责其渲染,因此我们不需要将其委托给<code>Renderer</code>类。让我们先定义这个类,它将有一个<code>init</code>方法来设置库和构建HUD所需要的资源。方法定义如下</p>
<pre><code class="language-java">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(&quot;Could not init nanovg&quot;);
}
fontBuffer = Utils.ioResourceToByteBuffer(&quot;/fonts/OpenSans-Bold.ttf&quot;, 150 * 1024);
int font = nvgCreateFontMem(vg, FONT_NAME, fontBuffer, 0);
if (font == -1) {
throw new Exception(&quot;Could not add font&quot;);
}
colour = NVGColor.create();
posx = MemoryUtil.memAllocDouble(1);
posy = MemoryUtil.memAllocDouble(1);
counter = 0;
}
</code></pre>
<p>我们首先要做的是创建一个NanoVG上下文。在本例中我们使用的是OpenGL3.0后端,因此我们引用的是<code>org.lwjgl.nanovg.NanoVGGL3</code>命名空间。如果抗锯齿被启用,我们将设置<code>NVG_ANTIALIAS</code>标志。</p>
<p>接下来,我们使用此前加载到<code>ByteBuffer</code>中的TrueType字体来创建字体。我们为它指定一个名词以便稍后在渲染文本时使用它。关于这点一件很重要的事情是用于加载字体的<code>ByteBuffer</code>必须在使用字体时储存在内存中。也就是说,它不能被回收,否则你将得到一个不错的核心崩溃。这就是将它储存为类属性的原因。</p>
<p>然后,我们创建一个颜色实例和一些有用的变量,这些变量将在渲染时使用。在初始化渲染之前,在游戏初始化方法中调用该方法:</p>
<pre><code class="language-java">@Override
public void init(Window window) throws Exception {
hud.init(window);
renderer.init(window);
...
</code></pre>
<p><code>Hud</code>类还定义了一个渲染方法该方法应在渲染场景后调用以便在其上绘制Hud。</p>
<pre><code class="language-java">@Override
public void render(Window window) {
renderer.render(window, camera, scene);
hud.render(window);
}
</code></pre>
<p>Hud类的<code>render</code>方法的开头如下所示:</p>
<pre><code class="language-java">public void render(Window window) {
nvgBeginFrame(vg, window.getWidth(), window.getHeight(), 1);
</code></pre>
<p>首先必须要做的第一件事是调用<code>nvgBeginFrame</code>方法。所有NanoVG渲染操作都必须保护在<code>nvgBeginFrame</code><code>nvgEndFrame</code>调用之间。<code>nvgBeginFrame</code>接受以下参数:</p>
<ul>
<li>NanoVG环境</li>
<li>要渲染的窗口的大小(宽度和高度)。</li>
<li>像素比。如果需要支持Hi-DPI可以修改此值。对于本例我们只将其设置为1。</li>
</ul>
<p>然后我们创建了几个占据整个屏幕的色带。第一条是这样绘制的:</p>
<pre><code class="language-java">// 上色带
nvgBeginPath(vg);
nvgRect(vg, 0, window.getHeight() - 100, window.getWidth(), 50);
nvgFillColor(vg, rgba(0x23, 0xa1, 0xf1, 200, colour));
nvgFill(vg);
</code></pre>
<p>渲染图形时,应调用的第一个方法是<code>nvgBeginPath</code>它指示NanoVG开始绘制新图形。然后定义要绘制的内容一个矩形填充颜色并通过调用<code>nvgFill</code>绘制它。</p>
<p>你可以查看源代码的其他部分,以了解其余图形是如何绘制的。当渲染文本是,不需要在渲染前调用<code>nvgBeginPath</code></p>
<p>完成所有图形的绘制后,我们只需要调用<code>nvgEndFrame</code>来结束渲染但在离开方法之前还有一件重要的事情要做。我们必须恢复OpenGL状态NanoVG修改OpenGL以执行其操作如果状态未正确还原你可能会看到场景没有正确渲染甚至被擦除。因此我们需要恢复渲染所需的相关OpenGL状态。这是委派到<code>Window</code>类中的:</p>
<pre><code class="language-java">// 还原状态
window.restoreState();
</code></pre>
<p>方法的定义如下:</p>
<pre><code class="language-java">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);
}
}
</code></pre>
<p>这就完事了(除了一些其它的清理方法),代码完成了。当你运行示例时,你将得到如下结果:</p>
<p><img alt="Hud" src="../_static/24/hud.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../25-optimizations-frustum-culling/" class="btn btn-neutral float-right" title="优化 - 截锥剔除">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../23-3d-object-picking/" class="btn btn-neutral" title="三维物体选取"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../23-3d-object-picking/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../25-optimizations-frustum-culling/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,408 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>优化 - 截锥剔除 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u4f18\u5316 - \u622a\u9525\u5254\u9664";
var mkdocs_page_input_path = "25-optimizations-frustum-culling.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">优化 - 截锥剔除</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#-">优化 - 截锥剔除一</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>优化 - 截锥剔除</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/25-optimizations-frustum-culling.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="-optimizations-frustum-culling">优化 - 截锥剔除Optimizations - Frustum Culling</h1>
<h2 id="-">优化 - 截锥剔除一</h2>
<p>现在我们使用了许多不同的图形效果例如光照、粒子等。此外我们还学习了如何实例化渲染以减少绘制许多相似对象的开销。然而我们仍有足够的空间进行一些简单的优化这将增加可以达到的帧率FPS</p>
<p>你可能想知道为什么我们会在每一帧中绘制整个游戏项列表即使其中一些项不可见因为它们在摄像机后面或距离摄像机太远。你甚至可能认为这是由OpenGL自动处理的这在某种程度上是正确的。OpenGL将放弃位于可见区域之外的顶点的渲染这称作裁剪Clipping。裁剪的问题是在执行顶点着色之后按顶点进行处理的。因此即使此操作节省了资源我们也可以通过不尝试渲染不可见的对象来提高效率。我们不会通过将数据发送到GPU以及对这些对象的每个顶点进行变换来浪费资源。我们需要移除不包含在视锥体View Frustum中的对象也就是说我们需要进行截锥剔除。</p>
<p>但是,首先让我们回顾一下什么是视锥体。视锥体是一个结合摄像机的位置和旋转以及使用的投影,包含所有可见物体的体积。通常,视锥体是一个四棱台,如下图所示:</p>
<p><img alt="视锥体I" src="../_static/25/view_frustum_i.png" /></p>
<p>如你所见,视锥体由六个平面定义,位于视锥体之外的任何内容都不会渲染。因此,截锥剔除是移除视锥体之外的对象的过程。</p>
<p>因此,为了进行截锥剔除,我们需要:</p>
<ul>
<li>
<p>使用观察和投影矩阵中包含的数据计算截锥平面。</p>
</li>
<li>
<p>对每个游戏项检查它是否包含在视锥体中,换句话说,在大小截锥平面之间。并从渲染流程中删除那些不包含在其中的。</p>
</li>
</ul>
<p><img alt="视锥体II" src="../_static/25/view_frustum_ii.png" /></p>
<p>那么让我们从计算截锥平面开始。平面由包含在其中的点和与该平面正交的向量定义,如下图所示:</p>
<p><img alt="平面" src="../_static/25/plane.png" /></p>
<p>平面方程的定义如下:</p>
<p>
<script type="math/tex; mode=display">Ax+By+Cz+D=0</script>
</p>
<p>因此我们需要计算视锥体的六个侧面的六个平面方程。为了达成这个目标你基本上有两个选项。你可以进行繁琐的计算得到六个平面方程的来自上述方程的四个常数A、B、C和D。另一个选项是让<a href="https://github.com/JOML-CI/JOML" title="JOML">JOML</a>库为你计算这个值。通常情况下,我们选择后一个选项。</p>
<p>让我们开始编码吧。我们将创建一个名为<code>FrustumCullingFilter</code>的新类,跟它的名字相同,它将根据视锥体执行筛选操作。</p>
<pre><code class="language-java">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 &lt; NUM_PLANES; i++) {
frustumPlanes[i] = new Vector4f();
}
}
</code></pre>
<p><code>FrustumCullingFilter</code>类也将有一个方法来计算平面方程,名为<code>updateFrustum</code>,它将在渲染之前调用。方法定义如下:</p>
<pre><code class="language-java">public void updateFrustum(Matrix4f projMatrix, Matrix4f viewMatrix) {
// 计算投影观察矩阵
prjViewMatrix.set(projMatrix);
prjViewMatrix.mul(viewMatrix);
// 获取视锥体平面
for (int i = 0; i &lt; NUM_PLANES; i++) {
prjViewMatrix.frustumPlane(i, frustumPlanes[i]);
}
}
</code></pre>
<p>首先,我们储存投影矩阵的副本,并将其与观察矩阵相乘,得到投影观察矩阵。然后,使用这个变换矩阵,我们只需要为每个截锥平面调用<code>frustumPlane</code>方法。需要注意的是,这些平面方程是用世界坐标表示的,所以所有的计算都要在这个空间中进行。</p>
<p>现在我们已经计算了所有平面,我们只需要检查<code>GameItem</code>实例是否在截锥体中。该怎么做?让我们首先确认一下如何检查一个点是否在截锥体内,可以通过计算点到每个平面的有符号的距离来实现这一点。如果点到平面的距离是正的,这意味着点在平面的前面(根据其法线)。如果是负的,则意味着点在平面的后面。</p>
<p><img alt="到平面的距离" src="../_static/25/distance_to_plane.png" /></p>
<p>因此,如果到截锥的所有平面的距离为正,则一个点位于视锥体的内部。点到平面的距离定义如下:</p>
<p>
<script type="math/tex">距离=Ax_{0}+By_{0}+Cz_{0}+D</script>,其中<script type="math/tex">x_{0}</script><script type="math/tex">y_{0}</script><script type="math/tex">z_{0}</script>是点的坐标。</p>
<p>因此,如果<script type="math/tex">Ax_{0}+By_{0}+Cz_{0}+D <= 0</script>则点在平面的后面。</p>
<p>但是,我们没有点,只有复杂的网格,我们不能仅仅用点来检查一个物品是否在截锥体内。你可以考虑检查<code>GameItem</code>的每个顶点看看它是否在截锥体内。如果任何一个点在里面游戏项应该被绘制出来。但这就是OpenGL在裁剪时所做的也是我们要避免的。记住网格越复杂截锥剔除的好处越明显。</p>
<p>我们需要把每一个<code>GameItem</code>放到一个简单的体中,这个体很容易检查。这里我们有两个选项:</p>
<ul>
<li>
<p>边界盒Bounding Box</p>
</li>
<li>
<p>边界球Bounding Sphere</p>
</li>
</ul>
<p>在本例中,我们将使用球体,因为这是最简单的方法。我们将把每一个游戏项放在一个球体中,并检查球体是否位于视锥体中。为了做到它,我们只需要球体的中心和半径。检查它几乎等同于检查点,但是我们需要考虑板甲。如果满足以下条件,则球体将位于截锥之外:<script type="math/tex">距离=Ax_{0}+By_{0}+Cz_{0} <= -半径</script></p>
<p><img alt="边界球" src="../_static/25/bounding_sphere.png" /></p>
<p>因此,我们将在<code>FrustumCullingFilter</code>类中添加一个新方法来检查球体是否在截锥中。方法的定义如下:</p>
<pre><code class="language-java">public boolean insideFrustum(float x0, float y0, float z0, float boundingRadius) {
boolean result = true;
for (int i = 0; i &lt; NUM_PLANES; i++) {
Vector4f plane = frustumPlanes[i];
if (plane.x * x0 + plane.y * y0 + plane.z * z0 + plane.w &lt;= -boundingRadius) {
result = false; return result;
}
}
return result;
}
</code></pre>
<p>然后,我们将添加过滤视锥体外的游戏项的方法:</p>
<pre><code class="language-java">public void filter(List&lt;GameItem&gt; 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));
}
}
</code></pre>
<p>我们在<code>GameItem</code>类中添加了一个新的属性<code>insideFrustum</code>来跟踪可见性。如你所见,边界球的板甲作为参数传递。这是由于边界球与<code>Mesh</code>管理,它不是<code>GameItem</code>的属性。但是,请记住,我们必须在世界坐标中操作,并且边界球的半径将在模型空间震。我们将应用为<code>GameItem</code>设置的比例将其转换为世界空间,我们还假设<code>GameItem</code>的位置是球体的中心(在世界空间坐标系中)。</p>
<p>最后一个方法只是一个实用方法,它接受网格表并过滤其中包含的所有<code>GameItem</code>实例:</p>
<pre><code class="language-java">public void filter(Map&lt;? extends Mesh, List&lt;GameItem&gt;&gt; mapMesh) {
for (Map.Entry&lt;? extends Mesh, List&lt;GameItem&gt;&gt; entry : mapMesh.entrySet()) {
List&lt;GameItem&gt; gameItems = entry.getValue();
filter(gameItems, entry.getKey().getBoundingRadius());
}
}
</code></pre>
<p>就这样。我们可以在渲染流程中使用该类,只需要更新截锥平面,计算出哪些游戏项是可见的,并在绘制实例网格和非实例网格时过滤掉它们:</p>
<pre><code class="language-java">frustumFilter.updateFrustum(window.getProjectionMatrix(), camera.getViewMatrix());
frustumFilter.filter(scene.getGameMeshes());
frustumFilter.filter(scene.getGameInstancedMeshes());
</code></pre>
<p>你可以启用或禁用过滤功能并可以检查你可以达到的FPS的增加和减少。在过滤时不考虑粒子但是添加它是很简单的。对于粒子在任何情况下最好检查发射器的位置而不是检查每个粒子。</p>
<h1 id="-_1">优化 - 截锥剔除二</h1>
<p>解释了截锥剔除的基础,我们可以使用<a href="https://github.com/JOML-CI/JOML" title="JOML">JOML</a>库中提供的更精细的方法。它特别地提供了一个名为<code>FrustumIntersection</code>的类,该类以按此<a href="http://gamedevs.org/uploads/fast-extraction-viewing-frustum-planes-from-world-view-projection-matrix.pdf" title="paper">文章</a>所述的一种更有效的方式获取视锥体的平面。除此之外,该类还提供了测试边界盒、点和球体的方法。</p>
<p>那么,让我们修改<code>FrustumCullingFilter</code>类。属性和构造函数简化如下:</p>
<pre><code class="language-java">public class FrustumCullingFilter {
private final Matrix4f prjViewMatrix;
private FrustumIntersection frustumInt;
public FrustumCullingFilter() {
prjViewMatrix = new Matrix4f();
frustumInt = new FrustumIntersection();
}
</code></pre>
<p><code>updateFrustum</code>方法只是将平面获取委托给<code>FrustumIntersection</code>实例:</p>
<pre><code class="language-java">public void updateFrustum(Matrix4f projMatrix, Matrix4f viewMatrix) {
// 计算投影识图矩阵
prjViewMatrix.set(projMatrix);
prjViewMatrix.mul(viewMatrix);
// 更新截锥相交类
frustumInt.set(prjViewMatrix);
}
</code></pre>
<p><code>insideFrustum</code>方法更简单:</p>
<pre><code class="language-java">public boolean insideFrustum(float x0, float y0, float z0, float boundingRadius) {
return frustumInt.testSphere(x0, y0, z0, boundingRadius);
}
</code></pre>
<p>使用该方法你甚至可以达到更高的FPS。此外还向<code>Window</code>类中添加了一个全局标记,以启用或禁用截锥剔除。<code>GameItem</code>类也有启用或禁用过滤的标记,因为对于某些项,截锥剔除过滤可能没有意义。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../26-cascaded-shadow-maps/" class="btn btn-neutral float-right" title="级联阴影映射">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../24-hud-revisited/" class="btn btn-neutral" title="回顾HUD - NanoVG"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../24-hud-revisited/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../26-cascaded-shadow-maps/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

View File

@@ -0,0 +1,449 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>级联阴影映射 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "\u7ea7\u8054\u9634\u5f71\u6620\u5c04";
var mkdocs_page_input_path = "26-cascaded-shadow-maps.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">级联阴影映射</a>
<ul class="current">
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>级联阴影映射</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/26-cascaded-shadow-maps.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="cascaded-shadow-maps">级联阴影映射Cascaded Shadow Maps</h1>
<p>在阴影一章中我们介绍了阴影映射技术以便在渲染三维场景时使用平行光显示阴影。此前介绍的方案中要求你手动调整一些参数以改进效果。在本章中我们将修改该技术以自动化所有流程并改进在开放空间中的效果。为了达到目的我们将使用一种称为级联阴影映射CMSCascaded Shadow Map的技术。</p>
<p>让我们首先看一下如何自动构造用于渲染阴影的光照视图矩阵和正交投影矩阵。如果你回想阴影一章,我们需要从光线的角度来绘制场景。这意味着创建一个光照视图矩阵,它就像一个作用于光源的摄像机和一个投影矩阵。由于光是定向的,而且应该位于无穷远处,所以我们选择了正交投影。</p>
<p>我们希望所有可见的物体都适用于光照视图投影矩阵。因此,我们需要将视截锥放入光截锥中。下图是我们最终想要实现的效果:</p>
<p><img alt="视截锥" src="../_static/26/view_frustum.png" /></p>
<p>该如何构造它?首先是计算观察投影矩阵的截锥顶点。我们得到世界空间的坐标,然后计算这个截锥的中心,这可以通过将所有的顶点的坐标相加并将结果除以顶点的数量来计算。</p>
<p><img alt="截锥中心" src="../_static/26/frustum_center.png" /></p>
<p>有了这些数据我们就可以设置光源的位置。其位置及其方向将用于构建光照视图矩阵。为了计算位置我们从此前得到的视锥体的中心开始逆着光的方向取相当于视锥体的近Z平面和远Z平面的距离的一点。</p>
<p><img alt="光源位置" src="../_static/26/light_position.png" /></p>
<p>构建完成光照视图矩阵我们需要设置正交投影矩阵。为了计算它们我们将截锥的顶点转换到光照视图空间只需要将它们乘以刚刚构建的光照视图矩阵。投影矩阵的尺寸是最大和最小的X和Y值近Z平面可以设置为与标准投影矩阵相同的值远Z平面则是光照视图空间中截锥顶点的最大和最小Z值之间的距离。</p>
<p>但是,如果在阴影示例代码的基础上实现上述算法,则可能会对阴影质量感到失望。</p>
<p><img alt="低质量阴影" src="../_static/26/low_quality_shadows.png" /></p>
<p>原因是阴影分辨率受纹理大小的限制。我们现在正覆盖一个潜在的巨大区域,并且我们用来储存深度信息的纹理没有足够的分辨率来取得良好的效果。你可能认为解决方法只是提高纹理分辨率,但这并不足以完全解决问题,你需要巨大的纹理。</p>
<p>有一个更聪明的解决方案。其关键是,靠近摄像机的物体需要比远处物体的阴影有更高的质量。一种方法是只渲染靠近摄像机的对象的阴影,但这会导致阴影在场景中移动时出现或消失。</p>
<p>级联阴影映射CSM使用的方法是将视锥体分割为多个部分。离摄像机较近的部分会覆盖较小的区域而距离较远的部分会覆盖更广的区域。下图显示了把一个视锥体分为三个部分。</p>
<p><img alt="级联分割" src="../_static/26/cascade_splits.png" /></p>
<p>对于每个部分,将渲染深度纹理,调整光照视图和投影矩阵以合适地覆盖每个分割的部分。因此,储存深度映射的纹理覆盖视锥体的区域缩小了。而且,由于离摄像机最近的部分会占用较少的空间,因此深度分辨率会提高。</p>
<p>从上述解释可以看出我们需要尽可能多的将深度图进行分割我们还将更改每个光照视图和投影矩阵。因此要使用CSM需要做的事情是</p>
<ul>
<li>将视锥体分为n个部分。</li>
<li>渲染深度纹理时,对于每个部分:</li>
<li>计算光照视图和投影矩阵。</li>
<li>从光源的角度将场景渲染为单独的深度图。</li>
<li>渲染场景时:</li>
<li>使用此前计算的深度图。</li>
<li>确定要绘制的片段所属的部分。</li>
<li>计算阴影因子,如阴影映射中所述。</li>
</ul>
<p>如你所见CSM的主要缺点是我们需要从灯光的角度为每个部分渲染场景。这就是为什么通常只用于开放空间的原因。不管怎么说我们将可以看到如何轻松地减少开销。</p>
<p>所以,让我们看看代码。但是在继续之前,有一个小小的提醒,我不会在这里写上完整的源代码,因为这会非常枯燥。相反,我将介绍主要类、它们的职责和可能需要进一步解释的片段,以便更好地理解。所有与着色相关的类都已移动到一个名为<code>org.lwjglb.engine.graph.shadow</code>的新包中。</p>
<p>渲染阴影的代码,换句话说,从光影透视的场景已经移动到了<code>ShadowRenderer</code>类中。(该代码以前在<code>Renderer</code>类中)。</p>
<p>类定义了以下常量:</p>
<pre><code class="language-java">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};
</code></pre>
<p>首先是层级或拆分的数量。第二个定义了每个拆分的部分的远Z平面的位置。如你所见它们的间距并不相等。离摄像机较近的部分在Z平面上的距离最短。</p>
<p>类还储存了用于渲染深度图的着色器程序的引用,一个列表,其中包含与每个部分相关联的信息,其由<code>ShadowCascade</code>类定义,以及对储存深度图数据(纹理)的对象的引用,由<code>ShadowBuffer</code>类定义。</p>
<p><code>ShadowRenderer</code>类具有用于设置着色器和所需属性的方法以及一个渲染方法。<code>render</code>方法的定义如下所示:</p>
<pre><code class="language-java">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 &lt; NUM_CASCADES; i++) {
ShadowCascade shadowCascade = shadowCascades.get(i);
depthShaderProgram.setUniform(&quot;orthoProjectionMatrix&quot;, shadowCascade.getOrthoProjMatrix());
depthShaderProgram.setUniform(&quot;lightViewMatrix&quot;, 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);
}
</code></pre>
<p>如你所见,我们为每个部分执行的几个渲染过程,类似于此前阴影图的渲染方法。在每次执行中,我们都会使用相关的<code>ShadowCascade</code>实例中保函的信息更改光照视图矩阵和正交投影矩阵。</p>
<p>此外,在每次执行中,我们都需要更改正在使用的纹理。每次都会将深度信息渲染为不同的纹理。此信息储存在<code>ShadowBuffer</code>类中并设置为供FBO使用代码如下</p>
<pre><code class="language-java">glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
</code></pre>
<p>正如刚刚提到的,<code>ShadowBuffer</code>类储存与用于储存深度信息的纹理相关的信息。代码与阴影章节中使用的代码非常相似,只是我们使用的是纹理数组。因此,我们创建了一个新的类<code>ArrTexture</code>,它创建了一个具有相同属性的纹理数组。此类还提供了一个<code>bind</code>方法,用于绑定所有纹理数组,以便在场景着色器中使用它们。该方法接收一个参数,从某个纹理单元开始绑定。</p>
<pre><code class="language-java">public void bindTextures(int start) {
for (int i = 0; i &lt; ShadowRenderer.NUM_CASCADES; i++) {
glActiveTexture(start + i);
glBindTexture(GL_TEXTURE_2D, depthMap.getIds()[i]);
}
}
</code></pre>
<p><code>ShadowCascade</code>类储存与一个部分关联的光照视图和正交投影矩阵。每个分割的部分由一个近Z平面距离和一个远Z平面距离定义并根据该信息计算矩阵。</p>
<p>该类提供并更新了以观察矩阵和光照方向为输入的方法该方法首先计算世界空间中的视锥顶点然后计算出光源的位置。根据光的方向从截锥的中心到相当于远Z平面到近Z平面之间的距离的距离计算出该位置。</p>
<pre><code class="language-java">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 &lt; 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();
}
</code></pre>
<p>根据光源的位置和方向,我们可以构造光照视图矩阵。这是在<code>updateLightViewMatrix</code>方法中完成的:</p>
<pre><code class="language-java">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);
}
</code></pre>
<p>最后,我们需要构造正交投影矩阵。这是在<code>updateLightProjectionMatrix</code>方法中完成的。该方法将视锥体坐标转换到光照空间。然后我们得到x、y坐标的最小值和最大值来构造包围视锥体的边界框。近Z平面可以设置为0远Z平面可以设置为坐标的最大值和最小值之间的距离。</p>
<pre><code class="language-java">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 &lt; 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);
}
</code></pre>
<p>记住,正交投影就像一个边界框,应该包含所有将要渲染的对象。该边界框以光照视图坐标空间表示。因此,我们要做的是计算包围视锥体的最小边界框,轴与光源位置对齐。</p>
<p><code>Renderer</code>类已被修改为使用视图包中的类以及修改传递给渲染器的信息。在渲染器中,我们需要处理模型、模型观察和光照视图矩阵。在此前的章节中,我们使用模型观察或光照视图矩阵来减少操作的数量。在本例中,我们选择简化要传递的元素的数量,现在只将模型、观察和光照矩阵传递给着色器。此外,对于粒子,我们需要保留比例,因为我们不再传递模型观察矩阵,所以现在信息将丢失。我们重用用于标记所选项的属性来设置比例信息。在粒子着色中,我们将使用该值再次设置缩放。</p>
<p>在场景的顶点着色器中,我们为每个部分计算模型光照视图矩阵,并将其作为输出传递给片元着色器。</p>
<pre><code class="language-glsl">mvVertexPos = mvPos.xyz;
for (int i = 0 ; i &lt; NUM_CASCADES ; i++) {
mlightviewVertexPos[i] = orthoProjectionMatrix[i] * lightViewMatrix[i] * modelMatrix * vec4(position, 1.0);
}
</code></pre>
<p>在片元着色器中,我们使用这些值根据片元所处的部分来查询适当的深度图。这需要在片元着色器中完成,因为对于特定项目,它们的片元可能位于不同的部分中。</p>
<p>此外在片元着色器中我们必须决定要分成哪个部分。为了实现它我们使用片元的Z值并将其与每个部分的最大Z值进行比较也就是远Z平面值。这些数据作为一个新的Uniform传递</p>
<pre><code class="language-glsl">uniform float cascadeFarPlanes[NUM_CASCADES];
</code></pre>
<p>如下所示,我们计算分割的部分,变量<code>idx</code>将储存要使用的部分:</p>
<pre><code class="language-glsl">int idx;
for (int i=0; i&lt;NUM_CASCADES; i++)
{
if ( abs(mvVertexPos.z) &lt; cascadeFarPlanes[i] )
{
idx = i;
break;
}
}
</code></pre>
<p>此外,在场景着色器中,我们需要传递一个纹理数组,一个<code>sampler2D</code>数组用于深度图即与我们分割部分相关联的纹理。源代码用一个Uniform列表而不是使用数组储存用于引用与每个部分关联的深度图的纹理单元。</p>
<pre><code class="language-glsl">uniform sampler2D normalMap;
uniform sampler2D shadowMap_0;
uniform sampler2D shadowMap_1;
uniform sampler2D shadowMap_2;
</code></pre>
<p>将其更改为一组Uniform会导致难以追踪此示例的其他纹理出现的问题。在任何情况下你都可以尝试在代码中修改它。</p>
<p>源代码中的其余修改和着色器只是基于上述更改所需的调整,你可以直接在源代码上查看它。</p>
<p>最后,当引入这些更改时,你可能会发现性能下降,这是因为我们绘制的深度图是此前的三倍。如果摄像机未被移动或场景项未发生更改,则不需要反复渲染深度图。深度图储存在纹理中,因此不需要每次渲染调用清除它们。因此,我们在<code>render</code>方法中添加了一个新变量用于指示是否更新深度图以避免更新深度图使其保持不变这会显著提高FPS。最终你会得到这样的效果</p>
<p><img alt="最终效果" src="../_static/26/csmpng.png" /></p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../27-assimp/" class="btn btn-neutral float-right" title="Assimp库">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../25-optimizations-frustum-culling/" class="btn btn-neutral" title="优化 - 截锥剔除"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../25-optimizations-frustum-culling/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../27-assimp/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

663
27-assimp/index.html Executable file
View File

@@ -0,0 +1,663 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="../img/favicon.ico">
<title>Assimp库 - Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="../css/theme.css" />
<link rel="stylesheet" href="../css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script>
// Current page data
var mkdocs_page_name = "Assimp\u5e93";
var mkdocs_page_input_path = "27-assimp.md";
var mkdocs_page_url = null;
</script>
<script src="../js/jquery-2.1.1.min.js" defer></script>
<script src="../js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul class="current">
<li class="toctree-l1 current"><a class="reference internal current" href="./">Assimp库</a>
<ul class="current">
<li class="toctree-l2"><a class="reference internal" href="#_1">静态网格</a>
</li>
<li class="toctree-l2"><a class="reference internal" href="#_2">动画</a>
</li>
</ul>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="..">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="..">Docs</a> &raquo;</li>
<li>Assimp库</li>
<li class="wy-breadcrumbs-aside">
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/27-assimp.md"
class="icon icon-github"> Edit on GitHub</a>
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="assimpassimp">Assimp库Assimp</h1>
<h2 id="_1">静态网格</h2>
<p>加载不同格式的复杂三维模型的功能对于编写游戏至关重要为其中一些编写解析器需要大量的工作即便只支持一种格式也可能很耗时。例如在第九章中描述的wavefront加载器只解析了规范中的一小部分根本不处理材质</p>
<p>幸运的是,已经可以使用<a href="http://assimp.sourceforge.net/">Assimp</a>库来解析许多常见的3D格式。它是一个C++库可以以各种格式加载静态和动画模型。LWJGL提供了绑定以便从Java代码中使用它们。在本章中我们将学习如何使用它。</p>
<p>首先是将Assimp的Maven依赖项添加到<code>pom.xml</code>文件中。我们需要添加编译时和运行时依赖项。</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-assimp&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.lwjgl&lt;/groupId&gt;
&lt;artifactId&gt;lwjgl-assimp&lt;/artifactId&gt;
&lt;version&gt;${lwjgl.version}&lt;/version&gt;
&lt;classifier&gt;${native.target}&lt;/classifier&gt;
&lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;
</code></pre>
<p>一旦设置了依赖项,我们将创建一个名为<code>StaticMeshesLoader</code>的新类,该类将用于加载不带动画的网格,该类定义了两个静态公共方法:</p>
<pre><code class="language-java">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 {
// ....
</code></pre>
<p>两种方法都有以下参数:
* <code>resourcePath</code>模型文件所在的文件路径。这是一个绝对路径因为Assimp可能需要加载其他文件例如wavefront、OBJ等文件的材质文件并且可能使用与资源路径相同的基路径。如果将资源嵌入JAR文件中那么assimp将无法导入它因此它必须是文件系统路径。
* <code>texturesDir</code>保存此模型的文件夹路径。这是CLASSPATH的相对路径。例如wavefront材质文件可以定义多个纹理文件。代码希望此文件位于<code>texturesDir</code>目录下。如果发现纹理加载错误,可能需要在模型文件中手动调整这些路径。</p>
<p>第二个方法有一个名为<code>flags</code>的额外参数。此参数允许调整加载过程。第一个方法调用第二个方法,并传递一些在大多数情况下都有用的值:
* <code>aiProcess_JoinIdenticalVertices</code>:此标记将减少使用的顶点数,识别可在面之间重用的顶点。
* <code>aiProcess_Triangulate</code>:模型可以使用四边形或其他几何图形来定义它们的元素。由于我们只处理三角形,因此必须使用此标记将所有的面拆分为三角形(如果有必要的话)。
* <code>aiProcess_FixInfacingNormals</code>:此标记将尝试反转可能指向内部的法线。</p>
<p>还有许多其他标记可以使用你可以在LWJGL的Javadoc文档中查阅它们。</p>
<p>回看到第二个载入方法,我们要做的第一件事是调用<code>aiImportFile</code>方法来加载带有所选标记的模型。</p>
<pre><code class="language-java">AIScene aiScene = aiImportFile(resourcePath, flags);
if (aiScene == null) {
throw new Exception(&quot;Error loading model&quot;);
}
</code></pre>
<p>载入方法的其余代码如下所示:</p>
<pre><code class="language-java">int numMaterials = aiScene.mNumMaterials();
PointerBuffer aiMaterials = aiScene.mMaterials();
List&lt;Material&gt; materials = new ArrayList&lt;&gt;();
for (int i = 0; i &lt; 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 &lt; numMeshes; i++) {
AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
Mesh mesh = processMesh(aiMesh, materials);
meshes[i] = mesh;
}
return meshes;
</code></pre>
<p>我们处理模型中包含的材质,材质定义组成模型的网格使用的颜色和纹理。然后我们处理不同的网格,模型可以定义多个网格,每个网格都可以使用为模型定义的一种材质。</p>
<p>如果你看到上面的代码你可能会看到很多对Assimp库的调用返回的<code>PointerBuffer</code>实例。你可以用C指针那样的方式看待它们它们只是指向储存数据的内存区域。你需要提前知道它们储存的数据类型以便处理它们。对于材质我们迭代该缓冲区创建<code>AIMaterial</code>类的实例。在第二种情况下,我们迭代储存网格数据的缓冲区,创建<code>AIMesh</code>类的实例。</p>
<p>让我们看看<code>processMaterial</code>方法:</p>
<pre><code class="language-java">private static void processMaterial(AIMaterial aiMaterial, List&lt;Material&gt; 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 &amp;&amp; textPath.length() &gt; 0) {
TextureCache textCache = TextureCache.getInstance();
texture = textCache.getTexture(texturesDir + &quot;/&quot; + 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);
}
</code></pre>
<p>我们检查材质是否定义了纹理。如果有,我们加载纹理。我们创建了一个名为<code>TextureCache</code>的新类,用于缓存纹理。这是因为几个网格可能共享相同的纹理,我们不想浪费空间一次又一次加载相同的数据。然后我们尝试获得环境、漫反射和镜面反射的材质颜色分量。幸运的是,我们对材质的定义已经包含了这些信息。</p>
<p><code>TextureCache</code>的定义非常简单它只是一个映射通过纹理文件的路径对不同的纹理进行索引你可以直接查看源代码。由于现在纹理可能使用不同的图像格式PNG、JPEG等我们已经修改了纹理的加载方式现在使用STB库来加载更多的格式而不是使用PNG库。</p>
<p>让我们看到<code>StaticMeshesLoader</code>类。<code>processMesh</code>的定义如下:</p>
<pre><code class="language-java">private static Mesh processMesh(AIMesh aiMesh, List&lt;Material&gt; materials) {
List&lt;Float&gt; vertices = new ArrayList&lt;&gt;();
List&lt;Float&gt; textures = new ArrayList&lt;&gt;();
List&lt;Float&gt; normals = new ArrayList&lt;&gt;();
List&lt;Integer&gt; 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 &gt;= 0 &amp;&amp; materialIdx &lt; materials.size()) {
material = materials.get(materialIdx);
} else {
material = new Material();
}
mesh.setMaterial(material);
return mesh;
}
</code></pre>
<p><code>Mesh</code>由一组顶点位置、法线方向、纹理坐标和索引定义。每个元素都在<code>processVertices</code><code>processNormals</code><code>processTextCoords</code><code>processIndices</code>方法中处理,网格也可以使用其索引指向材质。如果索引与之前处理的材质相对应,我们只需将它们与<code>Mesh</code>相关联。</p>
<p><code>processXXX</code>方法非常简单,它们只是在返回所需数据的<code>AIMesh</code>实例上调用相应的方法。例如,<code>processVertices</code>的定义如下:</p>
<pre><code class="language-java">private static void processVertices(AIMesh aiMesh, List&lt;Float&gt; vertices) {
AIVector3D.Buffer aiVertices = aiMesh.mVertices();
while (aiVertices.remaining() &gt; 0) {
AIVector3D aiVertex = aiVertices.get();
vertices.add(aiVertex.x());
vertices.add(aiVertex.y());
vertices.add(aiVertex.z());
}
}
</code></pre>
<p>你可以看到,我们只是通过调用<code>mVertices</code>方法来获取顶点的缓冲区简单地处理它们来创建一个储存顶点位置的浮点数列表。因为该方法只返回一个缓冲区你可以将该数据直接传给创建顶点的OpenGL方法。但我们不这样做原因有两个。第一个原因是尽量减少对代码库的修改第二个原因是通过加载数据到中间层中你可以执行一些专门的处理任务甚至调试加载过程。</p>
<p>如果你想要一个更有效的示例即直接将缓冲区传给OpenGL可以查看这个<a href="https://github.com/LWJGL/lwjgl3-demos/blob/master/src/org/lwjgl/demo/opengl/assimp/WavefrontObjDemo.java">例子</a></p>
<p><code>StaticMeshesLoader</code>类让<code>OBJLoader</code>类过时了因此它已经从源代码中删除。一个更复杂的OBJ文件已经作为示例提供如果运行它你将看到如下内容</p>
<p><img alt="模型" src="../_static/27/model.png" /></p>
<h2 id="_2">动画</h2>
<p>现在我们已经使用Assimp加载了静态网格可以继续讲解动画。如果你回想动画一章与网格关联的VAO包含顶点位置、纹理坐标、索引和应应用于关节位置以调整最终顶点位置的权重列表。</p>
<p><img alt="VAO动画" src="../_static/27/vao_animation.png" /></p>
<p>每个顶点位置都关联了一个改变最终位置的四个权重的列表引用了将组合以确定顶点最终位置的骨骼索引。在每一帧一个变换矩阵列表将被作为Uniform加载到每个关节。根据这些数据算出最终位置。</p>
<p>在动画一章中我们开发了一个MD5解析器来加载动画网格。在本章中我们将使用Assimp库。这将允许我们加载MD5之外的更多格式例如<a href="https://en.wikipedia.org/wiki/COLLADA" title="COLLADA">COLLADA</a><a href="https://en.wikipedia.org/wiki/FBX" title="FBX">FBX</a>等。</p>
<p>在开始编码之前让我们理清一些术语。在本章中我们将不加区分地提到骨骼和关节。骨骼或者关节都只是影响顶点的元素并且具有形成层次的父级。MD5格式使用术语关节而Assimp使用术语骨骼。</p>
<p>让我们先看一下由Assimp处理的储存着动画信息的结构。我们将首先从骨骼和权重数据开始。对于每个网格我们可以访问顶点位置、纹理坐标和索引。网格还储存着骨骼列表每个骨骼由以下属性定义</p>
<ul>
<li>一个名字。</li>
<li>一个偏移矩阵:稍后将用它来计算每个骨骼应该使用的最终变换。</li>
</ul>
<p>骨骼还指向一个权重列表,每个权重由以下属性定义:</p>
<ul>
<li>一个权重因子,即用于调节与每个顶点相关的骨骼变换影响的数字。</li>
<li>一个顶点标识符,即与当前骨骼相关联的顶点。</li>
</ul>
<p>下图展现了所有这些元素之间的关系。</p>
<p><img alt="网格、骨骼、权重和顶点之间的关系" src="../_static/27/mesh_bones_weights_vertices.png" /></p>
<p>因此,我们首先要做的事是从上述的结构构造顶点位置、骨骼/关节/索引和相关权重的列表。完成后,我们需要为模型中定一个所有动画帧预先计算每个骨骼/关节的变换矩阵。</p>
<p>Assimp场景对象是定义节点的层次结构每个节点都由一个名词和一个子节点列表定义。动画使用这些节点来定义应该应用的变换这个层次结构实际上定义了骨骼的层次结构。每个骨骼都是一个节点并且有一个父节点根节点除外可能还有一组子节点。有一些特殊的节点不是骨骼它们用于对变换进行分组在计算变换时进行处理。另一个问题是这些节点的层次结构是基于整个模型定义的对于每个网格我们没有单独的层次结构。</p>
<p>场景还定义了一组动画。一个模型可以有多个动画,可以对模型做行走、跑动等动画。每个动画定义了不同的变换。动画具有以下属性:</p>
<ul>
<li>一个名字。</li>
<li>持续时间。即动画的持续时间,由于动画是应用于每个不同帧的每个节点的变换列表,因此名称可能看起来很混乱。</li>
<li>一个动画通道Animation Channel的列表。动画通道储存应应用于每个节点的特定时间点的位移、旋转和缩放数据模型中储存动画通道数据的类是<code>AINodeAnim</code></li>
</ul>
<p>下图展示了上述所有元素之间的关系:</p>
<p><img alt="节点动画" src="../_static/27/node_animations.png" /></p>
<p>对于特定的时刻,或者说对于帧,要应用到骨骼的变换是在动画通道中为该时刻定义的变换,乘以所有父节点到根节点的变换。因此,我们需要对场景中存储的信息进行重新排序,流程如下:</p>
<ul>
<li>构造节点层次结构。</li>
<li>对每个动画,迭代每个动画通道(对每个动画节点):为所有帧构造变换矩阵。变换矩阵是位移、旋转和缩放矩阵的组合。</li>
<li>重新排列每一帧的信息:构造要应用于网格中每个骨骼的最终变换。这是通过将骨骼的变换矩阵(相关节点的变换矩阵)乘以所有父节点的变换矩阵直到根节点来实现的。</li>
</ul>
<p>让我们开始编码吧。首先将创建一个名为<code>AnimMeshesLoader</code>的类,它由<code>StaticMeshesLoader</code>扩展,但它不返回网格数组,而是返回一个<code>AnimGameItem</code>实例。它定义了两个公共方法:</p>
<pre><code class="language-java">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(&quot;Error loading model&quot;);
}
int numMaterials = aiScene.mNumMaterials();
PointerBuffer aiMaterials = aiScene.mMaterials();
List&lt;Material&gt; materials = new ArrayList&lt;&gt;();
for (int i = 0; i &lt; numMaterials; i++) {
AIMaterial aiMaterial = AIMaterial.create(aiMaterials.get(i));
processMaterial(aiMaterial, materials, texturesDir);
}
List&lt;Bone&gt; boneList = new ArrayList&lt;&gt;();
int numMeshes = aiScene.mNumMeshes();
PointerBuffer aiMeshes = aiScene.mMeshes();
Mesh[] meshes = new Mesh[numMeshes];
for (int i = 0; i &lt; 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&lt;String, Animation&gt; animations = processAnimations(aiScene, boneList, rootNode, rootTransfromation);
AnimGameItem item = new AnimGameItem(meshes, animations);
return item;
}
</code></pre>
<p>这些方法与<code>StaticMeshesLoader</code>中定义的方法非常相似,但有如下区别:</p>
<ul>
<li>传递默认加载标记的方法使用了新参数:<code>aiProcess_LimitBoneWeights</code>。这将会将影响顶点的最大权重数限制为4这也是我们当前在动画章节中支持的最大值</li>
<li>实际加载模型的方法只加载不同的网格,但它首先计算节点层次结构,然后在结尾调用<code>processAnimations</code>以生成<code>AnimGameItem</code>实例。</li>
</ul>
<p><code>processMesh</code>方法与<code>StaticMeshesLoader</code>类中的方法非常相似,只是它创建的网格将关节索引和权重作为参数传递:</p>
<pre><code class="language-java">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));
</code></pre>
<p>关节索引和权重按<code>processBones</code>方法计算:</p>
<pre><code class="language-java">private static void processBones(AIMesh aiMesh, List&lt;Bone&gt; boneList, List&lt;Integer&gt; boneIds,
List&lt;Float&gt; weights) {
Map&lt;Integer, List&lt;VertexWeight&gt;&gt; weightSet = new HashMap&lt;&gt;();
int numBones = aiMesh.mNumBones();
PointerBuffer aiBones = aiMesh.mBones();
for (int i = 0; i &lt; 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 &lt; numWeights; j++) {
AIVertexWeight aiWeight = aiWeights.get(j);
VertexWeight vw = new VertexWeight(bone.getBoneId(), aiWeight.mVertexId(),
aiWeight.mWeight());
List&lt;VertexWeight&gt; vertexWeightList = weightSet.get(vw.getVertexId());
if (vertexWeightList == null) {
vertexWeightList = new ArrayList&lt;&gt;();
weightSet.put(vw.getVertexId(), vertexWeightList);
}
vertexWeightList.add(vw);
}
}
int numVertices = aiMesh.mNumVertices();
for (int i = 0; i &lt; numVertices; i++) {
List&lt;VertexWeight&gt; vertexWeightList = weightSet.get(i);
int size = vertexWeightList != null ? vertexWeightList.size() : 0;
for (int j = 0; j &lt; Mesh.MAX_WEIGHTS; j++) {
if (j &lt; size) {
VertexWeight vw = vertexWeightList.get(j);
weights.add(vw.getWeight());
boneIds.add(vw.getBoneId());
} else {
weights.add(0.0f);
boneIds.add(0);
}
}
}
}
</code></pre>
<p>此方法遍历特定网格的骨骼定义,获取其权重并生成三个列表:</p>
<ul>
<li><code>boneList</code>:储存节点及其偏移矩阵的列表。稍后将使用它来计算节点变换。已创建一个名为<code>Bone</code>的新类来储存该信息。此列表将储存所有网格的骨骼。</li>
<li><code>boneIds</code>:只储存包含<code>Mesh</code>的每个顶点的骨骼标识。骨骼在渲染时根据其位置进行标识,此列表仅包含特定网格的骨骼。</li>
<li><code>weights</code>:储存要应用于关联骨骼的<code>Mesh</code>的每个顶点的权重。</li>
</ul>
<p><code>weights</code><code>boneIds</code>中储存的数据用于构造<code>Mesh</code>数据。<code>boneList</code>中储存的数据将在稍后计算动画数据时使用。</p>
<p>让我们回到<code>loadAnimGameItem</code>方法。一旦我们创建了网格,还得到了应用于根节点的变换,该变换也将用于计算最终的变换。之后,我们需要处理节点的层次结构,这是在<code>processNodesHierarchy</code>方法中完成的。这个方法非常简单,它只是从根节点开始遍历节点层次结构,构造一个节点树。</p>
<pre><code class="language-java">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 &lt; numChildren; i++) {
AINode aiChildNode = AINode.create(aiChildren.get(i));
Node childNode = processNodesHierarchy(aiChildNode, node);
node.addChild(childNode);
}
return node;
}
</code></pre>
<p>我们已经创建了一个新的<code>Node</code>类,该类将储存<code>AINode</code>实例的相关信息,并提供了查找方法来定位节点层次结构,以便按名称查找节点。回到<code>loadAnimGameItem</code>方法,我们只使用该数据计算<code>processAnimations</code>方法中的动画。该方法返回<code>Animation</code>实例的<code>Map</code>。请记住,一个模型可以有多个动画,因此它们按名称储存索引。有了这些数据,我们终于可以构建一个<code>AnimAgameItem</code>实例。</p>
<p><code>processAnimations</code>方法的定义如下所示:</p>
<pre><code class="language-java">private static Map&lt;String, Animation&gt; processAnimations(AIScene aiScene, List&lt;Bone&gt; boneList,
Node rootNode, Matrix4f rootTransformation) {
Map&lt;String, Animation&gt; animations = new HashMap&lt;&gt;();
// 处理所有动画
int numAnimations = aiScene.mNumAnimations();
PointerBuffer aiAnimations = aiScene.mAnimations();
for (int i = 0; i &lt; numAnimations; i++) {
AIAnimation aiAnimation = AIAnimation.create(aiAnimations.get(i));
// 为每个节点计算变换矩阵
int numChanels = aiAnimation.mNumChannels();
PointerBuffer aiChannels = aiAnimation.mChannels();
for (int j = 0; j &lt; numChanels; j++) {
AINodeAnim aiNodeAnim = AINodeAnim.create(aiChannels.get(j));
String nodeName = aiNodeAnim.mNodeName().dataString();
Node node = rootNode.findByName(nodeName);
buildTransFormationMatrices(aiNodeAnim, node);
}
List&lt;AnimatedFrame&gt; frames = buildAnimationFrames(boneList, rootNode, rootTransformation);
Animation animation = new Animation(aiAnimation.mName().dataString(), frames, aiAnimation.mDuration());
animations.put(animation.getName(), animation);
}
return animations;
}
</code></pre>
<p>将为每个动画处理其动画通道,每个通道定义了不同的变换,这些变化应该随着时间的推移应用于一个节点。为每个节点定义的变换在<code>buildTransFormationMatrices</code>方法中定义,这些矩阵被每个节点储存。一旦节点层次结构中储存完这些信息,我们就可以构建动画帧。</p>
<p>让我们先回顾一下<code>buildTransFormationMatrices</code>方法:</p>
<pre><code class="language-java">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 &lt; 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 &lt; aiNodeAnim.mNumScalingKeys()) {
aiVecKey = scalingKeys.get(i);
vec = aiVecKey.mValue();
transfMat.scale(vec.x(), vec.y(), vec.z());
}
node.addTransformation(transfMat);
}
}
</code></pre>
<p>如你所见,<code>AINodeAnim</code>实例定义了一组包含位移、旋转和缩放信息的键,这些键是指特定的时刻。我们假设数据是按时间顺序排列的,并构建一个储存要应用于每个帧的变换的矩阵列表。最后的计算是在<code>buildAnimationFrames</code>方法中完成的:</p>
<pre><code class="language-java">private static List&lt;AnimatedFrame&gt; buildAnimationFrames(List&lt;Bone&gt; boneList, Node rootNode,
Matrix4f rootTransformation) {
int numFrames = rootNode.getAnimationFrames();
List&lt;AnimatedFrame&gt; frameList = new ArrayList&lt;&gt;();
for (int i = 0; i &lt; numFrames; i++) {
AnimatedFrame frame = new AnimatedFrame();
frameList.add(frame);
int numBones = boneList.size();
for (int j = 0; j &lt; 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;
}
</code></pre>
<p>此方法返回<code>AnimatedFrame</code>实例的列表,每个<code>AnimatedFrame</code>实例将储存要应用于特定帧的每个骨骼的变换列表。这个方法只是迭代储存所有骨骼的列表。对于每个骨骼:</p>
<ul>
<li>获取相关联的节点。</li>
<li>通过将相关联的<code>Node</code>的变换与其父节点的所有变换相乘,生成变换矩阵,直至根节点。这是在<code>Node.getParentTransforms</code>方法中完成的。</li>
<li>它将该矩阵与骨骼的偏倚矩阵相乘。</li>
<li>最后的变换是通过将根节点的变换与上述步骤中计算的矩阵相乘来计算的。</li>
</ul>
<p>源代码中的其他修改是为了适应某些结构而进行的微小修改。最后,你能够加载类似于下图的动画(你需要按空格改变帧):</p>
<p><img alt="动画效果" src="../_static/27/animation_result.png" /></p>
<p>这个示例的复杂之处在于Assimp结构的调整使其适应本书中使用的引擎并预先计算每个帧的数据。此外这些概念与动画一章中的概念类似。你可以尝试修改源代码以在帧之间插入以获得更平滑的动画。</p>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="../28-deferred-shading/" class="btn btn-neutral float-right" title="延迟着色法">Next <span class="icon icon-circle-arrow-right"></span></a>
<a href="../26-cascaded-shadow-maps/" class="btn btn-neutral" title="级联阴影映射"><span class="icon icon-circle-arrow-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
<span><a href="../26-cascaded-shadow-maps/" style="color: #fcfcfc">&laquo; Previous</a></span>
<span><a href="../28-deferred-shading/" style="color: #fcfcfc">Next &raquo;</a></span>
</span>
</div>
<script>var base_url = '..';</script>
<script src="../js/theme_extra.js" defer></script>
<script src="../js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="../search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

1050
28-deferred-shading/index.html Executable file

File diff suppressed because it is too large Load Diff

246
404.html Executable file
View File

@@ -0,0 +1,246 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Mouse0w0">
<link rel="shortcut icon" href="/img/favicon.ico">
<title>Lwjglbook中文翻译</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />
<link rel="stylesheet" href="/css/theme.css" />
<link rel="stylesheet" href="/css/theme_extra.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css" />
<script src="/js/jquery-2.1.1.min.js" defer></script>
<script src="/js/modernizr-2.8.3.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="/." class="icon icon-home"> Lwjglbook中文翻译</a>
<div role="search">
<form id ="rtd-search-form" class="wy-form" action="//search.html" method="get">
<input type="text" name="q" placeholder="Search docs" title="Type search term here" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<ul>
<li class="toctree-l1"><a class="reference internal" href="/01-first-steps/">事前准备</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/02-the-game-loop/">游戏循环</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/03-a-brief-about-coordinates/">坐标简介</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/04-rendering/">渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/05-more-on-rendering/">渲染补充</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/06-transformations/">变换</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/07-textures/">纹理</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/08-camera/">摄像机</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/09-loading-more-complex-models/">加载更复杂的模型</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/10-let-there-be-light/">要有光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/11-let-there-be-even-more-light/">要有更多的光</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/12-game-hud/">游戏HUD</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/14-height-maps/">高度图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/15-terrain-collisions/">地形碰撞</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/16-fog/"></a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/17-normal-mapping/">法线贴图</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/18-shadows/">阴影</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/19-animations/">动画</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/20-particles/">粒子</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/21-instanced-rendering/">实例化渲染</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/22-audio/">音效</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/23-3d-object-picking/">三维物体选取</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/24-hud-revisited/">回顾HUD - NanoVG</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/26-cascaded-shadow-maps/">级联阴影映射</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/27-assimp/">Assimp库</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/28-deferred-shading/">延迟着色法</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/a01-opengl-debugging/">附录 A - OpenGL调试</a>
</li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="/glossary/">术语表</a>
</li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="/.">Lwjglbook中文翻译</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="/.">Docs</a> &raquo;</li>
<li class="wy-breadcrumbs-aside">
</li>
</ul>
<hr/>
</div>
<div role="main">
<div class="section">
<h1 id="404-page-not-found">404</h1>
<p><strong>Page not found</strong></p>
</div>
</div>
<footer>
<hr/>
<div role="contentinfo">
<!-- Copyright etc -->
<p>2019, Mouse0w0</p>
</div>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<div class="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span>
<a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="color: #fcfcfc"> GitHub</a>
</span>
</span>
</div>
<script>var base_url = '/';</script>
<script src="/js/theme_extra.js" defer></script>
<script src="/js/theme.js" defer></script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
<script src="/search/main.js" defer></script>
<script defer>
window.onload = function () {
SphinxRtdTheme.Navigation.enable(true);
};
</script>
</body>
</html>

BIN
_static/01/hello_world.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
_static/01/intellij.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
_static/01/maven_project.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
_static/03/opengl_coordinates.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
_static/04/rendering_pipeline.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
_static/04/triangle_window.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
_static/05/coloured_quad.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
_static/05/coordinates.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
_static/05/dolphin.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
_static/05/quad_coordinates.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
_static/05/quad_rendered.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
_static/06/2_2_matrix.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

BIN
_static/06/coordinates.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
_static/06/projection_matrix.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
_static/06/rectangle.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
_static/06/square_1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
_static/06/square_coloured.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
_static/06/transformations.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
_static/07/cube_coords.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
_static/07/cube_depth_test.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
_static/07/cube_no_depth_test.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
_static/07/cube_texture.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
_static/07/cube_with_texture.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
_static/07/texture_mapping.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
_static/08/actual_movement.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
_static/08/camera_movement.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
_static/08/cubes.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
_static/08/new_transf_eq.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
_static/08/roll_pitch_yaw.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
_static/09/edge_split.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
_static/09/face_definition.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
_static/09/normals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
_static/09/obj_export_options.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
_static/09/ordering_i.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
_static/09/ordering_ii.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
_static/09/ordering_iii.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
_static/09/ordering_iv.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
_static/09/ordering_v.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
_static/09/ordering_vi.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
_static/09/ordering_vii.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
_static/09/standford_bunny.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
_static/09/textured_cube.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
_static/09/vertex_normals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
_static/10/diffuse_calc_i.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
_static/10/diffuse_light.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
_static/10/dot_product.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
_static/10/light_reflection.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
_static/10/light_types.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
_static/10/lightning_result.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
_static/10/polished_surface.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
_static/10/surface.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
_static/11/directional_light.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
_static/11/multiple_lights.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Some files were not shown because too many files have changed in this diff Show More