当前位置: 代码迷 >> 综合 >> LearnGL - 11.1 - 实现简单的Phong光照模型
  详细解决方案

LearnGL - 11.1 - 实现简单的Phong光照模型

热度:80   发布时间:2024-01-29 23:56:07.0

文章目录

  • Phong
  • dot - 点积的作用
    • 图形了解顶点点积的作用
    • 漫反射
    • 高光
      • reflect - 反射高光方向
      • view - 观察方向
  • 实践
    • Shader - 着色器
  • 网格模型
  • References

LearnGL - 学习笔记目录

前一篇:LearnGL - 11 - 光与颜色前置篇

这篇:我们将对 Phong 光照模型实现一个简单的实现

本人才疏学浅,如有什么错误,望不吝指出。


Phong

在百度百科中也有简介:Phong光照模型

Phong 着色器模型,中文也叫:冯氏着色法,该光照模型是很简单的光照模型,只有:环境光、漫反射、高光。

  • 环境光 用于模拟物体反射周围的物体颜色,在 phong 中,只是纯颜色值作为环境光直接叠加。
  • 漫反射 用于模拟光入射到物体后的各种散射后,最后又从物体射出的光,在 phong 中,使用的是 lambert(兰伯特,或是half lambert半兰伯特) 漫反射模型,计算的是灯光方向与物体表面法线方向的夹角的余弦值。
  • 高光 用于模拟物体对射入光按物体表现法线方向做反射出去的光,一般有 phong 高光与 blinn-phong 高光模型。

计算公式,光照模型 I = i l l u m i n a t i o n I=illumination I = I a ? K a + i = 0 n ( I d ? K d ? d o t ( N , L i ) + I s ? K s ? p o w ( d o t ( r e f l e c t ( ? L i , N ) , V ) , G l o s s y ) I=I_a\cdot K_a+\sum_{i=0}^n(I_d\cdot K_d\cdot dot(N,L_i) + I_s\cdot K_s\cdot pow(dot(reflect(-L_i,N),V),Glossy)

其中:

  • I a I_a 是环境光,Phong 中这里就一个颜色值, K a K_a 是环境光强度系数
  • I d I_d 是漫反射颜色值, K d K_d 是漫反射强度系数, d o t ( N , L ) dot(N,L) 是lambert 漫反射模型,其中 N N 是物体表面法线单温方向, L i L_i 是物体表面指向第 i i 个灯光的一个方向,也叫灯光方向,也是个单位向量。
  • I s I_s 是高光颜色, K s K_s 是高光强度系数, ? L i -L_i 是灯光方向的反方向,即:灯光入射方向, r e f l e c t ( ? L i , N ) reflect(-L_i,N) 求的是入射方向在根据法线方向反弹的高光反射反向,再用该用 V V 视角方向(从顶点到相机/镜头/观察者的方向),再用反射向量于 V V 视角方向求点积值,最后 G l o s s y Glossy 是控制点积值的幂次。

上面的公式中,一般有些 Phong 光照是有限制 K d + K s = 1 K_d + K_s = 1 ,但我也看到过很多例子都不使用这个限制,我们可以在外部控制是否要有这个系数限制:只要 K d K_d K s K_s 都是1的分量系数即可。

这里头值得说明一下的是, d o t dot 的作用,在以前刚接触 shader 时,我根本不懂这个函数的作用,通过自己试验与网上资料查询后,才有所了解。


dot - 点积的作用

在图形学中, d o t dot 的作用一定要了解,这里为了完善 LearnGL 系列笔记,我将以前学习理解的 d o t dot 特性、本质才简单的描述一下。

百度百科:点积

这里面我不想太多抄袭其他的专业的公式来表达,我指向表达我在图形学中,什么情况下我会去用 d o t dot ,这样才能更加理解它的作用。如果你喜欢看公式,你可以点击上面的链接,如果还是不满足,就 google 或是 wiki 中了解。


图形了解顶点点积的作用

在这里插入图片描述

如上图,有两个单位向量 u \overrightarrow u v \overrightarrow v ,它们之前的夹角为 4 5 o 45^o ,然后 :
d o t ( u , v ) = u ? v = cos ? ( a ) = U d o t V = 0.71 dot(u,v)=u\cdot v=\cos(a)=UdotV=0.71

为何也等于 cos ? ( a ) \cos(a) 呢?

我们可以在上面说的百度百科中的公司: u ? v = u ? v ? cos ? ( a ) \overrightarrow u\cdot \overrightarrow v=|\overrightarrow u|\cdot |\overrightarrow v|\cdot \cos(a)

如果我们两个都是单位向量 u \overrightarrow u v \overrightarrow v ,它意味着向量长度为 1,即: u = 1 , v = 1 |\overrightarrow u| =1, |\overrightarrow v| =1

那么上面的公式就变成: u ? v = u ? v ? cos ? ( a ) ? u ? v = 1 ? 1 ? cos ? ( a ) ? u ? v = cos ? ( a ) \overrightarrow u\cdot \overrightarrow v=|\overrightarrow u|\cdot |\overrightarrow v|\cdot \cos(a) \longrightarrow \overrightarrow u\cdot \overrightarrow v=1 \cdot 1\cdot \cos(a) \longrightarrow \overrightarrow u\cdot \overrightarrow v=\cos(a)

cos ? ( a ) \cos(a) 可以理解为初中学的 = 余弦函数=\frac{邻边}{斜边}

在下图就可以理解为是 c o s ( a ) = A D v cos(a)=\frac{|AD|}{|\overrightarrow v|} A D |AD| 就是邻边, v |\overrightarrow v| 就是斜边,而 v = 1 |\overrightarrow v|=1 ,所以 c o s ( a ) = A D 1 = A D cos(a)=\frac{|AD|}{1}=|AD|

在这里插入图片描述

这个值有什么用呢?

我们通过GIF动态图了解一下这个值的规律:
在这里插入图片描述
可以看到: a = 0 o a=0^o ,点积值1, a = 9 0 p a=90^p ,点积值0。

在这里插入图片描述
a = 18 0 o a=180^o ,点积值-1。

通过这两点,可以总结: d o t dot 可以用于判断两个向量的方向相似程度,越相似值越接近1,垂直为0,越反向值接近-1。

这在我们计算 漫反射 与 高光都会用到: I = I a ? K a + i = 0 n ( I d ? K d ? d o t ( N , L i ) + I s ? K s ? p o w ( d o t ( r e f l e c t ( ? L i , N ) , V ) , G l o s s y ) I=I_a\cdot K_a+\sum_{i=0}^n(I_d\cdot K_d\cdot \red{dot(N,L_i)} + I_s\cdot K_s\cdot pow(\red{dot(reflect(-L_i,N),V)},Glossy)


漫反射

d o t ( N , L i ) \red{dot(N,L_i)} 说明, N N 法线 与 第 i i L i L_i 灯光方向 约相似,那么漫反射就越大
在这里插入图片描述

如,上图, N N 是法线方向, L L 是灯光方向,如果这两个向量反向越是相似,那么交点 I I 的漫反射值就越大。
在这里插入图片描述
如上图GIF,如果我把 L L 拉倒接近 N N 法线,那么 I I 交点就肯定越亮,这个是漫反射的特性,不过这里这么做都是 Phong 的经验模型,真正显示生活中的漫反射是非常复杂的,它是光输入物体内再各种吸收、折射、反射后又从物体内反射出来的光子,非常的复杂,我们只能模拟看起来比较像的效果。

所以漫反射我自己总结是:迎面的光,就是面向越是靠近光的越亮。


高光

是一种类似镜面反射的现象,用一张图表示的话,可以是这样的:
在这里插入图片描述

specular 就是镜面反射高光系数,但是 GGB 竟然没有pow,或是 power 次幂函数。

所以高光最终系数公式算法模型: S p e c u l a r = p o w ( d o t ( r e f l e c t ( N , L ) , V ) , g l o s s y ) Specular=pow(dot(reflect(N,L),V),glossy)

可以看到也有一个 d o t dot ,作用与前面的漫反射作用差不多,这里它是求,反射出来的 R R 高光方向与观察者(V,可以理解为眼睛的位置的方向)方向的相似度。意思,越是直接的照射到我们的眼睛的高光系数就越大、高光颜色越亮。


reflect - 反射高光方向

这在我们计算 漫反射 与 高光都会用到: I = I a ? K a + i = 0 n ( I d ? K d ? d o t ( N , L i ) + I s ? K s ? p o w ( d o t ( r e f l e c t ( ? L i , N ) , V ) , G l o s s y ) I=I_a\cdot K_a+\sum_{i=0}^n(I_d\cdot K_d\cdot \red{dot(N,L_i)} + I_s\cdot K_s\cdot pow(\red{dot(reflect(-L_i,N),V)},Glossy) 中的,高光: d o t ( r e f l e c t ( ? L i , N ) , V ) \red{dot(reflect(-L_i,N),V)} ,这里不管后面的 pow,它是用来调整光泽度的,glossy 就是光泽度的意思。

高光反向是使用 reflect GLSL 函数来实现的。

reflect 函数的算法是: ? L i + 2 ? N ? d o t ( N , L i ) -L_i+2 \cdot N \cdot dot(N,L_i)

画个图会好理解一些:

先是已知: N N L L
在这里插入图片描述

先是 ? L i -L_i (就是图中的L)

在这里插入图片描述

然后是 2 ? N 2 \cdot N ,但这里先不将它,我们先将后面的 d o t ( N , L ) dot(N,L) ,还记得它前面说的么? d o t dot 就是求 N , L N,L 量向量的相似度,但这里不是这么理解来使用的,它是当作 cos ? ( a ) \cos(a) 来使用的,它求的是什么?还是再画个图来理解吧:
在这里插入图片描述

d o t ( N , L ) dot(N,L) 求的就是 L L N N 上的投影长度,它也是 cos ? ( a ) \cos(a) ( ˙ N , L ) = = N ? L ? cos ? ( a ) \dot(N,L)=\frac{邻边}{斜边}=|\overrightarrow N|\cdot |\overrightarrow L| \cdot \cos(a) ,因为 N , L \overrightarrow N ,\overrightarrow L 都是1,因为都是单位向量,所以 ( ˙ N , L ) = = cos ? ( a ) \dot(N,L)=\frac{邻边}{斜边}=\cos(a) ,所以我们 d o t ( N , L ) dot(N,L) 求的是 L L N N 方向投影的长度,这个长度值用来缩放 N N 向量,那么如下图:
在这里插入图片描述

这个现在与我们的反射角度差不多了,在加多一个 N ? d o t ( N , L ) N\cdot dot(N,L) 看看会怎么样?如下图:
在这里插入图片描述

所以现在已经求出了反射向量了,只要我们将原点与这个 ? L + N ? d o t ( N , L ) + N ? d o t ( N , L ) -L+N \cdot dot(N,L)+N \cdot dot(N,L) 的点相连就是反射向量了,如下图:
在这里插入图片描述
然后我们的结果是: ? L + N ? d o t ( N , L ) + N ? d o t ( N , L ) -L+N \cdot dot(N,L)+N \cdot dot(N,L) ,后面部分有相同的相加,调整为乘法: ? L + 2 ? N ? d o t ( N , L ) -L+2\cdot N \cdot dot(N,L) ,那么这个结果就与我们之前的列出的reflect公式一模一样了。


view - 观察方向

求的了 reflect 的高光反射方向,我们就可以使用 RV(View,观察方向)来求相似度,还记得前面重复强调的 dot 是用来求量向量的相似度的吧?这里我们也是使用它来求 R 与 V 的的相似度。

它们越相似,说明反射光越是直接照射到我们眼球看向的方向的反方向。

留意这个值:
在这里插入图片描述

先看观察位置不变,只改变灯光方向的Specular 高光值效果,看下面的GIF图:
在这里插入图片描述
再看看,只改变观察方向的Specular 高光值效果,看下面的GIF图:
在这里插入图片描述

总结就是如我们上面所述的:只要 R 与 V 方向相似度越高,Specular 高光越大。

实践

先看效果图,下面是实时调整平行方向光的 方向强度灯光颜色
在这里插入图片描述
从 Phong 光照模型公司可得知: I = I a ? K a + i = 0 n ( I d ? K d ? d o t ( N , L i ) + I s ? K s ? p o w ( d o t ( r e f l e c t ( ? L i , N ) , V ) , G l o s s y ) I=I_a\cdot K_a+\sum_{i=0}^n(I_d\cdot K_d\cdot dot(N,L_i) + I_s\cdot K_s\cdot pow(dot(reflect(-L_i,N),V),Glossy) ,除了有漫反射、高光,还有一个:环境光。

环境光的计算非常简单,只是颜色*强度的结果相加到最终颜色的输出即可,具体可以参考下面的 Shader 代码

Shader - 着色器

这里主要看气球猫的 Shader 吧:

// jave.lin - testing_load_balloon_cat_mesh_shading.vert
#version 450 compatibility// camera uniform
uniform vec3 _CamWorldPos;	// 镜头世界坐标// scene uniform
uniform vec4 _Ambient;		// .xyz 环境光颜色, .w 环境光系数// object uniform
uniform float Glossy;		// 光滑度
uniform vec3 DiffuseK;		// 漫反射系数
uniform vec3 SpecularK;		// 高光系数// light uniform
uniform vec4 LightPos;		// 灯光世界坐标位置,w==0,或名是方向光,w==1说明是点光源,w == 0.5 是聚光灯
uniform vec4 LightColor;	// 灯光颜色,.xyz 顔色,.w 强度
// uniform vec3 LightDir; // 灯光类型为聚光灯的方向// transform matrix uniform
uniform mat4 mMat; 			// m.v.p 矩阵
uniform mat4 vMat; 
uniform mat4 pMat;
uniform mat4 I_mMat;		// model matrix 的逆矩阵// vertex data
attribute vec3 vPos;		// 顶点坐标
attribute vec2 vUV0;		// 顶点纹理坐标
attribute vec3 vNormal;		// 顶点法线// vertex data - interpolation
varying vec2 fUV0;			// 给 fragment shader 传入的插值
varying vec3 fAmbient;		// 环境光
varying vec3 fDiffuse; 		// 漫反射颜色
varying vec3 fSpecular; 	// 高光颜色// 将对象空间的法线转换到世界空间下的法线
vec3 ObjectToWorldNormal(vec3 n) {n = (vec4(n,0) * I_mMat).xyz;	// 等价于:transpose(I_mMat) * vec4(n, 0)return normalize(n);
}void main() {vec4 h_vPos = vec4(vPos, 1.0);	// 齐次坐标vec4 worldPos = mMat * h_vPos;	// 世界坐标vec3 viewDir = normalize(_CamWorldPos - worldPos.xyz); 	// 顶点坐标 指向 镜头坐标 的方向vec3 worldNormal = ObjectToWorldNormal(vNormal);		// 获取世界坐标下的法线// phong shading// ambientfAmbient = _Ambient.xyz * _Ambient.w;if (LightPos.w == 0) {// 下面使用的是Phong 光照模型// 如果是方向光,那么 LightPos.xyz 是灯光方向的反方向fDiffuse = LightColor.rgb * LightColor.w * DiffuseK * max(0, dot(LightPos.xyz, worldNormal)); // lambert// diffuse = LightColor * DiffuseK * max(0, dot(LightPos.xyz, worldNormal)) * 0.5 + 0.5; // half-lambert : -1~1 to 0~1fSpecular = max(vec3(0, 0, 0), fDiffuse) * LightColor.rgb * LightColor.w * SpecularK * pow(dot(reflect(LightPos.xyz, worldNormal), viewDir), Glossy);} else {// 点光源 或是 聚光灯if (LightPos.w == 1) {// 点光} else { // LightPos.w == 0.5,即:LightPos.w !=0 && LightPos.w != 1// 聚光灯}}// uv0fUV0 = vUV0;gl_Position = pMat * vMat * worldPos;
}// jave.lin - testing_load_balloon_cat_mesh_shading.frag
#version 450 compatibility// interpolation - 插值数据
varying vec2 fUV0;					// uv 坐标
varying vec3 fAmbient;				// 环境光
varying vec3 fDiffuse; 				// 漫反射颜色
varying vec3 fSpecular; 			// 高光颜色// local uniform
uniform sampler2D main_tex;			// 主纹理void main() {vec3 mainCol 	= texture(main_tex, fUV0).rgb;gl_FragColor 	= vec4(fAmbient + mainCol * fDiffuse + fSpecular, 1.0);
}

网格模型

这里的为了更好的观察光照效果,我就提前先将网格模型加载提前完整。

使用的是我之前在实现 用C# Bitmap作为画布写个3D软渲染器 的模型来作为练习用,分别是:球体气球猫

这两个模型我是从 Unity 资源中导出来的,在 Unity 里写了 CSharp 脚本,将Mesh的Vertex, Indices,UV, Color, Normal, Tangent导出到一个 *.m 的自定义文件,这个 m 可以理解为:model ,模型的意思,我把它放到了 github 上,分别是:

  • BalloonStupidCat_637003750150312129.m - Unity Demo 的 气球猫
  • Sphere_637003627289014299.m - Unity 的 primitive type mode - sphere 球体

模型*.m格式目前只支持三角形,够用就行,文件格式粗略说明:

  • 纯文本内容
  • 每个数据段会以:#label:count 的格式来说明后续的数据意义
    • label 是数据标签名字,可以是:
      • vertices 顶点数据
      • indices 索引值
      • colors 顶点颜色
      • uv 纹理坐标
      • normals 顶点法线
      • tangents 顶点切线
    • count 表示的就是该数据段有多少数量

例如,我手写一个三角形模型的结构如下:
(下面的切线我就没有归一化,通常我们加载后最后每个向量都归一化处理)

#vertices:3
-0.5,-0.5,0.0
0.5,-0.5,0.0
0.0,0.5,0.0
#indices:3
0,1,2
#colors:3
1.0,0.0,0.0,1.0
0.0,1.0,0.0,1.0
0.0,0.0,1.0,1.0
#uv:3
0.0,0.0
1.0,0.0
0.5,1.0
#normals:3
0.0,0.0,1.0
0.0,0.0,1.0
0.0,0.0,1.0
#tangents:3
0.0,1.0,0.0
-0.5,1.0,0.0
-0.5,-1.0,0.0

网格模型的读取:在光栅化渲染器中的 CSharp 代码有,在静态类 ModelReaderpublic static void ReadOut(string file, out Mesh mesh)可以看到是如何读取网格的。

在 LearnGL 中,读取 *.m 网格模型的函数,我也写了个 C++ 版本的。

References

  • 颜色
  • 基础光照