本教程涵盖(单步)视差映射。
它扩展并基于“凹凸表面的照明”部分。 请注意,本教程旨在教您此技术的工作原理。 如果您想在 Unity 中实际使用视差贴图,您应该使用支持它的内置着色器。
改进法线贴图
“凹凸表面的照明”一节中介绍的法线贴图技术只会改变平坦表面的照明,从而产生凹凸不平的错觉。 如果一个人直视一个表面(即在表面法向量的方向上),这非常有效。 然而,如果从其他角度看表面(如左图所示),凸起也应该伸出表面,而凹痕应该退回到表面中。 当然,这可以通过几何建模凸起和凹痕来实现; 然而,这需要处理更多的顶点。 另一方面,单步视差贴图是一种类似于法线贴图的非常有效的技术,它不需要额外的三角形,但仍然可以将虚拟凹凸移动几个像素,使它们突出平面。 但是,该技术仅限于小高度的凸起和凹痕,需要进行一些微调才能获得最佳效果。
视差映射解释
视差映射于 2001 年由
T
o
m
o
m
i
c
h
i
K
a
n
e
k
o
Tomomichi Kaneko
TomomichiKaneko 等人提出。 在他们的论文"
D
e
t
a
i
l
e
d
s
h
a
p
e
r
e
p
r
e
s
e
n
t
a
t
i
o
n
w
i
t
h
p
a
r
a
l
l
a
x
m
a
p
p
i
n
g
”
(
I
C
A
T
2001
)
Detailed shape representation with parallax mapping” (ICAT 2001)
Detailedshaperepresentationwithparallaxmapping”(ICAT2001)"中。基本思想是偏移用于表面纹理(特别是法线贴图)的纹理坐标。 如果纹理坐标的偏移计算得当,就可以移动纹理的一部分(例如凹凸),就像它们伸出表面一样。
上面的插图显示了观察者方向上的视图向量 V 和在片段着色器中光栅化的表面点中的表面法向量 N。 视差贴图分 3 个步骤进行:
-
在高度图中的光栅化点处查找高度 h h h,如图中底部直线顶部的波浪线所示。
-
计算 V V V 方向上的视线与平行于渲染表面的高度为 h h h 的表面的交点。 距离 o o o 是在 N N N 方向移动了 h h h 的光栅化表面点与该交点之间的距离。 如果这两个点投影到渲染表面上, o o o 也是光栅化点和表面上新点之间的距离(图中用十字标记)。 如果表面被高度图置换,这个新的表面点更好地近似于方向 V V V 上的视图光线实际可见的点。
-
将偏移量 o o o 转换为纹理坐标空间,以便为所有后续纹理查找计算纹理坐标的偏移量。
对于 o o o 的计算,我们需要光栅化点处的高度图的高度 h h h,在示例中通过纹理属性_ParallaxMap
的 A A A 分量中的纹理查找实现,它应该是表示高度的灰度图像。我们还需要由法向量( z z z 轴)、切向量( x x x轴)和副法向量( y y y 轴)形成的局部表面坐标系中的视图方向 V V V。为此,我们计算从局部表面坐标到对象空间的变换矩阵:
M s u r f a c e → o b j e c t = [ T x B x N x T y B y N y T z B z N z ] M_{surface \to object}= \begin{bmatrix} T_x&B_x &N_x \\ T_y&B_y &N_y\\ T_z&B_z &N_z \end{bmatrix} Msurface→object=⎣⎡TxTyTzBxByBzNxNyNz⎦⎤
其中 T T T、 B B B 和 N N N 在对象坐标中给出,我们计算对象空间中的视图方向 V V V(作为光栅化位置与从世界空间转换到对象空间的相机位置之间的差),然后我们使用矩阵 M s u r f a c e → o b j e c t M_{surface \to object} Msurface→object将其转换到局部表面空间,计算公式如下:
M s u r f a c e → o b j e c t = M s u r f a c e → o b j e c t − 1 = M s u r f a c e → o b j e c t T M_{surface \to object}=M_{surface \to object}^{-1}=M_{surface \to object}^T Msurface→object=Msurface→object−1=Msurface→objectT
这是可能的,因为 T T T、 B B B 和 N N N 是正交和归一化的。(实际上,情况有点复杂,因为我们不会对这些向量进行归一化,而是将他们的长度用于另一个变换,见下文。)因此,为了将 V V V从对象空间变换到局部表面空间,我们必须将它与它的转置矩阵 M s u r f a c e → o b j e c t T M_{surface \to object}^T Msurface→objectT相乘。这实际上很好,因为在 Cg 中更容易构建转置矩阵,因为 T T T、 B B B 和 N N N 分别是转置矩阵的行向量。
一旦我们在局部表面坐标系中获得了 V V V向量, z z z轴在法向量 N N N方向上,我们可以通过相似三角形来计算偏移量 o x o_x ox( x x x 方向)和 o y o_y oy( y y y 方向):o x h = V x V z \frac {o_x}{h}=\frac {V_x}{V_z} hox=VzVx 且 o y h = V y V z \frac {o_y}{h}=\frac {V_y}{V_z} hoy=VzVy
因此:
o x = h V x V z o_x=h \frac {V_x}{V_z} ox=hVzVx 且 o y = h V y V z o_y=h \frac {V_y}{V_z} oy=hVzVy请注意,没有必要对 V 进行归一化,因为我们仅使用其分量的比率,不受归一化影响。
最后,我们必须将 o_x 和 o_y 转换为纹理空间。 如果 Unity 不帮助我们,这将是相当困难的:切线属性 tangent 实际上被适当地缩放,并且有第四个分量 tangent.w 用于缩放副法向向量,以便视图方向 V 的变换适当地缩放 V_x 和 V_y 以获得 o_x 和 o_y 在纹理坐标空间中,无需进一步计算。
实现
该实现与“凹凸表面的照明”部分共享大部分代码。 特别是,使用与切线属性的第四个分量相同的副法线向量缩放,以便考虑从局部表面空间到纹理空间的偏移量的映射:
float3 binormal = cross(input.normal, input.tangent.xyz)
* input.tangent.w;
我们必须在局部表面坐标系中为视图向量V添加一个输出参数(使用轴的缩放以考虑到纹理空间的映射)。此参数为viewDirInScaledSurfaceCoords
,它是通过使用矩阵
M
s
u
r
f
a
c
e
→
o
b
j
e
c
t
T
M_{surface \to object}^T
Msurface→objectT转换对象坐标(viewDirInObjectCoords
)中的视图向量来计算的,综上所述:
float3 viewDirInObjectCoords = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0)).xyz
- input.vertex.xyz;
float3x3 localSurface2ScaledObjectT = float3x3(input.tangent.xyz, binormal, input.normal);
output.viewDirInScaledSurfaceCoords = mul(localSurface2ScaledObjectT, viewDirInObjectCoords )
顶点着色器的其余部分与法线贴图相同,请参见“凹凸表面的照明”部分,除了世界坐标中的视图方向是在顶点着色器而不是片段着色器中计算的,这对于保持片段着色器中的算术运算数量对于某些 GPU 来说足够小是必要的。
在片段着色器中,我们首先查询高度图以获取光栅化点的高度。该高度由纹理_ParallaxMap
的
A
A
A分量指定。
0
0
0 和
1
1
1 之间的值被转换为 -_Parallax/2
到 +_Parallax
的范围,并带有着色器属性 _Parallax
,以便为用户提供对效果强度的一些控制(并于FallBack着色器兼容):
float height = _Parallax * (-0.5 + tex2D(_ParallaxMap,_ParallaxMap_ST.xy
* input.tex.xy + _ParallaxMap_ST.zw).x);
如上所述计算偏移量
o
x
o_x
ox 和
o
y
o_y
oy 。我们还在用户指定的间隔 -_MaxTexCoordOffset
和 _MaxTexCoordOffset
之间限制每个偏移量,以确保偏移量保持在合理的范围内。(如果高度图由或多或少的恒定高度平坦的高原组成,这些高原之间有平滑的过渡,_MaxTexCoordOffset
应该小于这些过渡区域的厚度;否则采样点可能位于具有不同高度的不同高原,这将意味着交点的近似是任意糟糕的。)代码如下:
float2 texCoordOffsets = clamp(height * input.viewDirInScaledSurfaceCoords.xy
/ input.viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset,+_MaxTexCoordOffset);
在下面的代码中,我们必须在所有纹理查找中将偏移量应用于纹理坐标;即,我们必须用 (input.tex.xy + texCoordOffsets)
替换 float2(input.tex)
(或等效的 input.tex.xy
),例如:
float4 encodedNormal = tex2D(_BumpMap, _BumpMap_ST.xy
* (input.tex.xy + texCoordOffsets)
+ _BumpMap_ST.zw);
片段着色器代码的其余部分与“凹凸表面的照明”部分一样。
完整的着色器代码
如上一节所述,大部分代码取自“凹凸表面的照明”一节。 请注意,如果您想在带有 OpenGL ES 的移动设备上使用该代码,请确保按照该教程中的说明更改法线贴图的解码。
关于视差映射的部分实际上只有几行。 着色器属性的大部分名称都是根据回退着色器选择的; 用户界面标签更具描述性。
Shader "Cg 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
}
CGINCLUDE // common code for all passes of all subshaders
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform sampler2D _BumpMap;
uniform float4 _BumpMap_ST;
uniform sampler2D _ParallaxMap;
uniform float4 _ParallaxMap_ST;
uniform float _Parallax;
uniform float _MaxTexCoordOffset;
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float4 tex : TEXCOORD1;
float3 tangentWorld : TEXCOORD2;
float3 normalWorld : TEXCOORD3;
float3 binormalWorld : TEXCOORD4;
float3 viewDirWorld : TEXCOORD5;
float3 viewDirInScaledSurfaceCoords : TEXCOORD6;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.tangentWorld = normalize(
mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
output.normalWorld = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.binormalWorld = normalize(
cross(output.normalWorld, output.tangentWorld)
* input.tangent.w); // tangent.w is specific to Unity
float3 binormal = cross(input.normal, input.tangent.xyz)
* input.tangent.w;
// appropriately scaled tangent and binormal
// to map distances from object space to texture space
float3 viewDirInObjectCoords = mul(
modelMatrixInverse, float4(_WorldSpaceCameraPos, 1.0)).xyz
- input.vertex.xyz;
float3x3 localSurface2ScaledObjectT =
float3x3(input.tangent.xyz, binormal, input.normal);
// vectors are orthogonal
output.viewDirInScaledSurfaceCoords =
mul(localSurface2ScaledObjectT, viewDirInObjectCoords);
// we multiply with the transpose to multiply with
// the "inverse" (apart from the scaling)
output.posWorld = mul(modelMatrix, input.vertex);
output.viewDirWorld = normalize(
_WorldSpaceCameraPos - output.posWorld.xyz);
output.tex = input.texcoord;
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
// fragment shader with ambient lighting
float4 fragWithAmbient(vertexOutput input) : COLOR
{
// 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 + tex2D(_ParallaxMap, _ParallaxMap_ST.xy
* input.tex.xy + _ParallaxMap_ST.zw).x);
float2 texCoordOffsets =
clamp(height * input.viewDirInScaledSurfaceCoords.xy
/ input.viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
// normal mapping: lookup and decode normal from bump map
// in principle we have to normalize tangentWorld,
// binormalWorld, and normalWorld again; however, the
// potential problems are small since we use this
// matrix only to compute "normalDirection",
// which we normalize anyways
float4 encodedNormal = tex2D(_BumpMap,
_BumpMap_ST.xy * (input.tex.xy + texCoordOffsets)
+ _BumpMap_ST.zw);
float3 localCoords = float3(2.0 * encodedNormal.a - 1.0,
2.0 * encodedNormal.g - 1.0, 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
float3x3 local2WorldTranspose = float3x3(
input.tangentWorld,
input.binormalWorld,
input.normalWorld);
float3 normalDirection =
normalize(mul(localCoords, local2WorldTranspose));
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dot(normalDirection, lightDirection));
float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
input.viewDirWorld)), _Shininess);
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
// fragement shader for pass 2 without ambient lighting
float4 fragWithoutAmbient(vertexOutput input) : COLOR
{
// 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 + tex2D(_ParallaxMap, _ParallaxMap_ST.xy
* input.tex.xy + _ParallaxMap_ST.zw).x);
float2 texCoordOffsets =
clamp(height * input.viewDirInScaledSurfaceCoords.xy
/ input.viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
// normal mapping: lookup and decode normal from bump map
// in principle we have to normalize tangentWorld,
// binormalWorld, and normalWorld again; however, the
// potential problems are small since we use this
// matrix only to compute "normalDirection",
// which we normalize anyways
float4 encodedNormal = tex2D(_BumpMap,
_BumpMap_ST.xy * (input.tex.xy + texCoordOffsets)
+ _BumpMap_ST.zw);
float3 localCoords = float3(2.0 * encodedNormal.a - 1.0,
2.0 * encodedNormal.g - 1.0, 0.0);
localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
// approximation without sqrt: localCoords.z =
// 1.0 - 0.5 * dot(localCoords, localCoords);
float3x3 local2WorldTranspose = float3x3(
input.tangentWorld,
input.binormalWorld,
input.normalWorld);
float3 normalDirection =
normalize(mul(localCoords, local2WorldTranspose));
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dot(normalDirection, lightDirection));
float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation * _LightColor0.rgb
* _SpecColor.rgb * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
input.viewDirWorld)), _Shininess);
}
return float4(diffuseReflection + specularReflection,
1.0);
}
ENDCG
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
CGPROGRAM
#pragma vertex vert
#pragma fragment fragWithAmbient
// the functions are defined in the CGINCLUDE part
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment fragWithoutAmbient
// the functions are defined in the CGINCLUDE part
ENDCG
}
}
}
总结
恭喜! 如果您真的了解这整个着色器,那么您已经走了很长一段路。 实际上,着色器包含很多概念(坐标系之间的转换、Phong 反射模型、法线贴图、视差贴图等)。 更具体地说,在此我们已经看到:
- 视差贴图如何改进法线贴图。
- 如何在数学上描述视差映射。
- 如何实现视差映射。