目录
光照模型
Phong光照模型
环境光反射
漫反射
镜面反射
代码实现
GLSL简介
具体示例
改进的Phong模型
Phong模型缺陷
三种光的效果
完整代码
使用片元着色器绘制光照
光照模型
光照模型包括局部光照和全局光照。局部光照指物体表面上一点的颜色只取决于表面的材质属性、表面的局部集合性质以及光源的位置和属性,而与场景中其他的表面无关。而全局光照则需要考虑场景中所有表面和光源相互作用的照射效果。
如下图所示,左图中x点接收到周围环境的光线照射表示全局光照,右图中点x接收来自光源的直接照射表示局部光照。
Phong光照模型
使用该模型的原因:它能够用尽量简单的数学原理尽可能解释物体表面呈现出不同颜色及明暗程序的原因
Phong反射模型中涉及到了4个基本向量,如下图所示。其中p为三维物体表面上的一点,l是从点p指向光源位置的向量,n表示p点的法向量,v是从p点指向相机(观察者)的向量,r是沿着l方向入射光线按照反射定于的出射方向。注意,在下面的计算中,我们假定所有的向量都已经归一化。
?
在现实世界中有三种光照类型:
- 「高光」:物体完全反射光源照射到表面的光照到人眼(即镜面反射)
- 「漫反射光」:物体朝着任意方向反射相同亮度的光照到人眼
- 「环境光」:物体表面完全背对光源,但其它物体反射光照照射到该物体表面上,再通过该物体反射光照到人眼
下面从上到下依次是高光、漫反射光、环境光
Phong反射模型考虑了光线和材质之间的三种相互作用:环境光反射、漫反射和镜面反射,最终在三维物体表面上的每个点的颜色由这三个成分组合表示如下。
环境光是照射在其它物体上的光先反射到观察物体上,通过观察物体表面再反射至视角中。
Phong 模型中,举出了一个非常大胆的假设:物体表面接收到的各种环境光都是相同强度的。
这样可以简化计算,因此环境光与光源的角度无关,与观察角度也无关,所以它是一个常数。
?其中La可以表示任何单独的光源,也可以代表全局环境光。
作用:由于光线被场景中其他物体多次反射后会回到当前计算的点上,这样计算较为复杂,通过环境光分量近似做出贡献。
理想的漫反射表面会把光线向所有方向均匀散射。
公式推导过程:
根据Lambert定理,只有入射光线的垂直分量才对照明起到作用,也就是说漫反射部分的光照效果与法向量n和光源向量l的夹角大小有关。如下图所示。同时考虑漫反射系数kd表示表面对漫反射光的反射程度。其中Ld代表漫反射光照的强度。
根据Lambert定律,只有入射光线的垂直分量才对照明起到作用。
公式如下:
当光源
位于物体表面以下时,漫反射将没有作用,因此当入射光线的垂直分量为负数时,我们设定为0,代码就是max(l·n,0)
从物理角度来看,光照的强度会随着光源与物体间距离变长而逐渐衰减:
d表示光源和三维物体表面上点的距离,a,b和c表示距离衰减系数
最终公式如下:
漫反射是用于模拟粗糙表面的,而镜面反射是用于模拟光滑表面的。
反射出去的光线越集中在一个角度附近,越接近真实的镜子,如下图所示
根据生活知识很好理解,当观察方向和反射方向越接近时,光照强度越高,如下图所示
因此反射光的强度定义如下所示:
Phong提出了一个近似模型考虑镜面反射光部分,将表面看成是光滑的,观察者看到的光线强度取决于物体表面反射光的方向r和观察方向v这两者之间的夹角,用公式表示如下。
?类似于漫反射,我们可以像计算漫反射分量那样加上一个距离因子。
代码实现
GLSL简介
clamp函数用法:
返回的value介于A、B之间,若value小于min,返回min,若大于max,返回max
?
举例平面、三角形、球的法向量计算方式:
??法向量对光照计算有着重要作用,如果法向量计算有误,会造成许多绘制结果的缺陷
具体示例
本文以球形举例,因为球的法向量比较好计算。
?计算出法向量,生成VBO,然后传入到顶点着色器中。
vector<vec3f> ns;
// TODO 计算球模型在每个顶点的法向量,并存储到ns数组中
for (int i = 0; i < vs.size(); ++i) {
ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
}
// 生成VBO,并绑定法向量数据
glGenBuffers(1, &vertexNormalID);
glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);
接下来在顶点着色器中进行编程:
首先进行坐标变换,在顶点着色器中将坐标和法向量变换到相机坐标系下。
vPosition代表的是顶点位置,vNormal代表的是法向量。
接下来就可以计算N L V R 四个归一化的向量:
// TODO 计算N,L,V,R四个向量并归一化
vec3 N_norm = normalize(N);
vec3 L_norm = normalize(lightPos - V);
vec3 V_norm = normalize(-V);
vec3 R_norm = reflect(-L_norm, N_norm);
然后根据四个向量计算漫反射系数和镜面反射系数
// TODO 计算漫反射系数和镜面反射系数
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
然后就可以输出颜色
color = vec4( ambiColor+diffColor * lambertian+specColor * pow(specular, 10.0), 1.0);
//下面为考虑距离衰减
int a=0,b=0,c=1;
float d = length(lightPos - V);
//color = vec4( ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 10.0)/(a+b*d+c*d*d), 1.0);
改进的Phong模型
在镜面反射中,我们通过计算出射方向和视点的夹角来确定反射光的强度,在计算机中计算反射向量R的计算量比较大。因此我们可以用其他计算方法近似替代。
即下面代码中的计算量很大:
?
?所以,Blinn-Phong光照模型提出使用另一种计算方法:「半程向量」
有了半角向量后,我们不再是使用出射光方向r和视点v的夹角来计算镜面反射强度系数,而是采用半角向量h和法向量的夹角来计算。
?代码如下:
可以看到将镜面反射系数的代码更改了
// TODO 计算漫反射系数和镜面反射系数
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
//使用BLINN_PHONG模型
vec3 H_norm = normalize(L_norm + V_norm);
float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);
Phong模型缺陷
除此之外,phong模型还存在一个问题。
Phong模型会出现光照截断或者过度不自然的现象。即断层现象:
出现这种的情况的原因是视角和反射矢量的角度不能超过90度。如果角度大于90度,点积的结果为负导致镜面光分量值为0.0。
BlinnPhong使用视线与反射光线的中间向量与法线的点乘进行计算,这样做在任何情况下镜面反射都不会小于0。从而解决Phong模型面临的问题。
BlinnPhong模型的光照效果则较为柔和,没有断层现象:
三种光的效果
完整代码
顶点着色器:
#version 330 core
in vec3 vPosition;
in vec3 vNormal;
uniform vec3 lightPos;
uniform mat4 modelViewProjMatrix;
uniform mat4 modelViewMatrix;
out vec4 color;
// Phong 光照模型的实现 (per-vertex shading)
void main()
{
gl_Position = modelViewProjMatrix * vec4(vPosition, 1.0);
// TODO 将顶点坐标变换到相机坐标系
vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
vec3 V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;
vec3 N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
// TODO 设置三维物体的材质属性
vec3 ambiColor = vec3(0.2, 0.2, 0.2);
vec3 diffColor = vec3(0.5, 0.5, 0.5);
vec3 specColor = vec3(0.3, 0.3, 0.3);
// TODO 计算N,L,V,R四个向量并归一化
vec3 N_norm = normalize(N);
vec3 L_norm = normalize(lightPos - V);
vec3 V_norm = normalize(-V);
vec3 R_norm = reflect(-L_norm, N_norm);
// TODO 计算漫反射系数和镜面反射系数
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
//使用BLINN_PHONG模型
//vec3 H_norm = normalize(L_norm + V_norm);
//float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);
// float shininess = 10.0;
// TODO 计算最终每个顶点的输出颜色
// color = ...; m,,.,
color = vec4( ambiColor+diffColor * lambertian+specColor * pow(specular, 10.0), 1.0);
//下面为考虑距离衰减
int a=0,b=0,c=1;
float d = length(lightPos - V);
//color = vec4( ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 10.0)/(a+b*d+c*d*d), 1.0);
}
片元着色器:
#version 330 core
in vec4 color;
out vec4 fragmentColor;
void main()
{
fragmentColor = color;
}
main.cpp
#include "include/Angel.h"
#include "include/TriMesh.h"
#pragma comment(lib, "glew32.lib")
#include <cstdlib>
#include <iostream>
using namespace std;
GLuint programID;
GLuint vertexArrayID;
GLuint vertexBufferID;
GLuint vertexNormalID;
GLuint vertexIndexBuffer;
GLuint vPositionID;
GLuint vNormalID;
GLuint modelViewMatrixID;
GLuint modelViewProjMatrixID;
GLuint lightPosID;
TriMesh* mesh = new TriMesh();
vec3 lightPos(0.0, 0.0, 2.0);
//
// 相机参数设置,因为本周不涉及相机观察变换和投影变换,因此可以使用固定相机视角和投影变换。
// 可使用默认设置,注意默认设置不同于单位矩阵。
namespace Camera
{
mat4 modelMatrix;
mat4 viewMatrix;
mat4 projMatrix;
mat4 ortho( const GLfloat left, const GLfloat right,
const GLfloat bottom, const GLfloat top,
const GLfloat zNear, const GLfloat zFar )
{
// TODO 请按照实验课内容补全相机观察矩阵的计算
mat4 c;
c[0][0] = 2.0 / (right - left);
c[1][1] = 2.0 / (top - bottom);
c[2][2] = -2.0 / (zFar - zNear);
c[3][3] = 1.0;
c[0][3] = -(right + left) / (right - left);
c[1][3] = -(top + bottom) / (top - bottom);
c[2][3] = -(zFar + zNear) / (zFar - zNear);
return c;
}
mat4 perspective( const GLfloat fovy, const GLfloat aspect,
const GLfloat zNear, const GLfloat zFar)
{
// TODO 请按照实验课内容补全相机观察矩阵的计算
GLfloat top = tan(fovy * M_PI / 180 / 2) * zNear;
GLfloat right = top * aspect;
mat4 c;
c[0][0] = zNear / right;
c[1][1] = zNear / top;
c[2][2] = -(zFar + zNear) / (zFar - zNear);
c[2][3] = -(2.0*zFar*zNear) / (zFar - zNear);
c[3][2] = -1.0;
c[3][3] = 0.0;
return c;
}
mat4 lookAt( const vec4& eye, const vec4& at, const vec4& up )
{
// TODO 请按照实验课内容补全相机观察矩阵的计算
vec4 n = normalize(eye - at);
vec4 u = normalize(vec4(cross(up, n), 0.0));
vec4 v = normalize(vec4(cross(n, u), 0.0));
vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
mat4 c = mat4(u, v, n, t);
return c * Translate(-eye);
}
}
//
// OpenGL 初始化
void init()
{
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
programID = InitShader("vshader_vert.glsl", "fshader_vert.glsl");
// 从顶点着色器和片元着色器中获取变量的位置
vPositionID = glGetAttribLocation(programID, "vPosition");
vNormalID = glGetAttribLocation(programID, "vNormal");
modelViewMatrixID = glGetUniformLocation(programID, "modelViewMatrix");
modelViewProjMatrixID = glGetUniformLocation(programID, "modelViewProjMatrix");
lightPosID = glGetUniformLocation(programID, "lightPos");
// TODO 读取外部三维模型
mesh->read_off("sphere_coarse.off");
vector<vec3f> vs = mesh->v();
vector<vec3i> fs = mesh->f();
vector<vec3f> ns;
// TODO 计算球模型在每个顶点的法向量,并存储到ns数组中
for (int i = 0; i < vs.size(); ++i) {
ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
}
// 生成VAO
glGenVertexArrays(1, &vertexArrayID);
glBindVertexArray(vertexArrayID);
// 生成VBO,并绑定顶点数据
glGenBuffers(1, &vertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glBufferData(GL_ARRAY_BUFFER, vs.size() * sizeof(vec3f), vs.data(), GL_STATIC_DRAW);
// 生成VBO,并绑定法向量数据
glGenBuffers(1, &vertexNormalID);
glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);
// 生成VBO,并绑定顶点索引
glGenBuffers(1, &vertexIndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, fs.size() * sizeof(vec3i), fs.data(), GL_STATIC_DRAW);
// OpenGL相应状态设置
glEnable(GL_LIGHTING);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
}
//
// 渲染
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(programID);
// TODO 计算相机观察矩阵和投影矩阵,并传入顶点着色器
vec4 eye(0,0, 3.0, 1.0);
//由于物体在坐标原点,是单位圆,光源在0,0,2的地方,因此我们可以将观察点设置在0,0,3
vec4 at(0, 0, 0, 1); // 原点
vec4 up(0, 1, 0, 0); // 默认方向
Camera::modelMatrix = mat4(1.0);
Camera::viewMatrix = Camera::lookAt(eye, at, up);
Camera::projMatrix = Camera::perspective(45, 1,0.1,100);
//使用与3.2实验类似的方法来创建三个矩阵
mat4 modelViewMatrix = Camera::viewMatrix * Camera::modelMatrix;
mat4 modelViewProjMatrix = Camera::projMatrix * modelViewMatrix;
// TODO 将相机位置传入顶点着色器
glUniformMatrix4fv(modelViewMatrixID, 1, GL_TRUE, &modelViewMatrix[0][0]);
glUniformMatrix4fv(modelViewProjMatrixID, 1, GL_TRUE,&modelViewProjMatrix[0][0]);
// TODO 将光源位置传入顶点着色器
glUniform3fv(lightPosID, 1, &lightPos[0]);
glEnableVertexAttribArray(vPositionID);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glVertexAttribPointer(
vPositionID,
3,
GL_FLOAT,
GL_FALSE,
0,
(void*)0
);
glEnableVertexAttribArray(vNormalID);
glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
glVertexAttribPointer(
vNormalID,
3,
GL_FLOAT,
GL_FALSE,
0,
(void*)0
);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
glDrawElements(
GL_TRIANGLES,
int(mesh->f().size() * 3),
GL_UNSIGNED_INT,
(void*)0
);
glDisableVertexAttribArray(vPositionID);
glUseProgram(0);
glutSwapBuffers();
}
//
// 重新设置窗口
void reshape(GLsizei w, GLsizei h)
{
glViewport(0, 0, w, h);
}
//
// 鼠标响应函数
void mouse(/*int button,int state,*/int x, int y)//需要修改传入的参数
{
//if (button == GLUT_LEFT && state == GLUT_DOWN) {
lightPos = vec3(float(x - 250) / 250, float(250 - y) / 250, 2.0);
//}
// TODO 用鼠标控制光源的位置lightPos,以实时更新光照效果
}
//
// 键盘响应函数
void keyboard(unsigned char key, int x, int y)
{
switch(key)
{
case 033: // ESC键 和 'q' 键退出游戏
exit(EXIT_SUCCESS);
break;
case 'q':
exit (EXIT_SUCCESS);
break;
}
glutPostRedisplay();
}
//
void idle(void)
{
glutPostRedisplay();
}
//
void clean()
{
glDeleteBuffers(1, &vertexBufferID);
glDeleteProgram(programID);
glDeleteVertexArrays(1, &vertexArrayID);
if (mesh) {
delete mesh;
mesh = NULL;
}
}
//
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
glutInitWindowSize(500, 500);
glutCreateWindow("OpenGL-Tutorial");
glewInit();
init();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutPassiveMotionFunc(mouse);//Main函数中的鼠标回调函数也需要进行修改
glutKeyboardFunc(keyboard);
glutIdleFunc(idle);
glutMainLoop();
clean();
return 0;
}
使用片元着色器绘制光照
程序中有三个地方可以执行光照计算:在OpenGL应用程序代码中、在顶点着色器中或者在片元着色器中。无论在何处执行光照计算,都使用相同的基本光照模型,主要区别是在于绘制的效率和外观。
如果在顶点着色器中执行光照计算,那么在光栅化模块中将对顶点的颜色进行插值计算从而得到每个片元的颜色。
基于片元的光照计算,区别在于片元着色器的输入数据不是顶点着色器计算之后的每个顶点的颜色,而是每个顶点的法向量,在渲染过程中对法向量进行插值而计算片元的颜色,从而输出到光栅化模块中。需要注意的是,输入的每个顶点必须是经过模-视变换和投影变换之后位于裁剪体内部的顶点,因为只有这些顶点最后在光栅化模块中才能得到渲染并显示在绘制窗口中。
如下图所示,左边是采用逐顶点的光照计算的效果,右边采用逐片元光照计算的效果,从结果上来看,因为逐片元计算会对三维物体表面法向量进行差值,从而使得物体表面的法向量场更加均匀,所以产生的光照效果也更加均匀。
?此时顶点着色器代码如下:
#version 330 core
in vec3 vPosition;
in vec3 vNormal;
uniform vec3 lightPos;
uniform mat4 modelViewProjMatrix;
uniform mat4 modelViewMatrix;
out vec3 N;
out vec3 V;
out vec3 lightPos_new;
// Phong 光照模型的实现 (per-fragment shading)
void main()
{
gl_Position = modelViewProjMatrix * vec4(vPosition, 1.0);
// TODO 将顶点变换到相机坐标系下
vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
// 对顶点坐标做透视投影
V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;
// TODO 将光源位置变换到相机坐标系下
vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);
// 对光源坐标做透视投影
lightPos_new = lightPos_cameraspace.xyz / lightPos_cameraspace.w;
// TODO 将法向量变换到相机坐标系下并传入片元着色器
N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
}
区别就是在于,向量和颜色的计算是在片元着色器中进行的。
片元着色器代码如下:
#version 330 core
in vec3 N;
in vec3 V;
in vec3 lightPos_new;
out vec4 fragmentColor;
void main()
{
// TODO 设置三维物体的材质属性
vec3 ambiColor = vec3(0.1, 0.1, 0.1);
vec3 diffColor = vec3(0.5, 0.5, 0.5);
vec3 specColor = vec3(0.3, 0.3, 0.3);
// TODO 计算N,L,V,R四个向量并归一化
vec3 N_norm = normalize(N);
vec3 L_norm = normalize(lightPos_new - V);
vec3 V_norm = normalize(-V);
vec3 R_norm = reflect(-L_norm, N_norm);
// TODO 计算漫反射系数和镜面反射系数
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
//float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
//使用BLINN_PHONG模型
vec3 H_norm = normalize(L_norm + V_norm);
float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);
float shininess = 10.0;
float d = length(lightPos_new - V);
int a=0,b=0,c=1;
// TODO 计算最终每个片元的输出颜色
fragmentColor = vec4(ambiColor +
diffColor * lambertian +
specColor * pow(specular, 5.0), 1.0);
//fragmentColor=vec4(ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 5.0)/(a+b*d+c*d*d), 1.0);
}
|