源自Alpha-Parallax.shader
关于这个shader最重要的部分就是视差偏移ParallaxOffset这一块了
void surf (Input IN, inout SurfaceOutput o) {
half h = tex2D (_ParallaxMap, IN.uv_BumpMap).w;
float2 offset = ParallaxOffset (h, _Parallax, IN.viewDir);
IN.uv_MainTex += offset;
IN.uv_BumpMap += offset;
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
// Calculates UV offset for parallax bump mapping
inline float2 ParallaxOffset( half h, half height, half3 viewDir )
{
h = h * height - height/2.0;
float3 v = normalize(viewDir);
v.z += 0.42;
return h * (v.xy / v.z);
}
这里我们先抛开surface shader,我们自己做一个基础的视差映射的VertexFragment shader。
Shader "ShaderStore/UnitShader2017/CPOthers/Parallax_Study"
{
Properties
{
_Parallax("Height",Range(0.005,0.08))=0.02
_MainTex ("Texture", 2D) = "white" {}
_BumpMap("NormalMap",2D) = "bump"{}
_ParallaxMap("Heightmap(A)",2D) = "black"{}
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent: TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
sampler2D _ParallaxMap;
fixed _Parallax;
float4 _LightColor0;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float3 binormal = cross(v.normal,v.tangent.xyz)*v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);
o.lightDir = normalize(mul(rotation,ObjSpaceLightDir(v.vertex)));
o.viewDir = normalize(mul(rotation,ObjSpaceViewDir(v.vertex)));
return o;
}
//Simple Parallax
float2 offsetUV_Simple(fixed h,fixed height,fixed3 viewDir)
{
return viewDir.xy * h / viewDir.z * height;
}
fixed4 frag (v2f i) : SV_Target
{
float h = tex2D(_ParallaxMap,i.uv).a;
float2 offset = offsetUV_Simple(h,_Parallax,i.viewDir);
i.uv += offset;
float3 normal = UnpackNormal(tex2D(_BumpMap,i.uv));
float diff = max(0,dot(normal,i.lightDir));
fixed3 maincol = tex2D(_MainTex, i.uv).rgb;
fixed3 col = maincol * diff*_LightColor0.rgb;
return float4(col,1);
}
ENDCG
}
}
}
接下来我们继续给这个shader添加视差映射部分。(自己的理解,可能有错)
正常情况下,我们观测B点,就在纹理坐标Ta点处,但是,我们想让他根据height map去左右一下,来增强一下bump的视觉效果。所以我们想在Ta出看到A的高度,在没有heightmap做distort前,我们看A直接就看到了Tb’处。
这里的viewDir是tangent space下的,也就是说viewDir.xy的轴向是uv.xy,所以offset其实就是我们将要 求的Voffset在viewDir.xy方向的偏移,Voffset = offset * normalize(viewDir.xy)
像三角形相似还有平行四边形的基本定理什么的就不说了,我们可以得到
offset/len(AB) = len(viewDir.xy)/viewDir.z
而len(AB)是我们heightmap sample出来的值,也就是h,即h = len(AB)
offset = len(viewDir.xy) * h / viewDir.z
这样
Voffset = normalize(ViewDir.xy)*len(viewDir.xy)*h/viewDir.z
整理一下
Voffset = viewDir.xy * h * / viewDir.z
这样我们就找到了偏移后的纹理坐标的
Tb = Ta + Voffset;
我们去代码里code it
fixed4 frag (v2f i) : SV_Target
{
float h = tex2D(_ParallaxMap,i.uv).a;
float2 offset = i.viewDir.xy * h / i.viewDir.z;
i.uv += offset*_Parallax;
float3 normal = UnpackNormal(tex2D(_BumpMap,i.uv));
float diff = max(0,dot(normal,i.lightDir));
fixed3 maincol = tex2D(_MainTex, i.uv).rgb;
fixed3 col = maincol * diff*_LightColor0.rgb;
return float4(col,1);
}
上图就是在与面片比较平行的视角去观察的截图,明显不如源码里的效果。
所以可以折头看一下源码里方法和黑魔法。
//Simple Parallax
float2 offsetUV_Simple(fixed h,fixed height,fixed3 viewDir)
{
return viewDir.xy * h / viewDir.z * height;
}
//Builtin Parallax
float2 offsetUV_Builtin(fixed h,fixed height,fixed3 viewDir)
{
h = h*height - height/2.0;
viewDir.z += 0.42;
return viewDir.xy * h /viewDir.z;
}
可以看到Builtin的方法里对采样heightmap得到的h,与可调节参数height进行了一种remap的操作,因为我们的可调节的参数height(properties里的_Parallax)的范围很小(【0.005,0.08】),这个操作相当于将h(范围是【0,1】)的值域的跨度变小了,然后又将viewDir(单位化的)的Z分量往正值方向偏移了0.42(Magic Num?)
然后,我开始google之旅。。。找到了这篇。。。。研究一下 ,这个和上面那个一样的?,这个,这个,这个,这个,这个,还有这个
把研究的过程,记录一下吧。后来又找到了一个翻译版的
视差映射的根本:利用高度图偏移了采样颜色和法线贴图的纹理坐标。
视差映射的目的:精确计算viewDir与高度图定义出来的表面的交点。
视差映射的效果:增强normal map带来的bump的效果。(所以视差映射的计算与normal的计算是一样的,都在切线空间下)
Parallax mapping 与 Parallax Mapping with Offset Limiting
因为视差映射的计算是在切线空间下的,所以,很自然的我们就把viewDir转到了切线空间下了,而切线空间中,xy是顺着uv坐标的方向,也就是说,viewDir.xy其实就可相当于uv的偏移量。如果viewDir.xy/viewDir.z作为uv的偏移量的话,这就是传统的Parallax mapping,如果不除以z分量就是Parallax Mapping with Offset Limiting
//Simple Parallax
float2 offsetUV_Simple(fixed h,fixed height,fixed3 viewDir)
{
return viewDir.xy * h * height / viewDir.z;
}
//Parallax Mapping with Offset Limiting
float2 offsetUV_Limiting(fixed h,fixed height,fixed3 viewDir)
{
return viewDir.xy * h * height;
}
这两种方法的优点就是,只做了一次对heightmap的采样(h的获取就是对Heightmap的采样),所以性能是不错的。
缺点也挺明显的,viewDir.xy * h * height或者(viewDir.xy * h * height / viewDir.z)这个得到的uv的偏移量并不是准确的值。也就是说并不是viewDir与heightmap定义表面的准确交点。
Steep Parallax Mapping
float2 offsetUV_Steep(sampler2D Hmap, float2 uv,fixed height,fixed3 viewDir,out float parallaxHeight)
{
const float minLayers = 5;
const float maxLayers = 15;
float numLayers = lerp(maxLayers,minLayers,abs(dot(float3(0,0,1),viewDir)));
//height per layer
float layerHeight = 1.0/numLayers;
float currentHeight = 0;
float2 dtex = height * viewDir.xy/viewDir.z * layerHeight;
float2 currentUV = uv;
float h = tex2D(Hmap,uv).a;
[unroll(16)]
while(h > currentHeight)
{
currentHeight += layerHeight;
currentUV -= dtex;
h = tex2D(Hmap,currentUV).a;
}
parallaxHeight = currentHeight;
return currentUV;
}
Relief Parallax Mapping 和 Parallax Occlusion Mapping (POM)以及Parallax Mapping and self-shadowing
就不写了,手游上一时还用不到这个,一般情况下都是trick了
Shader "ShaderStore/UnitShader2017/CPOthers/Parallax_Study"
{
Properties
{
_Parallax("Height",Range(0.005,0.08))=0.02
_MainTex ("Texture", 2D) = "white" {}
_BumpMap("NormalMap",2D) = "bump"{}
_ParallaxMap("Heightmap(A)",2D) = "black"{}
[KeywordEnum(Builtin,Normal, Limiting, Steep,Relief,Occlusion)]_Type("Parallax Type",float) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
LOD 100
Pass
{
CGPROGRAM
#pragma shader_feature _TYPE_BUILTIN _TYPE_NORMAL _TYPE_LIMITING _TYPE_STEEP _TYPE_RELIEF _TYPE_OCCLUSION
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent: TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
sampler2D _ParallaxMap;
fixed _Parallax;
float4 _LightColor0;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float3 binormal = cross(v.normal,v.tangent.xyz)*v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);
o.lightDir = normalize(mul(rotation,ObjSpaceLightDir(v.vertex)));
o.viewDir = normalize(mul(rotation,ObjSpaceViewDir(v.vertex)));
return o;
}
//Simple Parallax
float2 offsetUV_Simple(sampler2D Hmap, float2 uv,fixed height,fixed3 viewDir)
{
float h = tex2D(Hmap,uv).a;
return uv + viewDir.xy * h * height / viewDir.z;
}
//Parallax Mapping with Offset Limiting
float2 offsetUV_Limiting(sampler2D Hmap, float2 uv,fixed height,fixed3 viewDir)
{
float h = tex2D(Hmap,uv).a;
return uv + viewDir.xy * h * height;
}
//Steep
float2 offsetUV_Steep(sampler2D Hmap, float2 uv,fixed height,fixed3 viewDir,out float parallaxHeight)
{
const float minLayers = 5;
const float maxLayers = 15;
float numLayers = lerp(maxLayers,minLayers,abs(dot(float3(0,0,1),viewDir)));
//height per layer
float layerHeight = 1.0/numLayers;
float currentHeight = 0;
float2 dtex = height * viewDir.xy/viewDir.z * layerHeight;
float2 currentUV = uv;
float h = tex2D(Hmap,uv).a;
[unroll(16)]
while(h > currentHeight)
{
currentHeight += layerHeight;
currentUV -= dtex;
h = tex2D(Hmap,currentUV).a;
}
parallaxHeight = currentHeight;
return currentUV;
}
//Relief
float2 offsetUV_Relief(sampler2D Hmap,in float2 T,fixed height, in float3 V, out float parallaxHeight)
{
// determine required number of layers
const float minLayers = 10;
const float maxLayers = 15;
float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0, 0, 1), V)));
// height of each layer
float layerHeight = 1.0 / numLayers;
// depth of current layer
float currentLayerHeight = 0;
// shift of texture coordinates for each iteration
float2 dtex = height * V.xy / V.z / numLayers;
// current texture coordinates
float2 currentTextureCoords = T;
// depth from heightmap
float heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
// while point is above surface
[unroll(16)]
while(heightFromTexture > currentLayerHeight)
{
// go to the next layer
currentLayerHeight += layerHeight;
// shift texture coordinates along V
currentTextureCoords -= dtex;
// new depth from heightmap
heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
}
///
// Start of Relief Parallax Mapping
// decrease shift and height of layer by half
float2 deltaTexCoord = dtex / 2;
float deltaHeight = layerHeight / 2;
// return to the mid point of previous layer
currentTextureCoords += deltaTexCoord;
currentLayerHeight -= deltaHeight;
// binary search to increase precision of Steep Paralax Mapping
const int numSearches = 5;
for(int i=0; i<numSearches; i++)
{
// decrease shift and height of layer by half
deltaTexCoord /= 2;
deltaHeight /= 2;
// new depth from heightmap
heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
// shift along or agains vector V
if(heightFromTexture > currentLayerHeight) // below the surface
{
currentTextureCoords -= deltaTexCoord;
currentLayerHeight += deltaHeight;
}
else // above the surface
{
currentTextureCoords += deltaTexCoord;
currentLayerHeight -= deltaHeight;
}
}
// return results
parallaxHeight = currentLayerHeight;
return currentTextureCoords;
}
//Occlusion
float2 offsetUV_Occlusion(sampler2D Hmap,in float2 T, fixed height,in float3 V, out float parallaxHeight)
{
// determine optimal number of layers
const float minLayers = 10;
const float maxLayers = 15;
float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0, 0, 1), V)));
// height of each layer
float layerHeight = 1.0 / numLayers;
// current depth of the layer
float curLayerHeight = 0;
// shift of texture coordinates for each layer
float2 dtex = height * V.xy / V.z / numLayers;
// current texture coordinates
float2 currentTextureCoords = T;
// depth from heightmap
float heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
// while point is above the surface
[unroll(16)]
while(heightFromTexture > curLayerHeight)
{
// to the next layer
curLayerHeight += layerHeight;
// shift of texture coordinates
currentTextureCoords -= dtex;
// new depth from heightmap
heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
}
///
// previous texture coordinates
float2 prevTCoords = currentTextureCoords + dtex;
// heights for linear interpolation
float nextH = heightFromTexture - curLayerHeight;
float prevH = tex2D(Hmap, prevTCoords).r
- curLayerHeight + layerHeight;
// proportions for linear interpolation
float weight = nextH / (nextH - prevH);
// interpolation of texture coordinates
float2 finalTexCoords = prevTCoords * weight + currentTextureCoords * (1.0-weight);
// interpolation of depth values
parallaxHeight = curLayerHeight + prevH * weight + nextH * (1.0 - weight);
// return result
return finalTexCoords;
}
//Builtin Parallax
float2 offsetUV_Builtin(sampler2D Hmap, float2 uv,fixed height,fixed3 viewDir)
{
float h = tex2D(Hmap,uv).a;
h = h*height - height/2.0;
viewDir.z += 0.42;
return uv + viewDir.xy * h / viewDir.z;
}
float parallaxSoftShadowMultiplier(sampler2D Hmap,in float2 initialTexCoord, fixed height,in fixed3 L,in float initialHeight)
{
float shadowMultiplier = 1;
const float minLayers = 15;
const float maxLayers = 30;
// calculate lighting only for surface oriented to the light source
if(dot(fixed3(0, 0, 1), L) > 0)
{
// calculate initial parameters
float numSamplesUnderSurface = 0;
shadowMultiplier = 0;
float numLayers = lerp(maxLayers, minLayers, abs(dot(fixed3(0, 0, 1), L)));
float layerHeight = initialHeight / numLayers;
float2 texStep = height * L.xy / L.z / numLayers;
// current parameters
float currentLayerHeight = initialHeight - layerHeight;
float2 currentTextureCoords = initialTexCoord + texStep;
float heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
int stepIndex = 1;
// while point is below depth 0.0 )
[unroll(16)]
while(currentLayerHeight > 0)
{
// if point is under the surface
if(heightFromTexture < currentLayerHeight)
{
// calculate partial shadowing factor
numSamplesUnderSurface += 1;
float newShadowMultiplier = (currentLayerHeight - heightFromTexture) *
(1.0 - stepIndex / numLayers);
shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
}
// offset to the next layer
stepIndex += 1;
currentLayerHeight -= layerHeight;
currentTextureCoords += texStep;
heightFromTexture = tex2D(Hmap, currentTextureCoords).r;
}
// Shadowing factor should be 1 if there were no points under the surface
if(numSamplesUnderSurface < 1)
{
shadowMultiplier = 1;
}
else
{
shadowMultiplier = 1.0 - shadowMultiplier;
}
}
return shadowMultiplier;
}
fixed4 frag (v2f i) : SV_Target
{
float pH = 1;
float shadowMultiplier = 1;
#if _TYPE_NORMAL
i.uv = offsetUV_Simple(_ParallaxMap,i.uv,_Parallax,i.viewDir);
#elif _TYPE_LIMITING
i.uv = offsetUV_Limiting(_ParallaxMap,i.uv,_Parallax,i.viewDir);
#elif _TYPE_STEEP
i.uv = offsetUV_Steep(_ParallaxMap,i.uv,_Parallax,i.viewDir,pH);
shadowMultiplier = parallaxSoftShadowMultiplier(_ParallaxMap,i.uv,_Parallax,i.lightDir,pH - 0.05);
#elif _TYPE_RELIEF
i.uv = offsetUV_Relief(_ParallaxMap,i.uv,_Parallax,i.viewDir,pH);
shadowMultiplier = parallaxSoftShadowMultiplier(_ParallaxMap,i.uv,_Parallax,i.lightDir,pH - 0.05);
#elif _TYPE_BUILTIN
i.uv = offsetUV_Builtin(_ParallaxMap,i.uv,_Parallax,i.viewDir);
#elif _TYPE_OCCLUSION
i.uv = offsetUV_Occlusion(_ParallaxMap,i.uv,_Parallax,i.viewDir,pH);
shadowMultiplier = parallaxSoftShadowMultiplier(_ParallaxMap,i.uv,_Parallax,i.lightDir,pH - 0.05);
#endif
float3 normal = UnpackNormal(tex2D(_BumpMap,i.uv));
float diff = max(0,dot(normal,i.lightDir));
fixed3 maincol = tex2D(_MainTex, i.uv).rgb;
fixed3 col = maincol * diff*_LightColor0.rgb * pow(shadowMultiplier, 4);
return float4(col,1);
}
ENDCG
}
}
}