URP | Deacl 贴花效果
那个人真狗
2022年04月26日 14:10
收录于文集
共43篇

效果

  • 贴花效果

实现原理

  • 前计算出裁剪空间位置信息,转换到世界空间

  • 计算世界空间的位置信息。 做一个透视除法,转换到物体空间。

  • 在物体空间内部,计算出场景物体相交的物体。

  • 在物体空间内部,输入纹理平铺到世界空间什么方向(Z)。

Shader Graphs实现

  • 使用深度计算出相交

  • 计算出显示,显示范围有多大

  • 计算纹理UV

代码实现

  • 准备基础Shader

代码块
JavaScript
自动换行
复制代码
Shader "URP/CS"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor("_BaseColor",Color) = (1,1,1,1)
        _EdgeStretchPrevent("_EdgeStretchPrevent",Range(-1,1)) = 0
		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend("Src Blend Mode", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend("Dst Blend Mode", Float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline"  " Queue" = "Transparent"}

        HLSLINCLUDE

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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

            };

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _BaseColor;
                float _EdgeStretchPrevent;
            CBUFFER_END


            TEXTURE2D (_MainTex);
            SAMPLER(sampler_MainTex);

        ENDHLSL

        Pass
        {
            //Tags{ "LightMode"="UniversalForward" }

            Blend[_SrcBlend][_DstBlend]
            ZTest LEqual
            ZWrite Off


            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {

                half4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);     //贴图生成方式

                return col;
            }
            ENDHLSL
        }
    }
}
复制成功

顶点着色器

  • 片元结构体 增加我们需要的属性

代码块
JavaScript
自动换行
复制代码
struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;

                float4 sstexcoord : TEXCOORD1;
                float4 cam2vertexRayOS : TEXCOORD2;
                float3 cameraPosOS : TEXCOORD3;             
            };
复制成功

输出

sstexcoord                  屏幕UV

cam2vertexRayOS      相机空间的射线

cameraPosOS             相机空间下模型顶点位置

  • 获取深度图

  • 顶点着色器阶段

代码块
JavaScript
自动换行
复制代码
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);                 //齐次裁剪空间
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);                              //贴图UV
复制成功

计算出屏幕UV[0-1]

代码块
JavaScript
自动换行
复制代码
o.sstexcoord.xy = o.positionCS.xy * 0.5 + 0.5 * o.positionCS.w;     //计算出屏幕UV[0-1]

                #ifdef UNITY_UV_STARTS_AT_TOP                     //判断是否是OpenGL平台     
                o.sstexcoord.y = o.positionCS.w - o.sstexcoord.y;
                #endif
                o.sstexcoord.zw = o.positionCS.zw;
复制成功
  • 计算相机空间下的模型顶点坐标

代码块
JavaScript
自动换行
复制代码
float4 posVS = mul(UNITY_MATRIX_V,mul(UNITY_MATRIX_M,v.positionOS));                              //相机空间下的模型顶点坐标    
o.cam2vertexRayOS.w = -posVS.z;                                                                   //取相机空间的顶点坐标的z值的负数
o.cam2vertexRayOS.xyz = mul(UNITY_MATRIX_I_M,mul(UNITY_MATRIX_I_V,float4(posVS.xyz,0))).xyz;      //相机空间转换到模型空间
o.cameraPosOS = mul(UNITY_MATRIX_I_M,mul(UNITY_MATRIX_I_V,float4(0,0,0,1))).xyz;                 //相机坐标从相机空间转到模型空间
复制成功

注意:相机空间转换到模型空间下,使用了V的逆矩阵和M的逆矩阵进行计算,但是它是向量,所以得忽略掉平移矩阵,故float4(posVS.xyz,0)后面得取0,而不是1。

扩展 空间转换方式

我们取到的模型空间下的射线,它是模型的顶点从模型坐标系转到相机坐标系下,然后忽略了平移矩阵后,又从相机坐标系转回模型坐标系。那有没有这种可能,只计算模型坐标系转到相机坐标系的平移矩阵,不用费力绕了一大圈又计算回来?我们可以自行推导该想法的正确性。

  • 先说明下这里需要使用的矩阵。

  • 然后我们的射线Ray和模型顶点坐标POS的计算式如下

  • 这里看似可以直接把对应的矩阵直接消掉

片元着色器

  • 前获取屏幕深度

代码块
JavaScript
自动换行
复制代码
float2 SSuv = i.sstexcoord.xy/i.sstexcoord.w; 
float SSDepth = LinearEyeDepth(SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,SSuv).x,_ZBufferParams);       //获取屏幕深度
复制成功
  • cam2vertexRayOS 检测射线,需要透视除法。

在计算出 贴花空间的坐标= 模型空间的相机坐标+模型空间的射线 * 屏幕深度

代码块
JavaScript
自动换行
复制代码
i.cam2vertexRayOS.xyz /= i.cam2vertexRayOS.w;                              //检测射线 透视除法
float3 decalPos = i.cameraPosOS + i.cam2vertexRayOS.xyz * SSDepth;          //裁剪不需要的地方
//输出看一下效果
return half4(decalPos.rgb,1);
复制成功
  • 效果

  • 移动物体,我们可以看到UV在变化。

    本质是和模型坐标系一致,不过它是沿着屏幕深度进行贴合采样。

  • 我们在看一下,发现这个uv和我们贴图UV是不一致的,那怎么解决这个问题。

代码块
JavaScript
自动换行
复制代码
float2 YdecalUV = decalPos.xz + 0.5;                                       //采样UV

half4 tex = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,YdecalUV);
复制成功
  • 效果

  • 我们目前看到,UV平铺出去了,所以,我们需要裁减一下。

  • 创建一个Mask 裁减掉不需要的

代码块
JavaScript
自动换行
复制代码
float mask = (abs(decalPos.x) < 0.5 ? 1:0)*(abs(decalPos.y) < 0.5 ? 1:0)*(abs(decalPos.z)<0.5?1:0);
float3 decalNormal = normalize(cross(ddy(decalPos),ddx(decalPos)));
复制成功
  • 效果

  • 然后放到立体的侧面的时候,会出现拉伸这不是我们想要的。

  • 那我们在做一个限制

代码块
JavaScript
自动换行
复制代码
mask *= decalNormal.y > 0.5 * _EdgeStretchPrevent ? 1:0;
复制成功
  • 效果

整理

  • 我们是在alpha处理调整

代码块
JavaScript
自动换行
复制代码
half4 col =  tex * _BaseColor;
col.a = tex.a * mask * _BaseColor.a;
复制成功

整理到alpha

我们现在看效果是错误的,又回到

  • 我们需要给alpha做一个剔除

代码块
JavaScript
自动换行
复制代码
clip(col.a - 0.05);
复制成功

  • 完成

  • 因为我们是加到场景需要增加 一个是场景雾效,一个是漫反射颜色。

  • 全代码

代码块
JavaScript
自动换行
复制代码
Shader "URP/Decals"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor("_BaseColor",Color) = (1,1,1,1)
        _EdgeStretchPrevent("_EdgeStretchPrevent",Range(-1,1)) = 0
		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend("Src Blend Mode", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend("Dst Blend Mode", Float) = 1
    }
    SubShader
    {
        Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Transparent"  "Queue" = "Transparent" "DisableBatch"="True" }


        HLSLINCLUDE
            
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 texcoord : TEXCOORD0;

            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;

                float4 sstexcoord : TEXCOORD1;
                float4 cam2vertexRayOS : TEXCOORD2;
                float3 cameraPosOS : TEXCOORD3;   
                half fogCoord: TEXCOORD4;          //输出雾效          
            };

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _BaseColor;
                float _EdgeStretchPrevent;
            CBUFFER_END

            TEXTURE2D (_MainTex);
            SAMPLER(sampler_MainTex);

            TEXTURE2D(_CameraDepthTexture);
            SAMPLER(sampler_CameraDepthTexture);


        ENDHLSL

        Pass
        {

            Blend[_SrcBlend][_DstBlend]
            ZTest LEqual
            ZWrite Off

            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_fog

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);                                    //齐次裁剪空间
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                o.sstexcoord.xy = o.positionCS.xy * 0.5 + 0.5 * o.positionCS.w;                             //计算出屏幕UV[0-1]
                #ifdef UNITY_UV_STARTS_AT_TOP                                                               //判断是否是OpenGL平台     
                o.sstexcoord.y = o.positionCS.w - o.sstexcoord.y;
                #endif
                o.sstexcoord.zw = o.positionCS.zw;

                float4 posVS = mul(UNITY_MATRIX_V,mul(UNITY_MATRIX_M,v.positionOS));                              //相机空间下的模型顶点坐标    
                o.cam2vertexRayOS.w = -posVS.z;                                                                   //取相机空间的顶点坐标的z值的负数
                o.cam2vertexRayOS.xyz = mul(UNITY_MATRIX_I_M,mul(UNITY_MATRIX_I_V,float4(posVS.xyz,0))).xyz;      //相机空间转换到模型空间
                o.cameraPosOS = mul(UNITY_MATRIX_I_M,mul(UNITY_MATRIX_I_V,float4(0,0,0,1))).xyz;                 //相机坐标从相机空间转到模型空间
                o.fogCoord = ComputeFogFactor(o.positionCS.z);                                                   //输出雾效
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
            
                float2 SSuv = i.sstexcoord.xy/i.sstexcoord.w; 
                float SSDepth = LinearEyeDepth(SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,SSuv).x,_ZBufferParams);       //获取屏幕深度

                i.cam2vertexRayOS.xyz /= i.cam2vertexRayOS.w;                               //检测射线 透视除法
                float3 decalPos = i.cameraPosOS + i.cam2vertexRayOS.xyz * SSDepth;          //裁剪不需要的地方

                float2 YdecalUV = decalPos.xz + 0.5;                                       //采样UV


                //输出看一下效果    
                float mask = (abs(decalPos.x) < 0.5 ? 1:0)*(abs(decalPos.y) < 0.5 ? 1:0)*(abs(decalPos.z)<0.5?1:0);
                float3 decalNormal = normalize(cross(ddy(decalPos),ddx(decalPos)));                                        //显示遮罩

                mask *= decalNormal.y > 0.5 * _EdgeStretchPrevent ? 1:0;                                                 //剔除Y 平面的

                half4 tex = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,YdecalUV);                                         //采样UV坐标

                half3 ambient = half3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);                                         //环境光颜色

                half4 col =  tex * _BaseColor;                                                                 
                col.rgb = MixFog(col,i.fogCoord) * ambient.rgb;                                                                        //雾效
                col.a = tex.a * mask * _BaseColor.a;                                                                     //输出Alpha

                clip(col.a - 0.05);                                                                                      //剔除不想要显示的部分
                return col;
        
            }
            ENDHLSL
        }
    }
}
复制成功

总结

  • 主要难点在空间空间转换阶段,容易出现错误,

  • 第二部分,采样UV不是 xy   是xz

  • 第三部分,计算遮罩

资料参考

URP下屏幕空间贴花(ScreenSpaceDecal)学习笔记 | 烟雨迷离半世殇的成长之路 (lfzxb.top)