本文同時(shí)發(fā)布在我的個(gè)人博客上:https://dragon_boy.gitee.io
實(shí)例化
??想一想某個(gè)游戲場(chǎng)景或動(dòng)畫場(chǎng)景呻顽,我們總會(huì)法線某些模型是重復(fù)的,也就是使用了同樣的頂點(diǎn)數(shù)據(jù)晤碘,只不過有了不同的世界空間變換层扶。比如草地果善,我們可能想通過一個(gè)簡(jiǎn)單的葉子模型進(jìn)行重復(fù)繪制來形成草地,一株草可能只包含幾個(gè)三角形美侦,但整個(gè)草地的三角形數(shù)量是巨大的产舞。
??如果真的逐個(gè)繪制重復(fù)的模型,我們大概會(huì)在渲染循環(huán)中這么實(shí)現(xiàn):
for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
DoSomePreparations(); // 綁定VAO菠剩,綁定紋理庞瘸,設(shè)置uniform之類的。
glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}
??由于對(duì)繪制命令的大量調(diào)用赠叼,我們很容易到達(dá)性能的瓶頸擦囊,因?yàn)槭褂妙愃苂lDrawArrays或glDrawElements的命令會(huì)消耗大量的性能,OpenGL必須在繪制前做一些必要的準(zhǔn)備(比如告知GPU從哪片緩沖區(qū)讀取數(shù)據(jù)嘴办,在哪里找到頂點(diǎn)屬性瞬场,這些操作都會(huì)在CPU和GPU之間進(jìn)行)。所以說即使渲染出圖像很快涧郊,但給GPU這些命令卻不是那么快贯被。
??但如果我們只向GPU運(yùn)送一次數(shù)據(jù),并使用一個(gè)渲染命令來繪制大量重復(fù)的物體妆艘,這將會(huì)更加有效率彤灶。而這種方法就是實(shí)例化。
??實(shí)例化在渲染大量重復(fù)物體時(shí)非常有用批旺,只需要使用一次渲染命令幌陕,節(jié)省了大量CPU與GPU之間的通信次數(shù)。使用實(shí)例化繪制物體我們使用glDrawArraysInstanced和glDrawElementsInstanced命令汽煮。這兩個(gè)命令需要額外的實(shí)例化數(shù)量參數(shù)來表明我們想繪制的重復(fù)物體的數(shù)量搏熄。我們將需要的數(shù)據(jù)送往GPU一次棚唆,然后告知GPU如何繪制這些實(shí)例化物體,接著GPU就可以大量繪制這些實(shí)例化物體了心例。
??這個(gè)方法本身由點(diǎn)局限性宵凌,因?yàn)樗欣L制的物體都和要實(shí)例化的物體一致,包括位置止后、紋理等一切信息瞎惫。也就是說,我們只能在屏幕上看到一個(gè)物體顯示译株。當(dāng)然瓜喇,考慮到這一點(diǎn),GLSL在頂點(diǎn)著色器中提供了一個(gè)內(nèi)建變量gl_InstanceID古戴。
??每渲染一個(gè)實(shí)例物體欠橘,gl_InstanceID就會(huì)從0開始自增1矩肩。如果我們渲染了43個(gè)實(shí)例物體现恼,那么gl_InstanceID=42。這樣黍檩,我們就可以根據(jù)這個(gè)值為每一個(gè)實(shí)例物體分配一些特定的屬性叉袍,比如改變每個(gè)實(shí)例物體的位置。
??比如下面我們繪制了100個(gè)相同的2d平面刽酱,我們喳逛,給每個(gè)平面一點(diǎn)偏移量,就可以得到下面的結(jié)果:
??每個(gè)四邊形包含2個(gè)三角形即6個(gè)頂點(diǎn)棵里。每個(gè)頂點(diǎn)包含1個(gè)2為標(biāo)準(zhǔn)坐標(biāo)和一個(gè)顏色向量润文。為了保證100個(gè)三角形能鋪滿屏幕,我們讓平面的大小盡可能地械盍:
float quadVertices[] = {
// positions // colors
-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,
-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, 1.0f, 1.0f
};
??平面的片元著色器很簡(jiǎn)單典蝌,輸出顏色使用我們?cè)O(shè)定的頂點(diǎn)顏色:
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}
??在頂點(diǎn)著色器中我們?cè)O(shè)置了一些變化:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out vec3 fColor;
uniform vec2 offsets[100];
void main()
{
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos + offset, 0.0, 1.0);
fColor = aColor;
}
??我們定義了一個(gè)100大小的uniform偏移數(shù)組,按順序(gl_InstanceID)對(duì)每個(gè)平面進(jìn)行偏移头谜。
??接著我們需要為uniform偏移數(shù)組賦值骏掀。我們?cè)谶M(jìn)入渲染循環(huán)前線定義一下一個(gè)偏移數(shù)組:
glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{
for(int x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (float)x / 10.0f + offset;
translation.y = (float)y / 10.0f + offset;
translations[index++] = translation;
}
}
??接著將數(shù)據(jù)傳入uniform偏移數(shù)組:
shader.use();
for(unsigned int i = 0; i < 100; i++)
{
shader.setVec2(("offsets[" + std::to_string(i) + "]")), translations[i]);
}
??最后,在渲染循環(huán)中柱告,我們繪制這100個(gè)平面:
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
實(shí)例化數(shù)組
??上述的例子非常有用截驮,但平常我們繪制實(shí)例化物體總會(huì)超過100個(gè),而這就表明我們會(huì)超過uniform數(shù)據(jù)數(shù)量的限制际度。另一個(gè)替代的方法為實(shí)例化數(shù)組葵袭。實(shí)例化數(shù)字被定義為一種頂點(diǎn)屬性,允許我們存儲(chǔ)更多的數(shù)據(jù)乖菱,并且數(shù)據(jù)是每個(gè)平面進(jìn)行更新而非每個(gè)頂點(diǎn)進(jìn)行更新眶熬。
??如果我們定義了新的頂點(diǎn)屬性妹笆,即實(shí)例化數(shù)組,我們需要在頂點(diǎn)著色器開頭聲明:
#version 330 core
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(aPos + aOffset, 0.0, 1.0);
fColor = aColor;
}
??由于實(shí)例化數(shù)組是一個(gè)頂點(diǎn)屬性娜氏,我們需要存儲(chǔ)在一個(gè)頂點(diǎn)緩沖對(duì)象中拳缠,并設(shè)置相應(yīng)的頂點(diǎn)屬性指針。首先將我們定義的偏移數(shù)組傳入一個(gè)新的VBO:
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);
??接著設(shè)置頂點(diǎn)屬性指針:
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
??注意最后贸弥,我們使用了glVertexAttribDivisor窟坐。這個(gè)方法告知何時(shí)更新頂點(diǎn)屬性(不是每個(gè)頂點(diǎn)都更新)。第一個(gè)參數(shù)代表頂點(diǎn)屬性的位置绵疲,第二個(gè)參數(shù)是屬性除數(shù)哲鸳。默認(rèn)情況下,屬性除數(shù)為0盔憨,表明每個(gè)頂點(diǎn)都會(huì)更新數(shù)據(jù)徙菠。這里使用1表明我們想在繪制一個(gè)新的實(shí)例時(shí)更新數(shù)據(jù)。如果設(shè)為2則是每繪制兩個(gè)實(shí)例更新數(shù)據(jù)郁岩,以此類推婿奔。通過將屬性除數(shù)設(shè)為1我們告知OpenGL在2位置的頂點(diǎn)屬性是實(shí)例化數(shù)組。
??其它不變问慎,結(jié)果如下:
??可以看到結(jié)果和第一種方法一致萍摊,但使用實(shí)例化數(shù)組允許我們存入更多的數(shù)據(jù),繪制更多的實(shí)例物體如叼。
??我們可以使用內(nèi)建的gl_InstanceID來逐行縮小平面玩一玩:
void main()
{
vec2 pos = aPos * (gl_InstanceID / 100.0);
gl_Position = vec4(pos + aOffset, 0.0, 1.0);
fColor = aColor;
}
??運(yùn)行結(jié)果如下:
??這里給出原文參考代碼:Code冰木。
小行星帶
??想象一個(gè)場(chǎng)景,中心有一個(gè)天體笼恰,又一圈大的小行星帶環(huán)繞踊沸。這樣一圈小行星帶包含成千上萬的巖石,如果就這么直接渲染的話社证,最好的顯卡也招架不住逼龟。我們可以使用實(shí)例化技術(shù)來實(shí)現(xiàn)這一看似不可能的渲染。行星帶的每塊巖石都可以看作是一塊源石進(jìn)行一些不同的變換得到的猴仑。
??為了體現(xiàn)實(shí)例化渲染的優(yōu)勢(shì)审轮,我們首先直接渲染模型。這里給出天體模型和行星帶巖石模型:天體辽俗,行星帶疾渣。
??我們?yōu)槊總€(gè)小行星帶的巖石設(shè)置model變換矩陣。我們首先根據(jù)一個(gè)圓形的半徑進(jìn)行不同的半徑替換(在原始半徑的基礎(chǔ)上在偏移范圍內(nèi)添加添加擾動(dòng))崖飘,得到位置榴捡,接著隨機(jī)縮放和旋轉(zhuǎn)一下:
unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // initialize random seed
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{
glm::mat4 model = glm::mat4(1.0f);
// 1. translation: displace along circle with 'radius' in range [-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; // keep height of field smaller compared to width of x and 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
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
float rotAngle = (rand() % 360);
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
// 4. now add to list of matrices
modelMatrices[i] = model;
}
??在加載天體和巖石模型以及設(shè)定一系列著色器后,渲染代碼長這樣:
// draw planet
shader.use();
glm::mat4 model = glm::mat4(1.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));
shader.setMat4("model", model);
planet.Draw(shader);
// draw meteorites
for(unsigned int i = 0; i < amount; i++)
{
shader.setMat4("model", modelMatrices[i]);
rock.Draw(shader);
}
??運(yùn)行結(jié)果如下:
??整個(gè)場(chǎng)景每幀包含1001次渲染命令調(diào)用朱浴,只是為了繪制1000個(gè)巖石吊圾。這里給出原文代碼參考:Code达椰。
??很明顯,這太浪費(fèi)資源了项乒,而且如果不斷增加巖石的數(shù)量啰劲,很快就會(huì)到達(dá)性能的瓶頸。
??接下來我們使用實(shí)例化來渲染整個(gè)場(chǎng)景檀何。首先調(diào)整頂點(diǎn)著色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;
out vec2 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0);
TexCoords = aTexCoords;
}
??我們直接將model矩陣定義為頂點(diǎn)屬性蝇裤。矩陣這里的大小為mat4,即4*vec4频鉴,我們的頂點(diǎn)屬性的數(shù)據(jù)最大大小為vec4栓辜,所以即使我們我們將位置定義為3,我們還需要使用4垛孔、5藕甩、6的位置來存儲(chǔ)矩陣數(shù)據(jù)。
??我們?cè)O(shè)置每個(gè)屬性指針并配置實(shí)例化數(shù)組:
// vertex buffer object
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++)
{
unsigned int VAO = rock.meshes[i].VAO;
glBindVertexArray(VAO);
// vertex attributes
std::size_t vec4Size = sizeof(glm::vec4);
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(1 * vec4Size));
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glVertexAttribDivisor(6, 1);
glBindVertexArray(0);
}
??接著我們使用glDrawElementsInstanced繪制巖石:
// draw meteorites
instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
glBindVertexArray(rock.meshes[i].VAO);
glDrawElementsInstanced(
GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
);
}
??為了體現(xiàn)實(shí)例化的優(yōu)勢(shì)周荐,我們將繪制數(shù)量調(diào)整為10萬狭莱,結(jié)果如下:
??這里給出原文代碼參考:Code。
??最后羡藐,貼出原文地址供參考:https://learnopengl.com/Advanced-OpenGL/Instancing