【写在前面】
本章主要内容:
1、基础光照类型( Ambient,Diffuse,Specular )
2、在GLSL中进行光照计算
【正文开始】
在前面的文章中,我们已经学会了如何在glsl中使用一般的顶点数据,并通过使用纹理让它更加的真实,但这还远远不够,在现实世界中,我们所看到一个物体的颜色并不是它本来的颜色,而是它不能吸收的颜色,或者说,它所反射出来的颜色。
如下图所示:
我们所看下方的矩形块颜色,是它反射了太阳光(白色光)中的红色(主要颜色,还有一些其他颜色)所综合起来的颜色。
当给定物体一个颜色 color,它能从光源light中反射出来颜色的计算方法很简单,进行相乘:result = color * light
例如:color = vec3(1.0f, 0.0f, 0.0f) 红色,light = vec3(1.0f, 1.0f, 1.0f) 白色,result = vec3(1.0f, 0.0f, 0.0f) 红色,即:我们看到物体的颜色为红色。
事实上,现实世界中的光照是非常复杂的,它需要考虑非常多的因素,所以直接模拟是不现实的,因此,在 OpenGL 中使用的是简化了的光照模型,这个模型被称为冯氏光照模型( Phong Lighting Model )。
冯氏光照模型的主要结构由 3 个分量组成:环境光( Ambient )、漫反射光( Diffuse )和镜面高光( Specular )。
【环境光】即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
【漫反射光】模拟光源对物体的方向性影响。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮,反之,它就会越亮。
【镜面高光】模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
那么,我们要如何在 glsl 中进行计算呢?
1、进行环境光的计算。
因为场景中存在环境光,我们的物体应该不是完全黑暗的,所以这里使用一个小的光照常量来进行相乘。
vec3 ambient = lightColor * vec3(0.2f, 0.2f, 0.2f);
2、进行漫反射光的计算。
要进行漫反射光的计算,我们必须要知道两个向量:光源的方向向量,片元的法线向量,
【法向量】一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点)。
实际上,法向量是可以通过相邻的顶点计算出来的,在高数中可以知道:两个向量进行叉乘可以得到一个垂直的向量,方向可以由右手法则确定。
void MyRender::initializeTriangle()
{
glm::vec3 normal1 = glm::cross(glm::vec3(0.75f, -0.75f, 0.75f) - glm::vec3(-0.75f, -0.75f, 0.75f),
glm::vec3(-0.375f, 0.75f, 0.375f) - glm::vec3(-0.75f, -0.75f, 0.75f));
glm::vec3 normal2 = glm::cross(glm::vec3(0.75f, -0.75f, -0.75f) - glm::vec3(0.75f, -0.75f, 0.75f),
glm::vec3(0.375f, 0.75f, 0.375f) - glm::vec3(0.75f, -0.75f, 0.75f));
glm::vec3 normal3 = glm::cross(glm::vec3(-0.75f, -0.75f, -0.75f) - glm::vec3(0.75f, -0.75f, -0.75f),
glm::vec3(0.375f, 0.75f, -0.375f) - glm::vec3(0.75f, -0.75f, -0.75f));
glm::vec3 normal4 = glm::cross(glm::vec3(-0.75f, -0.75f, 0.75f) - glm::vec3(-0.75f, -0.75f, -0.75f),
glm::vec3(-0.375f, 0.75f, -0.375f) - glm::vec3(-0.75f, -0.75f, -0.75f));
glm::vec3 normal5 = glm::cross(glm::vec3(0.375f, 0.75f, 0.375f) - glm::vec3(-0.375f, 0.75f, 0.375f),
glm::vec3(-0.375f, 0.75f, -0.375f) - glm::vec3(-0.375f, 0.75f, 0.375f));
glm::vec3 normal6 = glm::cross(glm::vec3(-0.75f, -0.75f, 0.75f) - glm::vec3(0.75f, -0.75f, 0.75f),
glm::vec3(0.75f, -0.75f, -0.75f) - glm::vec3(0.75f, -0.75f, 0.75f));
VertexData vertices[] =
{
{ glm::vec3( -0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal1 },
{ glm::vec3( 0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal1 },
{ glm::vec3(-0.375f, 0.75f, 0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal1 },
{ glm::vec3( 0.375f, 0.75f, 0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal1 },
{ glm::vec3( 0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal2 },
{ glm::vec3( 0.75f, -0.75f, -0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal2 },
{ glm::vec3(0.375f, 0.75f, 0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal2 },
{ glm::vec3(0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal2 },
{ glm::vec3( 0.75f, -0.75f, -0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal3 },
{ glm::vec3( -0.75f, -0.75f, -0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal3 },
{ glm::vec3( 0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal3 },
{ glm::vec3(-0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal3 },
{ glm::vec3( -0.75f, -0.75f, -0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal4 },
{ glm::vec3( -0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal4 },
{ glm::vec3(-0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal4 },
{ glm::vec3(-0.375f, 0.75f, 0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal4 },
{ glm::vec3(-0.375f, 0.75f, 0.375f), glm::vec3(0.8f, 0.8f, 0.0f), normal5 },
{ glm::vec3( 0.375f, 0.75f, 0.375f), glm::vec3(0.8f, 0.8f, 0.0f), normal5 },
{ glm::vec3(-0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal5 },
{ glm::vec3( 0.375f, 0.75f, -0.375f), glm::vec3(0.0f, 0.8f, 0.8f), normal5 },
{ glm::vec3( 0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal6 },
{ glm::vec3(-0.75f, -0.75f, 0.75f), glm::vec3(0.8f, 0.8f, 0.0f), normal6 },
{ glm::vec3( 0.75f, -0.75f, -0.75f), glm::vec3(0.0f, 0.8f, 0.8f), normal6 },
{ glm::vec3(-0.75f, -0.75f, -0.75f), glm::vec3(0.0f, 0.8f, 0.8f), normal6 }
};
GLushort indices[] =
{
0, 1, 2, 3, 3,
4, 4, 5, 6, 7, 7,
8, 8, 9, 10, 11, 11,
12, 12, 13, 14, 15, 15,
16, 16, 17, 18, 19, 19,
20, 20, 21, 22, 23
};
glGenBuffers(1, &m_cubeVao);
glBindVertexArray(m_cubeVao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glGenBuffers(1, &m_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
int location = 0;
glVertexAttribPointer(location, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)0);
glEnableVertexAttribArray(location);
glVertexAttribPointer(location + 1, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)(sizeof(glm::vec3)));
glEnableVertexAttribArray(location + 1);
glVertexAttribPointer(location + 2, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)(sizeof(glm::vec3) * 2));
glEnableVertexAttribArray(location + 2);
glGenBuffers(1, &m_lampVao);
glBindVertexArray(m_lampVao);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glVertexAttribPointer(location, 3, GL_FLOAT, GL_TRUE, sizeof(VertexData), (void *)0);
glEnableVertexAttribArray(location);
}
因为立方体有六个面,所以要计算六个面的法线 normal*,这里还需要注意的是,我使用了两个 vao,m_cubeVao 用于绘制我们的物体,因为它接受光照,使用的是光照着色器,m_lampVao 则是用于我们的灯光的可视化(这里不是必要的),它不需要进行光照计算,所以使用一般的着色器。
【glm::cross】返回两个向量进行叉乘的结果。
现在,法向量有了,我们还需要一个光源的方向向量,它是一个 uniform 变量。
//这里省略一些,具体见源码
.
.
.
static glm::vec3 lightColor = glm::vec3(1.0f, 1.0f, 1.0f);
static glm::vec4 lightPosition = glm::vec4(0.0f, 0.0f, 2.0f, 1.0f);
static float angle = 0.0f;
.
.
.
//将光源的进行旋转
glm::mat4 lightMatrix(1.0f);
lightMatrix = glm::rotate(lightMatrix, 0.04f, glm::vec3(1.0f, 1.0f, 1.0f));
lightPosition = lightMatrix * lightPosition;
.
.
.
//计算法线矩阵
glm::mat3 normalMatrix = glm::transpose(glm::inverse(modelMatrix));
GLuint normalLocation = glGetUniformLocation(m_cubeProgram, "normalMatrix");
glUniformMatrix3fv(normalLocation, 1, GL_FALSE, glm::value_ptr(normalMatrix));
.
.
.
//将光源的颜色和位置传入着色器中
GLuint lightColorLocation = glGetUniformLocation(m_cubeProgram, "lightColor");
glUniform3fv(lightColorLocation, 1, glm::value_ptr(lightColor));
GLuint lightPositionLocation = glGetUniformLocation(m_cubeProgram, "lightPosition");
glUniform3fv(lightPositionLocation, 1, glm::value_ptr(lightPosition));
片段着色器中:
in vec4 fragPos;
in vec3 normal;
uniform vec3 viewPosition;
uniform vec3 lightColor;
uniform vec3 lightPosition;
顶点着色器中:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color0;
layout (location = 2) in vec3 normal0;
out vec3 color;
out vec4 fragPos;
out vec3 normal;
uniform mat3 normalMatrix;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
color = color0;
fragPos = model * vec4(position, 1.0f);
normal = normalMatrix * normal0;
}
fragPos 即顶点在世界空间中的坐标(只左乘了 model 矩阵)。
normal 即法线,这里左乘了一个法线矩阵。
【法线矩阵】
每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵( Normal Matrix ),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。
法线矩阵被定义为「模型矩阵左上角的逆矩阵的转置矩阵」。
现在开始计算漫反射光:
如图,光源的方向向量为 = lightPosition - fragPos。
而光源对顶点的影响可以通过他们的夹角反映,他们的余弦值越大,夹角越大,对于单位向量,余弦值可以通过点乘得到( glsl 中使用 dot() 函数),使用 max() 函数保证一个大于0的值。
vec3 lightDir = normalize(lightPosition - vec3(fragPos));
float diff = max(dot(normalize(normal), lightDir), 0.0f);
vec3 diffuse = diff * lightColor;
3、进行镜面高光的计算。
镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。
如图所示,镜面高光还依赖于我们( 观察者 )的位置,即所谓的摄像机的位置,在上面的着色器中,它是 viewPosition 。
前面讲过观察矩阵,它是由三个向量进行计算得到的一个矩阵,这里我将它简单的封装到了一个 Camera 中:
Camera.h:
#ifndef CAMERA_H
#define CAMERA_H
#include <glm/matrix.hpp>
class Camera
{
public:
enum MoveDirection
{
Front = 0,
Back,
Left,
Right
};
enum RotateDirection
{
Vertical = 0,
Horizontal
};
public:
Camera() { }
Camera(const glm::vec3 &cameraPos, const glm::vec3 &cameraFront, const glm::vec3 &cameraUp);
~Camera();
void setCameraPos(const glm::vec3 &cameraPos) { m_cameraPos = cameraPos; }
glm::vec3 getCameraPos() const { return m_cameraPos; }
void setCameraFront(const glm::vec3 &cameraFront) { m_cameraFront = cameraFront; }
glm::vec3 getCameraFront() const { return m_cameraFront; }
glm::mat4 getViewMatrix() const;
//在Front, Back, Left, Right四个方向上移动摄像机, 距离为step
void move(MoveDirection direction, float step);
//在Vertical, Horizontal两个方向上旋转摄像机, 角度为angle
void rotate(RotateDirection direction, float angle);
private:
glm::vec3 m_cameraPos = glm::vec3(0.0f, 0.0f, 5.0f);
glm::vec3 m_cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 m_cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
};
#endif
Camera.cpp:
#include "Camera.h"
#include <glm/gtc/matrix_transform.hpp>
Camera::Camera(const glm::vec3 &cameraPos, const glm::vec3 &cameraFront, const glm::vec3 &cameraUp)
: m_cameraPos(cameraPos),
m_cameraFront(cameraFront),
m_cameraUp(cameraUp)
{
}
Camera::~Camera()
{
}
glm::mat4 Camera::getViewMatrix() const
{
return glm::lookAt(m_cameraPos, m_cameraPos + m_cameraFront, m_cameraUp);
}
void Camera::move(MoveDirection direction, float step)
{
switch (direction)
{
case Front:
m_cameraPos += step * m_cameraFront;
break;
case Back:
m_cameraPos -= step * m_cameraFront;
break;
case Left:
m_cameraPos -= step * glm::normalize(glm::cross(m_cameraFront, m_cameraUp));
break;
case Right:
m_cameraPos += step * glm::normalize(glm::cross(m_cameraFront, m_cameraUp));
break;
default:
break;
}
}
void Camera::rotate(RotateDirection direction, float angle)
{
static float pitch = 0.0f;
static float yaw = -90.0f;
if (direction == Vertical)
{
pitch += angle;
if (pitch > 89.9f)
pitch = 89.9f;
if (pitch < -89.9f)
pitch = -89.9f;
}
else if (direction == Horizontal)
yaw += angle;
float x = cos(glm::radians(yaw));
float y = sin(glm::radians(pitch));
float z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
m_cameraFront = glm::normalize(glm::vec3(x, y, z));
}
关于这个 class 有一些数学知识,我就不多说了,在这里,我们使用 getCameraPos() 即可得到摄像机的位置,然后将它传入着色器( viewPosition )中即可。
现在开始计算镜面高光:
首先计算 lightDir 的反射向量,在 GLSL 中使用 reflect() 函数,前面我们知道 lightDir 的方向是顶点指向光源,但是 reflect() 函数要求光源指向顶点,所以这里需要取反。
然后计算视线方向与反射方向的点乘,然后取它的 32 次幂。这个 32 是高光的反光度( Shininess )。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。
vec3 viewDir = normalize(viewPosition - vec3(fragPos));
vec3 reflectDir = reflect(-lightDir, normalize(normal));
float spec = pow(max(dot(reflectDir, viewDir), 0.0f), 32);
vec3 specular = 0.8f * spec * lightColor;
4、进行最终颜色的计算。
最后一步是最简单的:
vec3 resultColor = (diffuse + ambient + specular) * color;
fragColor = vec4(resultColor, 1.0f);
我们只需要将三种光照的影响相加,再乘以物体的颜色,就是它最终的颜色。
效果图如下:
【结语】
本篇主要讲了冯氏光照模型的简单 GLSL 实现,其中难点就是有一点点物理和数学的知识,不过都是些基础的知识,而且我应该讲得很清楚了,然后这个光照模型实际上是有一些问题的,这在后面将会进行改进的。