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

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

[复制链接]
发表于 2022-8-30 09:59 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第十二章《屏幕后处理效果》的第4节内容《高斯模糊》。

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

常见的使用卷积的模糊有:均值模糊、中值模糊。
均值模糊:使用的卷积核中各个元素都相等,且相加等于1(卷积后得到的像素值是其邻域内各个像素值的平均值)。
中值模糊:选择邻域内所有像素排序后的中值替换掉原颜色。
另一种更高级的模糊是高斯模糊
1.1 高斯滤波

高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素都是基于下面的高斯方程:



  • σ:标准方差(一般取值为1);
  • x、y:分别对应当前位置到卷积核中心的整数距离;
要构建高斯核,只需要计算高斯核中各个位置对应的高斯值。
为了保证滤波后的图像不会变暗,需要对高斯核中的权重进行归一化,即:让每一个权重除以所有权重的和,以保证所有权重的和为1,因此高斯函数中的e前面的系数实际不会对结果有任何影响。
1.2 性能优化

高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度——距离越近,影响越大。
高斯核的维数越高,模糊程度越大。
使用N x N的高斯核对图像进行卷积滤波,需要进行N x N x W x H(W、H分别是图像的宽和高)次纹理采样,N不断增大,采样次数会变得很大。

幸运的是,这个二维高斯函数可以拆分成两个一维函数:
使用两个一维的高斯核先后对图像进行滤波,得到的结果和直接使用二位高斯核是一样的,但采样次数只需要2 x N x W x H。

一个5 x 5的高斯核,左图显示了标准方差为1的高斯核权重分布,右图显示了拆分成两个一维的高斯核:


对于一个大小为5的一维高斯核,只需要记录3个权重值即可(左右权重对称相等)。
2. 案例:使用两个一维高斯核实现高斯模糊效果

2.1 效果

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



左:原图效果,右:高斯模糊后的效果

2.2 案例说明

本案例将使用5 x 5的高斯核对原图像进行高斯模糊,调用两个Pass,第一个Pass在竖直方向上使用一维高斯核对图像进行滤波,第二个Pass在水平方向上使用一维高斯核对图像进行滤波,最终得到目标图像。
实现中,对图像进行缩放,以进一步提高性能,并调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
2.3 准备工作

完成如下准备工作:

  • 新建名为Scene_12_4的场景,并去掉天空盒子;
  • 导入一张图片(案例图路径:Assets/Textures/Chap12/sakura1),调整图片纹理类型为Sprite (2D and UI),并拖拽到场景中,使其生成一个Sprite,调整其位置、朝向,使其正对相机;
  • 新建名为GaussianBlur的C#脚本,将脚本拖拽到相机;
  • 新建名为Chapter12-GaussianBlur的Unity Shader;
  • 保存场景。
2.4 编写C#脚本

打开GaussianBlurC#脚本,编写如下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 继承后处理基类
public class GaussianBlur : PostEffectsBase
{
    // 高斯模糊使用的Shader
    public Shader gaussianBlurShader;

    // 高斯模糊使用的材质
    private Material gaussianBlurMaterial = null;
    public Material material
    {
        get
        {
            gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
            return gaussianBlurMaterial;
        }
    }

    // 控制高斯模糊的程度
    [Range(0, 4)]
    public int iterations = 3;

    // 性能考虑:控制采样间距
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;

    // 性能考虑:纹理降采样(详见OnRenderImage逻辑)
    [Range(1, 8)]
    public int downSample = 2;

    // 高斯模糊后处理(普通版本)
    //private void OnRenderImage(RenderTexture source, RenderTexture destination)
    //{
    //    if (material != null)
    //    {
    //        int width = source.width;
    //        int height = source.height;

    //        // 分配一个和屏幕图像大小相同的缓存纹理
    //        RenderTexture buffer = RenderTexture.GetTemporary(width, height, 0);

    //        // 先使用第一个Pass进行纵向模糊,渲染到临时纹理中
    //        Graphics.Blit(source, buffer, material, 0);
    //        // 再使用第二个Pass进行横向模糊,从临时纹理渲染到最终的屏幕上
    //        Graphics.Blit(buffer, destination, material, 1);

    //        // 释放之前分配的缓存
    //        RenderTexture.ReleaseTemporary(buffer);
    //    }
    //    else
    //    {
    //        Graphics.Blit(source, destination);
    //    }
    //}

    // 高斯模糊后处理(性能优化版1)
    //private void OnRenderImage(RenderTexture source, RenderTexture destination)
    //{
    //    if (material != null)
    //    {
    //        // 对屏幕像素进行降采样
    //        int width = source.width / downSample;
    //        int height = source.height / downSample;

    //        // 分配降采样后的临时缓存
    //        RenderTexture buffer = RenderTexture.GetTemporary(width, height, 0);
    //        // 双线性过滤,防止降采样后的纹理像素看上去不连贯,丢失严重
    //        buffer.filterMode = FilterMode.Bilinear;

    //        // 先使用第一个Pass进行纵向模糊,渲染到临时纹理中
    //        Graphics.Blit(source, buffer, material, 0);
    //        // 再使用第二个Pass进行横向模糊,从临时纹理渲染到最终的屏幕上
    //        Graphics.Blit(buffer, destination, material, 1);

    //        // 释放之前分配的缓存
    //        RenderTexture.ReleaseTemporary(buffer);
    //    }
    //    else
    //    {
    //        Graphics.Blit(source, destination);
    //    }
    //}

    // 高斯模糊后处理(性能优化版2)
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material != null)
        {
            // 对屏幕像素进行降采样
            int width = source.width / downSample;
            int height = source.height / downSample;

            // 分配降采样后的临时缓存
            RenderTexture buffer0 = RenderTexture.GetTemporary(width, height, 0);
            // 双线性过滤,防止降采样后的纹理像素看上去不连贯,丢失严重
            buffer0.filterMode = FilterMode.Bilinear;
            // 简单降采样后,将图像渲染到临时缓存
            Graphics.Blit(source, buffer0);

            // 进行多次高斯模糊操作
            for (int i = 0; i < iterations; i++)
            {
                // 动态设置高斯核邻域的采样间距,每次高斯模糊采样间距逐渐加大
                material.SetFloat("_BlurSize", 1.0f + i * blurSpread);

                RenderTexture buffer1 = RenderTexture.GetTemporary(width, height, 0);
                Graphics.Blit(buffer0, buffer1, material, 0);

                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(width, height, 0);

                Graphics.Blit(buffer0, buffer1, material, 1);

                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }

            Graphics.Blit(buffer0, destination);
            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }
}
2.5 编写Shader代码

打开Chapter12-GaussianBlurUnity Shader,编写如下代码:
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur"
{
    Properties
    {
        // 主纹理
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _BlurSize ("Blur Size", Float) = 1.0
    }

    SubShader
    {
        // 定义可复用的代码块,可在不同Pass中调用,类似C++中头文件的功能
        CGINCLUDE

        #include "UnityCG.cginc"

        sampler2D _MainTex;
        // 主纹理的纹素大小(例如:一张512 * 512的纹理,纹素大小为1/512)
        // 利用纹素,做相邻区域内纹理采样时,计算各相邻区域的纹理坐标
        half4 _MainTex_TexelSize;
        float _BlurSize;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;
        };

        // 通过把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能
        // 由于从顶点着色器到片元着色器的差值是线性的,因此这样的转移并不会影响纹理坐标的计算结果
        v2f vertBlurVertical(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            // 构建一维的纵向高斯核
            half2 uv = v.texcoord;
            o.uv[0] = uv;
            // _BlurSize控制邻域像素之间的采样距离,_BlurSize值越大,模糊程度越高,但采样数不会受到影响
            // 但过大的_BlurSize值会造成虚影
            o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
            o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;

            return o;
        }

        v2f vertBlurHorizontal(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            // 构建一维的纵向高斯核
            half2 uv = v.texcoord;
            o.uv[0] = uv;
            // _BlurSize控制邻域像素之间的采样距离,_BlurSize值越大,模糊程度越高,但采样数不会受到影响
            // 但过大的_BlurSize值会造成虚影
            o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
            o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
            o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
            o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;

            return o;
        }

        fixed4 fragBlur(v2f i) : SV_Target
        {
            // 只需要记录3个高斯权重
            float weight[3] = {0.4026, 0.2442, 0.0545};
            // 权重和
            fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];

            for (int it = 1; it < 3; it++)
            {
                sum += tex2D(_MainTex, i.uv[it]).rgb * weight[it];
                sum += tex2D(_MainTex, i.uv[2 * it]).rgb * weight[it];
            }

            return fixed4(sum, 1.0);
        }

        ENDCG

        // 后处理“标配”
        ZTest Always
        Cull Off
        ZWrite Off

        Pass
        {
            // 为Pass定义名字,可以在其他Shader中直接通过这个名字使用该Pass,而无需再重复编写代码
            NAME "GAUSSIAN_BLUR_VERTICAL"

            CGPROGRAM

            // 使用代码块定义的函数作为顶点、片元着色函数
            #pragma vertex vertBlurVertical
            #pragma fragment fragBlur

            ENDCG
        }

        Pass
        {
            // 为Pass定义名字,可以在其他Shader中直接通过这个名字使用该Pass,而无需再重复编写代码
            NAME "GAUSSIAN_BLUR_HORIZONTAL"

            CGPROGRAM

            // 使用代码块定义的函数作为顶点、片元着色函数
            #pragma vertex vertBlurHorizontal
            #pragma fragment fragBlur

            ENDCG
        }
    }

    Fallback Off
}
2.6 配置脚本

将编写好的Shader文件拖拽到C#组件的Gaussian Blur Shader属性中:



运行场景后,调整脚本的参数可以得到不同的模糊效果:



以上是本次笔记的所有内容,下一篇笔记我们将学习《Bloom效果》的相关知识。

<hr/>
写在最后

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

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-9-22 01:04 , Processed in 0.090907 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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