OpenGL ES教程——光照进阶

526 阅读6分钟

image.png

上一篇基础光照阐述了opengl中颜色的定义,使用以及冯氏光照模型。本篇继续讲和光照相关的材质、光照贴图、投光物等知识。

1、材质

现实世界里,不同的材质在光照下有不同的表现,比如,金属制品在阳光下闪闪发光,但木头就不会,所以我们需要定义物体材质做区分

根据冯氏光照模型,材质也会用以下三个分量来定义:

  • 环境光照
  • 漫反射光照
  • 镜面光照

再加上反光度,这样就可以定义材质了:

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

在片段着色器中定义如上结构体,并且定义结构体uniform类型变量,那该怎么给这种类型的uniform变量赋值呢?

objectShader.setVec3("material.ambient",  1.0f, 0.5f, 0.31f);

2、光照

既然材质都已经按冯氏光照模型三个维度定义了,光照也是需要按冯氏模型维度定义的:

struct Light {
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light light;

在计算颜色输出时,需要光源的位置信息,所以光源结构体中也增加了光源的位置信息。

这样在计算最终颜色输出时,比如漫反射输出,则用材质的漫反射颜色乘以光源的漫反射颜色即可,一般写法为:

void main()
{
    vec3 ambient = light.ambient * material.ambient;

    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - fragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * (diff * material.diffuse);

    vec3 viewDir = normalize(viewPos - fragPos);
    vec3 reflectDir =  reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * (spec * material.specular);

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

3、光照贴图

目前光照效果只使用了颜色,如果要使用纹理怎么办呢?因为真实场景中,肯定是纹理使用得更多,更真实

纹理代表着一个buf,这个buf可能代表着一张图片,或者一段视频中的yuv某个分量数据,可以认为就是无数颜色的合集,那材质能用颜色,也可以用纹理,所以材质可以这么定义:

struct Material {
    sampler2D  diffuse;
    sampler2D  specular;
    float shininess;
};
uniform Material material;

所以,现在需要我们给uniform变量赋值,然后再来计算最终颜色输出:

//设置材质纹理id,纹理id就是0,1这类
objectShader.setInt("material.diffuse", 0);
objectShader.setInt("material.specular", 1);

//然后计算最终颜色输出,使用texture函数计算颜色值
void main()
{
    vec3 ambient = light.ambient * (texture(material.diffuse, texCoords)).rgb;

    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - fragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * (texture(material.diffuse, texCoords)).rgb;

    vec3 viewDir = normalize(viewPos - fragPos);
    vec3 reflectDir =  reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * (texture(material.specular, texCoords)).rgb;

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

4、平行光

光源有多种类型,平行光是其中一种。

  • 平行光,比如说太阳,光照强度足够强,距离足够远
  • 点光源,比如说灯泡,理论上点光源只能照亮点光源附近的地方,无法照亮远处
  • 聚光,类似于手电筒,在光的范围内可照亮物体,范围之外则无法照亮物体

还记得上一节中,光源结构体有个position变量,平行光是不需要光源的位置的,我们只需要光的方向,所以针对平行光,position变direction

struct Light { 
    // vec3 position; 
    // 使用定向光就不再需要了 
    vec3 direction; 
    vec3 ambient; 
    vec3 diffuse;
    vec3 specular;
};

//计算光广告,也需要改一改
vec3 lightDir = normalize(-light.direction);

5、点光源

一开始学习的示例就是点光源,但有一个特性,点光源没有考虑到。就是衰减,理论上离点光源位置越远,就会越暗,那怎么计算距离,并且计算光亮的程度呢?

glsl中有函数,可以计算距离:

float distance = length(light.position - fragPos);

image.png

衰减系数如上述公式,d既是距离,公式里这几个参数一般按如下方式挑选:

距离常数项一次项二次项
71.00.71.8
131.00.350.44
201.00.220.20
321.00.140.07
501.00.090.032
651.00.070.017
1001.00.0450.0075
1601.00.0270.0028
2001.00.0220.0019
3251.00.0140.0007
6001.00.0070.0002
32501.00.00140.000007

综上,点光源结构体应该这么定义:


struct Light {
    vec3 position;
    
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    //下边3个成员分别是常量、一次项系数、二次项系数
    float constant;
    float linear;
    float quadratic;
};

float attenuation = 1.0/(light.constant + light.linear * distance + light.quadratic * distance * distance);

距离衰减系数计算如上,最后把漫反射以及镜面结果分别自乘这个系数即可。

6、聚光

还有一种光源像手电筒一样,它能射出一定范围内的光线。

image.png

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phi:ϕ 指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
  • Thetaθ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小

所以,光源结构体就要这么定义了:

struct Light {
    vec3 position;
    vec3 direction;
    float cutOff;
    float outCutOff;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};

position是光源位置,direction是聚光源的指向,上图就是正向下

float theta = dot(lightDir, normalize(-light.direction));

if(theta > light.cutOff) 
{       
  // 执行光照计算
}
else  // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

但这么做会有个问题,很突兀,比如这样:

image.png

所以,为了解决这个问题,需要做边缘平滑工作。

为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。

为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。

我们可以用下面这个公式来计算这个值:

image.png

void main()
{
    vec3 lightDir = normalize(light.position - fragPos);
    float theta = dot(lightDir, normalize(-light.direction));
    float epsilon = light.cutOff - light.outCutOff;
    float intensity = clamp((theta - light.outCutOff)/epsilon, 0.0, 1.0);

    float distance = length(light.position - fragPos);
    float attenuation = 1.0/(light.constant + light.linear * distance + light.quadratic * distance * distance);

    vec3 ambient = light.ambient * (texture(material.diffuse, texCoords)).rgb;
    //        ambient = ambient * attenuation;

    vec3 norm = normalize(normal);

    //如果传的是方向值 ,就直接计算光方向的单位向量
    //    vec3 lightDir = normalize(-light.direction);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * (texture(material.diffuse, texCoords)).rgb;
    //        diffuse = diffuse * attenuation;
    diffuse *= intensity;

    vec3 viewDir = normalize(viewPos - fragPos);
    vec3 reflectDir =  reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * (texture(material.specular, texCoords)).rgb;
    //        specular = specular * attenuation;
    specular *= intensity;

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}