✠OpenGL-10-增强表面细节
Posted itzyjr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✠OpenGL-10-增强表面细节相关的知识,希望对你有一定的参考价值。
我们将探讨几种与实现凹凸表面相关的方法,通过使用光照效果,即使在实际对象模型表面平滑的情况下,也能使对象看起来具有逼真的表面纹理。我们将首先观察
凹凸贴图
和
法线贴图
,当直接为对象添加微小表面细节会使得计算代价过高时,它们可以为场景中的对象增加相当程度的真实感。我们还将研究通过高度贴图实际扰乱光滑表面中顶点的方法,这对于生成地形(和其他一些用途)非常有用。
凹凸贴图
如上图,如果我们想让一个物体看起来好像有凹凸(或皱纹,陨石坑等),一种方法是计算当表面确实凹凸不平时其上的法向量。当场景点亮时,光照会让人产生我们所期望的幻觉。
// 片段着色器
#version 430
in vec3 originalVertex;
in vec3 varyingNormal;
...
void main() {
...
float a = 0.25;// a 控制凸起的高度
float b = 100.0;// b 控制凸起的宽度
float x = originalVertex.x;
float y = originalVertex.y;
float z = originalVertex.z;
// 使用正弦函数扰乱传入法向量
N.x = varyingNormal.x + a * sin(b * x);
N.y = varyingNormal.y + a * sin(b * y);
N.z = varyingNormal.z + a * sin(b * z);
N = normailize(N);
...
}
片段着色器中唯一显著的变化是——输入的已插值法向量(在原程序中名为“varyingNormal”)在这里变得凹凸不平了,其方法是对环面模型的原始(未变形)顶点的 X、 Y 和 Z 轴应用正弦函数。
以这种方式对法向量进行改变,即在运行时使用数学函数进行计算,称为过程式凹凸贴图
。
法线贴图
凹凸贴图的一种替代方法是使用查找表
来替换法向量。这样我们就可以在不依赖数学函数的情况下,对凸起进行构造,例如月球上的陨石坑所对应的凸起。一种使用查找表的常见方法叫作法线贴图
。
我们就可以将法向量存储在彩色图像文件中,其中 R、 G 和 B 分量分别对应于 X、 Y 和 Z。
图像中的RGB值以字节存储,范围是[0…1];而向量可以有正负值,范围是[-1…1],所以图像文件中法向量N存储为像素的转换是:
R = (NX + 1) / 2、G = (NY + 1) / 2、B = (NZ + 1) / 2
法线贴图使用一个图像文件(称为法线贴图),该图像文件包含在光照下所期望表面外观的法向量。
在法线贴图中,向量相对于任意平面 XY 表示。其 X 和 Y 分量表示与“垂直”的偏差,其 Z 分量设置为 1。
严格垂直于 XY 平面的向量(即没有偏差)将表示为( 0, 0, 1),而不垂直的向量将具有非零的 X 和/或 Y 分量。
因为实际偏移的范围为[−1…+1],而 RGB 值的范围为[0…1],所以要将值转换至 RGB 空间。例如,(0, 0, 1)将存储为(0.5, 0.5, 1)。
同理,将颜色分量从纹理中存储范围[0…1]转换为其原始范围[−1 … + 1],则将其乘以 2.0 再减去 1.0。例如,读取到(0.5, 0.5, 1),则对应向量为(0, 0, 1)。
我们在纹理单元中存储所需的法向量而非颜色。然后,在给定片段中,我们就可以使用采样器从法线贴图中查找值,接下来,我们将所得的值作为法向量,而非输出像素颜色。
法线贴图图像文件并不适合作为图像查看,法线贴图最终看起来基本都是蓝色的。这是因为图像文件中每个像素的 B 值(蓝色值)都是 1(最大蓝色值),这会让它在作为图像时看起来是“蓝色的”。
下图展示了两个不同的法线贴图图像文件以及在Blinn-Phong 光照模型下将它们应用于球体的结果:
在对象的每个顶点处,我们考虑与对象相切的平面。顶点处的物体的法向量垂直于该切面。我们在该切面中定义两个相互垂直的向量,同时也垂直于法向量,称为切向量
和副切向量
(有时称为副法向量)。
对于球体,顶点(x,y,z)处的法向量就是vec3(x,y,z),那么:
切向量=cross(vec3(0,1,0), vec3(x,y,z))
副切向量=cross(切向量, vec3(x,y,z))
. . .
for (int i = 0; i <= prec; i++) {
for (int j = 0; j <= prec; j++) {
float y = (float)cos(toRadians(180.0f - i*180.0f / prec));
float x = -(float)cos(toRadians(j*360.0f / prec)) * (float)abs(cos(asin(y)));
float z = (float)sin(toRadians(j*360.0f / prec)) * (float)abs(cos(asin(y)));
vertices[i*(prec+1)+j] = glm::vec3(x, y, z);
// 计算切向量
if (((x==0) && (y==1) && (z==0)) || ((x==0) && (y==-1) && (z==0))) {// 如果是北极或南极,设置切向量为 -Z 轴
tangent[i*(prec+1)+j] = glm::vec3(0.0f, 0.0f, -1.0f);
} else {// 否则,计算切向量
tangent[i*(prec+1)+j] = glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(x,y,z));
}
. . . // 其余计算代码不变
}
}
对于那些表面不可导以至于无法精确求解切向量的模型,其切向量可以通过近似得到,例如在构造(或加载)模型时,将每个顶点指向下一个顶点的向量作为切向量。请注意,这种近似可能会导致切向量与顶点法向量不严格垂直。因此,如果要实现适用于各种模型的法线贴图,需要考虑这种可能性。我们的解决方案中对此进行了处理:
vec3 tangent = normalize(varyingTangent);
// 修正通过近似得到的切向量,确保切向量垂直于法向量
tangent = normalize(tangent - dot(tangent, normal) * normal);
下图直观展示了算法:
逆转置的应用将法向量和切向量转换为相机空间,之后我们使用叉积构造副切向量。
一旦我们在相机空间中得到法向量、 切向量和副切向量, 就可以使用它们来构造矩阵(依其分量命名为TBN矩阵
),TBN矩阵用于将从法线贴图中检索到的法向量转换为在相机空间中相对于物体表面的法向量。
然后,我们创建一个类型为 mat3 的 3×3 矩阵,作为 TBN。 mat3 构造函数接收 3 个向量作为参数,生成一个矩阵,其中顶行是第一个向量,中间行是第二个向量,底行是第三个向量。类似于从摄像机位置构建视图矩阵:
N指向上(“离开纹理图像”),T和B指向与U和V相同的方向。
切线空间——TBN矩阵
既然我们把法线向量(x、y、z)映射到一张贴图纹理的(r、g、b)分量上,那么第一思维直觉,每个片元的法向量肯定是垂直于这个纹理所在的平面(即st坐标组成的平面),三个分量的比重大部分都在z(b)分量上,所以的法线纹理多数就是呈现出偏蓝色的外观视觉。但是这样会有一个严峻的问题,对于正方体六个面而言,只有顶部面的法向量是(0, 0, 1),其他方向的面岂不是不能引用这张法线贴图?
思考一下,我们是怎么把模型顶点/纹理坐标,转换成世界坐标?法向量是如何同步到模型的变化操作?都是通过坐标系的矩阵运算,这里引入切线空间(tangent space)
坐标系。普通2维纹理坐标包含U、V两项,其中U坐标增长的方向,即切线空间中的切线方向(tangent轴);V坐标增加的方向,为切线空间中的副切线方向(bitangent轴)模型中不同的面,都有对应的切线空间,其tangent轴和bitangent轴分别位于绘制的平面上,结合对应的法线方向,我们称tangent轴(T)、bitangent轴(B)及法线轴(N)所组成的坐标系,即切线空间(TBN)(如下图)。
有了TBN切线空间坐标系,从法线贴图中提取的法向量,就是基于TBN的数值,然后我们再数学矩阵运算,乘以一个TBN转换矩阵,就可以正确的转换成模型所需要的正确方向的法线向量了。
mat3 tbmMat = mat3(tangent, bitangent, normal);
只有当它们全部在同一坐标系中时,才能用有意义的方式计算事物。 TBN允许你从一个转换到另一个。
其中TBN矩阵的求法,更深层次的数学转换原理、请参考以下链接:
https://blog.csdn.net/jxw167/article/details/58671953
https://blog.csdn.net/yuchenwuhen/article/details/71055602
// 顶点着色器
#version 430
out vec3 varyingNormal;
out vec3 varyingTangent;
uniform mat4 norm_matrix;// 逆转置矩阵
...
void main() {
varyingNormal = (norm_matrix * vec4(vertNormal, 1.0)).xyz;
varyingTangent = (norm_matrix * vec4(vertTangent, 1.0)).xyz;
...
}
// 片段着色器
#version 430
in vec3 varyingLightDir;
in vec3 varyingVertPos;
in vec3 varyingNormal;
in vec3 varyingTangent;
in vec3 originalVertex;
in vec2 tc;
in vec3 varyingHalfVector;
out vec4 fragColor;
layout(binding = 0) uniform sampler2D normMap;
...
vec3 calcNewNormal() {
vec3 normal = normalize(varyingNormal);
vec3 tangent = normalize(varyingTangent);
// 确保切向量垂直于法向量:
// 由于切向量可能是通过近似法得到的非精确值,
// 近似可能会导致切向量与顶点法向量不严格垂直,
// 下面是解决方案
tangent = normalize(tangent - dot(tangent, normal) * normal);
vec3 bitangent = cross(tangent, normal);
// 用来变换到相机空间的 TBN 矩阵
mat3 tbnMat = mat3(tangent, bitangent, normal);
vec3 retrievedNormal = texture(normMap, tc).xyz;
// 从 RGB 空间转换
retrievedNormal = retrievedNormal * 2.0 - 1.0;
vec3 newNormal = tbnMat * retrievedNormal;
newNormal = normalize(newNormal);
return newNormal;
}
void main() {
// 归一化光照向量,法向量和视图向量
vec3 L = normalize(varyingLightDir);
vec3 V = normalize(-varyingVertPos);
vec3 N = calcNewNormal();
// 获得光照向量和曲面法向量之间的角度
float cosTheta = dot(L, N);
// 为 Blinn 优化计算半向量
vec3 H = normalize(varyingHalfVector);
// 视图向量和反射光向量之间的角度
float cosPhi = dot(H, N);
// 计算 ADS 贡献(每个像素)
fragColor = globalAmbient * material.ambient
+ light.ambient * material.ambient
+ light.diffuse * material.diffuse * max(cosTheta,0.0)
+ light.specular * material.specular * pow(max(cosPhi,0.0), material.shininess*3.0);
}
下图展示了使用两种不同方式渲染的,用以表现月球表面的球体。
左图中,球体使用了原始的纹理贴图;右图中,球体使用法线贴图的图像作为纹理。它们都没有应用法线贴图。虽然左侧使用了纹理的“月球”非常逼真,但仔细观察可以发现,纹理图案很明显拍摄于阳光从左侧照亮月球的时候,因为其山脊的阴影投射到了右侧(在底部中心附近的火山口中最明显)。如果我们使用 Phong 着色为此场景添加光照,然后移动月球、相机或灯光来给场景添加动画,就会发现月球上的阴影不会如我们期望地改变。
此外,随着光源的移动(或相机移动),期望中会在山脊上出现许多镜面高光。但是下图左图使用了标准纹理的球体将只产生一个镜面高光,对应于光滑球体上所出现的高光,这看起来非常不现实。配合法线贴图可以显著提高这类对象在光照下的真实感。
当我们在球体上使用法线贴图(而不是纹理)时,我们会得到下图所示的结果。尽管它不像标准纹理那么真实(现在),但是现在它确实响应了光照变化。下图的第一张图像中从左侧进行光照,第二张图像中则从右侧进行光照。请注意蓝色和黄色箭头所示部分展示了山脊周围漫反射光的变化以及镜面反射高光的移动。
纹理加法线贴图
下展示了在使用 Phong 光照模型的情况下,将法线贴图与标准纹理相结合的效果。月球的图像通过漫射区域进行了增强,镜面高光区域也会响应光源的移动(或相机或物体移动)。两个图像中的光照分别来自左侧和右侧。
我们的程序现在需要两个纹理——一个用于月球表面图像,一个用于法线贴图——因此需要有两个采样器。片段着色器将纹理颜色与经光照计算所得的颜色进行混合(使用Blog“✠OpenGL-7-光照——结合光照与纹理”中的技术)。
// 片段着色器中的变量和结构与之前相同,加上
layout(binding = 0) uniform sampler2D s0; // 法线贴图
layout(binding = 1) uniform sampler2D s1; // 纹理
vec3 calcNewNormal() {
...
vec3 retrievedNormal = texture(s0, tc).xyz;
}
void main(void) { // 计算与之前相同,直到
vec3 N = calcNewNormal();
vec4 texel = texture(s1, tc); // 标准纹理
. . .
// 反射计算与之前相同,然后混合结果
fragColor = globalAmbient +
texel * (light.ambient + light.diffuse * max(cosTheta,0.0)
+ light.specular * pow(max(cosPhi,0.0), material.shininess));
}
用到的标准纹理与法线贴图如下:
伪影:
如下图,左边的球体(未使用多级渐远纹理贴图)周边有闪烁的伪影。
对于法线贴图而言,各向异性过滤( AF)更有效,它不但减少了闪烁的伪影,同时还保留了细节。左下图(使用AF后的法线贴图)右下角边缘细节充分,右下图(纹理+AF法线贴图)使用相等的纹理权重和光照权重,光照应用了法线贴图及AF的情况下得到的结果。
高度贴图
现在我们扩展法线贴图的概念——从纹理图像用于扰动法向量到扰乱顶点位置本身。实际上,以这种方式修改对象的几何体具有一定的优势,例如使表面特征沿着对象的边缘可见,并使特征能够响应阴影贴图。
一种实用的方法是使用纹理图像来存储高度值,然后使用该高度值来提升(或降低)顶点位置。含有高度信息的图像称为高度图
,使用高度图更改对象的顶点的方法称为高度贴图
。
这里使用了高度贴图说法, 通过纹理图像更改顶点的方法一般称为位移贴图/置换贴图。 高度图除了用于位移贴图/置换贴图,有时也用于视差贴图,请注意区别。
高度图通常将高度信息编码为灰度颜色:(0,0,0)(黑色)=低高度,(1,1,1)(白色)=高高度。这样一来通过算法或使用“画图”程序就可以轻松创建高度图。图像的对比度越高,其表示的高度变化越大。这些概念将在下面第一张图(显示随机生成的地图)和下面第二张图(显示有组织的模式的地图)中说明。
改变顶点位置是否有用取决于改变的模型。顶点操作可以在顶点着色器中轻松完成,当模型顶点细节级别够高(例如在足够高精度的球体中)时,改变顶点高度的方法效果很好。但是,当模型的顶点数量很少(例如立方体的角)时,渲染对象的表面需要依赖于光栅器中的顶点插值来填充细节。当顶点着色器中可用于改变高度的顶点很少时,许多像素的高度将无法从高度图中检索,而需要由插值生成,从而导致表面细节较差。当然,在片段着色器中是不可能进行顶点操作的,因为这时顶点已被光栅化为像素位置。
下面程序是将顶点“向外”(即在表面法向量的方向上)移动的顶点着色器代
码。它通过将顶点法向量乘以从高度图检索所得的值,然后将该乘积与顶点位置相加,以“向外”移动顶点。
// 顶点着色器
#version 430
layout(location = 0) in vec3 vertPos;
layout(location = 1) in vec2 texCoord;
layout(location = 2) in vec3 vertNormal;
out vec2 tc;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout(binding = 0) uniform sampler2D t; // 用于纹理
layout(binding = 1) uniform sampler2D h; // 用于高度图
void main(void) {
// p 是高度图所改变的顶点位置。
// 由于高度图是灰度图,因此使用其任何颜色分量都可以(我们使用"r"),
// 除以 5.0 用来调整高度
vec4 p = vec4(vertPos, 1.0) + vec4((vertNormal * ((texture(h, texCoord).r) / 5.0f)), 1.0f);
tc = tex_coord;
gl_Position = proj_matrix * mv_matrix * p;
}
下图展示了通过在画图程序中涂鸦创建的简单高度图(左上角)。高度图
图像中还绘制了一个白色矩形。绿色版本的高度图(左下角)用作纹理。使用上面程序中展示的着色器将高度图应用于 100× 100 的矩形网格模型时, 会产生类似“地形” 的感觉(如图右图所示)。注意白色矩形是如何生成右边的悬崖的。
上图展示的渲染结果还算可以,因为模型(网格和球体)有足够数量的顶点来对高度贴图值进行采样。也就是说,模型具有大量的顶点,而高度图相对粗糙并且以低分辨率充分地采样。然而,仔细观察仍然会发现存在分辨率伪影,例如沿图中地形右侧凸起的矩形盒子的左下边缘。凸起的矩形盒子两侧看起来不是完美矩形,而且颜色有渐变效果,其原因是底层网格 100 像素× 100 像素的分辨率无法与高度图中的白色矩形完全对齐, 从而导致纹理的光栅化坐标沿侧面产生伪影。
当尝试将其应用于要求更严苛的高度贴图时,在顶点着色器中进行高度贴图的限制会进一步暴露。 考虑之前图中展示的月球图像。 法线贴图在捕获图像细节方面表现非常出色,而且由于它是灰度图,因此尝试将其作为高度图应用似乎很自然。
但是,基于顶点着色器的高度贴图会无法胜任这个任务,因为顶点着色器中采样的顶点数(即使对于精度=500 的球体)比起图像中的细节级别,仍然太少。相较之下,法线贴图能够很好地捕获细节,因为在片段着色器中对法线贴图的采样是像素级的。
一个完整示例
earthspec1kBLUE.jpg
earthspec1kNEG.jpg
earthspec1kNORMAL.jpg
main.cpp
...
using namespace std;
float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }
#define numVAOs 1
#define numVBOs 4
float cameraX, cameraY, cameraZ;
float sphLocX, sphLocY, sphLocZ;
float lightLocX, lightLocY, lightLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
// variable allocation for display
GLuint mvLoc, projLoc, nLoc;
GLuint globalAmbLoc, ambLoc, diffLoc, specLoc, posLoc, mambLoc, mdiffLoc, mspecLoc, mshiLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat, invTrMat;
glm::vec3 currentLightPos;
float lightPos[3];
float rotAmt = 0.0f;
Sphere mySphere(96);
int numSphereVertices;
GLuint colorTexture;
GLuint normalTexture;
GLuint heightTexture;
// white light
float globalAmbient[4] = { 0.1f, 0.1f, 0.1f, 1.0f };
float lightAmbient[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
float lightDiffuse[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
float lightSpecular[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
// silver material
float* matAmb = Utils::silverAmbient();
float* matDif = Utils::silverDiffuse();
float* matSpe = Utils::silverSpecular();
float matShi = Utils::silverShininess();
void setupVertices(void) {
numSphereVertices = mySphere.getNumIndices();
std::vector<int> ind = mySphere.getIndices();
std::vector<glm::vec3> vert = mySphere.getVertices();
std::vector<glm::vec2> tex = mySphere.getTexCoords();
std::vector<glm::vec3> norm = mySphere.getNormals();
std::vector<glm::vec3> tang = mySphere.getTangents();
std::vector<float> pvalues;
std::vector<float> tvalues;
std::vector<float> nvalues;
std::vector<float> tanvalues;
for (int i = 0; i < mySphere.getNumIndices(); i++) {
pvalues.push_back((vert[ind[i]]).x);
pvaluespython [片段]由LIEF转储PE表面信息
合肥工业大学2021届物联网工程毕业设计《基于pix2pix的数据增强方法在工业芯片表面缺陷检测中的应用》
合肥工业大学2021届物联网工程毕业设计《基于pix2pix的数据增强方法在工业芯片表面缺陷检测中的应用》