Unity Decal 贴花效果测试

贴花效果,就和名字的直接意思类似,把一张图贴到另一个物体上显示,经常被用于表现一些重复出现的图案,比如弹孔,涂鸦,污渍等。效果图:
在这里插入图片描述

常规贴花实现

Unity官方提供了一个工程,这个工程主要是用来说明CommandBuffer是怎么使用的,其中有贴花的一些展示,主要是用CommandBuffer在Deferred渲染路径下实现贴花效果。使用CommandBuffer是因为需要把BuiltinRenderTextureType.GBuffer2中存储的法线信息传给Shader,而这次测试主要为了验证原理,不使用法线信息,所以可以不用CommandBuffer(即使使用法线信息也可以在Shader中通过_CameraDepthNormalsTexture结合DecodeDepthNormal方法来获取到法线信息)。原工程通过 cam.AddCommandBuffer (CameraEvent.BeforeLighting, buf); 这句实现把CommandBuffer插入到延迟渲染的光照计算Pass前面,也可以去掉不用。所以原工程的C#代码基本可以不使用,在Forward渲染路径下,完全在Shader中实现贴花效果。之前看文档说如果Shader中使用深度图的话需要在C#代码中设置相机的depthTextureMode,即 mainCam.depthTextureMode = DepthTextureMode.Depth;,但是我试了下不写这行代码在Shader中也可以正常使用深度图,有知道原因的同学可以告诉我下哈。

贴花效果的原理是建立一个立方体物体作为贴花物体(也有使用球体的),在贴花物体和被贴花的物体相交的XZ平面计算UV,显示贴花图案。具体的逻辑如下:

  1. 在顶点着色器中记录顶点在视空间的坐标(即相机到该点的方向向量,因为相机在视空间的原点)
  2. 在片元着色器中根据远裁切平面的距离(_ProjectionParams.z)和深度值重建片元在视空间的坐标
  3. 再根据unity_CameraToWorld和unity_WorldToObject矩阵计算出片元在模型空间的坐标
  4. 把模型空间坐标的xz分量映射到 [0,1] 区间,作为UV去读取贴花图案

直接上代码:

Shader "MJ/ForwardDecal"
{
	Properties
	{
		_MainTex ("Decal Texture", 2D) = "white" {}
	}

	SubShader
	{
		Tags{ "Queue"="Geometry+1" }

		Pass
		{
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float4 pos : SV_POSITION;
				float4 screenUV : TEXCOORD0;
				float3 ray : TEXCOORD1;
			};
			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos (v.vertex);
				o.screenUV = ComputeScreenPos (o.pos);
				o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1,-1,1);
				return o;
			}

			sampler2D _MainTex;
			sampler2D _CameraDepthTexture;
			float4 frag(v2f i) : SV_Target
			{
				i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
				float2 uv = i.screenUV.xy / i.screenUV.w;

				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);

				// 要转换成线性的深度值 //
				depth = Linear01Depth (depth);
				
				float4 vpos = float4(i.ray * depth,1);
				float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
				float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;
				clip (float3(0.5,0.5,0.5) - abs(opos.xyz));

				// 转换到 [0,1] 区间 //
				float2 texUV = opos.xz + 0.5;

				float4 col = tex2D (_MainTex, texUV);
				return col;
			}
			ENDCG
		}
	}

	Fallback Off
}

代码中需要注意的一些地方:

  1. 计算视空间方向时要乘以 float3(-1,-1,1),这一点没有想明白,试了其他值效果不对
  2. i.ray = i.ray * (_ProjectionParams.z / i.ray.z); 是为了求在当前ray方向上延伸到摄像机远平面位置的向量,这张图能清晰地说明问题,图片出自 这篇文章
    在这里插入图片描述
  3. o.screenUV = ComputeScreenPos (o.pos); 计算结果的xy分量到片元着色器中需要除以w分量才能使用,除以w后xy分量在[0,1]区间,用来作为UV去读取_CameraDepthTexture。为什么在frag除以w可以参考 文章
  4. clip (float3(0.5,0.5,0.5) - abs(opos.xyz)) 的意思是剔除在物体外的片元,opos为转换到模型空间下的坐标,该模型是一个立方体,其模型空间坐标范围是 [-0.5, 0.5]。
  5. depth = Linear01Depth (depth); 是为了得到线性的深度值,为了 float4 vpos = float4(i.ray * depth,1) 计算时能够得到正确的向量。SAMPLE_DEPTH_TEXTURE 方法取得的深度值是非线性的。参考文章
  6. float2 texUV = opos.xz + 0.5; 把坐标映射到 [0, 1] 区间,这里使用xz坐标,因为贴花要显示在xz平面上。

效果图:
在这里插入图片描述

考虑y方向偏移的贴花

在摆弄贴花物体时发现在拐角和边缘处显示效果不对,出现图片边缘被clamp的效果,如图:
在这里插入图片描述
原因在于在计算纹理坐标时 (float2 texUV = opos.xz + 0.5;)没有考虑y方向的变化,导致在边缘处的片元xz坐标都一样,和clamp对纹理坐标的处理一样。这种情况可以通过把贴花旋转一定的角度来消除,像这样(x轴旋转了-50度):在这里插入图片描述

也可以通过使用法线图来确定模型空间坐标在y方向上的偏差,使这部分偏差参与到UV的计算中,主要步骤有:

  1. 求出视空间的深度值和法线(通过_CameraDepthNormalsTexture属性和DecodeDepthNormal方法)
  2. 把法线转换到模型空间
  3. 把模型空间法线和模型空间Up方向(float3(0,1,0))点乘,求出垂直方向上的偏移程度,
  4. 把偏移程度加入到UV的计算中

Shader代码:

Shader "MJ/ForwardDecal_YOffset"
{
	Properties
	{
		_MainTex ("Decal Texture", 2D) = "white" {}
	}

	SubShader
	{
		Tags{ "Queue"="Geometry+1" }

		Pass
		{
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag2
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float4 pos : SV_POSITION;
				float4 screenUV : TEXCOORD1;
				float3 ray : TEXCOORD2;
				float3 orientation : TEXCOORD3;
				float3 worldNormal : TEXCOORD4;
			};
			
			v2f vert (appdata_base v)
			{
				v2f o;

				o.pos = UnityObjectToClipPos (v.vertex);
				o.screenUV = ComputeScreenPos (o.pos);
				o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1,-1,1);
				return o;
			}

			sampler2D _MainTex;
			sampler2D _CameraDepthNormalsTexture;

			float4 frag2(v2f i) : SV_Target
			{
				i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
				float2 uv = i.screenUV.xy / i.screenUV.w;

				float depth;
				float3 viewNormal;
				float4 encode = tex2D(_CameraDepthNormalsTexture, uv);

				// 返回一个视空间深度值 和 一个视空间法线 //
				// 该深度值是一个线性深度值, 范围是0到1, 精度小于使用SAMPLE_DEPTH_TEXTURE方法获得的深度值 //
				// 因为DecodeDepthNormal方法中的深度值用16位存储,而SAMPLE_DEPTH_TEXTURE中的深度值用32位存储 //
				DecodeDepthNormal(encode, depth, viewNormal);

				viewNormal = normalize(viewNormal);

				float4 vpos = float4(i.ray * depth,1);
				float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
				float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;
				clip (float3(0.5,0.5,0.5) - abs(opos.xyz));

				float3 objectNormal = mul(UNITY_MATRIX_T_MV, viewNormal);
				objectNormal = normalize(objectNormal);

				float3 upDir = float3(0,1,0);

				float NDotU = dot(objectNormal, upDir);
				// float offsetScale = sin(acos(NDotU));
				float offsetScale = 1 - NDotU;				// 效果一样,计算更少 //

				// 先只考虑XZ平面 //
				float2 texUV = opos.xz + float2(0.5, 0.5) + offsetScale * float2(0, opos.y);

				float4 col = tex2D (_MainTex, texUV);
				return col;
			}

			ENDCG
		}
	}

	Fallback Off
}

C#中需要加上 mainCam.depthTextureMode = DepthTextureMode.DepthNormals;, 这样可以在Shader中使用 _CameraDepthNormalsTexture 属性,结合 DecodeDepthNormal 方法可以获取到视空间中的深度值和法线。官方文档

效果图:
在这里插入图片描述
效果图中Scene窗口部分的贴花看起来很扭曲,非常不对,Game窗口的部分变化不大,但是也能看到有一些锯齿存在,关于这个问题我查了一些文章,大概猜测是深度值精度问题导致,_CameraDepthNormalsTexture 中的RG通道用来存储法线信息(16位),BA通道用来存储深度值(16位),而 _CameraDepthTexture 中32位都用来存储深度,所以通过 _CameraDepthTexture 读取的深度值比通过 _CameraDepthNormalsTexture 读取的精确度更高。

但是y方向偏移的方法也有一些问题,比如要通过贴花物体的y坐标来控制垂直部分纹理显示的多少,还有在计算decalUV时要考虑到X和Z两个方向坐标对y偏移的计算,上述例子的代码中为了简便只给Z方向上考虑了Y的偏移,可以在Shader中设置一个Enum,在场景同学摆放贴花时根据摆放位置来控制具体在哪个方向上考虑Y偏移,那么这样一来其实也可以直接用第一种常规方式来实现,反正都需要人工干预,而且第一种方式还少进行了一次矩阵乘法和点乘。

总结:
鉴于贴花在物体拐角和边缘处表现的不是很好,建议的使用方式是在离线时布置好贴花的,这样可以根据不同物体的旋转缩放等条件来调整贴花物体,来达到良好的表现。尽量避免在运行时动态生成,或者只在有限的场景条件里动态生成,比如平地,墙面之类,以减少不确定性以及避免出现预期以外的奇怪效果。

参考链接:
https://blog.csdn.net/NotMz/article/details/78712346
https://forum.unity.com/threads/decodedepthnormal-linear01depth-lineareyedepth-explanations.608452/
https://docs.unity3d.com/Manual/SL-CameraDepthTexture.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html

猜你喜欢

转载自blog.csdn.net/h5502637/article/details/86524543
今日推荐