# 第十八課: Billboard和粒子
# 第十八課:Billbard和粒子
公告板是3D世界中的2D元素。它既不是最頂層的2D菜單,也不是可以隨意轉動的3D平面,而是介于兩者之間的一種元素,比如游戲中的血條。
公告板的獨特之處在于:它位于某個特定位置,朝向是自動計算的,這樣它就能始終面向相機(觀察者)。
## 方案1:2D法
2D法十分簡單。只需計算出點在屏幕空間的坐標,然后在該處顯示2D文本(參見第十一課)即可。
```
<pre class="calibre16">```
<span class="token2">// Everything here is explained in Tutorial 3 ! There's nothing new.</span>
glm<span class="token1">:</span><span class="token1">:</span>vec4 <span class="token3">BillboardPos_worldspace</span><span class="token1">(</span>x<span class="token1">,</span>y<span class="token1">,</span>z<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>vec4 BillboardPos_screenspace <span class="token">=</span> ProjectionMatrix <span class="token">*</span> ViewMatrix <span class="token">*</span> BillboardPos_worldspace<span class="token1">;</span>
BillboardPos_screenspace <span class="token">/</span><span class="token">=</span> BillboardPos_screenspace<span class="token1">.</span>w<span class="token1">;</span>
<span class="token4">if</span> <span class="token1">(</span>BillboardPos_screenspace<span class="token1">.</span>z <span class="token"><</span> <span class="token6">0.0</span>f<span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Object is behind the camera, don't display it.</span>
<span class="token1">}</span>
```
```
就這么搞定了!
2D法優點是簡單易行,無論點與相機距離遠近,公告板始終保持大小不變。但此法總是把文本顯示在最頂層,有可能會遮擋其他物體,影響渲染效果。
## 方案2:3D法
與2D法相比,3D法常常效果更好,也沒復雜多少。我們的目的就是無論相機如何移動,都要讓公告板網格正對著相機:

可將此視為模型矩陣的構造問題之簡化版。基本思路是將公告板的各角落置于 (存疑待查)The idea is that each corner of the billboard is at the center position, displaced by the camera’s up and right vectors :

當然,我們僅僅知道世界空間中的公告板中心位置,因此還需要相機在世界空間中的up/right向量。
在相機空間,相機的up向量為(0,1,0)。要把up向量變換到世界空間,只需乘以觀察矩陣的逆矩陣(由相機空間變換至世界空間的矩陣)。
用數學公式表示即:
CameraRight\_worldspace = {ViewMatrix\[0\]\[0\], ViewMatrix\[1\]\[0\], ViewMatrix\[2\]\[0\]}CameraUp\_worldspace = {ViewMatrix\[0\]\[1\], ViewMatrix\[1\]\[1\], ViewMatrix\[2\]\[1\]}
接下來,頂點坐標的計算就很簡單了:
```
<pre class="calibre16">```
vec3 vertexPosition_worldspace <span class="token">=</span>
particleCenter_wordspace
<span class="token">+</span> CameraRight_worldspace <span class="token">*</span> squareVertices<span class="token1">.</span>x <span class="token">*</span> BillboardSize<span class="token1">.</span>x
<span class="token">+</span> CameraUp_worldspace <span class="token">*</span> squareVertices<span class="token1">.</span>y <span class="token">*</span> BillboardSize<span class="token1">.</span>y<span class="token1">;</span>
```
```
- `particleCenter_worldspace`顧名思義即公告板的中心位置,以vec3類型的uniform變量表示。
- `squareVertices`是原始的網格。左頂點的`squareVertices.x`為-0.5(存疑待查),which are thus moved towars the left of the camera (because of the \*CameraRight\_worldspace)
- `BillboardSize`是公告板大小,以世界單位為單位,uniform變量。
效果如下。怎么樣,是不是很簡單?

為了保證內容完整性,這里給出`squareVertices`的數據:
```
<pre class="calibre16">```
<span class="token2">// The VBO containing the 4 vertices of the particles.</span>
static const GLfloat g_vertex_buffer_data<span class="token1">[</span><span class="token1">]</span> <span class="token">=</span> <span class="token1">{</span>
<span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">0.5</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token1">}</span><span class="token1">;</span>
```
```
## 方案3:固定大小3D法
正如上面所看到的,公告板大小隨著相機與之的距離變化。有些情況下的確需要這樣的效果,但血條這類公告板則需要保持大小不變。
```
<pre class="calibre16">```
vertexPosition_worldspace <span class="token">=</span> particleCenter_wordspace<span class="token1">;</span>
<span class="token2">// Get the screen-space position of the particle's center</span>
gl_Position <span class="token">=</span> VP <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_worldspace<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Here we have to do the perspective division ourselves.</span>
gl_Position <span class="token">/</span><span class="token">=</span> gl_Position<span class="token1">.</span>w<span class="token1">;</span>
<span class="token2">// Move the vertex in directly screen space. No need for CameraUp/Right_worlspace here.</span>
gl_Position<span class="token1">.</span>xy <span class="token">+</span><span class="token">=</span> squareVertices<span class="token1">.</span>xy <span class="token">*</span> <span class="token3">vec2</span><span class="token1">(</span><span class="token6">0.2</span><span class="token1">,</span> <span class="token6">0.05</span><span class="token1">)</span><span class="token1">;</span>
```
```

## 方案4:限制垂直旋轉法
一些引擎以公告板表示遠處的樹和燈。不過,這些樹可不能任意轉向,**必須**是豎直的。So you need an hybrid system that rotates only around one axis.(存疑待查)
這個方案作為練習留給讀者。
# 粒子(Particles)與實例(Instancing)
粒子與3D公告板很類似。不過,粒子有如下四個特點:
- 數量較大
- 可以運動
- 有生有死
- 半透明
伴隨這些特點而來的是一系列問題。本課僅介紹**其中一種**解決方案,其他解決方案還多著呢……
## 一大波粒子正在接近中……
首先想到的思路就是套用上一課的代碼,調用`glDrawArrays`逐個繪制粒子。這可不是個好辦法。因為這種思路意味著你那锃光瓦亮的GTX 512顯卡一次只能繪制**一個**四邊形(很明顯,性能損失高達99%)。就這么一個接一個地繪制公告板。
顯然,我們得一次性繪制所有的粒子。
方法有很多種,如下是其中三種:
- 生成一個VBO,將所有粒子置于其中。簡單,有效,在各種平臺上均可行。
- 使用geometry shader。這不在本教程范圍內,主要是因為50%的機器不支持該特性。
- 使用實例(instancing)。大部分機器都支持該特性。
本課將采用第三種方法。這種方法兼具性能優勢和普適性,更重要的是,如果此法行得通,那第一種方法也就輕而易舉了。
## 實例
“實例”的意思是以一個網格(比如本課中由兩個三角形組成的四邊形)為藍本,創建多個該網格的實例。
具體地講,我們通過如下一些buffer實現instancing:
- 一部分用于描述原始網格
- 一部分用于描述各實例的特性
這些buffer的內容可自行選擇。在我們這個簡單的例子包含了:
- 一個網格頂點buffer。沒有index buffer,因此一共有6個`vec3`變量,構成兩個三角形,進而組合成一個四邊形。
- 一個buffer存儲粒子的中心。
- 一個buffer存儲粒子的顏色。
這些buffer都是標準buffer。創建方式如下:
```
<pre class="calibre16">```
<span class="token2">// The VBO containing the 4 vertices of the particles.</span>
<span class="token2">// Thanks to instancing, they will be shared by all particles.</span>
static const GLfloat g_vertex_buffer_data<span class="token1">[</span><span class="token1">]</span> <span class="token">=</span> <span class="token1">{</span>
<span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">0.5</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token">-</span><span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.5</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token1">}</span><span class="token1">;</span>
GLuint billboard_vertex_buffer<span class="token1">;</span>
<span class="token3">glGenBuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>billboard_vertex_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> billboard_vertex_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> <span class="token3">sizeof</span><span class="token1">(</span>g_vertex_buffer_data<span class="token1">)</span><span class="token1">,</span> g_vertex_buffer_data<span class="token1">,</span> GL_STATIC_DRAW<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// The VBO containing the positions and sizes of the particles</span>
GLuint particles_position_buffer<span class="token1">;</span>
<span class="token3">glGenBuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>particles_position_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_position_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Initialize with empty (NULL) buffer : it will be updated later, each frame.</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> MaxParticles <span class="token">*</span> <span class="token6">4</span> <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLfloat<span class="token1">)</span><span class="token1">,</span> NULL<span class="token1">,</span> GL_STREAM_DRAW<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// The VBO containing the colors of the particles</span>
GLuint particles_color_buffer<span class="token1">;</span>
<span class="token3">glGenBuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>particles_color_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_color_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Initialize with empty (NULL) buffer : it will be updated later, each frame.</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> MaxParticles <span class="token">*</span> <span class="token6">4</span> <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLubyte<span class="token1">)</span><span class="token1">,</span> NULL<span class="token1">,</span> GL_STREAM_DRAW<span class="token1">)</span><span class="token1">;</span>
```
```
粒子更新方法如下:
```
<pre class="calibre16">```
<span class="token2">// Update the buffers that OpenGL uses for rendering.</span>
<span class="token2">// There are much more sophisticated means to stream data from the CPU to the GPU,</span>
<span class="token2">// but this is outside the scope of this tutorial.</span>
<span class="token2">// http://www.opengl.org/wiki/Buffer_Object_Streaming</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_position_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> MaxParticles <span class="token">*</span> <span class="token6">4</span> <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLfloat<span class="token1">)</span><span class="token1">,</span> NULL<span class="token1">,</span> GL_STREAM_DRAW<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Buffer orphaning, a common way to improve streaming perf. See above link for details.</span>
<span class="token3">glBufferSubData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> ParticlesCount <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLfloat<span class="token1">)</span> <span class="token">*</span> <span class="token6">4</span><span class="token1">,</span> g_particule_position_size_data<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_color_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> MaxParticles <span class="token">*</span> <span class="token6">4</span> <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLubyte<span class="token1">)</span><span class="token1">,</span> NULL<span class="token1">,</span> GL_STREAM_DRAW<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Buffer orphaning, a common way to improve streaming perf. See above link for details.</span>
<span class="token3">glBufferSubData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> ParticlesCount <span class="token">*</span> <span class="token3">sizeof</span><span class="token1">(</span>GLubyte<span class="token1">)</span> <span class="token">*</span> <span class="token6">4</span><span class="token1">,</span> g_particule_color_data<span class="token1">)</span><span class="token1">;</span>
```
```
繪制之前還需綁定buffer。綁定方法如下:
```
<pre class="calibre16">```
<span class="token2">// 1rst attribute buffer : vertices</span>
<span class="token3">glEnableVertexAttribArray</span><span class="token1">(</span><span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> billboard_vertex_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glVertexAttribPointer</span><span class="token1">(</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// attribute. No particular reason for 0, but must match the layout in the shader.</span>
<span class="token6">3</span><span class="token1">,</span> <span class="token2">// size</span>
GL_FLOAT<span class="token1">,</span> <span class="token2">// type</span>
GL_FALSE<span class="token1">,</span> <span class="token2">// normalized?</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// stride</span>
<span class="token1">(</span>void<span class="token">*</span><span class="token1">)</span><span class="token6">0</span> <span class="token2">// array buffer offset</span>
<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// 2nd attribute buffer : positions of particles' centers</span>
<span class="token3">glEnableVertexAttribArray</span><span class="token1">(</span><span class="token6">1</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_position_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glVertexAttribPointer</span><span class="token1">(</span>
<span class="token6">1</span><span class="token1">,</span> <span class="token2">// attribute. No particular reason for 1, but must match the layout in the shader.</span>
<span class="token6">4</span><span class="token1">,</span> <span class="token2">// size : x + y + z + size => 4</span>
GL_FLOAT<span class="token1">,</span> <span class="token2">// type</span>
GL_FALSE<span class="token1">,</span> <span class="token2">// normalized?</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// stride</span>
<span class="token1">(</span>void<span class="token">*</span><span class="token1">)</span><span class="token6">0</span> <span class="token2">// array buffer offset</span>
<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// 3rd attribute buffer : particles' colors</span>
<span class="token3">glEnableVertexAttribArray</span><span class="token1">(</span><span class="token6">2</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> particles_color_buffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glVertexAttribPointer</span><span class="token1">(</span>
<span class="token6">2</span><span class="token1">,</span> <span class="token2">// attribute. No particular reason for 1, but must match the layout in the shader.</span>
<span class="token6">4</span><span class="token1">,</span> <span class="token2">// size : r + g + b + a => 4</span>
GL_UNSIGNED_BYTE<span class="token1">,</span> <span class="token2">// type</span>
GL_TRUE<span class="token1">,</span> <span class="token2">// normalized? *** YES, this means that the unsigned char[4] will be accessible with a vec4 (floats) in the shader ***</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// stride</span>
<span class="token1">(</span>void<span class="token">*</span><span class="token1">)</span><span class="token6">0</span> <span class="token2">// array buffer offset</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
繪制方法與以往有所不同。這次不使用`glDrawArrays`或者`glDrawElements`(如果原始網格有index buffer的話)。這次用的是`glDrawArraysInstanced`或者`glDrawElementsInstanced`,效果等同于調用`glDrawArrays`N次(N是最后一個參數,此例中即`ParticlesCount`)。
```
<pre class="calibre16">```
<span class="token3">glDrawArraysInstanced</span><span class="token1">(</span>GL_TRIANGLE_STRIP<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> <span class="token6">4</span><span class="token1">,</span> ParticlesCount<span class="token1">)</span><span class="token1">;</span>
```
```
有件事差點忘了。我們還沒告訴OpenGL哪個buffer是原始網格,哪些buffer是各實例的特性。調用`glVertexAttribDivisor`即可完成。有完整注釋的代碼如下:
```
<pre class="calibre16">```
<span class="token2">// These functions are specific to glDrawArrays*Instanced*.</span>
<span class="token2">// The first parameter is the attribute buffer we're talking about.</span>
<span class="token2">// The second parameter is the "rate at which generic vertex attributes advance when rendering multiple instances"</span>
<span class="token2">// http://www.opengl.org/sdk/docs/man/xhtml/glVertexAttribDivisor.xml</span>
<span class="token3">glVertexAttribDivisor</span><span class="token1">(</span><span class="token6">0</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">;</span> <span class="token2">// particles vertices : always reuse the same 4 vertices -> 0</span>
<span class="token3">glVertexAttribDivisor</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token6">1</span><span class="token1">)</span><span class="token1">;</span> <span class="token2">// positions : one per quad (its center) -> 1</span>
<span class="token3">glVertexAttribDivisor</span><span class="token1">(</span><span class="token6">2</span><span class="token1">,</span> <span class="token6">1</span><span class="token1">)</span><span class="token1">;</span> <span class="token2">// color : one per quad -> 1</span>
<span class="token2">// Draw the particules !</span>
<span class="token2">// This draws many times a small triangle_strip (which looks like a quad).</span>
<span class="token2">// This is equivalent to :</span>
<span class="token2">// for(i in ParticlesCount) : glDrawArrays(GL_TRIANGLE_STRIP, 0, 4),</span>
<span class="token2">// but faster.</span>
<span class="token3">glDrawArraysInstanced</span><span class="token1">(</span>GL_TRIANGLE_STRIP<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> <span class="token6">4</span><span class="token1">,</span> ParticlesCount<span class="token1">)</span><span class="token1">;</span>
```
```
如你所見,instancing是很靈活的,你可以將`AttribDivisor`設為任意整數。例如,'glVertexAttribDivisor(2, 10)'即設置后續10個實例都擁有相同的顏色。
## 意義何在?
意義在于如今我們只需在每幀中更新一個很小的buffer(粒子中心位置),而非整個網格。如此一來,帶寬利用效率提升了4倍。
## 生與死
于場景中其它對象不同的是,粒子的生死更替十分頻繁。我們得用一種速度相當快的方式來創建新粒子,拋棄舊粒子。`new Particle()`這種辦法顯然不夠好。
## 創建新粒子
首先得創建一個大的粒子容器:
```
<pre class="calibre16">```
<span class="token2">// CPU representation of a particle</span>
struct Particle<span class="token1">{</span>
glm<span class="token1">:</span><span class="token1">:</span>vec3 pos<span class="token1">,</span> speed<span class="token1">;</span>
unsigned char r<span class="token1">,</span>g<span class="token1">,</span>b<span class="token1">,</span>a<span class="token1">;</span> <span class="token2">// Color</span>
float size<span class="token1">,</span> angle<span class="token1">,</span> weight<span class="token1">;</span>
float life<span class="token1">;</span> <span class="token2">// Remaining life of the particle. if < 0 : dead and unused.</span>
<span class="token1">}</span><span class="token1">;</span>
const int MaxParticles <span class="token">=</span> <span class="token6">100000</span><span class="token1">;</span>
Particle ParticlesContainer<span class="token1">[</span>MaxParticles<span class="token1">]</span><span class="token1">;</span>
```
```
接下來,我們得想辦法創建新粒子。如下的函數在`ParticleContainer`中線性搜索(聽起來有些暴力)新粒子。不過,它是從上次已知位置開始搜索的,因此一般很快就返回了。
```
<pre class="calibre16">```
int LastUsedParticle <span class="token">=</span> <span class="token6">0</span><span class="token1">;</span>
<span class="token2">// Finds a Particle in ParticlesContainer which isn't used yet.</span>
<span class="token2">// (i.e. life < 0);</span>
int <span class="token3">FindUnusedParticle</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
<span class="token4">for</span><span class="token1">(</span>int i<span class="token">=</span>LastUsedParticle<span class="token1">;</span> i<span class="token"><</span>MaxParticles<span class="token1">;</span> i<span class="token">++</span><span class="token1">)</span><span class="token1">{</span>
<span class="token4">if</span> <span class="token1">(</span>ParticlesContainer<span class="token1">[</span>i<span class="token1">]</span><span class="token1">.</span>life <span class="token"><</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">{</span>
LastUsedParticle <span class="token">=</span> i<span class="token1">;</span>
<span class="token4">return</span> i<span class="token1">;</span>
<span class="token1">}</span>
<span class="token1">}</span>
<span class="token4">for</span><span class="token1">(</span>int i<span class="token">=</span><span class="token6">0</span><span class="token1">;</span> i<span class="token"><</span>LastUsedParticle<span class="token1">;</span> i<span class="token">++</span><span class="token1">)</span><span class="token1">{</span>
<span class="token4">if</span> <span class="token1">(</span>ParticlesContainer<span class="token1">[</span>i<span class="token1">]</span><span class="token1">.</span>life <span class="token"><</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">{</span>
LastUsedParticle <span class="token">=</span> i<span class="token1">;</span>
<span class="token4">return</span> i<span class="token1">;</span>
<span class="token1">}</span>
<span class="token1">}</span>
<span class="token4">return</span> <span class="token6">0</span><span class="token1">;</span> <span class="token2">// All particles are taken, override the first one</span>
<span class="token1">}</span>
```
```
現在我們可以把`ParticlesContainer[particleIndex]`當中的`life`、`color`、`speed`和`position`設置成一些有趣的值。欲知詳情請看代碼,此處大有文章可作。我們比較關心的是每一幀中要生成多少粒子。這跟具體的應用有關,我們就設為每秒10000個(噢噢,略多啊)新粒子好了:
```
<pre class="calibre16">```
int newparticles <span class="token">=</span> <span class="token1">(</span>int<span class="token1">)</span><span class="token1">(</span>deltaTime<span class="token">*</span><span class="token6">10000.0</span><span class="token1">)</span><span class="token1">;</span>
```
```
記得把個數限定在一個固定范圍內:
```
<pre class="calibre16">```
<span class="token2">// Generate 10 new particule each millisecond,</span>
<span class="token2">// but limit this to 16 ms (60 fps), or if you have 1 long frame (1sec),</span>
<span class="token2">// newparticles will be huge and the next frame even longer.</span>
int newparticles <span class="token">=</span> <span class="token1">(</span>int<span class="token1">)</span><span class="token1">(</span>deltaTime<span class="token">*</span><span class="token6">10000.0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token4">if</span> <span class="token1">(</span>newparticles <span class="token">></span> <span class="token1">(</span>int<span class="token1">)</span><span class="token1">(</span><span class="token6">0.016</span>f<span class="token">*</span><span class="token6">10000.0</span><span class="token1">)</span><span class="token1">)</span>
newparticles <span class="token">=</span> <span class="token1">(</span>int<span class="token1">)</span><span class="token1">(</span><span class="token6">0.016</span>f<span class="token">*</span><span class="token6">10000.0</span><span class="token1">)</span><span class="token1">;</span>
```
```
## 刪除舊粒子
這個需要一些技巧,參見下文=)
## 仿真主循環
`ParticlesContainer`同時容納了“活著的”和“死亡的”粒子,但發送到GPU的buffer僅含活著的粒子。
所以,我們要遍歷每個粒子,看它是否是活著的,是否應該“處死”。如果一切正常,那就添加重力,最后將其拷貝到GPU上相應的buffer中。
```
<pre class="calibre16">```
<span class="token2">// Simulate all particles</span>
int ParticlesCount <span class="token">=</span> <span class="token6">0</span><span class="token1">;</span>
<span class="token4">for</span><span class="token1">(</span>int i<span class="token">=</span><span class="token6">0</span><span class="token1">;</span> i<span class="token"><</span>MaxParticles<span class="token1">;</span> i<span class="token">++</span><span class="token1">)</span><span class="token1">{</span>
Particle<span class="token">&</span> p <span class="token">=</span> ParticlesContainer<span class="token1">[</span>i<span class="token1">]</span><span class="token1">;</span> <span class="token2">// shortcut</span>
<span class="token4">if</span><span class="token1">(</span>p<span class="token1">.</span>life <span class="token">></span> <span class="token6">0.0</span>f<span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Decrease life</span>
p<span class="token1">.</span>life <span class="token">-</span><span class="token">=</span> delta<span class="token1">;</span>
<span class="token4">if</span> <span class="token1">(</span>p<span class="token1">.</span>life <span class="token">></span> <span class="token6">0.0</span>f<span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Simulate simple physics : gravity only, no collisions</span>
p<span class="token1">.</span>speed <span class="token">+</span><span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.0</span>f<span class="token1">,</span><span class="token">-</span><span class="token6">9.81</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">)</span> <span class="token">*</span> <span class="token1">(</span>float<span class="token1">)</span>delta <span class="token">*</span> <span class="token6">0.5</span>f<span class="token1">;</span>
p<span class="token1">.</span>pos <span class="token">+</span><span class="token">=</span> p<span class="token1">.</span>speed <span class="token">*</span> <span class="token1">(</span>float<span class="token1">)</span>delta<span class="token1">;</span>
p<span class="token1">.</span>cameradistance <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">length2</span><span class="token1">(</span> p<span class="token1">.</span>pos <span class="token">-</span> CameraPosition <span class="token1">)</span><span class="token1">;</span>
<span class="token2">//ParticlesContainer[i].pos += glm::vec3(0.0f,10.0f, 0.0f) * (float)delta;</span>
<span class="token2">// Fill the GPU buffer</span>
g_particule_position_size_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">0</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>pos<span class="token1">.</span>x<span class="token1">;</span>
g_particule_position_size_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">1</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>pos<span class="token1">.</span>y<span class="token1">;</span>
g_particule_position_size_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">2</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>pos<span class="token1">.</span>z<span class="token1">;</span>
g_particule_position_size_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">3</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>size<span class="token1">;</span>
g_particule_color_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">0</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>r<span class="token1">;</span>
g_particule_color_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">1</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>g<span class="token1">;</span>
g_particule_color_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">2</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>b<span class="token1">;</span>
g_particule_color_data<span class="token1">[</span><span class="token6">4</span><span class="token">*</span>ParticlesCount<span class="token">+</span><span class="token6">3</span><span class="token1">]</span> <span class="token">=</span> p<span class="token1">.</span>a<span class="token1">;</span>
<span class="token1">}</span><span class="token4">else</span><span class="token1">{</span>
<span class="token2">// Particles that just died will be put at the end of the buffer in SortParticles();</span>
p<span class="token1">.</span>cameradistance <span class="token">=</span> <span class="token">-</span><span class="token6">1.0</span>f<span class="token1">;</span>
<span class="token1">}</span>
ParticlesCount<span class="token">++</span><span class="token1">;</span>
<span class="token1">}</span>
<span class="token1">}</span>
```
```
如下所示,效果看上去差不多了,不過還有一個問題……

## 排序
正如\[第十課\]\[1\]中所講,你必須按從后往前的順序對半透明對象排序,方可獲得正確的混合效果。
```
<pre class="calibre16">```
void <span class="token3">SortParticles</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
std<span class="token1">:</span><span class="token1">:</span><span class="token3">sort</span><span class="token1">(</span><span class="token">&</span>ParticlesContainer<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">,</span> <span class="token">&</span>ParticlesContainer<span class="token1">[</span>MaxParticles<span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
`std::sort`需要一個函數判斷粒子的在容器中的先后順序。重載`Particle::operator<`即可:
```
<pre class="calibre16">```
<span class="token2">// CPU representation of a particle</span>
struct Particle<span class="token1">{</span>
<span class="token1">.</span><span class="token1">.</span><span class="token1">.</span>
bool operator<span class="token"><</span><span class="token1">(</span>Particle<span class="token">&</span> that<span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Sort in reverse order : far particles drawn first.</span>
<span class="token4">return</span> this<span class="token">-</span><span class="token">></span>cameradistance <span class="token">></span> that<span class="token1">.</span>cameradistance<span class="token1">;</span>
<span class="token1">}</span>
<span class="token1">}</span><span class="token1">;</span>
```
```
這樣`ParticleContainer`中的粒子就是排好序的了,顯示效果已經變正確了:

## 延伸課題
## 動畫粒子
你可以用紋理圖集(texture atlas)實現粒子的動畫效果。將各粒子的年齡和位置發送到GPU,按照\[2D字體一課\]\[2\]的方法在shader中計算UV坐標,紋理圖集是這樣的:

## 處理多個粒子系統
如果你需要多個粒子系統,有兩種方案可選:要么僅用一個粒子容器,要么每個粒子系統一個。
如果選擇將**所有**粒子放在一個容器中,那么就能很好地對粒子進行排序。主要缺陷是所有的粒子都得使用同一個紋理。這個問題可借助紋理圖集加以解決。紋理圖集是一張包含所有紋理的大紋理,可通過UV坐標訪問各紋理,其使用和編輯并不是很方便。
如果為每個粒子系統設置一個粒子容器,那么只能在各容器內部對粒子進行排序。這就導致一個問題:如果兩粒子系統相互重疊,我們就會看到瑕疵。不過,如果你的應用中不會出現兩粒子系統重疊的情況,那這就不是問題。
當然,你也可以采用一種混合系統:若干個粒子系統,各自配備紋理圖集(足夠小,易于管理)。
## 平滑粒子
你很快就能發現一個常見的瑕疵:當粒子和幾何體相交時,粒子的邊界變得很明顯,十分難看:

(image from <http://www.gamerendering.com/2009/09/16/soft-particles/> )
一個通常采用的解決方法是測試當前繪制的片斷的深度值。如果該片斷的深度值是“較近”的,就將其淡出。
然而,這就需要對Z-Buffer進行采樣。這在“正常”的Z-Buffer中是不可行的。你得將場景渲染到一個\[渲染目標\]\[3\]。或者,你可以用`glBlitFrameBuffer`把Z-Buffer內容從一個幀緩沖拷貝到另一個。
[http://developer.download.nvidia.com/whitepapers/2007/SDK10/SoftParticles\_hi.pdf](http://developer.download.nvidia.com/whitepapers/2007/SDK10/SoftParticles_hi.pdf)
## 提高填充率
當前GPU的一個主要限制因素就是填充率:在16.6ms內可寫片段(像素)數量要足夠多,以達到60FPS。
這是一個大問題。由于粒子一般需要**很高**的填充率,同一個片段要重復繪制10多次,每次都是不同的粒子。如果不這么做,最終效果就會出現上述瑕疵。
在所有寫入的的片段中,很多都是毫無用處的:比如位于邊界上的片段。你的粒子紋理在邊界上通常是完全透明的,但粒子的網格卻仍然得繪制這些無用的片段,然后用與之前完全相同的值更新顏色緩沖。
這個小工具能夠計算紋理的緊湊包圍網格(這個也就是用`glDrawArraysInstanced()`渲染的那個網格):

[\[http://www.humus.name/index.php?page=Cool&ID=8\]\[4\]。Emil](http://www.humus.name/index.php?page=Cool&ID=8%5D%5B4%5D%E3%80%82Emil) Person的網站上也有很多精彩的文章。
## 粒子物理效果
有些應用中,你可能想讓粒子和世界產生一些交互。比如,粒子可以在撞到地面時反彈。
比較簡單的做法是為每個粒子做光線投射(raycasting),投射方向為當前位置與未來位置形成的向量。我們將在\[拾取教程\]\[5\]。但這種做法開銷太大了,你沒法做到在每一幀中為每個粒子做光線投射。
根據你的具體應用,可以用一系列平面來近似幾何體(譯注:k-DOP),然后 對這些平面做光線投射。你也可以采用真正的光線投射,將結果緩存起來,然后據此近似計算附近的碰撞(也可以兼用兩種方法)。
另一種迥異的技術是將現有的Z-Buffer作為幾何體的粗略近似,在此之上進行粒子碰撞。這種方法效果“足夠好”,速度快。不過由于無法在CPU端訪問Z-Buffer(至少速度不夠快),你得完全在GPU上進行仿真。因此,這種方法更加復雜。
如下是一些相關文章:\[\[<http://www.altdevblogaday.com/2012/06/19/hack-day-report/>\][6](http://www.altdevblogaday.com/2012/06/19/hack-day-report/%5D%5B6)\]
\[\[[http://www.gdcvault.com/search.php#&category=free&firstfocus=&keyword=Chris+Tchou’s%2BHalo%2BReach%2BEffects&conference\_id=](http://www.gdcvault.com/search.php#&category=free&firstfocus=&keyword=Chris+Tchou%E2%80%99s%2BHalo%2BReach%2BEffects&conference_id=)\][7](http://www.gdcvault.com/search.php#&category=free&firstfocus=&keyword=Chris+Tchou%E2%80%99s%2BHalo%2BReach%2BEffects&conference_id=%5D%5B7)\]
## GPU仿真
如上所述,你可以完全在GPU上模擬粒子的運動。你還是得在CPU端管理粒子的生命周期——至少在創建粒子時。
可選方案很多,不過都不屬于本課程討論范圍。這里僅給出一些指引。
- 采用變換反饋(Transform Feedback)機制。Transform Feedback讓你能夠將頂點著色器的輸出結果存儲到GPU端的VBO中。把新位置存儲到這個VBO,然后在下一幀以這個VBO為起點,然后再將更新的位置存儲到前一個VBO中。原理相同但無需Transform Feedback的方法:將粒子的位置編碼到一張紋理中,然后利用渲染到紋理(Render-To-Texture)更新之。
- 采用通用GPU計算庫:CUDA或OpenCL。這些庫具有與OpenGL互操作的函數。
- 采用計算著色器Compute Shader。這是最漂亮的解決方案,不過只在較新的GPU上可用。
> 請注意,為了簡化問題,在本課的實現中`ParticleContainer`是在GPU buffer都更新之后再排序的。這使得粒子的排序變得不準確了(有一幀的延遲),不過不是太明顯。你可以把主循環拆分成仿真、排序兩部分,然后再更新,就可以解決這個問題。