【安卓音视频开发OpenGLES】 开发入门(四):给3D图形加上光照是一种什么体验

1,328 阅读16分钟

本文正在参加「金石计划」

前面几篇文章我们讲解了OpenGL中的一些入门基础包括:着色器的使用,纹理的使用,立方体的绘制等内容,今天我们来讲解下OpenGL中的光照

OpenGLES光照模型基础

我们知道现实生活中的光线是比较复杂的,而且受诸多因素的影响,在现有的计算机处理能力中是没有完全模拟的,在OpenGL处理过程中,通常使用的是一些简化模型,比如冯氏光照模型。冯氏光照模型主要包括3部分:

环境光,散射光,镜面光。

  • 环境光照(ambient):环境光照是为了模拟在黑暗环境下物体的发光射,现实生活中就算在黑夜环境下也是会有颜色的。
  • 散射光照(diffuse):模拟光照在不同片段上的亮度,这部分是光照最显著的部分,面向光源那面亮度最大。
  • 镜面光照(specular):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。

img

使用冯氏模型的三种光照类型,可以模拟出现实生活中的大部分光照场景。

环境光照

环境光照可以简单理解为在无光环境下物体的一种颜色状态。

环境光照的计算方式比较简单:使用光的颜色乘以一个很小的常量因子,最后乘以物体的颜色即可

大致如下:

 //设置环境光照
float ambientStrength = 0.2f;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
color = vec4(result, 1.0f);

这里我们使用第二章节讲解的正方形为例子,如果你的代码没问题,应该会得到下面这种效果。

image-20230406104112539

可以看到这是一个非常暗的状态,到这里已经你完成了冯氏光照的第一阶段。

下面看第二阶段散射光照。

散射光照

散射光照是指物体上与光线排布越近的片段越能从光源处获得更多的亮度。这对物体会产生比较明显的光线效果。

散射光照模型如下

img

左上角有一个光源,他照射在物体上每个片段的距离是不一样的,我们需要测量这个光线与它所接触片段之间的角度。这里引入了一个法向量N,图中黄色部分:

法向量简单理解就是垂直于当前片段的一个单位向量

由于当前我们的光线是照射在一个正方形上面,法向量可以和顶点坐标一样定义,对于复杂图形,就需要使用特殊工具获取。

散射光照计算方式

  • 1.计算出当前被照射的片段到光源的向量:lightDir。计算方式:光源向量(lightPos)- 片段向量(FragPos)。
  • 2.计算lightDir和法向量N的夹角大小,夹角越小,说明光线到片段越接近垂直,光线越强。计算方式:使用向量的点乘获取到的是夹角的cos值,cos值越大,说明夹角越小,光线越强
  • 3.使用2中获取的夹角cos值乘以光线颜色向量得到散射光照强度,最后乘以

对应代码

/*
设置慢反射光照
1.先通过灯源pos和片段pos找到片段到灯源的向量:使用lightPos-FragPos
2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
*/
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos-FragPos);
float diff = max(dot(norm,lightDir),0.0f);
vec3 diffuse = diff*lightColor;

一切顺利的话,可以看到下面效果:

image-20230406110037542

下面进入冯氏光照的第三阶段:镜面光照。

镜面光照

镜面光照就是反射光线离观察者越近,光线就越强,模拟现实生活中更真实的场景:

模型如下:

img

镜面光照计算方式:

  • 1.计算片段到光源的向量lightDir:计算方式通散射光照。
  • 2.计算片段到光源的反射向量R:reflectDir,计算方式使用glsl的reflect方法。
  • 3.计算片段到观察者的向量ViewDir。计算方式:原点到观察者的向量(viewPos)-原点到片段的向量(FragPos)。
  • 4.计算向量reflectDir和viewDir的夹角大小,计算方式:通过点乘获取夹角的cos值,cos值越大,说明夹角越小,夹角越小,光线越亮。

对应代码如下

/*设置镜面高光
1.计算片段到观察者的向量:使用viewPos-FragPos得到
2.计算灯源的反射向量,使用reflect得到
3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈*/
​
float specularStrength = 0.5f;
//1
vec3 viewDir = normalize(viewPos-FragPos);
//2
vec3 reflectDir = reflect(-lightDir,norm);
//3
float spec = specularStrength*pow(max(dot(viewDir,reflectDir),0.0f),64.0f);

效果如下:

image-20230406113831334

以上就是关于冯氏光照三阶段的简单处理:

完整的片段着色器代码如下:

#version 300 es
precision mediump float;
in vec2 TexCoord;
in vec3 FragPos;
in vec3 Normal;
​
out vec4 fragColor;
uniform sampler2D textureColor;
uniform vec3 lightColor;
uniform vec3 objectColor;
uniform vec3 lightPos;
uniform vec3 viewPos;
​
void main()
{
   //设置环境光照
   float ambientStrength = 0.2f;
   vec3 ambient = ambientStrength * lightColor;
   /*
   设置慢反射光照
   1.先通过灯源pos和片段pos找到片段到灯源的向量:使用lightPos-FragPos
   2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
   */
   vec3 norm = normalize(Normal);
   vec3 lightDir = normalize(lightPos-FragPos);
   float diff = max(dot(norm,lightDir),0.0f);
   vec3 diffuse = diff*lightColor;
​
   /*设置镜面高光
   1.计算片段到观察者的向量:使用viewPos-FragPos得到
   2.计算灯源的反射向量,使用reflect得到
   3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈*/
​
   float specularStrength = 0.5f;
   //1
   vec3 viewDir = normalize(viewPos-FragPos);
   //2
   vec3 reflectDir = reflect(-lightDir,norm);
   //3
   float spec = specularStrength*pow(max(dot(viewDir,reflectDir),0.0f),64.0f);
​
   vec3 result = (ambient+diffuse+spec)*objectColor;
   fragColor = vec4(result, 1.0f);
}

材质

其实大部分情况下,我们是不会直接使用单独的颜色来表示物体的颜色,而是使用纹理:

我们会将物体的颜色:包括环境光照下以及散射光照下或者镜面光照下都会使用不同的纹理来计算,会封装在一个Material结构体中。

struct Material
{
//漫反射光照下物体颜色
   sampler2D diffuse;
//反射一个物体特定的镜面高光颜色
   sampler2D specular;
//放射光贴图
   sampler2D emission;
//影响镜面高光的散射/半径 如2,4,8,16,32,64,128,256
   float shininess;
};

投光物

现实生活中的投光物是很多很复杂的,我们这里来探讨三种比较常见的:定向光,点光,聚光

定向光

顾名思义就是光线的方向是固定的,对于足够远的光,我们就称之为定向光,如太阳光。

模型如下:

img

既然方向是固定的,那么计算散射光照的时候,就不需要计算夹角了,而是只需要知道光线的方向就可以。

这里我们将光线封装到一个Light结构体中:

struct Light
{
   //定向光线照射的方式
   vec3 direction;
   //光线在环境光照下的强度
   vec3 ambient;
   //光线在散射光照下的强度
   vec3 diffuse;
   //光线在镜面高光光照下的强度
   vec3 specular;
};

完整片段着色器代码如下:

#version 300 es
precision mediump float;
in vec2 TexCoord;
in vec3 FragPos;
in vec3 Normal;
​
out vec4 fragColor;
uniform vec3 viewPos;
struct Light
{
   //定向光线照射的方式
   vec3 direction;
   //光线在环境光照下的强度
   vec3 ambient;
   //光线在散射光照下的强度
   vec3 diffuse;
   //光线在镜面高光光照下的强度
   vec3 specular;
};
uniform Light light;
​
struct Material
{
//漫反射光照下物体颜色
   sampler2D diffuse;
//反射一个物体特定的镜面高光颜色
   sampler2D specular;
//放射光贴图
   sampler2D emission;
//影响镜面高光的散射/半径 如2,4,8,16,32,64,128,256
   float shininess;
};
uniform Material material;
void main()
{
   //设置环境光照
   vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoord));
   /*
   设置慢反射光照
   1.定向光方向固定
   2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
   */
   vec3 norm = normalize(Normal);
   vec3 lightDir = normalize(-light.direction);
   float diff = max(dot(norm,lightDir),0.0f);
   vec3 diffuse = light.diffuse*diff*vec3(texture(material.diffuse,TexCoord));
​
   /*
   设置镜面高光
   1.计算片段到观察者的向量:使用viewPos-FragPos得到
   2.计算灯源的反射向量,使用reflect得到
   3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈
   */
   //1
   vec3 viewDir = normalize(viewPos-FragPos);
   //2
   vec3 reflectDir = reflect(-lightDir,norm);
   //3
   float spec = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess);
   vec3 specular = light.specular*spec*vec3(texture(material.specular,TexCoord));
   vec3 result = (ambient+diffuse+specular);
​
   fragColor = vec4(result, 1.0f);
}

定向光效果:

image-20230406120643018

点光源

点光源就是光源只是一个点,那么他和每个片段的距离和角度就是不同的,需要计算不同片段的光源亮度。

其模型如下:

img

对于点光源的片段颜色计算方式,在前面其实已经讲解过了,需要配合当前片段的法向量进行处理,计算和法向量的夹角大小。

代码如下:

#version 300 es
precision mediump float;
in vec2 TexCoord;
in vec3 FragPos;
in vec3 Normal;
​
out vec4 fragColor;
uniform vec3 viewPos;
struct Light
{
   vec3 position;
   vec3 ambient;
   vec3 diffuse;
   vec3 specular;
   /*衰减
   constant:常数项
   linear:一次项
   quadratic:二次项
   衰减值 = 1.0f/constant+linear*distance+quadratic*distance*distance;
​
   灯光亮度要减去当前衰减值
   */
   float constant;
   float linear;
   float quadratic;
};
uniform Light light;
​
struct Material
{
//漫反射光照下物体颜色
   sampler2D diffuse;
//反射一个物体特定的镜面高光颜色
   sampler2D specular;
//放射光贴图
   sampler2D emission;
//影响镜面高光的散射/半径 如2,4,8,16,32,64,128,256
   float shininess;
};
uniform Material material;
void main()
{
   //设置环境光照
   vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoord));
   /*
   设置慢反射光照
   1.定向光方向固定
   2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
   */
   vec3 norm = normalize(Normal);
   vec3 lightDir = normalize(light.position-FragPos);
   float diff = max(dot(norm,lightDir),0.0f);
   vec3 diffuse = light.diffuse*diff*vec3(texture(material.diffuse,TexCoord));
​
   /*
   设置镜面高光
   1.计算片段到观察者的向量:使用viewPos-FragPos得到
   2.计算灯源的反射向量,使用reflect得到
   3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈
   */
   //1
   vec3 viewDir = normalize(viewPos-FragPos);
   //2
   vec3 reflectDir = reflect(-lightDir,norm);
   //3
   float spec = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess);
   vec3 specular = light.specular*spec*vec3(texture(material.specular,TexCoord));
​
   /*
   计算衰减值
   */
   float distance = length(light.position-FragPos);
   float attenuation = 1.0f/(light.constant+light.linear*distance+light.quadratic*distance*distance);
​
   vec3 result = (ambient+diffuse+specular)*attenuation;
   fragColor = vec4(result, 1.0f);
}

代码里面有个计算衰减值,这个衰减值就是用来模拟:片段的距离和光线越远,衰减的就越快,这也更加贴合现实生活。

衰减值 = 1.0f/constant+linear*distance+quadratic*distance*distance;

点光源效果如下:

image-20230406120944285

聚光

聚光是一种位于环境中某处的光源,它不是向所有方向照射,而是只朝某个方向照射。结果是只有一个聚光照射方向的确定半径内的物体才会被照亮,其他的都保持黑暗。聚光的好例子是路灯手电筒

OpenGL中的聚光用世界空间位置,一个方向和一个指定了聚光半径的切光角来表示。我们计算的每个片段,如果片段在聚光的切光方向之间(就是在圆锥体内),我们就会把片段照亮。下面的图可以让你明白聚光是如何工作的:

img

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phiϕϕ:定义聚光半径的切光角。每个落在这个角度之外的,聚光都不会照亮。
  • Thetaθθ:LightDir向量和SpotDir向量之间的角度。θθ值应该比ΦΦ值小,这样才会在聚光内。

所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和切光角ϕϕ对比。现在你应该明白聚光是我们下面将创建的手电筒的范例。

代码如下:

#version 300 es
precision mediump float;
in vec2 TexCoord;
in vec3 FragPos;
in vec3 Normal;
​
out vec4 fragColor;
uniform vec3 viewPos;
struct Light
{
   //聚光的位置
   vec3 position;
   //聚光指向的方向
   vec3 direction;
   //夹角的cos值,值大于cutoff说明夹角越小,需要显示聚光,小余,不需要显示聚光
   float cutoff;
   vec3 ambient;
   vec3 diffuse;
   vec3 specular;
​
   /*衰减
   constant:常数项
   linear:一次项
   quadratic:二次项
   衰减值 = 1.0f/constant+linear*distance+quadratic*distance*distance;
​
   灯光亮度要减去当前衰减值
   */
   float constant;
   float linear;
   float quadratic;
​
};
uniform Light light;
​
struct Material
{
//漫反射光照下物体颜色
   sampler2D diffuse;
//反射一个物体特定的镜面高光颜色
   sampler2D specular;
//放射光贴图
   sampler2D emission;
//影响镜面高光的散射/半径 如2,4,8,16,32,64,128,256
   float shininess;
};
uniform Material material;
void main()
{
​
   /*
   1.计算片段到光源的距离lightDir
   2.计算lightDir和光源方向direction的夹角的cos
   3.判断2中的cos值和cutoff比较:值大于cutoff说明夹角越小,需要显示聚光,小余,不需要显示聚光,使用环境光照显示
   */
   vec3 lightDir = normalize(light.position-FragPos);
   float theta = dot(lightDir,normalize(-light.direction));
   vec3 result;
   if(theta>light.cutoff){
      //设置环境光照
      vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoord));
      /*
   设置慢反射光照
   1.定向光方向固定
   2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
   */
      vec3 norm = normalize(Normal);
      vec3 lightDir = normalize(light.position-FragPos);
      float diff = max(dot(norm,lightDir),0.0f);
      vec3 diffuse = light.diffuse*diff*vec3(texture(material.diffuse,TexCoord));
​
      /*
      设置镜面高光
      1.计算片段到观察者的向量:使用viewPos-FragPos得到
      2.计算灯源的反射向量,使用reflect得到
      3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈
      */
      //1
      vec3 viewDir = normalize(viewPos-FragPos);
      //2
      vec3 reflectDir = reflect(-lightDir,norm);
      //3
      float spec = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess);
      vec3 specular = light.specular*spec*vec3(texture(material.specular,TexCoord));
​
      /*
      计算衰减值
      */
      float distance = length(light.position-FragPos);
      float attenuation = 1.0f/(light.constant+light.linear*distance+light.quadratic*distance*distance);
      result = (ambient+diffuse+specular)*attenuation;
   }else {
      //设置环境光照
      result = light.ambient * vec3(texture(material.diffuse,TexCoord));
   }
​
   fragColor = vec4(result, 1.0f);
}

效果如下:

image-20230406124032939

上面的光源看起来其实是不真实的,一个真实的聚光的光会在它的边界处平滑减弱的。

为创建聚光的平滑边,我们希望去模拟的聚光有一个内圆锥和外圆锥。我们可以把内圆锥设置为前面部分定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗。

平滑边缘效果

平滑效果定义方式:我们简单定义另一个余弦值,它代表聚光的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0

我们可以使用下面的公式计算这样的值:

image-20230406124352447

这里ϵϵ是内部(ϕϕ)和外部圆锥(γγ)(\epsilon = \phi - \gamma)的差。结果II的值是聚光在当前片段的亮度。

很难用图画描述出这个公式是怎样工作的,所以我们尝试使用一个例子:

θθθθ(角度)ϕϕ(内切)ϕϕ(角度)γγ(外切)γγ(角度)ϵϵII
0.87300.91250.82350.91 - 0.82 = 0.090.87 - 0.82 / 0.09 = 0.56
0.9260.91250.82350.91 - 0.82 = 0.090.9 - 0.82 / 0.09 = 0.89
0.97140.91250.82350.91 - 0.82 = 0.090.97 - 0.82 / 0.09 = 1.67
0.97140.91250.82350.91 - 0.82 = 0.090.97 - 0.82 / 0.09 = 1.67
0.83340.91250.82350.91 - 0.82 = 0.090.83 - 0.82 / 0.09 = 0.11
0.64500.91250.82350.91 - 0.82 = 0.090.64 - 0.82 / 0.09 = -2.0
0.966150.997812.50.95317.50.966 - 0.953 = 0.04480.966 - 0.953 / 0.0448 = 0.29

就像你看到的那样我们基本是根据θ在外余弦和内余弦之间插值。如果你仍然不明白怎么继续,不要担心。你可以简单的使用这个公式计算,当你更加老道和明白的时候再来看。

由于我们现在有了一个亮度值,当在聚光外的时候是个负的,当在内部圆锥以内大于1。如果我们适当地把这个值固定,我们在片段着色器中就再不需要if-else了,我们可以简单地用计算出的亮度值乘以光的元素:

glsl代码如下:

#version 300 es
precision mediump float;
in vec2 TexCoord;
in vec3 FragPos;
in vec3 Normal;
​
out vec4 fragColor;
uniform vec3 viewPos;
struct Light
{
   //聚光的位置
   vec3 position;
   //聚光指向的方向
   vec3 direction;
   //夹角的cos值,值大于cutoff说明夹角越小,需要显示聚光,小余,不需要显示聚光
   float cutoff;
   float outerCutoff;
   vec3 ambient;
   vec3 diffuse;
   vec3 specular;
​
   /*衰减
   constant:常数项
   linear:一次项
   quadratic:二次项
   衰减值 = 1.0f/constant+linear*distance+quadratic*distance*distance;
​
   灯光亮度要减去当前衰减值
   */
   float constant;
   float linear;
   float quadratic;
​
};
uniform Light light;
​
struct Material
{
//漫反射光照下物体颜色
   sampler2D diffuse;
//反射一个物体特定的镜面高光颜色
   sampler2D specular;
//放射光贴图
   sampler2D emission;
//影响镜面高光的散射/半径 如2,4,8,16,32,64,128,256
   float shininess;
};
uniform Material material;
void main()
{
​
   /*
   1.计算片段到光源的距离lightDir
   2.计算lightDir和光源方向direction的夹角的cos
   3.判断2中的cos值和cutoff比较:值大于cutoff说明夹角越小,需要显示聚光,小余,不需要显示聚光,使用环境光照显示
   */
   vec3 lightDir = normalize(light.position-FragPos);
   float theta = dot(lightDir,normalize(-light.direction));
   float epsilon = light.cutoff - light.outerCutoff;
   //clamp函数,它把第一个参数固定在0.0和1.0之间。这保证了亮度值不会超出[0, 1]以外
   float intensity = clamp(( theta - light.outerCutoff) / epsilon,0.0, 1.0);
​
   //设置环境光照
   vec3 ambient = light.ambient * vec3(texture(material.diffuse,TexCoord));
   /*
   设置慢反射光照
   1.定向光方向固定
   2.使用1中获取的片段到灯源的向量和法向量的点乘,可以获取夹角大小的cos值,如果cos值越大,说明夹角越小,当前片段的光越亮
   */
   vec3 norm = normalize(Normal);
   float diff = max(dot(norm,lightDir),0.0f);
   vec3 diffuse = light.diffuse*diff*vec3(texture(material.diffuse,TexCoord));
​
   /*
   设置镜面高光
   1.计算片段到观察者的向量:使用viewPos-FragPos得到
   2.计算灯源的反射向量,使用reflect得到
   3.获取1和2向量的夹角的cos值,cos值越大,夹角越小,光线越强烈
   */
   //1
   vec3 viewDir = normalize(viewPos-FragPos);
   //2
   vec3 reflectDir = reflect(-lightDir,norm);
   //3
   float spec = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess);
   vec3 specular = light.specular*spec*vec3(texture(material.specular,TexCoord));
​
   /*
   计算衰减值
   */
   float distance = length(light.position-FragPos);
   float attenuation = 1.0f/(light.constant+light.linear*distance+light.quadratic*distance*distance);
   vec3 result = (ambient+diffuse*intensity+specular*intensity)*attenuation;
​
   fragColor = vec4(result, 1.0f);
}

效果:

image-20230406130307988

这样就更贴近现实生活中的光照场景啦:

好了,完整代码已经贴到github上了,大家可以自行下载查看。我是小余,我们下期见》》》