在unity实现视差遮挡偏移(ParallaxOcclusionMapping)效果

  • Post author:
  • Post category:其他



光照效果相关文章目录


大家好,我是阿赵,之前介绍过法线贴图在Unlit类型shader里面的实现,这次来介绍一个效果更猛一点的。

Unity引擎实现视差偏移效果



一、效果介绍

我准备了一个Unity自带的面片

在这里插入图片描述

还有3张贴图

在这里插入图片描述

最后把面片做出了下面这种凹凸并且可以根据光线变化光影的效果:

在这里插入图片描述

在这里插入图片描述

这种技术就是ParallaxOcclusionMapping(视差遮挡偏移)



二、完整Shader

Shader "azhao/ParallaxOcclusionMapping"
{
    Properties
    {
		_baseMap("baseMap", 2D) = "white" {}
		_heightMap("heightMap", 2D) = "white" {}
		_normalMap("normalMap",2D) = "white" {}

		_pomScale("pomScale", Float) = 0
		_planeHeight("planeHeight", Float) = 0
		_minSamplesNum("minSampleNum",Float) = 8
		_maxSamplesNum("maxSampleNum",Float) = 8
		_tilling("tilling", Float) = 1
		
		_colPowVal("colPowVal", Float) = 1
		_colMulVal("colMulVal", Float) = 1

		_normalScale("normalScale", Range(-1 , 1)) = 0
		_specColor("SpecColor",Color) = (1,1,1,1)
		_shininess("shininess", Range(1 , 100)) = 1
		_specIntensity("specIntensity",Range(0,1)) = 1
		_ambientIntensity("ambientIntensity",Range(0,1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        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
            {                
                float4 pos : SV_POSITION;
				float3 worldPos : TEXCOORD0;
				float3 worldNormal : TEXCOORD1;
				float3 worldTangent :TEXCOORD2;
				float3 worldBitangent : TEXCOORD3;

            };

			sampler2D _baseMap;
			float _tilling;
			sampler2D _heightMap;
			float _pomScale;
			float _planeHeight;
			float _minSamplesNum;
			float _maxSamplesNum;


			float4 _heightMap_ST;
			float _colMulVal;
			float _colPowVal;
			
			float _normalScale;
			float4 _specColor;
			float _shininess;
			float _specIntensity;
			sampler2D _normalMap;
			float _ambientIntensity;
//计算视差遮挡偏移后的采样UV
			inline float2 POM(sampler2D heightMap, float2 uvs, float2 dx, float2 dy, float3 normalWorld, float3 viewWorld, float3 viewDirTan, int minSamples, int maxSamples, float parallax, float refPlane)
			{
				float3 result = 0;
				int stepIndex = 0;
				int numSteps = (int)lerp((float)maxSamples, (float)minSamples, saturate(dot(normalWorld, viewWorld)));
				float layerHeight = 1.0 / numSteps;
				float2 plane = parallax * (viewDirTan.xy / viewDirTan.z);
				uvs.xy += refPlane * plane;
				float2 deltaTex = -plane * layerHeight;
				float2 prevTexOffset = 0;
				float prevRayZ = 1.0f;
				float prevHeight = 0.0f;
				float2 currTexOffset = deltaTex;
				float currRayZ = 1.0f - layerHeight;
				float currHeight = 0.0f;
				float intersection = 0;
				float2 finalTexOffset = 0;
				while (stepIndex < numSteps + 1)
				{
					currHeight = tex2Dgrad(heightMap, uvs + currTexOffset, dx, dy).r;
					if (currHeight > currRayZ)
					{
						stepIndex = numSteps + 1;
					}
					else
					{
						stepIndex++;
						prevTexOffset = currTexOffset;
						prevRayZ = currRayZ;
						prevHeight = currHeight;
						currTexOffset += deltaTex;
						currRayZ -= layerHeight;
					}
				}
				int sectionSteps = 10;
				int sectionIndex = 0;
				float newZ = 0;
				float newHeight = 0;
				while (sectionIndex < sectionSteps)
				{
					intersection = (prevHeight - prevRayZ) / (prevHeight - currHeight + currRayZ - prevRayZ);
					finalTexOffset = prevTexOffset + intersection * deltaTex;
					newZ = prevRayZ - intersection * layerHeight;
					newHeight = tex2Dgrad(heightMap, uvs + finalTexOffset, dx, dy).r;
					if (newHeight > newZ)
					{
						currTexOffset = finalTexOffset;
						currHeight = newHeight;
						currRayZ = newZ;
						deltaTex = intersection * deltaTex;
						layerHeight = intersection * layerHeight;
					}
					else
					{
						prevTexOffset = finalTexOffset;
						prevHeight = newHeight;
						prevRayZ = newZ;
						deltaTex = (1 - intersection) * deltaTex;
						layerHeight = (1 - intersection) * layerHeight;
					}
					sectionIndex++;
				}
				return uvs.xy + finalTexOffset;
			}

			//简化版的转换法线并缩放的方法
			half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
			{
				half3 normal;
				//由于法线贴图代表的颜色是0到1,而法线向量的范围是-1到1
				//所以通过*2-1,把色值范围转换到-1到1
				normal = packednormal * 2 - 1;
				//对法线进行缩放
				normal.xy *= bumpScale;
				//向量标准化
				normal = normalize(normal);
				return normal;
			}



			//获取HalfLambert漫反射值
			float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
			{
				float3 lightDir = UnityWorldSpaceLightDir(worldPos);
				float NDotL = saturate(dot(worldNormal, lightDir));
				float halfVal = NDotL * 0.5 + 0.5;
				return halfVal;
			}

			//获取BlinnPhong高光
			float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
			{
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
				float specDir = max(dot(normalize(worldNormal), halfDir), 0);
				float specVal = pow(specDir, _shininess);
				return specVal;
			}

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldTangent = UnityObjectToWorldDir(v.tangent);
				float vertexTangentSign = v.tangent.w * unity_WorldTransformParams.w;
				o.worldBitangent = cross(o.worldNormal, o.worldTangent)*vertexTangentSign;
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {

				//构建TBN矩阵
				float3 tanToWorld0 = float3(i.worldTangent.x, i.worldBitangent.x, i.worldNormal.x);
				float3 tanToWorld1 = float3(i.worldTangent.y, i.worldBitangent.y, i.worldNormal.y);
				float3 tanToWorld2 = float3(i.worldTangent.z, i.worldBitangent.z, i.worldNormal.z);

				float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				float3 viewDir = tanToWorld0 * worldViewDir.x + tanToWorld1 * worldViewDir.y + tanToWorld2 * worldViewDir.z;
				viewDir = normalize(viewDir);
				float2 worldUV = POM(_heightMap, ((i.worldPos).xz * _tilling), ddx(((i.worldPos).xz * _tilling)), ddy(((i.worldPos).xz * _tilling)), i.worldNormal, worldViewDir, viewDir, _minSamplesNum, _maxSamplesNum, (_pomScale * 0.01), _planeHeight);


				//采样法线贴图的颜色
				half4 normalCol = tex2D(_normalMap, worldUV);
				//得到切线空间的法线方向
				half3 normalVal = UnpackScaleNormal(normalCol, _normalScale).rgb;
				//通过切线空间的法线方向和TBN矩阵,得出法线贴图代表的物体世界空间的法线方向
				float3 worldNormal = float3(dot(tanToWorld0, normalVal), dot(tanToWorld1, normalVal), dot(tanToWorld2, normalVal));

				//用法线贴图的世界空间法线,算漫反射
				half diffuseVal = GetHalfLambertDiffuse(i.worldPos, worldNormal);

				//用法线贴图的世界空间法线,算高光角度
				half3 specCol = _specColor * GetBlinnPhongSpec(i.worldPos, worldNormal)*_specIntensity;

				half3 baseRGB = tex2D(_baseMap, worldUV).rgb;
				baseRGB = pow(baseRGB* _colMulVal, _colPowVal);
				//最终颜色 = 环境色+漫反射颜色+高光颜色
				half3 finalCol = UNITY_LIGHTMODEL_AMBIENT * _ambientIntensity + baseRGB *diffuseVal + specCol;
                
                return half4(finalCol,1);
            }
            ENDCG
        }
    }
}



三、说明



1、最基础版本的视差偏移:

		half4 frag(v2f i) : SV_Target
		{
			//构建TBN矩阵
			float3 tanToWorld0 = float3(i.worldTangent.x, i.worldBitangent.x, i.worldNormal.x);
			float3 tanToWorld1 = float3(i.worldTangent.y, i.worldBitangent.y, i.worldNormal.y);
			float3 tanToWorld2 = float3(i.worldTangent.z, i.worldBitangent.z, i.worldNormal.z);

			float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
			float3 viewDir = tanToWorld0 * worldViewDir.x + tanToWorld1 * worldViewDir.y + tanToWorld2 * worldViewDir.z;
			float2 worldUV = (i.worldPos).xz * _tilling;
			float height = tex2D(_heightMap, worldUV).r;
			height = height*2-1;
			height *= _pomScale * 0.01;
			worldUV += viewDir.xy * height;
			half3 finalCol = tex2D(_baseMap, worldUV);
			return half4(finalCol,1);
		}

先看看这段简化版的shader着色器程序

首先说明的是,这里没用模型本身的UV坐标来采样高度图和颜色图,而是用了世界坐标的xz轴来模拟平铺在地面的UV,这样做的目的是当缩放地面的时候,上面的砖块大小不会发生变化。

然后要看的是,采样了一张高度图,然后取出了r通道的颜色,因为高度信息只需要一个通道就够了,所以实际的应用中,gba通道可以再放其他的遮罩图。

最后是重点,得到的height值是[0,1]范围的,所以我转换成[-1,1]范围,然后把原来算出来的世界UV,加上viewDir.xy*height,这个viewDir是切线空间下的观察方向。这里的目的是当某个地方的高度不是0的时候,其实那个点显示的颜色就不是正常UV采样的位置,而是应该根本高度偏移,同时偏移UV采样的坐标,显示另外一个位置的颜色。

视差偏移在没有多step采样时的表现

在这里插入图片描述

单纯是这样简单的操作一下UV,可以看到模型已经有一些变化效果了,不过这个时候由于只是采样了一次,而且只是单纯的线性偏移UV,效果还达不到遮挡的效果。所以想得到一个凹凸边缘比较的柔和的效果,必须是通过多次采样,每次偏移一点,采样多次之后再叠加起来的。



2、POM方法的简单介绍

在完整Shader里面,是通过一个POM函数来计算出实际视差遮挡偏移后的UV坐标的。

这个方法,其实我是从ASE的现成节点里面生成出来,并做了一些小修改的

在这里插入图片描述

这个方法里面的实现细节还是挺多的,比如他加入了一个基础平面的高度,可以调整偏移后的整个模型的高度。比如他可以传入采样的最大最小次数,来控制具体采样多少次,等。



3、完整光照模型的组合

这里又回到了老话题了,从完整代码里面,应该可以看到了很多熟悉的旧代码,比如简化版的转换法线并缩放的方法、获取HalfLambert漫反射值、获取BlinnPhong高光,这些方法都是之前多次讲过的了。

这里又把它们拿出来,无非就是,完整光照模型效果是通过环境光+漫反射+高光,这个是最基本的构成,所以到了实现效果的最后一步,基本上都会拿出来用,至于是不是用法线贴图来代替模型法线、用哪个漫反射模型或者高光模型,甚至是反射效果,这些都是可以自己选择的。这就像拼积木一样,根据自己的需要来增减或者替换组成的部件就好了。

由于我这里是真实的计算了光照模型,有法线有高光的,所以当光线变化的时候,模型上面的光影效果是会跟着变化的。



版权声明:本文为liweizhao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。