urp管线的自学hlsl之路 第二十三篇 科幻扫描效果前篇
雪风carel
2020年07月06日 16:05
收录于文集
共38篇

基础效果如上图

        此篇文章的核心原理是在urp管线的renderFeather里重建世界坐标系,本人利用该原理在配合一些小的技巧,实现了空间扫描效果。

         第一步,在后处理里重建世界坐标系。达到该效果的方式有很多种,譬如把得到的屏幕坐标经过ndc再逆透除得到裁剪空间的坐标,再经过vp的逆矩阵就得到相机空间坐标,再经过cameratoworld函数还原到世界坐标;但是整个过程都是在片元着色器中进行,我想避免overdraw的问题,故使用乐乐的《入门精要》第13.3章全局雾效里提到的利用相机的视锥体参数去重建世界坐标。

        在RenderFeature里,我们除了要和之前的专栏里一样使用blit去处理屏幕图像,还会向shader发送一个视锥体参数的矩阵。这个讲解在《入门精要》里有很详细的推导过程,这里我就只说明核心原理,在RenderFeather里的计算如下。

计算原理

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

示意图

         ΔOAE相似于ΔOXE',故有OA/OX=OE/OE&#​39;,即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&#​34;

{

    Properties

    {

      [HideInInspector]_MainTex("MainTex&#​34;,2D)="white&#​34;{}

      [HDR]_colorX("ColorX&#​34;,Color)=(1,1,1,1)

      [HDR]_colorY("ColorY&#​34;,Color)=(1,1,1,1)

      [HDR]_ColorZ("ColorZ&#​34;,Color)=(1,1,1,1)

      [HDR]_ColorEdge("ColorEdge&#​34;,Color)=(1,1,1,1)

      _width("Width&#​34;,float)=0.02

      _Spacing("Spacing&#​34;,float)=1

      _Speed("Speed&#​34;,float)=1

    }

    SubShader

    {

        Tags{

        "RenderPipeline&#​34;="UniversalRenderPipeline&#​34;

        }

        Cull Off ZWrite Off ZTest Always

        HLSLINCLUDE

        #include &#​34;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&#​34;);

            CommandBuffer cmd=CommandBufferPool.Get("扫描特效&#​34;);

            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&#​34;,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);

    }

}