本主题主要讲解灯光编程,其实灯光与变换一样,因为影响灯光的因素很多,冯氏模型提炼出三种影响因素,并选择了一种数学模型,可以达到较好的视觉效果。
1. 环境光:环境+物体颜色效果
2. 漫反射光:光源颜色 + 灯光位置 + 物体颜色
3. 镜面光:光源颜色 + 观察者位置 + 物体颜色
4. 冯氏模型:环境因子环境光 + 漫反射因子 漫反射光 + 镜面因子 * 镜面光 = 最终视觉效果
实际光线复杂在变换带给光线的影响。
灯光模型
-
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,有人把光照简化成一个简单的数学模型:称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:
- 环境(Ambient)光照;
- 漫反射(Diffuse)光照;
- 镜面(Specular)光照;
-
冯氏光照模型的最终输出颜色是:
- 环境(Ambient) + 漫反射(Diffuse) + 镜面(Specular)光照
-
备注:
- 冯氏模型:
冯氏着色和冯氏反射模型是由美国犹他大学(University of Utah)的Bui Tuong Phong于1975年在他的博士论文中提出的,都用其名字命名。
- 灯光模型主要解决了三种视觉效果:
- 物体在某个光线环境下的视觉效果;(以静的物体为主体的静态视觉效果)
- 物体在灯光不同方向照射下的视觉效果;(以灯光位置为主体的动态视觉效果)
- 观察者在不同角度观察物体的视觉效果;(以观察者为主体的动态视觉效果)
环境光
环境光数学模型
- 数学模型:
环境光颜色 3D对象颜色 环境光强弱因子
-
说明:
- 环境光是向量的乘法,实际是按照分量相乘。比如,得到的黑色光。因为根据光照的物理原理:
- 在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的颜色。就是那些不能被物体所吸收的颜色就是我们能够感知到的物体的颜色。
- 比如:红色照在蓝色或者绿色物体上,应该是黑色。
- 环境光是向量的乘法,实际是按照分量相乘。比如,得到的黑色光。因为根据光照的物理原理:
环境光代码实现
- 顶点着色器
- 传递3D对象颜色与顶点坐标;
#version 410 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec4 aColor;
uniform mat4 model;
uniform mat4 view;
uniform mat4 perspective;
out vec4 vColor;
void main(){
vColor = aColor;
gl_Position = perspective * view * model * vec4(aPos, 1.0);
}
- 片着色器
- 计算最终的环境光效果输出颜色;
- 下面的0.2是环境光强弱因子;
#version 410 core
out vec4 FragColor;
in vec4 vColor;
uniform vec4 lightColor;
void main(){
FragColor = vColor * lightColor * 0.2f;
}
-
没有环境光照与红色环境光照的效果对比(0.4的强度因子)
漫反射光
漫反射光数学模型
- 漫反射光是模拟光照的不同角度产生的不同视觉效果。
- 垂直于物体的光照最亮,反之逐步减弱。
- 数学模型
-
灯光颜色 物体颜色
- 就是漫反射因子。
- 光的强度(漫反射因子大小)依赖光线与物体的角度,为了表示这个角度,约定了1个数学上的几何名词:
- 法向量(垂直于物体表面光的方向,该向量取单位向量)
- 法向量与光线方向的夹角越大,光线越弱。
- 使用角度的余弦(余弦函数在0-90°区间是反单调的,90°时为0,0°为1)
- 夹角的余弦在单位向量的情况下,刚好是两个向量的内积(俗称点积)
-
灯光颜色 物体颜色
- 变换因素的考虑
- 因为物体最终的视觉效果还受Model,View与Perspective三个矩阵的影响,
漫反射光的代码实现
- 漫反射光需要的数据条件:
- 光源位置:lightPos;
- 光源颜色:lightColor;
- 顶点着色器
- 传递了物体坐标给片着色器,用于计算光源方向。
#version 410 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 perspective;
out vec3 vPos;
void main(){
vPos = aPos;
gl_Position = perspective * view * model * vec4(aPos, 1.0);
}
- 片着色器
- 使用unform传递了两个数据
- 光源颜色:
uniform vec4 lightColor;
- 光源位置:
uniform vec3 lightPos;
- 光源颜色:
- 使用unform传递了两个数据
#version 410 core
uniform vec3 lightPos;
uniform vec4 lightColor;
in vec3 vPos;
out vec4 FragColor;
void main(){
// 计算光源的方向
vec3 lightDir = normalize(lightPos - vPos); // 单位化
// 计算漫反射因子
float diffuse = max(dot(lightDir, vPos), 0); // max是防止内积为负数
// 计算漫反射光
vec4 diffuseColor = diffuse * lightColor;
FragColor = diffuseColor;
}
- 数据传递
- 传递了光源位置与颜色。
shader.initProgram();
shader.compileShaderFromFile("./glsl_script/glsl05_v_diffuse.glsl", GL_VERTEX_SHADER);
shader.compileShaderFromFile("./glsl_script/glsl05_f_diffuse.glsl", GL_FRAGMENT_SHADER);
shader.link();
// 传递光源位置
glm::vec3 lightPos = glm::vec3(1.0f, 1.0f, 1.0f);
shader.setUniform_3fv("lightPos", glm::value_ptr(lightPos));
// 传递光的颜色
glm::vec4 lightColor = glm::vec4(1.0f, 0.5f, 0.5f, 1.0f);
shader.setUniform_4fv("lightColor", glm::value_ptr(lightColor));
- 效果
改进
添加环境光
- 片着色器
- 强化了漫发射光,弱化了环境光。
#version 410 core
uniform vec3 lightPos;
uniform vec4 lightColor;
in vec3 vPos;
out vec4 FragColor;
void main(){
// 计算光源的方向
vec3 lightDir = normalize(lightPos - vPos); // 单位化
// 计算漫反射因子
float diffuse = max(dot(lightDir, vPos), 0); // max是防止内积为负数
// 计算漫反射光
vec4 diffuseColor = diffuse * lightColor;
// FragColor = diffuseColor;
// 改进:添加环境光
vec4 ambientColor = vec4(1.0f, 1.0f, 0.0f, 1.0f); // 环境光
float ambientFactor =0.4; // 环境光因子
FragColor = (ambientFactor * ambientColor + 2.0 * diffuseColor) * vec4(vPos, 1.0f);
}
- 效果
考虑物体Model变换的影响
- 上面光源没有变化,但漫反射产生的效果感觉光源位置在变化,这实际是物体的顶点在做世界坐标变换的时候,实际上计算法向量的时候没有考虑(就是计算光源方向的时候,没有考虑物体的世界变换)。
- 代码
#version 410 core
uniform vec3 lightPos;
uniform vec4 lightColor;
uniform mat4 model;
in vec3 vPos;
out vec4 FragColor;
void main(){
// 改进2:不直接使用vPos,而是使用世界变换后的顶点坐标
// 先补一个分量,完成计算后,再转换为vec3,这样世界变换中的位移没有影响
vec3 modelPos = vec3(model * vec4(vPos, 1.0));
vec3 lightDir = normalize(lightPos - modelPos); // 单位化
// 计算光源的方向
// vec3 lightDir = normalize(lightPos - vPos); // 单位化
// 计算漫反射因子
float diffuse = max(dot(lightDir, modelPos), 0); // max是防止内积为负数
// float diffuse = max(dot(lightDir, vPos), 0); // max是防止内积为负数
// 计算漫反射光
vec4 diffuseColor = diffuse * lightColor;
// FragColor = diffuseColor;
// 改进1:添加环境光
vec4 ambientColor = vec4(1.0f, 1.0f, 0.0f, 1.0f); // 环境光
float ambientFactor =0.4; // 环境光因子
FragColor = (ambientFactor * ambientColor + 2.0 * diffuseColor) * vec4(vPos, 1.0f);
}
- 效果
- 关于其他
- 实际光源还受很多因素影响,比如变换中的缩放等,照相机位置等。这些根据同样原理,牢牢关注方向的计算方式,然后调整对应的变换矩阵即可(当然这个矩阵可能会很复杂)。
- 这里我们假设球体是在原点,这样顶点坐标就是法向量(法向量就是顶点坐标与原点的连线),对不同情况下的球体法向量有标准求法-结果也是切面点与圆心的连线向量;
改进-变换与背面剔除
变换
- model
- view
- perspective
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 2.0f);
glm::mat4 view = glm::lookAt(
cameraPos, // 照相机位置
glm::vec3(0.0f, 0.0f, 0.0f), // 物体位置,暂时设置为原点
glm::vec3(0.0f, 1.0f, 0.0f)); // 相机向上的向量,计算右向量的条件
glm::mat4 perspective = glm::perspective(glm::radians(90.0f), 1.0f, 0.0f, 100.0f);
model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));
model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
shader.setUniform_4fm("model", glm::value_ptr(model));
shader.setUniform_4fm("view", glm::value_ptr(view));
shader.setUniform_4fm("perspective", glm::value_ptr(perspective));
- 其中model在每次绘制的时候旋转,产生动态效果
声明::
glm::mat4 model = glm::mat4(1.0f);
旋转:
void render(){
// glCullFace(GL_BACK);
model = glm::rotate(model, glm::radians(1.0f), glm::vec3(1.0f, 0.0f, 0.0f));
shader.setUniform_4fm("model", glm::value_ptr(model));
glClearDepth (0.0f);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.open();
data.open();
glPointSize(5.0f);
// glDrawArrays(GL_POINTS, 0, 6 * lats * lons);
glDrawArrays(GL_TRIANGLES, 0, 6 * lats * lons);
// glDrawArrays(GL_LINE_LOOP, 0, 6 * lats * lons);
data.close();
shader.close();
}
背面剔除
- 关键代码:
///////////////////////////////////////////////////
// 开启深度缓冲
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GEQUAL);
// 背面剔除
glEnable(GL_CULL_FACE); // 开启
glCullFace (GL_BACK); // 剔除背面
///////////////////////////////////////////////////
- 剔除背面的前后比较
附录
球体表面的法向量求法
-
球面坐标表示
- 首先使用来表示球面的点与轴的夹角,表示球面点在平面上投影点与轴的夹角。这样球面坐标表示为:
- 首先使用来表示球面的点与轴的夹角,表示球面点在平面上投影点与轴的夹角。这样球面坐标表示为:
-
求的偏导数,得到的求曲面切线,两个切线的叉乘就是法向量。
- 球面法向量为
- 其中
其实球面点的法向量就是其本身,只是范数有变化,这个标准化以后就没有差异了。
镜面光
镜面光数学模型
镜面光是模拟观察者在不同角度观察产生的视觉效果变化。
镜面光也好,漫反射光也好,环境光也好,观察者最终只有看到一个效果:这个效果是各种反射的混合;
-
真正影响颜色输出的两个因素:
- 光线颜色,环境颜色,物体颜色;
- 每种颜色的强弱,或者在最后混合中的比例,这种比例不同就产生各种神奇的视觉效果。
-
环境光:
- 处理最简单,直接一个常数因子 * 环境光颜色,常数因子取值最好0-1之间。
-
漫反射光:
- 灯光颜色 * 漫反射因子。漫反射因子大小与光源的位置有关,这样光线在不同位置,就产生不同效果。
- 漫反射因子就是光线方向与物体垂直方向的夹角的余弦(这是选择,你可以考虑不使用余弦,只要是反单调,并且取值在0-1之间就行)
-
镜面光:
- 灯光颜色 * 镜面因子。镜面因子大小与照相机位置有关,还与光源在物体上的反射方向有关(反射方向也与物体法向量有关),照相机位置变化就会产生不同的视觉效果。
- 镜面因子也是夹角余弦(这是冯氏模型的选择),只是夹角是照相机方向向量与光线的反射光方向向量的夹角。
-
镜面光示意图:
- 注意:
- 镜面光的难度是求反向光的向量。
- 反向光的向量计算数学模型是对称的模型,实际就是一个旋转变换(在GLSL与GLM都有实现),具体的推导交给数学家去玩。
镜面管的代码实现
- 镜面光需要增加一个条件:照相机的位置。
- 传动照相机位置
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
// 传送照相机位置
shader.setUniform_3fv("cameraPos",glm::value_ptr(cameraPos));
- 片着色器
- 其中使用指数pow函数,因为镜面光因子小于0,指数运算会降低镜面因子,亮度降低,但在合成的时候,使用了增强系数。
- 为了效果,把灯光设置为白色,环境光设置为红色;(可以试着调整参数,并明白其中的影响关系)
#version 410 core
uniform vec3 lightPos;
uniform vec4 lightColor;
uniform mat4 model;
uniform vec3 cameraPos;
in vec3 vPos;
out vec4 FragColor;
void main(){
vec3 modelPos = vec3(model * vec4(vPos, 1.0));
vec3 lightDir = normalize(lightPos - modelPos);
float diffuse = max(dot(lightDir, modelPos), 0);
vec4 diffuseColor = diffuse * lightColor;
vec4 ambientColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);
float ambientFactor =0.2;
// 计算照相机到物体的方向
vec3 cameraDir = normalize(cameraPos - modelPos);
// 计算光源的反射光向量
vec3 reflectDir = reflect(lightDir, modelPos);
// 计算反向光向量与照相机方向向量的内积
float specular = pow(max(dot(cameraDir, reflectDir), 0.0), 4);
// 镜面光输出颜色
vec4 specularColor = specular * lightColor;
FragColor = (ambientFactor * ambientColor + 0.4 * diffuseColor + 3.0f * specularColor) * vec4(1.0f, 1.0f, 1.0f, 1.0f);
}
3.变换数据发送
#include "gl_env.h"
#include "gl_data.h"
#include "gl_shader.h"
#include <iostream>
#include <glm/glm.hpp> //glm::vec3, glm::vec4, glm::mat4
#include <glm/ext.hpp> //glm::translate, glm::rotate, glm::scale, glm::perspective, glm::pi
#include <glm/gtc/type_ptr.hpp> // 把glm结构体转换为指针
#include <cmath>
// 环境
GL_Env env;
// 定点数组
GL_Data data;
// 着色器
GL_Shader shader;
glm::mat4 model = glm::mat4(1.0f);
GLuint lats = 30;
GLuint lons = 60;
void render();
/**
* 生成球面顶点,半径是是单位长度1;
*/
glm::vec3 getPoint(GLfloat u, GLfloat v){
GLfloat r = 0.9f;
GLfloat pi = glm::pi<GLfloat>();
GLfloat z = r * std::cos(pi * u);
GLfloat x = r * std::sin(pi * u) * std::cos(2 * pi * v);
GLfloat y = r * std::sin(pi * u) * std::sin(2 * pi * v);
// std::cout << x << "," << y << "," << z << std::endl;
return glm::vec3(x, y, z);
}
void createSphere(GLfloat *sphere, GLuint Longitude, GLuint Latitude){
// Longitude:经线切分个数
// Latitude:纬线切分个数
GLfloat lon_step = 1.0f/Longitude;
GLfloat lat_step = 1.0f/Latitude;
GLuint offset = 0;
for(int lat = 0; lat < Latitude; lat++){ // 纬线u
for(int lon = 0;lon < Longitude; lon++){ // 经线v
// 一次构造4个点,两个三角形,
glm::vec3 point1 = getPoint(lat * lat_step, lon * lon_step);
glm::vec3 point2 = getPoint((lat + 1) * lat_step, lon * lon_step);
glm::vec3 point3 = getPoint((lat + 1) * lat_step, (lon + 1) * lon_step);
glm::vec3 point4 = getPoint(lat * lat_step, (lon + 1) * lon_step);
memcpy(sphere + offset, glm::value_ptr(point1), 3 * sizeof(GLfloat));
offset += 3;
memcpy(sphere + offset, glm::value_ptr(point4), 3 * sizeof(GLfloat));
offset += 3;
memcpy(sphere + offset, glm::value_ptr(point3), 3 * sizeof(GLfloat));
offset += 3;
memcpy(sphere + offset, glm::value_ptr(point1), 3 * sizeof(GLfloat));
offset += 3;
memcpy(sphere + offset, glm::value_ptr(point3), 3 * sizeof(GLfloat));
offset += 3;
memcpy(sphere + offset, glm::value_ptr(point2), 3 * sizeof(GLfloat));
offset += 3;
}
}
// std::cout<<"offset:" << offset << std::endl;
}
int main(int argc, const char** argv) {
env.context("GLSL语言-镜面光");
env.opengl();
///////////////////////////////////////////////////
data.initVertextArrays();
GLfloat vertices[6 * 3 * lats * lons ];
createSphere(vertices, lons, lats);
data.addData(vertices, sizeof(vertices), 0, 3); // 开启Shader第一个参数位置,顶点3个一组。
///////////////////////////////////////////////////
shader.initProgram();
shader.compileShaderFromFile("./glsl_script/glsl07_v_specular.glsl", GL_VERTEX_SHADER);
shader.compileShaderFromFile("./glsl_script/glsl07_f_specular.glsl", GL_FRAGMENT_SHADER);
shader.link();
// 传递光源位置
glm::vec3 lightPos = glm::vec3(-5.0f, 5.0f, 0.0f);
shader.setUniform_3fv("lightPos", glm::value_ptr(lightPos));
// 传递光的颜色
glm::vec4 lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
shader.setUniform_4fv("lightColor", glm::value_ptr(lightColor));
// glm::mat4 view = glm::mat4(1.0f);
// glm::mat4 perspective = glm::mat4(1.0f);
// 添加变换
glm::vec3 cameraPos = glm::vec3(3.0f, 0.0f, 0.0f);
// 传送照相机位置
shader.setUniform_3fv("cameraPos",glm::value_ptr(cameraPos));
glm::mat4 view = glm::lookAt(
cameraPos,
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 perspective = glm::perspective(
glm::radians(90.0f),
1.0f,
0.0f,100.0f);
model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
shader.setUniform_4fm("model", glm::value_ptr(model));
shader.setUniform_4fm("view", glm::value_ptr(view));
shader.setUniform_4fm("perspective", glm::value_ptr(perspective));
///////////////////////////////////////////////////
// 开启深度缓冲
glEnable(GL_CULL_FACE);
glCullFace (GL_BACK);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GEQUAL);
///////////////////////////////////////////////////
env.run(render);
env.destroy();
return 0;
}
void render(){
// glCullFace(GL_BACK);
model = glm::rotate(model, glm::radians(1.0f), glm::vec3(1.0f, 0.0f, 0.0f));
shader.setUniform_4fm("model", glm::value_ptr(model));
glClearDepth (0.0f);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.open();
data.open();
glPointSize(5.0f);
// glDrawArrays(GL_POINTS, 0, 6 * lats * lons);
glDrawArrays(GL_TRIANGLES, 0, 6 * lats * lons);
// glDrawArrays(GL_LINE_LOOP, 0, 6 * lats * lons);
data.close();
shader.close();
}
// g++ -omain gl07_specular.cpp gl_env.cpp gl_data.cpp gl_shader.cpp -lglfw -lglew -framework opengl
- 效果