HuldaGnodim 发表于 2022-11-30 11:02

Unity Shader学习:扫描(Scan)

扫描(Scan)是很酷、较常见、有科技感的游戏效果,一般出现在有较大3D场景的游戏中,比如《塞尔达·旷野之息》中林克按住磁铁/时停扫描地图时的效果。本文使用后处理实现了2种扫描效果:全地图扫描、定点扫描,主要涉及获得深度纹理、计算屏幕像素在世界空间位置的技术点。使用Built-in管线。
深度纹理

在前向渲染(Forward Rendering)中,深度信息需要我们手动获得。生成屏幕空间的深度图,需要在相机中设置DepthTextureMode.Depth。本文在主相机下进行。
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
这样,我们就可以在Shader中使用深度纹理(_CameraDepthTexture)。使用宏SAMPLE_DEPTH_TEXTURE完成采样,使用接口Linear01Depth获得范围的线性深度值。为了让深度信息可视化,使用像素深度值填充颜色缓冲区。
fixed4 frag(v2f i) : SV_Target
{
    //HLSLSupport.cginc: #define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
    float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    // Z buffer to linear 0..1 depth
    float linear01Depth = Linear01Depth(depth);
    return linear01Depth;
}离相机越近,深度值越接近0,所以越黑。



深度图

全地图扫描

原理
全地图扫描是一种由近及远(指距离相机)变化的效果。从上文深度值范围可知,由近及远就是深度值从0到1。那么,只需随时间改变扫描的深度范围(_ScanDistance),在片元着色器中,将深度范围内的像素进行处理就能呈现出扫描的效果。
Mono脚本
重写MonoBehaviour的OnRenderImage接口,将随时间变化的_ScanDistance传入Shader。
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
        if (material != null)
        {
                scanDistance = Mathf.Lerp(scanDistance, 1, Time.deltaTime * speed);
                if (scanDistance > 0.9f)
                        scanDistance = 0;
                material.SetFloat("_ScanDistance", scanDistance);
                material.SetColor("_ScanColor", scanColor);
                Graphics.Blit(source, destination, material);
        }
        else
                Graphics.Blit(source, destination);
}
Shader
将原颜色与扫描色做混合(_ScanRange控制混合范围)。
fixed4 frag(v2f i) : SV_Target
{
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    //HLSLSupport.cginc: #define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
    float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.depth_uv);
    // Z buffer to linear 0..1 depth
    float linear01Depth = Linear01Depth(depth);

    if (linear01Depth < _ScanDistance && linear01Depth < 1 && _ScanDistance - linear01Depth < _ScanRange)
    {
      float diff = 1 - (_ScanDistance - linear01Depth) / _ScanRange;
      finalColor = lerp(finalColor, _ScanColor, diff);
    }

    return finalColor;
}效果



全地图扫描

定点扫描

原理
定点扫描是指定起始点,在一定范围内呈现扫描的效果。这里说的一定范围是3D世界空间的概念,但描述离相机距离的深度图是张2D纹理,所以,定点扫描问题被抽象为如何计算屏幕像素在世界空间位置。
引用冯乐乐《入门精要》第13章
屏幕后处理的原理是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。这个四边形面片的4个顶点就对应了近裁剪平面的4个角。
我们只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对摄像机的偏移量,把它们相加就可以得到该像素的世界坐标。计算过程如下:把从相机出发,到远(近)裁剪面(远、近裁剪面都是可以的)的4个角的向量传入顶点着色器,差值后传入片元着色器,计算视空间下相机到当前像素的向量(此向量的长度是从相机到远裁剪面),然后乘以视空间下深度值,就得到了该像素相对相机的偏移,最后加上相机在世界空间的坐标,得到该像素在世界空间的位置。
Makin' Stuff Look Good 视频对上述过程介绍的比较清楚。



From Makin Stuff Look Good

Mono脚本
添加相机到远裁剪面4个角的向量的计算。参考《入门精要》代码,把近裁剪面改成了远裁剪面(自觉更好理解些)。
private Matrix4x4 CalcFrustumCornersRay()
{
        Matrix4x4 frustumCorners = Matrix4x4.identity;

        float fov = mainCamera.fieldOfView;
        float far = mainCamera.farClipPlane;
        float aspect = mainCamera.aspect;

        float halfHeight = far * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
        Vector3 toRight = cameraTransform.right * halfHeight * aspect;
        Vector3 toTop = cameraTransform.up * halfHeight;

        Vector3 topLeft = cameraTransform.forward * far + toTop - toRight;
        float scale = topLeft.magnitude / far;

        topLeft.Normalize();
        topLeft *= scale;

        Vector3 topRight = cameraTransform.forward * far + toRight + toTop;
        topRight.Normalize();
        topRight *= scale;

        Vector3 bottomLeft = cameraTransform.forward * far - toTop - toRight;
        bottomLeft.Normalize();
        bottomLeft *= scale;

        Vector3 bottomRight = cameraTransform.forward * far + toRight - toTop;
        bottomRight.Normalize();
        bottomRight *= scale;

        frustumCorners.SetRow(0, bottomLeft);
        frustumCorners.SetRow(1, bottomRight);
        frustumCorners.SetRow(2, topRight);
        frustumCorners.SetRow(3, topLeft);

        return frustumCorners;
}
Shader
v2f vert(appdata_img v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.texcoord.xy;
    o.depth_uv = v.texcoord.xy;

    //当前顶点是四边形的哪个顶点:0-bl, 1-br, 2-tr, 3-tl
    int index = 0;
        if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
      index = 0;
    else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5)
      index = 1;
    else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
      index = 2;
    else
      index = 3;

    o.interpolatedRay = _FrustumCornersRay;

    return o;
}

fixed4 frag(v2f i) : SV_Target
{
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    //view space depth
    float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.depth_uv));
    //linearDepth * i.interpolatedRay.xyz:当前像素相对摄像机的偏移
    float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

    float distanceFromCenter = distance(worldPos, _ScanCenter);
    //z = far plane
    if (distanceFromCenter < _ScanDistance && linearDepth < _ProjectionParams.z)
    {
      fixed scanPercent = 1 - (_ScanDistance - distanceFromCenter)/_ScanRange;
      finalColor = lerp(finalColor, _ScanColor, scanPercent);
    }

    return finalColor;
}效果



定点扫描

参考

chenjd:神奇的深度图:复杂的效果,不复杂的原理
joker:Unity实现地图扫描效果
冯乐乐《入门精要》
Shaders Case Study - No Man's Sky: Topographic Scanner

RhinoFreak 发表于 2022-11-30 11:04

这个好

redhat9i 发表于 2022-11-30 11:06

acecase 发表于 2022-11-30 11:13

Arzie100 发表于 2022-11-30 11:22

通俗易懂,厉害的
页: [1]
查看完整版本: Unity Shader学习:扫描(Scan)