LearnOpenGL-光照章节学习笔记

LearnOpenGL-光照章节学习笔记

  • 颜色
    • 创建一个光照场景
  • 基础光照
    • 一、环境光照
    • 二、漫反射光照
    • 三、镜面反射
  • 材质
  • 光照贴图
    • 一、漫反射贴图
    • 二、镜面光贴图
    • 三、放射光贴图
  • 投光物
    • 一、平行光
    • 二、点光源
      • 衰减
      • 实现
    • 三、聚光灯
      • 平滑边缘
  • 多光源
    • 一、平行光(定向光)
    • 二、点光源

颜色

我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。
当把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。

vec3 lightColor = vec3(1.0,1.0,1.0); //光是白色
vec3 toyColor = vec3(1.0,0.5,0.31);
vec3 res = light * toyColor;

创建一个光照场景

物体的片元着色器新建两个uniform变量:lightColor、objectColor

#version 330 core
out vec4 FragColor;uniform vec3 objectColor;
uniform vec3 lightColor;void main()
{FragColor = vec4(lightColor * objectColor, 1.0);
}

我们还要创建一个光源立方体,所以要为这个物体创建专门的VAO

unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

然后设置颜色uniform变量

// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor",  1.0f, 1.0f, 1.0f);

当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯的颜色在接下来的教程中因光照计算的结果而受到影响,而是希望它能够与其它的计算分离。我们希望灯一直保持明亮,不受其它颜色变化的影响(这样它才更像是一个真实的光源)。所以为其准备一个专门的片元着色器

#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f);
}

然后根据需求分别计算物体和光源的mvp矩阵

基础光照

Phong光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
关于光照模型的原理可以看 Games101学习笔记 Shading
在这里插入图片描述

  • 环境光:其他物体的光,在Phong模型中设置为常量
  • 漫反射:假设光线到物体是均匀反射出去的,模拟光源对物体方向性影响
  • 镜面光照(高光反射):模拟有光泽物体上面出现的亮点。完全反射

一、环境光照

考虑物体接受来自其他物体的反射光,叫做全局光照,我们一般把环境光照设置为常量

float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

二、漫反射光照

计算漫反射光照需要用到法线向量,可以直接在顶点数据中定义,顶点着色器也要声明aNormal以及输出变量Normal

计算漫反射光照:diffuse = lightColor * objectColor * max(dot(normal, lightDir), 0)。为了得到光源方向,我们需要得到光源位置(在片段着色器中设置为uniform变量 lightPos)和顶点位置(在顶点着色器中作为输出 FragPos变量)

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;out vec3 FragPos;
out vec3 Normal;uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;void main()
{FragPos = vec3(model * vec4(aPos, 1.0));Normal = aNormal;  gl_Position = proj * view * vec4(FragPos, 1.0);
}
#version 330 core
out vec4 FragColor;in vec3 Normal;  
in vec3 FragPos;  uniform vec3 lightPos; 
uniform vec3 lightColor;
uniform vec3 objectColor;void main()
{// ambientfloat ambientStrength = 0.1;vec3 ambient = ambientStrength * lightColor;// diffuse vec3 norm = normalize(Normal);vec3 lightDir = normalize(lightPos - FragPos);float diff = max(dot(norm, lightDir), 0.0);vec3 diffuse = diff * lightColor;vec3 result = (ambient + diffuse) * objectColor;FragColor = vec4(result, 1.0);
} 

目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们应该把法向量也转换为世界空间坐标,我们需要一个法线矩阵来进行转换,法线矩阵被定义为「model矩阵左上角3x3部分的逆矩阵的转置矩阵」,可以在顶点着色器中进行

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

矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。

三、镜面反射

通过根据法向量翻折入射光的方向来计算反射向量。然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。

计算镜面反射:specular = lightColor * specularColor * max(dot(halfDir, normal)) ^ gloss
halfDir半程向量为光线方向与观察方向的角平分线方向向量,normalize(lightDir, viewDir)即可
viewDir即为摄像机位置-顶点位置

//specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 halfDir = normalize(viewDir + lightDir);
vec3 specular = lightColor * specularStrength * pow(max(dot(norm, halfDir), 0.0), 32);

材质

如果我们想要在OpenGL中模拟多种类型的物体,我们必须针对每种表面定义不同的材质(Material)属性。我们可以分别为三个光照分量定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。通过为每个分量指定一个颜色,我们就能够对表面的颜色输出有细粒度的控制了。
在片段着色器中,我们创建一个结构体(Struct)来储存物体的材质属性

#version 330 core
struct Material {vec3 ambient;vec3 diffuse;vec3 specular;float shininess; //反光度(Shininess)分量,影响镜面高光的散射/半径。
}; uniform Material material;

如果要调整每个部分的光照影响度

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

光照贴图

一、漫反射贴图

它是一个表现了物体所有的漫反射颜色的纹理图像。在着色器中使用漫反射贴图的方法和“纹理”教程是完全一样的。但这次会将纹理储存为Material结构体中的一个sampler2D。我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图

struct Material {sampler2D diffuse;vec3      specular;float     shininess;
}; 
...
in vec2 TexCoords;

更新两个VAO的顶点属性指针来匹配新的顶点数据,并加载箱子图像为一个纹理。在绘制箱子之前,我们希望将要用的纹理单元赋值到material.diffuse这个uniform采样器,并绑定箱子的纹理到这个纹理单元

lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);

二、镜面光贴图

对于木头来说,我们不想让它镜面反射,可以将镜面反射材质设为vec3(0.0),但是对于金属边框,我们可以使用专门用于镜面光的贴图。
镜面高光的强度可以通过每个像素的亮度(每个像素都可以用一个颜色向量表示)来获取,在片元着色器中,使用采样对应的颜色值乘上镜面强度

struct Material {sampler2D diffuse;sampler2D specular;float     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));
FragColor = vec4(ambient + diffuse + specular, 1.0);

在主程序中,新建一个纹理,在渲染之前先把它绑定到合适的纹理单元上

lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);

三、放射光贴图

是储存了每个片段的发光值(Emission Value)的贴图
直接用采样得到的像素值来附在纹理上就好,不受光照影响

struct Material {sampler2D diffuse;sampler2D specular;sampler2D emissive;float shininess; 
}; 
......//自发光
vec3 emissive = vec3(texture(material.emissive, TexCoords)).rgb;
vec3 result = (ambient + diffuse + specular + emissive) * objectColor;
FragColor = vec4(result, 1.0);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, emissiveMap);

投光物

将光投射(Cast)到物体的光源叫做投光物(Light Caster)。

一、平行光

假设光源处于无限远处的模型时,它就被称为定向光,光源的每条光线就会近似于互相平行,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。

直接使用光的direction向量而不是通过position来计算lightDir向量。

struct Light {// vec3 position; // 使用定向光就不再需要了vec3 direction;vec3 ambient;vec3 diffuse;vec3 specular;
};
...
void main()
{vec3 lightDir = normalize(-light.direction);...
}

二、点光源

光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减

衰减

根据片元距光源的距离计算了衰减值
在这里插入图片描述
我们需要定义三个可配置的量:常数项 K c K_{c} Kc、一次项 K l K_{l} Kl、二次项 K q K_{q} Kq

  • 常数项通常保持1.0,用途是保证分母不会小于1,否则会发生距离增大而光线增强的效果
  • 一次项以线性减少强度
  • 二次项会与 d 2 d^{2} d2 相乘,二次递减

在这里插入图片描述
在这里插入图片描述

实现

需要将light.direction,改为light.position,还需要增加三个量:常数项 K c K_{c} Kc、一次项 K l K_{l} Kl、二次项 K q K_{q} Kq

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

假设光源照亮范围为50

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

三、聚光灯

聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。OpenGL中聚光是用 一个世界空间位置、一个方向和一个切光角(Cutoff Angle) 来表示的,切光角指定了聚光的半径。
在这里插入图片描述

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • ϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
  • θ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
struct Light {vec3  position;vec3  direction;float cutOff;...
};

在主程序中,直接将 cosΦ 传入。

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

在片元着色器中,我们要对θ和Φ进行比较,如果 θ < Φ,那么就可以进行正常光照计算,反之就返回环境光照。要实现两个角度的比较,可以通过比较余弦值的大小(就不用耗性能的求反三角函数来得到角度)

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

平滑边缘

为了让聚光灯的边缘平滑,模拟聚光灯时需要一个外圆锥(再创建一个)和内圆锥(上面的圆锥)

为了创建一个外圆锥,我们只需要再定义一个余弦值γ来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
在这里插入图片描述

  • ϵ=ϕ−γ,内(ϕ)和外圆锥(γ)之间的余弦值差
    在这里插入图片描述

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]区间之外
  • 不需要再判断θ 和 Φ,因为已经有了强度值的限制

多光源

要进行多个光源计算,如果都放在main函数里那么会非常冗杂,可以为每种灯光设置相应的函数,然后汇总到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);
}

一、平行光(定向光)

可以在片元着色器中专门声明一个定向光的结构体

struct DirLight {vec3 direction;vec3 ambient;vec3 diffuse;vec3 specular;
};  
uniform DirLight dirLight;

然后将结构体传入以下函数

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{vec3 lightDir = light.direction;vec3 halfDir = normalize(lightDir + viewDir);vec3 diffuse = light.diffuse * vec3(texture(material.diffuse, TexCoords)) * max(dot(lightDir, normal), 0.0);vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));vec3 specular = light.specular * vec3(texture(material.specular, TexCoords)) * pow(max(dot(halfDir, normal), 0.0), material.shininess);return (diffuse + ambient + specular);
}

二、点光源

点光源位置、衰减…定义一个点光源结构体(需要四个点光源,在GLSL中使用了预处理指令来定义了我们场景中点光源的数量)

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];
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{float distance = length(light.position - fragPos);vec3 lightDir = normalize(lightPos - fragPos);vec3 halfDir = normalize(lightDir + viewDir);float atten = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);vec3 diffuse = light.diffuse * vec3(texture(material.diffuse, TexCoords)) * max(dot(normal, lightDir)) * atten;vec3 specular = light.specular * vec3(texture(material.specular, TexCoords)) * pow(max(dot(normal, halfDir), 0.0), material.shininess) * atten;vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)) * atten;return(diffuse + specular + ambient);
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

免费代理池是什么,如何使用代理IP进行网络爬虫?

互联网是一个庞大的数据集合体&#xff0c;网络信息资源丰富且繁杂&#xff0c;想要从中找到自己需要的信息要花费较多的时间。为了解决这个问题&#xff0c;网络爬虫技术应运而生&#xff0c;它的主要作用就是在海量的互联网信息中进行爬取&#xff0c;抓取有效信息并存储。然…

广州城市信息模型(CIM)白皮书学习

CIM平台定义 以建筑信息模型(BIM)、地理信息系统(GIS)、物联网(IoT)等技术为基础&#xff0c;整合城市地上地下、室内室外、历史现状未来多维多尺度信息模型数据和城市感知数据&#xff0c;构建起三维数字空间的城市信息有机综合体。 广州CIM平台建设历程 2019 年 6 月住房和…

动手学深度学习V2每日笔记(深度卷积神经网络AlexNet)

本文主要参考沐神的视频教程 https://www.bilibili.com/video/BV1h54y1L7oe/spm_id_from333.788.recommend_more_video.0&vd_sourcec7bfc6ce0ea0cbe43aa288ba2713e56d 文档教程 https://zh-v2.d2l.ai/ 本文的主要内容对沐神提供的代码中个人不太理解的内容进行笔记记录&…

13021.Nvidia AGX orin 平台学习记录

文章目录 1 Jetson AGX 开发板编译环境搭建1.1 官方资料包下载1.2 开发者手册1.2.1 安装jetpack 2 更新Image文件2.1 自编译的Image内核文件更新到系统 3 编译文档3.1 编译内核步骤3.1.1 下载kernel_src 源码包3.1.2 编译内核 3.2 编译内核工具链下载3.2 orin 介绍 4 csi_trace…

Shell定时上传日志到HDFS

Shell定时上传日志到HDFS 一、任务需求二、实现思路三、具体实现流程3.1 规划文件上传目录3.2 开发 shell 脚本3.3 授予 shell 可执行权限3.4 手动执行查看3.4 定时执行 shell 脚本 一、任务需求 公司在线服务器每天都会产生网站运行日志&#xff0c;为了避免志文件过大&#…

QT Word文档控件QAxWidget C++退出

我们知道每次加载word控件&#xff0c;都会导致后台启动一个WINWORD.EXE 如何安全退出呢 1、一个最简单的例子 QT core gui axcontainer MainWindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QAxWidget> #include…

【强化学习的数学原理】课程笔记--6(Actor-Critic方法)

目录 Actor-Critic 方法QAC 算法Advantage Actor-Critic 算法Baseline invariance Off-policy Actor-Critic重要性采样 Deterministic Policy Gradient (DPG) 系列笔记&#xff1a; 【强化学习的数学原理】课程笔记–1&#xff08;基本概念&#xff0c;贝尔曼公式&#xff09; …

Java哈希算法

哈希算法 哈希算法1.概述2.哈希碰撞3.常用的哈希算法4.哈希算法的用途4.1校验下载文件4.2存储用户密码MD5加密5.SHA-1加密小结&#xff1a; 哈希算法 1.概述 哈希算法&#xff08;Hash&#xff09;又称摘要算法&#xff08;Digest&#xff09;&#xff0c;它的作用是&#xf…

[软件测试·研究向] MuJava 工具遇到的问题汇总和体会

MuJava 是初学者&#xff08;研究向&#xff09;常常会去使用的一个工具&#xff0c;也是 Java 软件测试的一个老牌工具。用于为 Java 代码生成变异体和运行单元测试。但是此工具已经有十年没有更新了&#xff0c;这款软件可以说现在已经不能够支持对主流软件框架运行测试。但是…

软考-软件设计师 (计算机组成和体系结构习题)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…

优秀的行为验证码的应用场景与行业案例

应用场景 登录注册 &#xff1a; 验证码适用于App、Web及小程序等用户注册场景&#xff0c;可以抵御自动机恶意注册&#xff0c;垃圾注册、抵御撞库登录、暴力破解、验证账号敏感信息的修改&#xff0c;同时可以有效阻止撞库攻击&#xff0c;从源头进行防护&#xff0c;保障正…

ip地址冲突会影响整个网络吗

在数字化时代&#xff0c;网络已成为连接世界的桥梁&#xff0c;而IP地址则是这座桥梁上不可或缺的“门牌号”。然而&#xff0c;当这个独特的身份标识出现冲突时&#xff0c;整个网络的稳定运行将面临严峻挑战。IP地址冲突&#xff0c;这一看似微小的技术问题&#xff0c;实则…

【电路笔记】-无源衰减器

无源衰减器 文章目录 无源衰减器1、概述2、简单衰减器3、无源衰减器示例14、无源衰减器设计5、切换式衰减器6、总结无源衰减器是一种特殊类型的电气或电子双向电路,由完全电阻元件组成。 1、概述 无源衰减器基本上是两个端口电阻网络,旨在将电源提供的功率削弱或“衰减”(因…

递归深度问题和尾调用的关系

当我们在编写计算阶乘的函数&#xff0c;一般我们都会会选择使用迭代或递归的方法来实现。下面就让我们看看&#xff0c;同一个函数的两种实现方法。首先&#xff0c;是使用迭代方式实现的函数&#xff0c;我们使用循环的方式来计算阶乘&#xff1a; // 阶乘函数&#xff0c;计…

java之多线程篇

一、基本概念 1.什么是线程&#xff1f; 线程就是&#xff0c;操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。简单理解就是&#xff1a;应用软件中互相独立&#xff0c;可以同时运行的功能 2.什么是多线程&#xff1f; 有了多线…

无人机之飞行控制系统篇

一、飞行控制系统组成 包括惯性测量单位、GPS接收机、气压高度计、空速计等传感器&#xff0c;以及飞控计算机、伺服作动器等设备。 二、飞行控制原理 通过传感器实时感知无人机的飞行状态&#xff0c;将数据传输给飞控计算机进行处理&#xff0c;计算机再根据预设的飞行计划和…

13-按键的元件模型创建

1.画线的时候&#xff0c;栅格切为10mil 2.放置管脚的时候&#xff0c;栅格切为100mil

开发框架DevExpress XAF v24.2产品路线图预览——增强跨平台性

DevExpress XAF是一款强大的现代应用程序框架&#xff0c;允许同时开发ASP.NET和WinForms。XAF采用模块化设计&#xff0c;开发人员可以选择内建模块&#xff0c;也可以自行创建&#xff0c;从而以更快的速度和比开发人员当前更强有力的方式创建应用程序。 DevExpress XAF是一…

LLaMA- Adapter V2: Parameter-Efficient Visual Instruction Model

发表时间&#xff1a;28 Apr 2023 论文链接&#xff1a;https://arxiv.org/pdf/2304.15010 作者单位&#xff1a; Shanghai Artificial Intelligence Laboratory Motivation&#xff1a;如何有效地将大型语言模型 (LLM) 转换为指令追随者最近是一个流行的研究方向&#xff0…

Linux基于centOS7【内存与OS的随谈】,进程初学【PCB】【fork】【进程排队】

冯诺依曼体系结构——存储器 存储器主要指的是内存&#xff0c;它有个特点就是掉电易失 磁盘等其它输入和输出设备 为什么要在计算机体系结构中要存在内存 我们知道&#xff0c;CPU的处理速度很快很快&#xff0c;但输入设备&#xff0c;以及输出设备&#xff0c;是相对很慢的…