[OpenGL] opengl切线空间

目录

一 引入

二 TBN矩阵

三 代码实现

3.1手工计算切线和副切线

3.2 像素着色器

3.3 切线空间的两种使用方法

3.4 渲染效果

四 复杂的物体


本章节源码点击此处

继上篇法线贴图  来熟悉切线空间是再好不过的。对于法线贴图来说,我们知道它就是一个2D的颜色纹理,根据rgb来映射法线对应的xyz,从而达到在同一个平面上有多个不同方向法线的效果,这样就能根据光照的计算结果不同,从而得到凹凸不平(或者说更加细节)的平面。

一 引入

  • 我们可以尝试看下面这张图,由于我们的法线贴图中的rgb是固定的,也就是比如原来大多数是指向正z轴方向的法线,对于一个面向正z轴的平面来说是没有问题的,但是如果我们现在要在一个面向正y轴方向的屏幕也采用这个纹理贴图呢?还能够使用这个原有的法线贴图吗?
  • 光照看起来完全不对!发生这种情况是平面的表面法线现在指向了y,而采样得到的法线仍然指向的是z。结果就是光照仍然认为表面法线和之前朝向正z方向时一样;这样光照就不对了。

  • 有一种方案是要想正确的实现光照效果(也就是正确的法线),那么无非就是为每个单独的平面制作一个单独的法线贴图。如果是一个立方体的话我们就需要6个法线贴图,但是如果模型上有无数的朝向不同方向的表面,这就会变得极其复杂并且繁琐,无论是纹理制作者和使用者可能都容易出错。
  • 另一种方案就是,我们在计算光照时不在原有的世界坐标来计算,而是对于这个单独的平面的空间来计算,也就是我们想办法让坐标都变换到这个表平面的空间中。这个坐标空间你也可以理解为纹理空间,我们把纹理空间对应的UV(也就是xy)映射到这个坐标空间里,然后在这个空间中取出每个像素点的颜色值,这样法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间。

二 TBN矩阵

  • 法线贴图中的法线向量并不都指向切线空间的正Z方向。实际上,法线贴图中的每个像素代表的是该点在切线空间中的一个法线向量,这个向量可以指向任意方向,用来表示模型表面在那个点上的微小凸起或凹陷方向。
  • 我们需要使用一个特定的矩阵将世界坐标切换到切线空间坐标中,同时也可以使用这个矩阵的逆矩阵将切线空间坐标切换回世界坐标中。
  • 这种矩阵叫做TBN矩阵这三个字母分别代表tangent、bitangent和normal向量。TBN矩阵主要用于将不同的向量(如光照方向、视线方向等)从一个空间(通常是世界空间或模型空间)转换到切线空间。或者相互转换。这样做的目的是使光照计算能够在与法线贴图中存储的法线相匹配的坐标系中进行,因为法线贴图中的法线是在切线空间中定义的。,我们需要三个相互垂直的向量,它们沿一个表面的法线贴图对齐于:上、右、前;

  • 简单来说:TBN矩阵可以实现切线空间模型空间/世界坐标相互转换。这取决于你生成TBN矩阵时所用的坐标系。
  • T:切向量 Tangent
  • B:副切向量 Bitangent
  • N:法向量 Normal

  • P1,P2,P3纹理中的UV坐标(也就是纹理坐标),而E1和E2就是两个顶点之间的位置坐标
  • 注意图中边E2与纹理坐标的差ΔU2、Δ𝑉2构成一个三角形。Δ𝑈2与切线向量T𝑇方向相同,而ΔV2与副切线向量B方向相同。这也就是说,所以我们可以将三角形的边E1与E2写成切线向量T和副切线向量B的线性组合:

  •  具体的推导就需要线性代数的知识了,而实际最终我们的开发并不会自己计算,而是利用接口
  • 最终计算的TBN矩阵的推导公式如下。

  • 当我们知道TBN矩阵的任意两个坐标轴时,另一个都可以通过叉乘得到。

三 代码实现

我们使用的场景是一个简单的2D平面,但能实现其原理。

3.1手工计算切线和副切线

我们仍然使用之前的法线贴图,但是此时我们把顶点的坐标改变,也就是说让这个面面向y轴,

  • 首先生成4个顶点也就是组成两个三角形,以及对应的纹理坐标和法线值
  • 至于为什么要传入顶点的法线值,是因为对于这个平面来说,由于我们是在顶点着色器中计算使用的TBN矩阵,所以这个法线是相对准确的。
 // 首先准备4个顶点, 其实是两个三角形(两个面)QVector3D pos1(-1.0f,  0.0f, -1.0f);QVector3D pos2(-1.0f, 0.0f, 1.0f);QVector3D pos3( 1.0f, 0.0f, 1.0f);QVector3D pos4( 1.0f,  0.0f, -1.0f);// 准备对应的纹理坐标QVector2D uv1(0.0f, 1.0f);QVector2D uv2(0.0f, 0.0f);QVector2D uv3(1.0f, 0.0f);QVector2D uv4(1.0f, 1.0f);// 法线  这个法线是因为我们是在顶点着色器里面使用的TBN矩阵 所以这个法线应该是准确的QVector3D nm(0.0f, 1.0f, 0.0f);
  • 接下来就是按照上面的公式来生成TB向量了
 // 先准备两个平面的TB向量,需要分开计算QVector3D tangent1, bitangent1;QVector3D tangent2, bitangent2;// 第一个三角形QVector3D edge1 = pos2 - pos1;QVector3D edge2 = pos3 - pos1;QVector2D deltaUV1 = uv2 - uv1;QVector2D deltaUV2 = uv3 - uv1;// 先计算矩阵前面的系数float f = 1.0f / (deltaUV1.x() * deltaUV2.y() - deltaUV2.x() * deltaUV1.y());// 生成TB向量tangent1.setX(f * (deltaUV2.y() * edge1.x() - deltaUV1.y() * edge2.x()));tangent1.setY(f * (deltaUV2.y() * edge1.y() - deltaUV1.y() * edge2.y()));tangent1.setZ(f * (deltaUV2.y() * edge1.z() - deltaUV1.y() * edge2.z()));bitangent1.setX(f * (-deltaUV2.x() * edge1.x() + deltaUV1.x() * edge2.x()));bitangent1.setY(f * (-deltaUV2.x() * edge1.y() + deltaUV1.x() * edge2.y()));bitangent1.setZ(f * (-deltaUV2.x() * edge1.z() + deltaUV1.x() * edge2.z()));// 第二个三角形计算方法同上edge1 = pos3 - pos1;edge2 = pos4 - pos1;deltaUV1 = uv3 - uv1;deltaUV2 = uv4 - uv1;f = 1.0f / (deltaUV1.x() * deltaUV2.y() - deltaUV2.x() * deltaUV1.y());tangent2.setX(f * (deltaUV2.y() * edge1.x() - deltaUV1.y() * edge2.x()));tangent2.setY(f * (deltaUV2.y() * edge1.y() - deltaUV1.y() * edge2.y()));tangent2.setZ(f * (deltaUV2.y() * edge1.z() - deltaUV1.y() * edge2.z()));bitangent2.setX(f * (-deltaUV2.x() * edge1.x() + deltaUV1.x() * edge2.x()));bitangent2.setY(f * (-deltaUV2.x() * edge1.y() + deltaUV1.x() * edge2.y()));bitangent2.setZ(f * (-deltaUV2.x() * edge1.z() + deltaUV1.x() * edge2.z()));// 这些顶点和法线我们都通过VAO传递进去,由于我们用的是一个2D的平面测试程序,所以法线是同一个,这并不影响。float quadVertices[] = {// positions            // normal         // texcoords  // tangent                          // bitangentpos1.x(), pos1.y(), pos1.z(), nm.x(), nm.y(), nm.z(), uv1.x(), uv1.y(), tangent1.x(), tangent1.y(), tangent1.z(), bitangent1.x(), bitangent1.y(), bitangent1.z(),pos2.x(), pos2.y(), pos2.z(), nm.x(), nm.y(), nm.z(), uv2.x(), uv2.y(), tangent1.x(), tangent1.y(), tangent1.z(), bitangent1.x(), bitangent1.y(), bitangent1.z(),pos3.x(), pos3.y(), pos3.z(), nm.x(), nm.y(), nm.z(), uv3.x(), uv3.y(), tangent1.x(), tangent1.y(), tangent1.z(), bitangent1.x(), bitangent1.y(), bitangent1.z(),pos1.x(), pos1.y(), pos1.z(), nm.x(), nm.y(), nm.z(), uv1.x(), uv1.y(), tangent2.x(), tangent2.y(), tangent2.z(), bitangent2.x(), bitangent2.y(), bitangent2.z(),pos3.x(), pos3.y(), pos3.z(), nm.x(), nm.y(), nm.z(), uv3.x(), uv3.y(), tangent2.x(), tangent2.y(), tangent2.z(), bitangent2.x(), bitangent2.y(), bitangent2.z(),pos4.x(), pos4.y(), pos4.z(), nm.x(), nm.y(), nm.z(), uv4.x(), uv4.y(), tangent2.x(), tangent2.y(), tangent2.z(), bitangent2.x(), bitangent2.y(), bitangent2.z()};// 配置顶点缓冲glGenVertexArrays(1,&quadVAO);glGenBuffers(1,&quadVBO);glBindVertexArray(quadVAO);glBindBuffer(GL_ARRAY_BUFFER,quadVBO);glBufferData(GL_ARRAY_BUFFER,sizeof(quadVertices),&quadVertices, GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,14 * sizeof(float),0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(6 * sizeof(float)));glEnableVertexAttribArray(3);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(8 * sizeof(float)));glEnableVertexAttribArray(4);glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(float), (void*)(11 * sizeof(float)));glBindVertexArray(quadVAO);glDrawArrays(GL_TRIANGLES, 0, 6);glBindVertexArray(0);

3.2 像素着色器

    顶点着色器

  • 在定点着色器中,我们并没有使用传进来的B向量,因为在顶点着色器中传入的法线向量是准确的,我们只需要将这个法线N和主切线T进行点积就能得到一个正交坐标系。
  • 但需要注意的是,在某些情况下法线N与切线T可能不会垂直,我们需要额外处理一下。(试想一下我们计算切线T的时候,如果同一个顶点被多个平面共用,那么这里的纹理坐标可能就会被综合多个平面的效果,导致T切线计算后代结果稍微有偏差。)
  • 格拉姆-施密特正交化过程(Gram-Schmidt process)的数学技巧,我们可以对TBN向量进行重正交化,这样每个向量就又会重新垂直了。
  • 当然我们也可以直接使用传入的B切线生成,这样都是可以的。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
// T 向量
layout (location = 3) in vec3 aTangent;
// B 向量
layout (location = 4) in vec3 aBitangent;out VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;uniform vec3 lightPos;
uniform vec3 viewPos;uniform bool blin;
void main()
{// 顶点坐标传出的还是世界坐标vs_out.FragPos = vec3(model * vec4(aPos, 1.0));vs_out.TexCoords = aTexCoords;mat3 normalMatrix = transpose(inverse(mat3(model)));vec3 T = normalize(normalMatrix * aTangent);vec3 N = normalize(normalMatrix * aNormal);// 为了防止法向量和T向量不垂直T = normalize(T - dot(T, N) * N);// B向量我们采用N和T的点积计算得到Bvec3 B = cross(N, T);mat3 TBN = transpose(mat3(T, B, N));if(blin == true){vs_out.TangentLightPos = TBN * lightPos;vs_out.TangentViewPos  = TBN * viewPos;vs_out.TangentFragPos  = TBN * vs_out.FragPos;}else{vs_out.TangentLightPos = lightPos;vs_out.TangentViewPos  = viewPos;vs_out.TangentFragPos  = vs_out.FragPos;}gl_Position = projection * view * model * vec4(aPos, 1.0);
}

片段着色器:

  • 在顶点着色器中我们已经将光源,视线,以及顶点坐标转换到切线空间了,这时候我们只需要正常计算光照即可
#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} fs_in;uniform sampler2D diffuseMap;
uniform sampler2D normalMap;uniform vec3 lightPos;
uniform vec3 viewPos;void main()
{// 从法线贴图中获取法线值vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;// 将法线坐标标准化normal = normalize(normal * 2.0 - 1.0);// 获取漫反射的颜色值vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;// ambientvec3 ambient = 0.1 * color;// diffusevec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * color;// specularvec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);vec3 reflectDir = reflect(-lightDir, normal);vec3 halfwayDir = normalize(lightDir + viewDir);float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);vec3 specular = vec3(0.2) * spec;FragColor = vec4(ambient + diffuse + specular, 1.0);
}

3.3 切线空间的两种使用方法

  • 第一种方法也就是我们上面使用的方法: 在顶点着色器中将光源,视线,顶点所有相关向量在顶点着色器中转换到切线空间,不用在像素着色器中做这件事,不是把TBN矩阵的逆矩阵发送给像素着色器,而是将切线空间的光源位置,观察位置以及顶点位置发送给像素着色器。这样我们就不用在像素着色器里进行矩阵乘法了。这是一个极佳的优化,因为顶点着色器通常比像素着色器运行的少。
  • 第二种方法就是我们只需要在顶点着色器中将TBN传递给片段着色器,然后再片段着色器中将法线贴图的纹理使用TBN矩阵转换到世界坐标即可,这样看起来更简单,但片段着色器运行的次数更多,相对来说消耗更大。

3.4 渲染效果

  • 在渲染时我们加上开关,也就是可以控制是否使用切线空间来优化错误的法线贴图,看看他们不同的效果。
  • 因为片段着色器没有什么不同,也就是在顶点着色器中加上一个控制变量
  • 这个变量用于控制是否使用切线空间。
    if(blin == true){vs_out.TangentLightPos = TBN * lightPos;vs_out.TangentViewPos  = TBN * viewPos;vs_out.TangentFragPos  = TBN * vs_out.FragPos;}else{vs_out.TangentLightPos = lightPos;vs_out.TangentViewPos  = viewPos;vs_out.TangentFragPos  = vs_out.FragPos;}

四 复杂的物体

对于复杂的物体也就是平面(或者说网格)很多的物体,像Assimp这种模型加载库是会提供的,我们只需要利用其提供的API接口生成TBN矩阵即可,在着色器中的使用方法是一样的。

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

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

相关文章

使用DataGrip连接Elasticsearch

使用DataGrip连接Elasticsearch 前言,公司需要使用ES来做数据的查询,我安装完ES,安装完Kibana的时候,想先开始尝试一下,插入查询数据能否可用,但是上次使用ES是好久前了,增删改查的请求根本记不…

利用sql注入对某非法网站的渗透

本文仅用于技术讨论,切勿用于违法途径,且行且珍惜, 所有非经授权的渗透,都是违法行为 前言 这段时间一直在捣鼓sql注入,最近又通过一个sql注入点,成功进入某个非法网站的后台,拿到整个网站的…

Liunx基本指令以及权限(个人笔记)

Linux指令和权限 1.指令1.1ls指令1.2pwd命令1.3cd指令1.4touch指令1.5mkdir指令1.6rm指令1.7man指令1.8cp指令1.9mv指令1.10cat指令1.11less指令1.12head指令1.13tail指令1.14date显示1.15Cal指令1.16find指令1.17grep指令1.18zip/unzip指令1.19tar指令1.20bc指令1.21uname -r指…

【Tools】微服务工程中的通用功能模块抽取

Catalog 通用功能模块抽取一、需求二、步骤三、细节 通用功能模块抽取 一、需求 在微服务工程中,可能有一些工具类、实体类是多个微服务通用的,如果在每个微服务中都复制粘贴这些工具类,会产生很多重复性的代码,对开发来说也很繁…

uniapp+php服务端实现苹果iap内购的消耗性项目和非续期订阅项目,前后端代码加逻辑分析

前言:公司的项目app在上架苹果商店时发现人家要求里面的部分购买项目必须使用iap购买的方式,使用原本的微信支付方式审核不给通过,无奈只能重新研究这个东西。做起来还是有点麻烦,主要是网上的文章很少,不能直接硬抄。…

C语言笔记20 •整数和浮点数在内存中存储•

整数和浮点数在内存中存储 1.整数在内存中存储 整数在内存中存储比较简单,整数存储分为正整数存储和负整数存储。 对于有符号整数 符号位中0表示正整数,1表示负整数。 正整数在内存中存储: 正整数原码,反码 ,补码…

合约demo——hello contract

520的日子,没出现在各大水群,假装忙着约会,实则在这偷偷躲起来写博客,不能让人发现我今天很有空都没人约๑乛◡乛๑ 智能合约开发 性质 根本性质:表达商业、“法律”关系的契约 机制 运行机制 Transation驱动的E…

LangChain - 为何我们选择 ClickHouse 作为 LangSmith 的动力

本文字数:4742;估计阅读时间:12 分钟 作者:Ankush Gola 审校:庄晓东(魏庄) 本文在公众号【ClickHouseInc】首发 “我们在使用 ClickHouse 方面有着良好的经历。它使我们能够将 LangSmith 扩展到…

从ES到ClickHouse,Bonree ONE平台更轻更快!

本文字数:8052;估计阅读时间:21 分钟 作者:博睿数据 李骅宸(太道)& 娄志强(冬青) 本文在公众号【ClickHouseInc】首发 本系列第一篇内容: 100%降本增效!…

Mysql之基本架构

1.Mysql简介 mysql是一种关系型数据库,由表结构来存储数据与数据之间的关系,同时为sql(结构化查询语句)来进行数据操作。 sql语句进行操作又分为几个重要的操作类型 DQL: Data Query Language 数据查询语句 DML: Data Manipulation Language 添加、删…

重置服务器之后 SSH 登录报错:REMOTE HOST IDENTIFICATION HAS CHANGED!

问题原因: 报错是由于远程的主机的公钥发生了变化导致的。ssh服务是通过公钥和私钥来进行连接的,它会把每个曾经访问过计算机或服务器的公钥(public key),记录在~/.ssh/known_hosts 中,当下次访问曾经访问…

使用vue3实现右侧瀑布流滑动时左侧菜单的固定与取消固定

实现效果 实现方法 下面展示的为关键代码,想要查看完整流程及代码可参考https://blog.csdn.net/weixin_43312391/article/details/139197550 isMenuBarFixed为控制左侧菜单是否固定的参数 // 监听滚动事件 const handleScroll () > {const scrollTopThreshol…

读书笔记-Java并发编程的艺术--持续更新中

文章目录 第1章 并发编程的挑战1.1 上下文切换1.1.1 多线程一定快吗1.1.2 如何减少上下文切换 1.2 死锁1.3 资源限制的挑战 第2章 Java并发机制的底层实现原理第3章 Java内存模型第4章 Java编发编程基础第5章 Java中的锁第6章 Java并发容器和框架第7章 Java中的13个原子操作类第…

DA-CLIP论文阅读笔记

这是ICLR2024的一篇用VLM做multi-task image restoration的论文首页图看起来就很猛啊,一个unified模型搞定10个任务: 文章的贡献点主要是两个,一个是提出一个利用Image Controller,CLIP,cross-attention 和 diffusion …

使用elementUI的form表单校验时,错误提示位置异常解决方法

问题 最近在做项目时遇到一个问题&#xff0c;使用elementUI的Descriptions 描述列表与form表单校验时&#xff0c;遇到校验信息显示的位置不对&#xff0c;效果如图&#xff1a; 期望显示在表格中。 效果 代码 html <el-form :model"form":rules"rules…

深入解析文华量化交易策略---交易指令如何选择

随着金融投资的迅猛发展&#xff0c;自动化策略模型已逐渐成为现代投资领域的一股重要力量。量化交易模型均以数据为驱动&#xff0c;通过运用数学模型和算法&#xff0c;对期货、黄金等投资市场走势进行精准预测和高效交易。 艾云策略整理了量化策略相关资料&#xff0c;希望通…

浅谈后端boot框架整合第三方技术JUnit MyBatis Druid整体思想

整合第三方技术 不要单单学习指定技术与springboot整合的方式 学习目标的是整合整体的技术的思路 拿到任何一个第三方技术后我们在springboot中如何操作 这是真正我们应该学习的东西 以后能整合任意技术 整合JUnit JUnit 是一个流行的开源测试框架&#xff0c;用于 Java …

如何快速复现NEJM文章亚组分析森林图?

现在亚组分析好像越来越流行&#xff0c;无论是观察性研究还是RCT研究&#xff0c;亚组分析一般配备森林图。 比如下方NEJM这张图&#xff0c;配色布局都比较经典美观&#xff01; 但是在使用R语言绘制时&#xff0c;想要绘制出同款森林图&#xff0c;少不了复杂参数进行美化调…

windows安装kafka环境

1.安装jdk8 参考教程java8安装教程_java8u371安装教程-CSDN博客 下载kafak安装包&#xff1a; kafka_2.12-3.6.1.tgz 解压&#xff1a; 启动ZooKeeper软件&#xff0c;kafka内部已近集成了该软件。 进入Kafka解压缩文件夹的config目录&#xff0c;修改zookeeper.properti…

傲软抠图一款专为抠图打造的AI智能抠图应用,智能识别人物物体抠图软件,无广vip版 v1.8.1

软件介绍 傲软抠图&#xff0c;作为一款高度专业化的智能人工智能图片处理软件&#xff0c;被设计专门用于执行精确的图像提取功能。该程序利用其先进的算法能动地识别和分辨图像中的人物或物体轮廓&#xff0c;并能够从原始图片材料中分离出带有透明背景的目标图像。除了自动…