Unity Shader学习:贴花(Decal)
贴花(Decal)指在物体表面贴上图案,是游戏中较常见的效果,比如墙上的弹孔、地面的破裂、角色的纹身等。本文尝试了两种实现方法:Projector贴花、屏幕空间贴花(类似延迟贴花)。使用前向渲染、Built-in管线。Projector贴花
原理
Projector贴花可以理解为从一个管子(自建的projector,也可以是Camera)里看世界,管子的一头贴着图案,通过管子看到的物体就是要贴上图案的目标。按上面的描述,流程分两步:
[*]得到与projector相交(projector中可见)mesh;
[*]正常渲染后,逐mesh进行额外的贴花渲染。
步骤1是判断物体是否在视锥体中的过程。
步骤2中,使用自建的投影矩阵和ObjectToWorld矩阵得到MVP。在顶点阶段,将mesh顶点由模型空间变换到上述裁剪空间,映射到区间内得到uv,然后在片元阶段采样贴花纹理获得颜色。
Mono
这里使用了正交矩阵,计算得到projector的VP,传入shader。
float half = size / 2f;
orthoProjector = Matrix4x4.Ortho(-half, half, -half, half, nearClip, farClip);
var projectionMatrix = GL.GetGPUProjectionMatrix(orthoProjector, false);
var vpMatrix = projectionMatrix * this.transform.worldToLocalMatrix;
decalMat.SetMatrix("_DecalVPMatrix", vpMatrix);
关闭需贴花mesh的batching。遍历收集到的相交mesh,进行额外的贴花渲染。
for (int i = 0; i < gos.Count; ++i)
{
var go = gos;
var mesh = go.GetComponent<MeshFilter>().sharedMesh;
Graphics.DrawMesh(mesh, go.transform.localToWorldMatrix, decalMat, 6, null);
}
Shader
使用正常的透明度混合。
vertex中将顶点变换到自建的projector的裁剪空间,然后通过ComputeScreenPos获得齐次坐标系下的屏幕坐标值,范围。
frag中通过tex2Dproj采样贴花纹理获得最终颜色。
Blend SrcAlpha OneMinusSrcAlpha
vertex:
float4x4 decalMVP = mul(_DecalVPMatrix, unity_ObjectToWorld);
float4 decalProjectionPos = mul(decalMVP, v.vertex);
o.uvDecal = ComputeScreenPos(decalProjectionPos);
fragment:
fixed4 decalColor = tex2Dproj(_DecalTex, UNITY_PROJ_COORD(i.uvDecal));实践中出现了如下拖尾现象,发生在mesh与projector的forward方向几乎平行时。这里,我通过裁减掉顶点法线与projector forward几乎垂直的片元来避免。
拖尾
mono:
decalMat.SetVector(&#34;_DecalProjectorDir&#34;, this.transform.forward * -1f);
vertex:
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.clipV = dot(worldNormal, _DecalProjectorDir);
fragment:
clip(i.clipV - 0.05);效果
Projector贴花
Frame Debug
Projector贴花是通过多pass来实现的(上图也印证了),每多相交一个mesh,就多一个pass。另一方面,哪怕只相交了一点点,也需要遍历mesh的所有顶点和片元,性能开销需留意。
屏幕空间贴花
原理
屏幕空间贴花很巧妙,同时理解起来会略困难一些。
贴花渲染的对象是一个长方体,需要贴花的部分是该长方体与场景模型相交的、处于该长方体内的那些表面。所以,在frag中,将该长方体的片元根据场景深度信息,计算出其在世界空间的真实位置,然后变换到长方体的模型空间,从而得到该片元对应的贴花纹理uv。
其中的关键问题是“如何通过深度值计算世界空间位置”。
正常渲染的过程:
vertex in object space --(Matrix_M)--> world space --(Matrix_V)--> view space
--(Matrix_P)--> clip space --(齐次/透视除法) --> NDC --(屏幕映射) --> frag in screen space如果将上述过程反过来,就能从屏幕空间变换回模型空间。
Mono
在前向渲染中,获得深度信息需设置相机模式
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
使用Unity Cube创建一个长方体,位置、旋转、缩放按需调整,材质使用贴花shader
贴花用Cube
Shader
参考CJT:Unity从深度缓冲重建世界空间位置,通过深度值获得世界空间坐标
vert:
o.pos = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.pos);
frag:
//齐次除法,计算ndc
float4 divW = i.screenPos / i.screenPos.w;
float4 ndcPos = divW * 2 - 1;
//将屏幕像素对应在摄像机远平面的点转换到剪裁空间,也是相机(0,0,0)指向该点的向量
float far = _ProjectionParams.z;
float3 farClipVec = float3(ndcPos.xy, 1) * far;
//通过逆投影矩阵将向量转换到观察空间
float3 viewVec = mul(unity_CameraInvProjection, farClipVec.xyzz).xyz;
//将向量乘以线性深度值,得到在深度缓冲中储存的值在观察空间的位置
float2 screenUV = divW.xy;
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenUV);
float3 viewPos = viewVec * Linear01Depth(depth);
//观察空间变换到世界空间
float4 worldPos = mul(UNITY_MATRIX_I_V, float4(viewPos, 1.0));然后从世界空间变换到Cube的模型空间,clip掉不在模型内的像素,再压扁Y轴获得uv
float4 objectPos = mul(unity_WorldToObject, worldPos);
clip(float3(0.5, 0.5, 0.5) - abs(objectPos));
float2 uv = objectPos.xz + 0.5;
fixed4 finalColor = tex2D(_DecalTex, uv);效果
屏幕空间贴花
屏幕空间贴花pass只跟贴花的Cube数有关、与相交的mesh数无关,但需要额外pass生成深度图。
参考
Unity Shader-Decal贴花(SelfDecal,Alpha Blend,Mesh Decal,Projector,Deferred Decal)
CJT:Unity从深度缓冲重建世界空间位置
https://github.com/ColinLeung-NiloCat/UnityURPUnlitScreenSpaceDecalShader
Projector - Unity 手册
【Unity Shader】基于屏幕空间贴花(Screen Space Decal)的地形裂痕效果
https://juejin.cn/post/6844904067622404103 请教一下,有没有办法可以让deferred decal一直显示在镜头最前呢?就是我想做一个显示发生在物体背面状态的效果(比如被枪击,触摸等等),尝试了把decal shader的ztest,zwrite关了,但是没有用,改了渲染队列也没有效果。请问有可以把贴在物体背面的deferred decal也显示出来的办法吗?谢谢 是要画在物体的背面?感觉用projector好做些,cull front拿到物体背面。如果物体不是半透,在通过stencil把他画到前面。延迟渲染怎么实现没有太多思路,maybe,gbuffer中存背面,deffer贴花用的长方体做stencil标记然后再着色 好的!我去研究一下,多谢多谢!
页:
[1]