[OpenGL] opengl切线空间

news2024/11/18 1:31:25

目录

一 引入

二 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矩阵即可,在着色器中的使用方法是一样的。

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

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

相关文章

Go语言的命名规范是怎样的?

文章目录 Go语言的命名规范详解一、标识符命名规范示例代码 二、包名命名规范示例代码 三、变量命名规范示例代码 四、常量命名规范示例代码 五、函数命名规范示例代码 总结 Go语言的命名规范详解 在Go语言中,代码的命名规范对于项目的可读性、可维护性和可扩展性至…

智能体之斯坦福AI小镇(Generative Agents: Interactive Simulacra of Human Behavior)

相关代码地址见文末 论文地址:Generative Agents: Interactive Simulacra of Human Behavior | Proceedings of the 36th Annual ACM Symposium on User Interface Software and Technology 1.概述 论文提出了一种多个智能体进行协同,进而模拟可信的人…

Java中String类型的大小

java对象在HotSpot虚拟机中的结构 Java对象由三部分组成,对象头、实例数据、填充数据。 对象头 markwork 存储对象运行时数据,HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳等。在64位虚拟机中,如果没有开启压缩指…

数据库系统基础知识

一、基本概念 二、数据库三级模式两级映像 三、数据库的分析与设计过程 四、数据模型 五、关系代数 六、数据库完整性约束 七、关系型数据库SQL简介 八、关系数据库的规范化 九、数据库的控制功能 十、数据仓库与数据挖掘基础 十一、大数据基本概念 数据库基本概念 1、数据库…

论文笔记:GPT4Rec: Graph Prompt Tuning for Streaming Recommendation

SIGIR 2024 1 intro 1.1 背景 传统推荐系统方法在实际应用中往往未能兑现基准数据集所做的承诺 这种差异主要源于传统的离线训练和测试方法在这些场景中,模型在大型静态数据集上训练,然后在有限的测试集上评估,这一过程没有考虑到现实世界…

第二十届文博会沙井艺立方分会场启幕!大咖齐打卡!

2024年5月24日-27日,第二十届中国(深圳)国际文化产业博览交易会沙井艺立方分会场活动将在艺立方非遗(文旅)产业园盛大举办。 本届文博会艺立方分会场活动办展特色鲜明,亮彩纷呈,将以“种下梧桐树…

留学培训行业PaaS应用系统架构的设计与实践

随着留学需求的增长和教育培训市场的不断扩大,留学培训行业正面临着越来越多的挑战和机遇。在这个背景下,利用PaaS(Platform as a Service)平台来构建留学培训行业的应用系统架构,将成为提升服务质量和效率的重要手段。…

工厂生产管理系统

为应对一些国内验厂,如大疆等,他们需要客户有自己的生产管理系统的,但实际很多公司是没有引入ERP这类的系统的,从而想开发一套简单的生产管理系统。 参考了网上一个比较古老的StorageMange项目,此项目用到DevExpress的…

「探讨」:什么是网络审计?好用的网络审计系统推荐【图文详解】

网络是企业运营、政府管理、个人生活不可或缺的基础设施。 然而网络安全问题却日益凸显,数据泄露、网络攻击、欺诈行为等风险日益严重。 一、网络审计的定义 网络审计,又称信息技术审计或电子审计,是指审计人员运用专业技能和工具&#xff…

亚马逊测评技术自己掌控:打造爆款产品,快速突破销量瓶颈

不管新老店铺来说,出单都是至关重要的,在我们的理解当中测评应该是一种成长剂,是一个加快店铺成长的工具,因为它在店铺的破0、突破瓶颈期、引爆爆款以及在后期店铺的一个补量上都会有一个明显的作用 测评有什么意义? …

简历–工作经历–通用

雇主将会很注意简历中的工作经历这一部分。在看完求职目标后,他们想了解你的历史,你曾在哪儿工作,工作了多长时间。他们想弄明白的是“你是个稳定可靠的人吗?”,“你发挥出的才能有哪些?”最重要的是你是否…

python-找出四位数中的玫瑰花数

【问题描述】玫瑰花数指一个n位数(n>4),其每位上的数字的n次幂之和等于本身。 请求出所有四位数中的玫瑰花数 【输入形式】 【输出形式】 【样例输入】 【样例输出】1634 8208 9474 【样例说明】 【评分标准】 完整代码如下: for n in ra…

android实现PhotoShop里的魔棒效果

魔棒是画板工具一个重要的功能,非常实用,只要轻轻一点,就能把触摸到的颜色区域选中,做复制、剪切、擦除等工作。 那怎么实现呢? 先来看看效果: 要实现这个效果,需要对安卓canvas和paint理解比…

【研发日记】【策划向】(一)游戏策划其实就是一道加减法题

文章目录 序设计的过程其实是控制自己欲望的过程我海纳百川,你要不要看看?我跟别人不一样!我的人设就是没有人设,或者说任何人设都是我的人设 记 序 不知不觉进入这个行业几年了,也经历了独立开发和团队开发的过程。在…

【SQL】外连接 LEFT JOIN

目录 一.内连接与外连接 1.内连接(inner join) 2.外连接(outer join) 二.两表连接 1.我们先来试试看内连接: 2.我们再来试试外连接 三.单表外连接 四.总结 一.内连接与外连接 先得介绍内连接和外连接两个概念&…

git回退到指定版本,同时提交记录也会删除

第一步&#xff1a; git reset --hard xxx (需要恢复版本的 commit id)第二步&#xff1a;branch_name就是远程分支的名称 git push origin <branch_name> --force

解决GoLand无法Debug

goland 调试的的时候提示如下错误 WARNING: undefined behavior - version of Delve is too old for Go version 1.22.3 (maximum supported v 其实个原因是因为正在使用的Delve调试器版本太旧&#xff0c;无法兼容当前的Go语言版本1.22.3。Delve是Go语言的一个调试工具&#…

Java使用apache.poi生成excel插入word中

加油&#xff0c;新时代打工人&#xff01; 工作需求&#xff0c;上个文章我们生成好的word&#xff0c;这次将生成好的excel表格数据&#xff0c;插入word中。需要准备好excle数据&#xff0c;然后插入到word中。 最后个需要&#xff0c;就是把这些生成好的word文档转成pdf进行…

STL题单总结

下周争取全部刷掉 牛客 知乎推荐的题 收藏的文章1 收藏的文章2 收藏的文章3 洛谷题单

Python爬虫:爬取B站视频(最新、能用且讲解详细)【01】

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…