Unity Shader-Decal贴花[转]

简介

Decal(贴花)。这个效果其实非常常见,比如人物脚底的选中圈,还有最著名的就是CS里面的喷漆效果了,当年大战的时候没少和人互喷,要是说到CS(Quake),那这个技术真的是相当古老了。不过Decal的实现方式有很多种,今天来分别实现一下Self Decal,Additive贴片叠加,Projector投影,Mesh Decal,Deferred Decal(Depth Normal Decal)这几种方式。

Self Decal

我在网上查了半天,似乎也没有对这种贴花方式的命名,索性就叫它Self Decal吧。也可能是这种方式太捞(low)了,大佬们都不care了。所谓Self,也就是直接在同一个Shader里面在叠加一张贴图方式。可以直接采样uv,也可以将mesh制作第二套uv。相比于直接把贴图都做在Albedo中,使用Decal可以更加方便的替换贴图,而且二套uv方便控制特殊的效果,比如捏脸系统里面经常有的脑门上画个咒印之类的。

关于这种Decal,不做太多介绍了。Unity官方资源也包含一个名叫Decal的Surface Shader,我这里就直接简单粗暴地用Unlit类型的vf shader了。最后的叠加使用的是类似Alpha Blend的SrcAlpha OneMinusSrcAlpha,将Decal的颜色和原始颜色根据Decal的Alpha值进行插值:

/********************************************************************
 FileName: SelfDecal.shader
 Description: SelfDecal二套uv自身贴花效果
 history: 12:7:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/SelfDecal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_DecalTex ("Decal", 2D) = "black" {}
	}
	
	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;
				float2 uv_decal : TEXCOORD1;
			};
 
			struct v2f
			{
				float4 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
 
			sampler2D _MainTex;
			sampler2D _DecalTex;
			float4 _DecalTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv.xy = v.uv;
				//offset,tiling二套uv调整不同的decal
				o.uv.zw = TRANSFORM_TEX(v.uv_decal, _DecalTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv.xy);
				fixed4 decal = tex2D(_DecalTex, i.uv.zw);
				
				col.rgb = lerp(col.rgb, decal.rgb, decal.a);
 
				return col;
			}
			ENDCG
		}
	}
}

这里我们直接用二套uv进行decal贴花,可以更加容易控制需要细节部分的贴花同时也不影响主纹理的效果。于是我掏出了使用十分不熟练的3DMax给我最近常用的小狮子展了二套uv,贴图的话就使用我最喜欢的羊驼表情包啦:

效果如下,在石狮子的胸前就贴上了羊驼的头像:

由于我们通过TRANSFORM_TEX对二套uv进行了处理,所以我们可以很容易地通过二套uv的Tiling和Offset来修改贴花显示的内容,来一个羊驼表情幻灯片:


AlphaBlend贴片Decal

上面我们实现了自身Decal效果。但是也有很大的限制,比如我们希望在两个对象连接处叠加一个Decal,我们不方便给每个东西都额外去展uv,直接用世界空间或者物体空间坐标采样也不一定靠谱。其实我们可以考虑一种更简单的方法实现Decal,甚至不需要写代码。因为这个Shader实在太常见啦。可以说是最简单粗暴,而又很容易实现贴花细节的一种方法。

我们可以使用Particle Alpha Blended或者Mobile版本的Alpha Blended Shader,简单来说就是最基本的采样贴图输出的AlphaBlend的Shader就可以达到效果,比如下面的Shader:

/********************************************************************
 FileName: AlphaBlendedDecal.shader
 Description: AlphaBlend Shader
 history: 7:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/AlphaBlendedDecal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
		
		Blend SrcAlpha OneMinusSrcAlpha
		Lighting Off
		ZWrite Off
 
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};
 
			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
 
			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				return col;
			}
			ENDCG
		}
	}
}

这一次,我们就可以找一个面片,然后把我们的需要作为Decal的内容赋给面片,然后把面片贴在地面上或者墙上就可以得到效果啦。很多游戏场景中的地面,墙面细节,如特殊的污渍,血迹,如果不用SelfDecal或者地形刷混合权重的方式的话,直接叠上去一个面片也是一个不错的选择。比如下面我在地面上叠了两个血迹的Decal,即使在两个地砖接缝也可以使用:


Projector Decal

上面的两种Decal可以实现很多贴花的效果了,但是二者都有自身的局限。Self Decal可以贴合任意物体,但是需要物体自身展uv,无法实现一个Decal覆盖多个物体,仅适用于一些预制的贴花替换;而Alpha Blend叠片方式的Decal,可以任意摆放位置,也可以跨物体摆放,但是有一个很重要的问题,如果是平面,如墙面和地面,我们可以直接使用一个面片叠加,而如果是复杂物体之间的贴花效果,直接用面片叠加无法完美贴合,自己做一个贴合的模型也不现实。所以,是时候找一找普适性更高一些的贴花方法了(不过,上面两种贴花也很常用,毕竟如果用简单的方法就能达到效果,何必要浪费时间和性能呢)。

Unity内置的Projector是一个不错的选择。Projector可以做很多好玩的效果,不仅包括贴花,还可以实现阴影等效果。


Projector的使用

使用Projector,相当于Unity会对需要投影的对象使用投影材质重新渲染一遍。不过这个材质的Shader需要是特制的,而不是随便的Shader都可以得到正确的效果。因为我们需要在Shader里面根据投影器传入的Project矩阵进行uv计算,得到采样的uv值。我们可以参考Unity官方aras-p大佬的Projector Shader来编写我们自己的投影Shader,我将Blend模式改为了Blend SrcAlpha OneMinusSrcAlpha,然后直接输出颜色。Shader中采样并没有使用正常的uv坐标,而是通过两个Unity内置矩阵变换顶点,进而在fragment shader中采样,关于这两个内置矩阵的解释,可以参考这里。另外,我们可以通过FalloffTex实现在投影距离内的颜色渐变,通过采样一张falloff贴图,通过纹理的uv横向实现投影的衰减,使用Falloff有两个好处,一方面可以防止Projector背面物体也被投影,出现穿帮的问题;另一方面,通过远距离衰减可以得到更加自然的投影效果,而且也可以防止远处超出投影“视锥体”远裁剪面时出现的调变问题。投影的Shader代码如下:

/********************************************************************
 FileName: ProjectorDecal.shader
 Description: Projector Decal Shader
 history: 7:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/ProjectorDecal" 
{ 
	Properties 
	{
		_Color ("Main Color", Color) = (1,1,1,1)
		_DecalTex ("Cookie", 2D) = "" {}
		_FalloffTex ("FallOff", 2D) = "white" {}
	}
 
	Subshader 
	{
		Pass 
		{
			ZWrite Off
			Fog { Color (0, 0, 0) }
			ColorMask RGB
			Blend SrcAlpha OneMinusSrcAlpha
			Offset -1, -1
 
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			
			struct v2f 
			{
				float4 uvDecal : TEXCOORD0;
				float4 uvFalloff : TEXCOORD1;
				float4 pos : SV_POSITION;
			};
			
			float4x4 unity_Projector;
			float4x4 unity_ProjectorClip;
			fixed4 _Color;
			sampler2D _DecalTex;
			sampler2D _FalloffTex;
			
			v2f vert (float4 vertex : POSITION)
			{
				v2f o;
				o.pos = UnityObjectToClipPos (vertex);
				o.uvDecal = mul (unity_Projector, vertex);
				o.uvFalloff = mul (unity_ProjectorClip, vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 decal = tex2Dproj (_DecalTex, UNITY_PROJ_COORD(i.uvDecal));
				decal *= _Color;
 
				fixed falloff = tex2Dproj (_FalloffTex, UNITY_PROJ_COORD(i.uvFalloff)).r;
				decal *= falloff;
				return decal;
			}
			
			ENDCG
		}
	}
}


这样,我们直接在场景中挂一个Projector组件,然后使用上面Shader的材质,将投影器对准我们需要投影的对象,就可以得到完美贴合多个不规则对象的投影效果了,如下图,血迹的效果完美贴合在了地面和石狮子上:


通过FrameDebugger我们可以看到,Projector的Decal是通过额外的Pass渲染的,上图的石狮子和地面都额外进行了一次渲染,所以Projector也是由一定的性能代价的,如果一个Projector覆盖太多的物体可能会导致批次上升:


Projector的原理

Unity内置的Projector基本已经可以满足我们的需求了,不过还是了解一下Projector的原理更好。注,这里本人暂时先不考虑剔除相关的内容,仅考虑Projector渲染本身的原理。

首先我们观察一下默认的Projector组件的各种参数,远裁剪面,近裁剪面,FOV,Orthographic等,很明显这几个参数跟Camera的参数是一致的,可以认为Projector实际上就是在Projector位置摆放一个相机不过并非使用这个相机渲染,还是使用原始的相机进行渲染,只是通过这个相机的视矩阵和投影矩阵对uv进行转化。我们可以自定义一个脚本实现这一步的转化:

/********************************************************************
 FileName: ProjectorEffect.cs
 Description: Projector原理,自定义实现
 history: 4:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
[ExecuteInEditMode]
public class ProjectorEffect : MonoBehaviour
{
 
    private Camera projectorCam = null;
    public Material projectorMaterial = null;
 
 
    private void Awake()
    {
        projectorCam = GetComponent<Camera>();
    }
 
    private void Update()
    {
        var projectionMatrix = projectorCam.projectionMatrix;
        projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, false);
        var viewMatirx = projectorCam.worldToCameraMatrix;
        var vpMatrix = projectionMatrix * viewMatirx;
        projectorMaterial.SetMatrix("_ProjectorVPMatrix", vpMatrix);
    }
 
 
}

配套的Shader如下:

/********************************************************************
 FileName: ProjectorEffect.shader
 Description: Custom Projector Effect Shader
 history: 4:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/ProjectorEffectShader" 
{ 
	Properties 
	{
		_Color ("Main Color", Color) = (1,1,1,1)
		_DecalTex ("Cookie", 2D) = "" {}
	}
 
	Subshader 
	{
		Pass 
		{
			ZWrite Off
			Fog { Color (0, 0, 0) }
			ColorMask RGB
			Blend SrcAlpha OneMinusSrcAlpha
			Offset -1, -1
 
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			
			struct v2f 
			{
				float4 uvDecal : TEXCOORD0;
				float4 pos : SV_POSITION;
			};
			
			float4x4 _ProjectorVPMatrix;
			
			fixed4 _Color;
			sampler2D _DecalTex;
			
			v2f vert (float4 vertex : POSITION)
			{
				v2f o;
				o.pos = UnityObjectToClipPos (vertex);
				//转化到Decal相机的投影空间
				float4x4 decalMVP = mul(_ProjectorVPMatrix, unity_ObjectToWorld);
				float4 decalProjectSpacePos = mul(decalMVP, vertex);
				//转化到Decal所对应的屏幕空间(0,1)区间位置
				o.uvDecal = ComputeScreenPos(decalProjectSpacePos);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 decal = tex2Dproj (_DecalTex, i.uvDecal);
				decal *= _Color;
				return decal;
			}
			
			ENDCG
		}
	}
}

这样,通过借用在投影位置的相机的VP矩阵,以及渲染对象本身的ObjectToWorldMatrix,我们就可以实现由投影对象物体空间坐标转化到投影相机空间位置的坐标,再将其转化到(0,1)区间,就得到了一个完美的在投影相机视角的uv分布了。

使用自定义Decal的效果与直接Projector的Decal效果类似:


在Decal对应的相机视角的效果如下:


Mesh Decal

下面再来看一种十分常用的Decal实现,这里我们称之为Mesh Decal。简单来说,不是通过再渲染一遍物体实现,而是通过一个代理的Mesh进行碰撞检测,最常用的就是立方体,我们可以直接使用Unity内置的立方体即可,首先遍历所有MeshRenderer,然后获得与代理Mesh相交的物体,这一步我们可以通过Unity内置的bounds.Intersects实现判断,获得相交物体后,我们就需要对每一个相交的物体的所有顶点信息进行遍历,将所有的三角形转化到Decal代理Mesh所在的空间中,然后用立方体的六个面对每个三角形进行裁剪,如果三角形三个顶点都在面外,则直接舍弃,如果三角形三个顶点都在面内,则直接计入,这样就生成了经过立方体裁剪过的Mesh网格,我们仅保留了在Decal立方体内的我们需要的网格,将其渲染就可以得到Mesh Decal了,不过还有一个重要的问题,仅仅有三角形是不够的,我们还需要法线和uv。法线比较好办,有了三角形,我们就可以直接通过三角形的两个边直接cross生成垂直于该三角形的法线,重点在于Mesh Decal的uv,我们可以考虑一下,直接使用一个立方体作为Decal,那么实际上我们需要的uv就是立方体所对应的uv,只是将立方体上的内容投影到了目标对象上,再进一步,我们可以直接考虑将立方体的物体空间位置转化为uv,Unity的标准立方体是(-0.5,0.5)区间的,我们可以将其+0.5即转化为(0,1)区间。但是立方体的坐标位置是三维的,我们所需要的是二维的,所以我们只需要在其中取其中二维,当然,最好是取其中变化范围较大的两个维度,避免出现与被投影面垂直时导致出现uv拉伸的情况(这个问题不仅在MeshDecal会遇到,在Deferred Decal中也是很常见的问题,具体解决方法在Deferred Decal再讨论)。

这里我们先根据上面的基本思路实现一版最近本的贴花生成器:

/********************************************************************
 FileName: MeshDecal.cs
 Description: 生成贴合目标对象的贴花Mesh
 history: 30:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using System.Collections.Generic;
using UnityEngine;
 
public class MeshDecal : MonoBehaviour
{
    private MeshFilter meshFilter = null;
    private MeshRenderer meshRenderer = null;
    private Mesh currMesh = null;
 
    private List<Vector3> vertices = new List<Vector3>();
    private List<Vector3> normals = new List<Vector3>();
    private List<int> indices = new List<int>();
    private List<Vector2> texcoords = new List<Vector2>();
 
    private void Awake()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();
       
    }
 
    [ContextMenu("Gen Decal")]
    public void GetTargetObejcts()
    {
        var mrs = FindObjectsOfType<MeshRenderer>();
        foreach(var r in mrs)
        {
            //剔除Decal自身
            if (r.GetComponent<MeshDecal>() != null)
                continue;
            //遍历所有的MeshRenderer判断和自身立方体相交的Mesh进行Decal Mesh生成
            if (meshRenderer.bounds.Intersects(r.bounds))
            {
                GenerateDecalMesh(r);
            }   
        }
        //将存储的数据生成Unity使用的Mesh
        GenerateUnityMesh();
    }
 
   
    public void GenerateDecalMesh(MeshRenderer target)
    {
        var mesh = target.GetComponent<MeshFilter>().sharedMesh;
        //GC很高,可以优化
        var meshVertices = mesh.vertices;
        var meshTriangles = mesh.triangles;
        
        var targetToDecalMatrix = transform.worldToLocalMatrix * target.transform.localToWorldMatrix;
        for(int i = 0; i < meshTriangles.Length; i = i + 3)
        {
            var index1 = meshTriangles[i];
            var index2 = meshTriangles[i + 1];
            var index3 = meshTriangles[i + 2];
 
            var vertex1 = meshVertices[index1];
            var vertex2 = meshVertices[index2];
            var vertex3 = meshVertices[index3];
            //将网格的三角形转化到Decal自身立方体的坐标系中
            vertex1 = targetToDecalMatrix.MultiplyPoint(vertex1);
            vertex2 = targetToDecalMatrix.MultiplyPoint(vertex2);
            vertex3 = targetToDecalMatrix.MultiplyPoint(vertex3);
 
            var dir1 = vertex1 - vertex2;
            var dir2 = vertex1 - vertex3;
            var normalDir = Vector3.Cross(dir1, dir2).normalized;
 
            var vectorList = new List<Vector3>();
            vectorList.Add(vertex1);
            vectorList.Add(vertex2);
            vectorList.Add(vertex3);
 
            var result = CollisionChecker.CheckCollision(vectorList);
            if (result == true)
                AddPolygon(vectorList.ToArray(), normalDir);
          
        }
    }
 
    public void AddPolygon(Vector3[] poly, Vector3 normal)
    {
        int ind1 = AddVertex(poly[0], normal);
 
        for (int i = 1; i < poly.Length - 1; i++)
        {
            int ind2 = AddVertex(poly[i], normal);
            int ind3 = AddVertex(poly[i + 1], normal);
 
            indices.Add(ind1);
            indices.Add(ind2);
            indices.Add(ind3);
        }
    }
 
    private int AddVertex(Vector3 vertex, Vector3 normal)
    {
        //优先寻找是否包含该顶点
        int index = FindVertex(vertex);
        if (index == -1)
        {
            vertices.Add(vertex);
            normals.Add(normal);
            //物体空间的坐标作为uv,需要从(-0.5,0.5)转化到(0,1)区间
            float u = Mathf.Lerp(0.0f, 1.0f, vertex.x + 0.5f);
            float v = Mathf.Lerp(0.0f, 1.0f, vertex.y + 0.5f);
            texcoords.Add(new Vector2(u, v));
            return vertices.Count - 1;
        }
        else
        {
            //已包含时,将该顶点的法线与新插入的顶点进行平均,共享的顶点,需要修改法线
            normals[index] = (normals[index] + normal).normalized;
            return index;
        }
    }
 
    private int FindVertex(Vector3 vertex)
    {
        for (int i = 0; i < vertices.Count; i++)
        {
            if (Vector3.Distance(vertices[i], vertex) < 0.01f) return i;
        }
        return -1;
    }
 
    public void GenerateUnityMesh()
    {
        currMesh = new Mesh();
 
        currMesh.Clear(true);
 
        currMesh.vertices = vertices.ToArray();
        currMesh.normals = normals.ToArray();
        currMesh.triangles = indices.ToArray();
        currMesh.uv = texcoords.ToArray();
 
        vertices.Clear();
        normals.Clear();
        indices.Clear();
        texcoords.Clear();
 
        meshFilter.sharedMesh = currMesh;
    }
 
}
 
public class CollisionChecker
{
    /// <summary>
    /// 标准立方体的六个面
    /// </summary>
    private static Plane frontPlane = new Plane(Vector3.forward, 0.5f);
    private static Plane backPlane = new Plane(Vector3.back, 0.5f);
    private static Plane upPlane = new Plane(Vector3.up, 0.5f);
    private static Plane downPlane = new Plane(Vector3.down, 0.5f);
    private static Plane leftPlane = new Plane(Vector3.left, 0.5f);
    private static Plane rightPlane = new Plane(Vector3.right, 0.5f);
 
    /// <summary>
    /// 立方体每个面进行裁剪,判断三角形是否在面外
    /// </summary>
    /// <param name="plane"></param>
    /// <param name="vectorList"></param>
    /// <returns></returns>
    private static bool CheckCollision(Plane plane, List<Vector3> vectorList)
    {
        var outSidePointCount = 0;
        for(var current = 0; current < vectorList.Count; current++)
        {
            //var next = (current + 1) % vectorList.Count;
            var v1 = vectorList[current];
            //var v2 = vectorList[next];
 
            if (plane.GetSide(v1))
                outSidePointCount++;
        }
        if (outSidePointCount == vectorList.Count)
            return false;
        return true;
    }
 
    public static bool CheckCollision(List<Vector3> vectorList)
    {
        var result = CheckCollision(frontPlane, vectorList);
        result |= CheckCollision(backPlane, vectorList);
        result |= CheckCollision(upPlane, vectorList);
        result |= CheckCollision(downPlane, vectorList);
        result |= CheckCollision(leftPlane, vectorList);
        result |= CheckCollision(rightPlane, vectorList);
 
        return !result;
    }
 
}

我们使用一个Unity默认的立方体上使用该脚本,通过右键可以直接生成与当前立方体相交对象的Mesh Decal,然后Decal本身的Shader使用类似上面的Alpha Blend的Shader即可:

似乎已经有了Decal效果,然而这个Decal似乎若隐若现,而且随着视角变化会疯狂闪动,其实就是Z-Fighting现象,原因在于我们生成的Mesh与原有Mesh完全重合,这种情况下深度测试无法保证精准测试。要想解决这个问题,我们需要对生成的Mesh做一下进一步处理,即让Mesh沿着法线方向再偏移一些(外扩一些),与之前我们在描边效果中实现的外扩思想类似:

这样就可以避免ZFighing的问题,得到正常的Decal效果:

不过,还有一个很重要的问题,就是我们在考虑立方体面与三角形裁剪时,仅考虑了最简单的裁剪方式,即要么都在,要么都不在的方式,而实际上,如果一个三角形有一部分在立方体内,一部分在外面时,这种情况还是很常见的,此时我们的Decal效果就不对了,比如下图中立方体上边界的锯齿状,以及石狮子下部分石台被框住但是也没有生成MeshDecal的情况:

所以,裁剪时遇到三角形部分在我们需要将三角形切割,生成贴合边界的新顶点,我们重写一下裁剪部分的代码,对每个顶点遍历立方体的每个面,如果当前顶点在对应面内部,加入list,查看下一顶点,如果对应的下一顶点在外面,则用面裁剪两个点,得到在面上的新顶点,加入list,直至所有顶点以及六个面循环结束。


完整的剔除的代码如下(注,未考虑优化,直接使用GC&消耗爆表):

/********************************************************************
 FileName: MeshDecal.cs
 Description: 生成贴合目标对象的贴花Mesh
 history: 30:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using System.Collections.Generic;
using UnityEngine;
 
public class MeshDecal : MonoBehaviour
{
    private MeshFilter meshFilter = null;
    private MeshRenderer meshRenderer = null;
    private Mesh currMesh = null;
 
    private List<Vector3> vertices = new List<Vector3>();
    private List<Vector3> normals = new List<Vector3>();
    private List<int> indices = new List<int>();
    private List<Vector2> texcoords = new List<Vector2>();
 
    private void Awake()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();
 
    }
 
    [ContextMenu("Gen Decal")]
    public void GetTargetObejcts()
    {
        var mrs = FindObjectsOfType<MeshRenderer>();
        foreach (var r in mrs)
        {
            //剔除Decal自身
            if (r.GetComponent<MeshDecal>() != null)
                continue;
            //遍历所有的MeshRenderer判断和自身立方体相交的Mesh进行Decal Mesh生成
            if (meshRenderer.bounds.Intersects(r.bounds))
            {
                GenerateDecalMesh(r);
            }
        }
        //将存储的数据生成Unity使用的Mesh
        GenerateUnityMesh();
    }
 
 
    public void GenerateDecalMesh(MeshRenderer target)
    {
        var mesh = target.GetComponent<MeshFilter>().sharedMesh;
        //GC很高,可以优化
        var meshVertices = mesh.vertices;
        var meshTriangles = mesh.triangles;
 
        var targetToDecalMatrix = transform.worldToLocalMatrix * target.transform.localToWorldMatrix;
        for (int i = 0; i < meshTriangles.Length; i = i + 3)
        {
            var index1 = meshTriangles[i];
            var index2 = meshTriangles[i + 1];
            var index3 = meshTriangles[i + 2];
 
            var vertex1 = meshVertices[index1];
            var vertex2 = meshVertices[index2];
            var vertex3 = meshVertices[index3];
            //将网格的三角形转化到Decal自身立方体的坐标系中
            vertex1 = targetToDecalMatrix.MultiplyPoint(vertex1);
            vertex2 = targetToDecalMatrix.MultiplyPoint(vertex2);
            vertex3 = targetToDecalMatrix.MultiplyPoint(vertex3);
 
            var dir1 = vertex1 - vertex2;
            var dir2 = vertex1 - vertex3;
            var normalDir = Vector3.Cross(dir1, dir2).normalized;
 
            var vectorList = new List<Vector3>();
            vectorList.Add(vertex1);
            vectorList.Add(vertex2);
            vectorList.Add(vertex3);
            //if (Vector3.Angle(Vector3.forward, -normalDir) <= 90.0f)
            {
                CollisionChecker.CheckCollision(vectorList);
                if (vectorList.Count  > 0)
                    AddPolygon(vectorList.ToArray(), normalDir);
            }
        }
    }
 
    public void AddPolygon(Vector3[] poly, Vector3 normal)
    {
        int ind1 = AddVertex(poly[0], normal);
 
        for (int i = 1; i < poly.Length - 1; i++)
        {
            int ind2 = AddVertex(poly[i], normal);
            int ind3 = AddVertex(poly[i + 1], normal);
 
            indices.Add(ind1);
            indices.Add(ind2);
            indices.Add(ind3);
        }
    }
 
    private int AddVertex(Vector3 vertex, Vector3 normal)
    {
        //优先寻找是否包含该顶点
        int index = FindVertex(vertex);
        if (index == -1)
        {
            vertices.Add(vertex);
            normals.Add(normal);
            //物体空间的坐标作为uv,需要从(-0.5,0.5)转化到(0,1)区间
            float u = Mathf.Lerp(0.0f, 1.0f, vertex.x + 0.5f);
            float v = Mathf.Lerp(0.0f, 1.0f, vertex.z + 0.5f);
            texcoords.Add(new Vector2(u, v));
            return vertices.Count - 1;
        }
        else
        {
            //已包含时,将该顶点的法线与新插入的顶点进行平均,共享的顶点,需要修改法线
            normals[index] = (normals[index] + normal).normalized;
            return index;
        }
    }
 
    private int FindVertex(Vector3 vertex)
    {
        for (int i = 0; i < vertices.Count; i++)
        {
            if (Vector3.Distance(vertices[i], vertex) < 0.01f) return i;
        }
        return -1;
    }
 
    public void HandleZFighting(float distance)
    {
        for (int i = 0; i < vertices.Count; i++)
        {
            vertices[i] += normals[i] * distance;
        }
    }
 
    public void GenerateUnityMesh()
    {
        currMesh = new Mesh();
        HandleZFighting(0.001f);
 
 
        currMesh.Clear(true);
 
        currMesh.vertices = vertices.ToArray();
        currMesh.normals = normals.ToArray();
        currMesh.triangles = indices.ToArray();
        currMesh.uv = texcoords.ToArray();
 
        vertices.Clear();
        normals.Clear();
        indices.Clear();
        texcoords.Clear();
 
        meshFilter.sharedMesh = currMesh;
    }
 
}
 
public class CollisionChecker
{
 
    private static List<Plane> planList = new List<Plane>();
 
    static CollisionChecker()
    {
        //front
        planList.Add(new Plane(Vector3.forward, 0.5f));
        //back
        planList.Add(new Plane(Vector3.back, 0.5f));
        //up
        planList.Add(new Plane(Vector3.up, 0.5f));
        //down
        planList.Add(new Plane(Vector3.down, 0.5f));
        //left
        planList.Add(new Plane(Vector3.left, 0.5f));
        //right
        planList.Add(new Plane(Vector3.right, 0.5f));
 
    }
 
    private static void CheckCollision(Plane plane, List<Vector3> vectorList)
    {
        var newList = new List<Vector3>();
        for (var current = 0; current < vectorList.Count; current++)
        {
            var next = (current + 1) % vectorList.Count;
            var v1 = vectorList[current];
            var v2 = vectorList[next];
            var currentPointIn = plane.GetSide(v1);
            if (currentPointIn == true)
                newList.Add(v1);
 
            if (plane.GetSide(v2) != currentPointIn)
            {
                float distance;
                var ray = new Ray(v1, v2 - v1);
                plane.Raycast(ray, out distance);
                var newPoint = ray.GetPoint(distance);
                newList.Add(newPoint);
            }
        }
        vectorList.Clear();
        vectorList.AddRange(newList);
    }
 
    public static void CheckCollision(List<Vector3> vectorList)
    {
        foreach (var curPlane in planList)
        {
            CheckCollision(curPlane, vectorList);
        }
    }
 
}

效果如下,可见,在边界处不是直接把整个三角形剔除,而是严格按照Decal立方体的边界进行剔除:

我们改用一个Alpha Blend的材质,完整效果如下,可见MeshDecal也实现了完美贴合物体的贴花效果:


Deferred Decal

除了Unity内置的Projector可以实现Decal外,我们再来看一种很常见的Decal实现方法,Deferred Decal。毕竟,在效果较好的贴花方法中,Defferred Decal是相对性能很好的一个方式(不考虑生成深度+法线本身的消耗,因为Defferred本身就需要)。

Deferred Decal的思想本身跟延迟渲染光照的思想类似,通过一个特殊的包围几何体作为贴花的代理进行渲染。其实贴花本身只是一个面片,但是一般都是使用立方体或者球体进行贴花的渲染,因为可以保证任意方向的观察效果以及拐角处的贴花。下一步就考虑怎样处理贴花的uv了,毕竟我们无法用立方体本身的uv来渲染。

这里,我们需要用到全屏的深度纹理,有可能需要用到全屏法线纹理。对于前向渲染和延迟渲染,DepthTexture都是可以得到的,但是全屏的法线纹理,对于延迟和前向渲染是不一致的。延迟渲染通过GBuffer获得,前向渲染可以通过DepthNormalTexture获得。但是不管怎样,二者的实现原理是一致的,而且Depth Normal Texture本身就是一个Mini GBuffer,所以这里索性将其统一称之为Deferred Decal了。


Depth Decal

首先,我们需要在渲染时获得屏幕空间当前深度对应的位置,也就是重建世界坐标。这个我们在后处理阶段经常使用,具体可以参考之前本人的文章:Unity Shader-深度相关内容总结。但是直接在渲染物体阶段实现深度重建,与后处理阶段略有不同。我们还是采用视空间射线三角形相似的方式,但是不需要使用屏幕后处理的uv重建,而是直接将物体转换到视空间作为边界,如下图:

A为相机对应点,C为我们要计算的vertex顶点,而D为经过光栅化插值后对应像素的点,也是我们所要求的E点所在的射线在远裁剪面的投影点,三角形AEF相似于三角形ADB。D点我们可以直接通过顶点转化到视空间进行计算,假设为Ray,AB为Ray.z,AF为已知的深度值,即对应屏幕坐标点深度计算得到的视空间距离(linear01depth * _ProjectionParam.z),根据三角形相似,我们就可以求得E点的坐标值,也就是对应的视空间坐标值。

先来看一下Shader:

/********************************************************************
 FileName: DepthDecal.shader
 Description: 根据深度的贴花效果
 history: 17:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/DepthDecal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	
	SubShader
	{
		Tags {"Queue"="Transparent+100"}
		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
			};
 
			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
				float3 ray : TEXCOORD2;
			};
 
			sampler2D _MainTex;
			sampler2D_float _CameraDepthTexture;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.screenPos = ComputeScreenPos(o.vertex);
				o.ray =UnityObjectToViewPos(v.vertex) * float3(-1,-1,1);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				//深度重建视空间坐标
				float2 screenuv = i.screenPos.xy / i.screenPos.w;
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenuv);
				float viewDepth = Linear01Depth(depth) * _ProjectionParams.z;
				float3 viewPos = i.ray * viewDepth / i.ray.z;
				//转化到世界空间坐标
				float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
				//转化为物体空间坐标
				float3 objectPos = mul(unity_WorldToObject, worldPos);
				//剔除掉在立方体外面的内容
				clip(float3(0.5, 0.5, 0.5) - abs(objectPos));
				//使用物体空间坐标的xz坐标作为采样uv
				float2 uv = objectPos.xz + 0.5;
				fixed4 col = tex2D(_MainTex, uv);
				return col;
			}
			ENDCG
		}
		
	}
}

首先,我们需要用一个立方体进行贴花的渲染,直接使用Unity默认的立方体(1,1,1)大小的。当立方体光栅化后,在fragment阶段时,针对每一个像素点,我们可以获得当前像素点的深度值(注意,这里的这个像素点对应的并非立方体自身,而是该点出深度Buffer存储的像素点对应的位置重建得到的物体空间位置),使用这个深度值根据上述方法重建视空间坐标,进而反推得到世界空间坐标,物体空间坐标。那么,该点深度重建得到位置,与立方体边界进行比较,就可以保证在立方体内的贴花才会渲染,立方体外的贴花不进行渲染。而我们采样贴花贴图的坐标也可以直接使用转化到立方体空间内的三维向量中的两个方向进行,立方体的坐标是(-0.5,0.5),我们将其+0.5就刚好转化为(0,1)区间,这样,一个最简单的基于深度的贴花就完成了,还是我们的小狮子,这次在脑袋上贴一个血迹效果:


Depth Normal Decal


上面的Depth Decal虽然可以实现贴花,然而在某些特定情况下效果很糟糕,比如下面这种,当我们的立方体对象xz轴和要投影的对象垂直时,那么投影的结果就是一条拉长的拖尾:

我们来分析一下应该很容易可以得到原因,因为我们使用的是深度重建的物体空间位置,使用其中的xz轴作为纹理坐标进行的采样,所以y轴对应的内容就是拉伸的拖尾效果。我们可以改成xy,但是另一个轴也会出现问题。一般而言,对于不规则物体,xyz都是有变化的,这种现象不是很明显,但是一旦出现类似石狮子底座这种平面完全垂直的,拖尾现象就很严重了。

所以,要解决这个问题最简单的方法就是让xz平面不与被投影面垂直,也就是说摆放Decal的时候要风骚,不能横平竖直滴摆,而是像下面这样,斜着摆,保证没有垂直的情况,这样uv拖影的情况就会大大降低了:

虽然上面的方式可以解决,但是毕竟再风骚的走位也有失误的时候,我们没法保证所有的情况下任意面都不垂直于xz,所以还是要想一个办法来解决这个问题。另一种解决问题的方式就是,如果这个效果不好看,那么这种效果有还不如没有,所以,我们可以考虑直接根据法线方向把与xz轴垂直的面剔除掉。

当然,仅使用屏幕空间深度已经解决不了了,我们还需要一些信息来判断当前平面的朝向。这里自然而然就可以想到全屏法线了。我们判断当前像素点的法线方向,然后把一个物体空间的y轴方向(0,1,0)转化到当前屏幕法线所在的空间,然后进行dot计算夹角,如果接近垂直的阈值,那么直接Clip掉。这种做法也就是Unity官方的做法,可以参考官方Command Buffer实现的Deferred Decal的实现。为了方便,我们优先还是实现前向渲染下的Decal。即,使用DepthNormalTexture获得全屏法线,DepthNormalTexture中的法线方向是视空间的,所以我们要将(0,1,0)转化到视空间中,再直接与视空间法线dot比较:

/********************************************************************
 FileName: DepthNormalDecal.shader
 Description: 根据深度和法线的贴花效果
 history: 17:12:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/DepthNormalDecal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_NormalClipThreshold("NormalClip Threshold", Range(0,0.3)) = 0.1
	}
	
	SubShader
	{
		Tags {"Queue"="Transparent+100"}
		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
			};
 
			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
				float3 ray : TEXCOORD2;
				float3 yDir : TEXCOORD3;
			};
 
			sampler2D _MainTex;
			sampler2D_float _CameraDepthNormalsTexture;
			float _NormalClipThreshold;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.screenPos = ComputeScreenPos(o.vertex);
				o.ray =UnityObjectToViewPos(v.vertex) * float3(-1,-1,1);
				//将立方体y轴方向转化到视空间
				o.yDir = UnityObjectToViewPos(float3(0,1,0));
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				//深度重建视空间坐标
				float2 screenuv = i.screenPos.xy / i.screenPos.w;
				float linear01Depth;
				float3 viewNormal;	
				float4 cdn = tex2D(_CameraDepthNormalsTexture, screenuv);
				DecodeDepthNormal(cdn, linear01Depth, viewNormal);
 
				float viewDepth = linear01Depth * _ProjectionParams.z;
				float3 viewPos = i.ray * viewDepth / i.ray.z;
				//转化到世界空间坐标
				float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
				//转化为物体空间坐标
				float3 objectPos = mul(unity_WorldToObject, worldPos);
				//剔除掉在立方体外面的内容
				clip(float3(0.5, 0.5, 0.5) - abs(objectPos));
				
				//根据法线方向剔除与xz垂直的面投影的内容
				float3 yDir = normalize(i.yDir);
				clip(dot(yDir, viewNormal) - _NormalClipThreshold);
				
				//使用物体空间坐标的xz坐标作为采样uv
				float2 uv = objectPos.xz + 0.5;
				
				fixed4 col = tex2D(_MainTex, uv);
				return col;
			}
			ENDCG
		}
		
	}
}

同时需要配合相机开启DepthNormalTexture:

using UnityEngine;
 
[ExecuteInEditMode]
public class CameraDepthNormalHelper : MonoBehaviour
{
    public DepthTextureMode depthMode = DepthTextureMode.DepthNormals;
 
    private void OnEnable()
    {
        GetComponent<Camera>().depthTextureMode = depthMode;
    }
 
    private void OnDisable()
    {
        GetComponent<Camera>().depthTextureMode = DepthTextureMode.None;
    }
 
}

这样,还是之前的羊驼+小狮子的效果,适当调整NormalClipThreshold,就可以得到较好的效果了,在没有开启法线剔除时:

开启法线剔除后的效果:

Depth Normal Decal有一个很大的问题在于将Depth和Normal存储在同一个Buffer中,深度和法线分别占16位,就会导致深度的精度不足,重建世界坐标时就会有抖动的问题,不过经过本人的测试,如果相机的远裁剪面比较近的话,几百左右还是可以的,但是如果达到了上千就会闪,当然,Scene相机必定会闪的。要想解决这个问题,有两种方案,如果是延迟渲染,那么就直接用G-Buffer即可;如果是前向渲染,再单独渲染一遍Normal Buffer肯定的吃不消的,所以另一种方式是直接使用全屏深度斜率重建Normal,不过代价就是一遍比较费的全屏后处理,而且只能处理顶点法线,法线贴图是没有效果的.


真·Deferred Decal

最后,我们还是要来看一下真正的Deferred Decal,毕竟之前的Depth以及Depth Normal Decal都是山寨的,233。一方面,在延迟渲染下,Depth和Normal都是有的,而且都是高精度的,可以实现更加精确的效果;而另一方面由于Deferred Decal是发生在G-Buffer之后,全屏着色之前,我们可以直接写入一个颜色信息到GBuffer0也就是Albedo中,最终着色使用的仍然是默认的Deferred Shading着色器;也就是说我们可以用一个很简单的Shader实现Decal与原始场景光照效果完美融合,而不必专门再写一个特殊的带光照的Decal Shader。

我们写一个最简单的Deferred Decal系统,通过Command Buffer将渲染Decal的DC插入在Lighting之前:

/********************************************************************
 FileName: DeferredDecal.cs
 Description: 延迟贴花效果
 history: 5:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
 
[ExecuteInEditMode]
public class DeferredDecal : MonoBehaviour {
 
    private MeshFilter meshFilter = null;
    private MeshRenderer meshRenderer = null;
 
    private CommandBuffer commandBuffer = null;
 
    private void Awake()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();
    }
 
    private void OnEnable()
    {
        commandBuffer = new CommandBuffer();
        commandBuffer.SetRenderTarget(BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.CameraTarget);
        commandBuffer.DrawMesh(meshFilter.sharedMesh, transform.localToWorldMatrix, meshRenderer.sharedMaterial);
        Camera.main.AddCommandBuffer(CameraEvent.BeforeLighting, commandBuffer);
    }
 
    private void OnDisable()
    {
        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeLighting, commandBuffer);
    }
}

然后我们修改一下之前的Depth Normal Decal,Depth改回使用_CameraDepthTexture,而Normal改为使用GBuffer中的Normal,但是有一点略微不同,GBuffer中的Normal是世界空间的,所以我们的变化均改为了世界空间进行:

/********************************************************************
 FileName: DeferredDecal.shader
 Description: 延迟贴花效果
 history: 5:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/DeferredDecal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_NormalClipThreshold("NormalClip Threshold", Range(0,0.3)) = 0.1
	}
	
	SubShader
	{
		Tags {"Queue"="Transparent+100"}
		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
			};
 
			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
				float3 ray : TEXCOORD2;
				float3 yDir : TEXCOORD3;
			};
 
			sampler2D _MainTex;
			sampler2D_float _CameraDepthTexture;
			sampler2D _CameraGBufferTexture2;
			float _NormalClipThreshold;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.screenPos = ComputeScreenPos(o.vertex);
				o.ray =UnityObjectToViewPos(v.vertex) * float3(-1,-1,1);
				//将立方体y轴方向转化到世界空间,GBuffer的Normal是世界空间的
				o.yDir = mul((float3x3)unity_ObjectToWorld, float3(0,1,0));
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				//深度重建视空间坐标
				float2 screenuv = i.screenPos.xy / i.screenPos.w;
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenuv);
				float linear01Depth = Linear01Depth(depth);
				float viewDepth = linear01Depth * _ProjectionParams.z;
				float3 viewPos = i.ray * viewDepth / i.ray.z;
				//转化到世界空间坐标
				float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
				//转化为物体空间坐标
				float3 objectPos = mul(unity_WorldToObject, worldPos);
				//剔除掉在立方体外面的内容
				clip(float3(0.5, 0.5, 0.5) - abs(objectPos));
				
				//GBuffer取世界空间法线方向
				float3 worldNormal = tex2D(_CameraGBufferTexture2, screenuv).rgb * 2.0 - 1.0;
				//根据法线方向剔除与xz垂直的面投影的内容
				float3 yDir = normalize(i.yDir);
				clip(dot(yDir, worldNormal) - _NormalClipThreshold);
				
				//使用物体空间坐标的xz坐标作为采样uv
				float2 uv = (objectPos.xz + 0.5);
				
				fixed4 col = tex2D(_MainTex, uv);
				return col;
			}
			ENDCG
		}
	}
}

这样,我们还是使用这样一个没有光照效果的Decal Shader进行贴花,得到的贴花结果仍然是带有光照的。这里来一个对比,左侧的效果是直接叠加上去一个图的效果,没有任何附加的效果,而右侧的石狮子上的贴花,是和场景光照一致的效果(图中由于光照方向,使贴花部分显得暗淡)。

影响Normal的Deferred Decal

使用Deferred Decal,即Decal绘制在GBuffer之后,Lighting之前进行,除了上面的两个优点之外,还有一个很重要的优势在于我们除了可以影响Albedo,还可以影响任意G-Buffer中的内容,实现更加复杂的贴花效果。比如影响法线,影响occlusion,影响roughness,影响Emission等等,只要是出现在G-Buffer中的都可以。这里我们尝试一下Normal的影响。

要想影响Normal效果,我们就需要除了影响Albedo的Buffer之外,还需要影响Normal所在的Buffer,可以分别绘制,但是既然都使用Deferred Shading了,MRT神马的肯定是不在话下了,我们也可以用MRT实现一个DC同时写入两个Buffer。不过有一个地方需要注意一下,我们在实现Decal的时候使用了Normal进行穿帮位置的贴花剔除,相当于读取了Normal RT,但是还要写入这个RT,这就有问题了。一般情况下,一张RT是不允许同时读写的,在PC上可能看不出来问题,但是移动平台轻则失效,重则黑屏,所以我们需要避免对同一个RT进行同时读写操作。这里我们将GBuffer中的Normal拷贝出来一份,读取拷贝的Normal,写入原始的Normal Buffer即可。

C#代码如下:

/********************************************************************
 FileName: DeferredDecal.cs
 Description: 延迟贴花效果,写入NormalBuffer
 history: 5:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
 
[ExecuteInEditMode]
public class DeferredDecal : MonoBehaviour {
 
    private MeshFilter meshFilter = null;
    private MeshRenderer meshRenderer = null;
 
    private CommandBuffer commandBuffer = null;
 
    private void Awake()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();
    }
 
    private void OnEnable()
    {
        commandBuffer = new CommandBuffer();
        //拷贝一份临时的NormalRT用于读取,防止同RT同时读写
        var normalCopyRTId = Shader.PropertyToID("_NormalCopyRT");
        commandBuffer.GetTemporaryRT(normalCopyRTId, -1, -1);
        commandBuffer.Blit(BuiltinRenderTextureType.GBuffer2, normalCopyRTId);
 
        //同时写入Albedo和Normal
        var mrt = new RenderTargetIdentifier[] { BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer2 };
        commandBuffer.SetRenderTarget(mrt, BuiltinRenderTextureType.CameraTarget);
        commandBuffer.DrawMesh(meshFilter.sharedMesh, transform.localToWorldMatrix, meshRenderer.sharedMaterial);
        commandBuffer.ReleaseTemporaryRT(normalCopyRTId);
 
        Camera.main.AddCommandBuffer(CameraEvent.BeforeLighting, commandBuffer);
    }
 
    private void OnDisable()
    {
        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeLighting, commandBuffer);
    }
}

Shader代码也需要进行些许修改,首先要将输出改为两个,同时写入Normal和Albedo,然后需要采样BumpMap,并通过矩阵变换:

/********************************************************************
 FileName: DeferredDecalNormal.shader
 Description: 延迟贴花效果,同时写入Normal buffer
 history: 5:1:2019 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Decal/DeferredDecalNormal"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_BumpMap ("Normals", 2D) = "bump" {}
		_NormalClipThreshold("NormalClip Threshold", Range(0,0.3)) = 0.1
	}
	
	SubShader
	{
		Tags {"Queue"="Transparent+100"}
		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct appdata
			{
				float4 vertex : POSITION;
			};
 
			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
				float3 ray : TEXCOORD2;
				float3 xDir : TEXCOORD3;
				float3 yDir : TEXCOORD4;
				float3 zDir : TEXCOORD5;
			};
 
			sampler2D _MainTex;
			sampler2D _BumpMap;
			sampler2D_float _CameraDepthTexture;
			sampler2D _NormalCopyRT;
			float _NormalClipThreshold;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.screenPos = ComputeScreenPos(o.vertex);
				o.ray =UnityObjectToViewPos(v.vertex) * float3(-1,-1,1);
				//将xyz立方体物体空间转化到世界空间
				o.xDir = mul((float3x3)unity_ObjectToWorld, float3(1,0,0));
				o.yDir = mul((float3x3)unity_ObjectToWorld, float3(0,1,0));
				o.zDir = mul((float3x3)unity_ObjectToWorld, float3(0,0,1));
				return o;
			}
			
			//输出改为两个,同时写入albedo和normal
			void frag (v2f i, out half4 outAlbedo : COLOR0, out half4 outNormal : COLOR1)
			{
				//深度重建视空间坐标
				float2 screenuv = i.screenPos.xy / i.screenPos.w;
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenuv);
				float linear01Depth = Linear01Depth(depth);
				float viewDepth = linear01Depth * _ProjectionParams.z;
				float3 viewPos = i.ray * viewDepth / i.ray.z;
				//转化到世界空间坐标
				float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
				//转化为物体空间坐标
				float3 objectPos = mul(unity_WorldToObject, worldPos);
				//剔除掉在立方体外面的内容
				clip(float3(0.5, 0.5, 0.5) - abs(objectPos));
				
				//GBuffer拷贝的NormalRT取世界空间法线方向
				float3 worldNormal = tex2D(_NormalCopyRT, screenuv).rgb * 2.0 - 1.0;
				//根据法线方向剔除与xz垂直的面投影的内容
				float3 yDir = normalize(i.yDir);
				clip(dot(yDir, worldNormal) - _NormalClipThreshold);
				
				//使用物体空间坐标的xz坐标作为采样uv
				float2 uv = (objectPos.xz + 0.5);
				outAlbedo = tex2D(_MainTex, uv);
				
				//采样BumpMap
				fixed3 bump = UnpackNormal(tex2D(_BumpMap, uv));
				float3x3 normalMat = float3x3(i.xDir, i.zDir, i.yDir);
				bump = mul(bump, normalMat);
				outNormal = fixed4(bump * 0.5 + 0.5, 1.0);
			}
			ENDCG
		}
	}
}

这样,我们就实现了可以同时影响Albdeo和Normal的贴花效果,这里我们使用了一张满是噪点的法线,看一下贴花的效果:

关于Deferred Decal就到这里了,可以参考Unity官方的Command Buffer的Demo,一个重要的功能就是实现Deferred Decal。


总结

本篇blog主要讨论了一下目前常见的一些贴花的实现方式,并全部在Unity中给出了基本的实现,包括自身二套uv贴花(Self Decal),叠片贴花(Alpha Blend面片贴花),内置投影器贴花(Projector),碰撞网格构造贴花(Mesh Decal),延迟贴花的多种实现方式(Depth Decal,Depth Normal Decal,Deferred Decal,同时影响多通道的贴花)等。还是那句话,再捞(low)的方式也有有用处的地方,只有最适合自己项目的实现方式。

原文链接:https://blog.csdn.net/puppet_master/article/details/84310361

作者: Josh Chen

技术引领潮流!