如果想看视频的话,可以观看我在B站翻译的教程:BV1vt4y1U7HV
实现扫描效果不可能对每个着色器都去增加一段shader代码来实现扫描效果,这是不合理的,不利于项目扩展。所以正确的做法是对于屏幕中看到的像素去做处理,也就是像后处理一样,把rendertexture进行处理后在渲染在屏幕上。
获取深度缓冲
在获取当前渲染的摄像机组件后,对其成员depthTextureMode 赋值就能获得深度贴图。
GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;
于是shader中就能够通过声明sampler2D _CameraDepthTexture来获取深度贴图。
亲自尝试了发现:
如果场景中有灯光能投射阴影,即使不去设置摄像机的深度贴图模式也能获取到深度贴图。
重构空间坐标
因为要实现扫描效果,我们需要计算像素点离扫描点的距离。但如何获得空间顶点坐标呢?因为是后处理,所以在shader中处理的顶点其实只有屏幕四个角落的点。所以想要获取当前像素在世界空间的坐标,我们需要使用深度贴图来重构世界空间坐标。
重构空间坐标有两种方法:
- 构建出当前像素的NDC坐标,再通过当前摄像机的视角*投影矩阵的逆矩阵来得到世界空间下的像素坐标。(但这需要在片元着色器中进行矩阵乘法操作,会影响游戏性能)
- 对图像空间下的视锥体射线(从摄像机触发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。
计算视锥体射线
float camFar = Camera.main.farClipPlane;
float camFov = Camera.main.fieldOfView;
float camAspect = Camera.main.aspect;
float fovWHalf = camFov * 0.5f;
Vector3 toRight = Camera.main.transform.right * Mathf.Tan(fovWHalf * Mathf.Deg2Rad) * camAspect;
Vector3 toTop = Camera.main.transform.up * Mathf.Tan(fovWHalf * Mathf.Deg2Rad);
Vector3 topLeft = (Camera.main.transform.forward - toRight + toTop);
float camScale = topLeft.magnitude * camFar;
topLeft.Normalize();
topLeft *= camScale;
Vector3 topRight = (Camera.main.transform.forward + toRight + toTop);
topRight.Normalize();
topRight *= camScale;
Vector3 bottomRight = (Camera.main.transform.forward + toRight - toTop);
bottomRight.Normalize();
bottomRight *= camScale;
Vector3 bottomLeft = (Camera.main.transform.forward - toRight - toTop);
bottomLeft.Normalize();
bottomLeft *= camScale;
通过以上代码计算完四个角落的视锥体射线后,我们需要把它传入到shader的TEXCOORD中,这样它在传递到片元着色器的适合就能够插值。
但和普通后处理不一样,我们需要给shader的插值寄存器(TEXCOORD)灌值。不能使用简单的Graphic.Blit函数。我们需要自定义这个流程。
所以我们需要使用Unity的GL库(底层图形库)来绘制结果图像在屏幕上。
以下代码写在摄像机的脚本的OnRenderImage中
// Custom Blit, encoding Frustum Corners as additional Texture Coordinates
RenderTexture.active = dest;
mat.SetTexture("_MainTex", source);
GL.PushMatrix();
GL.LoadOrtho();//用于绘制2D图形,详情看官方文档
mat.SetPass(0);//设置使用哪个pass来渲染
GL.Begin(GL.QUADS);//渲染一个四边形,接下来的代码中出现的Vertex3,将每4个绘制一个四边形。
GL.MultiTexCoord2(0, 0.0f, 0.0f);//意思是在TEXCOORD0上传入(0,0)
GL.MultiTexCoord(1, bottomLeft);//意思是在TEXCOORD1上传入视锥体射线bottomLeft
GL.Vertex3(0.0f, 0.0f, 0.0f);//声明以上配置针对顶点(0,0,0)
//以下类似
GL.MultiTexCoord2(0, 1.0f, 0.0f);
GL.MultiTexCoord(1, bottomRight);
GL.Vertex3(1.0f, 0.0f, 0.0f);
GL.MultiTexCoord2(0, 1.0f, 1.0f);
GL.MultiTexCoord(1, topRight);
GL.Vertex3(1.0f, 1.0f, 0.0f);
GL.MultiTexCoord2(0, 0.0f, 1.0f);
GL.MultiTexCoord(1, topLeft);
GL.Vertex3(0.0f, 1.0f, 0.0f);
GL.End();//结束
GL.PopMatrix();
GL.PushMatrix和GL.PopMatrix可以视为一个固定模版,保证转换矩阵正确。
shader代码
Shader "Hidden/TestImage"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_CenterPoint("中心点",Vector)=(0,0,0,0)
[HDR]_EdgeColor("边缘颜色",Color)=(1,1,1,1)
[HDR]_EdgeColor2("边缘颜色2",Color)=(1,1,1,1)
_Radius("半径",Range(0,100))=1
_Width("宽度",Range(0,100))=1
}
SubShader
{
// No culling or depth
Cull off
ZWrite off
ZTest always
Tags{"queue"="transparent"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _CenterPoint;
float4 _EdgeColor;
float4 _EdgeColor2;
sampler2D _CameraDepthTexture;
float _Radius;
float _Width;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 ray:TEXCOORD1;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 interpolatedRay:TEXCOORD2;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.interpolatedRay=v.ray;
o.vertex = UnityObjectToClipPos(v.vertex);
// v.ray=o.vertex-fixed4(_WorldSpaceCameraPos,0);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
float rawDepth=DecodeFloatRG(tex2D(_CameraDepthTexture,i.uv));
float linearDepth=Linear01Depth(rawDepth);
float4 wsDir=linearDepth*i.interpolatedRay;
float3 wsPos=_WorldSpaceCameraPos+wsDir;
float dist=distance(wsPos,_CenterPoint);
fixed4 col = tex2D(_MainTex, i.uv);
if(dist<_Radius&&dist>_Radius-_Width&&linearDepth<1)
{
float diffvalue=1-(_Radius-dist)/_Width;
fixed4 tepColor= lerp(col,_EdgeColor,diffvalue);
tepColor=lerp(tepColor,_EdgeColor2,pow(diffvalue,3));
return tepColor;
}
return col;
}
ENDCG
}
}
}
另外要注意的是,对于天空盒,它的深度信息是最远的,和远裁剪平面同一级。所以要想扫描不出现在天空上,需要对深度扫描的范围进行约束,使Linear01后的深度值<1。