学习OpenGL ES for Android(十三)— 投光物

在之前的章节学习的光源都是一个点,虽然效果不错,和现实世界的光源还有一定的差距。将光投射(Cast)到物体的光源叫做投光物(Light Caster),这里我们学习几种比较常见的光源:定向光(Directional Light),点光源(Point Light)还有聚光(Spotlight)。

定向光

当光源非常远时,来自光源的每条光线就会近似于互相平行,而且光源强度相同。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光。定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:

因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。

我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接使用光的direction向量而不是通过position来计算lightDir向量。我们修改着色器代码,

struct Light {
    // vec3 position; // 使用定向光就不再需要位置了
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-(aLightMatrix *light.direction));
  ...
}

首先我们对光的向量在View空间计算,得到最终的lightDir向量,随后再用它来计算环境光和镜面光。这里我们创建多个箱子并对它们进行不同的缩放和位移,来展示光照的效果。直接来看效果图

源码地址https://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersDirectionalRenderer.java

点光源

在前面的章节中,我们用的光源都是点光源,但是我们看到被光照的地方亮度都是相同的,而实际上光是会随着距离变大而变弱的,而且超过最大距离后就没有光照效果了,这通常叫做衰减。我们需要一个公式来减少光的强度,不用担心已经有前辈解决了这个问题,

在这里d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc_、一次项K_l和二次项Kq_

  • 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
  • 一次项会与距离值相乘,以线性的方式减少强度。
  • 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。

由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:

怎么选择合适的值呢,可以参考文档http://wiki.ogre3d.org/tiki-index.php?page=-Point+Light+Attenuation,我们选择32到100距离之内的值都可以,这里我们选择50距离的值:1.0, 0.09, 0.032,我们给着色器代码的光源结构体添加这三个常数项参数,并计算衰减,计算衰减后再与三个光照效果相乘,最后得到结果,关键代码如下,

……
struct Light {

    ……
    float constant;
    float linear;
    float quadratic;
};
……
void main() {
    ……
    // 衰减
    float distance = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    // 结果
    vec3 result = ambient + diffuse + specular;

    gl_FragColor = vec4(result, 1.0);
}

我们查看效果图,可以看到离光源远的地方光照效果更弱

源码地址https://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersPointRenderer.java

聚光

聚光也很常见,例如手电筒,有灯罩的路灯等等,这种光之后朝一个特定方向照射,在光照范围外就没有光照效果。先看下图聚光灯的工作,

我们先要计算得到顶点上与光源的向量:LightDir,然后 计算其与光源方向(SoptDir)的夹角θ,如果这个角度大于聚光的角度ϕ则该顶点可以被照射到,反之则不可以。所以我们要定义这三个参数,光源位置,光源照射方向,还有光源的照射范围

// 定义光源结构体
struct Light {
    vec3 position;
    vec3 direction;
    float cutOff;
};

其中cutOff为切光角,是用角度值计算得到的余弦值,我们需要计算LightDir和SpotDir向量的点积,然后和cutOff对比。主要代码如下

……
void main() {
    vec3 lightDir = normalize(light.position - fragPos);

    // 检查当前点与光源的连线是否在聚光灯内
    float theta = dot(lightDir, normalize(-(aLightMatrix * light.direction)));

    if(theta > light.cutOff){
        // 被照射到
        ……
    } else {
        // 没有被照射到
        gl_FragColor = vec4(light.ambient * texture2D(material.diffuse, TextCoord).rgb, 1.0);
    }

}

我们传入各项值后查看效果,

我们看到聚光灯内的光照比较亮,所以我们需要一个逐渐变化的效果。我们定义一个内圆锥和一个外圆锥:大于外圆锥不显示光照效果;小于内圆锥显示最大亮度;而在外圆锥和内圆锥内的区域光照效果逐渐变化。具体公式如下,

这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度。值若小于0时置为0,这时我们就不用再使用if else了,主要代码如下,

float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
……
diffuse  *= intensity;
specular *= intensity;
……

此时我们看到聚光的效果就比较平滑了

源码地址https://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersSpotLightRenderer.java

此章对应文档https://learnopengl-cn.github.io/02%20Lighting/05%20Light%20casters/,可以查看文档更加详细的了解。

发布了53 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/jklwan/article/details/103772909