边长为0.4, 中心位置为(0, 0, 0),大部分内容与二维图形的绘制是比较接近的,主要不同之处在于坐标系的变换以及深度测试的开启。
这里立方体的每个面我们用两个三角形表示:
float vertices[] = {
// 顶点坐标 // 纹理坐标
-0.4f, -0.4f, -0.4f, 0.0f, 0.0f, // 可以考虑用EBO优化 有时间尝试一下
0.4f, -0.4f, -0.4f, 1.0f, 0.0f,
0.4f, 0.4f, -0.4f, 1.0f, 1.0f,
0.4f, 0.4f, -0.4f, 1.0f, 1.0f,
-0.4f, 0.4f, -0.4f, 0.0f, 1.0f,
-0.4f, -0.4f, -0.4f, 0.0f, 0.0f,
// 其余五个面用同样的方法给顶点赋值
...
};
接着绑定VAO、VBO(调用vertexBinding函数),方法与前面两次作业类似,只需要注意步长的更改:
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
在前两次的作业中对于图形的渲染都是直接设置顶点的颜色然后进行光栅化,这次的作业里采用纹理来给图形增添细节。
为了能够把纹理映射到三角形上,我们需要制定三角形的每个顶点各自对应纹理的哪个部分,也即是每个顶点都会关联一个纹理坐标(如上面vertices中所示)。接下来我们只需要给顶点着色器传递其对应的纹理坐标,而纹理坐标接着会被传送到片段着色器中,片段着色器会给每个片段进行纹理坐标的插值。
// 顶点着色器
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec2 aTexCoord;\n" // 纹理坐标
"out vec2 TexCoord;\n"
...// 坐标系转换相关,下个部分解释
"void main()\n"
"{\n"
"gl_Position = projection * view * model * vec4(aPos, 1.0);\n"
"TexCoord = vec2(aTexCoord.x, aTexCoord.y);\n"
"}\0";
// 片段着色器
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec2 TexCoord;\n" // 纹理坐标
"uniform sampler2D ourTexture;\n" // 所采用的纹理
"void main()\n"
"{\n"
" FragColor = texture(ourTexture, TexCoord);\n" // 纹理与其坐标对应进行渲染,如果是两个纹理按比例混合可使用mix函数
"}\n\0";
glTexParameter*
分别对单独的一个坐标轴(s、t,如果是3D纹理则还有一个r轴,它们分别和x、y、z轴等价)进行设置:// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴。最后一个参数需要我们传递一个环绕方式(GL_REPEAT、GL_MIRRORED_REPEAT、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER),在这里设定纹理环绕方式GL_REPEAT。
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素映射到纹理坐标。过滤方式包括临近过滤和线性过滤,这两种过滤方式的含义类似于图像处理过程中的最邻近赋值和线性插值两种方法给像素赋值——这里是给纹理像素赋值。GL_NEAREST 产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。依旧使用函数glTexParameter*
来指定纹理的过滤方式:
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
int main () {
...
unsigned int ourTexture = 0;
textureSetting(&ourTexture, "3.png");
myShader.use();
// 纹理单元,这里可以省去,在使用多个纹理时必要
glUniform1i(glGetUniformLocation(myShader.shaderID, "ourTexture"), 0);
...
}
其中textureSetting
函数的实现如下:
// 注意texture的值是需要返回到main函数中以便后续的处理的,所以传入的是texture的指针
void textureSetting(unsigned int * texture, char* name) {
glGenTextures(1, &(*texture)); // 声明并绑定texture,纹理也是ID引用的,类似于VAOVBO
glBindTexture(GL_TEXTURE_2D, *texture);
// 环绕方式的设置
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 载入图片
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
unsigned char *data = stbi_load(name, &width, &height, &nrChannels, 0);
if (data)
{
// 纹理的生成
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 多级渐远纹理的设置
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
}
glBindTexture(GL_TEXTURE_2D, ourTexture);
这一点老师在课上讲的已经比较清楚,我们需要关注的主要有三个矩阵:model用于设置物体自身的变化和运动,view观察矩阵(在这一次作业中较为简单),projection透视投影矩阵。
在render loop中设置:
glm::mat4 model;
glm::mat4 view;
glm::mat4 projection;
// translate平移
model = glm::translate(model, glm::vec3(1, 2, 0));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -200.0f));
projection = glm::perspective(glm::radians(45.0f), (float)SRC_WIDTH / (float)SRC_HEIGHT, 0.1f, 100000000.0f);
特别注意对model处理时,OpenGL中变换顺序最好保持为缩放、旋转、平移。
开启z深度
关闭深度测试
第二个立方体的显示十分奇怪。这是由于立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。之所以这样是因为OpenGL是一个三角形一个三角形地来绘制你的立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。
具体实现方式与上面类似,gui使用方式与前面作业相同。主要不同之处在于model矩阵的设置:
if (rotate_by_angle) // 按角度旋转
{
float radius;
ImGui::SliderFloat("rotate radius", &radius, 0, 360); // 添加滑块
model = glm::rotate(model, radius, glm::vec3(0.5f, 1.0f, 0.0f));
}
if (rotate_by_time) // 按时间旋转
{
model = glm::rotate(model, (float)glfwGetTime()*250, glm::vec3(0.0f, 1.0f, 1.0f)); //glfwGetTime的使用
}
if (translation) // 平移立方体
{
float x, y;
ImGui::SliderFloat("X", &x, -1.0f, 1.0f);
ImGui::SliderFloat("Y", &y, -1.0f, 1.0f);
model = glm::rotate(model, 45.0f, glm::vec3(0.5f, 1.0f, 0.0f));
model = glm::translate(model, glm::vec3(x, y, 0));
}
if (scaling) // 均匀缩放。若要实现非均匀缩放更改model scale中vec3的数值即可
{
float rate;
ImGui::SliderFloat("rate", &rate, 0.0f, 3.0f);
model = glm::scale(model, glm::vec3(rate,rate,rate));
model = glm::rotate(model, 45.0f, glm::vec3(0.5f, 1.0f, 0.0f));
}
然后再进行渲染即可:
unsigned int modelLoc = glGetUniformLocation(myShader.shaderID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
unsigned int viewLoc = glGetUniformLocation(myShader.shaderID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, &view[0][0]);
glUniformMatrix4fv(glGetUniformLocation(myShader.shaderID, "projection"), 1, GL_FALSE, &projection[0][0]);
myShader.use();
// render box
glBindVertexArray(VAO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 36);
ImGui::Render();
ImGui_ImplGlfwGL3_RenderDrawData(ImGui::GetDrawData());