

基础效果如上图
此篇文章的核心原理是在urp管线的renderFeather里重建世界坐标系,本人利用该原理在配合一些小的技巧,实现了空间扫描效果。
第一步,在后处理里重建世界坐标系。达到该效果的方式有很多种,譬如把得到的屏幕坐标经过ndc再逆透除得到裁剪空间的坐标,再经过vp的逆矩阵就得到相机空间坐标,再经过cameratoworld函数还原到世界坐标;但是整个过程都是在片元着色器中进行,我想避免overdraw的问题,故使用乐乐的《入门精要》第13.3章全局雾效里提到的利用相机的视锥体参数去重建世界坐标。
在RenderFeature里,我们除了要和之前的专栏里一样使用blit去处理屏幕图像,还会向shader发送一个视锥体参数的矩阵。这个讲解在《入门精要》里有很详细的推导过程,这里我就只说明核心原理,在RenderFeather里的计算如下。

计算原理
想知道某点的世界坐标,我们只需要计算出该点在相机空间的坐标+相机本身坐标即可得到,相机本身的坐标很容易知道,而我们要实现的就是计算出该点在相机空间下的坐标。下图中,O处为我们的相机,ABCDFGH为我们的视锥体,其中ABCD平面为近裁剪面,距离相机的距离OE=nearplane;X所在平面为我们要求的平面,该平面距离相机的距离为OE‘。因为AE//XE',故很容易推导出ΔOAE~ΔOXE'。

示意图
ΔOAE相似于ΔOXE',故有OA/OX=OE/OE',即OX=OA*OE'/OE。这里,令salce=OA/OE,OA可以由向量OE+向量EA得到向量OA然后取模长得到;OE就是相机近裁剪面的距离,则可以得scale的计算如下:

计算scale
得到scale后代入原式,则有OX=scale*OE‘。但是该等式的2边都是标量,在等式两边同时乘一个向量:normalize(向量OA)则有
normalize(向量OA)*OX=normalize(向量OA)*scale*OE‘
显而易见向量OA和向量OX共线,normalize(向量OA)=normalize(向量OX),则有
向量OX=normalize(向量OA)*scale*OE‘
这里我们令向量TopLeft=normalize(向量OA)*scale,而OE’就是相机空间的深度值depth(这里的相机空间并未严格意义的相机空间,我们并未考虑相机空间坐标的Z朝向正负,为什么?因为我们只想在左手坐标系里计算),向量OX即可由向量TopLeft*depth得到。
但是深度值需要在shader里进行计算得到,故在RenderFeather里只需要计算出向量TopLeft并把它传到shader里即可。

其他三个向量的计算方法如法炮制,然后把它们组成矩阵4X4的矩阵传给shader。注意这里是按行进行存储,在shader里同样需要按行进行取出。

传矩阵
最后再申请commandbuffer,申请RT,执行我们的绘制命令,归还RT,归还cmd。

执行绘制
第二步,shader计算部分。定义一个矩阵Matrix来接收commandbuffer传过来的矩阵。

shader里的接收矩阵
顶点着色器里,根据顶点所在位置来定义需要传递给片元着色器的向量(这个新的向量我定义为Direction,即上面的向量TopLeft),用来在片元着色器里得到正确的相机空间坐标,为了区分我们到底是传向量TopLeft?还是向量TopRight?还是向量ButtomLeft?还是ButtomRight? 即取矩阵的哪一行进行计算,我们巧妙得利用了uv的值去判断。众所周知,屏幕本身就是一个片,只有4个顶点,其UV坐标为(0,0)(1,0)(1,1)(0,1),通过多个if逻辑来判断,我们可以很方便去得到正确的向量Direction,虽然shader里使用逻辑判断会有性能瓶颈,但是由于只有4个顶点,所以计算量可以忽略不计。

顶点着色器里
在片元着色器里,我们先采样得到相机空间的线性深度值depth,再用它乘以顶点着色器里传过来的向量Direction,再加上相机本身坐标,可以得到正确的世界坐标。

得到世界坐标
得到正确的世界空间坐标后,我们将它输出到屏幕,效果如下。X方向为红色(1,0,0,0),Y方向为绿色(0,1,0,0),Z方向为蓝色(0,0,1,0),和unity引擎右上角的世界坐标轴朝向完全一致!这样我们就成功在后处理里重建了世界坐标系。既然后处理里都能拿到每个像素的坐标了,那岂不是为所欲为.....它能做非常多炫酷的效果,本专栏里所演示的只是它的冰山一角。

把世界坐标输出到屏幕
得到世界坐标后,因为我们的坐标轴取值范围是从-∞到+∞,而颜色的范围只是0-1之间,如果对世界坐标使用取余函数就可以得到只在0-1的值了,上一次这样的骚操作是在序列帧算法这篇文章https://www.bilibili.com/read/cv6482589里提到过,有兴趣的读者可以去看看。

取余数
我们把余数输出出来看看效果如何,所有的值都是在0-1之间不断循环变换。

取余数
对于在0-1之间均匀变换的我们想得到它的边界位置,所以直接来个step函数,我们只取0.98到1.0之间的值为1,其他的值为0,我们把它输出出来。是不是有那味了,线框就直接出来了。

但是这个线框的颜色红蓝绿(为什么是红蓝绿?是因为它对应XYZ三个轴向)很乱而且不好看,我们想要自由控制颜色,我们定义三个颜色,去分别控制XYZ方向的线框颜色,并把它输出。

线框颜色
然后我们再看看输出后的效果如何,颜色加上HDR确实变漂亮了许多。

得到正常的线框效果
然后采样颜色,两者相加,即可得到正常的效果。

和屏幕颜色混合后的效果
最后我们通过一个参数Spacing去控制线框的间距,Width参数去控制线框宽度。下一篇,我们将通过世界坐标制作一个mask遮罩,通过sobel算子去得到描边效果,再修改RenderFeather去控制这些效果。先附上这一篇的源码,如果直接复制过去报错是空格问题,请替换掉所有空格。
Shader源码
Shader "WX/URP/Post/scan"
{
Properties
{
[HideInInspector]_MainTex("MainTex",2D)="white"{}
[HDR]_colorX("ColorX",Color)=(1,1,1,1)
[HDR]_colorY("ColorY",Color)=(1,1,1,1)
[HDR]_ColorZ("ColorZ",Color)=(1,1,1,1)
[HDR]_ColorEdge("ColorEdge",Color)=(1,1,1,1)
_width("Width",float)=0.02
_Spacing("Spacing",float)=1
_Speed("Speed",float)=1
}
SubShader
{
Tags{
"RenderPipeline"="UniversalRenderPipeline"
}
Cull Off ZWrite Off ZTest Always
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END
real4 _colorX;
real4 _colorY;
real4 _ColorZ;
real4 _ColorEdge;
float _width;
float _Spacing;
float _Speed;
TEXTURE2D( _MainTex);
SAMPLER(sampler_MainTex);
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
float4x4 Matrix;
struct a2v
{
float4 positionOS:POSITION;
float2 texcoord:TEXCOORD;
};
struct v2f
{
float4 positionCS:SV_POSITION;
float2 texcoord:TEXCOORD;
float3 Dirction:TEXCOORD1;
};
ENDHLSL
pass
{
HLSLPROGRAM
#pragma vertex VERT
#pragma fragment FRAG
v2f VERT(a2v i)
{
v2f o;
o.positionCS=TransformObjectToHClip(i.positionOS.xyz);
o.texcoord=i.texcoord;
int t=0;
if(i.texcoord.x<0.5&&i.texcoord.y<0.5)
t=0;
else if(i.texcoord.x>0.5&&i.texcoord.y<0.5)
t=1;
else if(i.texcoord.x>0.5&&i.texcoord.y>0.5)
t=2;
else
t=3;
o.Dirction=Matrix[t].xyz;
return o;
}
real4 FRAG(v2f i):SV_TARGET
{
real4 tex=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord);
half depth=LinearEyeDepth(SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,i.texcoord).x,_ZBufferParams).x;
float3 WSpos=_WorldSpaceCameraPos+depth*i.Dirction+float3(0.1,0.1,0.1);//得到世界坐标
//return real4(frac(WSpos),1);
float3 Line=step(1-_width,frac(WSpos/_Spacing));//线框
//return real4(Line,1);
float4 Linecolor=Line.x*_colorX+Line.y*_colorY+Line.z*_ColorZ;//给线框上色
return Linecolor+tex;
}
ENDHLSL
}
}
}
RenderFeather源码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
[ExecuteInEditMode]
public class scan : ScriptableRendererFeature
{
[System.Serializable]public class setting
{
public Material mat;
public RenderPassEvent Event=RenderPassEvent.AfterRenderingTransparents;
}
public setting mysetting =new setting();
class CustomRenderPass : ScriptableRenderPass
{
public Material mat;
RenderTargetIdentifier sour;
public void set(RenderTargetIdentifier sour)
{
this.sour=sour;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
int temp=Shader.PropertyToID("temp");
CommandBuffer cmd=CommandBufferPool.Get("扫描特效");
RenderTextureDescriptor desc=renderingData.cameraData.cameraTargetDescriptor;
Camera cam= renderingData.cameraData.camera;
float height=cam.nearClipPlane*Mathf.Tan(Mathf.Deg2Rad*cam.fieldOfView*0.5f);
Vector3 up=cam.transform.up*height;
Vector3 right=cam.transform.right*height*cam.aspect;
Vector3 forward=cam.transform.forward*cam.nearClipPlane;
Vector3 ButtomLeft=forward-right-up;
float scale=ButtomLeft.magnitude/cam.nearClipPlane;
ButtomLeft.Normalize();
ButtomLeft*=scale;
Vector3 ButtomRight=forward+right-up;
ButtomRight.Normalize();
ButtomRight*=scale;
Vector3 TopRight=forward+right+up;
TopRight.Normalize();
TopRight*=scale;
Vector3 TopLeft=forward-right+up;
TopLeft.Normalize();
TopLeft*=scale;
Matrix4x4 MATRIX=new Matrix4x4();
MATRIX.SetRow(0,ButtomLeft);
MATRIX.SetRow(1,ButtomRight);
MATRIX.SetRow(2,TopRight);
MATRIX.SetRow(3,TopLeft);
mat.SetMatrix("Matrix",MATRIX);
cmd.GetTemporaryRT(temp,desc);
cmd.Blit(sour,temp,mat);
cmd.Blit(temp,sour);
context.ExecuteCommandBuffer(cmd);
cmd.ReleaseTemporaryRT(temp);
CommandBufferPool.Release(cmd);
}
}
CustomRenderPass m_ScriptablePass;
public override void Create()
{
m_ScriptablePass = new CustomRenderPass();
m_ScriptablePass.mat=mysetting.mat;
m_ScriptablePass.renderPassEvent = mysetting.Event;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
m_ScriptablePass.set(renderer.cameraColorTarget);
renderer.EnqueuePass(m_ScriptablePass);
}
}