【寫在前面】
本章主要內容:
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 實現,其中難點就是有一點點物理和數學的知識,不過都是些基礎的知識,而且我應該講得很清楚了,然後這個光照模型實際上是有一些問題的,這在後面將會進行改進的。