概述
实现环境:
- Unity2020.3.26
- 使用的管线是Built-In渲染管线
- 需要一定Unity基础以及Shader基础
其实这个效果比较容易实现,比较难的点在于规定任意位置然后向外扩散波纹,这就涉及到怎么
把相机中Fragment的相机坐标转化成世界坐标
然后在Shader中使用。
基础原理
因为是后处理效果,所以关键的一步是
拿到fragment的世界坐标
,想要拿到这个坐标可以通过如上图过程拿到:
-
首先拿到
相机的世界坐标,也就是世界中心到相机的向量
(图中黄色那条) -
每个fragment向远裁剪面发射线,拿到
fragment射线方向
-
通过深度图拿到每个
fragment深度信息
-
fragment射线方向 * fragment深度信息 = 相机到fragment世界位置的向量
(图中绿色那条) -
然后简单的向量相加,就可以直接拿到
世界中心指向fragment世界坐标的向量
(图中紫色那条),也就是
fragment的世界坐标
实现
相机脚本
先上脚本具体代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScannerEffect : MonoBehaviour
{
//扫描的起点,扫描线材质(预处理材质),已扫描距离
public Transform ScannerOrigin;
public Material EffectMaterial;
public float ScanDistance;
public float ScanSpeed = 50;
private Camera mCamera;
private bool scanning;
// Update is called once per frame
void Update()
{
if (scanning)
{
ScanDistance += Time.deltaTime * ScanSpeed;
}
//按下R键重置距离
if (Input.GetKeyDown(KeyCode.R))
{
scanning = true;
ScanDistance = 0f;
}
//鼠标左键单击,射线检测获得世界坐标然后设置起点
if (Input.GetMouseButtonDown(0))
{
Ray ray = mCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
scanning = true;
ScanDistance = 4;
ScannerOrigin.position = hit.point;
}
}
}
private void OnEnable()
{
mCamera = GetComponent<Camera>();
}
[ImageEffectOpaque]
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
EffectMaterial.SetVector("_WorldSpaceScannerPos", ScannerOrigin.position);
EffectMaterial.SetFloat("_ScanDistance", ScanDistance);
RaycastCornerBlit(source, destination, EffectMaterial);
}
private void RaycastCornerBlit(RenderTexture source, RenderTexture destination, Material material)
{
// Compute Frustum Corners
float camFar = mCamera.farClipPlane;
float camFov = mCamera.fieldOfView;
float camAspect = mCamera.aspect;
float fovWHalf = camFov * 0.5f;
Vector3 toRight = mCamera.transform.right * Mathf.Tan(fovWHalf * Mathf.Deg2Rad) * camAspect;
Vector3 toTop = mCamera.transform.up * Mathf.Tan(fovWHalf * Mathf.Deg2Rad);
Vector3 topLeft = (mCamera.transform.forward - toRight + toTop);
float camScale = topLeft.magnitude * camFar;
topLeft.Normalize();
topLeft *= camScale;
Vector3 topRight = (mCamera.transform.forward + toRight + toTop);
topRight.Normalize();
topRight *= camScale;
Vector3 bottomRight = (mCamera.transform.forward + toRight - toTop);
bottomRight.Normalize();
bottomRight *= camScale;
Vector3 bottomLeft = (mCamera.transform.forward - toRight - toTop);
bottomLeft.Normalize();
bottomLeft *= camScale;
// Custom Blit, encoding Frustum Corners as additional Texture Coordinates
RenderTexture.active = destination;
material.SetTexture("_MainTex", source);
GL.PushMatrix();
GL.LoadOrtho();
material.SetPass(0);
GL.Begin(GL.QUADS);
GL.MultiTexCoord2(0, 0.0f, 0.0f);
GL.MultiTexCoord(1, bottomLeft);
GL.Vertex3(0.0f, 0.0f, 0.0f);
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();
}
}
首先这是一个
后处理
效果,所以是把当前屏幕设为MainTex进行采样处理,写一个脚本拿到单击屏幕的世界位置,并且设为扫描的扩散中心,并且把扫描边缘位置不断扩散:
进入
后处理
,传入Shader中需要脚本提供的参数(当前的扫描距离和扫描中心的
世界位置
):
脚本中最后一个函数的作用是把屏幕进行裁剪之后传入Shader进行处理:
具体Shader
Shader的具体代码:
Shader "OtherShader/ScannerShader"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_DetailTex("Texture", 2D) = "white" {}
_ScanDistance("Scan Distance", float) = 0
_ScanWidth("Scan Width", float) = 10
_LeadSharp("Leading Edge Sharpness", float) = 10
_LeadColor("Leading Edge Color", Color) = (1, 1, 1, 0)
_MidColor("Mid Color", Color) = (1, 1, 1, 0)
_TrailColor("Trail Color", Color) = (1, 1, 1, 0)
_HBarColor("Horizontal Bar Color", Color) = (0.5, 0.5, 0.5, 0)
}
SubShader
{
Cull off
ZWrite off
ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 ray : TEXCOORD1;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};
float4 _MainTex_TexelSize;
float4 _CameraWS;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv = v.uv.xy;
o.uv_depth = v.uv.xy;
//注意平台适配,如果跟dx一样从上开始的话就要把后处理的uv上下颠倒一下,即1minus
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv.y = 1 - o.uv.y;
#endif
o.interpolatedRay = v.ray;
return o;
}
sampler2D _MainTex;
sampler2D _DetailTex;
sampler2D_float _CameraDepthTexture;
float4 _WorldSpaceScannerPos;
//视觉效果相关参数
float _ScanDistance;
float _ScanWidth;
float _LeadSharp;
float4 _LeadColor;
float4 _MidColor;
float4 _TrailColor;
float4 _HBarColor;
//用一个函数写横向纹理
float4 horizBars(float2 p)
{
return 1 - saturate(round(abs(frac(p.y * 100) * 2)));
}
//用一个函数写横纵向纹理
float4 horizTex(float2 p)
{
return tex2D(_DetailTex, float2(p.x * 30, p.y * 40));
}
float4 frag (v2f i) : SV_Target
{
float4 col = tex2D(_MainTex, i.uv);
float rawDepth = DecodeFloatRG(tex2D(_CameraDepthTexture, i.uv_depth));
float linearDepth = Linear01Depth(rawDepth);
//linearDepth存储了每个fragment对于相机的深度信息
//i.interpolatedRay是相机射到远裁剪面的射线,乘上深度信息就是wsDir,即相机到fragment的向量
//_WorldSpaceCameraPos是世界原点到相机的向量,加上wsDir之后就是fragment的世界坐标
float4 wsDir = linearDepth * i.interpolatedRay;
float3 wsPos = _WorldSpaceCameraPos + wsDir;
float4 scannerCol = float4(0, 0, 0, 0);
float dist = distance(wsPos, _WorldSpaceScannerPos);
//对扫描边界线进行渲染,值得注意的是linearDepth < 1,如果不加这一条的话会一路扫描到天空盒
if (dist < _ScanDistance && dist > _ScanDistance - _ScanWidth && linearDepth < 1)
{
float diff = 1 - (_ScanDistance - dist) / (_ScanWidth);
half4 edge = lerp(_MidColor, _LeadColor, pow(diff, _LeadSharp));
scannerCol = lerp(_TrailColor, edge, diff) + horizBars(i.uv) * _HBarColor;
scannerCol *= diff;
}
return col + scannerCol;
}
ENDCG
}
}
}
拿到fragment世界坐标
的代码:
深度
可以直接拿到,
相机的世界位置
也可以直接拿到,
相机射到远裁剪面的射线
也可以直接拿到,简单相加就可以拿到
fragment的世界位置
(wsPos)了。
然后就是根据需要写各种效果了:
这里我跟据坐标修改了一下,外圈位置不变,然后对于距离小于外圈位置的fragment进行一个关于波的函数的计算,就能拿到一个向外扩散的效果,
if中的参数
都是可以修改的,可以修改然后拿到自己满意的扩散效果:
总之拿到了fragment的世界坐标就可以在后处理做各种神奇的操作了!
还有如下根据位置实现一部分正常处理,一部分边缘检测效果的后处理效果(两个Pass就可以了):
具体代码
具体代码在个人github的shader合集中:
Raler’s Unity Shader Book
扫描效果Scan Effect
参考