一,简介

        本文在Catlike Coding 实现的【Custom SRP 2.5.0】基础上参考知乎文章Unity实现Decal贴花和运用  实现相关功能。

二,环境

        Unity :2022.3.18f1

        CRP Library :14.0.10

        URP基本结构 :Custom SRP 2.5.0

三,实现

        贴花里最重要的概念是投影,好比拿喷漆在物体表面上喷射一样。

        在工程上的实现具体要考虑的是要在哪里画。为此需要实现两个点,一是限制绘制范围,二是绘制的点在贴图上的相对位置。

        

        本文通过Stencil Box的方式实现。

        在场景中创建一个Cube,创建新的shader与材质并挂上后,在Shader添加如下代码。

Shader "Custom/DecalShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        HLSLINCLUDE
		#include "Custom RP/ShaderLibrary/Common.hlsl"
		#include "Custom RP/ShaderLibrary/LitInput.hlsl"
		ENDHLSL

        Pass
        {
           Stencil
           {
               Ref 1
               Comp Always
               Pass Replace
           }

           ZTest GEqual
           ZWrite Off
           Cull Front
           ColorMask 0 


            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            
            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

        
            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                return o;
            }
            float4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }


        
    }
}

        注意 :Include 中的hlsl代码是Custom SRP 2.5.0中的实现,他封装并自己实现了一些SRP中HLSL相关功能。TransformObjectToHClip 这个函数是Unity中封装的函数。

        这个Pass的主要目的是限制绘制范围,通过深度比较将需要绘制的区域写入到模板里面。

         在去掉     ColorMask 0  后你会看到这样的场景。

        黑色区域就是需要绘制的区域。

        

        然后就是要知道这些黑色区域的地方对应的是贴图那个位置了。这也是投影这个概念要应用的地方。

        获取这个位置需要分成三个步骤。

        1,知道当前渲染点的世界空间坐标。

        2,知道当前渲染点的本地空间坐标。

        3,转换本地空间坐标到贴图uv。

        具体实现代码如下:

        

Pass
{
    Tags { "LightMode"="CustomLit" }

    
   Stencil
   {
       Ref 1
       Comp Equal
       Pass Keep
   }

   Blend SrcAlpha OneMinusSrcAlpha
   ZWrite Off
   Cull Back

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    struct appdata
    {
        float4 vertex : POSITION;
    };

    struct v2f
    {
        float4 vertex : SV_POSITION;

    };

       
    sampler2D _MainTex;
    float4 _MainTex_ST;
    float4x4 _WorldToDecal;


    v2f vert (appdata v)
    {
        v2f o;
        o.vertex = TransformObjectToHClip(v.vertex);
        return o;
    }


    float4 frag (v2f i) : SV_Target
    {
        float2 posUV = i.vertex.xy / _ScreenParams.xy;
        float3 wpos = GetWorldPosByScreenUV(posUV);

        // 转换到贴花本地空间
        float3 decalPos = mul(_WorldToDecal, float4(wpos, 1)).xyz;

        float2 uv = decalPos.xz + 0.5;

        float4 col = tex2D(_MainTex, uv);
        return col;
    }
    ENDHLSL
}

        获取世界空间坐标的时候需要注意,不能简单的通过当前像素的裁切空间坐标转换到世界空间,虽然这确实能拿到当前像素的世界空间坐标,但与实际需要的像素点的世界空间坐标不对应。这是因为第二个Pass在渲染的时候ZTest 默认是 LEqual,通过模板测试,且深度不在绘制范围的片元都经过了渲染,从逻辑上来说这里ZTest 应该设置为Equal,这样片元上的的世界空间坐标就是我们需要的坐标了,但是设置为Equal的时候Pass渲染不出结果,原因不明。

        所以获取世界空间坐标的方式改为从深度图重建世界空间坐标。

        GetWorldPosByScreenUV 相关实现如下:

// 根据线性深度值和屏幕UV,还原世界空间下,相机到顶点的位置偏移向量
half3 ReconstructViewPos(float2 uv, float linearEyeDepth) {
	// Screen is y-inverted
	uv.y = 1.0 - uv.y;

	float zScale = linearEyeDepth * _ProjectionParams2.x; // divide by near plane
	float3 viewPos = _CameraViewTopLeftCorner.xyz + _CameraViewXExtent.xyz * uv.x + _CameraViewYExtent.xyz * uv.y;
	viewPos *= zScale;

	return viewPos;
}

float3 GetViewPosByScreenUV(float2 uv) {
	//深度图重构观察空间坐标
	float depth = SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, uv, 0);
	depth = IsOrthographicCamera() ? OrthographicDepthBufferToLinear(depth) : LinearEyeDepth(depth, _ZBufferParams);

	return ReconstructViewPos(uv, depth);
}

float3 GetWorldPosByScreenUV(float2 uv) {
	//深度图重构世界坐标
	float3 vpos = GetViewPosByScreenUV(uv);
	float3 wpos = _WorldSpaceCameraPos + vpos;
	return wpos;
}

        部分参数从C# 传入:

	void SetCameraParams(CommandBuffer buffer)
	{
		Matrix4x4 view = camera.worldToCameraMatrix;
		Matrix4x4 proj = camera.projectionMatrix;

		// 将camera view space 的平移置为0,用来计算world space下相对于相机的vector
		Matrix4x4 cview = view;
		cview.SetColumn(3, new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
		Matrix4x4 cviewProj = proj * cview;

		// 计算viewProj逆矩阵,即从裁剪空间变换到世界空间
		Matrix4x4 cviewProjInv = cviewProj.inverse;

		// 计算世界空间下,近平面四个角的坐标
		var near = camera.nearClipPlane;

		Vector4 topLeftCorner = cviewProjInv.MultiplyPoint(new Vector4(-1.0f, 1.0f, -1.0f, 1.0f));
		Vector4 topRightCorner = cviewProjInv.MultiplyPoint(new Vector4(1.0f, 1.0f, -1.0f, 1.0f));
		Vector4 bottomLeftCorner = cviewProjInv.MultiplyPoint(new Vector4(-1.0f, -1.0f, -1.0f, 1.0f));

		// 计算相机近平面上方向向量
		Vector4 cameraXExtent = topRightCorner - topLeftCorner;
		Vector4 cameraYExtent = bottomLeftCorner - topLeftCorner;

		//设置参数
		buffer.SetGlobalVector(CameraViewTopLeftCorner, topLeftCorner);
		buffer.SetGlobalVector(CameraViewXExtent, cameraXExtent);
		buffer.SetGlobalVector(CameraViewYExtent, cameraYExtent);
		buffer.SetGlobalVector(ProjectionParams2, new Vector4(1.0f / near, camera.transform.position.x, camera.transform.position.y, camera.transform.position.z));

	}

        得到渲染点在世界空间坐标后,将该世界空间坐标通过物体本身的转换矩阵转换到本地空间坐标。

        _WorldToDecal获取如下:

        

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CustomDecal : MonoBehaviour
{
    static MaterialPropertyBlock block;

    // Start is called before the first frame update
    void Start()
    {
        SetPropertyBlock();

    }
    void OnValidate()
    {
        SetPropertyBlock();
    }
    // Update is called once per frame
    void Update()
    {
        SetPropertyBlock();
    }

    private void SetPropertyBlock()
    {
        if (block == null)
        {
            block = new MaterialPropertyBlock();

        }
        // 获取从世界空间到该物体局部空间的变换矩阵
        Matrix4x4 worldToDecal = transform.worldToLocalMatrix;

        // 将矩阵设置到材质中
        block.SetMatrix("_WorldToDecal", worldToDecal);
        GetComponent<Renderer>().SetPropertyBlock(block);
    }
}

        最后只要获得本地空间坐标的话根据投射角度选择对应的xz值,至此投影效果便实现了。

        效果如下:

参考资料:

        Unity实现Decal贴花和运用

        Unity Shader-Decal贴花

        Unity Shader学习:贴花(Decal)

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐