目录

  • 凹凸贴图
  • 法线贴图
    • 切线空间——TBN矩阵
    • 纹理加法线贴图
  • 高度贴图
    • 一个完整示例(结合:纹理+法线贴图+高度贴图)
  • 补充说明

我们将探讨几种与实现凹凸表面相关的方法,通过使用光照效果,即使在实际对象模型表面平滑的情况下,也能使对象看起来具有逼真的表面纹理。我们将首先观察 凹凸贴图法线贴图,当直接为对象添加微小表面细节会使得计算代价过高时,它们可以为场景中的对象增加相当程度的真实感。我们还将研究通过高度贴图实际扰乱光滑表面中顶点的方法,这对于生成地形(和其他一些用途)非常有用。

凹凸贴图


如上图,如果我们想让一个物体看起来好像有凹凸(或皱纹,陨石坑等),一种方法是计算当表面确实凹凸不平时其上的法向量。当场景点亮时,光照会让人产生我们所期望的幻觉。

// 片段着色器
#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 = normalize(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
法线贴图最终看起来基本都是蓝色的,因为图像文件中每个像素的 B 值(蓝色值)都是 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 -1。例如,读取到(0.5, 0.5, 1),则对应向量为(0, 0, 1)。

我们在纹理单元中存储所需的法向量而非颜色。然后,在给定片段中,我们就可以使用采样器从法线贴图中查找值,接下来,我们将所得的值作为法向量,而非输出像素颜色。但之个法向量不能直接使用,也不能乘MV矩阵的逆转置矩阵,而是要乘TBN矩阵才能使用。

用GIMP制作法线贴图的步骤:https://jingyan.baidu.com/article/a24b33cd2e66e519fe002bec.html

下图展示了两个不同的法线贴图图像文件以及在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));}. . . // 其余计算代码不变}
}

逆转置的应用将法向量和切向量转换为相机空间,之后我们使用叉积构造副切向量。

一旦我们在相机空间中得到法向量、 切向量和副切向量, 就可以使用它们来构造矩阵(依其分量命名为TBN矩阵),TBN矩阵作用:将从法线贴图中检索到的法向量转换为在相机空间中相对于物体表面的法向量

从法线贴图中读取的法线位于“切线空间(TBN)”中的。
纹理坐标与位置坐标,可以通过切线空间联系起来。如下图所示:


如上图所示,假定△ABC纹理映射到模型中△abc这块三角形上,纹理中存储的rgb分量代表该点处法线在纹理空间中的坐标,我们要将左边的纹理空间中的法向量转换到右边的模型空间中(这里先只讨论uv二维的坐标,第三维直接取模型空间的法向量即可),已知顶点的UV坐标,模型坐标以及法向量;现在将两个在UV坐标系下的向量用TB坐标系去表示。我们可以列出一个方程组:
ab⃗=AB⃗●u⃗∗t⃗+AB⃗●v⃗∗b⃗ac⃗=AC⃗●u⃗∗t⃗+AC⃗●v⃗∗b⃗\vec{ab} = \vec{AB}●\vec{u}*\vec{t} + \vec{AB}●\vec{v}*\vec{b}\\ \vec{ac} = \vec{AC}●\vec{u}*\vec{t} + \vec{AC}●\vec{v}*\vec{b} ab=AB●u∗t+AB●v∗bac=AC●u∗t+AC●v∗b
加上第三维坐标时,一般会选择垂直于TB平面的法向量N,这个向量是已知的。最后再将T,B,N归一化,就得到了转换矩阵。

我们创建一个类型为 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 tbnMat = mat3(tangent, bitangent, normal);

只有当它们全部在同一坐标系中时,才能用有意义的方式计算事物。 TBN允许你从一个转换到另一个。

其中TBN矩阵的求法,更深层次的数学转换原理、请参考以下链接:
https://blog.csdn.net/jxw167/article/details/58671953
https://blog.csdn.net/yuchenwuhen/article/details/71055602
https://blog.csdn.net/qq_35312463/article/details/105874855

对于那些表面不可导以至于无法精确求解切向量的模型,其切向量可以通过近似得到,例如在构造(或加载)模型时,将每个顶点指向下一个顶点的向量作为切向量(如果不这样做,一个复杂曲面的切向量可能实在不好求)。请注意,这种近似可能会导致切向量与顶点法向量不严格垂直。因此,如果要实现适用于各种模型的法线贴图,需要考虑这种可能性。我们的解决方案中对此进行了处理:修正通过近似得到的切向量,确保切向量垂直于法向量。下图直观展示了算法:

向量PQ是严格垂直于法向量normal的,即确保了切向量垂直于法向量。

// 顶点着色器
#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的情况下得到的结果。

// 如果启用【多级渐远纹理贴图】
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);// 如果还启用【各向异性过滤】
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {GLfloat anisoSetting = 0.0f;glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoSetting);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoSetting);
}
高度贴图

现在我们扩展法线贴图的概念——从纹理图像用于扰动法向量到扰乱顶点位置本身。实际上,以这种方式修改对象的几何体具有一定的优势,例如使表面特征沿着对象的边缘可见,并使特征能够响应阴影贴图。

一种实用的方法是使用纹理图像来存储高度值,然后使用该高度值来提升(或降低)顶点位置。含有高度信息的图像称为高度图,使用高度图更改对象的顶点的方法称为高度贴图

这里使用了高度贴图说法, 通过纹理图像更改顶点的方法一般称为位移贴图/置换贴图。 高度图除了用于位移贴图/置换贴图,有时也用于视差贴图,请注意区别。

高度图通常将高度信息编码为灰度颜色:(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像素的分辨率无法与高度图中的白色矩形完全对齐, 从而导致纹理的光栅化坐标沿侧面产生伪影。

为什么凸起的矩形盒子: (a)不是完美矩形 (b)颜色有渐变效果 ?

已知:底面矩形平面分辨率是100×100像素;纹理图和高度图原始分辨率是320×320像素。
如果将纹理图和高度图尺寸调整为100×100像素,则效果如下图:

凸起的矩形盒子更粗糙了,因为纹理图和高度图分辨率更低了,误差就更大了。
将纹理图和高度图尺寸调整为500×500像素的高分辨率,并只显示凸起的矩形盒子,效果如下图:

原因:格子地面分辨率无法与凸起的矩形盒子完全对齐!分辨率不对齐,导致光栅化时插值让误差变大。接缝处无法彻底的一刀切开,接缝处总有游离的像素。
如果是凸起的“圆形”盒子,则误差更大,因为矩形四边切的相对还是很好的,而圆形的边缘切的误差肯定更大了。效果如下图:

除此之外,无论怎么进行优化,高度贴图永远都是一个“欺骗”手段,很难做到十分完美;如果仔细从各个角度去看就能发现一些不自然又或者纹理错误,因此大部分情况下高度贴图只会用在墙壁以及地面上,并且会保证玩家很难以各种奇怪的角度去“看”它们。

当尝试将其应用于要求更严苛的高度贴图时,在顶点着色器中进行高度贴图的限制会进一步暴露。 考虑之前图中展示的月球图像。 法线贴图在捕获图像细节方面表现非常出色,而且由于它是灰度图,因此尝试将其作为高度图应用似乎很自然。但是,基于顶点着色器的高度贴图会无法胜任这个任务,因为顶点着色器中采样的顶点数(即使对于精度=500 的球体)比起图像中的细节级别,仍然太少。相较之下,法线贴图能够很好地捕获细节,因为在 [片段着色器] 中对法线贴图的采样是像素级的。

一个完整示例(结合:纹理+法线贴图+高度贴图)

earthspec1kBLUE.jpg(纹理)

earthspec1kNORMAL.jpg(法线贴图)

earthspec1kNEG.jpg(高度贴图)

main.cpp

...
using namespace std;float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }#define numVAOs 1
#define numVBOs 4float 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);pvalues.push_back((vert[ind[i]]).y);pvalues.push_back((vert[ind[i]]).z);tvalues.push_back((tex[ind[i]]).s);tvalues.push_back((tex[ind[i]]).t);nvalues.push_back((norm[ind[i]]).x);nvalues.push_back((norm[ind[i]]).y);nvalues.push_back((norm[ind[i]]).z);tanvalues.push_back((tang[ind[i]]).x);tanvalues.push_back((tang[ind[i]]).y);tanvalues.push_back((tang[ind[i]]).z);}glGenVertexArrays(1, vao);glBindVertexArray(vao[0]);glGenBuffers(numVBOs, vbo);glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);glBufferData(GL_ARRAY_BUFFER, tanvalues.size() * 4, &tanvalues[0], GL_STATIC_DRAW);
}void installLights(glm::mat4 vMatrix) {glm::vec3 transformed = glm::vec3(vMatrix * glm::vec4(currentLightPos, 1.0));lightPos[0] = transformed.x;lightPos[1] = transformed.y;lightPos[2] = transformed.z;// get the locations of the light and material fields in the shaderglobalAmbLoc = glGetUniformLocation(renderingProgram, "globalAmbient");ambLoc = glGetUniformLocation(renderingProgram, "light.ambient");diffLoc = glGetUniformLocation(renderingProgram, "light.diffuse");specLoc = glGetUniformLocation(renderingProgram, "light.specular");posLoc = glGetUniformLocation(renderingProgram, "light.position");mambLoc = glGetUniformLocation(renderingProgram, "material.ambient");mdiffLoc = glGetUniformLocation(renderingProgram, "material.diffuse");mspecLoc = glGetUniformLocation(renderingProgram, "material.specular");mshiLoc = glGetUniformLocation(renderingProgram, "material.shininess");//  set the uniform light and material values in the shaderglProgramUniform4fv(renderingProgram, globalAmbLoc, 1, globalAmbient);glProgramUniform4fv(renderingProgram, ambLoc, 1, lightAmbient);glProgramUniform4fv(renderingProgram, diffLoc, 1, lightDiffuse);glProgramUniform4fv(renderingProgram, specLoc, 1, lightSpecular);glProgramUniform3fv(renderingProgram, posLoc, 1, lightPos);glProgramUniform4fv(renderingProgram, mambLoc, 1, matAmb);glProgramUniform4fv(renderingProgram, mdiffLoc, 1, matDif);glProgramUniform4fv(renderingProgram, mspecLoc, 1, matSpe);glProgramUniform1f(renderingProgram, mshiLoc, matShi);
}void init(GLFWwindow* window) {renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.0f;sphLocX = 0.0f; sphLocY = 0.0f; sphLocZ = -1.0f;lightLocX = 3.0f; lightLocY = 2.0f; lightLocZ = 3.0f;glfwGetFramebufferSize(window, &width, &height);aspect = (float)width / (float)height;pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);setupVertices();colorTexture = Utils::loadTexture("earthspec1kBLUE.jpg");// 纹理贴图normalTexture = Utils::loadTexture("earthspec1kNORMAL.jpg");// 法线贴图heightTexture = Utils::loadTexture("earthspec1kNEG.jpg");// 高度贴图
}void display(GLFWwindow* window, double currentTime) {glClear(GL_DEPTH_BUFFER_BIT);glClearColor(0.0, 0.0, 0.0, 1.0);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(renderingProgram);mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");nLoc = glGetUniformLocation(renderingProgram, "norm_matrix");glfwGetFramebufferSize(window, &width, &height);aspect = (float)width / (float)height;pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));mMat = glm::translate(glm::mat4(1.0f), glm::vec3(sphLocX, sphLocY, sphLocZ));mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));mMat = glm::rotate(mMat, rotAmt, glm::vec3(0.0f, 1.0f, 0.0f));rotAmt += 0.002f;mvMat = vMat * mMat;invTrMat = glm::transpose(glm::inverse(mvMat));currentLightPos = glm::vec3(lightLocX, lightLocY, lightLocZ);installLights(vMat);glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);glEnableVertexAttribArray(0);glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);glEnableVertexAttribArray(1);glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);glEnableVertexAttribArray(2);glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, 0);glEnableVertexAttribArray(3);glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, colorTexture);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, normalTexture);glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, heightTexture);glEnable(GL_CULL_FACE);glFrontFace(GL_CCW);glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glDrawArrays(GL_TRIANGLES, 0, numSphereVertices);
}int main(void) {...
}

vertShader.glsl

#version 430
layout (location = 0) in vec3 vertPos;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in vec3 vertNormal;
layout (location = 3) in vec3 vertTangent;out vec3 varyingLightDir;
out vec3 varyingVertPos;
out vec3 varyingNormal;
out vec3 varyingTangent;
out vec2 tc;layout (binding=2) uniform sampler2D h;struct PositionalLight {vec4 ambient;vec4 diffuse;vec4 specular;vec3 position;
};
struct Material {vec4 ambient;vec4 diffuse;vec4 specular;float shininess;
};uniform PositionalLight light;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;// 逆转置矩阵void main(void) {// 用于法线贴图varyingNormal = (norm_matrix * vec4(vertNormal,1.0)).xyz;varyingTangent = (norm_matrix * vec4(vertTangent,1.0)).xyz;// 用于高度贴图vec4 P1 = vec4(vertPos, 1.0);vec4 P2 = vec4((vertNormal*((texture(h,texCoord).r)/15.0)), 1.0);vec4 P = vec4((P1.xyz + P2.xyz), 1.0);varyingVertPos = (mv_matrix * P).xyz;varyingLightDir = light.position - varyingVertPos;// 用于纹理贴图tc = texCoord;gl_Position = proj_matrix * mv_matrix * P;// 顶点P是经高度贴图变换后的
}

fragShader.glsl

#version 430in vec3 varyingLightDir;
in vec3 varyingVertPos;
in vec3 varyingNormal;
in vec3 varyingTangent;
in vec2 tc;out vec4 fragColor;layout (binding=0) uniform sampler2D t;// 纹理贴图
layout (binding=1) uniform sampler2D n;// 法线贴图struct PositionalLight {vec4 ambient;  vec4 diffuse;  vec4 specular;  vec3 position;
};
struct Material {vec4 ambient;  vec4 diffuse;  vec4 specular;  float shininess;
};uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;vec3 CalcBumpedNormal() {vec3 Normal = normalize(varyingNormal);vec3 Tangent = normalize(varyingTangent);Tangent = normalize(Tangent - dot(Tangent, Normal) * Normal);vec3 Bitangent = cross(Tangent, Normal);vec3 BumpMapNormal = texture(n,tc).xyz;// 法线贴图BumpMapNormal = BumpMapNormal * 2.0 - 1.0;mat3 TBN = mat3(Tangent, Bitangent, Normal);vec3 NewNormal = TBN * BumpMapNormal;NewNormal = normalize(NewNormal);return NewNormal;
}void main(void) {// normalize the light, normal, and view vectors:vec3 L = normalize(varyingLightDir);vec3 V = normalize(-varyingVertPos);//vec3 N = CalcBumpedNormal();vec3 N = CalcBumpedNormal();// get the angle between the light and surface normal:float cosTheta = dot(L,N);// compute light reflection vector, with respect N:vec3 R = normalize(reflect(-L, N));// angle between the view vector and reflected light:float cosPhi = dot(V, R);// compute ADS contributions (per pixel):fragColor =0.5 * texture(t, tc)// 纹理贴图+0.5 * (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));
}

补充说明

凹凸贴图或法线贴图的一个基本限制是,虽然它们能够在渲染对象的内部提供表面细节的外观,但是物体轮廓(外边界)无法显示这些细节(它保持平滑)。
高度贴图在用于实际修改顶点位置时修复了这个缺陷,但它也有其自身的局限性。正如我们将在之后将看到的,有时可以使用几何着色器或曲面细分着色器来增加顶点的数量,使高度贴图更加实用、有效。
这里冒昧地简化了一些凹凸贴图和法线贴图计算。在重要应用中可以使用更准确和/或更有效的解决方案。

✠OpenGL-10-增强表面细节相关推荐

  1. openGL增强表面细节--凹凸贴图

    openGL系列文章目录 文章目录 openGL系列文章目录 前言 一.凹凸贴图 二.代码 1.主程序c++ 总结 前言 假设我们想要对不规则表面的物体进行建模,例如橘子凹凸的表皮.葡萄干褶皱的表面 ...

  2. openGL增强表面细节--凹凸贴图具体实现

    openGL系列文章目录 文章目录 openGL系列文章目录 前言 一.凹凸贴图 二.代码 1.主程序c++ 2.着色器程序 运行效果 源码下载 前言 假设我们想要对不规则表面的物体进行建模,例如橘子 ...

  3. openGL增强表面细节----高度贴图

    openGL系列文章目录 文章目录 openGL系列文章目录 前言 一.代码 主程序c++ 效果 前言 现在我们扩展法线贴图的概念--从纹理图像用于扰动法向量到扰乱顶点位置本身.实 际上,以这种方式修 ...

  4. C#中使用tao.opengl绘制三维模型

    一.tao.Opengl技术简介 Opengl是一种C风格的图形库,即opengl中没有类和对象,只有大量的函数.Opengl在内部就是一个状态机,利用不同的函数来修改opengl状态机的状态,以达到 ...

  5. tao.Opengl

    一.tao.Opengl技术简介 Opengl是一种C风格的图形库,即opengl中没有类和对象,只有大量的函数.Opengl在内部就是一个状态机,利用不同的函数来修改opengl状态机的状态,以达到 ...

  6. tao.opengl + C#

    一.tao.Opengl技术简介 Opengl是一种C风格的图形库,即opengl中没有类和对象,只有大量的函数.Opengl在内部就是一个状态机,利用不同的函数来修改opengl状态机的状态,以达到 ...

  7. 木纤维增强聚丙烯复合材料:压缩和注塑成型工艺

    木纤维增强聚丙烯复合材料: 压缩和注塑成型工艺 摘要 含有不同种类的木质纤维(硬木纤维和软木纤维)的木纤维增强聚丙烯复合材料经过注射成型和挤压成型工艺而成.人们针对不同处理系统和相容剂对复合材料的力学 ...

  8. 神雕侠侣手游服务器维护,《神雕侠侣》10月25日更新维护公告

    亲爱的玩家朋友: 为了给您提供更好的游戏体验,所有服务器将于2012年10月25日早上8:00点进行停机维护,所有服务器预计维护完成时间为12:00,具体开服时间以官方信息为准.请各位玩家提前处理好游 ...

  9. 普通For循环和增强For循环

    目录 定义 代码演示 总结 定义 普通for循环 步骤:初始表达式:条件表达式:递增表达式) 有时候递增表达式也可以省去,一般是在使用Iterator迭代时(Iterator的hasNext方法判断时 ...

  10. Actuate 10把企业报表带进WEB 2.0 时代

    Actuate 10把企业报表带进WEB 2.0 时代 2008-12-25 13:40:41[作者]畅享网 畅享网消息:Actuate (安讯) 公司近日宣布,即将于中国市场推出安讯最新一代产品Ac ...

最新文章

  1. 利用OpenCV、Python和Ubidots构建行人计数器程序(附完整代码)
  2. ZABBIX作集中式NGINX性能监控的注意要点
  3. [BUUCTF-pwn]——pwn1_sctf_2016
  4. 官宣!阿里Blink和Flink合并计划出炉
  5. (转)Hibernate框架基础——cascade属性
  6. 如何在oracle中查询所有用户表的表名、主键名称、索引、外键等 - Oracle   基础和管理_files...
  7. 物联网处理器五大类型应用分析指南
  8. 从上往下打印二叉树(C++)
  9. BZOJ3456: 城市规划 多项式求逆
  10. 基于Servlet+jsp的web计算器
  11. Java阶乘中数值溢出
  12. 破解大众点评 css加密
  13. python加密狗的制作_制作u盘加密狗
  14. 二元二次拟合 matlab函数
  15. 虚拟服务器修改教程,【新挑战】十二职业虚拟机一键端图文架设修改教程
  16. 【Hello,互联网】百家争鸣的互联网时代
  17. laravel的Eloquent模型
  18. Android Jetpack 六大架构组件全面了解
  19. java framemaker教程_Freemarker入门案例
  20. Zookeeper之基础知识

热门文章

  1. 超级账本学习之三:创建超级账本网络
  2. crm客户管理系统如何助力企业销售管理
  3. oracle时间回溯,关于Oracle降序索引的定意及回溯
  4. QT中窗口置顶失效问题解决方案
  5. 黄芪桂圆枸杞红枣茶的作用
  6. python画三维(3D)图
  7. 图像变形算法:实现Photoshop液化工具箱中向前变形工具
  8. 【STM32】 电解电容
  9. 闲聊调度系统 Apache Airflow
  10. git color 让git有颜色