【OpenGL手册15】 多光源

news2024/11/16 3:17:39

目录

  • 一、说明
  • 二、定向光
  • 三、点光源
  • 四、合并结果
  • 练习

一、说明

   我们在前面的教程中已经学习了许多关于OpenGL中光照的知识,其中包括冯氏着色(Phong Shading)、材质(Material)、光照贴图(Lighting Map)以及不同种类的投光物(Light Caster)。在这一节中,我们将结合之前学过的所有知识,创建一个包含六个光源的场景。我们将模拟一个类似太阳的定向光(Directional Light)光源,四个分散在场景中的点光源(Point Light),以及一个手电筒(Flashlight)。

   为了在场景中使用多个光源,我们希望将光照计算封装到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);
}

   实际的代码对每一种实现都可能不同,但大体的结构都是差不多的。我们定义了几个函数,用来计算每个光源的影响,并将最终的结果颜色加到输出颜色向量上。例如,如果两个光源都很靠近一个片段,那么它们所结合的贡献将会形成一个比单个光源照亮时更加明亮的片段。

二、定向光

   我们需要在片段着色器中定义一个函数来计算定向光对相应片段的贡献:它接受一些参数并计算一个定向光照颜色。

   首先,我们需要定义一个定向光源最少所需要的变量。我们可以将这些变量储存在一个叫做DirLight的结构体中,并将它定义为一个uniform。需要的变量在上一节中都介绍过:

struct DirLight {
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  

uniform DirLight dirLight;

   接下来我们可以将dirLight传入一个有着以下原型的函数。

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);

   和C/C++一样,如果我们想调用一个函数(这里是在main函数中调用),这个函数需要在调用者的行数之前被定义过。在这个例子中我们更喜欢在main函数以下定义函数,所以上面要求就不满足了。所以,我们需要在main函数之上定义函数的原型,这和C语言中是一样的。

   你可以看到,这个函数需要一个DirLight结构体和其它两个向量来进行计算。如果你认真完成了上一节的话,这个函数的内容应该理解起来很容易:

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 合并结果
    vec3 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);
}

   我们基本上只是从上一节中复制了代码,并使用函数参数的两个向量来计算定向光的贡献向量。最终环境光、漫反射和镜面光的贡献将会合并为单个颜色向量返回。

三、点光源

   和定向光一样,我们也希望定义一个用于计算点光源对相应片段贡献,以及衰减,的函数。同样,我们定义一个包含了点光源所需所有变量的结构体:

struct PointLight {
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  

#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];

   你可以看到,我们在GLSL中使用了预处理指令来定义了我们场景中点光源的数量。接着我们使用了这个NR_POINT_LIGHTS常量来创建了一个PointLight结构体的数组。GLSL中的数组和C数组一样,可以使用一对方括号来创建。现在我们有四个待填充数据的PointLight结构体。

   我们也可以定义一个大的结构体(而不是为每种类型的光源定义不同的结构体),包含所有不同种光照类型所需的变量,并将这个结构体用到所有的函数中,只需要忽略用不到的变量就行了。然而,我个人觉得当前的方法会更直观一点,不仅能够节省一些代码,而且由于不是所有光照类型都需要所有的变量,这样也能节省一些内存。

   点光源函数的原型如下:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);

   这个函数从参数中获取所需的所有数据,并返回一个代表该点光源对片段的颜色贡献的vec3。我们再一次聪明地从之前的教程中复制粘贴代码,完成了下面这样的函数:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 衰减
    float distance    = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
                 light.quadratic * (distance * distance));    
    // 合并结果
    vec3 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);
}

   将这些功能抽象到这样一个函数中的优点是,我们能够不用重复的代码而很容易地计算多个点光源的光照了。在main函数中,我们只需要创建一个循环,遍历整个点光源数组,对每个点光源调用CalcPointLight就可以了。

四、合并结果

   现在我们已经定义了一个计算定向光的函数和一个计算点光源的函数了,我们可以将它们合并放到main函数中。

void main()
{
    // 属性
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    // 第一阶段:定向光照
    vec3 result = CalcDirLight(dirLight, norm, viewDir);
    // 第二阶段:点光源
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);    
    // 第三阶段:聚光
    //result += CalcSpotLight(spotLight, norm, FragPos, viewDir);    

    FragColor = vec4(result, 1.0);
}

   每个光源类型都将它们的贡献加到了最终的输出颜色上,直到所有的光源都处理完了。最终的颜色包含了场景中所有光源的颜色影响所合并的结果。如果你想的话,你也可以实现一个聚光,并将它的效果加到输出颜色中。我们会将CalcSpotLight函数留给读者作为练习。

   设置定向光结构体的uniform应该非常熟悉了,但是你可能会在想我们该如何设置点光源的uniform值,因为点光源的uniform现在是一个PointLight的数组了。这并不是我们以前讨论过的话题。

   很幸运的是,这并不是很复杂,设置一个结构体数组的uniform和设置一个结构体的uniform是很相似的,但是这一次在访问uniform位置的时候,我们需要定义对应的数组下标值:

lightingShader.setFloat("pointLights[0].constant", 1.0f);

   在这里我们索引了pointLights数组中的第一个PointLight,并获取了constant变量的位置。但这也意味着不幸的是我们必须对这四个点光源手动设置uniform值,这让点光源本身就产生了28个uniform调用,非常冗长。你也可以尝试将这些抽象出去一点,定义一个点光源类,让它来为你设置uniform值,但最后你仍然要用这种方式设置所有光源的uniform值。

   别忘了,我们还需要为每个点光源定义一个位置向量,所以我们让它们在场景中分散一点。我们会定义另一个glm::vec3数组来包含点光源的位置:

glm::vec3 pointLightPositions[] = {
    glm::vec3( 0.7f,  0.2f,  2.0f),
    glm::vec3( 2.3f, -3.3f, -4.0f),
    glm::vec3(-4.0f,  2.0f, -12.0f),
    glm::vec3( 0.0f,  0.0f, -3.0f)
};

   接下来我们从pointLights数组中索引对应的PointLight,将它的position值设置为刚刚定义的位置值数组中的其中一个。同时我们还要保证现在绘制的是四个灯立方体而不是仅仅一个。只要对每个灯物体创建一个不同的模型矩阵就可以了,和我们之前对箱子的处理类似。

   如果你还使用了手电筒的话,所有光源组合的效果将看起来和下图差不多:
在这里插入图片描述

   你可以看到,很显然天空中有一个全局照明(像一个太阳),我们有四个光源分散在场景中,以及玩家视角的手电筒。看起来是不是非常不错?

   你可以在这里找到最终程序的源代码。

   上面图片中的所有光源都是使用上一节中所使用的默认属性,但如果你愿意实验这些数值的话,你能够得到很多有意思的结果。艺术家和关卡设计师通常都在编辑器中不断的调整这些光照参数,保证光照与环境相匹配。在我们刚刚创建的简单光照环境中,你可以简单地调整一下光源的属性,创建很多有意思的视觉效果:
在这里插入图片描述

   我们也改变了清屏的颜色来更好地反应光照。你可以看到,只需要简单地调整一些光照参数,你就能创建完全不同的氛围。

   相信你现在已经对OpenGL的光照有很好的理解了。有了目前所学的这些知识,我们已经可以创建出丰富有趣的环境和氛围了。尝试实验一下不同的值,创建出你自己的氛围吧。

练习

   你能通过调节光照属性变量,(大概地)重现最后一张图片上不同的氛围吗

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1510158.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

回收站删除的文件怎么恢复?这3个方法可行!

“求助&#xff01;回收站删除的文件还有没有机会恢复呀&#xff1f;我不小心将回收站清空了&#xff0c;但是有一些很重要的文件还在里面&#xff0c;应该怎么恢复它们啊&#xff01;” 在使用电脑的过程中&#xff0c;我们难免会遇到误删文件的情况。而当文件被删除并清空回收…

c++: 引用能否替代指针? 详解引用与指针的区别.

文章目录 前言1. 引用和指针的最大区别:引用不能改变指向2. 引用和指针在底层上面是一样的3. 引用和指针在sizeof面前大小不同4. 有多级指针,没有多级引用5.引用是引用的实体,指针会向后偏移同一个类型的大小 总结 前言 新来的小伙伴如果不知道引用是什么?可以看我的上一篇文…

Kubernetes | 起源 | 组件详解

起源 起源&#xff1a; Kubernetes&#xff08;常简称为K8s&#xff09;起源于Google内部的Borg项目&#xff0c;是一个开源的容器编排引擎&#xff0c;于2014年首次对外发布。 Google Borg Google Borg 是 Google 内部开发和使用的大规模集群管理系统&#xff0c;用于管理和运…

锐捷 EWEB auth 远程命令执行漏洞复现

一、漏洞信息 漏洞名称:锐捷 EWEB auth 远程命令执行漏洞 漏洞类别:远程代码执行 风险等级:高危 二、漏洞描述 锐捷睿易是锐捷网络针对商业市场的子品牌。拥有易网络、交换机、路由器、无线、安全、云服务六大产品线,解决方案涵盖商贸零售、酒店、KTV、网吧、监控安防…

GPT出现Too many requests in 1 hour. Try again later.

换节点 这个就不用多说了&#xff0c;你都可以上GPT帐号了&#xff0c;哈…… 清除cooki 然后退出账号&#xff0c;重新登录即可

KBL610-ASEMI开关电源专用KBL610

编辑&#xff1a;ll KBL610-ASEMI开关电源专用KBL610 型号&#xff1a;KBL610 品牌&#xff1a;ASEMI 封装&#xff1a;KBL-4 最大重复峰值反向电压&#xff1a;1000V 最大正向平均整流电流(Vdss)&#xff1a;6A 功率(Pd)&#xff1a;中小功率 芯片个数&#xff1a;4 …

encoding和embedding的区别

本文作者&#xff1a; slience_me 文章目录 encoding和embedding的区别EmbeddingEncoding总结 encoding和embedding的区别 “Embedding” 和 “Encoding” 是两个在计算机科学和机器学习领域中常用的术语&#xff0c;它们虽然有些相似&#xff0c;但指代的概念和用途有所不同。…

国创证券|lpr下调25个基点是多少?lpr下调对股市债市有什么影响?

lpr是借款市场报价利率&#xff0c;其间lpr下调25个基点是指lpr利率下降0.25%&#xff0c;比方&#xff0c;下调之前五年期以上的lpr为4.2%&#xff0c;下调25个基点之后&#xff0c;变为3.95%。 lpr下调对股市债市存在以下影响&#xff1a; 1、券商股 借款利率下降&#xf…

发现创作灵感的聚集地:乐歌M9G与M9S升降办公台对比解析

在现代工作环境中&#xff0c;全天待在桌前的时间已经超过了其他的项目&#xff0c;尤其是以编程开发、动画设计为主的互联网从业者&#xff0c;工作量的增加&#xff0c;会导致颈椎和腰椎僵硬&#xff0c;根据人工工程学提出的人与环境之间的协调关系&#xff0c;对于上班一族…

ssm蛋糕甜品商城系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

场的概念---数量场(标量场)和矢量场介绍理解

目录 一、场的概念 二、场的分类 三、数量场&#xff08;标量场&#xff09;的等值面 四、矢量场中的矢量线 矢量线方程推导&#xff1a; 一、场的概念 场在数学上是指一个向量到另一个向量或数的映射。场指物体在空间中的分布情况。场是用空间位置函数来表征的。在物理学…

C#集合和数据结构,随笔记录没空排版,,,

C#集合和数据结构 System.Collections命名空间包含接口和类&#xff0c;这些接口和类定义各种对象&#xff08;如列表/链表、位数组、哈希表、队列和堆栈&#xff09;的集合 System.Collections.Generic命名空间&#xff1a; 所有集合都直接或间接基于ICollection接口 列表类集…

网络通信另个角度的认识(进程间通信),端口号(为什么要有,和pid的关系,如何封装,和进程的定位原理+对应关系)客户端如何拿到服务端的port

目录 另一个角度认识网络通信 端口号 引入 -- 为什么要有端口号 问题 解决 端口号和pid 举例 介绍 分类 知名端口 注册端口 动态端口 客户端如何知道服务端的端口号 封装端口号 定位原理 进程和端口号的对应关系 数据如何被上层进程读到 另一个角度认识网络…

抽样算法——【数据科学与工程算法基础】

一、前言 这是课程的第二章节——抽样算法&#xff0c;主要分为三类。 详情可参考&#xff1a; 数据科学的算法基础——学习记录跳转中心 二、正篇 1.系统抽样 课本只介绍了最简单的——等距抽样。 直线等距抽样&#xff08;Nn*k&#xff09;&#xff1a;即总体个数可以被抽…

webgl instance 绘制

webgl instance 绘制 效果: key1: 创建实例缓存 function createMesh() {for (let i 0; i < NUM_CUBE; i) {const angle i * 2 * Math.PI / NUM_CUBE;const x Math.sin(angle) * RADIUS;const y 0;const z Math.cos(angle) * RADIUS;cubes[i] {scale: new THREE.V…

从零开始实现ORB_SLAM2编译与运行

文章目录 前言一、前期准备二、库安装1.engin库安装方法1&#xff1a;apt安装方法2&#xff1a;源码安装 2.Pangolin库安装3.openCV源码安装 三、build编译四、测试五、ROS安装六、build-ros编译七、测试总结 前言 ORB_SLAM2作为一种优秀的开源VSLAM解决方案&#xff0c;以其高…

高浓度纸浆废水如何处理达标排放

在纸浆生产过程中&#xff0c;高浓度纸浆废水是一个常见的环境污染源&#xff0c;其处理对于环境保护和可持续发展至关重要。为了实现高浓度纸浆废水的达标排放&#xff0c;必须采用一系列专业的处理技术&#xff0c;以确保废水处理的高效性和环境友好性。 首先&#xff0c;通过…

Oracle之ADG与DG的区别?

在上云后的Oracle数据灾备场景中&#xff0c;我们经常听到DBA迁移工程师讲到“在这个项目中用ADG进行数据实时备份&#xff0c;ADG比DG更好&#xff01;”。究竟ADG作Oracle数据灾备的优势在什么地方&#xff1f; 一、ADG主要解决了DG时代读写不能并行的问题 DG时代的数据同步…

Kutools For Excel | 新增 300+ 高级功能

Kutools For Excel 是一个便捷的 Excel 插件&#xff0c;具有 300 多种高级功能&#xff0c;可将各种复杂的任务简化为在 Excel 中的几次单击。 功能强大且用户友好的加载项将为 Excel 用户节省大量工作时间&#xff0c;并大大提高工作效率。支持 Excel 2021 / 2019 / 2016 / …

【Python使用】python高级进阶知识md总结第3篇:静态Web服务器-返回指定页面数据,静态Web服务器-多任务版【附代码文档】

python高级进阶全知识知识笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;操作系统&#xff0c;虚拟机软件&#xff0c;Ubuntu操作系统&#xff0c;Linux内核及发行版&#xff0c;查看目录命令&#xff0c;切换目录命令&#xff0c;绝对路径和相对…