学习OpenGL:光源的种类和实现

按照冯氏光照模型的说法,任何光线都被分割成环境光照、漫反射光照和镜面光照三部分,这三部分分别与物体产生作用叠加后得到我们看到的颜色。但是实际上在现实中有许多种不同的光源(Light Source),比如太阳、蜡烛、手电筒、灯泡等等,仔细观察可以发现这些光源发射光线的方式是不同的,概括起来有三种:


(三张图片来自百度图片搜索)
  • 方向光(Directional Light):又称定向光、平行光,这种光源发射出来的光线是平行光线,每一条光线的方向是相同的,太阳光可以看成是方向光;
  • 点光源(Point Light):一个点光源朝四周发射出放射状的光线,灯泡、蜡烛等就是这种光源;
  • 聚光灯(Spot Light):光源发射出一个圆锥体光,圆锥体外部没有光线,内部光线强度分布表现为靠近圆锥边缘的光强会逐渐变小,手电筒、舞台投光灯等就是这种光源

当然这三种光源不能概括现实中所有的光源,还有更加复杂的,比如灯管就不包括在上述三种灯源中(可以用一排点光源模拟)。话虽如此,这三种光源还是能代表大多数真实光源的,如果用过unity,应该就知道它也提供了这三种光源类型,并且只有这三种才支持实时渲染。

所以我们也用OpenGL去模拟这三种光源的光照效果,并且实现片段着色器的编写。


方向光

方向光(平行光)的特点就是光的照射方向不变,并且光照强度不随着空间变化而衰减。这是因为我们默认把太阳光当成最重要的平行光,而太阳距离地球很远,在地球上的一点点距离变化导致的衰减可以忽略不计,但若是人工制造的平行光(比如用凸透镜+点光源制造的平行光)就必须要考虑衰减了。为了方便起见,渲染时就不考虑衰减了。

方向光需要考虑的参数包括:

  • 光照方向 d i r e c t i o n direction direction
  • 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular

有了这几个参数,按照冯氏光照模型的方法计算三个光照分量的值,然后累加即可。

点光源

点光源的特点是光线呈放射状,并且光照强度随着距离的增大而衰减(Attenuation)。这里的衰减是两方面的:一方面点光源是球面波,按照能量守恒,光强会按 1 R 2 \frac{1}{R^2} R21衰减,原理就是反比于球体的表面积;另一方面光与介质发生作用而导致能量形式转变,光能变成了热能等等,所以会衰减。假设待渲染的某个片段和光源的距离为 d d d ,那么该片段处的光照衰减值近似为:
L u m i n o s i t y = 1.0 Q u a d r a t i c ∗ d 2 + L i n e a r ∗ d + C o n s t a n t Luminosity=\frac{1.0}{Quadratic*d^2+Linear*d+Constant} Luminosity=Quadraticd2+Lineard+Constant1.0
式中的三个参数 Q u a d r a t i c , L i n e a r , C o n s t a n t {Quadratic,Linear,Constant} Quadratic,Linear,Constant 的取值可以参考这里,一般来说,常数项可以保持为 1.0 1.0 1.0不变,而一次项和二次项的系数随着覆盖距离的增大而减小,因为越小的系数在越大的距离才会衰减到一定小的值。

点光源需要考虑的参数包括:

  • 光源位置 p o s i t i o n position position
  • 三个衰减系数 Q u a d r a t i c , L i n e a r , C o n s t a n t {Quadratic,Linear,Constant} Quadratic,Linear,Constant
  • 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular

因此我们计算完三个光照分量的值之后,还需要乘上 L u m i n o s i t y Luminosity Luminosity ,才得到最终的结果。

聚光灯

聚光灯的特点除了包括点光源的所有特点之外,还把光照限制在了一个圆锥体内部,因此聚光灯不可避免的有一个光照方向。在圆锥体的内部的同一圆切面中,光照强度的分布也不是均匀的,体现为偏离中心比较多的光照强度会变小。因此我们给聚光灯定义内圆锥面和外圆锥面,在同一个圆形切面中,内圆锥面内部的光强相等,内圆锥面与外圆锥面之间的光强线性递减到 0 。

定义内圆锥半角(圆锥半角就是母线的夹角)为 α \alpha α,外圆锥半角为 β \beta β,被照射位置和光源的连线与轴的夹角为 θ \theta θ ,那么新的一个光强系数满足:
I n t e n s i t y = c o s θ − c o s β c o s α − c o s β Intensity=\frac{cos\theta-cos\beta}{cos\alpha-cos\beta} Intensity=cosαcosβcosθcosβ
I n t e n s i t y Intensity Intensity **约束(Clamp)**在 [ 0 , 1 ] [0,1] [0,1]之间,就得到了想要的结果。

聚光灯需要考虑的参数包括:

  • 光照方向 d i r e c t i o n direction direction
  • 光源位置 p o s i t i o n position position
  • 内外圆锥半角的余弦值 i n n e r C o s , o u t e r C o s innerCos,outerCos innerCos,outerCos
  • 三个衰减系数 Q u a d r a t i c , L i n e a r , C o n s t a n t {Quadratic,Linear,Constant} Quadratic,Linear,Constant
  • 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular

在计算完三个光照分量的值之后,乘上 L u m i n o s i t y ∗ I n t e n s i t y Luminosity*Intensity LuminosityIntensity,就得到最终的结果。


片段着色器代码

在OpenGL中编写片段着色器的GLSL代码,把每种光源需要的参数封装成结构体,每种光源计算得到RGB的过程封装成函数,然后可以任意定义任意个数量的光源,然后累加起来光照强度得到最终的颜色输出。具体的代码已经注释得很详细:

#version 430 core

// 从前面传下来的数据
in vec3 fragNormal;     // 片段法向量(非单位向量)
in vec3 FragPos;        // 片段位置
in vec2 TexCoords;      // 纹理采样坐标

// 片段着色器的颜色输出
out vec4 FragColor;

// 定义物体材质,因为这里用了图片纹理做光照贴图,因此就可以用采样器直接获得对应坐标的RGB
// 也可以直接定义纯色的RGB向量
struct Material
{
    sampler2D diffuse;  // 漫反射纹理
    sampler2D specular; // 镜面反射纹理
    float shininess;    // 反光度:越高,反光能力越强,散射的越少,高光点越小
};
uniform Material material;  // 物体材质
uniform vec3 viewPos;       // 观察者位置


// 各个光源类型的定义,以及对应求解反射光的函数定义
// 函数参数的normal、viewDir必须是单位向量

//----------------- 方向光 ------------------
struct DirLight
{
    vec3 direction;     // 光照方向
    // 三个光照分量
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

vec3 calDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // 环境光照
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    // 漫反射光照
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    // 镜面光照
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);   // 材质中【反光度】决定的系数
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    // 合并
    return (ambient + diffuse + specular);
}
//-------------------------------------------

//----------------- 点光源 ------------------
struct PointLight
{
    vec3 position;  // 光源位置
    // 三个衰减系数
    float constant;
    float linear;
    float quadratic;
    // 三个光照分量
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

vec3 calPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 环境光照
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    // 漫反射光照
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    // 镜面光照
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);   // 材质中【反光度】决定的系数
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    // 合并,考虑光强衰减
    float distance = length(light.position - fragPos);
    float luminosity = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);   // 光强衰减系数
    return (ambient + diffuse + specular) * luminosity;
}
//-------------------------------------------

//----------------- 聚光灯 ------------------
struct SpotLight
{
    vec3 direction;     // 光照方向
    vec3 position;      // 光源位置
    float innerCos;     // 内圆锥半角余弦值
    float outerCos;     // 外圆锥半角余弦值
    // 三个衰减系数
    float constant;
    float linear;
    float quadratic;
    // 三个光照分量
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

vec3 calSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 环境光照
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    // 漫反射光照
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    // 镜面光照
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);   // 材质中【反光度】决定的系数
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    // 考虑光强衰减
    float distance = length(light.position - fragPos);
    float luminosity = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);   // 光强衰减系数
    // 从这里开始加入聚光灯的判断
    float theta = dot(lightDir, normalize(-light.direction));
    float epsilon = light.innerCos - light.outerCos;
    float intensity = clamp((theta - light.outerCos) / epsilon, 0.0, 1.0);    // 聚光灯决定的光强系数

    return (ambient + diffuse + specular) * luminosity * intensity;
}
//-------------------------------------------


// !!!!! 前面部分定义了一些到目前为止必需的东西,而后面可以根据自己的需要进行编写 !!!!!
// 比如后面,我定义了一个方向光、一个聚光灯和四个点光源

uniform DirLight dirLight;
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform SpotLight spotLight;

uniform int isSpotLight;    // 用这个控制聚光灯的亮灭

void main()
{
    vec3 norm = normalize(fragNormal);              // 法向量的单位向量
    vec3 viewDir = normalize(viewPos - FragPos);    // 观察方向的单位向量
    vec3 result = vec3(0.0f, 0.0f, 0.0f);

    // 后面直接调用函数就可以很方便地计算出每种光照的影响
    result += calDirLight(dirLight, norm, viewDir);
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += calPointLight(pointLights[i], norm, FragPos, viewDir);
    if(isSpotLight == 1)
        result += calSpotLight(spotLight, norm, FragPos, viewDir);

    FragColor = vec4(result, 1.0f);
}

Shader的参数比较多,一定小心注意要把所有用到的uniform都赋好值。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值