知识点
假如你有一个有许多模型的场景,而这些模型的顶点数据都一样,只是进行了不同的世界空间的变换。想象一下,有一个场景中充满了草叶:每根草都是几个三角形组成的。你可能需要绘制很多的草叶,最终一次渲染循环中就肯能有成千上万个草需要绘制了。因为每个草叶只是由几个三角形组成,绘制一个几乎是即刻完成,但是数量巨大以后,执行起来就很慢了。
像这样绘制出你模型的其他实例,多次绘制之后,很快将达到一个瓶颈,这是因为你glDrawArrays或glDrawElements这样的函数(Draw call)过多。这样渲染顶点数据,会明显降低执行效率,这是因为OpenGL在它可以绘制你的顶点数据之前必须做一些准备工作(比如告诉GPU从哪个缓冲读取数据,以及在哪里找到顶点属性,所有这些都会使CPU到GPU的总线变慢)。所以即使渲染顶点超快,而多次给你的GPU下达这样的渲染命令却未必。
for(GLuint i = 0; i < amount_of_models_to_draw; i++)
{
DoSomePreparations(); //在这里绑定VAO、绑定纹理、设置uniform变量等
glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}
如果我们能够将数据一次发送给GPU,就会更方便,然后告诉OpenGL使用一个绘制函数,将这些数据绘制为多个物体。这就是我们将要展开讨论的实例化(Instancing)。
实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染物体时从CPU到GPU的通信时间,而且只需做一次即可。要使用实例化渲染,我们必须将glDrawArrays和glDrawElements各自改为glDrawArraysInstanced和glDrawElementsInstanced。这些用于实例化的函数版本需要设置一个额外的参数,叫做实例数量(Instance Count),它设置我们打算渲染实例的数量。这样我们就只需要把所有需要的数据发送给GPU一次就行了,然后告诉GPU它该如何使用一个函数来绘制所有这些实例。
就其本身而言,这个函数用处不大。渲染同一个物体一千次对我们来说没用,因为每个渲染出的物体不仅相同而且还在同一个位置;我们只能看到一个物体!出于这个原因GLSL在着色器中嵌入了另一个内建变量,叫做gl_InstanceID。
在通过实例化绘制时,gl_InstanceID的初值是0,它在每个实例渲染时都会增加1。如果我们渲染43个实例,那么在顶点着色器gl_InstanceID的值最后就是42。每个实例都拥有唯一的值意味着我们可以索引到一个位置数组,并将每个实例摆放在世界空间的不同的位置上。
我们调用一个实例化渲染函数,在标准化设备坐标中绘制一百个2D四边形来看看实例化绘制的效果是怎样的。通过对一个储存着100个偏移量向量的索引,我们为每个实例四边形添加一个偏移量。最后,窗口被排列精美的四边形网格填满:
效果展示:
顶点着色器:
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
out vec3 fColor;
uniform vec2 offsets[100];
void main()
{
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(position + offset, 0.0f, 1.0f);
fColor = color;
}
片段着色器:
#version 330 core
in vec3 fColor;
out vec4 color;
void main()
{
color = vec4(fColor, 1.0f);
}
核心主程序:
Shader shader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt"
, "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt"
);
GLfloat 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,
-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
};
GLuint quadVAO, quadVBO;
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER,quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for (GLint y = -10; y < 10; y += 2)
{
for (GLint x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (GLfloat)x / 10.0f + offset;
translation.y = (GLfloat)y / 10.0f + offset;
translations[index++] = translation;
}
}
// Game loop
while (!glfwWindowShouldClose(window))
{
// Set frame time
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
std::cout << deltaTime << std::endl;
// Check and call events
glfwPollEvents();
Do_Movement();
// Clear buffers
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();
for (GLuint 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);
}
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
glBindVertexArray(0);
// Swap the buffers
glfwSwapBuffers(window);
printError();
}
glfwTerminate();
return 0;
实例化数组
在这种特定条件下,前面的实现很好,但是当我们有100个实例的时候(这很正常),最终我们将碰到uniform数据数量的上线。为避免这个问题另一个可替代方案是实例化数组(Instanced Array),它使用顶点属性来定义,这样就允许我们使用更多的数据了,当顶点着色器渲染一个新实例时它才会被更新。
使用顶点属性,每次运行顶点着色器都将让GLSL获取到下个顶点属性集合,它们属于当前顶点。当把顶点属性定义为实例数组时,顶点着色器只更新每个实例的顶点属性的内容而不是顶点的内容。这使我们在每个顶点数据上使用标准顶点属性,用实例数组来储存唯一的实例数据。
我们要绘制100个三角形,每个三角形包含:顶点位置2个float值、顶点颜色3个float值、位置偏移2个float值。
1.定义三角形数据:
位置和颜色数据:
GLfloat 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,
-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
};
偏移数据:
glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for (GLint y = -10; y < 10; y += 2)
{
for (GLint x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (GLfloat)x / 10.0f + offset;
translation.y = (GLfloat)y / 10.0f + offset;
translations[index++] = translation;
}
}
2.设置解析数据方式(解析出数据整体)
根据顶点着色器的顶点属性定义:
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 offset;
我们可以看出,我们需要解析出position、color、offset,根据其对应数据类型,我们需要分别解析出2个float、3个float、2个float。由于位置和颜色数据定义在一个数组中,而offser数据单独定义在一个数组中,所以使用两个VBO读取。
解析出position和color
GLuint quadVAO, quadVBO;
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER,quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
// 一个个
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
解析出offset:
GLuint 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);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
3.绘制物体:
绘制物体代码:
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
glBindVertexArray(0);
原理讲解
上文绘制物体的代码,先绑定了VAO,而VAO中记录了如何从两个顶点数据数组中读取数据的方式,也记录了包含的VBO即从哪里读取数据。
现在调用 glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100) 绘制。这条语句表明要绘制100次,每次传入6个顶点,绘制的图形类型是三角形,从第0个顶点开始传入。
也就是说,OpenGL负责使用6个顶点基于三角形绘制图形,重复100次,而我们负责每次传入6个顶点。
根据顶点着色器中的顶点属性可知每个顶点要有position、color、offset三个顶点属性值。当我们设置VAO时,就已经设置好了数据的对应方式。
于是根据VAO,从第0号顶点开始,读取出一个个position、color、offset。可是我们发现offset是针对一整个图形的,即一个图形6个顶点才对应一个offset,而一个图形每个顶点对应一个position和color,这时就要设置属性除数了。
绘制6个顶点构成一个图形,这个图形叫做绘制的实例。每次绘制实例,会迭代顶点着色器6次,每次传入一个新的顶点的position和color。因此position和color两种顶点属性是在顶点着色器每次迭代时更新的,而offset是在每次绘制不同实例的时候更新的。
使用glVertexAttribDivisor函数可设置顶点属性的更新。
glVertexAttribDivisor(2,1)设置offset顶点属性在每次绘制实例时更新,即每次传入6个顶点绘制新的实例时依据VAO中的解析方法更新offset。
而glVertexAttribDivisor(1,0)设置color顶点属性在每次顶点着色器迭代时更新,即每个顶点更新一次color。当VAO指针将数组读取完后会回到数组起始位置再读取,而一个绘制函数glDrawArrays会存储当前VAO解析到的位置,因此虽然重复绘制100次,但是offset对应数据的数组的读取并不会在每个实例或每个顶点时,回到数组的起始位置,而是基于上一个顶点和实例读取的位置来读取。
效果展示:
顶点着色器:
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 offset;
out vec3 fColor;
void main()
{
gl_Position = vec4(position + offset, 0.0f, 1.0f);
fColor = color;
}
片段着色器:
#version 330 core
in vec3 fColor;
out vec4 color;
void main()
{
color = vec4(fColor, 1.0f);
}
核心主程序:
Shader shader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt"
, "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt"
);
GLfloat 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,
-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
};
GLuint quadVAO, quadVBO;
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER,quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for (GLint y = -10; y < 10; y += 2)
{
for (GLint x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (GLfloat)x / 10.0f + offset;
translation.y = (GLfloat)y / 10.0f + offset;
translations[index++] = translation;
}
}
GLuint 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);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
// Game loop
while (!glfwWindowShouldClose(window))
{
// Set frame time
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
std::cout << deltaTime << std::endl;
// Check and call events
glfwPollEvents();
Do_Movement();
// Clear buffers
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
glBindVertexArray(0);
// Swap the buffers
glfwSwapBuffers(window);
printError();
}
glfwTerminate();
return 0;
将gl_InstanceID与实例数组结合使用:
void main()
{
vec2 pos = position * (gl_InstanceID / 100.0f);
gl_Position = vec4(pos + offset, 0.0f, 1.0f);
fColor = color;
}
效果展示:
小行星
效果展示:
顶点着色器:
// Vertex shader:
// ================
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
out vec2 TexCoords;
out vec3 fragPosition;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
fragPosition = vec3(model * vec4(position, 1.0f));
Normal = mat3(transpose(inverse(model))) * normal;
TexCoords = texCoords;
}
片段着色器:
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D texture_diffuse1;
void main()
{
color = vec4(texture(texture_diffuse1, TexCoords));
}
主程序:
// Std. Includes
#include <string>
#include <algorithm>
using namespace std;
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// GL includes
#include "Shader.h"
#include "Camera.h"
#include "Model.h"
// GLM Mathemtics
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// Other Libs
#include <SOIL.h>
// Properties
GLuint screenWidth = 800, screenHeight = 600;
// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void Do_Movement();
void printError();
GLuint loadTexture(const GLchar* path);
GLuint loadCubemap(vector<const GLchar*> faces);
// Camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
bool keys[1024];
GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
// The MAIN function, from here we start our application and run our Game loop
int main()
{
// Init GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr); // Windowed
glfwMakeContextCurrent(window);
// Set the required callback functions
glfwSetKeyCallback(window, key_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// Options
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// Initialize GLEW to setup the OpenGL Function pointers
glewExperimental = GL_TRUE;
glewInit();
glGetError(); // Debug GLEW bug fix
// Define the viewport dimensions
glViewport(0, 0, screenWidth, screenHeight);
// Setup some OpenGL options
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_PROGRAM_POINT_SIZE);
// Setup and compile our shaders
Shader shader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt"
, "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt"
);
Model rock("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Debug\\rock\\rock.obj");
Model planet("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Debug\\planet\\planet.obj");
unsigned int amount = 3000;
glm::mat4* modelMatrices;
GLfloat rotaSpeedArray[3000];
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // 初始化随机种子
float radius = 30.0;
float offset = 2.5f;
for (unsigned int i = 0; i < amount; i++)
{
glm::mat4 model = glm::mat4(1.0f);
// 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. 缩放:在 0.05 和 0.25f 之间缩放
float scale = (rand() % 20) / 100.0f + 0.05;
model = glm::scale(model, glm::vec3(scale));
// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
float rotAngle = (rand() % 360);
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
float speed = (rand() % 100) / 50.0f + 0.05;
rotaSpeedArray[i] = speed;
// 4. 添加到矩阵的数组中
modelMatrices[i] = model;
}
GLfloat rotaSpeed1 = 0.1;
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));
// Game loop
while (!glfwWindowShouldClose(window))
{
// Set frame time
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
std::cout << deltaTime << std::endl;
// Check and call events
glfwPollEvents();
Do_Movement();
// Clear buffers
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 绘制行星
shader.Use();
glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth / (float)screenHeight, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
model = glm::rotate(model, rotaSpeed1 * deltaTime, glm::vec3(0.4f, 0.6f, 0.8f));
shader.setMat4("view", view);
shader.setMat4("model", model);
shader.setMat4("projection", projection);
planet.Draw(shader);
// 绘制小行星
for (unsigned int i = 0; i < amount; i++)
{
float rotAngle = (rand() % 200)/100 * deltaTime;
modelMatrices[i] = glm::rotate(modelMatrices[i], rotaSpeedArray[i] * deltaTime, glm::vec3(0.4f, 0.6f, 0.8f));
shader.setMat4("view", view);
shader.setMat4("model", modelMatrices[i]);
shader.setMat4("projection", projection);
rock.Draw(shader);
}
// Swap the buffers
glfwSwapBuffers(window);
printError();
}
glfwTerminate();
return 0;
}
void printError()
{
GLuint errorCode = glGetError();
if (errorCode)
std::cout << errorCode << std::endl;
}
// Loads a cubemap texture from 6 individual texture faces
// Order should be:
// +X (right)
// -X (left)
// +Y (top)
// -Y (bottom)
// +Z (front)
// -Z (back)
GLuint loadCubemap(vector<const GLchar*> faces)
{
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image;
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
for (GLuint i = 0; i < faces.size(); i++)
{
image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return textureID;
}
// This function loads a texture from file. Note: texture loading functions like these are usually
// managed by a 'Resource Manager' that manages all resources (like textures, models, audio).
// For learning purposes we'll just define it as a utility function.
GLuint loadTexture(const GLchar* path)
{
//Generate texture ID and load texture data
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGB);
// Assign texture to ID
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
// Parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
SOIL_free_image_data(image);
return textureID;
}
#pragma region "User input"
// Moves/alters the camera positions based on user input
void Do_Movement()
{
// Camera controls
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(FORWARD, deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(LEFT, deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (action == GLFW_PRESS)
keys[key] = true;
else if (action == GLFW_RELEASE)
keys[key] = false;
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
#pragma endregion
升级版小行星
从CPU到GPU时间长,我们需要尽可能减少它们之间数据传输的次数。
对于投影矩阵可以假定固定,只传一次。顶点数据也只需要传输一次每次渲染时绑定VAO即可。而观察矩阵只需要每次游戏循环每个着色器传入一次即可,最麻烦的是模型矩阵。每次循环渲染物体都要改变一次,我们使用顶点属性来使它一次性传输完成。
顶点着色器:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in mat4 instanceMatrix;
out vec2 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
gl_Position = projection * view * instanceMatrix * vec4(position, 1.0f);
TexCoords = texCoords;
}
要注意的是我们不再使用模型uniform变量,取而代之的是把一个mat4的顶点属性,送一我们可以将变换矩阵储存为一个实例数组(instanced array)。然而,当我们声明一个数据类型为顶点属性的时候,它比一个vec4更大,是有些不同的。顶点属性被允许的最大数据量和vec4相等。因为一个mat4大致和4个vec4相等,我们为特定的矩阵必须保留4个顶点属性。因为我们将它的位置赋值为3个列的矩阵,顶点属性的位置就会是3、4、5和6。
下一步我们再次获得网格的VAO,这次使用glDrawElementsInstanced进行绘制:
// Draw meteorites
instanceShader.Use();
for(GLuint 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
);
glBindVertexArray(0);
}
主程序:
// Std. Includes
#include <string>
#include <algorithm>
using namespace std;
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// GL includes
#include "Shader.h"
#include "Camera.h"
#include "Model.h"
// GLM Mathemtics
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// Other Libs
#include <SOIL.h>
// Properties
GLuint screenWidth = 1920, screenHeight = 1080;
// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void Do_Movement();
void printError();
GLuint loadTexture(const GLchar* path);
GLuint loadCubemap(vector<const GLchar*> faces);
// Camera
Camera camera(glm::vec3(0.0f, 40.0f, 185.0f));
bool keys[1024];
GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
// The MAIN function, from here we start our application and run our Game loop
int main()
{
// Init GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr); // Windowed
glfwMakeContextCurrent(window);
// Set the required callback functions
glfwSetKeyCallback(window, key_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// Options
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// Initialize GLEW to setup the OpenGL Function pointers
glewExperimental = GL_TRUE;
glewInit();
glGetError(); // Debug GLEW bug fix
// Define the viewport dimensions
glViewport(0, 0, screenWidth, screenHeight);
// Setup some OpenGL options
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_PROGRAM_POINT_SIZE);
// Setup and compile our shaders
Shader planetShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\vertexShader.txt"
, "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\fragmentShader.txt"
);
Shader instanceShader("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\rockVertexShader.txt"
, "C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Shader\\rockFragmentShader.txt"
);
Model rock("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Debug\\rock\\rock.obj");
Model planet("C:\\Users\\32156\\source\\repos\\LearnOpenGL\\Debug\\planet\\planet.obj");
glm::mat4 projection = glm::perspective(45.0f, (GLfloat)screenWidth / (GLfloat)screenHeight, 1.0f, 10000.0f);
planetShader.Use();
glUniformMatrix4fv(glGetUniformLocation(planetShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
// Also of instance shader
instanceShader.Use();
glUniformMatrix4fv(glGetUniformLocation(instanceShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
// Generate a large list of semi-random model transformation matrices
GLuint amount = 10000000;
glm::mat4* modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // initialize random seed
GLfloat radius = 150.0f;
GLfloat offset = 25.0f;
for (GLuint i = 0; i < amount; i++)
{
glm::mat4 model = glm::mat4(1.0f);
// 1. Translation: Randomly displace along circle with radius '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 = -2.5f + displacement * 0.4f; // Keep height of asteroid field smaller compared to width of x and z
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat 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;
model = glm::scale(model, glm::vec3(scale));
// 3. Rotation: add random rotation around a (semi)randomly picked rotation axis vector
GLfloat 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;
}
// forward declare the buffer
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
// Set transformation matrices as an instance vertex attribute (with divisor 1)
// NOTE: We're cheating a little by taking the, now publicly declared, VAO of the model's mesh(es) and adding new vertexAttribPointers
// Normally you'd want to do this in a more organized fashion, but for learning purposes this will do.
for (GLuint i = 0; i < rock.meshes.size(); i++)
{
GLuint VAO = rock.meshes[i].VAO;
glBindVertexArray(VAO);
// Set attribute pointers for matrix (4 times vec4)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)0);
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(sizeof(glm::vec4)));
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(2 * sizeof(glm::vec4)));
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(3 * sizeof(glm::vec4)));
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glVertexAttribDivisor(6, 1);
glBindVertexArray(0);
}
// Game loop
while (!glfwWindowShouldClose(window))
{
// Set frame time
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
std::cout << deltaTime << std::endl;
// Check and call events
glfwPollEvents();
Do_Movement();
// Clear buffers
glClearColor(0.03f, 0.03f, 0.03f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Add transformation matrices
planetShader.Use();
glUniformMatrix4fv(glGetUniformLocation(planetShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(camera.GetViewMatrix()));
instanceShader.Use();
glUniformMatrix4fv(glGetUniformLocation(instanceShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(camera.GetViewMatrix()));
// Draw Planet
planetShader.Use();
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, -5.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
glUniformMatrix4fv(glGetUniformLocation(planetShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
planet.Draw(planetShader);
// Draw meteorites
instanceShader.Use();
glBindTexture(GL_TEXTURE_2D, rock.textures_loaded[0].id); // Note we also made the textures_loaded vector public (instead of private) from the model class.
for (GLuint 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);
glBindVertexArray(0);
}
// Swap the buffers
glfwSwapBuffers(window);
}
delete[] modelMatrices;
glfwTerminate();
return 0;
}
void printError()
{
GLuint errorCode = glGetError();
if (errorCode)
std::cout << errorCode << std::endl;
}
// Loads a cubemap texture from 6 individual texture faces
// Order should be:
// +X (right)
// -X (left)
// +Y (top)
// -Y (bottom)
// +Z (front)
// -Z (back)
GLuint loadCubemap(vector<const GLchar*> faces)
{
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image;
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
for (GLuint i = 0; i < faces.size(); i++)
{
image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return textureID;
}
// This function loads a texture from file. Note: texture loading functions like these are usually
// managed by a 'Resource Manager' that manages all resources (like textures, models, audio).
// For learning purposes we'll just define it as a utility function.
GLuint loadTexture(const GLchar* path)
{
//Generate texture ID and load texture data
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGB);
// Assign texture to ID
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
// Parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
SOIL_free_image_data(image);
return textureID;
}
#pragma region "User input"
// Moves/alters the camera positions based on user input
void Do_Movement()
{
// Camera controls
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(FORWARD, deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(LEFT, deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(RIGHT, deltaTime);
}
// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (action == GLFW_PRESS)
keys[key] = true;
else if (action == GLFW_RELEASE)
keys[key] = false;
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
#pragma endregion