Files
2024-07-17 21:58:59 +08:00

868 lines
40 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 = "\u7c92\u5b50";
var mkdocs_page_input_path = "20-particles.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>
<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="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/20-particles.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="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="../19-animations/" class="btn btn-neutral float-left" title="动画"><span class="icon icon-circle-arrow-left"></span> Previous</a>
<a href="../21-instanced-rendering/" 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="../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 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>