mirror of
https://github.com/mouse0w0/lwjglbook-CN-Translation.git
synced 2025-08-22 20:25:29 +08:00
468 lines
25 KiB
HTML
Executable File
468 lines
25 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html class="writer-html5" lang="en" >
|
||
<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="../css/theme.css" />
|
||
<link rel="stylesheet" href="../css/theme_extra.css" />
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.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>
|
||
|
||
<!--[if lt IE 9]>
|
||
<script src="../js/html5shiv.min.js"></script>
|
||
<![endif]-->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||
<script>hljs.highlightAll();</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" aria-label="Search docs" title="Type search term here" />
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="Navigation menu">
|
||
<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="Mobile navigation menu">
|
||
<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=".." class="icon icon-home" aria-label="Docs"></a></li>
|
||
<li class="breadcrumb-item active">地形碰撞</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" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||
<div class="section" itemprop="articleBody">
|
||
|
||
<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 < terrainSize; row++) {
|
||
for (int col = 0; col < terrainSize; col++) {
|
||
float xDisplacement = (col - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getXLength();
|
||
float zDisplacement = (row - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getZLength();
|
||
|
||
GameItem terrainBlock = new GameItem(heightMapMesh.getMesh());
|
||
terrainBlock.setScale(scale);
|
||
terrainBlock.setPosition(xDisplacement, 0, zDisplacement);
|
||
gameItems[row * terrainSize + col] = terrainBlock;
|
||
|
||
boundingBoxes[row][col] = getBoundingBox(terrainBlock);
|
||
}
|
||
}
|
||
}
|
||
</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 < terrainSize && !found; row++) {
|
||
for (int col = 0; col < terrainSize && !found; col++) {
|
||
terrainBlock = gameItems[row * terrainSize + col];
|
||
boundingBox = boundingBoxes[row][col];
|
||
found = boundingBox.contains(position.x, position.z);
|
||
}
|
||
}
|
||
|
||
// 如果我们找到了一个包含我们位置的地形块
|
||
// 计算该位置的地形高度
|
||
if (found) {
|
||
Vector3f[] triangle = getTriangle(position, boundingBox, terrainBlock);
|
||
result = interpolateHeight(triangle[0], triangle[1], triangle[2], position.x, position.z);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
</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 < 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 <= 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="../14-height-maps/" class="btn btn-neutral float-left" title="高度图"><span class="icon icon-circle-arrow-left"></span> Previous</a>
|
||
<a href="../16-fog/" 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/readthedocs/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">« Previous</a></span>
|
||
|
||
|
||
<span><a href="../16-fog/" style="color: #fcfcfc">Next »</a></span>
|
||
|
||
</span>
|
||
</div>
|
||
<script src="../js/jquery-3.6.0.min.js"></script>
|
||
<script>var base_url = "..";</script>
|
||
<script src="../js/theme_extra.js"></script>
|
||
<script src="../js/theme.js"></script>
|
||
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
||
<script src="../search/main.js"></script>
|
||
<script>
|
||
jQuery(function () {
|
||
SphinxRtdTheme.Navigation.enable(true);
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|