[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                          // bitangent
        pos1.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的点积计算得到B
    vec3 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;
   // ambient
   vec3 ambient = 0.1 * color;
   // diffuse
   vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
   float diff = max(dot(lightDir, normal), 0.0);
   vec3 diffuse = diff * color;
   // specular
   vec3 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矩阵即可,在着色器中的使用方法是一样的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

qmt量化教程4----订阅全推数据

文章链接 qmt量化教程4----订阅全推数据 (qq.com) 上次写了订阅单股数据的教程 量化教程3---miniqmt当作第三方库设置,提供源代码 全推就主动推送,当行情有变化就会触发回调函数,推送实时数据,可以理解为数据驱动类型&#xff0…

并发编程笔记7--并发编程基础

1、线程简介 1.1、什么是线程 现代操作系统中运行一个程序,会为他创建一个进程。而每一个进程中又可以创建许多个线程。现代操作系统中线程是最小的调度单元。 两者关系:一个线程只属于一个进程,而一个进程可以拥有多个线程。线程是一个轻量…

测试基础05:软件测试的分类

课程大纲 1、两种架构(Architecture) 1.1、B/S(Browser/Server) 浏览器服务器架构(大体3步):用户通过浏览器向服务器发出请求,服务器处理请求,将结果通过网络返回到用户…

【数据挖掘】四分位数识别数据中的异常值(附代码)

写在前面: 首先感谢兄弟们的订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 路虽远,行则将至&#…

口碑比较好的相亲交友平台有哪些?正规靠谱的相亲软件排行榜测评

在网络时代,越来越多的人热衷于使用相亲交友软件来寻找生命中的另一半。这些软件确实为许多用户提供了真实可靠的交友平台。然而,市面上的相亲软件种类繁多,质量良莠不齐,让人难以选择。今天,我将介绍几款我使用过且认…

【ARM 裸机】按键输入

本节学习按键输入,先拷贝上一节工程文件, 1、驱动编写 新建 key 的 .h 和 .c 文件; 再查看一下硬件原理图如下; 由此可知,KEY0 按键接在 UART1_CTS 引脚上,默认情况下为高电平,按键按下为…

【LeetCode】30.串联所有单词的子串

串联所有单词的子串 题目描述: 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 例如,如果 words ["ab","cd",&qu…

超值分享50个DFM模型格式的素人直播资源,适用于DeepFaceLive的DFM合集

50直播模型:点击下载 作为直播达人,我在网上购买了大量直播用的模型资源,包含男模女模、明星脸、大众脸、网红脸及各种稀缺的路人素人模型。现在,我将这些宝贵的资源整理成合集分享给大家,需要的朋友们可以直接点击下…

工业路由器在工厂数字化的应用及价值

随着科技的飞速发展,数字化转型已成为工厂提高效率、降低成本、实现智能化管理的关键途径。在这个过程中,工业路由器凭借其独特的优势,正逐渐成为工厂数字化建设不可或缺的核心组件。本文将深入探讨工业路由器在工厂数字化中的应用及价值&…

c# 画一个正弦函数

1.概要 c# 画一个正弦函数 2.代码 using System; using System.Drawing; using System.Windows.Forms;public class SineWaveForm : Form {private const int Width 800;private const int Height 600;private const double Amplitude 100.0;private const double Period…

光电直读抄表技术详细说明

1.技术简述 光电直读抄表是一种智能化智能计量技术,主要是通过成像原理立即载入电度表里的标值,不用人工干预,大大提升了抄表效率数据可靠性。此项技术是智慧能源不可或缺的一部分,为电力公司的经营管理提供了有力的适用。 2.原…

2024年5月26日 十二生肖 今日运势

小运播报:2024年5月26日,星期日,农历四月十九 (甲辰年己巳月庚寅日),法定节假日。 红榜生肖:马、猪、狗 需要注意:牛、蛇、猴 喜神方位:西北方 财神方位:…

基于open3d加载kitti数据集bin文件

前言 在自动驾驶领域,Kitti数据集是一个非常流行的点云数据集,广泛用于3D目标检测、跟踪和其他相关研究。Open3D是一个强大的开源库,专门用于处理和可视化三维数据。本文将介绍如何使用Open3D来加载和可视化Kitti数据集中的.bin文件。 准备…

marimo,Python notebook 的未来

你好,我是坚持分享干货的 EarlGrey,翻译出版过《Python编程无师自通》、《Python并行计算手册》等技术书籍。 如果我的分享对你有帮助,请关注我,一起向上进击。 marimo,号称是下一代 Jupyter Notebook,是 P…

长文处理更高效:一键章节拆分,批量操作轻松搞定,飞速提升工作效率!

在当今信息爆炸的时代,我们时常需要处理大量的文本内容。无论是阅读长篇小说、整理专业资料还是编辑大型文档,TXT文本文件的普遍性不言而喻。然而,当TXT文本内容过于庞大时,阅读、编辑和管理都变得异常繁琐。此时,一款…

echarts-树图、关系图、桑基图、日历图

树图 树图主要用来表达关系结构。 树图的端点也收symbol的调节 树图的特有属性: 树图的方向: layout、orient子节点收起展开:initialTreeDepth、expandAndCollapse叶子节点设置: leaves操作设置:roam线条&#xff1a…

eNSP学习——OSPF单区域配置

目录 相关命令 实验背景 实验目的 实验步骤 实验拓扑 实验编址 实验步骤 1、基础配置 2、部署单区域OSPF网络 3、检查OSPF单区域的配置结果 OSPF——开放式最短路径优先 基于链路状态的协议,具有收敛快、路由无环、扩展性好等优点; 相关命令 […

电信光猫的USB存储对外网开放访问

前提条件当然是要有公网IP地址了,没有的话去找电信索要,然后可以使用动态域名正常访问。 我的电信光猫发现共享访问速度还可以,会有31M/s左右的写入速度 但是有一个不方便的是,无法从外网提供访问,SMB协议所用的445端…

军队仓库管理系统|DW-S301系统特点

部队仓库管理系统DW-S301系统通过数据采集、互联网和物联网技术,实现数字化智能管控,以提高军用物资的仓储准确率和流转率,缩短周转时间,降低库存成本,也有助于消除生产过程中的不确定性。 系统功能:通过部…

WebService的wsdl详解

webservice服务的wsdl内容详解,以及如何根据其内容编写调用代码 wsdl示例 展示一个webservice的wsdl,及调用这个接口的Axis客户端 wsdl This XML file does not appear to have any style information associated with it. The document tree is shown…