这两天研究了屏幕图像相关的内容,有一些心得记录下来。在build in管线里,我们通过grab pass/RT获取屏幕图像去制作玻璃效果,在URP管线下,我们则可以使用urp提供的copy color去得到一张同等分辨率屏幕的图像,它的名字叫_CameraColorTexture,它是在所有的opaque模型和skybox渲染完成后抓取的一张图像。

要获取这张图像,得在URP的配置文件里开启opaque texture才可以获取到它。

然后在shader里,利用SAMPLER()函数采样这张贴图。得到了这张贴图后,下一步就是获取屏幕的uv,本人总结了三种方式获取屏幕uv。
1.使用computegrabscreenpos()函数去获取,这是《入门精要》的操作,这里不多赘述。
2.自己手动在顶点着色器里,从HClip空间计算到屏幕空间,经我多次研究,总结了顶点着色器的HClip的数据特点,并通过它计算得到屏幕uv的。

在编辑器DX11下,近裁剪面的坐标最大值在右下角(near,near),最小值在左上角(-near,-near),对应的深度Z为near;远裁剪面的坐标最大值在右下角(far,far),最小值在左上角(-far,-far),对应的深度Z为0。很明显,这个已经和我们在《入门精要》的坐标不一样了:1.因为入门精要的内容是openGL下的,Y轴已经反向了;2.Z轴为了平衡近裁剪面和远裁剪面的精度也做了Z反向(利用了浮点数靠近0处的精度很高来达到让远裁剪面的精度提高;利用了反比例函数曲线的特性让靠近近裁剪面的精度也提高)。3.Z轴的比例也进行了缩放,整个视锥体的长度应该是(0,far-near),这里被缩放成了(0,near)。
这里我们只关注屏幕uv不关注深度z,屏幕uv它应该就是(xy/w*0.5+0.5),但是我们不希望在顶点着色器里进行透视除法,所以结果应该是w*(xy/w*0.5+0.5)=xy*0.5+0.5*w,随后在片元着色器里进行透视除法(其实是可以在顶点着色器里进行透视除法,因为视锥体的非线性只影响了Z,我们这里不会用到Z(至少暂时如此),但是这种骚操作可能导致一些后续的问题(万一后面要用z,搞混淆了不好),还是推荐不在顶点着色器里进行透除。

在片元着色器里,我们对拿到的屏幕uv进行透除,考虑到openGL和DX11的Y轴反向问题,还得判断一下当前的平台是否对y进行oneminus,最终就能得到正常的屏幕uv。

3.如果你被第二种获取屏幕uv搞晕了,不用担心,还有第三种超级简单的方法,直接在片元着色器里获取HClip的值,通过它得到uv,经我测试,HClip.xy就是屏幕空间的物理像素大小,利用它在除以_ScreenParams.xy,就是我们的屏幕UV了,惊不惊喜,意不意外。

前面大费精力得到了屏幕图像和屏幕uv了,是时候展示真正的技术了。通过第五篇专栏的法线贴图的计算方式,我们得到了世界空间的法线,计算过程中也得到了切线空间的法线。这2个法线去分别偏移屏幕uv有什么区别呢?本人通过shader_feather去做了2个shader变体来观察2种不同的偏移方式带来的不同的效果。


计算完成后,我们把颜色输出:如果使用的是世界空间法线,则扭曲的效果除了扭曲之外,还会朝着一个方向进行偏移;而切线空间下只会扭曲,并不会偏移。如果提高强度,世界空间法线下的效果会偏移到不知道什么地方的图像,而切线空间下还是只会加剧扭曲程度,原来的颜色还是停留在原地。


两者的差距会如此巨大,原因是世界空间的法线它是取决于整个世界坐标系;而切线空间下的法线是取决与模型本身,与世界无关。如果我们旋转模型,观察某一个面的法线朝向,世界空间法线会随着旋转而变化;而该面的切线空间的法线始终不会改变。正是因为参考系的不同,才导致了2种截然不同的效果,感兴趣的读者可以自己把2种坐标系下的法线的x分量,y分量分别返回到片元着色器输出,旋转模型看看效果。
说了这么多,该附上最后的代码了,如果代码复制过去报错,那是空格复制的错误,把每行代码里最前面的空格替换成ide里的空格即可。
Shader "WX/URP/grass"
{
Properties
{
_NormalTex("Normal",2D)="bump"{}
_NormalScale("NormalScale",Range(0,1))=1
_BaseColor("BaseColor",Color)=(1,1,1,1)
_Amount("amount",float)=100
[KeywordEnum(WS_N,TS_N)]_NORMAL_STAGE("NormalStage",float)=1
}
SubShader
{
Tags{
"RenderPipeline"="UniversalRenderPipeline"
"RenderType"="Transparent"
"Queue"="Transparent"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/lighting.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _NormalTex_ST;
half4 _BaseColor;
float _NormalScale;
float _Amount;
CBUFFER_END
float4 _CameraColorTexture_TexelSize;//该向量是非本shader独有,不能放在常量缓冲区
TEXTURE2D(_NormalTex);
SAMPLER(sampler_NormalTex);
SAMPLER(_CameraColorTexture);
struct a2v
{
float4 positionOS:POSITION;
float4 normalOS:NORMAL;
float2 texcoord:TEXCOORD;
float4 tangentOS:TANGENT;
};
struct v2f
{
float4 positionCS:SV_POSITION;
float2 texcoord:TEXCOORD;
float4 normalWS:NORMAL;
float4 tangentWS:TANGENT;
float4 BtangentWS:TEXCOORD1;
};
ENDHLSL
pass
{
Tags{
"LightMode"="UniversalForward"
}
HLSLPROGRAM
#pragma vertex VERT
#pragma fragment FRAG
#pragma shader_feature_local _NORMAL_STAGE_WS_N
v2f VERT(a2v i)
{
v2f o;
o.positionCS=TransformObjectToHClip(i.positionOS.xyz);
o.texcoord=TRANSFORM_TEX(i.texcoord,_NormalTex);
o.normalWS.xyz=normalize(TransformObjectToWorldNormal(i.normalOS.xyz));
o.tangentWS.xyz=normalize(TransformObjectToWorldDir(i.tangentOS.xyz));
o.BtangentWS.xyz=cross(o.normalWS.xyz,o.tangentWS.xyz)*i.tangentOS.w*unity_WorldTransformParams.w;
float3 positionWS=TransformObjectToWorld(i.positionOS.xyz);
o.tangentWS.w=positionWS.x;
o.BtangentWS.w=positionWS.y;
o.normalWS.w=positionWS.z;
return o;
}
half4 FRAG(v2f i):SV_TARGET
{
half4 normalTex=SAMPLE_TEXTURE2D(_NormalTex,sampler_NormalTex,i.texcoord)*_BaseColor;//获取法线贴图
float3 normalTS=UnpackNormalScale(normalTex,_NormalScale);//得到我们想要对比的切线空间法线
float2 SS_texcoord=i.positionCS.xy/_ScreenParams.xy;//获取屏幕UV
#ifdef _NORMAL_STAGE_WS_N//计算偏移的2张方式
float3x3 matrix_T2W={i.tangentWS.xyz,i.BtangentWS.xyz,i.normalWS.xyz};//构建tbn矩阵
float3 WSnor=mul(normalTS,matrix_T2W);//得到我们想要的世界空间法线
//return real4(WSnor,1);//测试世界空间法线
float2 SS_bias=WSnor.xy*_Amount*_CameraColorTexture_TexelSize;//如果取的世界空间的法线则执行它计算偏移,但是世界空间的法线由世界空间确定,会随着模型的旋转而变化;
#else
float2 SS_bias=normalTS.xy*_Amount*_CameraColorTexture_TexelSize;//如果取的是切线空间的法线则执行它计算偏移,但是切线空间的法线不随着模型的旋转而变换;
#endif
float4 glassColor=tex2D(_CameraColorTexture,SS_texcoord+SS_bias);//把最终的颜色输出到屏幕即可
return real4(glassColor.xyz,1);
}
ENDHLSL
}
}
}