类似于凡科的网站,营销型网站哪家做的好,企业标准网站模板,怎么做电子商务营销LearnOpenGL——法线贴图、视差贴图学习笔记 法线贴图 Normal Mapping一、基本概念二、切线空间1. TBN矩阵2. 切线空间中的法线贴图 三、复杂模型四、小问题 视差贴图 Parallax Mapping一、基本概念二、实现视差贴图三、陡峭视差映射 Steep Parallax Mapping四、视差遮蔽映射 P… LearnOpenGL——法线贴图、视差贴图学习笔记 法线贴图 Normal Mapping一、基本概念二、切线空间1. TBN矩阵2. 切线空间中的法线贴图 三、复杂模型四、小问题 视差贴图 Parallax Mapping一、基本概念二、实现视差贴图三、陡峭视差映射 Steep Parallax Mapping四、视差遮蔽映射 Parallax Occlusion Mapping 法线贴图 Normal Mapping
一、基本概念
通过调整每个曲面的法向量来让光照变化进而模拟凹凸不平的表面。为使法线贴图工作我们需要为每个fragment提供一个法线。我们可以使用2D纹理来存储法线数据然后通过采样来得到特定纹理的法向量。 将法线向量的x、y、z元素储存到纹理中代替颜色的r、g、b元素因为法线向量的范围在[-1,1]所以我们要先将其映射到[0,1]变换为RGB颜色元素。
vec3 rgb_normal normal * 0.5 0.5;法线贴图多是蓝色为主是因为法线基本上以z轴正方向为主存储为B分量蓝色。法线向量从z轴方向也有向其他方向的偏差颜色也就发生了轻微的变化。 加载纹理绑定到合适的纹理单元然后将片元着色器中添加对法线贴图的采样。
uniform sampler2D normalMap; void main()
{ // 从法线贴图范围[0,1]获取法线normal texture(normalMap, fs_in.TexCoords).rgb;// 将法线向量转换为范围[-1,1]normal normalize(normal * 2.0 - 1.0); [...]// 像往常那样处理光照
}目前如果我们让平面竖直面对我们此时效果正常因为法线贴图中的法线方向指向z正方向并且平面的法线也指向z轴正方向。但当我们移动旋转平面时就会发现光照不正确。比如下图因为此时平面法线方向为y轴正方向但法线贴图中的方向仍然为z轴正方向。
解决办法在一个不同的坐标空间中处理所有的光照——切线空间 这个坐标空间中的法线贴图矢量总是指向z轴正方向然后其他照明矢量如光源方向、观察方向等相对于这个z方向进行变换。这样法线贴图不需要根据物体的方向变化而变化。无论物体如何旋转光照计算都能在切线空间中正确处理这简化了计算过程。
二、切线空间
切线空间是位于三角形表面上的空间法线相对于单个三角形的局部坐标系。可以看成法线贴图向量的局部坐标系。无论最终变换到什么方向它们都指向z轴正方向。我们可以使用一个特殊的矩阵来将法线贴图中的法线向量从切线空间变换到世界或观察空间使它们与表面的法线方向对齐。
1. TBN矩阵
Tangent正切、Bitangent双切、Normal法向量。 为了构造这个矩阵我们需要向上N、向右T、向前B三个向量。目前我们已知向上的向量N。接下来我们将会推导计算T和B的过程。需要一点数学基础 我们发现法线贴图的T和B坐标跟纹理的UV坐标很相似我们可以从这里入手。因为纹理坐标和切线向量在同一空间中 U就是T坐标V就是B坐标不难发现 然后我们将上述方程组写成矩阵乘法 然后左右两边都乘上UV矩阵的逆矩阵 现在难点就是计算UV矩阵的逆矩阵可以用伴随矩阵来求解逆矩阵不过对于2×2的矩阵我们可以直接写 来个代码例子 目前我们有两个三角形123和134我们挑选其中一个三角形来计算。我们只需为每个三角形计算一个切线/副切线它们对于每个三角形上的顶点都是一样的。
// positions
glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// texture coordinates
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
// normal vector
glm::vec3 nm(0.0, 0.0, 1.0);我们计算第一个三角形的 E 和 deltaUV
glm::vec3 edge1 pos2 - pos1;
glm::vec3 edge2 pos3 - pos1;
glm::vec2 deltaUV1 uv2 - uv1;
glm::vec2 deltaUV2 uv3 - uv1;然后就可以根据上面的公式来计算tangent和bitangent
tangent1.x f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
tangent1 glm::normalize(tangent1);bitangent1.x f * (-deltaUV2.x * edge1.x deltaUV1.x * edge2.x);
bitangent1.y f * (-deltaUV2.x * edge1.y deltaUV1.x * edge2.y);
bitangent1.z f * (-deltaUV2.x * edge1.z deltaUV1.x * edge2.z);
bitangent1 glm::normalize(bitangent1); [...] // 对平面的第二个三角形采用类似步骤计算切线和副切线2. 切线空间中的法线贴图
为了让法线贴图工作我们需要创建一个TBN矩阵我们可以将之前计算的切线和副切线传给顶点着色器。然后在main中创建TBN矩阵
#version 330 core
layout (location 0) in vec3 position;
layout (location 1) in vec3 normal;
layout (location 2) in vec2 texCoords;
layout (location 3) in vec3 tangent;
layout (location 4) in vec3 bitangent;void main()
{[...]vec3 T normalize(vec3(model * vec4(tangent, 0.0)));vec3 B normalize(vec3(model * vec4(bitangent, 0.0)));vec3 N normalize(vec3(model * vec4(normal, 0.0)));mat3 TBN mat3(T, B, N)
}有两种使用TBN矩阵的办法
直接使用TBN矩阵 将TBN矩阵传给片元着色器并使用TBN矩阵将法线向量从切线空间传到世界空间。让法线与其他光照变量处于同一空间。因为法线贴图中的法线向量是在切线空间中的而其他光照矢量是在世界空间中的。
out VS_OUT {vec3 FragPos;vec2 TexCoords;mat3 TBN;
} vs_out; void main()
{[...]vs_out.TBN mat3(T, B, N);
}在片元着色器中我们用mat3作为输入变量
in VS_OUT {vec3 FragPos;vec2 TexCoords;mat3 TBN;
} fs_in;然后将采样的法线贴图来转换先采样再映射再转换
normal texture(normalMap, fs_in.TexCoords).rgb;
normal normalize(normal * 2.0 - 1.0);
normal normalize(fs_in.TBN * normal);使用TBN的逆矩阵将所有世界空间向量转换到切线空间中计算
vs_out.TBN transpose(mat3(T, B, N));我们这里使用的是transpose是因为TBN是正交矩阵正交矩阵的转置和逆矩阵相等。在shader中使用逆矩阵的开销比转置大。 然后将TBN逆矩阵传给片元着色器将其他变量都转换为切线空间进行计算法线向量不做变换。
void main()
{ vec3 normal texture(normalMap, fs_in.TexCoords).rgb;normal normalize(normal * 2.0 - 1.0); vec3 lightDir fs_in.TBN * normalize(lightPos - fs_in.FragPos);vec3 viewDir fs_in.TBN * normalize(viewPos - fs_in.FragPos); [...]
}我们可以不用在片元着色器中进行转换我们可以直接在顶点着色器中对lightPos、viewPos以及FragPos进行变换这样就可以免去在片元着色器中的操作了。也可以节省开销因为顶点着色器运行次数比片元着色器少。以下是在顶点着色器中
out VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;uniform vec3 lightPos;
uniform vec3 viewPos;[...]void main()
{ [...]mat3 TBN transpose(mat3(T, B, N));vs_out.TangentLightPos TBN * lightPos;vs_out.TangentViewPos TBN * viewPos;vs_out.TangentFragPos TBN * vec3(model * vec4(position, 0.0));
}在像素着色器中我们使用这些新的输入变量来计算切线空间的光照。因为法线向量已经在切线空间中了光照就有意义了。
glm::mat4 model;
model glm::rotate(model, (GLfloat)glfwGetTime() * -10, glm::normalize(glm::vec3(1.0, 0.0, 1.0)));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
RenderQuad();三、复杂模型
对于复杂的模型Assimp加载器已经帮我们实现了为每个顶点计算出柔和的切线和副切线向量。我们可以通过下面的代码用Assimp获取计算出来的切线空间
vector.x mesh-mTangents[i].x;
vector.y mesh-mTangents[i].y;
vector.z mesh-mTangents[i].z;
vertex.Tangent vector;当加载模型时Assimp的aiTextureType_NORMAL并不会加载它的法线贴图而aiTextureType_HEIGHT却能
vector normalMaps loadMaterialTextures(material, aiTextureType_HEIGHT, texture_normal);四、小问题
对于网格很大的模型上面有很多共享的顶点法线贴图应用到这些表面时会讲切线向量平均化。但是这样的话TBN可能不会相互垂直因此TBN可能不再是正交矩阵了法线贴图就会稍稍偏移。
我们可以对其进行格拉姆-施密特正交化对TBN进行重正交化。在顶点着色器中
vec3 T normalize(vec3(model * vec4(tangent, 0.0)));
vec3 N normalize(vec3(model * vec4(normal, 0.0)));
// re-orthogonalize T with respect to N
T normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B cross(T, N);mat3 TBN mat3(T, B, N)视差贴图 Parallax Mapping
一、基本概念
视差贴图是和法线贴图类似也是用来增加表面细节而不需要额外增加几何信息。它对根据储存在纹理中的几何信息对顶点进行位移或偏移。每个纹理像素包含了高度值的纹理叫做高度贴图
视差贴图是根据观察方向和高度图来改变纹理坐标。 红色线表示高度图中的值V是观察方向。视差贴图目的是在A位置上的片元不再使用A的纹理坐标而是使用B的纹理坐标。
如何从点A得到点B的纹理坐标视差贴图通过A片元的高度值来缩放观察方向V。我们将V的长度缩放为等于A处高度 H(A)然后我们确定P向量作为纹理坐标偏移量。这个点B得到的还是近似值当高度快速变化的时候看起来就不会很真实。 我们在旋转之后点P就很难定位了所以仿照法线贴图我们引入了切线空间来计算。我们将观察方向变化到切线空间中所以P向量的x和y分量会与表面切线和副切线对齐由于切线和副切线向量与表面纹理坐标的方向相同我们可以用P的x和y元素作为纹理坐标的偏移量。
二、实现视差贴图
这个例子的高度图的颜色是相反的我们叫他深度贴图模拟深度比高度更容易一些。 这个时候我们使用向量V减去A的纹理坐标得到P。在着色器中我们使用1-采样得到的深度贴图中的深度值。 位移贴图是在像素着色器中实现的我们需要得到观察方向V所以需要切线空间中的观察者位置和片元位置。
顶点着色器如下
#version 330 core
layout (location 0) in vec3 position;
layout (location 1) in vec3 normal;
layout (location 2) in vec2 texCoords;
layout (location 3) in vec3 tangent;
layout (location 4) in vec3 bitangent;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;void main()
{gl_Position projection * view * model * vec4(position, 1.0f);vs_out.FragPos vec3(model * vec4(position, 1.0)); vs_out.TexCoords texCoords; vec3 T normalize(mat3(model) * tangent);vec3 B normalize(mat3(model) * bitangent);vec3 N normalize(mat3(model) * normal);mat3 TBN transpose(mat3(T, B, N));vs_out.TangentLightPos TBN * lightPos;vs_out.TangentViewPos TBN * viewPos;vs_out.TangentFragPos TBN * vs_out.FragPos;
}在片元着色器中我们实现视差贴图的逻辑
#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 sampler2D depthMap;uniform float height_scale;vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);void main()
{ // Offset texture coordinates with Parallax Mappingvec3 viewDir normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);vec2 texCoords ParallaxMapping(fs_in.TexCoords, viewDir);// then sample textures with new texture coordsvec3 diffuse texture(diffuseMap, texCoords);vec3 normal texture(normalMap, texCoords);normal normalize(normal * 2.0 - 1.0);// proceed with lighting code[...]
}
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ float height texture(depthMap, texCoords).r; vec2 p viewDir.xy / viewDir.z * (height * height_scale);return texCoords - p;
}我们定义了一个ParallaxMapping函数来获得纹理坐标。在此函数中我们先从深度图中采样到深度值然后计算偏移p同时引入了一个height_scale来控制视差效果的强度。
为什么要用viewDir.xy / viewDir.z 通过除以 viewDir.z我们确保了视角接近平行于表面即 viewDir.z 接近0偏移量 p 会更大。这模拟了当一个物体从边缘观察时由于视差效应你能够看到的物体部分与直接正面观察时不同的现象。 此时视差贴图的边缘仍然有古怪的现象原因是在平面的边缘上纹理坐标超出了0到1的范围进行采样根据纹理的环绕方式导致了不真实的结果。解决的方法是当它超出默认纹理坐标范围进行采样的时候就丢弃这个fragment
texCoords ParallaxMapping(fs_in.TexCoords, viewDir);
if(texCoords.x 1.0 || texCoords.y 1.0 || texCoords.x 0.0 || texCoords.y 0.0)discard;我们会发现在一些极端的视角还是会有明显的走样。
三、陡峭视差映射 Steep Parallax Mapping
相比于正常的视差贴图陡峭视差贴图用更多的样本点来确定向量P到B所以即使陡峭的高度变化由于提高了样本数量效果也会不错。
陡峭视差贴图的思想是将总深度划分为多个相等深度的层然后对于每一层都对深度图进行采样沿着P方向移动纹理坐标直到找到一个采样深度值小于当前层的深度值 我们需要修改一下ParallaxMapping函数
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ // number of depth layersconst float numLayers 10;// calculate the size of each layerfloat layerDepth 1.0 / numLayers;// depth of current layerfloat currentLayerDepth 0.0;// the amount to shift the texture coordinates per layer (from vector P)vec2 P viewDir.xy * height_scale; vec2 deltaTexCoords P / numLayers;vec2 currentTexCoords texCoords;float currentDepthMapValue texture(depthMap, currentTexCoords).r;while(currentLayerDepth currentDepthMapValue){// shift texture coordinates along direction of PcurrentTexCoords - deltaTexCoords;// get depthmap value at current texture coordinatescurrentDepthMapValue texture(depthMap, currentTexCoords).r; // get depth of next layercurrentLayerDepth layerDepth; }return currentTexCoords;
} 首先设置层数然后用1除以层数得到每层的深度值初始化currentLayerDepth当前层深度值然后计算得到P再用P除以层数将P也分层得到分层后的纹理坐标再初始化当前纹理坐标的深度值开始循环比较若当前层的深度值 当前纹理坐标的深度值就继续下一层直到当前层深度值 当前纹理坐标的深度值就停止循环返回此时纹理坐标
我们再改进一下当视角方向是垂直表面时就不需要太多采样点当视角方向偏向侧面时就增大采样点
const float minLayers 8;
const float maxLayers 32;
float numLayers mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));陡峭视差贴图同样有自己的问题。因为这个技术是基于有限的样本数量的我们会遇到锯齿效果以及图层之间有明显的断层。
四、视差遮蔽映射 Parallax Occlusion Mapping
与陡峭视差映射差不多但我们不采用碰撞后的第一个深度层的纹理坐标而是在碰撞前和碰撞后的深度层之间进行线性插值。线性插值的权重取决于表面高度与两个深度层值之间的距离。 我们还是修改ParallaxMapping代码
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ // number of depth layersconst float minLayers 10;const float maxLayers 20;float numLayers mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir))); // calculate the size of each layerfloat layerDepth 1.0 / numLayers;// depth of current layerfloat currentLayerDepth 0.0;// the amount to shift the texture coordinates per layer (from vector P)vec2 P viewDir.xy / viewDir.z * height_scale; vec2 deltaTexCoords P / numLayers;// get initial valuesvec2 currentTexCoords texCoords;float currentDepthMapValue texture(depthMap, currentTexCoords).r;while(currentLayerDepth currentDepthMapValue){// shift texture coordinates along direction of PcurrentTexCoords - deltaTexCoords;// get depthmap value at current texture coordinatescurrentDepthMapValue texture(depthMap, currentTexCoords).r; // get depth of next layercurrentLayerDepth layerDepth; }// -- parallax occlusion mapping interpolation from here on// get texture coordinates before collision (reverse operations)vec2 prevTexCoords currentTexCoords deltaTexCoords;// get depth after and before collision for linear interpolationfloat afterDepth currentDepthMapValue - currentLayerDepth;float beforeDepth texture(depthMap, prevTexCoords).r - currentLayerDepth layerDepth;// interpolation of texture coordinatesfloat weight afterDepth / (afterDepth - beforeDepth);vec2 finalTexCoords prevTexCoords * weight currentTexCoords * (1.0 - weight);return finalTexCoords;
}