一个笔试中遇到的题目,完成一个以主角为中心的扫描效果。下图是我完成的效果图。
主要功能是按下v键,以场景中的角色或者摄像机为中心发出扫描圈,路过的有效目标被高亮且透视。使用了三个脚本和一个Shader完成效果。
脚本
实现思路是ScanCenter脚本监测按键,并且完成切换摄像机和判断是否激活或者重置扫描器。激活的过程中将此脚本所在Object的世界坐标传入扫描器。
void Update()
{
if (Input.GetKeyDown(KeyCode.V))
{
if (scanCamera.enabled == false)
{
mainCamera.enabled = false;
scanCamera.enabled = true;
Scanner.CallScan(transform.position);
}
else {
mainCamera.enabled = true;
scanCamera.enabled = false;
Scanner.reset();
}
}
}
Target脚本实现目标材质和高亮材质的切换。
public void highlight()
{
rend.material = targetMaterial;
}
//重置材质
public void recover()
{
rend.material = baseMaterial;
}
void Start()
{
rend = GetComponent<Renderer>();
rend.enabled = true;
baseMaterial = rend.materials[0];
}
Scanner脚本用于获取摄像机的深度+法线纹理,并且计算指向摄像机的四个角的向量,传入Shader。这个部分在冯乐乐的《Unity Shader入门级精要》中全局雾效的部分有详细说明。Shader获取到向量后。再经过计算反推出片元所在的世界坐标。
以下是计算指向摄像机近裁/远裁平面四个角的归一向量的计算方式,计算结果由四维矩阵_FrustumCorners传入Shader:
//传入着色器的属性
material.SetVector("_WorldSpaceScannerPos", scanPosition);
material.SetFloat("_ScanDistance", ScanDistance);
Matrix4x4 frustumCorners = Matrix4x4.identity;
//计算摄像机指向摄像机空间四个角的归一化向量
float camFar = _camera.farClipPlane;
float camFov = _camera.fieldOfView;
float camAspect = _camera.aspect;
float fovWHalf = camFov * 0.5f;
Vector3 toRight = _camera.transform.right * Mathf.Tan(fovWHalf * Mathf.Deg2Rad) * camAspect;
Vector3 toTop = _camera.transform.up * Mathf.Tan(fovWHalf * Mathf.Deg2Rad);
Vector3 topLeft = (_camera.transform.forward - toRight + toTop);
float camScale = topLeft.magnitude * camFar;
topLeft.Normalize();
topLeft *= camScale;
Vector3 topRight = (_camera.transform.forward + toRight + toTop);
topRight.Normalize();
topRight *= camScale;
Vector3 bottomRight = (_camera.transform.forward + toRight - toTop);
bottomRight.Normalize();
bottomRight *= camScale;
Vector3 bottomLeft = (_camera.transform.forward - toRight - toTop);
bottomLeft.Normalize();
bottomLeft *= camScale;
frustumCorners.SetRow(0, topLeft);
frustumCorners.SetRow(1, topRight);
frustumCorners.SetRow(2, bottomRight);
frustumCorners.SetRow(3, bottomLeft);
material.SetMatrix("_FrustumCorners", frustumCorners);
自定义一个GraphicsBlit让Shader可以通过索引访问该矩阵以求出interpolatedRay,也就是摄像机指向顶点的方向向量。
// 自定义 GraphicsBlit, 将上文四个角设置为顶点的z轴索引,shader中直接提取顶点的vertex.z就可以得到索引。
material.SetTexture("_MainTex", source);
RenderTexture.active = dest;
GL.PushMatrix();
GL.LoadOrtho();
material.SetPass(0);
GL.Begin(GL.QUADS);
GL.MultiTexCoord2(0, 0.0f, 0.0f);
GL.Vertex3(0.0f, 0.0f, 3.0f);
GL.MultiTexCoord2(0, 1.0f, 0.0f);
GL.Vertex3(1.0f, 0.0f, 2.0f);
GL.MultiTexCoord2(0, 1.0f, 1.0f);
GL.Vertex3(1.0f, 1.0f, 1.0f);
GL.MultiTexCoord2(0, 0.0f, 1.0f);
GL.Vertex3(0.0f, 1.0f, 0.0f);
GL.End();
GL.PopMatrix();
Shader
Shader主要用于使用Scanner脚本传入的参数反推片元世界坐标并用图片纹理作出扫描效果(一维纹理)
v2f vert(input v)
{
v2f o;
half index = v.vertex.z;
v.vertex.z = 0.1;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv.xy;
o.uv_depth = v.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv.y = 1 - o.uv.y;
#endif
o.interpolatedRay = _FrustumCorners[(int)index];
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
//获取深度值并且反推出像素点的世界坐标
float linearDepth = Linear01Depth(DecodeFloatRG(tex2D(_CameraDepthTexture, i.uv_depth)));
float3 worldPos = _WorldSpaceCameraPos + (linearDepth * i.interpolatedRay.xyz);
half4 sc = half4(0, 0, 0, 0);
//扫描特效
float dist = distance(worldPos, _WorldSpaceScannerPos);
if (dist < _ScanDistance && dist > _ScanDistance - _ScanWidth && linearDepth < 1)
{
float diff = 1 - (_ScanDistance - dist) / (_ScanWidth);
sc = tex2D(_ScanTex, float2(diff,diff));
sc = _ScanColor*sc.a*_Alpha;
}
return col + sc;
}
Shader的顶点着色器中计算出interpolatedRay作为摄像机指向顶点的方向向量传入片元着色器。片元着色器将interpolatedRay和摄像机深度纹理采集到的深度值相乘获得摄像机指向片元的向量,加上摄像机坐标及是片元的位置坐标。
得到位置坐标即可判断是否有扫描特效。