贴花效果,就和名字的直接意思类似,把一张图贴到另一个物体上显示,经常被用于表现一些重复出现的图案,比如弹孔,涂鸦,污渍等。效果图:
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,显示贴花图案。具体的逻辑如下:
直接上代码:
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
}
代码中需要注意的一些地方:
float3(-1,-1,1)
,这一点没有想明白,试了其他值效果不对i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
是为了求在当前ray方向上延伸到摄像机远平面位置的向量,这张图能清晰地说明问题,图片出自 这篇文章o.screenUV = ComputeScreenPos (o.pos);
计算结果的xy分量到片元着色器中需要除以w分量才能使用,除以w后xy分量在[0,1]区间,用来作为UV去读取_CameraDepthTexture
。为什么在frag除以w可以参考 文章clip (float3(0.5,0.5,0.5) - abs(opos.xyz))
的意思是剔除在物体外的片元,opos为转换到模型空间下的坐标,该模型是一个立方体,其模型空间坐标范围是 [-0.5, 0.5]。depth = Linear01Depth (depth);
是为了得到线性的深度值,为了 float4 vpos = float4(i.ray * depth,1)
计算时能够得到正确的向量。SAMPLE_DEPTH_TEXTURE
方法取得的深度值是非线性的。参考文章。float2 texUV = opos.xz + 0.5;
把坐标映射到 [0, 1] 区间,这里使用xz坐标,因为贴花要显示在xz平面上。在摆弄贴花物体时发现在拐角和边缘处显示效果不对,出现图片边缘被clamp的效果,如图:
原因在于在计算纹理坐标时 (float2 texUV = opos.xz + 0.5;
)没有考虑y方向的变化,导致在边缘处的片元xz坐标都一样,和clamp对纹理坐标的处理一样。这种情况可以通过把贴花旋转一定的角度来消除,像这样(x轴旋转了-50度):
也可以通过使用法线图来确定模型空间坐标在y方向上的偏差,使这部分偏差参与到UV的计算中,主要步骤有:
_CameraDepthNormalsTexture
属性和DecodeDepthNormal
方法)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偏移,那么这样一来其实也可以直接用第一种常规方式来实现,反正都需要人工干预,而且第一种方式还少进行了一次矩阵乘法和点乘。
总结:
鉴于贴花在物体拐角和边缘处表现的不是很好,建议的使用方式是在离线时布置好贴花的,这样可以根据不同物体的旋转缩放等条件来调整贴花物体,来达到良好的表现。尽量避免在运行时动态生成,或者只在有限的场景条件里动态生成,比如平地,墙面之类,以减少不确定性以及避免出现预期以外的奇怪效果。