文章目录
- 物体的光照模型
- 立方体坐标
- 构建立方体的6个面
- 代码框架
- widget.cpp
- 顶点着色器
- 片元着色器
- Ambient 环境光
- Diffuse 漫反色
- 法向量
- 计算漫反射分量
- Specular Highlight镜面高光
- 计算镜面反射分量
- 补充:半程向量的使用
物体的光照模型
出于性能的原因,一般使用冯氏光照模型(Phong Lighting Model)。
冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:
- 环境光照(Ambient Lighting) :让物体从各个角度都能看到一些微弱的但很均匀的色彩
- 漫反射光照(Diffuse Lighting):让物体产生明暗对比效果的一个重要的分量,产生了表面的粗糙度的感觉
- 镜面光照(Specular Lighting) :让物体表面出现光泽,产生了表面的光滑度的感觉
基础光照 - LearnOpenGL CN
立方体坐标
直观上这几个点很容易就能获取,但注意这8个点不是单位球面上的点。如果想细分立方体获取球面,简单的方式是做归一化处理。
// 中心位于坐标原点,边与坐标轴平行的单位立方体的顶点
QVector3D v[] = {
{ -0.5, -0.5, 0.5 },
{ -0.5, 0.5, 0.5 },
{ 0.5, 0.5, 0.5 },
{ 0.5, -0.5, 0.5 },
{ -0.5, -0.5, -0.5 },
{ -0.5, 0.5, -0.5 },
{ 0.5, 0.5, -0.5 },
{ 0.5, -0.5, -0.5 },
};
// 在细分前归一化处理8个顶点
for (int i = 0; i < 8; ++i) {
v[i].normalize ();
qout << v[i];
}
recursion(0);
构建立方体的6个面
// 组成6个面,12个三角形,36个顶点(每个顶点赋一种颜色),按照右手法则,保证正面朝外
int tindices[6][4] = {
{1,0,3,2}, // 103 和 132 两个三角形,右手法则
{2,3,7,6}, // 237 和 276,下同里
{3,0,4,7},
{6,5,1,2},
{4,5,6,7},
{5,4,0,1}
};
// 6个面,每面两个三角形,可进一步细分,如果不想细分,传入n=0即可
void recursion( int n ){
for (int i = 0; i < 6; ++i) {
divide_triangle (v[ tindices[i][0] ],v[tindices[i][1]],v[tindices[i][2]], n);
divide_triangle (v[ tindices[i][0] ],v[tindices[i][2]],v[tindices[i][3]], n);
}
}
recursion(0); recursion(3);
代码框架
widget.cpp
#include "Widget.h"
#include "qmatrix4x4.h"
#include "qvector3d.h"
#include "qvector4d.h"
#include <QApplication>
#include <QMouseEvent>
#include <QThread>
#include <QVector3D>
#include <vector>
#define qRandom QRandomGenerator::global ()
#define qout if( 1 ) qDebug() << __FILE__ << __LINE__ << ": "
bool rotateFlag = false;
// 中心位于坐标原点,边与坐标轴平行的单位立方体的顶点
QVector3D v[] = {
{ -0.5, -0.5, 0.5 },
{ -0.5, 0.5, 0.5 },
{ 0.5, 0.5, 0.5 },
{ 0.5, -0.5, 0.5 },
{ -0.5, -0.5, -0.5 },
{ -0.5, 0.5, -0.5 },
{ 0.5, 0.5, -0.5 },
{ 0.5, -0.5, -0.5 },
};
int tindices[6][4] = {
{1,0,3,2},
{2,3,7,6},
{3,0,4,7},
{6,5,1,2},
{4,5,6,7},
{5,4,0,1}
};
std::vector<QVector3D> vdata;
std::vector<QVector3D> normals;
void triangle(QVector3D a, QVector3D b, QVector3D c){
vdata.push_back (a);
vdata.push_back (b);
vdata.push_back (c);
auto normal = QVector3D::crossProduct (a-b,b-c).normalized();
normals.push_back (normal);
normals.push_back (normal);
normals.push_back (normal);
}
void divide_triangle(QVector3D a, QVector3D b, QVector3D c, int n ){
QVector3D ab, bc, ca;
if( n > 0 ) {
// 如果使用归一化,顶点和的结果是否除以 2 都不影响结果
ab = ( a + b )/2;
ca = ( a + c )/2;
bc = ( b + c )/2;
ab.normalize ();
ca.normalize ();
bc.normalize ();
divide_triangle(a, ab, ca, n-1);
divide_triangle(b, bc, ab, n-1);
divide_triangle(c, ca, bc, n-1);
divide_triangle(ab,bc, ca, n-1);
}
else
triangle (a,b,c);
}
void recursion( int n ){
for (int i = 0; i < 6; ++i) {
divide_triangle (v[tindices[i][0]],v[tindices[i][1]],v[tindices[i][2]], n);
divide_triangle (v[tindices[i][0]],v[tindices[i][2]],v[tindices[i][3]], n);
}
}
// 相对于每个坐标轴的旋转角度(°)
enum {Xaxis, Yaxis, Zaxis, NumAxes};
int Axis = Zaxis;
//float Theta[NumAxes] = { 0.0, 0.0, 0.0 };
//QVector3D Theta = { 20.0, -10.0, 0.0 };
QVector3D Theta = { 70.0, 0.0, 0.0 };
//QVector3D Theta;
Widget::Widget(QWidget *parent)
: QOpenGLWidget(parent)
{
setWindowTitle ("12_basic_lighting");
resize (100,100);
for (int i = 0; i < 8; ++i) {
v[i].normalize ();
qout << v[i];
}
recursion(0);
}
Widget::~Widget()
{
makeCurrent ();
glDeleteBuffers (1,&VBO);
glDeleteVertexArrays (1,&VAO);
doneCurrent ();
}
void Widget::initializeGL()
{
initializeOpenGLFunctions ();
const char *version =(const char *) glGetString (GL_VERSION);
qout << QString(version);
// ---------------------------------
// 创建一个顶点数组对象
glGenVertexArrays (1,&VAO);
glBindVertexArray(VAO);
// ---------------------------------
// 创建并初始化一个缓冲区对象
glGenBuffers (1,&VBO);
glBindBuffer (GL_ARRAY_BUFFER,VBO);
glBufferData (GL_ARRAY_BUFFER,
sizeof(QVector3D)*vdata.size () + sizeof(QVector3D)*normals.size () ,
nullptr,
GL_STATIC_DRAW);
glBufferSubData (GL_ARRAY_BUFFER, 0 , sizeof(QVector3D)*vdata.size () , vdata.data ());
glBufferSubData (GL_ARRAY_BUFFER, sizeof(QVector3D)*vdata.size () ,sizeof(QVector3D)*normals.size () , normals.data ());
glVertexAttribPointer(0,
3, // 变量中元素的个数,
GL_FLOAT, // 类型
GL_FALSE, // 标准化,是否在 [-1,1] 之间
sizeof(QVector3D), // 步长
(void*)0 ); // 变量的偏移量,在多个变量混合时指定变量的偏移
glEnableVertexAttribArray(0); // 使用 location = 0 的索引
glVertexAttribPointer(1,
3, // 变量中元素的个数,
GL_FLOAT, // 类型
GL_FALSE, // 标准化,是否在 [-1,1] 之间
sizeof(QVector3D), // 步长
(void*)( sizeof(QVector3D) * vdata.size () ) ); // 变量的偏移量,在多个变量混合时指定变量的偏移
glEnableVertexAttribArray(1); // 使用 location = 0 的索引
// ---------------------------------
QString filename = ":/shader";
shaderProgram.addShaderFromSourceFile (QOpenGLShader::Vertex, filename+".vert");
shaderProgram.addShaderFromSourceFile (QOpenGLShader::Fragment, filename+".frag");
shaderProgram.link ();
glBindBuffer (GL_ARRAY_BUFFER,0);
// 控制多边形的正面和背面的绘图模式
// glPolygonMode (GL_FRONT_AND_BACK,GL_LINE);
// 深度测试
glEnable(GL_DEPTH_TEST);
}
void Widget::paintGL()
{
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); // 设置背景色
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindVertexArray(VAO);
shaderProgram.bind ();
// uniform 变量
QVector3D lightColor ( 1.0f, 1.0f, 1.0f );
QVector3D objectColor( 1.0f, 0.5f, 0.31f);
QVector3D objectPos ( -1.5f, 0.0f, -5.0f );
QVector3D lightSourcePos ( 0, 1.0f, 1.0f );
float ambientStrength = 0.2f;
float specularStrength = 0.5;
// ---------------------------------
// // 绘制 物体
QMatrix4x4 translateModel;
// translateModel.translate (objectPos);
QMatrix4x4 rx,ry,rz;
rx.rotate (Theta[Xaxis],1.0,0.0,0.0);
ry.rotate (Theta[Yaxis],0.0,1.0,0.0);
rz.rotate (Theta[Zaxis],0.0,0.0,1.0);
QMatrix4x4 scaleModel;
scaleModel.scale (0.5);
QMatrix4x4 model = translateModel * rx * ry * rz * scaleModel;
QMatrix4x4 view;
view.setColumn (3,QVector4D(0,0,-3,1));
QMatrix4x4 projection;
projection.perspective (45.0f, (float)width ()/height (), 0.1f, 100.0f);
shaderProgram.setUniformValue ("model", model);
shaderProgram.setUniformValue ("view", view);
shaderProgram.setUniformValue ("projection", projection);
shaderProgram.setUniformValue ("lightColor", lightColor);
shaderProgram.setUniformValue ("objectColor", objectColor );
shaderProgram.setUniformValue ("lightPos", lightSourcePos );
shaderProgram.setUniformValue ("ambientStrength",ambientStrength );
shaderProgram.setUniformValue ("specularStrength",specularStrength );
shaderProgram.setUniformValue ("lighting", true );
shaderProgram.setUniformValue ("viewPos", QVector3D(0,0,3) );
// qout << lightColor * objectColor;
glDrawArrays (GL_TRIANGLES, 0, (int)vdata.size ());
// 绘制 灯
QMatrix4x4 lightModel;
lightModel.translate (5,5,-15); // 灯是随意摆放,对光照计算没啥用处。只是为了简单示意
// lightModel.scale (0.2f);
// lightColor /= ambientStrength;
shaderProgram.setUniformValue ("model", lightModel);
shaderProgram.setUniformValue ("objectColor", 0.2f, 0.2f, 0.2f);
shaderProgram.setUniformValue ("lightColor", lightColor / ambientStrength);
shaderProgram.setUniformValue ("lighting", false);
glDrawArrays (GL_TRIANGLES, 0, (int)vdata.size () );
if( rotateFlag ){
QThread::currentThread ()->msleep (50);
Theta[Axis] += 2;
if( Theta[Axis] > 360.0 )
Theta[Axis] = - 360.0;
update ();
}
}
void Widget::mousePressEvent(QMouseEvent *e)
{
rotateFlag = !rotateFlag;
if(rotateFlag == false) {
qout << Theta;
return;
}
auto b = e->button ();
switch (b) {
case Qt::LeftButton:
Axis = Xaxis;
break;
case Qt::MiddleButton:
Axis = Yaxis;
break;
case Qt::RightButton:
Axis = Zaxis;
break;
default:
break;
}
update ();
}
顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
片元着色器
#version 330 core
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
gl_FragColor = vec4(result, 1.0);
}
Ambient 环境光
在顶点着色器中实现,简单来水就是用一个很小的环境光强度乘上光 再乘上物体的颜色就行。
效果:均匀着色
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 result;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform vec3 lightColor;
uniform vec3 objectColor;
uniform float ambientStrength;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vec3 ambient = ambientStrength * lightColor;
result = ( ambient ) * objectColor;
}
strength : 0.1 strength : 0.2 strength: 0.3 strength: 0.4
Diffuse 漫反色
漫反射开始对物体产生显著的明暗效果。
从反射角度可以看到明亮的表面,偏离反射角度观察,物体表面开始暗淡下去。
所以,计算漫反射光照需要什么?
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。
法向量
法向量是一个垂直于顶点表面的(单位)向量。
由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。
我们对构成立方体的每一个三角形的3个顶点计算法向量,可以简单地把法线数据手工添加到顶点数据中。
void triangle(QVector3D a, QVector3D b, QVector3D c){
vdata.push_back (a);
vdata.push_back (b);
vdata.push_back (c);
auto normal = QVector3D::crossProduct (a-b,b-c).normalized();
normals.push_back (normal);
normals.push_back (normal);
normals.push_back (normal);
}
计算漫反射分量
-
计算光源和片段位置之间的光线向量
vec3 lightDir = normalize(lightPos - FragPos);
-
计算光线向量和法向量的点积
float diff = max(dot(norm, lightDir), 0.0);
-
得到漫反射分量
vec3 diffuse = diff * lightColor;
Specular Highlight镜面高光
和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向,例如玩家是从什么方向看向这个片段的。
镜面光照决定于表面的反射特性。如果我们把物体表面设想为一面镜子,那么镜面光照最强的地方就是我们看到表面上反射光的地方。你可以在下图中看到效果:
计算镜面反射分量
-
定义一个镜面强度
float specularStrength = 0.5;
-
计算视线和片段位置之间的向量
vec3 viewDir = normalize(viewPos - FragPos);
-
计算光线的镜面反射向量
vec3 reflectDir = reflect(-lightDir, norm);
reflect
函数是个内置函数,要求第一个向量是从光源指向片段位置的向量,但是lightDir
当前正好相反,是从片段指向光源(由先前我们计算lightDir
向量时,减法的顺序决定)。为了保证我们得到正确的reflect
向量,我们通过对lightDir
向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm
向量。 -
计算视线向量和镜面反射向量的点积
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
计算得到点乘(并确保不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。从上面的图片里,你会看到不同反光度的视觉效果影响。
-
计算镜面高光分量
vec3 specular = specularStrength * spec * lightColor;
另外:这些光照计算可以在 顶点着色器中实现。在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。
补充:半程向量的使用
参考 Blinn-Phong光照模型解析及其实现_晴夏。的博客-CSDN博客_blinn phong
相关的讲解在 GAMES101-现代计算机图形学入门-闫令琪 视频中学习
在镜面反射中,我们通过计算出射方向和视点的夹角来确定反射光的强度,在计算机中计算反射向量R的计算量比较大。因此我们可以用其他计算方法近似替代。
float specularStrength = 0.50;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 H_norm = normalize(lightDir + viewDir); // 使用半程向量,只是简单的加减就可以获取到
float spec = pow(max(dot(H_norm, norm), 0.0), 256);
vec3 specular = specularStrength * spec * lightColor;