【Unity Shader入门精要 第12章】屏幕后处理效果(二)

1. 卷积

在图像处理中,卷积操作就是使用一个卷积核对一张图像中的每个像素做一系列的操作。

卷积核通常是一个四方形网格结构,如2x2、3x3的方形区域,该区域内每个方格都有一个权重值。

当对图像中的某个像素进行卷积操作时,将卷积核的中心放置于要处理的像素上,按照卷积核每个格子中的权重值,将卷积核覆盖到的像素的进行加权计算,得到最终结果。

下图为用一个3x3的卷积核对原始图像中A像素进行卷积操作时的示意:

在这里插入图片描述

2. 边缘检测

在这个例子中,我们实现一个简单边缘检测效果。使用的方法是,通过边缘检测算子直接对屏幕图像进行卷积操作,基于卷积结果判断某像素是否位于边缘上。

边缘检测算子就是一个预先定义好的卷积核,本次使用的为Sobel算子,如下:

在这里插入图片描述
原理为对于每个像素,按照算子的范围,计算该像素左右和上下一定范围内的相邻像素的灰度值差异大小——也就是卷积的结果,我们称之为梯度。梯度越大,代表像素两侧的灰度值差异越大,当前像素就越可能是某个边缘上的像素。

因此,在片元着色器中,对于每个处理中的片元,需要采样该片元相邻的9个像素,并按照算子中的权重值计算最终梯度。为了减少在片元着色器中逐像素计算uv偏移的消耗,我们把这个计算uv偏移的过程移到了顶点着色器中,在 v2f 结构中通过一个长度为9的uv数组传递给片元着色器。

在计算得到梯度值之后,按照梯度值在原始采样颜色和边缘颜色之间进行插值,完成描边效果。

另外,也提供了一个只显示描边的展示效果,通过 _OnlyEdge 变量控制是否只显示描边,通过 _BackgroundColor 变量控制当只显示描边时非边缘区域的颜色。

最后,实现新增一个 PostEffect_EdgeDetection 脚本用于调用描边处理效果,继承自前文的后处理父类 PostEffectBase,并在 OnRenderImage 方法中为Shader中的变量赋值及调用Blit方法。

测试脚本如下:

using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffect_EdgeDetection : PostEffectBase
{
    public Shader EdShader;
    public Material EdMat;
    
    public Color EdgeColor = Color.black;
    public Color BackgroundColor = Color.white;
    [Range(0.0f, 1.0f)]
    public float OnlyEdge = 0f;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Material _mat = CheckShaderAndMaterial(EdShader, EdMat);
        if (null == _mat) Graphics.Blit(src, dest);
        else
        {
            _mat.SetColor("_EdgeColor", EdgeColor);
            _mat.SetColor("_BackgroundColor", BackgroundColor);
            _mat.SetFloat("_OnlyEdge", OnlyEdge);
            
            Graphics.Blit(src, dest, _mat);
        }
    }
}

测试Shader如下:

Shader "MyShader/Chapter_12/Chapter_12_EdgeDetection_Shader"
{
    Properties
    {
        _MainTex("MainTex", 2D) = "white"{}
    }
    SubShader
    {
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            ZTest Always
            ZWrite Off
            Cull Off
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv[9] : TEXCOORD0;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_TexelSize;
            fixed4 _EdgeColor;
            fixed _OnlyEdge;
            fixed4 _BackgroundColor;
            
            v2f vert(appdata_img v)
            {
               v2f o;
               o.pos = UnityObjectToClipPos(v.vertex);
               half2 _uv = v.texcoord;
               //计算sobel算子覆盖范围的uv偏移
               o.uv[0] = _uv + _MainTex_TexelSize.xy * half2(-1, -1);
               o.uv[1] = _uv + _MainTex_TexelSize.xy * half2(0, -1);
               o.uv[2] = _uv + _MainTex_TexelSize.xy * half2(1, -1);
               o.uv[3] = _uv + _MainTex_TexelSize.xy * half2(-1, 0);
               o.uv[4] = _uv + _MainTex_TexelSize.xy * half2(0, 0);
               o.uv[5] = _uv + _MainTex_TexelSize.xy * half2(1, 0);
               o.uv[6] = _uv + _MainTex_TexelSize.xy * half2(-1, 1);
               o.uv[7] = _uv + _MainTex_TexelSize.xy * half2(0, 1);
               o.uv[8] = _uv + _MainTex_TexelSize.xy * half2(1, 1);
               return o;
            }
            
            fixed Luminance(fixed4 _color)
            {
                return 0.2125 * _color.r + 0.7154 * _color.g + 0.0721 * _color.b;
            }
            
            half Sobel(v2f i)
            {
                //Sobel算子
                const half Gx[9] = {-1, -2, -1,
                                     0,  0,  0,
                                     1,  2,  1};
                const half Gy[9] = {-1,  0,  1,
                                    -2,  0,  2,
                                    -1,  0,  1};
                
                fixed _luminance;
                half _edgeX = 0;
                half _edgeY = 0;
                for(int it = 0; it < 9; it++)
                {
                    _luminance = Luminance(tex2D(_MainTex, i.uv[it]));
                    _edgeX += _luminance * Gx[it];
                    _edgeY += _luminance * Gy[it];
                }
                
                //差异越大,值越小,越可能是边界
                return 1 - abs(_edgeX) - abs(_edgeY);
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                half _edge = Sobel(i);
                fixed4 _samplerColor = tex2D(_MainTex, i.uv[4]);
                fixed4 _withEdgeColor = lerp(_EdgeColor, _samplerColor, _edge);
                fixed4 _onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, _edge);
                return lerp(_withEdgeColor, _onlyEdgeColor, _OnlyEdge);
            }
            
            ENDCG
        }
    }
}

效果如下:
在这里插入图片描述

3. 高斯模糊

3.1 高斯核

常见的几种通过卷积操作实现模糊效果的方式:

  • 均值模糊:通过一个各项权值相等且经过归一化处理的卷积核对图像进行卷积,也就是用像素周围一定范围相邻像素的均值作为颜色
  • 中值模糊:将相邻像素进行排序,用中值作为颜色
  • 高斯模糊:通过高斯核对图像进行卷积操作,高斯核中每项的权值通过高斯方程获得(归一化处理)

使用的高斯方程:
在这里插入图片描述
其中:

  • σ是标准方差,通常取1
  • xy分别代表横纵坐标到当前像素的整数距离

从上述公式可以看出,当σ为1时,只有xy会影响最终的模糊效果:距离越大影响越大、维度越高模糊程度越高。

由于高斯核中的权重都是由该公式求出的,因此只要确定了高斯核的维度,则其中的每一项就已经固定了,比如下面的例子中使用的5x5高斯核为:
在这里插入图片描述

3.2 优化手段

拆分高斯核

对于一个 WxH 的纹理,如果用 NxN 的高斯核直接进行滤波,则采样次数为:NxNxWxH

可以将高斯核拆分成一横一纵两个一维的高斯核,用拆分后的高斯核分别进行滤波,则采样次数就可以降低为:2xNxWxH

注意,是经过两个PASS分别滤波,也就是先用横向的高斯核对原始纹理进行滤波得到中间纹理,再用纵向的高斯核对中间纹理进行滤波得到最终的效果。并不是在一个PASS中同时用横纵高斯核进行处理,因为这样横纵高斯核采样的都是原始纹理的颜色。

降采样

由于卷积操作是在片元着色器中对纹理中的每个像素进行处理,因此,纹理的像素数量(或者说纹理的尺寸)就直接影响到计算的次数

我们可以先按照一定的比例对原始的纹理进行降采样处理,生成一个尺寸较小的临时纹理,然后对临时纹理进行高斯滤波,这样可以大大降低计算的数量。

扩大步长

要达到更好的模糊效果,最只管的方式是提高高斯核的维度,但是提高维度也意味着增加计算量。基于上文对高斯方程的分析,XY的范围越大影响越大,因此,在不提高维度的情况下,要获得更好的模糊效果,我们还可以增加XY的采样步长,也就是扩大XY的范围。

增加迭代次数

另外,还可以设置进行滤波的迭代次数,这样可以动态修改迭代次数,在满足性能要求的前提下,获得更好的模糊效果。

3.3 示例

下面的例子中,我们将5X5的高斯核拆分成如下两个一维的高斯核:
在这里插入图片描述
观察拆分后的高斯核,可以发现无论横纵,高斯权值都是对称的,且横纵高斯核中相同距离的权值相等,因此在Shader中只需要用一个长度为3的数组就可以表达拆分后的高斯核。

测试脚本

using UnityEngine;

public class PostEffect_GaussianBlur : PostEffectBase
{
    public Shader GaussianBlurShader;
    public Material GaussianBlurMat;

    [Range(1, 8)]
    public float DownSampler = 1;
    [Range(0.2f, 3f)]
    public float SmaplerStep = 1;
    [Range(1, 4)]
    public float BlurRound = 1;
    
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Material _mat = CheckShaderAndMaterial(GaussianBlurShader, GaussianBlurMat);
        if(null == _mat) Graphics.Blit(src, dest);
        else
        {
            int _width = (int)(src.width / DownSampler);
            int _height = (int)(src.height / DownSampler);
            RenderTexture _buffer_0 = RenderTexture.GetTemporary(_width, _height, 0);
            _buffer_0.filterMode = FilterMode.Bilinear;
            //原始图像降采样至 _buffer_0,并保持后续每次操作中 _buffer_0 都是待处理的图像
            Graphics.Blit(src, _buffer_0);
            
            for (int i = 0; i < BlurRound; i++)
            {
                _mat.SetFloat("_SmaplerStep", SmaplerStep * i);
                
                //横向滤波
                RenderTexture _buffer_1 = RenderTexture.GetTemporary(_width, _height, 0);
                Graphics.Blit(_buffer_0, _buffer_1, _mat, 0);
                RenderTexture.ReleaseTemporary(_buffer_0);
                _buffer_0 = _buffer_1;
                
                //纵向滤波
                _buffer_1 = RenderTexture.GetTemporary(_width, _height, 0);
                Graphics.Blit(_buffer_0, _buffer_1, _mat, 1);
                RenderTexture.ReleaseTemporary(_buffer_0);
                _buffer_0 = _buffer_1;
            }
            
            //将最终结果输出
            Graphics.Blit(_buffer_0, dest);
            RenderTexture.ReleaseTemporary(_buffer_0);
        }
    }
}

测试Shader

Shader "MyShader/Chapter_12/Chapter_12_GaussianBlur_Shader"
{
    Properties
    {
        _MainTex("MainTex", 2D) = "white"{}
    }
    SubShader
    {
        ZTest Always ZWrite Off Cull Off
    
        //预定义着色器函数,可以在后续的Pass中直接调用
        //避免在多个Pass中重复实现同样的代码
        CGINCLUDE
        #include "UnityCG.cginc"
        
        sampler2D _MainTex;
        float4 _MainTex_TexelSize;
        half _SmaplerStep;
        
        struct v2f
        {
            float4 pos : SV_POSITION;
            float2 uv[5] : TEXCOORD0;
        };
        
        //横向滤波顶点着色函数
        v2f vertHorizental(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv[0] = v.texcoord;
            o.uv[1] = v.texcoord + _MainTex_TexelSize.xy * float2(-1, 0) * _SmaplerStep;
            o.uv[2] = v.texcoord + _MainTex_TexelSize.xy * float2(1, 0) * _SmaplerStep;
            o.uv[3] = v.texcoord + _MainTex_TexelSize.xy * float2(-2, 0) * _SmaplerStep;
            o.uv[4] = v.texcoord + _MainTex_TexelSize.xy * float2(2, 0) * _SmaplerStep;
            return o;
        }
        
        //纵向滤波顶点着色函数
        v2f vertVertical(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv[0] = v.texcoord;
            o.uv[1] = v.texcoord + _MainTex_TexelSize.xy * float2(0, -1) * _SmaplerStep;
            o.uv[2] = v.texcoord + _MainTex_TexelSize.xy * float2(0, 1) * _SmaplerStep;
            o.uv[3] = v.texcoord + _MainTex_TexelSize.xy * float2(0, -2) * _SmaplerStep;
            o.uv[4] = v.texcoord + _MainTex_TexelSize.xy * float2(0, 2) * _SmaplerStep;
            return o;
        }
        
        //通用滤波片元着色函数
        fixed4 frag(v2f i) : SV_Target
        {
            const half G[3] = {0.4026, 0.2442, 0.0545};
            
            fixed3 _color = tex2D(_MainTex, i.uv[0]) * G[0];
            for(int it = 1; it < 3; it++)
            {
                _color += tex2D(_MainTex, i.uv[it * 2]) * G[it];
                _color += tex2D(_MainTex, i.uv[it * 2 - 1]) * G[it];
            }
            
            return fixed4(_color, 1);
        }
        
        ENDCG
    
        //横向滤波Pass
        Pass
        {
            //将Pass命名,方便后续其他效果使用
            Name "GAUSSIAN_BLUR_HORIZENTAL"
            CGPROGRAM
            #pragma vertex vertHorizental
            #pragma fragment frag
            ENDCG
        }
        
        //纵向滤波Pass
        Pass
        {
            Name "GAUSSIAN_BLUR_VERTICAL"
            CGPROGRAM
            #pragma vertex vertVertical
            #pragma fragment frag
            ENDCG
        }
    }
}

高斯:
在这里插入图片描述

高斯模糊:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/666070.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ios:文本框默认的copy、past改成中文复制粘贴

问题 ios 开发&#xff0c;对于输入框的一些默认文案展示&#xff0c;如复制粘贴是英文的&#xff0c;那么如何改为中文的呢 解决 按照路径找到这个文件 ios/项目/Info.plist&#xff0c;增加 <key>CFBundleAllowMixedLocalizations</key> <true/> <…

《QT实用小工具·六十九》基于QT开发的五子棋AI游戏

1、概述 源码放在文章末尾 该项目实现了五子棋对战AI&#xff0c;可以享受和AI下棋的快乐&#xff0c;项目实现思路如下&#xff1a; 博弈树 ●Alpha-Beta剪枝(性能提高较大) ●启发式搜索(性能提高较大) ●落子区域限制(性能提高较大) ●Zobrist哈希(性能小幅提升) ●Qt…

香港云服务器好还是国内的好?

香港云服务器与国内云服务器各有其优点和缺点&#xff0c;选择哪种类型的云服务器主要取决于业务需求、用户群体、网络需求以及成本考虑。以下是对两者进行详细比较的内容。 首先&#xff0c;从网络速度和稳定性来看&#xff0c;香港云服务器具有独特的优势。由于香港是全球数据…

vue-Dialog 自定义title样式

展示结果 vue代码 <el-dialog :title"title" :visible.sync"classifyOpen" width"500px" :showClose"false" class"aboutDialog"> <el-form :model"classifyForm" :rules"classifyRules">…

移动电商服务器单点部署

知识图谱 任务一&#xff1a;Web服务器部署 1.知识结构 2.WEB服务器的介绍 Web服务器一般指网站服务器&#xff0c;是指驻留于因特网上提供某种特定类型计算机的程序&#xff0c;Web服务器可以向浏览器等Web客户端提供文档&#xff0c;也可以放置网站文件&#xff0c;让全世界…

红外超声波雷达测距(water)

文章目录 一 RS-232二 RS485三 Modbus四 stm32多路超声波测距4.1 设计方案4.2 代码 参考资料总结 实验要求 一. 采用stm32F103和HC-SR04超声波模块&#xff0c; 使用标准库或HAL库 定时器中断&#xff0c;完成1或2路的超声波障碍物测距功能。 1&#xff09;测试数据包含噪声&am…

Rasa.3X中使用lookup实现对实体的抽取

rasa3.6的DIETClassifier实体提取器不准确&#xff0c;使用RegexEntityExtractor的实体提取器替换。在实战过程解决以下两个问题&#xff1a; 1、RegexEntityExtractor实体提取器的应用 首先在domain.yml中明确对应的实体以及意图&#xff1a; version: "3.0" ent…

数据治理(三)-平台架构

数据治理大致分为两类&#xff0c;一种贴合业务&#xff0c;特殊情况特殊治理&#xff1b;另外一种平台型治理&#xff0c;不考虑具体业务。本文从一个平台数据架构师视角去理解数据治理。 1.什么是治理 数据管理治理 数据治理的职能是指导其他所有的数据管理职能。数据治理的…

CMake的原理与使用方法

一.为什么需要CMake&#xff0c;什么是CMake 1.由于各种make工具遵循不同的规范和标准&#xff0c;所执行的Makefile格式也不同&#xff0c;例如 GNU Make &#xff0c;QT 的 qmake &#xff0c;微软的 MS nmake&#xff0c;BSD Make&#xff08;pmake&#xff09;&#xff0c;…

【JAVA入门】Day06 - 字符串

【JAVA入门】Day06 - 字符串 文章目录 【JAVA入门】Day06 - 字符串一、API二、字符串2.1 创建 String 对象的方式2.2 字符串内存模型 三、字符串的相关操作3.1 字符串的比较3.2 遍历字符串3.3 统计字符出现次数3.4 拼接字符串3.5 字符串反转 四、StringBuilder3.1 构造方法3.2 …

束测后台实操文档2-OpenWrt

束测后台实操文档1-PVE、PBS 上面文&#xff0c;把proxmox装好并添加好PBS上的镜像存储空间后&#xff0c;还原已经做好的镜像基本上就可以在已有的镜像下开展工作了。 调试的PVE环境一般两个网口&#xff0c;一个外网wan&#xff0c;一个子网lan&#xff0c;虚拟机一般在lan…

体验Photoshop:无需下载,直接在浏览器编辑图片

搜索Photoshop时&#xff0c;映入眼帘的是PS软件下载&#xff0c;自学PS软件需要多长时间&#xff0c;学PS软件有必要报班吗...PS软件的设计功能很多&#xff0c;除了常见的图像处理功能外&#xff0c;还涉及图形、文本、视频、出版等。不管你是平面设计师&#xff0c;UI/UX设计…

可变参数

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 在Python中&#xff0c;还可以定义可变参数。可变参数也称不定长参数&#xff0c;即传入函数中的实际参数可以是任意多个。 定义可变参数时&#xf…

植物大战僵尸杂交版破解C++实现

文章目录 前言准备工作&#xff1a;基地址与偏移UI界面设计和绑定项目模板总览图生成与实现信号处理1、阳光值更新:BTN12、三种钱币值更新:BTN2-BTN43、冷却刷新:BTN54、锁定阳光&#xff1a;check15、无冷却&#xff1a;check26、OnTimer&#xff08;&#xff09;和OnClose&am…

Linux上传文件

在finalshell中连接的Linux系统中&#xff0c;输入命令rz然后选择windows中的文件即可。

透视茅台股东大会三大关键词:稳定、竞争力、创新

执笔 | 尼 奥 编辑 | 扬 灵 “让我们携手共同致力于茅台的稳定、健康、可持续发展。”上任刚满一个月的贵州茅台党委书记、董事长张德芹&#xff0c;在5月29日的贵州茅台酒股份有限公司2023年度股东大会上迎来首秀。 张德芹在40多分钟脱稿演讲与30多分钟互动环节中&#x…

TiDB学习9:Ti Cloud简介

目录 1. 为什么选择TiDB 2. 多租户 3. TiDB架构 4. 什么是TiDB Cloud 5. TiDB Cloud Provider Region 6. TiDB Cloud 入门 6.1 在浏览器中打开TiDB Cloud 6.2 创建您的账户 6.3 Developer Tier 与Dedicated Tier 6.3.1 Developer Tier 6.3.2 Dedicated Tier 6.3.2.…

项目纪实 | 版本升级操作get!GreatDB分布式升级过程详解

某客户项目现场&#xff0c;因其业务系统要用到数据库新版本中的功能特性&#xff0c;因此考虑升级现有数据库版本。在升级之前&#xff0c;万里数据库项目团队帮助客户在本地测试环境构造了相同的基础版本&#xff0c;导入部分生产数据&#xff0c;尽量复刻生产环境进行升级&a…

【NVM】nvm常用命令,切换node版本命令

nvm常用的命令&#xff0c;切换node版本命令 nvm 查看支持安装的node版本 nvm list available nvm安装指定版本node nvm install 版本号 例如&#xff1a;nvm install 10.24.1 nvm查看本机安装所有node版本 nvm list nvm切换node版本 nvm use 10.24.1 检测当前node版本 node -…

玄机平台应急响应—Linux日志分析

1、前言 啥是日志呢&#xff0c;日志就是字面意思&#xff0c;用来记录你干了啥事情。日志大体可以分为网站日志和系统日志&#xff0c;网站日志呢就是记录哪个用户在哪里什么时候干了啥事&#xff0c;以及其它的与网站相关的事情。系统日志呢&#xff0c;就是记录你的电脑系统…