GLSL 编程/Unity/凹凸表面的光照
本教程涵盖法线贴图。
它是关于纹理技术的系列教程中的第一个,该技术超越了二维表面(或表面层)。在本教程中,我们从法线贴图开始,这是一种非常成熟的技术,即使在粗糙的多边形网格上也能模拟小凹凸的光照。本教程的代码基于“平滑镜面高光”部分和“纹理球体”部分.
左侧描绘的卡拉瓦乔的画作是关于圣多马的疑惑,他直到将手指放在基督的肋旁才相信基督复活。使徒们的皱眉不仅象征着这种疑惑,而且通过常见的面部表情清楚地传达了这种疑惑。然而,为什么我们知道他们的额头实际上是皱起的,而不是用一些明暗线条画出来的?毕竟,这只是一幅平面画作。事实上,观众直觉地假设它们是皱眉的,而不是画出来的眉毛,即使这幅画本身允许两种解释。教训是:光滑表面的凹凸通常可以通过光照本身来令人信服地传达,而无需任何其他线索(阴影、遮挡、视差效果、立体声等)。
法线贴图试图通过根据一些虚拟凹凸改变表面法线向量来传达光滑表面上的凹凸(即具有插值法线的粗糙三角形网格)。当使用这些修改后的法线向量计算光照时,观众通常会感知到虚拟凹凸,即使渲染的是完全平坦的三角形。这种幻觉当然会失效(尤其是在轮廓线处),但在许多情况下它非常令人信服。
更具体地说,代表虚拟凹凸的法线向量首先被编码到纹理图像中(即法线贴图)。然后,片段着色器在纹理图像中查找这些向量,并根据它们计算光照。就是这样。当然,问题在于在纹理图像中编码法线向量。有不同的可能性,片段着色器必须适应用于生成法线贴图的特定编码。
好消息是,您可以使用 Unity 从灰度图像轻松创建法线贴图:在您喜欢的绘图程序中创建灰度图像,并对表面的常规高度使用特定的灰色,对凹凸使用较浅的灰色,对凹陷使用较深的灰色。确保不同灰色之间的过渡平滑,例如通过模糊图像。当您使用资产 > 导入新资产导入图像时,在检查器视图中将纹理类型更改为法线贴图,并选中从灰度生成。单击应用后,预览应显示带有红色和绿色边缘的蓝色图像。或者,您可以导入左侧的编码法线贴图(不要忘记取消选中从灰度生成框)。
不太好的消息是,片段着色器必须执行一些计算才能解码法线。首先,纹理颜色存储在二维纹理图像中,即只有 alpha 分量 和一个可用颜色分量。颜色分量可以作为红色、绿色或蓝色分量访问,在所有情况下,都会返回相同的值。在这里,我们使用绿色分量 ,因为 Unity 也使用它。两个分量, 和 ,存储为 0 到 1 之间的数字;但是,它们表示 -1 到 1 之间的坐标 和 。映射是
and
从这两个分量中,可以计算出三维法线向量 n 的第三个分量 ,因为它们被归一化为单位长度
如果我们选择轴沿着光滑法线向量(从顶点着色器中设置的法线向量插值得到)的轴,则只有“+”解是必要的,因为我们无法渲染具有向内指向法线向量的表面。片元着色器中的代码片段可能如下所示
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
对于使用 OpenGL ES 的设备,解码实际上更简单,因为 Unity 在这种情况下不使用双分量纹理。因此,对于移动平台,解码变为
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords = 2.0 * encodedNormal.rgb - vec3(1.0);
但是,本教程的其余部分(以及“凹凸表面的投影”部分)将只涵盖(桌面)OpenGL。
Unity 使用每个表面点的局部表面坐标系来指定法线贴图中的法线向量。该局部坐标系的轴由世界空间中的光滑插值法线向量 N 给出,而平面是该表面切平面,如左侧图像所示。具体而言,轴由 Unity 提供给顶点的切线属性 T 指定(参见“着色器的调试”部分中关于属性的讨论)。给定和轴,轴可以通过顶点着色器中的叉积计算,例如 B = N × T。(字母 B 指的是这个向量的传统名称“副法线”。)
请注意,法线向量 N 使用模型矩阵的逆矩阵的转置从物体空间变换到世界空间(因为它与表面正交;参见“应用矩阵变换”部分),而切线向量 T 指定表面上点之间的方向,因此使用模型矩阵变换。副法线向量 B 代表第三类向量,其变换方式不同。(如果你真的想知道:与“B×”对应的斜对称矩阵 B 像二次型一样变换。)因此,最好的选择是首先将 N 和 T 变换到世界空间,然后使用变换后的向量的叉积在世界空间中计算 B。
使用世界空间中归一化的方向 T、B 和 N,我们可以轻松地形成一个矩阵,该矩阵将法线贴图的任何法线向量 n 从局部表面坐标系映射到世界空间,因为该矩阵的列只是轴的向量;因此,将 n 映射到世界空间的 3×3 矩阵为
这些计算由顶点着色器执行,例如,以这种方式执行
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from
// local surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
计算 binormal
中的因子 Tangent.w
特定于 Unity,即 Unity 提供切线向量和法线贴图,因此我们必须进行此乘法。
在片元着色器中,我们将 localSurface2World
中的矩阵乘以 n。例如,使用以下代码行
vec3 normalDirection =
normalize(localSurface2World * localCoords);
使用世界空间中的新法线向量,我们可以像在“光滑镜面高光”部分中一样计算光照。
完整的着色器代码
[edit | edit source]此着色器代码只是集成了所有代码片段,并使用我们针对像素光照的标准双通道方法。
Shader "GLSL normal mapping" {
Properties {
_BumpMap ("Normal Map", 2D) = "bump" {}
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
GLSLPROGRAM
// User-specified properties
uniform sampler2D _BumpMap;
uniform vec4 _BumpMap_ST;
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from local
// surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// in principle we have to normalize the columns of
// "localSurface2World" again; however, the potential
// problems are small since we use this matrix only to
// compute "normalDirection", which we normalize anyways
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
vec3 normalDirection =
normalize(localSurface2World * localCoords);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* 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(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor = vec4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
// User-specified properties
uniform sampler2D _BumpMap;
uniform vec4 _BumpMap_ST;
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec4 textureCoordinates;
varying mat3 localSurface2World; // mapping from
// local surface coordinates to world coordinates
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
localSurface2World[0] = normalize(vec3(
modelMatrix * vec4(vec3(Tangent), 0.0)));
localSurface2World[2] = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
localSurface2World[1] = normalize(
cross(localSurface2World[2], localSurface2World[0])
* Tangent.w); // factor Tangent.w is specific to Unity
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// in principle we have to normalize the columns of
// "localSurface2World" again; however, the potential
// problems are small since we use this matrix only to
// compute "normalDirection", which we normalize anyways
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * textureCoordinates.xy
+ _BumpMap_ST.zw);
vec3 localCoords =
vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
vec3 normalDirection =
normalize(localSurface2World * localCoords);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* 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(_LightColor0)
* vec3(_SpecColor) * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor =
vec4(diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Bumped Specular"
}
请注意,我们使用了在“纹理化球体”部分中解释的平铺和偏移一致变量 _BumpMap_ST
,因为此选项通常对凹凸贴图特别有用。
总结
[edit | edit source]恭喜你!你完成了本教程!我们已经了解了
- 人类对形状的感知通常依赖于光照。
- 什么是法线贴图。
- Unity 如何对法线贴图进行编码。
- 片元着色器如何解码 Unity 的法线贴图并将其用于逐像素光照。
如果您还想了解更多
- 关于纹理映射(包括平铺和偏移),您应该阅读 “纹理球体”部分。
- 关于使用 Phong 反射模型进行逐像素光照,您应该阅读 “平滑镜面高光”部分。
- 关于变换法向量,您应该阅读 “应用矩阵变换”部分。
- 关于法线贴图,您可以阅读 Mark J. Kilgard: “A Practical and Robust Bump-mapping Technique for Today’s GPUs”,GDC 2000:Advanced OpenGL Game Development,该文章可在 网上获取。