# 前言
我很想这样说:
“今天花了相当长的时间,就为了研究视差贴图 (Parallax),可怎么试效果怎么不对!本来视差贴图也不过是在法线贴图的基础上发展出来的算法,法线贴图完全没问题,加上对视差贴图相关计算,这么久不行了呢?最终查了又查,发现居然是自己代码中关于视线的一个计算问题造成的!白白又浪费多的时间.... 这使我决定就算暂时不继续看书了,挤出点时间、再熬点夜,也得把这玩意好好进行分析下,写出来。”
实际上,那是发生在星期三的事儿了,最终还是拖到了周末。
# 法线贴图
谈到视差贴图,那么就不得不说一下法线贴图了。视差贴图实际上属于法线贴图得一个优化版本,它在法线贴图的基础上,计算了一个视觉上的偏移,因此,法线贴图是其基础。
对于法线贴图,之前我似乎也没讨论过,所以就放这儿一并简单说一下吧。
# 切空间
法线贴图的计算,一般都需要用到 “切空间”。切空间即每个点以自身为准的一个坐标系统,之所以有这个概念,是因为只有以点自身为准的坐标系,才能做到 “复用”。包括以物体坐标系为准之类的,都只能说适用于特定物体,详细情况可以参照《CG - 可编程实时图形权威指南》第八章关于凹凸映射的解释。我就稍微画个图吧:
简单来说,切空间就是以垂直点自身得法线、垂直法线的切线、垂直法线切线的 Binormal 组成的一个坐标系。法线贴图保存的东西,就是这儿的。
切空间至模型空间的矩阵由此构成:
$$ M_{切空间->模型空间}= \begin{bmatrix} T_x&B_x&N_x\\ T_y&B_y&N_y\\ T_z&B_z&N_z\\ \end{bmatrix} $$所以,这个矩阵的逆矩阵,就是模型空间到切空间的转化矩阵。同时由于 N、T、Bt 这三个量构造的矩阵属于正交矩阵,正交矩阵的逆矩阵就是它本身的转置矩阵。所以求出模型空间到切空间的矩阵就简单了:
所以:
$$ M_{模型空间->切空间}= \begin{bmatrix} T_x&T_x&T_x\\ B_y&B_y&B_y\\ N_z&N_z&N_z\\ \end{bmatrix} $$然后,我们使用这个矩阵就可以转化相应的向量了。无论最终是在切空间计算,或者是模型空间、世界空间,都取决于自己 —— 反正只要都在一个空间,其它都是任意的。
# 法线贴图格式
法线贴图的格式是经过压缩的,因为大家都知道,向量是 “具有大小和方向的量”,那么对于规范化向量来说,虽然大小不用说了,不过那方向必定也还是有的,即 [-1,1] 范围。然后,对于图片来说,范围又只在 [0,1] 之间,所就得现将其映射至 [-1,1] 范围。这就只需要简单地乘以 2 再减一就可以了。
另外,还有一点就是:在 Unity 中,法线贴图被压缩为 DTXnm 格式,其中有效数据是 alpha 及 green 通道,代表 XY 分量,Z 分量则由 XY 进行计算获得。所以,最终使用的时候,我们还需要再次进行解码,计算方式如下:
normal=normalTex.ag;
normal.z=sqrt(1-normal.xnormal.x-normal.ynormal.y);
# 实现
现在终于轮到实现了。
作为一个 Unity 程序,现在当然是打开 Unity,新建一个 Unlit Shader 了,删掉无用的代码。然后在 appdata 中添加两个变量 normal 及 tangent 的语义,以获取模型的法线及切线:
float3 normal : NORMAL;
float4 tangent : TANGENT;
接着,需要在传入片段程序的结构体 v2f 中,继续加入我们需要传入片段程序的变量,分别是视线、以及光源方向,这几个方向都属于切空间的,我们会在顶点程序中进行计算:
float3 V : TEXCOORD3;
float3 L : TEXCOORD4;
然后是顶点程序中的计算,首先缓存法线、切线,并计算出 Binormal (注意最后 Binormal 乘以的 w 系数,这似乎是 Unity 对切线提供的某种缩放):
float3 N = v.normal;
float3 T = v.tangent;
float3 Bt =normalize( cross(N,T)*v.tangent.w);
接着,构建相应的切空间矩阵,并将视线、光源方向皆转化为切空间坐标,如下:
//构建前往切空间的矩阵
float3x3 o2Surf = float3x3(T, Bt, N);
//链接构建世界空间至切空间矩阵
float3x3 w2Surf = mul(o2Surf, (float3x3)_World2Object);
float3 wPos = mul(_Object2World,v.vertex);
float3 V= _WorldSpaceCameraPos - wPos;
o.V = mul(w2Surf,V);
o.L = mul(w2Surf,_WorldSpaceLightPos0.xyz);
最后,就是片段程序中的计算了,除了多了一个额外法线的计算,其它与前文所述并无不认同。
首先将传入片段程序的各个插值后的向量进行归一化:
float3 V = normalize(i.V);
float3 L = normalize(i.L);
重点就是这儿,按照上边说的,将法线从贴图中解压出来,并替换掉:
//解压法线
float4 normalTex = tex2D(_BumpTex,i.uv);
//映射至[-1,1]
normalTex = normalTex * 2 - 1;
float3 localNormal;
localNormal = float3(normalTex.ag,0);
localNormal.z = sqrt(1 - dot(localNormal, localNormal));
上边的 localNormal 的 Z 分量计算跟上边谈到的是一个性质的,稍微思考一下就明白了哦。
现在,新的法线已经计算出来,用这个法线进行光照的计算:
fixed4 col = tex2D(_MainTex, i.uv);
float4 diffuse = max(0, dot(localNormal, L))*col;
float4 specialer = col*pow(max(0, dot(V, reflect(-L, localNormal))), 25);
col = col*UNITY_LIGHTMODEL_AMBIENT +diffuse+ specialer;
完。
效果如下:
# 视差贴图
# 实现
视差贴图 (ParallaxMaping) 是法线贴图的一个升级。
效果如下:
完。。。
怎么可能... 当然确实是在法线贴图上改动不是大就是了。
首先是在属性中添加了两个变量 “_ParallaxTex” 即视差计算中需要的高度图,以及 “_ParallaxHeight” 用于调整这个高度图取值的一个 float 变量,一般来说是在 0~0.08 之间,太多就会糊掉了:
_ParallaxTex("Parallax Texture", 2D) = "white"{}
_ParallaxHeight("Parallax Height",RANGE(0,0.08))=0.01
然后,顶点程序基本上没什么变化,只是在片段程序中加了一段代码,用于计算 UV 偏移:
//解压视差贴图
float height = _ParallaxHeight* tex2D(_ParallaxTex, i.uv).a;
float2 uvOffset =height*(V.xy / V.z);
float2 uv = i.uv + uvOffset;
然后使用新的 UV 对法线及贴图进行采样:
//解压法线
float4 normalTex = tex2D(_BumpTex,uv);
//映射至[-1,1]
normalTex = normalTex * 2 - 1;
float3 localNormal;
localNormal = float3(normalTex.ag,0);
localNormal.z = sqrt(1 - dot(localNormal, localNormal));
fixed4 col = tex2D(_MainTex, uv);
float4 diffuse = max(0, dot(localNormal, L))*col;
float4 specialer = col*pow(max(0, dot(V, reflect(-L, localNormal))), 25);
col = col*UNITY_LIGHTMODEL_AMBIENT +diffuse+ specialer;
# 理论
法线贴图虽然可以做到表现一种凹凸不平的质感,不过最好的效果只是针对于垂直的方向,一旦角度变化,这个 “凹凸” 质感就会随视线角度的加大,而变的越发虚假。
视差贴图的理论就是,在法线贴图的基础上,通过计算视线角度对贴图的 UV 也进行一个偏移,增强法线贴图效果同时使其更不宜穿帮... 当然,如果视线与点的夹角太大,依然是毫无作用的。
可以画个图稍微看看:
在此,我们需要计算的,就是最终偏移点的 UV,并使用这个新的 UV 进行采样。
根据三角形相似法则,计算这个偏移量的公式为:
所以就可由此计算偏移点:
更详细得情况可以参考维基百科,这儿我就不当搬运工了。
# Unity 提供函数
在 Unity 中,其实也提供了有关法线及视差计算的函数.
法线:
inline fixed3 UnpackNormal(fixed4 packednormal)
视差偏移:
inline float2 ParallaxOffset( half h, half height, half3 viewDir )
有兴趣的话,也可以扒出来看看,虽然有些不一样,不过实际计算是差不多的。特别是视差偏移计算,Unity 的函数多了点步骤:也就是在计算视线的分量之上添加了 0.42,使得比值变小,也就是说最终计算出来的偏移也会变小,相当于缩放,不过要注意的上边计算 h 中,Unity 也是不一样的。所以如果将我上边的视差计算换成 Unity 的 ParallaxOffset 函数,效果会有点轻微差别。
有时候如果觉得自己计算麻烦,这两个函数也都是可以直接拿来用的。
# 源码
//Author:CWHISME
//Date:2016.6.4
//Decription:
Shader "CWH/ParallaxMaping"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BumpTex("Bump Texture", 2D) = "white"{}
_ParallaxTex("Parallax Texture", 2D) = "white"{}
_ParallaxHeight("Parallax Height",RANGE(0,0.08))=0.01
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
sampler2D _BumpTex;
sampler2D _ParallaxTex;
float _ParallaxHeight;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//float3 N : TEXCOORD1;
//float3 T : TEXCOORD2;
//float3 Bt : TEXCOORD3;
float3 V : TEXCOORD1;
float3 L : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
float3 N = v.normal;
float3 T = v.tangent;
float3 Bt =normalize( cross(N,T)*v.tangent.w);
//构建前往切空间的矩阵
float3x3 o2Surf = float3x3(T, Bt, N);
//链接构建世界空间至切空间矩阵
float3x3 w2Surf = mul(o2Surf, (float3x3)_World2Object);
float3 wPos = mul(_Object2World,v.vertex);
float3 V= _WorldSpaceCameraPos - wPos;
o.V = mul(w2Surf,V);
o.L = mul(w2Surf,_WorldSpaceLightPos0.xyz);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//float3 N = normalize(i.N);
//float3 T = normalize(i.T);
//float3 Bt = normalize(i.Bt);
float3 V = normalize(i.V);
float3 L = normalize(i.L);
////构建前往切空间的矩阵
//float3x3 o2Surf = float3x3(T,Bt,N);
////链接构建世界空间至切空间矩阵
//float3x3 w2Surf = mul(o2Surf,(float3x3)_World2Object);
//float3 L = mul(w2Surf,_WorldSpaceLightPos0.xyz);
//解压视差贴图
float height = _ParallaxHeight* tex2D(_ParallaxTex, i.uv).a;
float2 uvOffset =height*(V.xy / V.z);
float2 uv = i.uv + uvOffset;
//解压法线
float4 normalTex = tex2D(_BumpTex,uv);
//映射至[-1,1]
normalTex = normalTex * 2 - 1;
float3 localNormal;
localNormal = float3(normalTex.ag,0);
localNormal.z = sqrt(1 - dot(localNormal, localNormal));
fixed4 col = tex2D(_MainTex, uv);
float4 diffuse = max(0, dot(localNormal, L))*col;
float4 specialer = col*pow(max(0, dot(V, reflect(-L, localNormal))), 25);
col = col*UNITY_LIGHTMODEL_AMBIENT +diffuse+ specialer;
return col;
}
ENDCG
}
}
}
# 结尾
本来打算一鼓作气就把这篇 Blog 写完的,结果理想是丰富的,现实,是骨感的。写法线贴图居然就花了一上午,而且回头看,居然内容也不是很多的样子...
感觉像是回到了小学 (或者初中?),当年考试的时候,试卷都从来没有做完的时候。
特别是语文,基本上阅读都还没做... 完?突然就发现考试都结束了!
然后被问到为什么成绩这么差?就只好说:这不是我自己不行,是因为时间太少了啊,试卷一大半都还没做,结果能不差嘛?
呃.....