大家好,我是 同学小张,+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++等相关资料,持续收集更新中! 包括但不限于:
清华大学 - DeepSeek资料合集(多篇)
DeepSeek指导手册(24页).pdf
《如何向 ChatGPT 提问以获得高质量答案:提示技巧工程完全指南》
《OpenAI:GPT 最佳实践(大白话编译解读版)》
人工智能精选电子书