找回密码
 立即注册
查看: 199|回复: 0

《Unity Shader入门精要》笔记(二十五)

[复制链接]
发表于 2022-11-13 14:24 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第十三章《使用深度和法线纹理》的第三节内容《全局雾效》。

本文相关代码,详见:
原书代码,详见原作者github:
<hr/>1. 概念原理

1.1 Unity内置的雾效

在Shader中添加#pragma multi_compile_fog指令,需要用到相关的内置宏:UNITY_FOG_COORDS、UNITY_TRANSFER_FOG、UNITY_APPLY_FOG。
缺点:

  • 需要所有物体添加相关的代码
  • 实现的效果有限

1.2 全局雾效

本节将学习基于屏幕后处理的全局无效的实现,无需修改场景内物体的Shader代码,仅需要一次后处理。
优点:

  • 自由度高

1.3 全局雾效实现原理

关键:根据深度纹理来重建每个像素在世界空间下的位置。
(上一节运动模糊中实现过,但运算是在片元着色器中实现的,影响性能)

快速从深度纹理中重构世界坐标的方法:

  • 对图像空间下的视椎体射线进行插值
  • (视椎体射线:从相机触发,指向图像上的某点的射线,它存储了该像素在世界空间下到摄像机的方向信息)
  • 把射线和线性化后的视角空间下的深度值相乘,加上相机的世界位置,得到该像素在世界空间下的位置

1.3.1 重建世界坐标

思想:坐标系的一个顶点可以通过它相对的另一个坐标的偏移量求得。

像素的世界坐标 = 相机的世界坐标 + 像素相对于相机的偏移量:
// _WorldSpaceCameraPos:内置变量,相机在世界空间下的位置
// linearDepth:由深度纹理得到的线性深度值
// interpolatedRay:由顶点着色器输出并插值后得到的射线,包含方向和距离信息
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
interpolatedRay的求法:
源于对近裁剪平面的4个角的某个特定向量的插值(包含他们到相机的方向和距离信息),可利用相机的近裁剪平面、FOV、横纵比计算得到。



计算toTop和toRight:
// Near近裁剪平面的距离
// FOV 竖直方向的视角范围
halfHeight = Near X tan(FOV / 2)
// camera.up:相机正上方
toTop = camera.up X halfHeight
// camera.right:相机正右方
toRight = camera.right X halfHeight · aspect
计算4个角相对相机的方向:
TL = camera.forward · Near + toTop - toRight
TR = camera.forward · Near + toTop + toRight
BL = camera.forward · Near - toTop - toRight
BR = camera.forward · Near - toTop + toRight



注意:线性深度值并非点到相机欧式距离,而是z方向上的距离,不能直接使用深度值和4个角的单位方向的乘积来计算它们到相机的偏移量。

线性深度实现思路:
相似三角形原理,相机的深度值 / 它到相机的实际距离 = 近裁剪平面的距离 / TL(或TR、BL、BR)向量的模。
depth / dist = Near / |TL|
因此得到TL距离摄像机的欧氏距离dist:
dist = |TL| / Near X depth

因4个点相互对称,其他3个向量的模和TL相等,所以它们可共用同一个因子和单位向量相乘,得到对应的向量值:
scale = |TL| / Near

RayTL = TL / |TL| X scale
RayTR = TR / |TR| X scale
RayBL = BL / |BL| X scale
RayBR = BR / |BR| X scale

屏幕后处理原理:
使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。


  • 这个四边形的4个订单对应了近裁剪平面的4个角,因此可以把上面的计算结果传递给顶点着色器
  • 顶点着色器根据当前的位置选择它对应的向量,然后将其输出
  • 经插值后传递给片元着色器,得到interpolatedRay
  • 利用本小节最开始的公式重建该像素在世界空间下的位置

1.3.2 雾的计算

一个无效系数f:作为混合原始颜色和雾的颜色的混合系数。
float3 afterFog = f * fogColor + (1 - f) * originColor;

Unity内置的雾效实现支持3种计算方式:线性、指数、指数的平方。
雾效系数f的计算公式对应分别是:
// -- 线性 --
// dMin、dMax分别表示受雾影响的最小距离、最大距离
f = (dMax - |z|) / (dMax - dMin);

// -- 指数 --
// d:控制雾的浓度的参数
f = e ^ (-d · |z|)

// -- 指数的平方 --
// d:控制雾的浓度的参数
f = e ^ ((-d · |z|) ^ 2)

接下来使用类似线性雾的计算方式,计算基于高度的雾效:
// hEnd、hStart分别表示受雾影响的起始高度和终止高度
f = (hEnd - y) / (hEnd - hStart)

2. 案例:基于高度的全局雾效

2.1 效果预览

本案例将实现如下效果:



2.2 准备工作

完成如下准备工作:

  • 新建名为Scene_13_3的场景,并去掉天空盒子;
  • 搭建一个雾效场景,构建包含3面墙的房间,并放置几个立方体,详细可参考Git工程里对应的场景布置;
  • 拖拽一个控制相机不断围绕一个中心运动的脚本Translating.cs到相机组件,详细可从工程中找;
  • 新建一个名为FogWithDepthTexture的C#脚本,并拖拽给相机;
  • 新建一个名为Chapter13-FogWithDepthTexture的Unity Shader,并拖拽到上一步创建的脚本里的Fog Shader属性;
  • 保存场景。

2.3 编写C#脚本:FogWithDepthTexture

编写脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FogWithDepthTexture : PostEffectsBase
{
    // 雾效Shader
    public Shader fogShader;
    // 雾效材质
    private Material fogMaterial = null;
    public Material material
    {
        get
        {
            fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
            return fogMaterial;
        }
    }

    // 需要借助相机获取相关参数,如:近裁剪平面、FOV等
    private Camera myCamera;
    public Camera camera
    {
        get
        {
            if (myCamera == null) myCamera = GetComponent<Camera>();
            return myCamera;
        }
    }

    // 相机Transform
    private Transform myCameraTransform;
    public Transform cameraTransform
    {
        get
        {
            if (myCameraTransform == null) myCameraTransform = camera.transform;
            return myCameraTransform;
        }
    }

    // 雾效密度
    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;
    // 雾效颜色
    public Color fogColor = Color.white;
    // 雾效起始高度
    public float fogStart = 0.0f;
    // 雾效终止高度
    public float fogEnd = 2.0f;

    private void OnEnable()
    {
        // 为了在Shader中获取深度纹理
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material == null)
        {
            Graphics.Blit(source, destination);
            return;
        }

        // 用4X4的矩阵记录4个角的位置
        Matrix4x4 frustumCorners = Matrix4x4.identity;

        // 相机参数
        float fov = camera.fieldOfView;
        float near = camera.nearClipPlane;
        float aspect = camera.aspect;

        // 近裁剪平面一半的高度
        float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
        // 近裁剪平面中心点向右的向量,包含近裁剪平面一半的宽度距离
        Vector3 toRight = cameraTransform.right * halfHeight * aspect;
        // 近裁剪平面中心点向上的向量,包含近裁剪平面一半的高度距离
        Vector3 toTop = cameraTransform.up * halfHeight;

        // 左上角顶点的位置
        Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
        // 近似三角形原理中使用的公式的比例系数
        float scale = topLeft.magnitude / near;
        topLeft.Normalize();
        topLeft *= scale;

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

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

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

        // 将四个角的向量存储到矩阵中
        frustumCorners.SetRow(0, bottomLeft);
        frustumCorners.SetRow(1, bottomRight);
        frustumCorners.SetRow(2, topRight);
        frustumCorners.SetRow(3, topLeft);

        // 往Shader中传入属性的值
        material.SetMatrix("_FrustumCornersRay", frustumCorners);
        material.SetMatrix("_ViewProjectionInverseMatrix", (camera.projectionMatrix * camera.worldToCameraMatrix).inverse);

        material.SetFloat("_FogDensity", fogDensity);
        material.SetColor("_FogColor", fogColor);
        material.SetFloat("_FogStart", fogStart);
        material.SetFloat("_FogEnd", fogEnd);

        Graphics.Blit(source, destination, material);
    }
}

2.4 编写Shader:Chapter13-FogWithDepthTexture

编写代码如下:
Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        // 雾效最大密度
        _FogDensity ("Fog Density", Float) = 1.0
        // 雾效颜色
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        // 雾效起点高度
        _FogStart ("Fog Start", Float) = 0.0
        // 雾效终点高度
        _FogEnd ("Fog End", Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE

        #include "UnityCG.cginc"

        // 记录相机到画面四个角的向量
        // (没有在Properties里声明,但是可以通过脚本传递到Shader)
        float4x4 _FrustumCornersRay;
        
        sampler2D _MainTex;
        // 主纹理的纹素大小
        half4 _MainTex_TexelSize;
        // 深度纹理
        sampler2D _CameraDepthTexture;
        // 雾效最大密度
        half _FogDensity;
        // 雾效颜色
        fixed4 _FogColor;
        // 雾效起点高度
        float _FogStart;
        // 雾效终点高度
        float _FogEnd;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0;
            half2 uv_depth : TEXCOORD1;
            float4 interpolatedRay : TEXCOORD2;
        };

        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            // 深度纹理UV
            o.uv_depth = v.texcoord;

            // 兼容不同平台
            // 通常Unity会对DirectX、Metal这样的平台(他们的(0,0)在左上角)的图像进行翻转,
            // 但如果这些平台开启了抗锯齿,Unity不会进行翻转
            // 所以我们需要根据纹素的正负来判定图像是否需要翻转
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y;
            #endif

            // 使用索引值,来获取_FrustCornersRay中对应的行作为该顶点的interpolatedRay值
            int index = 0;
            float x = v.texcoord.x;
            float y = v.texcoord.y;
            // 一般使用if会造成比较大的性能问题,但本案例中用到的模型是一个四边形网格,质保函4个顶点,所以影响不大
            if (x < 0.5 && y < 0.5)
                index = 0;
            else if (x > 0.5 && y < 0.5)
                index = 1;
            else if (x > 0.5 && y > 0.5)
                index = 2;
            else
                index = 3;
            
            // 兼容不同平台
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                index = 3 - index;
            #endif

            o.interpolatedRay = _FrustumCornersRay[index];

            return o;
        }

        fixed4 frag(v2f i) : SV_Target
        {
            // 对深度纹理进行采样,并转化为视角空间下的线性深度值
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            // 基于线性深度值,得到当前像素相对于相机位置的偏移,以此得到当前像素的世界位置
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

            // 基于y分量计算得到归一化的雾效密度
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
            // 与雾效最大密度相乘,得到实际的雾效密度
            fogDensity = saturate(fogDensity * _FogDensity);

            // 主纹理采样
            fixed4 finalColor = tex2D(_MainTex, i.uv);
            // 根据雾效密度,在实际颜色和雾效颜色之间进行差值
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

            return finalColor;
        }

        ENDCG

        // 雾效Pass
        Pass
        {
            ZTest Always
            Cull Off
            ZWrite Off

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            ENDCG
        }
    }

    Fallback Off
}

2.5 参数配置

配置好脚本必要的参数,即可得到最终的效果:



3. 注意

本案例基于透视投影的相机进行实现,若是正交投影的相机,计算方式会有不同。原作者给出了文章链接,但已经失效,感兴趣的小伙伴可自行查阅资料。

以上是本次笔记的所有内容,下一篇笔记,我们将学习《再谈边缘检测》的相关知识。

<hr/>
写在最后


本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-30 06:10 , Processed in 0.114966 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表