> 文章列表 > 光照的个人推导过程与GL实现

光照的个人推导过程与GL实现

光照的个人推导过程与GL实现

目录

1、前提知识

1.1、GL的绘图过程:

1.2、点积的规则和作用:

1.3、normalize在方向处理上的作用

2、光照控制的理论基础

2.1、自由的实现:

2.2、带有方向性的光——基于dot product的实现

最终效果演示如下:

3、关键代码介绍:

具体代码位置:


        对于感兴趣的领域。不求甚解地做出同样的东西其实是不让我满足的,我想要的是获得思考出这些规则的基础能力,这样才可能自由地创造更多的东西。创造力的基础就是真正的掌握自身,所以我打算从0开始自己推导一遍,完全靠自己实现这些功能,所以不一定完全和教程的一致。掌握怎么用是不够的,我还想掌握这是怎么想出来的

1、前提知识

1.1、GL的绘图过程:

        不知道大家有没有想过,为什么设定3个顶点,GL就能帮助用户形成一个三角形面呢?其实是因为GL会根据顶点之间的dx、dy、dz以线性的方式自动逐渐变化填充而成一个面。每一次drawcall都可以这么理解:gl都会按照用户设定的分组配置,只要是vertexAttribute形态的对象,就会以每次一组数据,如一组顶点、一组颜色,以它们之间的线性变化值,每变化一次就送入顶点shader和片元shader执行,如下所示:

        举个例子,设置顶点[ [(-1,0,0), (1,0,0), (0,1,0) ]]。

        x轴会以一个很小的dx,从-1慢慢到1再到0,每次dx变化都会应用顶点shader和片元shader的逻辑,形成一个各参数按要求连续变换的面。

        因此哪怕只是设定了3个顶点和3个颜色,GL也依然能填充出一个光滑的三角形并以光滑的片元颜色进行填充,而无需人为地沿着顶点逐步微分绘制,非常方便。这种自动微分的特性让用户得以方便地实现各种数学方法。

        若以二维来看,可以观察如下例子,在设定了三个顶点后,GL就会微分并遍历这些顶点,就像下图所示。向量并微分为无数个小向量并形成一个三角形面:

 

1.2、点积的规则和作用:

        这里其他作者已经写得挺好了,就直接转载吧:

向量点乘与叉乘的概念及几何意义 - 知乎 (zhihu.com)

        这里只截取一些关键概念介绍

 

1.3、normalize在方向处理上的作用

可以参考我之前的博客文章:

http://t.csdn.cn/pZ6Tt

        简单来说,normalize处理后的向量只有各轴投影相对向量长度的比例,也就是它的方向,这在很多场合都有重要作用,例如把不同大小的向量拉到同一标准比较、一些只需要对向量的方向关系进行处理和比较,而向量长度对处理过程无益的场合等,都会用到normalize进行处理。图形图像中大量用到这个方法。

2、光照控制的理论基础

2.1、自由的实现:

        因为所有的效果都是基于数学和逻辑之上,所以实现的方式是完全自由的、甚至可以是不自然的,不像显示的光线必须有一点发出,再通过物体反射到视点,在gl的虚拟空间中,可以简化为:

        1、光源顶点距离物体顶点越远,顶点对应片元颜色越浅

        2、从光源出发到某顶点的向量和物体顶点的方向性越一直,或者夹角越小,顶点对应片元颜色越深

2.2、带有方向性的光——基于dot product的实现

首先

        已知1:标准化计算可以得到向量的方向特征,

        已知2:点积计算可以计算两个向量的方向上的相似性,换句话说就是夹角越小值越大。

        但颜色值最大只为1,如果使用原始向量,点积后的结果可能比1大,因此我打算先标准化再进行对比。

        于是颜色浓度公式即:

dot(normalize(光向量), normalize(顶点向量))

        但这个式子的问题在于,GL的顶点向量实际可以理解为无数同方向特征的、长度很小的分向量相加起来的,这些分向量和顶点向量本身有相同的方向特征,因此dot结果对总向量处处有效,无法实现只让部分分向量显示颜色的需求。

        于是,我们可以这样想,光向量只要在顶点构成的面范围内,那么必然有一条顶点向量与它完全重合,若把顶点向量-光向量,即把光向量的起始点从原点,变为顶点向量-光向量的位置上,也就是变为顶点向量中某长度的一点上,这样在这个长度之下的分向量的点积就会变成负数(负数颜色在GL中不显示),从而实现“只让总向量部分分向量显示颜色的需求”。

 以上逻辑的式子:

        dot(normalize(光向量), normalize(顶点向量 - 光向量))

以上逻辑的图示如下:

        红色为光向量,整个右半象限的顶点向量和分向量的方向特征向量与光向量的方向特征向量点积都>0,也就是整个右半象限都有光,即以原点为起点的光线,而光线向量其实唯一的作用就是提供一个方向特征

 

        但现在我希望以光向量的终点为起点进行照射,那我就把当前gl传入的顶点分向量,减去光向量(即绿色的新向量),以绿色向量根部为原点作二维坐标系,可以很容易发现,绿色向量的方向特征向量与顶点向量的方向特征向量点积后,只有绿色向量根部右侧的向量能得到>0的值,从而顺利筛选了发光与不发光的部分。此时光线向量除了提供方向外,它的终点提供了光线起点的作用。

        所有向量以及分向量都做一次上述操作后,即可实现任意起点、任意方向的光线效果了。

        

 

        另外,在真实世界中,光的强度与距离成反比,因此可以再乘以一个与距离 成反比的式子实现更真实的光照:

        (10.0 / distance(光顶点, 物体顶点)) * 5.0

最终效果演示如下:

        以原点到光斑方向设定为光线方向,可以看到看到盒子内部只有被照射到的 地方有颜色,而且有正常的渐变。

 

 

 

3、关键代码介绍:

        3.1、 盒子的顶点shader。因为我的demo里只用了一个面,通过变换矩阵旋转多次形成一个盒子,因此在做向量计算时不能直接用传入的顶点,而是变换后的最终结果的顶点,结果保存在objPos中。而为了保持场景的一致性,光线向量也要跟随场景缩放变换,结果保存在lightPos中

#version 300 es
uniform mat4 uMVPMatrix; //旋转平移缩放 总变换矩阵。物体矩阵乘以它即可产生变换
uniform mat4 uMVPMatrixWithoutRotate; //总变换矩阵,但没有旋转变换,契合光斑不参与房间旋转的特性
uniform mat4 objMatrix;
in vec3 objectPosition; //物体位置向量,参与运算但不输出给片源
uniform vec3 lightDotPos;
in vec4 objectColor; //物理颜色向量
in vec2 vTexCoord; //纹理内坐标
out vec4 fragObjectColor;//输出处理后的颜色值给片元程序
out vec2 fragVTexCoord;//输出处理后的纹理内坐标给片元程序
out vec3 objPos;
out vec3 lightPos;void main() {vec4 pos = uMVPMatrix * vec4(objectPosition, 1.0);gl_Position = pos; //设置物体位置fragVTexCoord = vTexCoord; //默认无任何处理,直接输出物理内采样坐标fragObjectColor = objectColor; //默认无任何处理,输出颜色值到片源objPos = (uMVPMatrix * vec4(objectPosition, 1.0)).xyz; //给片元传一下实际转换后实际的坐标lightPos = (uMVPMatrixWithoutRotate * vec4(lightDotPos, 1.0)).xyz;
}

        3.2、盒子的片元shader:通过之前描述的逻辑,把片元颜色的浓度按照顶点的关系进行处理

#version 300 es
precision highp float;
in vec2 fragVTexCoord;//接收vertShader处理后的纹理内坐标给片元程序
in vec4 fragObjectColor;
in vec3 objPos;
in vec3 lightPos;
out vec4 fragColor;//输出到的片元颜色
uniform int funcChoice; //光照方式选择void main() {vec4 color;vec3 lightVec = lightPos;//通过dot product得到光线向量和顶点向量之间的相似性,再把相似性系数作为颜色深度系数switch (funcChoice) {default:case 0: //使用dot product方法//怎样才可以表达一条不是由原点出发的光线?/*nice1 假设顶点向量包含了光源向量,那么顶点向量 - 光源向量 = 光源为源头到顶点的向量,也就是去除了原点到光源的距离,其具体含义就是把光线设定为原点到光圈的方向,通过点乘计算当前顶点与前述向量的相关性,以这个相关性作为颜色的浓淡系数*/color = vec4(fragObjectColor.rgb * dot(normalize(lightVec), normalize(objPos - lightVec)), fragObjectColor.a);color = color * (10.0 / distance(lightVec, objPos)) * 5.0; //叠加一下与光强度与光源距离成反比的关系式break;case 1: //使用距离光:color = vec4(fragObjectColor.rgb * (1.0 / distance(lightPos, objPos)) * 5.0, fragObjectColor.a);break;}fragColor = color;
}

        3.3、光斑的顶点shader:

#version 300 esuniform mat4 uMVPMatrix; //旋转平移缩放 总变换矩阵。物体矩阵乘以它即可产生变换
uniform mat4 objMatrix;
in vec3 objectPosition; //物体位置向量,参与运算但不输出给片源
in vec2 vTexCoord; //纹理内坐标
out vec2 fragVTexCoord;//输出处理后的纹理内坐标给片元程序
out vec3 objPos;void main() {vec4 pos = uMVPMatrix * vec4(objectPosition, 1.0);gl_Position = pos; //设置物体位置fragVTexCoord = vTexCoord; //默认无任何处理,直接输出物理内采样坐标objPos = pos.xyz;
}

        3.4、光斑的片元shader,那种球形电灯的效果是利用纹理采样坐标距离中心点成非线性的反比实现,并通过光线向量本身的方向特征,偏移中心点实现形变:

intensity 扩散程度**/
vec4 getSpotLightOne(vec2 uv, vec2 center, float intensity, vec3 color) {//使用公式 1 / sqrt(当前遍历到的x,y 到 目标x,y 的距离),实现二维平面范围内距离中心点越远颜色越非线性变淡的效果float dist = intensity / sqrt(distance(uv, center));float alpha = 1.0;if (dist > 0.3) {alpha = dist;} else {alpha = 0.0;}return vec4(color * dist, alpha);
}void fireflyEffect(out vec4 fragColor, in vec2 fragCoord) {//以面的中心点位置发光 光斑纹理原点在左上角,因此最大点为(1,1),所以中i的那为(1/2, 1/2),再减去光线本身的方向性即可向发光方向拉伸光斑fragColor = getSpotLightOne(fragCoord, vec2(0.5, 0.5) - normalize(objPos).xy * 0.5, 0.2, vec3(1.0, 1.0, 1.0));
}void main() {fireflyEffect(fragColor, fragVTexCoord);
}

最后效果可以看我的视频:

        【OpenGL ES使用向量点积实现有方向的光照效果】 https://www.bilibili.com/video/BV1yT411n7BY/?share_source=copy_web&vd_source=c870102a8d6b63ae5be697515b19d95f

具体代码位置:

learnopengl/app/src/main/java/com/cjztest/glMyLightModel at main · cjzjolly/learnopengl · GitHub

learnopengl/app/src/main/assets/cjztest/lightmodel at main · cjzjolly/learnopengl (github.com)