Cg 编程/Unity/凹凸表面的投影
本教程涵盖(单步)视差贴图。
它扩展并基于“凹凸表面的光照”部分。请注意,本教程旨在教您此技术的工作原理。如果您想在 Unity 中实际使用视差贴图,您应该使用支持它的内置着色器。
在“凹凸表面的光照”部分中介绍的法线贴图技术只改变平面的光照,以创建凹凸和凹陷的错觉。如果一个人直视表面(即在表面法线向量方向),这将非常有效。但是,如果一个人从其他角度(如左侧图像所示)看向表面,凹凸也应该从表面突出,而凹陷则应该凹进表面。当然,这可以通过几何建模凹凸和凹陷来实现;但是,这将需要处理更多顶点。另一方面,单步视差贴图是一种非常有效的技术,类似于法线贴图,它不需要额外的三角形,但仍然可以将虚拟凹凸移动几个像素,使其从平面上突出。但是,该技术仅限于高度较小的凹凸和凹陷,并且需要一些微调才能获得最佳效果。
视差贴图由 Tomomichi Kaneko 等人在 2001 年的论文“使用视差贴图的详细形状表示”(ICAT 2001)中提出。基本思想是偏移用于表面纹理映射(尤其是法线贴图)的纹理坐标。如果以适当的方式计算纹理坐标的这个偏移,就可以移动纹理的一部分(例如凹凸),就好像它们从表面突出一样。
左侧的插图显示了指向观察者的视向量 V 和在片段着色器中光栅化的表面的点处的表面法线向量 N。视差贴图分 3 步进行
- 在光栅化点处的 高度在高度图中的查找,该高度图由插图底部直线上方的波浪线表示。
- 计算方向为 V 的视射线与平行于渲染表面的 高度的表面的交点。距离 是在方向为 N 的方向上移动 的光栅化表面点和这个交点之间的距离。如果将这两个点投影到渲染的表面上, 也是光栅化点和表面上的一个新点(在插图中用十字标记)之间的距离。如果表面被高度图位移,这个新的表面点更接近实际在方向为 V 的视射线中可见的点。
- 将偏移 转换为纹理坐标空间,以便为所有后续纹理查找计算纹理坐标的偏移。
为了计算,我们需要在光栅化点处的高度图中的 高度,这在示例中通过纹理属性 _ParallaxMap
的 A 分量的纹理查找来实现,它应该是一个灰度图像,表示“凹凸表面的光照”部分中讨论的高度。我们还需要局部表面坐标系中的视方向 V,该坐标系由法线向量 ( 轴)、切线向量 ( 轴)和副法线向量 ( 轴)组成,它们也在“凹凸表面的光照”部分中介绍。为此,我们使用以下公式计算从局部表面坐标到物体空间的变换:
其中 **T**、**B** 和 **N** 以物体坐标给出。(在 “Lighting of Bumpy Surfaces” 部分,我们有一个类似的矩阵,但向量在世界坐标中。)
我们计算物体空间中的视图方向 **V**(作为栅格化位置与从世界空间变换到物体空间的摄像机位置之差),然后使用矩阵 将其变换到局部表面空间,该矩阵可以计算为
这是可能的,因为 **T**、**B** 和 **N** 是正交且归一化的。(实际上,情况稍微复杂一些,因为我们不会对这些向量进行归一化,而是使用它们的长度进行另一种变换;见下文。)因此,为了将 **V** 从物体空间变换到局部表面空间,我们必须用转置矩阵 相乘。这实际上很好,因为在 Cg 中,更容易构建转置矩阵,因为 **T**、**B** 和 **N** 是转置矩阵的行向量。
一旦我们在局部表面坐标系中获得了 **V**,其中 轴指向法向量 **N** 的方向,我们就可以使用相似三角形计算偏移量 (在 方向)和 (在 方向)(与插图比较)
以及 .
因此
以及 .
请注意,没有必要对 **V** 进行归一化,因为我们只使用其分量的比率,这些比率不受归一化的影响。
最后,我们需要将 和 转换到纹理空间。如果 Unity 不帮助我们,这将非常困难:切线属性 tangent
实际上是按比例缩放的,并且有一个第四个分量 tangent.w
用于缩放副法线向量,这样视图方向 V 的变换就可以按比例缩放 和 ,以便在纹理坐标空间中获得 和 ,无需进一步计算。
实现
[edit | edit source]实现与 “凹凸表面的光照” 部分的大部分代码相同。特别是,为了考虑从局部表面空间到纹理空间的偏移映射,使用了 tangent
属性的第四个分量对副法线向量进行相同比例缩放。
float3 binormal = cross(input.normal, input.tangent.xyz)
* input.tangent.w;
我们需要在局部表面坐标系中添加一个视图向量 V 的输出参数(轴比例缩放以考虑映射到纹理空间)。此参数名为 viewDirInScaledSurfaceCoords
。它是通过使用矩阵 (localSurface2ScaledObjectT
)变换对象坐标中的视图向量 (viewDirInObjectCoords
) 来计算的,如上所述。
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)
除了将世界坐标中的视图方向在顶点着色器中计算而不是在片段着色器中计算之外,其余顶点着色器与法线映射相同,请参阅 “凹凸表面的光照” 部分。这样做是为了保持片段着色器中的算术运算数量足够少,以适应某些 GPU。
在片段着色器中,我们首先查询高度图以获取光栅化点的海拔高度。此高度由纹理 _ParallaxMap
的 A 分量指定。将 0 到 1 之间的数值通过着色器属性 _Parallax
转换为 -_Parallax
/2 到 +_Parallax
的范围,以提供一些用户对效果强度的控制(并且与回退着色器兼容)。
float height = _Parallax
* (-0.5 + tex2D(_ParallaxMap, _ParallaxMap_ST.xy
* input.tex.xy + _ParallaxMap_ST.zw).x);
然后根据上述描述计算偏移量 和 。但是,我们还将每个偏移量限制在用户指定的 -_MaxTexCoordOffset
和 _MaxTexCoordOffset
区间内,以确保偏移量保持在合理的范围内。(如果高度图由更多或更少的高度恒定的平坦高原组成,这些高原之间存在平滑过渡,则 _MaxTexCoordOffset
应小于这些过渡区域的厚度;否则,采样点可能位于具有不同高度的不同高原中,这意味着对交点近似的精度将任意地差。)代码如下:
float2 texCoordOffsets =
clamp(height * input.viewDirInScaledSurfaceCoords.xy
/ input.viewDirInScaledSurfaceCoords.z,
-_MaxTexCoordOffset, +_MaxTexCoordOffset);
在以下代码中,我们需要将偏移量应用到所有纹理查找中的纹理坐标;即,我们需要将 float2(input.tex)
(或等效地 input.tex.xy
)替换为 (input.tex.xy + texCoordOffsets)
,例如:
float4 encodedNormal = tex2D(_BumpMap,
_BumpMap_ST.xy * (input.tex.xy + texCoordOffsets)
+ _BumpMap_ST.zw);
片段着色器代码的其余部分与 “凹凸表面的光照” 部分相同。
完整着色器代码
[edit | edit source]如上一节所述,此代码的大部分内容来自 “凹凸表面的光照” 部分。请注意,如果您想在使用 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
}
}
}
总结
[edit | edit source]恭喜!如果您真正理解整个着色器,那么您已经走了很长一段路。事实上,着色器包含许多概念(坐标系之间的转换、Phong 反射模型、法线贴图、视差贴图等)。更具体地说,我们已经看到了以下内容:
- 视差贴图如何改进法线贴图。
- 视差贴图的数学描述。
- 视差贴图的实现方式。
进一步阅读
[edit | edit source]如果您还想了解更多
- 关于着色器代码的详细信息,请阅读 “凹凸表面的光照” 部分。
- 关于视差贴图,您可以阅读 Kaneko Tomomichi 等人发表的原始论文:“带有视差贴图的详细形状表示”,ICAT 2001,第 205-208 页,该论文可在 网上 获取。