GLSL 编程/Unity/凹凸表面的投影
本教程介绍(单步)视差贴图。
它扩展并基于“凹凸表面的光照”部分。
在“凹凸表面的光照”部分中介绍的法线贴图技术只改变平面的光照效果,以创造凹凸和凹陷的错觉。如果一个人直视表面(即朝向表面法线向量方向),这种方法效果很好。但是,如果一个人从其他角度观察表面(如左侧图像所示),凹凸也应该从表面突出,而凹陷应该凹进表面。当然,可以通过几何建模凹凸和凹陷来实现这一点;但是,这将需要处理更多的顶点。另一方面,单步视差贴图是一种类似于法线贴图的非常高效的技术,它不需要额外的三角形,但仍然可以将虚拟凹凸移动几个像素,使它们从平面表面突出。然而,这种技术仅限于高度较小的凹凸和凹陷,需要一些微调才能获得最佳效果。
视差贴图是由 Tomomichi Kaneko 等人于 2001 年在其论文“视差贴图的详细形状表示”(ICAT 2001)中提出的。基本思想是对用于表面纹理的纹理坐标(尤其是法线贴图)进行偏移。如果这种纹理坐标偏移计算得当,则可以移动纹理的一部分(例如凹凸),就好像它们从表面突出一样。
左侧的插图显示了指向观察者的视向量 V 和在片段着色器中光栅化的表面点的表面法线向量 N。视差贴图分三步进行
- 在光栅化点查找高度图中的高度 ,该高度图由插图底部直线上方的波浪线表示。
- 计算方向为 V 的视线与高度为 的表面(平行于渲染表面)的交点。距离 是光栅化表面点在 N 方向上移动 后的点与该交点的距离。如果将这两个点投影到渲染的表面上,则 也是光栅化点与表面上一个新点的距离(插图中用十字标记)。如果表面被高度图位移,这个新的表面点更接近于方向为 V 的视线实际上可见的点。
- 将偏移量 转换为纹理坐标空间,以便为所有后续纹理查找计算纹理坐标偏移量。
为了计算 ,我们需要光栅化点的高度图高度 ,这在示例中通过纹理属性 _ParallaxMap
的 A 分量的纹理查找来实现,它应该是一个灰度图像,如“凹凸表面的光照”部分中所述,表示高度。我们还需要以法线向量( 轴)、切线向量( 轴)和副法线向量( 轴)形成的局部表面坐标系中的视方向 V,这也在“凹凸表面的光照”部分中介绍过。为此,我们计算从局部表面坐标到物体空间的变换,方法是:
其中 T、B 和 N 在物体坐标系中给出。(“凹凸表面的光照”部分 中我们有一个类似的矩阵,但向量在世界坐标系中。)
我们在物体空间中计算视角方向 V(作为光栅化位置和从世界空间变换到物体空间的相机位置之间的差),然后使用矩阵 将其变换到局部表面空间,该矩阵可以计算为
这是可能的,因为 T、B 和 N 是正交且归一化的。(实际上,情况稍微复杂一些,因为我们不会归一化这些向量,而是使用它们的长度进行另一个变换;见下文。)因此,为了将 V 从物体空间变换到局部表面空间,我们必须用转置矩阵 进行乘法。在 GLSL 中,这是通过将向量从左侧乘以矩阵 来实现的。
一旦我们在局部表面坐标系中得到 V,其中 轴指向法向量 N 的方向,我们就可以使用相似三角形(与插图比较)计算偏移量 (在 方向)和 (在 方向)。
and .
因此
and .
请注意,这里不需要对 **V** 进行归一化,因为我们只使用其分量的比例,而比例不受归一化影响。
最后,我们需要将 和 转换到纹理空间。如果没有 Unity 的帮助,这将非常困难:Tangent
属性实际上已经适当缩放,并且具有第四个分量 Tangent.w
用于缩放副法线向量,以使观察方向 **V** 的转换能够适当缩放 和 ,以便在纹理坐标空间中获得 和 ,而无需进行进一步的计算。
实现
[edit | edit source]实现代码与 “凹凸表面照明”部分 的代码共享大部分内容。特别是,使用相同的副法线向量缩放与 Tangent
属性的第四个分量,以便将偏移量从局部表面空间到纹理空间的映射考虑在内。
vec3 binormal = cross(gl_Normal, vec3(Tangent)) * Tangent.w;
在顶点着色器中,我们需要为局部表面坐标系中的观察向量 **V** 添加一个变化量(考虑到映射到纹理空间的轴缩放)。这个变化量称为 viewDirInScaledSurfaceCoords
。它通过从左侧乘以矩阵 (localSurface2ScaledObject
)来计算观察向量,如上所述。
vec3 viewDirInObjectCoords = vec3(
modelMatrixInverse * vec4(_WorldSpaceCameraPos, 1.0)
- gl_Vertex);
mat3 localSurface2ScaledObject =
mat3(vec3(Tangent), binormal, gl_Normal);
// vectors are orthogonal
viewDirInScaledSurfaceCoords =
viewDirInObjectCoords * localSurface2ScaledObject;
// we multiply with the transpose to multiply with
// the "inverse" (apart from the scaling)
顶点着色器的其余部分与法线贴图相同,请参阅 “凹凸表面照明”部分。
在片段着色器中,我们首先查询高度图以获取栅格化点的深度。该深度由纹理 _ParallaxMap
的 A 分量指定。0 到 1 之间的数值通过着色器属性 _Parallax
转换为 -_Parallax
/2 到 +_Parallax
的范围,以便提供一些用户对效果强度的控制(并与备用着色器兼容)。
float height = _Parallax
* (-0.5 + texture2D(_ParallaxMap, _ParallaxMap_ST.xy
* textureCoordinates.xy + _ParallaxMap_ST.zw).a);
偏移量 和 然后按照上述方法计算。但是,我们还将每个偏移量限制在一个用户指定的范围 -_MaxTexCoordOffset
和 _MaxTexCoordOffset
之间,以确保偏移量保持在合理的范围内。(如果高度图由更多或更少的具有恒定深度的平坦平台组成,这些平台之间平滑过渡,则 _MaxTexCoordOffset
应该小于这些过渡区域的厚度;否则采样点可能会位于具有不同深度的另一个平台上,这意味着交点的近似值是任意不好的)。代码如下:
vec2 texCoordOffsets =
clamp(height * viewDirInScaledSurfaceCoords.xy
/ viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
在下面的代码中,我们需要将偏移量应用于所有纹理查找中的纹理坐标;也就是说,我们需要用 (textureCoordinates.xy + texCoordOffsets)
替换 vec2(textureCoordinates)
(或等效地 textureCoordinates.xy
),例如:
vec4 encodedNormal = texture2D(_BumpMap,
_BumpMap_ST.xy * (textureCoordinates.xy
+ texCoordOffsets) + _BumpMap_ST.zw);
片段着色器的其余代码与 “凹凸表面照明”部分 中的代码相同。
完整着色器代码
[edit | edit source]如上一节所述,此代码的大部分内容来自 “凹凸表面照明”部分。请注意,如果你想在具有 OpenGL ES 的移动设备上使用此代码,请确保更改法线贴图的解码方式,如该教程中所述。
关于视差贴图的部分实际上只有几行代码。大多数着色器属性的名称是根据备用着色器选择的;用户界面标签更具描述性。
Shader "GLSL parallax mapping" {
Properties {
_BumpMap ("Normal Map", 2D) = "bump" {}
_ParallaxMap ("Heightmap (in A)", 2D) = "black" {}
_Parallax ("Max Height", Float) = 0.01
_MaxTexCoordOffset ("Max Texture Coordinate Offset", Float) =
0.01
_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 sampler2D _ParallaxMap;
uniform vec4 _ParallaxMap_ST;
uniform float _Parallax;
uniform float _MaxTexCoordOffset;
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 unity_Scale; // w = 1/uniform scale;
// should be multiplied to _World2Object
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
varying vec3 viewDirInScaledSurfaceCoords;
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
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);
vec3 binormal =
cross(gl_Normal, vec3(Tangent)) * Tangent.w;
// appropriately scaled tangent and binormal
// to map distances from object space to texture space
vec3 viewDirInObjectCoords = vec3(modelMatrixInverse
* vec4(_WorldSpaceCameraPos, 1.0) - gl_Vertex);
mat3 localSurface2ScaledObject =
mat3(vec3(Tangent), binormal, gl_Normal);
// vectors are orthogonal
viewDirInScaledSurfaceCoords =
viewDirInObjectCoords * localSurface2ScaledObject;
// we multiply with the transpose to multiply
// with the "inverse" (apart from the scaling)
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// parallax mapping: compute height and
// find offset in texture coordinates
// for the intersection of the view ray
// with the surface at this height
float height =
_Parallax * (-0.5 + texture2D(_ParallaxMap,
_ParallaxMap_ST.xy * textureCoordinates.xy
+ _ParallaxMap_ST.zw).a);
vec2 texCoordOffsets =
clamp(height * viewDirInScaledSurfaceCoords.xy
/ viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
// normal mapping: lookup and decode normal from bump map
// 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
+ texCoordOffsets) + _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);
// per-pixel lighting using the Phong reflection model
// (with linear attenuation for point and spot lights)
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 sampler2D _ParallaxMap;
uniform vec4 _ParallaxMap_ST;
uniform float _Parallax;
uniform float _MaxTexCoordOffset;
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 unity_Scale; // w = 1/uniform scale;
// should be multiplied to _World2Object
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
varying vec3 viewDirInScaledSurfaceCoords;
#ifdef VERTEX
attribute vec4 Tangent;
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
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);
vec3 binormal =
cross(gl_Normal, vec3(Tangent)) * Tangent.w;
// appropriately scaled tangent and binormal
// to map distances from object space to texture space
vec3 viewDirInObjectCoords = vec3(modelMatrixInverse
* vec4(_WorldSpaceCameraPos, 1.0) - gl_Vertex);
mat3 localSurface2ScaledObject =
mat3(vec3(Tangent), binormal, gl_Normal);
// vectors are orthogonal
viewDirInScaledSurfaceCoords =
viewDirInObjectCoords * localSurface2ScaledObject;
// we multiply with the transpose to multiply
// with the "inverse" (apart from the scaling)
position = modelMatrix * gl_Vertex;
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
// parallax mapping: compute height and
// find offset in texture coordinates
// for the intersection of the view ray
// with the surface at this height
float height =
_Parallax * (-0.5 + texture2D(_ParallaxMap,
_ParallaxMap_ST.xy * textureCoordinates.xy
+ _ParallaxMap_ST.zw).a);
vec2 texCoordOffsets =
clamp(height * viewDirInScaledSurfaceCoords.xy
/ viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
// normal mapping: lookup and decode normal from bump map
// 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
+ texCoordOffsets) + _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);
// per-pixel lighting using the Phong reflection model
// (with linear attenuation for point and spot lights)
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 "Parallax Specular"
}
总结
[edit | edit source]恭喜!如果你真的理解了整个着色器,那么你已经走了很远了。事实上,着色器包含了许多概念(坐标系之间的变换,从左侧乘以正交矩阵的逆矩阵来应用它,Phong 反射模型,法线贴图,视差贴图……)。更具体地说,我们已经看到了
- 视差贴图是如何改进法线贴图的。
- 视差贴图是如何用数学方式描述的。
- 视差贴图是如何实现的。
进一步阅读
[edit | edit source]如果你想了解更多
- 关于着色器代码的细节,你应该阅读 “凹凸表面照明”部分。
- 关于视差贴图,您可以阅读 Kaneko Tomomichi 等人撰写的原始出版物:“Detailed shape representation with parallax mapping”,ICAT 2001,第 205-208 页,可在 网上 获取。