OpenGL学习笔记(十二):初级光照:投光物/多光源(平行光、点光源、聚光)

server/2025/2/11 4:16:26/

文章目录

  • 平行光
  • 点光源
  • 聚光
  • 多光源


现实世界中,我们有很多种类的光照,每种的表现都不同。将光投射(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(-light.direction);...
}

首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。

最终的lightDir向量将和以前一样用在漫反射和镜面光计算中:

	// diffuse vec3 norm = normalize(Normal);//vec3 lightDir = normalize(light.position - FragPos);vec3 lightDir = normalize(-light.direction);float diff = max(dot(norm, lightDir), 0.0);vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;  // specularvec3 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; 

同时,不要忘记定义光源的方向(注意我们将方向定义为从光源出发的方向,你可以很容易看到光的方向朝下)。

lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);

为了清楚地展示定向光对多个物体具有相同的影响,我们将会再次定义了十个不同的箱子位置,并对每个箱子都生成了一个不同的模型矩阵,每个模型矩阵都包含了对应的局部-世界坐标变换

for(unsigned int i = 0; i < 10; i++)
{glm::mat4 model;model = glm::translate(model, cubePositions[i]);float angle = 20.0f * i;model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));lightingShader.setMat4("model", model);glDrawArrays(GL_TRIANGLES, 0, 36);
}
  • 我们一直将光的位置和位置向量定义为vec3,但一些人会喜欢将所有的向量都定义为vec4
  • 当我们将位置向量定义为一个vec4时,很重要的一点是要将w分量设置为1.0,这样 变换和投影 才能正确应用。
  • 然而,当我们定义一个方向向量为vec4的时候,我们不想让位移有任何的效果(因为它仅仅代表的是方向),所以我们将w分量设置为0.0
  • 这也可以作为一个快速检测光照类型的工具:你可以检测w分量是否等于1.0,来检测它是否是光的位置向量;w分量等于0.0,则它是光的方向向量,这样就能根据这个来调整光照计算了。
  • 这正是旧OpenGL决定光源是定向光还是位置光源(Positional Light Source)的方法,并根据它来调整光照。

完整代码参考

在这里插入图片描述

点光源

定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减

想象作为投光物的灯泡和火把,它们都是点光源。

之前我们一直都在使用一个小的立方体当作简易的点光源。但我们定义的光源模拟的是永远不会衰减的光线,这看起来像是光源亮度非常的强。在大部分的3D模拟中,我们都希望模拟的光源仅照亮光源附近的区域而不是整个场景。

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。接下来为了计算衰减值,我们定义3个项:常数项Kc、一次项Kl和二次项Kq

在这里插入图片描述

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

更新 片段着色器 中的Light结构体:

struct Light {vec3 position;  vec3 ambient;vec3 diffuse;vec3 specular;float constant;float linear;float quadratic;
};

我们希望光源能够覆盖50的距离(覆盖距离和系数关系可以由Ogre3D的Wiki查到),然后我们将在OpenGL中设置这些项:

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

根据公式计算衰减值,之后再分别乘以环境光、漫反射和镜面光分量。

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;

点光源就是一个能够配置位置和衰减的光源。它是我们光照工具箱中的又一个光照类型。完整代码参考

在这里插入图片描述

聚光

聚光(Spotlight)是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。

聚光很好的例子就是路灯或手电筒。

OpenGL中聚光是用一个世界空间位置一个方向一个切光角(Cutoff Angle) 来表示的,切光角指定了聚光的半径。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。

在这里插入图片描述

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

所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积(它会返回两个单位向量夹角的余弦值),并将它与切光角ϕ值对比。你现在应该了解聚光究竟是什么了,下面我们将以手电筒的形式创建一个聚光。


手电筒(Flashlight) 是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。

更新片段着色器,我们需要的值有聚光的位置向量(来计算光的方向向量)聚光的方向向量和一个切光角。将它们储存在Light结构体中:

struct Light {vec3  position;vec3  direction;float cutOff;...
};

将合适的值传到着色器中:

lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));

我们并没有给切光角设置一个角度值,反而是用角度值计算了一个余弦值,将余弦结果传递到片段着色器中。
这样做的原因是在片段着色器中,我们会计算LightDirSpotDir向量的点积,这个点积返回的将是一个余弦值而不是角度值,所以我们不能直接使用角度值和余弦值进行比较。为了获取角度值我们需要计算点积结果的反余弦,这是一个开销很大的计算。所以为了节约一点性能开销,我们将会计算切光角对应的余弦值,并将它的结果传入片段着色器中。由于这两个角度现在都由余弦角来表示了,我们可以直接对它们进行比较而不用进行任何开销高昂的计算。

接下来就是计算θ值,并将它和切光角ϕ对比,来决定是否在聚光的内部:

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

我们首先计算了lightDir和取反的direction向量(取反的是因为让向量指向光源而不是从光源出发)之间的点积。对所有的相关向量标准化。

因为比较的是余弦cos值,θ比切光角的小,θ的余弦比切光角的余弦大。

完整代码参考

在这里插入图片描述

与真实的手电筒看起来还是有区别。真实情况下边缘应该是逐渐变暗的,而不是界限分明的。

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

实现一个在聚光外是负的,在内圆锥内大于1.0的,在边缘处于两者之间的强度值。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要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;
...

使用了clamp函数把第一个参数约束(Clamp)在了0.0到1.0之间。这保证强度值不会在[0, 1]区间之外。

使用的内切光角是12.5,外切光角是17.5效果:完整代码参考

在这里插入图片描述

多光源

结合之前学过的所有知识,我们可以将多种光源结合起来布置在同一场景中。

为了在场景中使用多个光源,我们希望将光照计算封装到GLSL函数中。这样做的原因是,每一种光源都需要一种不同的计算方法,而一旦我们想对多个光源进行光照计算时,代码很快就会变得非常复杂。如果我们只在main函数中进行所有的这些计算,代码很快就会变得难以理解。

GLSL中的函数和C函数很相似,它有一个函数名、一个返回值类型,如果函数不是在main函数之前声明的,我们还必须在代码文件顶部声明一个原型。我们对每个光照类型都创建一个不同的函数:定向光、点光源和聚光。

当我们在场景中使用多个光源时,通常使用以下方法:我们需要有一个单独的颜色向量代表片段的输出颜色。对于每一个光源,它对片段的贡献颜色将会加到片段的输出颜色向量上。所以场景中的每个光源都会计算它们各自对片段的影响,并结合为一个最终的输出颜色。大体的结构会像是这样:

out vec4 FragColor;void main()
{// 定义一个输出颜色值vec3 output;// 将定向光的贡献加到输出中output += someFunctionToCalculateDirectionalLight();// 对所有的点光源也做相同的事情for(int i = 0; i < nr_of_point_lights; i++)output += someFunctionToCalculatePointLight();// 也加上其它的光源(比如聚光)output += someFunctionToCalculateSpotLight();FragColor = vec4(output, 1.0);
}

封装后的片段着色器:(完整源码参考)

#version 330 core
out vec4 FragColor;struct Material {sampler2D diffuse;sampler2D specular;float shininess;
}; struct DirLight {vec3 direction;vec3 ambient;vec3 diffuse;vec3 specular;
};struct PointLight {vec3 position;float constant;float linear;float quadratic;vec3 ambient;vec3 diffuse;vec3 specular;
};struct SpotLight {vec3 position;vec3 direction;float cutOff;float outerCutOff;float constant;float linear;float quadratic;vec3 ambient;vec3 diffuse;vec3 specular;       
};#define NR_POINT_LIGHTS 4in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;uniform vec3 viewPos;
uniform DirLight dirLight;
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform SpotLight spotLight;
uniform Material material;// function prototypes
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir);void main()
{    // propertiesvec3 norm = normalize(Normal);vec3 viewDir = normalize(viewPos - FragPos);// == =====================================================// Our lighting is set up in 3 phases: directional, point lights and an optional flashlight// For each phase, a calculate function is defined that calculates the corresponding color// per lamp. In the main() function we take all the calculated colors and sum them up for// this fragment's final color.// == =====================================================// phase 1: directional lightingvec3 result = CalcDirLight(dirLight, norm, viewDir);// phase 2: point lightsfor(int i = 0; i < NR_POINT_LIGHTS; i++)result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);    // phase 3: spot lightresult += CalcSpotLight(spotLight, norm, FragPos, viewDir);    FragColor = vec4(result, 1.0);
}// calculates the color when using a directional light.
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{vec3 lightDir = normalize(-light.direction);// diffuse shadingfloat diff = max(dot(normal, lightDir), 0.0);// specular shadingvec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);// combine resultsvec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));return (ambient + diffuse + specular);
}// calculates the color when using a point light.
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{vec3 lightDir = normalize(light.position - fragPos);// diffuse shadingfloat diff = max(dot(normal, lightDir), 0.0);// specular shadingvec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);// attenuationfloat distance = length(light.position - fragPos);float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));    // combine resultsvec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));ambient *= attenuation;diffuse *= attenuation;specular *= attenuation;return (ambient + diffuse + specular);
}// calculates the color when using a spot light.
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{vec3 lightDir = normalize(light.position - fragPos);// diffuse shadingfloat diff = max(dot(normal, lightDir), 0.0);// specular shadingvec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);// attenuationfloat distance = length(light.position - fragPos);float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));    // spotlight intensityfloat theta = dot(lightDir, normalize(-light.direction)); float epsilon = light.cutOff - light.outerCutOff;float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);// combine resultsvec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));ambient *= attenuation * intensity;diffuse *= attenuation * intensity;specular *= attenuation * intensity;return (ambient + diffuse + specular);
}

http://www.ppmy.cn/server/166657.html

相关文章

ArcGIS中的空值问题

空值是数据的一种特殊状态&#xff0c;当某个字段没有被赋值或者在数据获取、处理过程中数据缺失时就会出现空值。 01、空值的表现形式 【空值】不同于数字型的【零值】或者字符串型的【空格】。 数字型的【零值】是一个确定的数值&#xff0c;表示数量上为0。 字符串型的【…

换电脑了如何快速导出vscode里的插件

当你换电脑了&#xff0c;之前vscode里的插件又不想全部手动重装&#xff0c;那么恭喜你&#xff0c;刷到了这篇文章。 1. 将 VSCode 添加到系统路径 macOS 打开 VSCode。按下 Command Shift P 打开命令面板。 3。 输入 Shell Command: Install ‘code’ command in PATH …

【C语言篇】深入探究 C 语言指针:揭开指针变量与地址的神秘面纱

我的个人主页 我的专栏&#xff1a;C语言&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 目录 引言指针的基础概念 2.1 什么是指针2.2 指针的声明与初始化2.3 指针的存储模型与内存布局 指针的操作 3.1 获取地址与解引用操作3.2 指针的算术…

spring 学习(工厂方式 实例化对象(静态工厂,实例化工厂,实现factorybean 规范))

目录 前言 第一种&#xff1a;静态工厂方式实例化对象 静态工厂的特点 demo(案例&#xff09; 第二种&#xff1a;实例工厂的方式 实例工厂和静态工厂的区别 demo(案例&#xff09; 第三种&#xff1a;实现FactoryBean规范的方式 demo(案例&#xff09; 前言 spring 实…

IDEA启动项目慢问题处理

IDEA启动项目慢问题处理 一、问题现象二、问题排查排查点1&#xff1a;idea内存排查点2&#xff1a;应用内存排查点3&#xff1a;shorten command lineclasspath filejar manifest 排查点4&#xff1a;jstack排查 三、问题定位 一、问题现象 多模块工程&#xff0c;启动模块为…

以Unity6.0为例,如何在Unity中开启DLSS功能

DLSS DLSS&#xff08;NVIDIA 深度学习超级采样&#xff09;&#xff1a;NVIDIA DLSS 是一套由 GeForce RTX™ Tensor Core 提供支持的神经渲染技术&#xff0c;可提高帧率&#xff0c;同时提供可与原生分辨率相媲美的清晰、高质量图像。目前最新突破DLSS 4 带来了新的多帧…

web3是什么,最简单的介绍

Web3是指第三代互联网技术&#xff0c;也被称为分布式互联网。它是在传统互联网&#xff08;Web2.0&#xff09;基础上发展出来的一种新技术体系&#xff0c;旨在通过区块链技术来重新定义人们访问和使用网络服务的方式。以下是关于Web3的详细介绍&#xff1a; 一、核心特点 …

【OneAPI】通过网页预渲染让搜索引擎收录网页

API简介 网页预渲染&#xff0c;适用于动态网页以及单页面的SEO&#xff0c;支持网页缓存。 您无须更改代码即可让搜索引擎收录您的网页。只要将需要预渲染的页面转发的本接口即可。 如果您使用Nginx作为网页服务器&#xff0c;推荐使用以下配置&#xff1a; #您的网站locat…