[OpenGL] opengl切线空间

2024-05-25 23:28
文章标签 空间 opengl 切线

本文主要是介绍[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矩阵即可,在着色器中的使用方法是一样的。

这篇关于[OpenGL] opengl切线空间的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1002912

相关文章

Linux环境变量&&进程地址空间详解

《Linux环境变量&&进程地址空间详解》本文介绍了Linux环境变量、命令行参数、进程地址空间以及Linux内核进程调度队列的相关知识,环境变量是系统运行环境的参数,命令行参数用于传递给程序的参数,... 目录一、初步认识环境变量1.1常见的环境变量1.2环境变量的基本概念二、命令行参数2.1通过命令编程

【高等代数笔记】线性空间(一到四)

3. 线性空间 令 K n : = { ( a 1 , a 2 , . . . , a n ) ∣ a i ∈ K , i = 1 , 2 , . . . , n } \textbf{K}^{n}:=\{(a_{1},a_{2},...,a_{n})|a_{i}\in\textbf{K},i=1,2,...,n\} Kn:={(a1​,a2​,...,an​)∣ai​∈K,i=1,2,...,n

win7系统中C盘空间缩水的有效处理方法

一、深度剖析和完美解决   1、 休眠文件 hiberfil.sys :   该文件在C盘根目录为隐藏的系统文件,隐藏的这个hiberfil.sys文件大小正好和自己的物理内存是一致的,当你让电脑进入休眠状态时,Windows 7在关闭系统前将所有的内存内容写入Hiberfil.sys文件。   而后,当你重新打开电脑,操作系统使用Hiberfil.sys把所有信息放回内存,电脑

求空间直线与平面的交点

若直线不与平面平行,将存在交点。如下图所示,已知直线L过点m(m1,m2,m3),且方向向量为VL(v1,v2,v3),平面P过点n(n1,n2,n3),且法线方向向量为VP(vp1,vp2,vp3),求得直线与平面的交点O的坐标(x,y,z): 将直线方程写成参数方程形式,即有: x = m1+ v1 * t y = m2+ v2 * t

OPENGL顶点数组, glDrawArrays,glDrawElements

顶点数组, glDrawArrays,glDrawElements  前两天接触OpenGL ES的时候发现里面没有了熟悉的glBegin(), glEnd(),glVertex3f()函数,取而代之的是glDrawArrays()。有问题问google,终于找到答案:因为OpenGL ES是针对嵌入式设备这些对性能要求比较高的平台,因此把很多影响性能的函数都去掉了,上述的几个函数都被移除了。接

OpenGL ES学习总结:基础知识简介

什么是OpenGL ES? OpenGL ES (为OpenGL for Embedded System的缩写) 为适用于嵌入式系统的一个免费二维和三维图形库。 为桌面版本OpenGL 的一个子集。 OpenGL ES管道(Pipeline) OpenGL ES 1.x 的工序是固定的,称为Fix-Function Pipeline,可以想象一个带有很多控制开关的机器,尽管加工

OpenGL雾(fog)

使用fog步骤: 1. enable. glEnable(GL_FOG); // 使用雾气 2. 设置雾气颜色。glFogfv(GL_FOG_COLOR, fogColor); 3. 设置雾气的模式. glFogi(GL_FOG_MODE, GL_EXP); // 还可以选择GL_EXP2或GL_LINEAR 4. 设置雾的密度. glFogf(GL_FOG_DENSITY, 0

opengl纹理操作

我们在前一课中,学习了简单的像素操作,这意味着我们可以使用各种各样的BMP文件来丰富程序的显示效果,于是我们的OpenGL图形程序也不再像以前总是只显示几个多边形那样单调了。——但是这还不够。虽然我们可以将像素数据按照矩形进行缩小和放大,但是还不足以满足我们的要求。例如要将一幅世界地图绘制到一个球体表面,只使用glPixelZoom这样的函数来进行缩放显然是不够的。OpenGL纹理映射功能支持将

OpenGL ES 2.0渲染管线

http://codingnow.cn/opengles/1504.html Opengl es 2.0实现了可编程的图形管线,比起1.x的固定管线要复杂和灵活很多,由两部分规范组成:Opengl es 2.0 API规范和Opengl es着色语言规范。下图是Opengl es 2.0渲染管线,阴影部分是opengl es 2.0的可编程阶段。   1. 顶点着色器(Vert

[Linux]:环境变量与进程地址空间

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 环境变量 1.1 概念 **环境变量(environment variables)**一般是指在操作系统中用来指定操作系统运行环境的一些参数,具有全局属性,可以被子继承继承下去。 如:我们在编写C/C++代码的时,在链接的时候,我们并不知