pc8888888 发表于 2022-11-25 13:19

《Unity Shader入门精要》笔记(三十一)

本文为《Unity Shader入门精要》第十五章《使用噪声》的第三节内容《再谈全局雾效》。
本文相关代码,详见:

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

在13.3节我们曾实现过基于屏幕后处理的全局雾效,由深度纹理重建每个像素在世界空间下的位置,再使用基于高度的公式来计算雾效的混合系数,最后使用该系数来混合雾的颜色和原屏幕颜色。本节我们将在此基础上,使用噪声纹理让雾动起来。
关于雾效的原理,这里不再赘述,感兴趣的小伙伴可以移步至:《Unity Shader入门精要》笔记(二十五)》。

2. 案例:让全局雾效动起来

2.1 效果预览

本案例最终实现效果如下:



2.2 准备工作

完成如下准备工作:

[*]新建名为Scene_15_3的场景,并去掉天空盒;
[*]搭建雾效测试场景,3面墙的房间,放置几个立方体,详情可参考文章开头链接中的工程;
[*]新建名为FogWithNoise的C#脚本,并拖拽给场景中的相机;
[*]新建名为Chapter15-FogWithNoise的Unity Shader;
[*]保存场景。

2.3 编写C#脚本:FogWithNoise

编写代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 集成后处理基类
public class FogWithNoise : 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;
      }
    }

    // 获取相机在世界空间下的前方、上方和右方等方向
    private Transform myCameraTransform;
    public Transform cameraTransform
    {
      get
      {
            if (myCameraTransform == null) myCameraTransform = camera.transform;
            return myCameraTransform;
      }
    }
   
    // 雾效浓度
   
    public float fogDensity = 1.0f;
    // 雾效颜色
    public Color fogColor = Color.white;

    // 雾效起点高度
    public float fogStart = 0.0f;
    // 雾效终点高度
    public float fogEnd = 2.0f;

    // 雾效噪声纹理
    public Texture noiseTexture;
    // 雾效噪声纹理x轴向移动速度
   
    public float fogXSpeed = 0.1f;
    // 雾效噪声纹理y轴向移动速度
   
    public float fogYSpeed = 0.1f;
    // 噪声纹理影响程度
   
    public float noiseAmount = 1.0f;

    void OnEnable()
    {
      // 启用深度纹理
      camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
      if (material == null)
      {
            Graphics.Blit(src, dest);
            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.SetFloat("_FogDensity", fogDensity);
      material.SetColor("_FogColor", fogColor);
      material.SetFloat("_FogStart", fogStart);
      material.SetFloat("_FogEnd", fogEnd);

      material.SetTexture("_NoiseTex", noiseTexture);
      material.SetFloat("_FogXSpeed", fogXSpeed);
      material.SetFloat("_FogYSpeed", fogYSpeed);
      material.SetFloat("_NoiseAmount", noiseAmount);

      Graphics.Blit(src, dest, material);
    }
}

2.4 编写Shader代码:Chapter15-FogWithNoise

编写如下代码:
Shader "Unity Shaders Book/Chapter 15/Fog With Noise"
{
    Properties
    {
      _MainTex ("Base (RGB)", 2D) = "white" {}
      // 雾效浓度
      _FogDensity ("Fog Density", Float) = 1.0
      // 雾效颜色
      _FogColor ("Fog Color", Color) = (1.0, 1.0, 1.0, 1.0)
      // 雾效起始高度
      _FogStart ("Fog Start", Float) = 0.0
      // 雾效终点高度
      _FogEnd ("Fog End", Float) = 1.0
      // 噪声纹理
      _NoiseTex ("Noise Texture", 2D) = "white" {}
      // 噪声纹理x轴向速度
      _FogXSpeed ("Fog Horizontal Speed", Float) = 0.1
      // 噪声纹理y轴向速度
      _FogYSpeed ("Fog Vertical Speed", Float) = 0.1
      // 噪声纹理的影响程度
      _NoiseAmount ("Noise Amount", Float) = 1.0
    }

    SubShader
    {
      CGINCLUDE

      #include "UnityCG.cginc"

      // 相机到深度纹理四个角的4个方向,
      // 因为是脚本中传参,所以不需要在Properties中定义
      float4x4 _FrustumCornersRay;

      sampler2D _MainTex;
      half4 _MainTex_TexelSize;
      // 相机看到的法线纹理
      sampler2D _CameraDepthTexture;
      half _FogDensity;
      fixed4 _FogColor;
      float _FogStart;
      float _FogEnd;
      sampler2D _NoiseTex;
      half _FogXSpeed;
      half _FogYSpeed;
      half _NoiseAmount;

      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;

            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;

            // 基于时间变量和噪声纹理的偏移速度,得到噪声纹理的偏移量
            float2 offset = _Time.y * float2(_FogXSpeed, _FogYSpeed);
            // 对噪声纹理的uv值偏移后进行采样(只取r分量),减去0.5后(为了让数值有正有负,有加强有衰减),
            // 并乘以噪声纹理影响强度得到噪声值
            float noise = (tex2D(_NoiseTex, i.uv + offset).r - 0.5) * _NoiseAmount;

            // 根据当前片元的高度以及雾效高度范围得到当前片元雾效的浓度比值
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
            // 将浓度比值乘以总的浓度,加上噪声的干扰,得到最终的雾效浓度
            fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));

            // 对主纹理进行采样
            fixed4 finalColor = tex2D(_MainTex, i.uv);
            // 通过雾效浓度,在原始颜色和雾效颜色之间进行差值,得到最终颜色
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

            return finalColor;
      }

      ENDCG

      Pass
      {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            ENDCG
      }
    }

    Fallback Off
}

2.5 配置雾效脚本

将Chapter15-FogWithNoiseUnity Shader拖到FogWithNoise脚本的Fog Shader属性值中,并配置雾效相关参数如下:


噪声纹理可从文章开头给出链接的工程中找到。

3. 本章扩展

噪声纹理由计算机利用某些算法生成的,可以被认为是一种程序纹理(Procedure Texture),最常使用的噪声纹理有2类:Perlin纹理、Worley纹理。

Perlin噪声可以用于生成更自然的噪声纹理;
Worley噪声则通常用于模拟诸如石头、水、纸张等多孔噪声。

扩展阅读:

[*]Perlin噪声:https://adrianb.io/2014/08/09/perlinnoise.html

以上是本次笔记的所有内容,下一篇笔记我们将开启新的篇章,学习《渲染优化》的相关技术。

<hr/>
写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(三十一)