作业要求:
a. 四棱锥边长均为 2;
b. 四棱锥各个顶点颜色不同;
c. 四棱锥的中心为(1,2,3);
d. 要求使用多种光,包括环境光、镜面光和散射光等,可以通过控制物体对光的反射
材质因子和不同光的因子达到不同的光照效果。
运行说明参见README.md文件。效果展示见录屏视频。
初始化OpenGL
建立窗口,注册回调函数,加载glad,并设置OpenGL的基本属性。
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, true); // comment this line in a release build!
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Materials", NULL, NULL);
if (window == NULL){
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);// 用GLFW注册了回调函数之后,鼠标一移动mouse_callback函数就会被调用
glfwSetScrollCallback(window, scroll_callback);//注册鼠标滚轮的回调函数
if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// configure global OpenGL state
glEnable(GL_DEPTH_TEST);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
准备数据
建立顶点数组对象。同时分别注册顶点的位置属性、颜色属性和法向量属性,属性的序号值指明了数据的存放位置,以在着色器中通过对应位置找到位置和颜色数据。
顶点的position属性满足题设要求。Color属性自己随便设。Normal属性需要自己算,注意的是这里我采用了sharp edge的形式,在渲染每一个三角形面片时,三个顶点都用相同的法向量(而非像常见的,用一个顶点周围所有面的法向量来平均得到顶点的法向量,这样得到的光照效果会光滑过渡)。因此在后面渲染时也是使用glDrawArrays(GL_TRIANGLES, 0, 36)
来绘制每个三角形,即不同的三角形面片之间不能复用顶点数据。
float sqr2 = sqrt(2.0f);
float pyramid_vertices[] = {
// Positions Color Normal
0.0f, sqr2, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, //0
0.0f, 0.0f, sqr2, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //2
sqr2, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //1
0.0f, sqr2, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, //0
-sqr2, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //3
0.0f, 0.0f, sqr2, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //2
0.0f, sqr2, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, //0
0.0f, 0.0f,-sqr2, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //4
-sqr2, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //3
0.0f, sqr2, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, //0
sqr2, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //1
0.0f, 0.0f,-sqr2, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //4
sqr2, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //1
0.0f, 0.0f, sqr2, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //2
-sqr2, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //3
sqr2, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //1
-sqr2, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, //3
0.0f, 0.0f,-sqr2, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, //4
};
for (int iFace = 0; iFace < 6; iFace++) {
int vid0 = 3*iFace, vid1 = vid0+1, vid2 = vid1+1;
glm::vec3 a = glm::vec3(pyramid_vertices[vid1*9+0]-pyramid_vertices[vid0*9+0], \
pyramid_vertices[vid1*9+1]-pyramid_vertices[vid0*9+1], \
pyramid_vertices[vid1*9+2]-pyramid_vertices[vid0*9+2]);
glm::vec3 b = glm::vec3(pyramid_vertices[vid2*9+0]-pyramid_vertices[vid0*9+0], \
pyramid_vertices[vid2*9+1]-pyramid_vertices[vid0*9+1], \
pyramid_vertices[vid2*9+2]-pyramid_vertices[vid0*9+2]);
glm::vec3 nm= glm::normalize(glm::cross(a, b));
for (int i = vid0; i <= vid2; i++) {
pyramid_vertices[i*9+6] = nm[0];
pyramid_vertices[i*9+7] = nm[1];
pyramid_vertices[i*9+8] = nm[2];
}
}
// first, configure the pyramid's VAO, VBO, EBO
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyramid_vertices), pyramid_vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
// Normal attribute
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6*sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);//unbind
同时需要注意的是,在定义pyramid_vertices
数组时,需要合理安排顶点顺序,保证每个三角形面按此顶点定义顺序(在右手坐标系下)的面法向量朝外。
除了四棱锥,还设置另外一个充当光源的物体(其实不必要,只是为了好看而已)。也类似地炮制。
float cube_vertices[] = {
// Positions
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f
};
// second, configure the light's VAO (VBO stays the same; the vertices are the same for the light object which is also a 3D cube)
unsigned int lightCubeVAO, lightCubeVBO;
glGenVertexArrays(1, &lightCubeVAO);
glGenBuffers(1, &lightCubeVBO);
glBindVertexArray(lightCubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, lightCubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(cube_vertices), cube_vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);//unbind
建立着色器
Shader pyramidShader ("../src/materials.vs", "../src/materials.fs");
Shader lightCubeShader("../src/lightcube.vs", "../src/lightcube.fs");
对于片段着色器,需要注意将法向量从局部空间转换到世界空间。而由于是在世界空间中进行所有光照的计算,因此我们需要一个在世界空间中的顶点位置worldCoord
。
#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec3 aNormal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 objectColor;
out vec3 Normal;
out vec3 worldCoord;
void main()
{
worldCoord = vec3(model * vec4(aPos, 1.0));
//Normal = aNormal;
//法线矩阵:模型矩阵左上角的逆矩阵的转置矩阵,用它来移除对法向量错误缩放的影响
//Normal = mat3(transpose(inverse(model))) * aNormal;
Normal = transpose(inverse(mat3(model))) * aNormal;
gl_Position = projection * view * model * vec4(aPos, 1.0);
objectColor = aColor;
}
涉及环境光照、漫反射光照、镜面反射光照的片段着色器,较之前复杂一些。一般地,需要定义物体的材质struct Material
,其中的ambient
,diffuse
,specular
三个分量均为vec3
类型,以能表征各向异性的材料。对光源struct Light
也是同理。
在这里需要根据顶点着色器传入的顶点的世界空间中的坐标和法向量计算与光线之间的漫反射和镜面反射分量。所有分量叠加后再与物体本身的颜色objectColor
相乘。
#version 460 core
out vec4 FragColor;
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform vec3 viewPos;//观察者/摄像机的位置
uniform Material material;
uniform Light light;
in vec3 objectColor;
in vec3 Normal;
in vec3 worldCoord;
void main()
{
//环境光照
vec3 ambient = material.ambient * light.ambient;
//漫反射光照
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - worldCoord);//从片段指向光源
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * (diff * material.diffuse);
//镜面光照
vec3 specular;
if (diff > 0.0) {//要使光照方向与法向夹角小于90°才能有镜面光照效果,否则白搭
vec3 viewDir = normalize(viewPos - worldCoord);//从片段指向摄像机
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);//高光的反光度(Shininess),一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。
specular = spec * light.specular * material.specular;
} else {
specular = vec3(0.0f, 0.0f, 0.0f);
}
vec3 result = (ambient + diffuse + specular) * objectColor;
// vec3 result = Normal;
FragColor = vec4(result, 1.0);
}
物体材质
关于物体材质的选择上,我参考了LearnOpenGL网站上材质一节中提供的链接中包含的材质列表。将所有可选的材质添加到程序中作为materials_lists
。但值得注意的是,如这张材质列表提供时所注明的:
注意材质表格中的环境光值可能与漫反射值不一样,它们没有考虑光照的强度。要想纠正这一问题,你需要将所有的光照强度都设置为vec3(1.0),这样才能得到正确的输出
因此若使用这张材质列表的所有数据时,在渲染循环中需要设置光源参数为
glm::vec3 curr_lightPos = glm::vec3(radius*cos(currentFrame), lightPos.y, radius*sin(currentFrame));
pyramidShader.setVec3("light.position", curr_lightPos);
pyramidShader.setVec3("light.ambient", glm::vec3(1.0f, 1.0f, 1.0f));
pyramidShader.setVec3("light.diffuse", glm::vec3(1.0f, 1.0f, 1.0f));
pyramidShader.setVec3("light.specular", glm::vec3(1.0f, 1.0f, 1.0f));
为了增加趣味性,我将光源位置设置成在某个高度(y
坐标固定)上围绕四棱锥中心轴不断旋转。
一共有24种材质供选择,切换材质通过键盘输入0~9, q, e, r, t, y, u, i, o, p, f, g, h, j ,k等键。在process_input
函数里切换:
if (glfwGetKey(window, GLFW_KEY_0) == GLFW_PRESS)
material_id = 0;
else if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS)
material_id = 1;
... ...
同时在渲染循环中根据material_id
切换:
// material's effect
pyramidShader.setVec3("material.ambient", materials_lists[material_id][0], materials_lists[material_id][1], materials_lists[material_id][2]);
pyramidShader.setVec3("material.diffuse", materials_lists[material_id][3], materials_lists[material_id][4], materials_lists[material_id][5]);
pyramidShader.setVec3("material.specular",materials_lists[material_id][6], materials_lists[material_id][7], materials_lists[material_id][8]);
pyramidShader.setFloat("material.shininess", materials_lists[material_id][9]);
渲染四棱锥和光源物体
老生常谈地,设置着色器中的世界空间矩阵model
,视角矩阵view
,和投影矩阵projection
。每次draw之前先将顶点数组对象绑定。
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
pyramidShader.setMat4("projection", projection);
pyramidShader.setMat4("view", view);
// world transformation
glm::mat4 model = glm::mat4(1.0f);
// model = glm::translate(model, glm::vec3(1.0f, 2.0f, 3.0f));
pyramidShader.setMat4("model", model);
// render the pyramid
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 18);
/* -------------------------------------------------------------------------------------------- */
// also draw the lamp object (光源)
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, curr_lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
效果
选取光源移动到不同位置时的效果,展示如下。
|
|
|
|
|
|