LearnOpenGL-光照-2.基础光照

news2024/10/2 10:43:13

本人刚学OpenGL不久且自学,文中定有代码、术语等错误,欢迎指正

我写的项目地址:https://github.com/liujianjie/LearnOpenGLProject

文章目录

  • 基础光照
  • 环境光照
  • 漫反射光照
    • 法向量
    • 计算漫反射光照
    • 最后一件事
  • 镜面光照

基础光照

  • 简介

    现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。

    OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些

    这些光照模型都是基于我们对光的物理特性的理解,众多模型下有一个模型被称为冯氏光照模型(Phong Lighting Model)。

  • 冯氏光照模型

    分为

    • 环境光照((Ambient)

      即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的

    • 漫反射光照((Diffuse)

      模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量

    • 镜面光照((Specular)

      模拟有光泽物体上面出现的亮点

  • 图示

环境光照

  • 简介

    光通常都不是来自于同一个光源,而是来自于我们周围分散的很多光源,即使它们可能并不是那么显而易见。

    光的一个属性是,它可以向很多方向发散并反弹,从而能够到达不是非常直接临近的点。

    光能够在其它的表面上反射,对一个物体产生间接的影响。

    考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。

    我们将会先使用一个简化的全局照明模型,即环境光照。

  • 如何做

    我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中。

    我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:

    void main(){
        float ambientStrength = 0.1;// 常量环境因子
        vec3 ambient = ambientStrength * lightColor;// 常量(光照)颜色
    
        vec3 result = ambient * objectColor;
        FragColor = vec4(result, 1.0);
    }
    

漫反射光照

漫反射光照使物体上与光线方向(不是光照射方向)越接近(越垂直)的片段能从光源处获得更多的亮度

  • 图示

    标注图的光线方向

  • 解释

    I是灯泡光源的方向,-I是灯泡的光指向的方向。

    N是法向量。

  • 计算漫反射光照需要什么

    需要光对当前片段的光照强度,这个光照强度就是漫反射光的强度

    这个强度 = cos夹角 = 余弦值

    • 夹角0度,cos夹角 = 1,能从光源处获得全部的亮度
    • 夹角90度,cos夹角= 0,不能从光源处获得全部的亮度
    • 夹角>90度,cos夹角<0,不能从光源处获得全部的亮度
  • 如何计算这个漫反射光的强度

    • 使用光的方向与N法线的点积

      强度 = I与N的点积 = I长度 * N长度 * cos角度

      但这不完全正确,I长度和N长度会影响强度(余弦值)

    • 修改

      为了(只)得到两个向量夹角的余弦值(cos角度),使用I和N的单位向量,单位向量的长度为1

      强度 = I与N的点积 = I长度 * N长度 * cos角度 = 1 * 1 * cos角度 = cos角度=余弦值

    • 所以

      需要确保所有的向量(I、N)都是标准化的

  • 计算准备

    • 法向量
    • 光的方向(不是光的照射方向)
      • 片段位置
      • 光的位置

法向量

  • 简介

    法向量是一个垂直于顶点表面的(单位)向量。

  • 如何计算顶点的法向量

    由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面,从而才能得到顶点法线。

    我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量。

    但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中

  • glsl要接受顶点的法线

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    
    out vec3 Normal;// 传给片段着色器
    
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos, 1.0);
        Normal = aNormal;
    }
    

    更新物体顶点数组的顶点属性指针

    // 顶点位置
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 法线
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3*sizeof(float)));// 法线位置记得偏移
    glEnableVertexAttribArray(1);
    

    更新光源顶点数组的顶点属性指针

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 变为
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
  • glsl 顶点着色器接收法线后要传到片段着色器中

    所有光照的计算都是在片段着色器里进行,因为在片段着色器,由一开始的顶点围成的区域范围内的每一个片段都会通过插值得到自己的坐标和法线。

    in vec3 Normal;
    

计算漫反射光照

  • 要做的操作

    • 片段着色器定义uniform

      接收光源的位置向量

      uniform vec3 lightPos;
      
      lightingShader.setVec3("lightPos", lightPos);// 若放渲染循环外面,得记得lightingShader.use();,而我就忘记了
      
  • 将顶点坐标移到世界空间

    在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置

    如何做:

    在顶点着色器内

    把顶点位置乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标,这样顶点位置在世界空间,而片段会经过插值从而也在世界空间!

    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;
    
  • 在片段着色器开始计算

    • 光的方向

      vec3 lightDir = normalize(lightPos - FragPos);
      
      • 标准化得到单位向量

      • 这是光的方向,不是光的照射方向

        光的照射方向为(FragPos - lightPos)

    • 法线

      由顶点着色器传入顶点的法线给片段着色器,片段的法线由插值顶点的法线而得出

      这里只需要标准化

      vec3 norm = normalize(Normal);
      

    原文:当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。

    • 点乘得到光源对当前片段实际的漫反射影响

      float diff = max(dot(norm, lightDir), 0.0);
      
    • 将漫反射影响乘以光的颜色

      vec3 diffuse = diff * lightColor;// 漫反射分量
      

      两个向量之间的角度越大,漫反射分量就会越小,因为cos在0-90度是递减的。

      这里漫反射强度*光照颜色lightColor=vec3(1,1,1),相乘后是光照颜色各个分量的强度,称为漫反射分量

    • 最后的颜色

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

最后一件事

  • 问题引出

    当前片段着色器里的计算都是在世界空间坐标中进行,所以将法向量、顶点位置从顶点着色器传到了片段着色器,顶点位置使用了model矩阵转为世界空间的坐标,但是法向量没有转

  • 如何转法向量为世界空间

    • 不完全正确

      法线乘以一个模型矩阵,模型model矩阵包含平移、旋转、缩放

    • 解释

      • 法向量只是一个方向向量,不能表达空间中的特定位置

      • 法向量没有齐次坐标(顶点位置中的w分量)

        这意味着,位移不应该影响到法向量,Model矩阵有位移且乘了法线就不正确

    • 那该如何做

      • 若乘以一个模型矩阵

        我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移部分

        对于法向量,我们只希望对它实施缩放和旋转变换,即:只有顶点只发生位移时才可以保持不变。

    • 另外一个问题:不等比缩放

      如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了,因此,我们不能用上述这样去除了位移的模型矩阵来变换法向量。

      • 如何解决

        用法线矩阵:「模型矩阵左上角3x3部分的逆矩阵的转置矩阵」

      • 代码

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

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

      • 经后面发现 2.5节《投光物》

        当物体发生旋转的时候,法线也要更新。

        即:物体发生旋转后,法线需通过法线矩阵变换才能继续垂直于顶点

        void main()
        {
            gl_Position = projection * view * model * vec4(aPos, 1.0);
            FragPos = vec3(model * vec4(aPos, 1.0));
            Normal = mat3(transpose(inverse(model))) * aNormal;
            TexCoords = aTexCoords;
        }
        

镜面光照

  • 简介

    漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向

  • 图示

    镜面光照决定于表面的反射特性

    如果我们把物体表面设想为一面镜子,那么镜面光照最强的地方就是我们看到表面上反射光的地方

    要计算时图示向量的方向

  • 计算相当于观察者位置,光源对当前片段的镜面光影响方式

    • 根据法向量翻折入射光的方向来计算反射向量

    • 计算反射向量与观察方向的角度差

      它们之间夹角越小,镜面光的作用就越大

      由此产生的效果就是,我们看向在入射光在表面的反射方向时,会看到一点高光。

  • 观察向量

    使用观察者的世界空间位置片段的世界空间位置来计算它:观察者位置减去片段的位置

  • 根据以上思路,镜面光照分量

    镜面光照分量 = 镜面光照强度(降低光源高亮白色)*光源对当前片段的镜面光影响*光源的颜色

  • 代码

    // 观察者位置,放在片段着色器就好
    uniform vec3 viewPos;
    
    lightingShader.setVec3("viewPos", camera.Position);
    

    因为摄像机的位置向量,就是在世界空间,不需要乘以什么model矩阵!

    vec3 viewDir = normalize(viewPos - FragPos);// 是观察者方向,不是观察者看向的方向
    vec3 reflectDir = reflect(-lightDir, norm);
    

    lightDir向量进行了取反,lightDir是光源的方向,-lightDir才是光源照向指向的方向,因为reflect函数要求第一个向量是光源指向片段位置的向量

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

    32是高光的反光度(Shininess),一个物体的反光度越高,反射光的能力越强,散射得越少,高光范围就会越小。

    // 由上一节2.1颜色所说,光源的颜色(冯氏)与物体的颜色值相乘 = 物体的颜色
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
    

    请添加图片描述

  • 注意

    原文:我们选择在世界空间进行光照计算,但是大多数人趋向于更偏向在观察空间进行光照计算。在观察空间计算的优势是,观察者的位置总是在(0, 0, 0),所以你已经零成本地拿到了观察者的位置。然而,若以学习为目的,我认为在世界空间中计算光照更符合直觉。如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量也用观察矩阵进行变换(不要忘记也修改法线矩阵)。

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

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

相关文章

JavaEE初阶---初始进程

系统分配资源的最小单元--进程啥是进程看看进程操作系统如何管理进程PCB中的一些属性pid(进程id)内存指针文件描述符表进程调度并行和并发:进程调度的特性状态:优先级:记账信息:上下文:虚拟地址空间进程间交互我们日常使用计算机,大致可以抽象成如下形式:自上向下依次是 各种软…

易优cms switch 条件判断标签使用方法

【基础用法】 标签&#xff1a;switch 描述&#xff1a;简单条件判断&#xff0c;比if判断标签少些不等于相同功能&#xff0c;视个人习惯而用。 用法&#xff1a; {eyou:switch name$eyou.field.has_children} {eyou:case value1}当前栏目列表的栏目ID有1个下级栏目{/eyo…

跨境电商卖家如何应对拒付、盗卡

跨境电商主要是通过电子商务平台进行交易&#xff0c;在平台上完成支付结算&#xff0c;并通过国际物流将商品送达买方&#xff0c;从而实现跨国零售交易。与传统贸易相比&#xff0c;跨境电商存在交易链条更短、回款周期更快、数据及时透明等优势。商务部数据显示&#xff0c;…

sumifs的交叉 表的例子

比如这样&#xff0c;那么冰箱绿山店的栏位中&#xff0c;SUMIFS($D$3:$D$10,$B$3:$B$10,$F3,$C$3:$C$10,G$2)就是把求和范围&#xff0c;条件1设置为固定列的复合引用&#xff0c;条件2设置为固定行的复合引用即可。

LeetCode 1653. Minimum Deletions to Make String Balanced【字符串,动态规划,枚举】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

shell基本知识

为什么学习和使用Shell编程 什么是Shell shell的起源 shell的功能 shell的分类 如何查看当前系统支持的shell&#xff1f; 如何查看当前系统默认shell&#xff1f; 驼峰语句 shell脚本的基本元素 shell脚本编写规范 shell脚本的执行方式 shell脚本的退出状态 &#xf…

数位dp-- 数字游戏

题目 思路 也是一道比较典型的数位dp的问题&#xff0c;关键的思想跟我上一篇博客很像&#xff0c; 首先把区间值变成[1,Y]-[1,X-1]的值&#xff0c;然后单独计算得到结果。 总的来说就是把这个数的每一位都单独拿出来&#xff0c;然后根据选0-an-1和选**an**两种方案单独计算&…

LeetCode 热题 C++ 538. 把二叉搜索树转换为累加树 543. 二叉树的直径 560. 和为 K 的子数组

538. 把二叉搜索树转换为累加树 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&#xff08;Greater Sum Tree&#xff09;&#xff0c;使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。 提醒一下&#xff0c;二…

Java基础算法题

目录 练习一 : 优化代码 扩展 : CRTL Alt M 自动抽取方法 练习二: 方法1: 方法2: 方法3: Math : 顾名思义&#xff0c;Math类就是用来进行数学计算的&#xff0c;它提供了大量的静态方法来便于我们实现数学计算&#xff1a; 练习三 : 练习四 : 练习五: 练习…

【GO】K8s 管理系统项目34[Docker方式–应用部署]

K8s 管理系统项目[Docker方式–应用部署] 1. 数据库 1.1 创建数据库目录 mkdir -p /data/mysql5.7/1.2 创建容器 docker run --name mysql -itd -h mysql-server -e MYSQL_ROOT_PASSWORDroot -v /data/mysql5.7:/var/lib/mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 …

layui框架实战案例(19):layui-table模块表格综合应用(筛选查询、导入导出、群发短信、一键审核、照片展示、隐私加密)

系列文章目录 layui动态表格翻页和搜索的代码分析layui框架实战案例(3)&#xff1a;layui上传错误请求上传接口出现异常解决方案layui框架实战案例(9)&#xff1a;layPage 静态数据分页组件layui框架实战案例(10)&#xff1a;短信验证码60秒倒计时layui框架实战案例(11)&#…

《实践论》笔记及当下反思(二)

目录 笔记 1、马克思主义所说的绝对真理是什么&#xff1f; 2、客观现实世界的变化运动永远没有完结&#xff0c;人们在实践中对于真理的认识也就永远没有完结 3、改造客观世界&#xff0c;也改造自己的主观世界-——改造自己的认识能力 4、实践、认识、再实践、再认识&…

ARP报文内容详细分析

ARP报文格式如图&#xff1a; 字段1&#xff1a;ARP请求的目的以太网地址&#xff0c;全1时&#xff0c;代表广播地址。 字段2&#xff1a;发送ARP请求的以太网地址。 字段3&#xff1a;以太网帧类型表示后面的数据类型&#xff0c;ARP请求和ARP应答此字段为&#xff1a;0x0806…

SourceTree 重置提交、合并、撤销、回滚

SourceTree重置当前分支到此次提交使用场景&#xff1a;已提交未推送的修改撤销、想把某一次的错误修改全部撤销当前发布代码有bug需要切到上次提交发布版本Git中的HEAD解释# 使用最新一次提交重制暂存区git reset HEAD -- filename# 使用最新一次提交重制暂存区和工作区git re…

SpringBoot——统一功能处理

处理登陆拦截 上一片博客中讲到SpringAOP可以对页面进行拦截&#xff0c;我们可以用SpringAOP实现对登陆的拦截 但是由于拦截需要HttpSession对象&#xff0c;并且之后还需要页面重定向&#xff0c;因此在实际应用中&#xff0c;并不用SpringAOP进行登陆拦截&#xff0c;而是…

HBase---idea操作Hbase数据库并且映射到Hive

idea操作Hbase数据库并且映射到Hive 文章目录idea操作Hbase数据库并且映射到Hiveidea操作Hbase数据库环境准备启动服务创建Maven工程在测试类中编写初始化方法在测试类中编写关闭方法在测试类中编写创建命名空间方法在测试类中编写创建表方法在测试类中编写查看表结构方法在测试…

tmall.product.match.schema.get( 获取匹配产品规则 )

&#xffe5;免费必须用户授权 ISV发布商品前&#xff0c;需要先查找到产品ID&#xff0c;这个接口返回查找产品规则入参规则 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 点击获取key和secret 请求示例 TaobaoClient …

ORACLE SQL格式化小数点

ORACLE SQL格式化小数点 select CONCAT(TO_CHAR(0.00100,‘990.999’),‘%’) as a0 , CONCAT(TO_CHAR(1100,‘990.999’),‘%’) as a1 , CONCAT(TO_CHAR(0.236100,‘990.999’),‘%’) as a2 , CONCAT(TO_CHAR(0.0200100,‘990.999’),‘%’) as a3 , CONCAT(TO_CHAR(1.0310…

状态机的Go语言实现版本

一、状态机 1. 定义 有限状态机&#xff08;Finite-state machine, FSM&#xff09;&#xff0c;简称状态机&#xff0c;是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。 2. 组成要素 现态&#xff08;src state&#xff09;&#xff1a;事务当前所处的状…

Java程序中空指针异常的最佳实践

1、空指针问题 NullPointerException 是 Java 代码中最常见的异常&#xff0c;将其最可能出现的场景归为以下 5 种&#xff1a; 参数值是 Integer 等包装类型&#xff0c;使用时因为自动拆箱出现了空指针异常&#xff1b;字符串比较出现空指针异常&#xff1b;诸如 Concurren…