【learnOpenGL学习笔记_12】OpenGL光源类型:三大光源轻松玩转,新手速成!

大家好,我是 同学小张,+v: jasper_8017 一起交流,持续学习AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,订阅我的大模型专栏,共同学习和进步。
现在订阅专栏,+微信私信我 返3元,即将涨价!


前面几个教程,我们都是用的一个点或一个物体作为光源,但是现实生活中,并不只有点光源。今天就带大家继续学习 OpenGL 中的三大光源 —— 定向光、点光源和聚光灯。不管你是不是初学者,跟着这篇文章一步步来,保证你能快速上手,打造出炫酷的光照效果!

1. 定向光:模拟太阳光

定向光就是所有光线都平行的光源,就像太阳光一样。当一个光源被设置为无限远时,它被称为定向光(Directional Light),因为所有的光线都有着同一个方向,它会独立于光源的位置。

在这里插入图片描述

1.1 修改片段着色器

还记得我们之前设置的光照方向吗?

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

......

vec3 lightDir = normalize(light.position - outFragPos);

对于定向光,因为所有光线都是平行的,所以每个片段上光的方向一致,不再需要通过光源位置来计算方向了。因此,修改如下:

struct Light
{
    // vec3 position; // 现在不在需要光源位置了,因为它是无限远的
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

......

vec3 lightDir = normalize(light.position - outFragPos);

首先,我们要定义一个光的方向向量,而不是位置。比如,你可以这样写:

struct Light {
    vec3 direction; // 光的方向向量
    vec3 ambient;   // 环境光强度
    vec3 diffuse;   // 漫反射光强度
    vec3 specular;  // 镜面反射光强度
};

......

vec3 lightDir = normalize(-light.direction);

在这里,对方向向量取反并标准化,得到光源方向:

vec3 lightDir = normalize(-light.direction);

为啥要取反呢?因为通常我们习惯定义光的全局方向是从光源发出的,但计算光照时需要从片段指向光源的方向。取反后,它就变成了指向光源的方向了。

1.2 主程序中设置定向光

在主程序中,将光的方向传递进来:

ourShader.setVec3("light.direction", lightDir);

1.3 增加几个物体展示定向光对所有物体都有同样的影响

下面我们增加几个物体,展示定向光对所有物体都有同样的影响。增加几个物体,只要更改每个物体的模型矩阵即可。

(1)给定几个物体的偏移量,这样它们看起来不会重叠。

glm::vec3 cubePositions[] = {
    glm::vec3( 0.0f,  0.0f,  0.0f),
    glm::vec3( 2.0f,  5.0f, -15.0f),
    glm::vec3(-1.5f, -2.2f, -2.5f),
    glm::vec3(-3.8f, -2.0f, -12.3f),
    glm::vec3( 2.4f, -0.4f, -3.5f),
    glm::vec3(-1.7f,  3.0f, -7.5f),
    glm::vec3( 1.3f, -2.0f, -2.5f),
    glm::vec3( 1.5f,  2.0f, -2.5f),
    glm::vec3( 1.5f,  0.2f, -1.5f),
    glm::vec3(-1.3f,  1.0f, -1.5f)
};

(2)在渲染循环中,使用不同的模型矩阵来渲染每个物体。

glBindVertexArray(VAO);
for(GLuint i = 0; i < 10; i++)
{
    model = glm::mat4();
    model = glm::translate(model, cubePositions[i]);
    GLfloat angle = 20.0f * i;
    model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
    ourShader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}
glBindVertexArray(0);

1.4 运行程序,看看效果!

在这里插入图片描述

2. 点光源:带衰减的灯光

点光源就像灯泡一样,在某个位置向四周发光,而且光线会随着距离变远而变暗,这种衰减效果让场景更真实。

在这里插入图片描述

前面的教程其实我们使用的就是点光源,只是没有设置衰减项。今天我们就来学习如何设置点光源的衰减效果。

2.1 衰减

2.1.1 概念和公式

随着光线穿越距离的变远使得亮度也相应地减少的现象,通常称之为衰减(Attenuation)。一种随着距离减少亮度的方式是使用线性等式。这样的一个随着距离减少亮度的线性方程,可以使远处的物体更暗。然而,这样的线性方程效果会有点假。在真实世界,通常光在近处时非常亮,但是一个光源的亮度,开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一种不同的方程来减少光的亮度。

在这里插入图片描述

在这里d代表片段到光源的距离。为了计算衰减值,我们定义3个(可配置)项:常数项Kc,一次项Kl和二次项Kq。

  • 常数项通常是1.0,它的作用是保证分母永远不会比1小。
  • 一次项用于与距离值相乘,这会以线性的方式减少亮度。
  • 二次项用于与距离的平方相乘,为光源设置一个亮度的二次递减。二次项在距离比较近的时候相比一次项会比一次项更小,但是当距离更远的时候比一次项更大。

在这里插入图片描述

2.1.2 可配置项如何确定?

可配置项值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。下面的表格展示一些各项的值,它们模拟现实(某种类型的)光源,覆盖特定的半径(距离)。第一栏定义一个光的距离。

在这里插入图片描述

2.2 实现衰减

先定义光源的位置,还有三个衰减项:常数项、一次项和二次项。直接看代码:

struct Light {
    vec3 position;   // 光源位置
    vec3 ambient;    // 环境光强度
    vec3 diffuse;    // 漫反射光强度
    vec3 specular;   // 镜面反射光强度
    float constant;  // 常数衰减项
    float linear;    // 一次衰减项
    float quadratic; // 二次衰减项
};

注意这里是点光源了,所以片段Light结构体中还是光源的位置,而不是平行光时的方向。

然后在主程序中,设置光源的衰减项:

ourShader.setFloat("light.constant", 1.0f);
ourShader.setFloat("light.linear", 0.09f);
ourShader.setFloat("light.quadratic", 0.032f);

最后,在片段着色器中,计算衰减参数,加入到最后的光照结果中去:

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

vec3 finalColor = (diffuseambient + ambientdiffuse + specular) * attenuation;

2.3 运行程序,看看效果!

在这里插入图片描述

这样,离光源近的物体亮,远的暗,效果是不是很逼真?就像真的灯泡在发光一样!

3. 聚光灯:手电筒效果

聚光灯(Spotlight)只朝一个方向照射,形成一个圆锥形的光照区域,就像手电筒一样。这种光源用好了,能给你的场景增添很多神秘感和层次感。

在这里插入图片描述

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

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

所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和切光角ϕ对比。

3.1 修改光源结构体

在光源结构里加上方向向量和切光角:

struct Light {
    vec3 position;   // 光源位置
    vec3 direction;  // 光源方向
    float cutOff;    // 切光角(内圆锥角度的余弦值)
    // 其他光照参数...
};

3.2 主程序中设置聚光灯方向和切光角

ourShader.setVec3("light.position", camera.m_position);
ourShader.setVec3("light.direction", camera.m_front);
ourShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f))); // 注意这里使用了 glm::cos 来获取角度的余弦值,而不是角度本身,节省性能。

3.3 修改片段着色器中的计算逻辑

float theta = dot(lightDir, -light.direction);
if (theta > light.cutOff)
{
    // 光照计算逻辑
}
else
{
    // 片段在聚光范围外,只保留环境光
    color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
}

因为这里dot计算出来的是余弦值,所以之前在主程序中传入的切光角最好是余弦值,避免dot之后反余弦计算,消耗性能。

完整的片段着色器:

#version 330 core
out vec4 FragColor;

in vec3 outNormal;
in vec3 outFragPos;
in vec2 TexCoords;
uniform vec3 viewPos;

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

struct Light
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant;
    float linear;
    float quadratic;
    vec3 direction;  // 光源方向
    float cutOff;    // 切光角(内圆锥角度的余弦值)
};
uniform Light light;

void main()
{
    // 环境光
    vec3 ambient = vec3(texture(material.diffuse, TexCoords)) * light.ambient;
    vec3 lightDir = normalize(light.position - outFragPos);

    float theta = dot(lightDir, -light.direction);
    if (theta > light.cutOff)
    {
        // 漫反射光
        vec3 normal = normalize(outNormal);
        float diff = max(dot(lightDir, normal), 0.0);
        vec3 diffuse = vec3(texture(material.diffuse, TexCoords)) * diff * light.diffuse;

        // 镜面光
        vec3 viewDir = normalize(viewPos - outFragPos);
        vec3 reflectDir = reflect(-lightDir, outNormal);
        float diff_spec = max(dot(viewDir, reflectDir), 0.0);
        float spec = pow(diff_spec, material.shininess);
        vec3 specular = vec3(texture(material.specular, TexCoords)) * spec * light.specular;

        // 衰减
        float distance = length(light.position - outFragPos);
        float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
        vec3 finalColor = (ambient + diffuse + specular) * attenuation;
        FragColor = vec4(finalColor, 1.0);
    }
    else
    {
        vec3 finalColor = ambient;
        FragColor = vec4(finalColor, 1.0);
    }
}

3.4 运行程序,看看效果!

在这里插入图片描述

它看起来仍然有点假,原因是聚光有了一个硬边。片段着色器一旦到达了聚光的圆锥边缘,它就立刻黑了下来,却没有任何平滑减弱的过度。一个真实的聚光的光会在它的边界处平滑减弱的。

3.5 平滑/软化边缘

在这里插入图片描述

3.5.1 修改光源结构体

在光源结构体中,加入外圆锥角:

struct Light {
    vec3 position;
    vec3 direction;
    float cutOff;
    float outerCutOff;
    // 其他光照参数...
};
3.5.2 主程序中设置外圆锥角
ourShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
3.5.3 修改片段着色器中的计算逻辑

(1) 计算外切光角和内切光角之间的差值,得到平滑过渡的epsilon值。
(2) 使用 clamp 函数,把光照强度限制在 0 到 1 之间。
(3) 把 intensity 值乘到光照上。

float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);

因为现在 intensity 范围是 0 到 1,在内圆锥内是 1,在外圆锥内是 0,在边缘处平滑过渡。所以我们也不再需要上面的 if 判断了。

完整片段着色器:

#version 330 core
out vec4 FragColor;

in vec3 outNormal;
in vec3 outFragPos;
in vec2 TexCoords;
uniform vec3 viewPos;

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

struct Light
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant;
    float linear;
    float quadratic;
    vec3 direction;  // 光源方向
    float cutOff;    // 切光角(内圆锥角度的余弦值)
    float outerCutOff; // 外圆锥角度的余弦值
};
uniform Light light;

void main()
{
    // 环境光
    vec3 ambient = vec3(texture(material.diffuse, TexCoords)) * light.ambient;
    vec3 lightDir = normalize(light.position - outFragPos);

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

    // 漫反射光
    vec3 normal = normalize(outNormal);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = vec3(texture(material.diffuse, TexCoords)) * diff * light.diffuse;

    // 镜面光
    vec3 viewDir = normalize(viewPos - outFragPos);
    vec3 reflectDir = reflect(-lightDir, outNormal);
    float diff_spec = max(dot(viewDir, reflectDir), 0.0);
    float spec = pow(diff_spec, material.shininess);
    vec3 specular = vec3(texture(material.specular, TexCoords)) * spec * light.specular;

    // 衰减
    float distance = length(light.position - outFragPos);
    float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
    vec3 finalColor = ambient + (diffuse + specular) * attenuation * intensity;
    FragColor = vec4(finalColor, 1.0);
}

3.6 运行程序,看看效果!

在这里插入图片描述

4. 总结

本文主要介绍了 OpenGL 中的三种投光物:点光源、方向光和聚光。对于点光源,主要记住要增加衰减。对于聚光,主要记住要增加内外圆锥角,并使用 clamp 函数来平滑过渡。

篇幅有限,完整程序可私信我获取。

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~


  • 大家好,我是 同学小张,持续学习C++进阶、OpenGL、WebGL知识AI大模型应用实战案例
  • 欢迎 点赞 + 关注 👏,持续学习持续干货输出
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号搜同学小张 🙏

私信免费领取AI、C++等相关资料,持续收集更新中! 包括但不限于:

  1. 清华大学 - DeepSeek资料合集(多篇)

  2. DeepSeek指导手册(24页).pdf

  3. 《如何向 ChatGPT 提问以获得高质量答案:提示技巧工程完全指南》

  4. 《OpenAI:GPT 最佳实践(大白话编译解读版)》

  5. 人工智能精选电子书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

同学小张

如果觉得有帮助,欢迎给我鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值