Phong光照模型原理及着色器实现

news2024/12/23 5:51:26

现实世界中的照明极其复杂,取决于太多因素,我们无法以有限的处理能力来计算这些因素。 因此,OpenGL 中的光照基于使用简化模型的现实近似值,这些模型更容易处理并且看起来相对相似。

这些照明模型基于我们所理解的光物理学。 其中一种模型称为 Phong 光照模型。 Phong 光照模型的主要构建块由 3 个组件组成:环境光照、漫反射光照和镜面反射光照。 下面你可以看到这些照明组件单独和组合后的样子:
在这里插入图片描述

推荐:用 NSDT设计器 快速搭建可编程3D场景。

  • 环境照明:即使在黑暗中,世界上的某个地方通常仍然有一些光(月亮,远处的光),因此物体几乎永远不会完全黑暗。 为了模拟这一点,我们使用环境照明常量,该常量总是为对象提供一些颜色。
  • 漫射照明:模拟光对对象的定向影响。 这是照明模型中视觉上最重要的组成部分。 物体的一部分越靠近光源,它就越亮。
  • 镜面照明:模拟出现在闪亮物体上的光亮点。 镜面高光更倾向于光的颜色而不是物体的颜色。

为了创建视觉上有趣的场景,我们至少要模拟这 3 个照明组件。 我们将从最简单的一个开始:环境照明。

1、环境照明

光通常不是来自单一光源,而是来自分散在我们周围的许多光源,即使它们不是立即可见的。 光的特性之一是它可以向多个方向散射和反射,到达无法直接看到的点; 因此,光可以在其他表面反射并对物体的照明产生间接影响。 考虑到这一点的算法称为全局照明算法,但这些算法复杂且计算成本昂贵。

由于我们不太喜欢复杂且昂贵的算法,因此我们将首先使用非常简单的全局照明模型,即环境照明。 正如你在上一节中看到的,我们使用了一个小的恒定(光)颜色,将其添加到对象片段的最终结果颜色中,从而使其看起来即使在没有直接光源的情况下也始终存在一些散射光 。

向场景添加环境照明非常简单。 我们获取光的颜色,将其与一个小的恒定环境因子相乘,再将其与对象的颜色相乘,并将其用作立方体对象着色器中片段的颜色:

void main()
{
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}  

如果你现在运行该程序,就会注意到第一阶段的照明现已成功应用于该对象。 该对象非常暗,但并不完全暗,因为应用了环境照明(请注意,光立方体不受影响,因为我们使用了不同的着色器)。 它应该看起来像这样:
在这里插入图片描述

2、漫射照明

环境照明本身不会产生最有趣的结果,但漫射照明却会开始对物体产生显着的视觉影响。 物体的碎片与光源发出的光线越接近,漫射照明赋予物体的亮度就越高。 为了让你更好地了解漫射照明,请查看下图:
在这里插入图片描述

在左侧,我们发现一个光源,其光线瞄准物体的单个碎片。 我们需要测量光线以什么角度接触碎片。 如果光线垂直于物体表面,则光线的影响最大。 为了测量光线和碎片之间的角度,我们使用称为法线矢量的东西,即垂直于片元表面的矢量(此处描绘为黄色箭头); 我们稍后再讨论。 然后可以使用点积轻松计算两个向量之间的角度。

你可能记得在变换章节中,两个单位向量之间的角度越小,点积就越倾向于值 1。当两个向量之间的角度为 90 度时,点积变为 0。这同样适用于 θ : θ 越大,光线对片段颜色的影响就越小。

请注意,为了(仅)获得两个向量之间角度的余弦,我们将使用单位向量(长度为 1 的向量),因此我们需要确保所有向量都已标准化,否则点积返回的不仅仅是余弦( 参见转换)。

由此产生的点积返回一个标量,我们可以用它来计算光线对片段颜色的影响,从而根据片段对光线的方向产生不同的光照片段。

那么,我们需要什么来计算漫反射照明:

  • 法线向量:垂直于顶点表面的向量。
  • 定向光线:方向向量,是光位置与片段位置之间的差向量。 为了计算这条光线,我们需要光线的位置向量和片段的位置向量。

3、法向量

法向量是垂直于顶点表面的(单位)向量。 由于顶点本身没有表面(它只是空间中的单个点),我们通过使用其周围的顶点来检索法线向量来找出顶点的表面。

我们可以使用一个小技巧,通过使用叉积来计算所有立方体顶点的法向量,但由于 3D 立方体不是一个复杂的形状,我们可以简单地将它们手动添加到顶点数据中。 可以在此处找到更新的顶点数据数组。 尝试想象法线确实是垂直于每个平面表面的向量(立方体由 6 个平面组成)。

由于我们向顶点数组添加了额外的数据,因此我们应该更新立方体的顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...

现在我们向每个顶点添加了法线向量并更新了顶点着色器,我们还应该更新顶点属性指针。 请注意,光源的立方体对其顶点数据使用相同的顶点数组,但灯着色器不使用新添加的法线向量。 我们不必更新灯的着色器或属性配置,但我们至少必须修改顶点属性指针以反映新顶点数组的大小:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

我们只想使用每个顶点的前 3 个浮点数并忽略最后 3 个浮点数,因此我们只需将步幅参数更新为浮点数大小的 6 倍即可。

使用灯着色器未完全使用的顶点数据可能看起来效率低下,但顶点数据已经从容器对象存储在 GPU 内存中,因此我们不必将新数据存储到 GPU 内存中。 与专门为灯分配新的 VBO 相比,这实际上使其效率更高。
所有的光照计算都是在片段着色器中完成的,因此我们需要将法向量从顶点着色器转发到片段着色器。 让我们这样做:

out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
} 

剩下要做的就是在片段着色器中声明相应的输入变量:

in vec3 Normal;  

4、计算漫反射颜色

现在我们有了每个顶点的法线向量,但我们仍然需要灯光的位置向量和片段的位置向量。 由于灯光的位置是单个静态变量,我们可以在片段着色器中将其声明为统一变量:

uniform vec3 lightPos;

然后在渲染循环中更新uniform(或在外部,因为它不会每帧改变)。 我们使用上一章中声明的 lightPos 向量作为漫反射光源的位置:

lightingShader.setVec3("lightPos", lightPos);  

那么我们最后需要的是实际片段的位置。 我们将在世界空间中进行所有光照计算,因此我们需要首先在世界空间中的顶点位置。 我们可以通过将顶点位置属性仅与模型矩阵(而不是视图和投影矩阵)相乘以将其转换为世界空间坐标来实现此目的。 这可以在顶点着色器中轻松完成,因此让我们声明一个输出变量并计算其世界空间坐标:

out vec3 FragPos;  
out vec3 Normal;
  
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

最后将相应的输入变量添加到片段着色器中:

in vec3 FragPos;  

这个输入变量将从三角形的 3 个世界位置向量进行插值,以形成 FragPos 向量,即每个片段的世界位置。 现在所有必需的变量都已设置,我们可以开始照明计算。

我们需要计算的第一件事是光源和片段位置之间的方向向量。 从上一节我们知道,光的方向向量是光的位置向量和片段的位置向量之间的差向量。 你可能还记得转换章节中的内容,我们可以通过将两个向量相减来轻松计算出此差异。 我们还希望确保所有相关向量最终都作为单位向量,因此我们对法线向量和结果方向向量进行归一化:

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);  

在计算光照时,我们通常不关心矢量的大小或其位置;而是关心矢量的大小。 我们只关心他们的方向。 因为我们只关心它们的方向,所以几乎所有计算都是使用单位向量完成的,因为它简化了大多数计算(如点积)。 因此,在进行光照计算时,请确保始终对相关向量进行归一化,以确保它们是实际的单位向量。 忘记对向量进行归一化是一个常见的错误。

接下来,我们需要通过取norm和lightDir向量之间的点积来计算光对当前片段的漫反射影响。 然后将结果值与光的颜色相乘以获得漫反射分量,两个向量之间的角度越大,漫反射分量越暗:

float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

如果两个向量之间的角度大于 90 度,则点积的结果实际上将变为负值,最终得到负的漫反射分量。 因此,我们使用 max 函数返回两个参数中的最高值,以确保漫反射分量(以及颜色)永远不会变为负值。 负色的光照并没有真正定义,所以最好远离它,除非你是那些古怪的艺术家之一。

现在我们有了环境光和漫反射组件,我们将两种颜色相互添加,然后将结果与对象的颜色相乘以获得结果片段的输出颜色:

vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

如果你的应用程序(和着色器)编译成功,应该看到如下内容:
在这里插入图片描述

你可以看到,通过漫射照明,立方体开始再次看起来像一个真正的立方体。 尝试在头脑中想象法向矢量,并围绕立方体移动相机,你会发现法向矢量与光线方向矢量之间的角度越大,片段变得越暗。

如果遇到困难,请将你的源代码与此处的完整源代码进行比较。

5、最后一件事

在上一节中,我们将法线向量直接从顶点着色器传递到片段着色器。 然而,片段着色器中的计算都是在世界空间中完成的,所以我们不应该将法向量也转换为世界空间坐标吗? 基本上是的,但它并不像简单地将其与模型矩阵相乘那么简单。

首先,法向量只是方向向量,并不代表空间中的特定位置。 其次,法向量没有齐次坐标(顶点位置的 w 分量)。 这意味着平移不应该对法向量产生任何影响。 因此,如果我们想要将法向量与模型矩阵相乘,我们需要通过模型矩阵左上角的 3x3 矩阵来删除矩阵的平移部分(请注意,我们也可以将法向量的 w 分量设置为 0 并与 4x4 矩阵相乘)。

其次,如果模型矩阵执行非均匀缩放,则顶点将以法线向量不再垂直于表面的方式发生变化。 下图显示了此类模型矩阵(具有非均匀缩放)对法向量的影响:

在这里插入图片描述

每当我们应用非均匀缩放(注意:均匀缩放仅改变法线的大小,而不是其方向,这可以通过标准化来轻松修复)时,法线向量不再垂直于相应的表面,这会扭曲照明。

修复此行为的技巧是使用专门为法向量定制的不同模型矩阵。 该矩阵称为法线矩阵,并使用一些线性代数运算来消除错误缩放法线向量的影响。 如果你想知道这个矩阵是如何计算的,我建议你阅读这个文章。

法线矩阵定义为“模型矩阵左上角 3x3 部分的逆矩阵的转置”。 唷,这有点拗口,如果你不太明白这意味着什么,别担心; 我们还没有讨论逆矩阵和转置矩阵。 请注意,大多数资源将法线矩阵定义为从模型视图矩阵导出,但由于我们在世界空间(而不是视图空间)中工作,我们将从模型矩阵导出它。

在顶点着色器中,我们可以使用顶点着色器中适用于任何矩阵类型的逆函数和转置函数来生成法线矩阵。 请注意,我们将矩阵转换为 3x3 矩阵,以确保它失去平移属性并且可以与 vec3 法线向量相乘:

Normal = mat3(transpose(inverse(model))) * aNormal;  

对于着色器来说,逆矩阵是一项成本高昂的操作,因此尽可能避免进行逆操作,因为它们必须在场景的每个顶点上完成。 出于学习目的,这很好,但对于高效的应用程序,你可能需要计算 CPU 上的法线矩阵,并在绘制之前通过统一将其发送到着色器(就像模型矩阵一样)。

在漫反射照明部分,照明效果很好,因为我们没有对对象进行任何缩放,因此实际上不需要使用法线矩阵,我们只需将法线与模型矩阵相乘即可。 然而,如果你正在进行非均匀缩放,则必须将法线向量与法线矩阵相乘。

6、镜面照明

如果你还没有被所有的光照讨论弄得精疲力尽,我们可以通过添加镜面高光来开始完成 Phong 光照模型。

与漫反射照明类似,镜面照明基于光的方向矢量和物体的法线矢量,但这次它也基于视图方向,例如 玩家从哪个方向观看片段。 镜面照明基于表面的反射特性。 如果我们将物体的表面视为一面镜子,那么无论我们在表面上看到反射的光,镜面照明都是最强的。 你可以在下图中看到此效果:

在这里插入图片描述

我们通过围绕法向量反射光线方向来计算反射向量。 然后我们计算该反射矢量与视图方向之间的角距离。 它们之间的角度越近,镜面光的影响就越大。 由此产生的效果是,当我们观察通过表面反射的光的方向时,我们会看到一点亮点。

视图向量是镜面照明所需的一个额外变量,我们可以使用观看者的世界空间位置和片段的位置来计算它。 然后我们计算镜面反射的强度,将其与光颜色相乘,并将其添加到环境和漫反射分量中。

我们选择在世界空间中进行照明计算,但大多数人倾向于在视图空间中进行照明。 视图空间的一个优点是观察者的位置始终位于 (0,0,0),因此你已经轻易地获得了观察者的位置。 然而,我发现计算世界空间中的光照对于学习目的来说更直观。 如果你仍然想计算视图空间中的照明,还需要使用视图矩阵来转换所有相关向量(不要忘记也更改法线矩阵)。

为了获得观察者的世界空间坐标,我们只需获取相机对象(当然是观察者)的位置向量。 因此,让我们向片段着色器添加另一个uniform,并将相机位置向量传递给着色器:

uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position); 

现在我们有了所有必需的变量,可以计算镜面反射强度。 首先,我们定义一个镜面反射强度值,为镜面高光提供中等亮度的颜色,这样它就不会产生太大的影响:

float specularStrength = 0.5;

如果我们将其设置为 1.0f,我们会得到非常明亮的镜面反射分量,这对于珊瑚立方体来说有点太多了。 在下一章中,我们将讨论如何正确设置所有这些照明强度以及它们如何影响对象。 接下来我们计算视图方向向量和相应的沿法线轴的反射向量:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm); 

请注意,我们对 lightDir 向量求反。 Reflect 函数期望第一个向量从光源指向片段的位置,但 lightDir 向量当前指向相反的方向:从片段指向光源(这取决于我们之前计算 lightDir 向量时的减法顺序)。 为了确保我们获得正确的反射向量,我们首先通过对 lightDir 向量求负来反转其方向。 第二个参数需要一个法向量,因此我们提供归一化的范向量。

然后剩下要做的就是实际计算镜面反射分量。 这是通过以下公式完成的:

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;  

我们首先计算视图方向和反射方向之间的点积(并确保它不是负数),然后将其提高到 32 次方。这个 32 值就是高光的光泽度值。 物体的光泽度值越高,它就越能正确地反射光线,而不是将光线四处散射,因此高光变得越小。 你可以在下面看到一张显示不同光泽度值的视觉影响的图像:
在这里插入图片描述

我们不希望镜面反射分量太分散注意力,因此我们将指数保持在 32。剩下要做的唯一的事情就是将其添加到环境和漫反射分量中,并将组合结果与对象的颜色相乘:

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

现在我们计算了 Phong 光照模型的所有光照组件。 根据你的视角,应该看到如下内容:

你可以在此处找到该应用程序的完整源代码。


原文链接:Phong光照模型 — BimAnt

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

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

相关文章

桥接模式-处理多维度变化

程序员小名去摆摊卖奶茶了,口味有香、甜。 型号有大、中、小。假如小名先在家里把这些奶茶装好,那么最少要装2x3 6杯奶茶,如果此时新增一个口味:酸,那么就需要多装3杯奶茶了。而且这样做,等客户买走一种&a…

c++网络编程:Boost.asio源码剖析

1、前言 Boost库是一个可移植、提供源代码的C库,作为标准库的后备,是C标准化进程的开发引擎之一。Boost库由C标准委员会库工作组成员发起,其中有些内容有望成为下一代C标准库内容。在C社区中影响甚大,是不折不扣的“准”标准库。…

HCIA练习4

题目如下: 目录 第一步:IP的规划 第二步:缺省路由 第三步:开启telnet 第四步:编写ACL表 第五步:测试 思路分析: 华为默认允许所有,所以我们可以先写拒绝要求,再写允…

MobaXterm通过SSH访问Ubuntu服务器遇到的一个问题

在Windows下的MobaXterm界面配置完ubuntuIP以后显示access denied,排查发现是因为在ubuntu那边忘记安装了SSH Serve,安装过程如下: 第一步:安装所需包 让我们从打开终端输入一些必要命令开始。 注意,在安装新的包或…

【独家解答】面试题曝光:Spring容器中的Bean会不会被GC?

大家好,我是你们的小米!今天,我们来聊一个有关Spring容器中的Bean的话题,也是面试中经常被问到的问题:“Spring 容器中的Bean是否会被GC呢?为什么?”让我们一起揭开这个神秘的面纱,解…

记录一次软件安全测试过程

一.前言 xx的安测已经过去一段时间了,这两天xx需求刚发布。xx的外呼需求文档还没确定,所以趁着这个时间,进行一下复盘。这次xx安测,全程参与,经历了3轮,从5月份开始,到6月底正式通过安测,实属不易。安测主要分为漏洞扫描、越权扫描、日志审查、软件版本基线检查下面也将…

Visual Studio 2022 程序员必须知道高效调试手段与技巧(上)

🎬 鸽芷咕:个人主页 🔥 个人专栏:《C语言初阶篇》 《C语言进阶篇》 ⛺️生活的理想,就是为了理想的生活! 文章目录 📋 前言💬 什么是bug?💬 调试是什么?有多重要&#x…

css设置八等分圆

现需要上图样式的布局,我通过两张向右方的图片,通过定位和旋转完成了布局。 问题: 由于是通过旋转获取到的样式,实际的盒子是一个长方形,当鼠标移入对应的箭头时选中的可能是其他盒子,如第一张设计稿可以看…

uniapp 语音文本播报功能

最近uniapp项目上遇到一个需求 就是在接口调用成功的时候加上语音播报 , ‘创建成功’ ‘开始成功’ ‘结束成功’ 之类的。 因为是固定的文本 ,所以我先利用工具生成了 文本语音mp3文件,放入项目中,直接用就好了。 这里用到的工…

Android adb shell 查看App内存(java堆内存/vss虚拟内存/详细的内存状况/内存快照hprof)和系统可用内存

1.adb shell 获取app 进程的pid adb shell "ps|grep com.xxx包名"根据某个渠道包,去查询对应的pid,如下所示: 2.通过adb shell 查看设备的java dalvik 堆内存的最大值 执行命令行: adb shell getprop dalvik.vm.h…

【Java SE】继承与多态

目录 【1】继承 【1.1】为什么要继承 【1.2】继承概念 【1.3】继承的语法 【1.4】父类成员访问 【1.4.1】子类中访问父类的成员变量 【1.4.2】子类中访问父类的成员变量 【1.5】super关键字 【1.6】子类构造方法 【1.7】super和this 【1.8】再谈初始化 【1.9】prot…

element-plus中的collapse组件中的箭头图标移到左边问题解决(改进版)

1、问题BUG 这个问题虽然解决了,但是依旧有一些bug,首先我们来看一下,他的样式变成了什么样的。 下面就是我们修改后的样子了,但是对于我们的需求来说,左边的箭头样式是没问题了 可是到了箭头到了右边却变成了下面的…

17_LinuxLCD驱动

目录 Framebuffer设备 LCD驱动简析 LCD驱动程序编写 LCD屏幕参数节点信息修改 LCD 屏幕背光节点信息 使能Linux logo显示 设置LCD作为终端控制台 Framebuffer设备 先来回顾一下裸机的时候LCD驱动是怎么编写的,裸机LCD驱动编写流程如下: 1.初始化I.MX6U的eLCDIF控制器,…

Gumbel Softmax Trick

Gumbel Softmax Trick 重参数化技巧(re-parameters trick)Gumbel softmax trick基于Softmax的采样基于Gumbel-max的采样基于Gumbel-softmax采样Softmax中的温度系数tau 算法学习之gumbel softmax 【Learning Notes】Gumbel 分布及应用浅析 gumbel-softma…

Failed to connect to 127.0.0.1 port 7890科学上网导致的问题

找了很多种解法: 首先这个.config配置文件有两个地方存在:先使用第一种方式,不管用再试第二种 第一个位置git安装路径:不需要重启 E:\git\Git\etc,这个需要看你自己的安装路径,找到http_proxy删除即可第二…

类型转换函数

再论类型转换 标准数据类型之间会进行隐式的类型安全转换 转换规则如下: 问题 普通类型与类类型之间能否进行类型转换? 类类型之间能否进行类型转换? 再论构造函数 构造函数可以定义不同类型的参数 参数满足下列条件时称为转换构造函数…

LocalDateTime的json格式化问题

目录 解决: 1、注册日期序列化器 2、自定义LocalDateTime的JSON格式 3、使用第三方库 总结: 实体类中定义了LocalDateTime类型的属性,获取数据会出现以下日期格式问题: 讲述: 对于LocalDateTime的JSON序列化和反序…

解析数据可视化工具:如何选择最合适的软件

在当今信息爆炸的时代,数据已成为各行各业的重要资源。为了更好地理解和分析数据,数据可视化成为一种必不可少的工具。市面上数据可视化工具不说上千也有上百,什么帆软、powerbi、把阿里datav,腾讯云图、山海鲸可视化等等等等&…

【VCS】(6)Code Coverage

Code Coverage VCS 中 Code Coverage 的类型Code Coverage Flow代码覆盖率选项Lab Code Coverage初步尝试其他格式的覆盖率报告屏蔽部分代码屏蔽整个模块 设计和验证到底要做到什么程度? 这里其中一个指标就是 Code Coverage。 代码覆盖率一般考虑以下几个方面&…

前置操作符和后置操作符

下面的代码有没有区别?为什么? 意想不到的事实 现代编译器产品会对代码进行优化 优化使得最终的二进制程序更加高效 优化后的二进制程序丢失了 C/C 的原生语义 不可能从编译后的二进制程序还原 C/C 程序 思考 操作符可以重载吗? 如何区分…