Unity基于自定义管线实现贴花效果

一,简介

本文在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)

相关推荐
上证50指数etf4 小时前
unity面试总结(项目篇)
unity·游戏引擎
sensen_kiss4 小时前
CPT306 Principles of Computer Games Design 电脑游戏设计原理 Pt.2 游戏引擎
学习·游戏引擎
zyh______16 小时前
unity值属性修改步骤
unity·游戏引擎
小贺儿开发18 小时前
Unity3D 四星探秘:手势互动演示
科技·unity·人机交互·科普·硬件·leap motion·互动
风酥糖18 小时前
Godot游戏练习01-第11节-显示优化,游戏背景,Shader
游戏·游戏引擎·godot
码界奇点21 小时前
基于模块化架构的Unity游戏开发框架设计与实现
java·c++·unity·架构·毕业设计·源代码管理
风酥糖1 天前
Godot游戏练习01-第13节-粒子系统,武器攻击特效
游戏·游戏引擎·godot
张老师带你学1 天前
unity船资源,快艇,帆船,游轮
科技·游戏·unity·游戏引擎·模型
C蔡博士1 天前
Unity游戏物体渲染顺序
unity·游戏引擎·游戏开发