概述
视差贴图简单来说,就是和法线贴图功能类似的一种东西,但是他解决了某些情况下法线贴图可能会出现的一些视觉错误。
先回忆一下法线贴图的作用,一般游戏开发中用的模型是先在建模软件中建一个高面数的精细模型,但是由于性能问题,这个模型不能直接导出在游戏中使用,所以需要导出一个优化过的低面数的模型。但是低面数模型的光照效果肯定没有高面数模型那么精细。一般光照效果都是依赖法线来计算,所以有人就想出了低面数的模型加上高面数模型的法线贴图,然后光照计算用的法线就用高面数模型的法线。这样就能达到高面数模型的光照效果了。
因此现在一般都是低模+高模导出的法线贴图,同时兼顾了性能和视觉效果,不是贴的很近看的话基本都看不出和直接渲染高模的画面差别。
但是法线贴图有什么问题呢?如下图所示,e代表视线方向,这个凸出就代表一个模型的凸出的地方,我们现在的目标是用法线贴图来在一个平面上模拟这个凸出。
按常理来说红线此时是完全看不见的,最多只能看见这个蓝点。但是因为模型并不是真的有凹凸,而是一个平面,整条红线是看的见的,只是这个红线部分会由于法线贴图变得很暗。
主要问题就是法线贴图实际上没有对顶点的位置进行位移,只是依靠法线的朝向模拟了明暗效果,所以这种模型自身相互遮挡的问题就是无法解决的,这就是法线贴图的局限性。
有人可能会说那直接用一张贴图对顶点进行位移不就好了吗,就是实际把这个凸出生成出来。确实有这种作法叫做顶点置换贴图,通常还要和曲面细分着色器配合,但是这种作法实际上就是生成移动了更多的顶点,肯定没有视差贴图的性能好,视差贴图是不用移动顶点的。
我们想想另外一种解决办法,如下图,按理说视线e会看到a点,但是由于这个凸起实际上不存在,所以e最后看到的是b点,那么我们只要把采样b点时的纹理坐标偏移到a点就可以了。
这个偏移的距离比较难计算,如果要迭代的话是可以得到一个比较精确的近似值的,但是在shader里进行循环不太好,因此我们的做法就是用一个变量来控制这个偏移值。
而视差贴图实际上就是保存了顶点高度的一张贴图,我们利用这个高度来进行偏移,越高的地方偏移的越少,越矮的地方偏移的越多,这个从图上也可以看出来。
关键代码如下,在输入结构体中声明viewDir即切线空间中的视线方向,而切线空间中(0,0,1)代表没有偏移过的法线,如果视线方向也等于(0,0,1)的话就代表视线方向和法线方向重合,此时也就不用做什么处理,根本不会出现遮挡的问题。因此我们只要用viewDir.xy来计算就可以,先用uv采样视差贴图得到高度,乘上视线方向的xy值,然后乘上_ParallaxScale 这个控制变量,最后调整控制变量到某个值,就可以得到看上去正确的效果了。
float2 offset = (tex2D(_ParallaxMap, IN.uv_MainTex).r - 1) * IN.viewDir.xy * _ParallaxScale + IN.uv_MainTex;
当时发现只算一次效果不好,所以手动加了几次迭代,也可以改成用一个循环来迭代
float2 offset = (tex2D(_ParallaxMap, IN.uv_MainTex).r - 1) * IN.viewDir.xy * _ParallaxScale + IN.uv_MainTex;
float2 Offset1 = (tex2D(_ParallaxMap, offset).r - 1) * IN.viewDir.xy * _ParallaxScale + offset;
float2 Offset2 = (tex2D(_ParallaxMap, Offset1).r - 1) * IN.viewDir.xy * _ParallaxScale + Offset1;
float2 Offset3 = (tex2D(_ParallaxMap, Offset2).r - 1) * IN.viewDir.xy * _ParallaxScale + Offset2;
最后用这个偏移值来对主纹理图来进行采样就可以了,同时法线贴图也可以和视差贴图一起使用效果更好。
float2 uv = Offset3;
fixed4 c = tex2D(_MainTex, uv) * _Color;
fixed3 normal = UnpackNormal(tex2D(_NormalMap, uv));
normal.xy *= _NormalScale;
normal = normalize(normal);
o.Normal = normal;
o.Albedo = c.rgb;
最后效果如下,比只用法线贴图效果要好很多,看起来这个平面似乎真的‘凸起’了一样。
当然,因为不是真的偏移顶点只是偏移了uv坐标,因此视线如果和这个平面接近平行,还是会出现和法线贴图一样的问题,不过远处看的话还好,所以视差贴图还是适合用在一些远处的景物上。
参数面板
完整代码
Shader "LX/parallaxMap"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
_NormalMap("Normal Map", 2D) = "bump" {}
_NormalScale("Normal Scale", Range( -8 , 8)) = 1
_ParallaxMap("Parallax Map", 2D) = "black" {}
_ParallaxScale("Parallax Scale", float) = 1
}
SubShader
{
Tags
{
"RenderType"="Opaque"
}
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
sampler2D _NormalMap;
float _NormalScale;
sampler2D _ParallaxMap;
float _ParallaxScale;
struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
float3 viewDir;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf(Input IN, inout SurfaceOutputStandard o)
{
_ParallaxScale /= 100.0f;
//viewDir是切线空间的
float2 offset = (tex2D(_ParallaxMap, IN.uv_MainTex).r - 1) * IN.viewDir.xy * _ParallaxScale + IN.uv_MainTex;
float2 Offset1 = (tex2D(_ParallaxMap, offset).r - 1) * IN.viewDir.xy * _ParallaxScale + offset;
float2 Offset2 = (tex2D(_ParallaxMap, Offset1).r - 1) * IN.viewDir.xy * _ParallaxScale + Offset1;
float2 Offset3 = (tex2D(_ParallaxMap, Offset2).r - 1) * IN.viewDir.xy * _ParallaxScale + Offset2;
float2 uv = Offset3;
fixed4 c = tex2D(_MainTex, uv) * _Color;
fixed3 normal = UnpackNormal(tex2D(_NormalMap, uv));
normal.xy *= _NormalScale;
normal = normalize(normal);
o.Normal = normal;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
另外代码也传到github仓库里了,大家也可以关注一下哦~
我的github