
贴花效果

前计算出裁剪空间位置信息,转换到世界空间
计算世界空间的位置信息。 做一个透视除法,转换到物体空间。
在物体空间内部,计算出场景物体相交的物体。
在物体空间内部,输入纹理平铺到世界空间什么方向(Z)。
使用深度计算出相交

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

计算纹理UV

准备基础Shader
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
}
}
} 片元结构体 增加我们需要的属性
struct v2f
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
float4 sstexcoord : TEXCOORD1;
float4 cam2vertexRayOS : TEXCOORD2;
float3 cameraPosOS : TEXCOORD3;
}; 输出
sstexcoord 屏幕UV
cam2vertexRayOS 相机空间的射线
cameraPosOS 相机空间下模型顶点位置
获取深度图

顶点着色器阶段
o.positionCS = TransformObjectToHClip(v.positionOS.xyz); //齐次裁剪空间
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); //贴图UV 计算出屏幕UV[0-1]
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; //相机坐标从相机空间转到模型空间 注意:相机空间转换到模型空间下,使用了V的逆矩阵和M的逆矩阵进行计算,但是它是向量,所以得忽略掉平移矩阵,故float4(posVS.xyz,0)后面得取0,而不是1。
扩展 空间转换方式
我们取到的模型空间下的射线,它是模型的顶点从模型坐标系转到相机坐标系下,然后忽略了平移矩阵后,又从相机坐标系转回模型坐标系。那有没有这种可能,只计算模型坐标系转到相机坐标系的平移矩阵,不用费力绕了一大圈又计算回来?我们可以自行推导该想法的正确性。
先说明下这里需要使用的矩阵。

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

这里看似可以直接把对应的矩阵直接消掉
前获取屏幕深度
float2 SSuv = i.sstexcoord.xy/i.sstexcoord.w;
float SSDepth = LinearEyeDepth(SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,SSuv).x,_ZBufferParams); //获取屏幕深度 cam2vertexRayOS 检测射线,需要透视除法。
在计算出 贴花空间的坐标= 模型空间的相机坐标+模型空间的射线 * 屏幕深度
i.cam2vertexRayOS.xyz /= i.cam2vertexRayOS.w; //检测射线 透视除法
float3 decalPos = i.cameraPosOS + i.cam2vertexRayOS.xyz * SSDepth; //裁剪不需要的地方
//输出看一下效果
return half4(decalPos.rgb,1); 效果

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

本质是和模型坐标系一致,不过它是沿着屏幕深度进行贴合采样。
我们在看一下,发现这个uv和我们贴图UV是不一致的,那怎么解决这个问题。

float2 YdecalUV = decalPos.xz + 0.5; //采样UV
half4 tex = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,YdecalUV); 效果

我们目前看到,UV平铺出去了,所以,我们需要裁减一下。
创建一个Mask 裁减掉不需要的
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; 效果

我们是在alpha处理调整
half4 col = tex * _BaseColor;
col.a = tex.a * mask * _BaseColor.a; 整理到alpha
我们现在看效果是错误的,又回到

我们需要给alpha做一个剔除
clip(col.a - 0.05);

完成

因为我们是加到场景需要增加 一个是场景雾效,一个是漫反射颜色。
全代码
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)