文章目录

  • Gouraud
  • dot - 点积的作用
    • 图形了解顶点点积的作用
    • 漫反射
      • 纯漫反射效果
        • Diffuse - Shader
        • GLSL 的中奇怪的问题
    • 高光
      • reflect - 反射高光方向
      • GLSL 中的公式的不同
      • view - 观察方向
      • 纯高光效果
        • Specular - Shader
  • 实践
    • Normal - 法线
      • 关于法线从对象空间转换到世界空间
    • diffuse effect - 漫反射效果
    • Shader - 着色器
    • Shader 中的 uniform
  • 网格模型
    • Assimp Lib
    • .obj file format - .obj 文件格式
  • References

LearnGL - 学习笔记目录

前一篇:LearnGL - 11 - 光与颜色前置篇 了解了光与颜色的基本概念。

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

因为一年前在 Unity 写过 Gouraud 与 Phong 的文章,时隔一年我又忘记了他们的叫法,之前把它们又搞混了名字,QAQ,所以下面有些图片显示:Phong 描述

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


Gouraud

Gouraud Shading,也叫:高洛德着色模型,该光照模型是很简单的光照模型,只有:环境光、漫反射、高光。

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

计算公式,光照模型I=illuminationI=illuminationI=illumination:I=Ia⋅Ka+∑i=0n(Id⋅Kd⋅dot(N,Li)+Is⋅Ks⋅pow(dot(reflect(−Li,N),V),Glossy))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=Ia​⋅Ka​+∑i=0n​(Id​⋅Kd​⋅dot(N,Li​)+Is​⋅Ks​⋅pow(dot(reflect(−Li​,N),V),Glossy))

其中:

  • IaI_aIa​ 是环境光,Gouraud 中这里就一个颜色值,KaK_aKa​ 是环境光强度系数
  • IdI_dId​ 是漫反射颜色值,KdK_dKd​ 是漫反射强度系数,dot(N,L)dot(N,L)dot(N,L)是lambert 漫反射模型,其中NNN是物体表面法线单温方向,LiL_iLi​ 是物体表面指向第iii个灯光的一个方向,也叫灯光方向,也是个单位向量。
  • IsI_sIs​ 是高光颜色,KsK_sKs​ 是高光强度系数,−Li-L_i−Li​ 是灯光方向的反方向,即:灯光入射方向,reflect(−Li,N)reflect(-L_i,N)reflect(−Li​,N)求的是入射方向在根据法线方向反弹的高光反射反向,再用该用VVV视角方向(从顶点到相机/镜头/观察者的方向),再用反射向量于VVV视角方向求点积值,最后GlossyGlossyGlossy是控制点积值的幂次。

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

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


dot - 点积的作用

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

百度百科:点积

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


图形了解顶点点积的作用

如上图,有两个单位向量 u→\overrightarrow uu 和 v→\overrightarrow vv,它们之前的夹角为 45o45^o45o,然后 :
dot(u,v)=u⋅v=cos⁡(a)=UdotV=0.71dot(u,v)=u\cdot v=\cos(a)=UdotV=0.71dot(u,v)=u⋅v=cos(a)=UdotV=0.71

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

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

如果我们两个都是单位向量 u→\overrightarrow uu 和 v→\overrightarrow vv,它意味着向量长度为 1,即:∣u→∣=1,∣v→∣=1|\overrightarrow u| =1, |\overrightarrow v| =1∣u∣=1,∣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)u⋅v=∣u∣⋅∣v∣⋅cos(a)⟶u⋅v=1⋅1⋅cos(a)⟶u⋅v=cos(a)

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

在下图就可以理解为是 cos(a)=∣AD∣∣v→∣cos(a)=\frac{|AD|}{|\overrightarrow v|}cos(a)=∣v∣∣AD∣​,∣AD∣|AD|∣AD∣ 就是邻边,∣v→∣|\overrightarrow v|∣v∣就是斜边,而∣v→∣=1|\overrightarrow v|=1∣v∣=1,所以 cos(a)=∣AD∣1=∣AD∣cos(a)=\frac{|AD|}{1}=|AD|cos(a)=1∣AD∣​=∣AD∣

这个值有什么用呢?

我们通过GIF动态图了解一下这个值的规律:

可以看到:a=0oa=0^oa=0o,点积值1,a=90pa=90^pa=90p,点积值0。


a=180oa=180^oa=180o,点积值-1。

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

这在我们计算 漫反射 与 高光都会用到:I=Ia⋅Ka+∑i=0n(Id⋅Kd⋅dot(N,Li)+Is⋅Ks⋅pow(dot(reflect(−Li,N),V),Glossy))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))I=Ia​⋅Ka​+∑i=0n​(Id​⋅Kd​⋅dot(N,Li​)+Is​⋅Ks​⋅pow(dot(reflect(−Li​,N),V),Glossy))


漫反射

dot(N,Li)\red{dot(N,L_i)}dot(N,Li​) 说明,NNN 法线 与 第iii 个 LiL_iLi​ 灯光方向 越相似,那么漫反射就越大

如,上图,NNN 是法线方向,LLL 是灯光方向,如果这两个向量反向越是相似,那么交点III 的漫反射值就越大。

如上图GIF,如果我把LLL 拉倒接近 NNN 法线,那么 III 交点就肯定越亮,这个是漫反射的特性,不过这里这么做都是 Gouraud-Phong 的经验模型,真正显示生活中的漫反射是非常复杂的,它是光输入物体内再各种吸收、折射、反射后又从物体内反射出来的光子,非常的复杂,我们只能模拟看起来比较像的效果。

所以漫反射我自己总结是:迎面的光,就是表面的法线方向越是靠近光源的方向,则该表面越亮。


纯漫反射效果


Diffuse - Shader

// jave.lin - testing_gouraud_phong_only_diffuse_shading.vert
#version 450 compatibility// light uniform
uniform vec4 LightPos;      // 灯光世界坐标位置,w==0,或名是方向光,w==1说明是点光源,w == 0.5 是聚光灯// transform matrix uniform
uniform mat4 mMat;          // m.v.p 矩阵
uniform mat4 vMat;
uniform mat4 pMat;
uniform mat4 IT_mMat;       // model matrix 的逆矩阵的转置// vertex data
attribute vec3 vPos;        // 顶点坐标
attribute vec3 vNormal;     // 顶点法线// vertex data - interpolation
varying vec3 fCol;          // 片段插值颜色// 将对象空间的法线转换到世界空间下的法线
vec3 ObjectToWorldNormal(vec3 n) {return normalize(mat3(IT_mMat) * n);  // 等价于:transpose(I_mMat) * vec4(n, 0)
}void main() {vec4 worldPos = mMat * vec4(vPos, 1.0);                  // 世界坐标vec3 worldNormal = ObjectToWorldNormal(vNormal);        // 获取世界坐标下的法线float LdotN = dot(LightPos.xyz, worldNormal);fCol = vec3(LdotN);// uv0gl_Position = pMat * vMat * worldPos;
}
// jave.lin - testing_phong_only_diffuse_shading.frag
#version 450 compatibility// interpolation - 插值数据
varying vec3 fCol;          // 片段插值颜色void main() {gl_FragColor  = vec4(fCol, 1.0);
}

GLSL 的中奇怪的问题

可能你会留意到,我在 ObjectToWorldNormal 函数中注释到:normalize(mat3(IT_mMat) * n) != normalize((IT_mMat * vec4(n,0))) ,这个结果真的是服了。

vec3 ObjectToWorldNormal(vec3 n) {// return normalize((IT_mMat * vec4(n,0))).xyz;    // 等价于:transpose(I_mMat) * vec4(n, 0)// 下面 normalize(mat3(IT_mMat) * n) 的结果才是正确的, normalize((IT_mMat * vec4(n,0))).xyz 的不正确// 意思:normalize(mat3(IT_mMat) * n) != normalize((IT_mMat * vec4(n,0))).xyz ,我服了!!!return normalize(mat3(IT_mMat) * n); // 等价于:transpose(I_mMat) * vec4(n, 0)
}

这个问题,我在 Unity ShaderLabel 可是没有出现这类问题,Unity ShaderLabel 的shader 是类似 HLSL 的,编写起来,比 GLSL 舒服多了。
这个问题,我单独用一篇记录了一下:GLSL 中奇怪的问题 - [已解决] - 自己马虎的问题

这个是我自己的问题,因为在 C++ 层复制代码若的祸,QAQ,因为我在 C++ 有些类似的写法,因为 C++ 里头一次 mat3 转换性能还不如果世界 mat4 * vec4(vec3,0) 的高。

首先:(IT_mMat * vec4(n,0) 求出来的xyzw,中第四个w分量必然是0,这时候再去归一化,就会将第四个分量都算进去,我去!!!竟然写得这么马虎,我服了我自己!


高光

是一种类似镜面反射的现象,用一张图表示的话,可以是这样的:

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

所以高光最终系数公式算法模型:Specular=pow(dot(reflect(−L,N),V),glossy)Specular=pow(dot(reflect(-L,N),V),glossy)Specular=pow(dot(reflect(−L,N),V),glossy)

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


reflect - 反射高光方向

这在我们计算 漫反射 与 高光都会用到:I=Ia⋅Ka+∑i=0n(Id⋅Kd⋅dot(N,Li)+Is⋅Ks⋅pow(dot(reflect(−Li,N),V),Glossy))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))I=Ia​⋅Ka​+∑i=0n​(Id​⋅Kd​⋅dot(N,Li​)+Is​⋅Ks​⋅pow(dot(reflect(−Li​,N),V),Glossy)) 中的,高光:dot(reflect(−Li,N),V)\red{dot(reflect(-L_i,N),V)}dot(reflect(−Li​,N),V),这里不管后面的 pow,它是用来调整光泽度的,glossy 就是光泽度的意思。

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

这里的推导 reflect 公式为:−Li+2⋅N⋅dot(N,Li)-L_i+2 \cdot N \cdot dot(N,L_i)−Li​+2⋅N⋅dot(N,Li​),但是 GLSL 中 reflect 的公式与我这里列出的不太一样,其实我觉我这个会更方便理解。而且一般处于性能优化,会调整运算元素的位置,原则是:分量相同先处理,例如:−Li+2⋅N⋅dot(N,Li)-L_i+2 \cdot N \cdot dot(N,L_i)−Li​+2⋅N⋅dot(N,Li​) 和可能会调整为:−Li+2⋅dot(N,Li)⋅N-L_i+2 \cdot dot(N,L_i) \cdot N−Li​+2⋅dot(N,Li​)⋅N 因为 2⋅dot(N,Li)\red{2 \cdot dot(N,L_i)}2⋅dot(N,Li​) 都是1分量的标量,先运算好它们的结果再与 NNN 向量相乘,运算量即可减少。

GLSL 中的 reflect 的第一个参数是入射角,而我这里的公式是 光源方向。

先画个图会好理解一些:

已知:NNN、LLL

先是 LiL_iLi​(就是图中的L)

然后是2⋅N2 \cdot N2⋅N,但这里先不管它,先讲后面的 dot(N,L)dot(N,L)dot(N,L),还记得它前面说的么?dotdotdot就是求N,LN,LN,L量向量的相似度,但这里不是这么理解来使用的,它是当作 cos⁡(a)\cos(a)cos(a) 来使用的,它求的是什么?还是再画个图来理解吧:

dot(N,L)dot(N,L)dot(N,L) 求的就是 LLL 在 NNN 上的投影长度,它也是 cos⁡(a)\cos(a)cos(a),dot(N,L)=邻边斜边=∣N→∣⋅∣L→∣⋅cos⁡(a)dot(N,L)=\frac{邻边}{斜边}=|\overrightarrow N|\cdot |\overrightarrow L| \cdot \cos(a)dot(N,L)=斜边邻边​=∣N∣⋅∣L∣⋅cos(a),邻边就是 ∣N→∣|\overrightarrow N|∣N∣ 斜边就是 ∣L→∣|\overrightarrow L|∣L∣,因为 ∣N→∣,∣L→∣|\overrightarrow N| ,|\overrightarrow L|∣N∣,∣L∣都是1,因为都是单位向量,所以 dot(N,L)=邻边斜边=cos⁡(a)dot(N,L)=\frac{邻边}{斜边}=\cos(a)dot(N,L)=斜边邻边​=cos(a),所以我们dot(N,L)dot(N,L)dot(N,L)求的是 LLL 在 NNN 方向投影的长度,这个长度值用来缩放NNN向量,那么如下图:

这个现在与我们的反射角度差不多了,在加多一个 N⋅dot(N,L)N\cdot dot(N,L)N⋅dot(N,L)看看会怎么样?如下图:

所以现在已经求出了反射向量了,只要我们将原点与这个−L+N⋅dot(N,L)+N⋅dot(N,L)-L+N \cdot dot(N,L)+N \cdot dot(N,L)−L+N⋅dot(N,L)+N⋅dot(N,L) 的点相连就是反射向量了,如下图:

然后我们的结果是:−L+N⋅dot(N,L)+N⋅dot(N,L)-L+N \cdot dot(N,L)+N \cdot dot(N,L)−L+N⋅dot(N,L)+N⋅dot(N,L),后面部分有相同的相加,调整为乘法:−L+2⋅N⋅dot(N,L)-L+2\cdot N \cdot dot(N,L)−L+2⋅N⋅dot(N,L),那么这个结果就与我们之前的列出的 reflect 公式一模一样了。


GLSL 中的公式的不同

但是 GLSL 中的 reflect(-L,N) 总的公式结果与上面的一致,但是公式不一样
GLSL 中的 reflect(I,N)reflect(I, N)reflect(I,N),III是入射角,也就是我们上面的 −L-L−L 或是 −Li-L_i−Li​

  • 我这里的公式是:−L+2⋅N⋅dot(N,L)-L+2\cdot N \cdot dot(N,L)−L+2⋅N⋅dot(N,L)
  • GLSL 中公式是:I−2⋅dot(N,I)⋅NI-2 \cdot dot(N,I) \cdot NI−2⋅dot(N,I)⋅N,其中 I=−LI = -LI=−L
    (也可以参考 CG 的 reflect)

但是结果是一致的,我这里介绍的是灯光方向,GLSL 需要传入的是 光源入射方向,所以我们在使用 GLSL 中的 reflect 需要注意,使用的是 光源入射方向

但是推导过程原理是一样的,过程的插图我就不发了,有兴趣同学可以自己去尝试。

在 shader 中可以这么使用:

// 第一个参数注意不是入射角
vec3 my_reflect(vec3 L, vec3 N) {// return -L + 2 * N * dot(N, L);return -L + 2 * dot(N, L) * N; // 优化调整分量相同先乘
}// vec3 R = reflect(-LightPos.xyz, worldNormal);  // GLSL 内置的 reflect,注意第一个参数是入射角
vec3 R = my_reflect(LightPos.xyz, worldNormal);    // 这个是我们根据上面推导的过程编写的

view - 观察方向

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

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

留意这个值:

先看观察位置不变,只改变灯光方向的Specular 高光值效果,看下面的GIF图:

再看看,只改变观察方向的Specular 高光值效果,看下面的GIF图:

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


纯高光效果


Specular - Shader

// jave.lin - testing_gouraud_phong_only_specular_shading.vert
#version 450 compatibility
// camera uniform
uniform vec3 _CamWorldPos;  // 镜头世界坐标
// object uniform
uniform float Glossy;       // 光滑度
// light uniform
uniform vec4 LightPos;      // 灯光世界坐标位置,w==0,或名是方向光,w==1说明是点光源,w == 0.5 是聚光灯
// transform matrix uniform
uniform mat4 mMat;          // model matrix
uniform mat4 mvpMat;        // m.v.p 矩阵
uniform mat4 IT_mMat;       // model matrix 的逆矩阵的转置
// vertex data
attribute vec3 vPos;        // 顶点坐标
attribute vec3 vNormal;     // 顶点法线
// vertex data - interpolation
varying vec3 fCol;          // 片段插值颜色
// 将对象空间的法线转换到世界空间下的法线
vec3 ObjectToWorldNormal(vec3 n) {return normalize(mat3(IT_mMat) * n);  // 等价于:transpose(I_mMat) * vec4(n, 0)
}
vec3 my_reflect(vec3 L, vec3 N) {// return -L + 2 * N * dot(N, L);return -L + 2 * dot(N, L) * N; // 优化调整分量相同先乘
}void main() {vec3 worldPos         = (mMat * vec4(vPos, 1.0)).xyz;                // 世界坐标vec3 worldNormal     = ObjectToWorldNormal(vNormal);                // 获取世界坐标下的法线vec3 viewDir       = normalize(_CamWorldPos - worldPos);      // 顶点坐标 指向 镜头坐标 的方向float LdotN      = dot(LightPos.xyz, worldNormal);// vec3 R                 = reflect(-LightPos.xyz, worldNormal); // 注意内置的是用入射角vec3 R                 = my_reflect(LightPos.xyz, worldNormal);   // 注意我们自己编写的 reflect 是灯光方向float RdotV       = max(0, dot(R, viewDir));float S = 0;if (LdotN > 0) {S = pow(RdotV, Glossy);}fCol = vec3(S);// uv0gl_Position = mvpMat * vec4(vPos, 1.0);
}// jave.lin - testing_gouraud_phong_only_specular_shading.frag
#version 450 compatibility// interpolation - 插值数据
varying vec3 fCol;          // 片段插值颜色void main() {gl_FragColor  = vec4(fCol, 1.0);
}

实践

先看效果图,下面是实时调整平行方向光的 方向强度灯光颜色

从 Gouraud 光照模型公司可得知:I=Ia⋅Ka+∑i=0n(Id⋅Kd⋅dot(N,Li)+Is⋅Ks⋅pow(dot(reflect(−Li,N),V),Glossy))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=Ia​⋅Ka​+∑i=0n​(Id​⋅Kd​⋅dot(N,Li​)+Is​⋅Ks​⋅pow(dot(reflect(−Li​,N),V),Glossy)),除了有漫反射、高光,还有一个:环境光。

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


Normal - 法线

上面所说的法线(Normal),是怎么来的呢,在我的 Cube 的模型中,我们是通过程序计算出来的,我们可以通过三角面的三个点来计算出发现,这与左右手坐标有光,也与三角面顶点顺序有光。

发现如何求得,可以查考我之前的文章:

  • Unity Shader - Billboard 广告板/广告牌 - 向量叉乘顺序

(其实我之前的软光栅器也有类似的处理)

在 C++ 的 LearnGL 系列笔记的项目中,我也重写了一个版本:

 void Mesh::recalculateNormal() {if (_vertices == NULL || _indices == NULL) return;DESTROY(_normals);size_t pos_count = _vertices->size() / 3;size_t arr_count = pos_count * 3;GLfloat* normals_data = (GLfloat*)malloc(sizeof(GLfloat) * arr_count);//GLfloat* normals_data = new GLfloat[arr_count];// fill normals here,目前这里只支持三角形vec3 pos0, pos1, pos2;for (size_t i = 0; i < _indices->size(); i += 3) {GLuint idx0 = _indices->at(i + 0);GLuint idx1 = _indices->at(i + 1);GLuint idx2 = _indices->at(i + 2);pos0.x = _vertices->at(idx0 * 3 + 0);pos0.y = _vertices->at(idx0 * 3 + 1);pos0.z = _vertices->at(idx0 * 3 + 2);pos1.x = _vertices->at(idx1 * 3 + 0);pos1.y = _vertices->at(idx1 * 3 + 1);pos1.z = _vertices->at(idx1 * 3 + 2);pos2.x = _vertices->at(idx2 * 3 + 0);pos2.y = _vertices->at(idx2 * 3 + 1);pos2.z = _vertices->at(idx2 * 3 + 2);vec3 normal = glm::normalize(glm::cross(pos1 - pos0, pos2 - pos0));memcpy(normals_data + (idx0 * 3), glm::value_ptr(normal), sizeof(vec3));memcpy(normals_data + (idx1 * 3), glm::value_ptr(normal), sizeof(vec3));memcpy(normals_data + (idx2 * 3), glm::value_ptr(normal), sizeof(vec3));//std::cout << "idx0 : " << idx0 << ", pos0 : " << pos0.x << "," << pos0.y << "," << pos0.z << "\n";//std::cout << "idx1 : " << idx1 << ", pos1 : " << pos1.x << "," << pos1.y << "," << pos1.z << "\n";//std::cout << "idx2 : " << idx2 << ", pos2 : " << pos2.x << "," << pos2.y << "," << pos2.z << "\n";//std::cout << "normals : " << normal.x << "," << normal.y << "," << normal.z << "\n";//std::cout << "data : ";//std::cout << "idx : " << idx0 << ", normal data : ";//for (size_t j = 0; j < 3; j++) {//    std::cout << *(normals_data + (idx0 * 3) + j) << ",";//}//std::cout << "idx : " << idx1 << ", normal data : ";//for (size_t j = 0; j < 3; j++) {//  std::cout << *(normals_data + (idx1 * 3) + j) << ",";//}//std::cout << "idx : " << idx2 << ", normal data : ";//for (size_t j = 0; j < 3; j++) {//  std::cout << *(normals_data + (idx2 * 3) + j) << ",";//}//std::cout << "\n";}_normals = new FixedArray<GLfloat>(normals_data, arr_count);free(normals_data);//delete[] normals_data;}

外部应用 recalculateNormal 如下:

先加载好指定的模型后,判断如果没有法线数据(mesh->normals_count == 0),则使用上面 API recalculateNormal 来计算。

用图片来表达,形象的理解为:一个三角面肯定有三个点,用这个切面上的三个点,组成两个切面上的向量,再通过 cross 叉乘求出法线。

而在一些 *.m 文件中加载出来的模型如果有发现的都不会再去重新生成法线,而且一般的 3D 模型中的法线是有处理过平滑的(顶点之间的插值再导出来的)

有了法线,就可以实现上面所说的光照模型的计算了

关于法线从对象空间转换到世界空间

可以参考这篇:顶点法向量从物体坐标系变换到世界坐标系,其实里头的公式推导的挺好理解的,但是我想写个更好理解的

因为内容边幅也不少,所以还是独立出一篇来说明会比较容易理解:LearnGL - 11.5 - 矩阵04 - 法线从对象空间变换到世界空间


diffuse effect - 漫反射效果

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 IT_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) {return normalize(mat3(IT_mMat) * n);  // 等价于:transpose(I_mMat) * vec4(n, 0)
}void main() {vec4 h_vPos = vec4(vPos, 1.0);   // 齐次坐标vec4 worldPos = mMat * h_vPos;  // 世界坐标vec3 viewDir = normalize(_CamWorldPos - worldPos.xyz);  // 顶点坐标 指向 镜头坐标 的方向vec3 worldNormal = ObjectToWorldNormal(vNormal);        // 获取世界坐标下的法线// 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.rgb * LightColor.w * DiffuseK * (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);
}

Shader 中的 uniform

可以看到 材质 在 GLSL 中使用了 struct 结构体,为了更方便的对数据分类的话,还是挺有用的

struct Material {vec3 ambient;vec3 diffuse;vec3 specular;float shininess;
}; uniform Material material;

然后在 C++ 层可以使用 "data_struct_name.member_name" 的方式来设置,如下:

shader->setVec3("material.ambient",     vec3(1.0f,1.0f,1.0f));
shader->setVec3("material.diffuse",    vec3(0.5f,0.5f,0.5f));
shader->setVec3("material.specular",   vec3(0.5f,0.5f,0.5f));
shader->setFloat("material.shininess",     32.0f);

而上面我的 shader 中,都是没个独立的 uniform 为了书写方便而已,也因为之前使用 Unity ShaderLab 的习惯影响


网格模型

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

使用的是我之前在实现 用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 model - 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++ 版本的。

Assimp Lib

Assimp 一个开源的资源导入库,还没用过,给有需要的人看看

.obj file format - .obj 文件格式

你可以在 Wiki 中查看这个格式:Wavefront .obj file

注意是 Wavefront Technology 公司发明的 3D 模型格式。

说起 Wavefront Technology 公司,我以前了解并不多,但是最近看到它要告 华为 公司,说是使用了 .jpge, .png, 压缩技术,等等很多杂七杂八的技术专利没给专利费。

这家公司被 IT 业内称为:专利流氓公司,靠专利费活下来的。

google、microsoft 都被告过专利费。

如果你要自己手写一个加载 .obj 文件的话,也可以查看此文件格式说明。但是如果你以后开公司了,发大财了,就要小心使用了 Wavefront 的所有技术专利问题。


References

  • 颜色
  • 基础光照
  • 材质
  • 网格、模型
    • Assimp - 一个开源的资源导入库,还没用过,给有需要的人看看
    • 网格 - 我没参考他的封装,在编写此文章前,都是自己封装的,这时阅读到后面的文章时,发现他的 LearnGL 系列也有说明
    • 模型 - 演示使用 Assimp 实现了对一些 *.fbx, *.obj,等模型资源的加载,并带有对 *.mtl 材质处理,貌似都是 Phong 光照模型的材质系统的,这些我暂时没用上,我还是觉得自己封装的使用起来会顺手很多
  • 顶点法向量从物体坐标系变换到世界坐标系

LearnGL - 11.1 - 实现简单的Gouraud光照模型 dot 点积/点乘的作用相关推荐

  1. 【转】正则表达式简介及在C++11中的简单使用教程

    [转]正则表达式简介及在C++11中的简单使用教程 正则表达式Regex(regular expression)是一种强大的描述字符序列的工具.在许多语言中都存在着正则表达式,C++11中也将正则表达 ...

  2. 安卓APP_ 控件(11)webView —— 简单应用:显示网页

    摘自:安卓APP_ 控件(11)webView -- 简单应用:显示网页 作者:丶PURSUING 发布时间: 2021-05-11 11:50:52 网址:https://blog.csdn.net ...

  3. DIV布局——仿英雄联盟LOL首页(11页) 大学生简单个人静态HTML网页设计作品 DIV布局个人介绍网页模板代码 DW学生个人网站制作成品下载

    HTML5期末大作业:仿英雄联盟网站设计--仿英雄联盟LOL首页(11页) 大学生简单个人静态HTML网页设计作品 DIV布局个人介绍网页模板代码 文章目录 HTML5期末大作业:仿英雄联盟网站设计- ...

  4. HTML5期末大作业:仿英雄联盟网站设计——仿英雄联盟LOL首页(11页) 大学生简单个人静态HTML网页设计作品 DIV布局个人介绍网页模板代码 DW学生个人网站制作成品下载

    HTML5期末大作业:仿英雄联盟网站设计--仿英雄联盟LOL首页(11页) 大学生简单个人静态HTML网页设计作品 DIV布局个人介绍网页模板代码 DW学生个人网站制作成品下载 常见网页设计作业题材有 ...

  5. 简单一文带你读懂Java变量的作用和三要素

    Java变量的作用 不只是java,在其他的编程语言中变量的作用只有一个:存储值(数据) 在java中,变量本质上是一块内存区域,数据存储在java虚拟机(JVM)内存中 变量的三要素 变量的三要素分 ...

  6. JZOJ 5483. 【清华集训2017模拟11.26】简单路径

    Description 给定一棵带边权的树,选择两条没有公共边的简单路径(长度可以为0),使得所有在任意一条路径上的边的异或和尽量大. Input 第一行一个数n表示点数,点的编号是0到n-1. 接下 ...

  7. 请简述gouraud光照模型_OpenGL ES for Android(冯氏光照)

    冯氏光照模型 想要模拟真实世界的光照效果是比较困难的,我们使用一种叫做冯氏光照模型(Phong Lighting Model)的模型来实现近似的效果.冯氏光照模型的主要结构由3个分量组成:环境(Amb ...

  8. C++11,使简单的事情简单

    Joe Armstrong说过一句话: 面向对象编程语言的问题在于,它总是附带着所有它需要的隐含环境.你想要一个香蕉,但得到的却是一个拿着香蕉的大猩猩,和整个丛林. 显然地,这将简单问题复杂化了.我们 ...

  9. 请简述gouraud光照模型_光照模型汇总

    1. Lambert模型 理想漫反射模型,各个方向一样. Lambert光照模型是一个非常简单的模型,由Lambert在200多年前提出,当光照射到粗糙的表面时,它将向四周均匀的反射.这种各向同性 的 ...

最新文章

  1. SQL学习总结-思维导图
  2. 【FI模块学习笔记】 固定资产概述(下)
  3. 直接拿来用,10个PHP代码片段(收藏)
  4. 阿里云Quick BI——让人人都成为分析师
  5. mysql-初识MySQL
  6. ST-Link刷成J-Link
  7. 【Kafka】Failed to send data to Kafka: Failed to allocate memory within the configured max blocking
  8. python set 和 ^ 的妙用
  9. matlab 中常用的日期格式转换
  10. wps html嵌入ppt,wps文档怎么插入打开幻灯片 WPS文字添加ppt幻灯片教程
  11. 金融计量模型(一):引言
  12. PG数据库内核分析学习笔记_XLOG日志恢复策略
  13. Skype for Business Web 应用
  14. [JZOJ5594][min25筛]最大真因数
  15. 4.Matplotlib多子图,文字和注释以及自定义坐标轴
  16. 全志H616香橙派OrangePi Zero2开发板连接USB以太网卡测试
  17. 手把手教你用视频做闪屏页
  18. office365打开服务器文件出错,新安装的Office 365,打开Excel文件时出错,停止工作...
  19. 航空插头网线转接2.0排针线序图
  20. python第三方库pygame的使用

热门文章

  1. WebView Cache 缓存清除
  2. php 当地天气预报,php 天气预报代码 采集自中央气象台范围覆盖全国_PHP教程
  3. python hook pc微信_python实现微信跳一跳辅助工具步骤详解
  4. python用函数计算个人所得税_用if函数计算个人所得税
  5. 微信消息订阅功能开发流程
  6. TIOBE 编程语言排行,各个语言优缺点,以及你适合那种编程语言
  7. 2023 TIOBE 2月编程语言榜:年度语言是TA!
  8. 当运行npm install 命令的时候带上ignore-scripts,会发生什么?
  9. macos+win10切换到ubuntu的全记录
  10. Windows+CentOS 7双系统(最完全攻略!!)-------囊括所有安装CentOS双系统的问题!