mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-22 20:25:28 +08:00
Rewrite 04-10
This commit is contained in:
@@ -3,41 +3,41 @@
|
||||
原文 | [Instancing](http://learnopengl.com/#!Advanced-OpenGL/Instancing)
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
翻译 | Meow J
|
||||
校对 | 暂未校对
|
||||
|
||||
假如你有一个有许多模型的场景,而这些模型的顶点数据都一样,只是进行了不同的世界空间的变换。想象一下,有一个场景中充满了草叶:每根草都是几个三角形组成的。你可能需要绘制很多的草叶,最终一次渲染循环中就肯能有成千上万个草需要绘制了。因为每个草叶只是由几个三角形组成,绘制一个几乎是即刻完成,但是数量巨大以后,执行起来就很慢了。
|
||||
假设你有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换。想象一个充满草的场景:每根草都是一个包含几个三角形的小模型。你可能会需要绘制很多根草,最终在每帧中你可能会需要渲染上千或者上万根草。因为每一根草仅仅是由几个三角形构成,渲染几乎是瞬间完成的,但上千个渲染函数调用却会极大地影响性能。
|
||||
|
||||
如果我们渲染这样多的物体的时候,也许代码会写成这样:
|
||||
如果我们需要渲染大量物体时,代码看起来会像这样:
|
||||
|
||||
```c++
|
||||
for(GLuint i = 0; i < amount_of_models_to_draw; i++)
|
||||
for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
|
||||
{
|
||||
DoSomePreparations(); //在这里绑定VAO、绑定纹理、设置uniform变量等
|
||||
DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等
|
||||
glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
|
||||
}
|
||||
```
|
||||
|
||||
如果像这样绘制模型的大量<def>实例</def>(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。与绘制顶点本身相比,使用<fun>glDrawArrays</fun>或<fun>glDrawElements</fun>函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以,即便渲染顶点非常快,命令GPU去渲染却未必。
|
||||
|
||||
像这样绘制出你模型的其他实例,多次绘制之后,很快将达到一个瓶颈,这是因为你`glDrawArrays`或`glDrawElements`这样的函数(Draw call)过多。这样渲染顶点数据,会明显降低执行效率,这是因为OpenGL在它可以绘制你的顶点数据之前必须做一些准备工作(比如告诉GPU从哪个缓冲读取数据,以及在哪里找到顶点属性,所有这些都会使CPU到GPU的总线变慢)。所以即使渲染顶点超快,而多次给你的GPU下达这样的渲染命令却未必。
|
||||
如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是<def>实例化</def>(Instancing)。
|
||||
|
||||
如果我们能够将数据一次发送给GPU,就会更方便,然后告诉OpenGL使用一个绘制函数,将这些数据绘制为多个物体。这就是我们将要展开讨论的**实例化(Instancing)**。
|
||||
实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。如果想使用实例化渲染,我们只需要将<fun>glDrawArrays</fun>和<fun>glDrawElements</fun>的渲染调用分别改为<fun>glDrawArraysInstanced</fun>和<fun>glDrawElementsInstanced</fun>就可以了。这些渲染函数的**实例化**版本需要一个额外的参数,叫做<def>实例数量</def>(Instance Count),它能够设置我们需要渲染的实例个数。这样我们只需要将必须的数据发送到GPU一次,然后使用一次函数调用告诉GPU它应该如何绘制这些实例。GPU将会直接渲染这些实例,而不用不断地与CPU进行通信。
|
||||
|
||||
实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染物体时从CPU到GPU的通信时间,而且只需做一次即可。要使用实例化渲染,我们必须将`glDrawArrays`和`glDrawElements`各自改为`glDrawArraysInstanced`和`glDrawElementsInstanced`。这些用于实例化的函数版本需要设置一个额外的参数,叫做**实例数量(Instance Count)**,它设置我们打算渲染实例的数量。这样我们就只需要把所有需要的数据发送给GPU一次就行了,然后告诉GPU它该如何使用一个函数来绘制所有这些实例。
|
||||
这个函数本身并没有什么用。渲染同一个物体一千次对我们并没有什么用处,每个物体都是完全相同的,而且还在同一个位置。我们只能看见一个物体!处于这个原因,GLSL在顶点着色器中嵌入了另一个内建变量,<var>gl_InstanceID</var>。
|
||||
|
||||
就其本身而言,这个函数用处不大。渲染同一个物体一千次对我们来说没用,因为每个渲染出的物体不仅相同而且还在同一个位置;我们只能看到一个物体!出于这个原因GLSL在着色器中嵌入了另一个内建变量,叫做**`gl_InstanceID`**。
|
||||
在使用实例化渲染调用时,<var>gl_InstanceID</var>会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的<var>gl_InstanceID</var>将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。
|
||||
|
||||
在通过实例化绘制时,`gl_InstanceID`的初值是0,它在每个实例渲染时都会增加1。如果我们渲染43个实例,那么在顶点着色器`gl_InstanceID`的值最后就是42。每个实例都拥有唯一的值意味着我们可以索引到一个位置数组,并将每个实例摆放在世界空间的不同的位置上。
|
||||
|
||||
我们调用一个实例化渲染函数,在标准化设备坐标中绘制一百个2D四边形来看看实例化绘制的效果是怎样的。通过对一个储存着100个偏移量向量的索引,我们为每个实例四边形添加一个偏移量。最后,窗口被排列精美的四边形网格填满:
|
||||
为了体验一下实例化绘制,我们将会在标准化设备坐标系中使用一个渲染调用,绘制100个2D四边形。我们会索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上。最终的结果是一个排列整齐的四边形网格:
|
||||
|
||||

|
||||
|
||||
每个四边形是2个三角形所组成的,因此总共有6个顶点。每个顶点包含一个2D标准设备坐标位置向量和一个颜色向量。下面是例子中所使用的顶点数据,每个三角形为了适应屏幕都很小:
|
||||
每个四边形由2个三角形所组成,一共有6个顶点。每个顶点包含一个2D的标准化设备坐标位置向量和一个颜色向量。
|
||||
下面就是这个例子使用的顶点数据,为了大量填充屏幕,每个三角形都很小:
|
||||
|
||||
```c++
|
||||
GLfloat quadVertices[] = {
|
||||
// ---位置--- ------颜色-------
|
||||
float quadVertices[] = {
|
||||
// 位置 // 颜色
|
||||
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
|
||||
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
|
||||
-0.05f, -0.05f, 0.0f, 0.0f, 1.0f,
|
||||
@@ -48,25 +48,26 @@ GLfloat quadVertices[] = {
|
||||
};
|
||||
```
|
||||
|
||||
片段着色器接收从顶点着色器发送来的颜色向量,设置为它的颜色输出,从而为四边形上色:
|
||||
片段着色器会从顶点着色器接受颜色向量,并将其设置为它的颜色输出,来实现四边形的颜色:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 fColor;
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(fColor, 1.0f);
|
||||
FragColor = vec4(fColor, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
到目前为止没有什么新内容,但顶点着色器开始变得有意思了:
|
||||
到现在都没有什么新内容,但从顶点着色器开始就变得很有趣了:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
layout (location = 1) in vec3 color;
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec3 aColor;
|
||||
|
||||
out vec3 fColor;
|
||||
|
||||
@@ -75,205 +76,210 @@ uniform vec2 offsets[100];
|
||||
void main()
|
||||
{
|
||||
vec2 offset = offsets[gl_InstanceID];
|
||||
gl_Position = vec4(position + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
gl_Position = vec4(aPos + offset, 0.0, 1.0);
|
||||
fColor = aColor;
|
||||
}
|
||||
```
|
||||
|
||||
这里我们定义了一个uniform数组,叫`offsets`,它包含100个偏移量向量。在顶点着色器里,我们接收一个对应着当前实例的偏移量,这是通过使用 `gl_InstanceID`来索引offsets得到的。如果我们使用实例化绘制100个四边形,使用这个顶点着色器,我们就能得到100位于不同位置的四边形。
|
||||
这里我们定义了一个叫做<var>offsets</var>的数组,它包含100个偏移向量。在顶点着色器中,我们会使用<var>gl_InstanceID</var>来索引<var>offsets</var>数组,获取每个实例的偏移向量。如果我们要实例化绘制100个四边形,仅使用这个顶点着色器我们就能得到100个位于不同位置的四边形。
|
||||
|
||||
我们一定要设置偏移位置,在游戏循环之前我们用一个嵌套for循环计算出它来:
|
||||
当前,我们仍要设置这些偏移位置,我们会在进入渲染循环之前使用一个嵌套for循环计算:
|
||||
|
||||
```c++
|
||||
glm::vec2 translations[100];
|
||||
int index = 0;
|
||||
GLfloat offset = 0.1f;
|
||||
for(GLint y = -10; y < 10; y += 2)
|
||||
float offset = 0.1f;
|
||||
for(int y = -10; y < 10; y += 2)
|
||||
{
|
||||
for(GLint x = -10; x < 10; x += 2)
|
||||
for(int x = -10; x < 10; x += 2)
|
||||
{
|
||||
glm::vec2 translation;
|
||||
translation.x = (GLfloat)x / 10.0f + offset;
|
||||
translation.y = (GLfloat)y / 10.0f + offset;
|
||||
translation.x = (float)x / 10.0f + offset;
|
||||
translation.y = (float)y / 10.0f + offset;
|
||||
translations[index++] = translation;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里我们创建100个平移向量,它包含着10×10格子所有位置。除了生成`translations`数组外,我们还需要把这些位移数据发送到顶点着色器的uniform数组:
|
||||
这里,我们创建100个位移向量,表示10x10网格上的所有位置。除了生成<var>translations</var>数组之外,我们还需要将数据转移到顶点着色器的uniform数组中:
|
||||
|
||||
```c++
|
||||
shader.Use();
|
||||
for(GLuint i = 0; i < 100; i++)
|
||||
shader.use();
|
||||
for(unsigned int i = 0; i < 100; i++)
|
||||
{
|
||||
stringstream ss;
|
||||
string index;
|
||||
ss << i;
|
||||
index = ss.str();
|
||||
GLint location = glGetUniformLocation(shader.Program, ("offsets[" + index + "]").c_str())
|
||||
glUniform2f(location, translations[i].x, translations[i].y);
|
||||
shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
|
||||
}
|
||||
```
|
||||
|
||||
这一小段代码中,我们将for循环计数器i变为string,接着就能动态创建一个为请求的uniform的`location`创建一个`location`字符串。将offsets数组中的每个条目,我们都设置为相应的平移向量。
|
||||
在这一段代码中,我们将for循环的计数器<var>i</var>转换为一个<fun>string</fun>,我们可以用它来动态创建位置值的字符串,用于uniform位置值的索引。接下来,我们会对<var>offsets</var> uniform数组中的每一项设置对应的位移向量。
|
||||
|
||||
现在所有的准备工作都结束了,我们可以开始渲染四边形了。用实例化渲染来绘制四边形,我们需要调用`glDrawArraysInstanced`或`glDrawElementsInstanced`,由于我们使用的不是索引绘制缓冲,所以我们用的是`glDrawArrays`对应的那个版本`glDrawArraysInstanced`:
|
||||
现在所有的准备工作都做完了,我们可以开始渲染四边形了。对于实例化渲染,我们使用<fun>glDrawArraysInstanced</fun>或<fun>glDrawElementsInstanced</fun>。因为我们使用的不是索引缓冲,我们会调用<fun>glDrawArrays</fun>版本的函数:
|
||||
|
||||
```c++
|
||||
glBindVertexArray(quadVAO);
|
||||
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
|
||||
glBindVertexArray(0);
|
||||
```
|
||||
|
||||
`glDrawArraysInstanced`的参数和`glDrawArrays`一样,除了最后一个参数设置了我们打算绘制实例的数量。我们想展示100个四边形,它们以10×10网格形式展现,所以这儿就是100.运行代码,你会得到100个相似的有色四边形。
|
||||
<fun>glDrawArraysInstanced</fun>的参数和<fun>glDrawArrays</fun>完全一样,除了最后多了个参数用来设置需要绘制的实例数量。因为我们想要在10x10网格中显示100个四边形,我们将它设置为100.运行代码之后,你应该能得到熟悉的100个五彩的四边形。
|
||||
|
||||
## 实例化数组
|
||||
|
||||
在这种特定条件下,前面的实现很好,但是当我们有100个实例的时候(这很正常),最终我们将碰到uniform数据数量的上线。为避免这个问题另一个可替代方案是实例化数组(Instanced Array),它使用顶点属性来定义,这样就允许我们使用更多的数据了,当顶点着色器渲染一个新实例时它才会被更新。
|
||||
虽然之前的实现在目前的情况下能够正常工作,但是如果我们要渲染远超过100个实例的时候(这其实非常普遍),我们最终会超过最大能够发送至着色器的uniform数据大小[上限](http://www.opengl.org/wiki/Uniform_(GLSL)#Implementation_limits)。它的一个代替方案是<def>实例化数组</def>(Instanced Array),它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。
|
||||
|
||||
使用顶点属性,每次运行顶点着色器都将让GLSL获取到下个顶点属性集合,它们属于当前顶点。当把顶点属性定义为实例数组时,顶点着色器只更新每个实例的顶点属性的内容而不是顶点的内容。这使我们在每个顶点数据上使用标准顶点属性,用实例数组来储存唯一的实例数据。
|
||||
使用顶点属性时,顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,而不是每个顶点,更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性,而对逐实例的数据使用实例化数组。
|
||||
|
||||
为了展示一个实例化数组的例子,我们将采用前面的例子,把偏移uniform表示为一个实例数组。我们不得不增加另一个顶点属性,来更新顶点着色器。
|
||||
为了给你一个实例化数组的例子,我们将使用之前的例子,并将偏移量uniform数组设置为一个实例化数组。我们需要在顶点着色器中再添加一个顶点属性:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
layout (location = 1) in vec3 color;
|
||||
layout (location = 2) in vec2 offset;
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec3 aColor;
|
||||
layout (location = 2) in vec2 aOffset;
|
||||
|
||||
out vec3 fColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(position + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
|
||||
fColor = aColor;
|
||||
}
|
||||
```
|
||||
|
||||
我们不再使用`gl_InstanceID`,可以直接用`offset`属性,不用先在一个大uniform数组里进行索引。
|
||||
我们不再使用<var>gl_InstanceID</var>,现在不需要索引一个uniform数组就能够直接使用<var>offset</var>属性了。
|
||||
|
||||
因为一个实例化数组实际上就是一个和位置和颜色一样的顶点属性,我们还需要把它的内容储存为一个顶点缓冲对象里,并把它配置为一个属性指针。我们首先将平移变换数组贮存到一个新的缓冲对象上:
|
||||
因为实例化数组和<var>position</var>与<var>color</var>变量一样,都是顶点属性,我们还需要将它的内容存在顶点缓冲对象中,并且配置它的属性指针。我们首先将(上一部分的)<var>translations</var>数组存到一个新的缓冲对象中:
|
||||
|
||||
```c++
|
||||
GLuint instanceVBO;
|
||||
unsigned int instanceVBO;
|
||||
glGenBuffers(1, &instanceVBO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
```
|
||||
|
||||
之后我们还需要设置它的顶点属性指针,并开启顶点属性:
|
||||
之后我们还需要设置它的顶点属性指针,并启用顶点属性:
|
||||
|
||||
```c++
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glVertexAttribDivisor(2, 1);
|
||||
```
|
||||
|
||||
代码中有意思的地方是,最后一行,我们调用了 **`glVertexAttribDivisor`**。这个函数告诉OpenGL什么时候去更新顶点属性的内容到下个元素。它的第一个参数是提到的顶点属性,第二个参数是属性除数(attribute divisor)。默认属性除数是0,告诉OpenGL在顶点着色器的每次迭代更新顶点属性的内容。把这个属性设置为1,我们就是告诉OpenGL我们打算在开始渲染一个新的实例的时候更新顶点属性的内容。设置为2代表我们每2个实例更新内容,依此类推。把属性除数设置为1,我们可以高效地告诉OpenGL,location是2的顶点属性是一个实例数组(instanced array)。
|
||||
这段代码很有意思的地方在于最后一行,我们调用了<fun>glVertexAttribDivisor</fun>。这个函数告诉了OpenGL该**什么时候**更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute Divisor)。默认情况下,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时,我们希望每2个实例更新一次属性,以此类推。我们将属性除数设置为1,是在告诉OpenGL,处于位置值2的顶点属性是一个实例化数组。
|
||||
|
||||
如果我们现在再次使用`glDrawArraysInstanced`渲染四边形,我们会得到下面的输出:
|
||||
如果我们现在使用<fun>glDrawArraysInstanced</fun>,再次渲染四边形,会得到以下输出:
|
||||
|
||||

|
||||
|
||||
和前面的一样,但这次是使用实例数组实现的,它使我们为绘制实例向顶点着色器传递更多的数据(内存允许我们存多少就能存多少)。
|
||||
这和之前的例子是完全一样的,但这次是使用实例化数组实现的,这让我们能够传递更多的数据到顶点着色器(只要内存允许)来用于实例化绘制。
|
||||
|
||||
你还可以使`用gl_InstanceID`从右上向左下缩小每个四边形。
|
||||
为了更有趣一点,我们也可以使用<var>gl_InstanceID</var>,从右上到左下逐渐缩小四边形:
|
||||
|
||||
```c++
|
||||
void main()
|
||||
{
|
||||
vec2 pos = position * (gl_InstanceID / 100.0f);
|
||||
gl_Position = vec4(pos + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
vec2 pos = aPos * (gl_InstanceID / 100.0);
|
||||
gl_Position = vec4(pos + aOffset, 0.0, 1.0);
|
||||
fColor = aColor;
|
||||
}
|
||||
```
|
||||
|
||||
结果是第一个实例的四边形被绘制的非常小,随着绘制实例的增加,`gl_InstanceID`越来越接近100,这样更多的四边形会更接近它们原来的大小。这是一种很好的将`gl_InstanceID`与实例数组结合使用的法则:
|
||||
结果就是,第一个四边形的实例会非常小,随着绘制实例的增加,<var>gl_InstanceID</var>会越来越接近100,四边形也就越来越接近原始大小。像这样将实例化数组与<var>gl_InstanceID</var>结合使用是完全可行的。
|
||||
|
||||

|
||||
|
||||
如果你仍然不确定实例渲染如何工作,或者想看看上面的代码是如何组合起来的,你可以在[这里找到应用的源码](http://learnopengl.com/code_viewer.php?code=advanced/instancing_quads)。
|
||||
如果你还是不确定实例化渲染是如何工作的,或者想看看所有代码是如何组合起来的,你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.1.instancing_quads/instancing_quads.cpp)找到程序的源代码。
|
||||
|
||||
虽然很有趣,但是这些例子并不是实例化的好例子。是的,它们的确让你知道实例化是怎么工作的,但是我们还没接触到它最有用的一点,绘制巨大数量的相似物体。出于这个原因,我们将会在下一部分进入太空探险,见识实例化渲染真正的威力。
|
||||
|
||||
这些例子不是实例的好例子,不过挺有意思的。它们可以让你对实例的工作方式有一个概括的理解,但是当绘制拥有极大数量的相同物体的时候,它极其有用,现在我们还没有展示呢。出于这个原因,我们将在接下来的部分进入太空来看看实例渲染的威力。
|
||||
|
||||
# 小行星带
|
||||
|
||||
想象一下,在一个场景中有一个很大的行星,行星周围有一圈小行星带。这样一个小行星大可能包含成千上万的石块,对于大多数显卡来说几乎是难以完成的渲染任务。这个场景对于实例渲染来说却不再话下,由于所有小行星可以使用一个模型来表示。每个小行星使用一个变换矩阵就是一个经过少量变化的独一无二的小行星了。
|
||||
想象这样一个场景,在宇宙中有一个大的行星,它位于小行星带的中央。这样的小行星带可能包含成千上万的岩块,在很不错的显卡上也很难完成这样的渲染。实例化渲染正是适用于这样的场景,因为所有的小行星都可以使用一个模型来表示。每个小行星可以再使用不同的变换矩阵来进行少许的变化。
|
||||
|
||||
为了展示实例渲染的影响,我们先不使用实例渲染,来渲染一个小行星围绕行星飞行的场景。这个场景的大天体可以[从这里下载](http://learnopengl.com/data/models/planet.rar),此外要把小行星放在合适的位置上。小行星可以[从这里下载](http://learnopengl.com/data/models/rock.rar)。
|
||||
为了展示实例化渲染的作用,我们首先会**不使用**实例化渲染,来渲染小行星绕着行星飞行的场景。这个场景将会包含一个大的行星模型,它可以在[这里](../data/planet.rar)下载,以及很多环绕着行星的小行星。小行星的岩石模型可以在[这里](../data/rock.rar)下载。
|
||||
|
||||
为了得到我们理想中的效果,我们将为每个小行星生成一个变换矩阵,作为它们的模型矩阵。变换矩阵先将小行星平移到行星带上,我们还要添加一个随机位移值来作为偏移量,这样才能使行星带更自然。接着我们应用一个随机缩放,以及一个随机旋转向量。最后,变换矩阵就会将小行星变换到行星的周围,同时使它们更自然,每个行星都有别于其他的。
|
||||
在代码例子中,我们将使用在[模型加载](../03 Model Loading/01 Assimp.md)小节中定义的模型加载器来加载模型。
|
||||
|
||||
为了得到想要的效果,我们将会为每个小行星生成一个变换矩阵,用作它们的模型矩阵。变换矩阵首先将小行星位移到小行星带中的某处,我们还会加一个小的随机偏移值到这个偏移量上,让这个圆环看起来更自然一点。接下来,我们应用一个随机的缩放,并且以一个旋转向量为轴进行一个随机的旋转。最终的变换矩阵不仅能将小行星变换到行星的周围,而且会让它看起来更自然,与其它小行星不同。最终的结果是一个布满小行星的圆环,其中每一个小行星都与众不同。
|
||||
|
||||
```c++
|
||||
GLuint amount = 1000;
|
||||
glm::mat4* modelMatrices;
|
||||
unsigned int amount = 1000;
|
||||
glm::mat4 *modelMatrices;
|
||||
modelMatrices = new glm::mat4[amount];
|
||||
srand(glfwGetTime()); // initialize random seed
|
||||
GLfloat radius = 50.0;
|
||||
GLfloat offset = 2.5f;
|
||||
for(GLuint i = 0; i < amount; i++)
|
||||
srand(glfwGetTime()); // 初始化随机种子
|
||||
float radius = 50.0;
|
||||
float offset = 2.5f;
|
||||
for(unsigned int i = 0; i < amount; i++)
|
||||
{
|
||||
glm::mat4 model;
|
||||
// 1. Translation: displace along circle with 'radius' in range [-offset, offset]
|
||||
GLfloat angle = (GLfloat)i / (GLfloat)amount * 360.0f;
|
||||
GLfloat displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat x = sin(angle) * radius + displacement;
|
||||
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat y = displacement * 0.4f; // y value has smaller displacement
|
||||
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat z = cos(angle) * radius + displacement;
|
||||
// 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
|
||||
float angle = (float)i / (float)amount * 360.0f;
|
||||
float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
|
||||
float x = sin(angle) * radius + displacement;
|
||||
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
|
||||
float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小
|
||||
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
|
||||
float z = cos(angle) * radius + displacement;
|
||||
model = glm::translate(model, glm::vec3(x, y, z));
|
||||
// 2. Scale: Scale between 0.05 and 0.25f
|
||||
GLfloat scale = (rand() % 20) / 100.0f + 0.05;
|
||||
|
||||
// 2. 缩放:在 0.05 和 0.25f 之间缩放
|
||||
float scale = (rand() % 20) / 100.0f + 0.05;
|
||||
model = glm::scale(model, glm::vec3(scale));
|
||||
// 3. Rotation: add random rotation around a (semi)randomly picked rotation axis vector
|
||||
GLfloat rotAngle = (rand() % 360);
|
||||
|
||||
// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
|
||||
float rotAngle = (rand() % 360);
|
||||
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
|
||||
// 4. Now add to list of matrices
|
||||
|
||||
// 4. 添加到矩阵的数组中
|
||||
modelMatrices[i] = model;
|
||||
}
|
||||
```
|
||||
|
||||
这段代码看起来还是有点吓人,但我们基本上是沿着一个半径为radius的圆圈变换小行星的x和y的值,让每个小行星在-offset和offset之间随机生成一个位置。我们让y变化的更小,这让这个环带就会成为扁平的。接着我们缩放和旋转变换,把结果储存到一个modelMatrices矩阵里,它的大小是amount。这里我们生成1000个模型矩阵,每个小行星一个。
|
||||
这段代码看起来可能有点吓人,但我们只是将小行星的`x`和`z`位置变换到了一个半径为<var>radius</var>的圆形上,并且在半径的基础上偏移了<var>-offset</var>到<var>offset</var>。我们让`y`偏移的影响更小一点,让小行星带更扁平一点。接下来,我们应用了缩放和旋转变换,并将最终的变换矩阵储存在<var>modelMatrices</var>中,这个数组的大小是<var>amount</var>。这里,我们一共生成1000个模型矩阵,每个小行星一个。
|
||||
|
||||
加载完天体和小行星模型后,编译着色器,渲染代码是这样的:
|
||||
在加载完行星和岩石模型,并编译完着色器之后,渲染的代码看起来是这样的:
|
||||
|
||||
```c++
|
||||
// 绘制行星
|
||||
shader.Use();
|
||||
shader.use();
|
||||
glm::mat4 model;
|
||||
model = glm::translate(model, glm::vec3(0.0f, -5.0f, 0.0f));
|
||||
model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
|
||||
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||
shader.setMat4("model", model);
|
||||
planet.Draw(shader);
|
||||
|
||||
// 绘制石头
|
||||
for(GLuint i = 0; i < amount; i++)
|
||||
// 绘制小行星
|
||||
for(unsigned int i = 0; i < amount; i++)
|
||||
{
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrices[i]));
|
||||
shader.setMat4("model", modelMatrices[i]);
|
||||
rock.Draw(shader);
|
||||
}
|
||||
```
|
||||
|
||||
我们先绘制天体模型,要把它平移和缩放一点以适应场景,接着,我们绘制amount数量的小行星,它们按照我们所计算的结果进行变换。在我们绘制每个小行星之前,我们还得先在着色器中设置相应的模型变换矩阵。
|
||||
我们首先绘制了行星的模型,并对它进行位移和缩放,以适应场景,接下来,我们绘制<var>amount</var>数量的岩石模型。在绘制每个岩石之前,我们首先需要在着色器内设置对应的模型变换矩阵。
|
||||
|
||||
结果是一个太空样子的场景,我们可以看到有一个自然的小行星带:
|
||||
最终的结果是一个看起来像是太空的场景,环绕着行星的是看起来很自然的小行星带:
|
||||
|
||||

|
||||
|
||||
这个场景包含1001次渲染函数调用,每帧渲染1000个小行星模型。你可以在这里找到[场景的源码](http://learnopengl.com/code_viewer.php?code=advanced/instancing_asteroids_normal),以及[顶点](http://learnopengl.com/code_viewer.php?code=advanced/instancing&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced/instancing&type=fragment)着色器。
|
||||
这个场景每帧包含1001次渲染调用,其中1000个是岩石模型。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.2.asteroids/asteroids.cpp)找到源代码。
|
||||
|
||||
当我们开始增加数量的时候,很快就会注意到帧数的下降,而且下降的厉害。当我们设置为2000的时候,场景慢得已经难以移动了。
|
||||
当我们开始增加这个数字的时候,你很快就会发现场景不再能够流畅运行了,帧数也下降很厉害。当我们将<var>amount</var>设置为2000的时候,场景就已经慢到移动都很困难的程度了。
|
||||
|
||||
我们再次使用实例渲染来渲染同样的场景。我们先得对顶点着色器进行一点修改:
|
||||
现在,我们来尝试使用实例化渲染来渲染相同的场景。我们首先对顶点着色器进行一点修改:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 position;
|
||||
layout (location = 2) in vec2 texCoords;
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 2) in vec2 aTexCoords;
|
||||
layout (location = 3) in mat4 instanceMatrix;
|
||||
|
||||
out vec2 TexCoords;
|
||||
@@ -283,35 +289,36 @@ uniform mat4 view;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection * view * instanceMatrix * vec4(position, 1.0f);
|
||||
TexCoords = texCoords;
|
||||
gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0);
|
||||
TexCoords = aTexCoords;
|
||||
}
|
||||
```
|
||||
|
||||
我们不再使用模型uniform变量,取而代之的是把一个mat4的顶点属性,送一我们可以将变换矩阵储存为一个实例数组(instanced array)。然而,当我们声明一个数据类型为顶点属性的时候,它比一个vec4更大,是有些不同的。顶点属性被允许的最大数据量和vec4相等。因为一个mat4大致和4个vec4相等,我们为特定的矩阵必须保留4个顶点属性。因为我们将它的位置赋值为3个列的矩阵,顶点属性的位置就会是3、4、5和6。
|
||||
我们不再使用模型uniform变量,改为一个<fun>mat4</fun>的顶点属性,让我们能够存储一个实例化数组的变换矩阵。然而,当我们顶点属性的类型大于<fun>vec4</fun>时,就要多进行一步处理了。顶点属性最大允许的数据大小等于一个<fun>vec4</fun>。因为一个<fun>mat4</fun>本质上是4个<fun>vec4</fun>,我们需要为这个矩阵预留4个顶点属性。因为我们将它的位置值设置为3,矩阵每一列的顶点属性位置值就是3、4、5和6。
|
||||
|
||||
然后我们必须为这4个顶点属性设置属性指针,并将其配置为实例数组:
|
||||
接下来,我们需要为这4个顶点属性设置属性指针,并将它们设置为实例化数组:
|
||||
|
||||
```c++
|
||||
for(GLuint i = 0; i < rock.meshes.size(); i++)
|
||||
// 顶点缓冲对象
|
||||
unsigned int buffer;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
|
||||
|
||||
for(unsigned int i = 0; i < rock.meshes.size(); i++)
|
||||
{
|
||||
GLuint VAO = rock.meshes[i].VAO;
|
||||
// Vertex Buffer Object
|
||||
GLuint buffer;
|
||||
unsigned int VAO = rock.meshes[i].VAO;
|
||||
glBindVertexArray(VAO);
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
|
||||
// Vertex Attributes
|
||||
// 顶点属性
|
||||
GLsizei vec4Size = sizeof(glm::vec4);
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)0);
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
|
||||
glEnableVertexAttribArray(4);
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(vec4Size));
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
|
||||
glEnableVertexAttribArray(5);
|
||||
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(2 * vec4Size));
|
||||
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
|
||||
glEnableVertexAttribArray(6);
|
||||
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(3 * vec4Size));
|
||||
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));
|
||||
|
||||
glVertexAttribDivisor(3, 1);
|
||||
glVertexAttribDivisor(4, 1);
|
||||
@@ -322,31 +329,30 @@ for(GLuint i = 0; i < rock.meshes.size(); i++)
|
||||
}
|
||||
```
|
||||
|
||||
要注意的是我们将Mesh的VAO变量声明为一个public(公有)变量,而不是一个private(私有)变量,所以我们可以获取它的顶点数组对象。这不是最干净的方案,但这能较好的适应本教程。若没有这点hack,代码就干净了。我们声明了OpenGL该如何为每个矩阵的顶点属性的缓冲进行解释,每个顶点属性都是一个实例数组。
|
||||
注意这里我们将<fun>Mesh</fun>的<var>VAO</var>从私有变量改为了公有变量,让我们能够访问它的顶点数组对象。这并不是最好的解决方案,只是为了配合本小节的一个简单的改动。除此之外代码就应该很清楚了。我们告诉了OpenGL应该如何解释每个缓冲顶点属性的缓冲,并且告诉它这些顶点属性是实例化数组。
|
||||
|
||||
下一步我们再次获得网格的VAO,这次使用`glDrawElementsInstanced`进行绘制:
|
||||
接下来,我们再次使用网格的<var>VAO</var>,这一次使用<fun>glDrawElementsInstanced</fun>进行绘制:
|
||||
|
||||
```c++
|
||||
// Draw meteorites
|
||||
instanceShader.Use();
|
||||
for(GLuint i = 0; i < rock.meshes.size(); i++)
|
||||
// 绘制小行星
|
||||
instanceShader.use();
|
||||
for(unsigned int i = 0; i < rock.meshes.size(); i++)
|
||||
{
|
||||
glBindVertexArray(rock.meshes[i].VAO);
|
||||
glDrawElementsInstanced(
|
||||
GL_TRIANGLES, rock.meshes[i].vertices.size(), GL_UNSIGNED_INT, 0, amount
|
||||
GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
|
||||
);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
```
|
||||
|
||||
这里我们绘制和前面的例子里一样数量(amount)的小行星,只不过是使用的实例渲染。结果是相似的,但你会看在开始增加数量以后效果的不同。不实例渲染,我们可以流畅渲染1000到1500个小行星。而使用了实例渲染,我们可以设置为100000,每个模型由576个顶点,这几乎有5千7百万个顶点,而且帧率没有丝毫下降!
|
||||
这里,我们绘制与之前相同数量<var>amount</var>的小行星,但是使用的是实例渲染。结果应该是非常相似的,但如果你开始增加<var>amount</var>变量,你就能看见实例化渲染的效果了。没有实例化渲染的时候,我们只能流畅渲染1000到1500个小行星。而使用了实例化渲染之后,我们可以将这个值设置为100000,每个岩石模型有576个顶点,每帧加起来大概要绘制5700万个顶点,但性能却没有受到任何影响!
|
||||
|
||||

|
||||
|
||||
上图渲染了十万小行星,半径为150.0f,偏移等于25.0f。你可以在这里找到这个演示实例渲染的[源码](http://learnopengl.com/code_viewer.php?code=advanced/instancing_asteroids_instanced)。
|
||||
上面这幅图渲染了10万个小行星,半径为`150.0f`,偏移量等于`25.0f`。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/asteroids_instanced.cpp)找到实例化渲染的代码。
|
||||
|
||||
!!! Important
|
||||
|
||||
有些机器渲染十万可能会有点吃力,所以尝试修改这个数量知道你能获得可以接受的帧率。
|
||||
在某些机器上,10个小行星可能会太多了,所以尝试修改这个值,直到达到一个你能接受的帧率。
|
||||
|
||||
就像你所看到的,在合适的条件下,实例渲染对于你的显卡来说和普通渲染有很大不同。处于这个理由,实例渲染通常用来渲染草、草丛、粒子以及像这样的场景,基本上来讲只要场景中有很多重复物体,使用实例渲染都会获得好处。
|
||||
可以看到,在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,以及上面这样的场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。
|
BIN
docs/data/planet.rar
Normal file
BIN
docs/data/planet.rar
Normal file
Binary file not shown.
BIN
docs/data/rock.rar
Normal file
BIN
docs/data/rock.rar
Normal file
Binary file not shown.
@@ -406,6 +406,15 @@
|
||||
- Implode:向内爆炸
|
||||
- Fur:毛发
|
||||
|
||||
## 04-10
|
||||
|
||||
- CPU to GPU Bus:CPU到GPU总线
|
||||
- Instance:实例
|
||||
- Instancing:实例化
|
||||
- Instance Count:实例数量
|
||||
- Instanced Array:实例化数组
|
||||
- Attribute Divisor:属性除数
|
||||
|
||||
## 06-01
|
||||
|
||||
- Debugging:调试
|
||||
|
Reference in New Issue
Block a user