GLSL 编程/GLUT/凹凸表面的光照
本教程涵盖法线贴图。
这是关于纹理技术的两个教程中的第一个,这两个教程超越了二维表面(或表面的层)。在本教程中,我们从法线贴图开始,这是一种非常成熟的技术,可以伪造小的凹凸和凹痕的光照 - 即使在粗糙的多边形网格上也是如此。本教程的代码基于平滑镜面高光教程和纹理球体教程.
卡拉瓦乔的这幅画描绘了多马的怀疑,他不相信基督复活,直到他将手指放在基督的肋旁。使徒们的紧锁眉头不仅象征着这种怀疑,而且通过一种常见的面部表情清晰地传达了它。然而,为什么我们知道他们的额头实际上是紧锁的,而不是用一些明暗线条画出来的呢?毕竟,这只是一幅平面的画作。事实上,观众凭直觉假设这些是紧锁的眉头而不是画出来的眉头 - 尽管这幅画本身允许这两种解释。教训是:光滑表面上的凹凸通常可以通过光照本身来令人信服地传达,而无需任何其他线索(阴影、遮挡、视差效应、立体声等)。
法线贴图试图通过根据一些虚拟凹凸更改表面法线向量来传达光滑表面(即具有插值法线的粗糙三角形网格)上的凹凸。当使用这些修改后的法线向量计算光照时,观看者通常会感知到虚拟凹凸 - 即使渲染的是一个完全平坦的三角形。这种错觉当然会失效(尤其是在轮廓处),但在许多情况下它非常令人信服。
更具体地说,表示虚拟凹凸的法线向量首先被编码到纹理图像(即法线贴图)中。然后,片段着色器在纹理图像中查找这些向量,并根据它们计算光照。就这样。当然,问题在于如何将法线向量编码到纹理图像中。存在不同的可能性,片段着色器必须适应用于生成法线贴图的特定编码。
我们将使用左边的法线贴图并编写一个 GLSL 着色器来使用它。
法线贴图可以使用 Blender(以及其他软件)进行测试和创建;请参见Blender 3D: Noob to Pro wikibook 中的描述.
在本教程中,你应该使用立方体网格,而不是纹理球体教程中使用的 UV 球体。除此之外,你可以按照相同的步骤将材质和纹理图像分配给对象。请注意,你应该在属性窗口 > 对象数据选项卡中指定一个默认的UV贴图。此外,你应该在属性窗口 > 纹理选项卡 > 映射中指定坐标 > UV。
在解码法线信息时,最好知道数据是如何编码的。但是,选择并不多;因此,即使你不知道法线贴图是如何编码的,一些尝试通常也可以获得足够好的结果。首先,RGB 分量是介于 0 和 1 之间的数字;但是,它们通常在局部表面坐标系中表示介于 -1 和 1 之间的坐标(因为向量是归一化的,所以任何坐标都不能大于 +1 或小于 -1)。因此,从 RGB 分量到法线向量n 的映射可能是
, , 以及
但是, 坐标通常是正数(因为表面法线不允许指向内部)。这可以通过对 使用不同的映射来利用
, , and
如果有疑问,应该选择后者解码,因为它永远不会生成指向内部的表面法线。此外,通常需要对结果向量进行归一化。
在片段着色器中的实现,计算归一化向量 n 在变量 localCoords
中,可能是
vec4 encodedNormal = texture2D(normalmap, texCoords);
vec3 localCoords = 2.0 * encodedNormal.rgb - vec3(1.0);
通常,使用每个表面的点的局部表面坐标系来指定法线贴图中的法线向量。该局部坐标系的 轴由平滑的、插值的法线向量 N 给出,而 平面是表面的切平面,如左侧图像所示。具体而言, 轴由 3D 引擎提供给顶点的切线属性 T 指定。给定 和 轴, 轴可以通过顶点着色器中的叉积来计算,例如 B = T × N。(字母 B 指的是此向量的传统名称“副法线”。)
请注意,法线向量 N 使用模型视图矩阵的逆矩阵的转置从物体空间变换到视图空间(因为它与表面正交;参见 “应用矩阵变换”),而切线向量 T 指定表面上点之间的方向,因此使用模型视图矩阵进行变换。副法线向量 B 代表第三类向量,其变换方式不同。(如果你真的想知道:与“B×”对应的斜对称矩阵 B 像二次形式一样变换。)因此,最佳选择是首先将 N 和 T 变换到视图空间,然后使用变换后的向量的叉积在视图空间中计算 B。
还要注意,这些轴的配置取决于所提供的切线数据、法线贴图的编码和纹理坐标。但是,轴实际上总是正交的,法线贴图的蓝色调表示蓝色分量位于插值法线向量方向上。
有了视图空间中的归一化方向 T、B 和 N,我们可以轻松地形成一个矩阵,该矩阵将法线贴图的任何法线向量 n 从局部表面坐标系映射到视图空间,因为该矩阵的列只是轴向量的向量;因此,将 n 映射到视图空间的 3×3 矩阵是
这些计算由顶点着色器执行,例如以这种方式
attribute vec3 v_tangent;
varying mat3 localSurface2View; // mapping from
// local surface coordinates to view coordinates
varying vec4 texCoords; // texture coordinates
varying vec4 position; // position in view coordinates
void main()
{
mat4 mvp = p*v*m;
position = m * v_coord;
// the signs and whether tangent is in localSurface2View[1] or
// localSurface2View[0] depends on the tangent attribute, texture
// coordinates, and the encoding of the normal map
localSurface2World[0] = normalize(vec3(m * vec4(v_tangent, 0.0)));
localSurface2World[2] = normalize(m_3x3_inv_transp * v_normal);
localSurface2World[1] = normalize(cross(localSurface2World[2], localSurface2World[0]));
varyingNormal = normalize(m_3x3_inv_transp * v_normal);
texCoords = v_texcoords;
gl_Position = mvp * v_coord;
}
在片段着色器中,我们将该矩阵与 n(即 localCoords
)相乘。例如,使用此行
vec3 normalDirection = normalize(localSurface2World * localCoords);
有了视图空间中的新法线向量,我们就可以像 平滑镜面反射教程 中那样计算光照。
完整着色器代码
[edit | edit source]完整的片段着色器只是将所有代码段和 平滑镜面反射教程 中的逐像素光照整合在一起。
attribute vec4 v_coord;
attribute vec3 v_normal;
attribute vec2 v_texcoords;
attribute vec3 v_tangent;
uniform mat4 m, v, p;
uniform mat3 m_3x3_inv_transp;
varying vec4 position; // position of the vertex (and fragment) in world space
varying vec2 texCoords;
varying mat3 localSurface2World; // mapping from local surface coordinates to world coordinates
void main()
{
mat4 mvp = p*v*m;
position = m * v_coord;
// the signs and whether tangent is in localSurface2View[1] or
// localSurface2View[0] depends on the tangent attribute, texture
// coordinates, and the encoding of the normal map
localSurface2World[0] = normalize(vec3(m * vec4(v_tangent, 0.0)));
localSurface2World[2] = normalize(m_3x3_inv_transp * v_normal);
localSurface2World[1] = normalize(cross(localSurface2World[2], localSurface2World[0]));
texCoords = v_texcoords;
gl_Position = mvp * v_coord;
}
uniform mat4 m, v, p;
uniform mat4 v_inv;
uniform sampler2D normalmap;
varying vec4 position; // position of the vertex (and fragment) in world space
varying vec2 texCoords; // the texture coordinates
varying mat3 localSurface2World; // mapping from local surface coordinates to world coordinates
struct lightSource
{
vec4 position;
vec4 diffuse;
vec4 specular;
float constantAttenuation, linearAttenuation, quadraticAttenuation;
float spotCutoff, spotExponent;
vec3 spotDirection;
};
lightSource light0 = lightSource(
vec4(0.0, 2.0, -1.0, 1.0),
vec4(1.0, 1.0, 1.0, 1.0),
vec4(1.0, 1.0, 1.0, 1.0),
0.0, 1.0, 0.0,
180.0, 0.0,
vec3(0.0, 0.0, 0.0)
);
vec4 scene_ambient = vec4(0.2, 0.2, 0.2, 1.0);
struct material
{
vec4 ambient;
vec4 diffuse;
vec4 specular;
float shininess;
};
material frontMaterial = material(
vec4(0.2, 0.2, 0.2, 1.0),
vec4(0.920, 0.471, 0.439, 1.0),
vec4(0.870, 0.801, 0.756, 0.5),
50.0
);
void main()
{
vec4 encodedNormal = texture2D(normalmap, texCoords);
vec3 localCoords = 2.0 * encodedNormal.rgb - vec3(1.0);
vec3 normalDirection = normalize(localSurface2World * localCoords);
vec3 viewDirection = normalize(vec3(v_inv * vec4(0.0, 0.0, 0.0, 1.0) - position));
vec3 lightDirection;
float attenuation;
if (0.0 == light0.position.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(light0.position));
}
else // point light or spotlight (or other kind of light)
{
vec3 positionToLightSource = vec3(light0.position - position);
float distance = length(positionToLightSource);
lightDirection = normalize(positionToLightSource);
attenuation = 1.0 / (light0.constantAttenuation
+ light0.linearAttenuation * distance
+ light0.quadraticAttenuation * distance * distance);
if (light0.spotCutoff <= 90.0) // spotlight?
{
float clampedCosine = max(0.0, dot(-lightDirection, light0.spotDirection));
if (clampedCosine < cos(radians(light0.spotCutoff))) // outside of spotlight cone?
{
attenuation = 0.0;
}
else
{
attenuation = attenuation * pow(clampedCosine, light0.spotExponent);
}
}
}
vec3 ambientLighting = vec3(scene_ambient) * vec3(frontMaterial.ambient);
vec3 diffuseReflection = attenuation
* vec3(light0.diffuse) * vec3(frontMaterial.diffuse)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0) // light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0); // no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * vec3(light0.specular) * vec3(frontMaterial.specular)
* pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), frontMaterial.shininess);
}
gl_FragColor = vec4(ambientLighting + diffuseReflection + specularReflection, 1.0);
}
总结
[edit | edit source]恭喜!您完成了本教程!我们已经了解了
- 人类对形状的感知如何常常依赖于光照。
- 什么是法线贴图。
- 如何解码常见的法线贴图。
- 片段着色器如何解码法线贴图并将其用于逐像素光照。
进一步阅读
[edit | edit source]如果您还想了解更多
- 关于纹理映射(包括平铺和偏移),您应该阅读 纹理球体教程。
- 关于使用 Phong 反射模型的逐像素光照,您应该阅读 平滑镜面反射教程。
- 关于变换法线向量,您应该阅读 “应用矩阵变换”。
- 关于法线贴图,你可以阅读 Mark J. Kilgard 的文章:“一种针对现代 GPU 的实用且稳健的凹凸贴图技术”,GDC 2000:高级 OpenGL 游戏开发,可以在 网上找到。
返回 OpenGL 编程 - 光照部分 | 返回 GLSL 编程 - GLUT 部分 |