Unity Shader 学习13:屏幕后处理 - 使用高斯模糊的Bloom辉光效果

server/2025/2/27 6:52:30/

        目录

一、基本的后处理流程 - 以将画面转化为灰度图为例

1. C#调用shader

2. Shader实现效果

二、Bloom辉光效果

1. 主要变量

2. Shader效果

(1)提取较亮区域 - pass1

(2)高斯模糊 - pass2&3

(3)图像混合 - pass4

3. C#调用流程


一、基本的后处理流程 - 以将画面转化为灰度图为例

需要使用到2个文件:Shader用来写效果处理,C#在每帧渲染时调用shader

① shader文件就和普通的效果一样正常写,只是处理对象是 整个场景渲染好后(此时已经是一张平面贴图)的贴图:_MainTex以及可以省略顶点着色器的输入结构体,用unity提供的appdata_img代替。

② 而C#脚本则是在OnRenderImage函数中根据算法逻辑按需使用pass进行渲染。这里将图片转为灰度图是不需要什么逻辑啦,基本上就是可以直接进行渲染,但是后面讲的bloom效果就需要加点东西。

1. C#调用shader

使用 Shader.Find 找到相应shader并创建对应的材质material,在OnRenderImage中可以利用 m.setxxx( ) 来给shader的参数赋值,再利用 Graphics.Blit(src, dest, m) 使该材质作用于_MainTex并渲染到屏幕上。

这里要传的参数就是变灰的程度。

public class BWEffect : MonoBehaviour
{Material m;[Range(0, 1)] public float bwBlend = 0;void Awake(){m = new Material(Shader.Find("Hidden/BWDiffuse"));}void OnRenderImage(RenderTexture src, RenderTexture dest){Debug.Log("OnRenderImage called");m.SetFloat("_bwBlend", bwBlend); //传参Graphics.Blit(src, dest, m); //渲染}
}

2. Shader实现效果

在片元着色器中,对_MainTex采样,并用 col.r * 0.3 + col.g * 0.59 + col.b * 0.11 提取出其灰度,用 C# 传来的 灰度程度值 原图和灰图之间做插值

Shader "Hidden/BWDiffuse"
{Properties{_MainTex ("Texture", 2D) = "white" {}_bwBlend ("WBlend", Range(0,1)) = 0}SubShader{Cull Off ZWrite Off ZTest AlwaysPass{CGPROGRAM#pragma vertex vert_img#pragma fragment frag#include "UnityCG.cginc"uniform sampler2D _MainTex;uniform float _bwBlend;fixed4 frag (v2f_img i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv); //原色float lum = col.r * 0.3 + col.g * 0.59 + col.b * 0.11; float4 bw = float4(lum, lum, lum, 1); //灰色float4 result = lerp(col, bw, _bwBlend); //插值return result;}ENDCG}}
}


二、Bloom辉光效果

Bloom效果的实现可以分为3步:

① 提取较亮区域 

② 用高斯模糊,模拟亮区的光线扩散 

③模糊图与原图混合

1. 主要变量

[Range(0, 4)] public int iterations = 3;//高斯模糊迭代次数
[Range(0.2f, 3.0f)] public float blurSpread = 0.6f;//每次迭代模糊范围的增长速度
[Range(1, 8)] public int downSample = 2;//将图片像素量减少的降采样系数,能减少需要处理的像素量,提高性能
[Range(0.0f, 4.0f)] public float luminaceThreshold = 0.6f;//模糊阈值

luminaceThreshold:模糊阈值,它能决定提取亮部的区域范围
iterations:迭代次数,可以对图片进行多次的模糊
blurSpread:每次迭代模糊后,都要对模糊范围 (BlurSize) 进行扩大,其控制每次扩大的速度
blurSize:就是上述的blurSize,其与blurSpread的关系为 1.0f + 迭代次数i * blurSpeed ,加一是为了保证值最小能为1
downSample:对原图进行降采样,也就是降低图片的像素,这样既能优化性能,又能获得更平滑的模糊效果

2. Shader效果

(1)提取较亮区域 - pass1

将图片转为灰度,灰度就能表示该像素的亮度,之后对亮度减去阈值,此时只有原本亮度值大于阈值的值能够依然保持为正数

不知道有没有人和我一样对最后一步的 c * val 有疑惑,确实暗部区域归0了,但是亮部区域也可能会变得比原本暗,这对吗?最后我的理解是,因为在最后一个pass中,将模糊后的图和原图混合的方式是 “相加”,也就是在原图亮度的基础上进行一个提亮,所以这样处理也能让辉光更加柔和,当然只是我的想法啦~

v2fExtractBright vertExtractBright (appdata_img v){v2fExtractBright o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;return o;
}fixed luminance(fixed4 col){//计算灰度值return col.r * 0.3 + col.g * 0.59 + col.b * 0.11 ;
} fixed4 fragExtractBright(v2fExtractBright i): SV_TARGET0{fixed4 c = tex2D(_MainTex, i.uv);fixed lum = luminance(c);fixed val = clamp(lum - _LuminaceThreshold, 0.0, 1.0);return c * val;//截取较亮区域
}

(2)高斯模糊 - pass2&3

高斯模糊的本质是对每个顶点,利用他附近的点的颜色进行平均,使得图片变得模糊。做法就不说啦,有点老生常谈,讲几个写代码时需要对算法进行优化的点:

① 优化1:

将高斯模糊分为两个pass实现:将高斯的卷积核(比如是5x5)成了一个纵向向量(5x1)与一个横向向量(1x5),也就是先对图片在纵向上模糊一次,再在横向上模糊一次,反过来也成立,这就是高斯核的分离性。

这样能节省性能开销,因为 不拆的时候,假如原图有1000x1000个像素,那么模糊需要的采样数则为1000x1000(总像素数)x5x5(每个卷积核有25个值);而如果拆成两个一维向量的乘积 进行两次模糊,就只需要1000x1000x5x2次采样。

② 优化2:

另外,由于卷积核是对称的,所以在写代码时,仅用3个位置就能表示出一个完整的高斯核。
 

_MainTex_TexelSize指的是 纹理单个像素的大小

v2fBlur vertBlurVertical (appdata_img v){v2fBlur o;o.pos = UnityObjectToClipPos(v.vertex);half2 uv = v.texcoord;//计算邻域的纹理坐标(纵向5维向量)o.uv[0] = uv;o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;//上移1个单位o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;//下移1个单位o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;//上移2个单位o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;//下移2个单位return o;
}v2fBlur vertBlurHorizontal (appdata_img v){v2fBlur o;o.pos = UnityObjectToClipPos(v.vertex);half2 uv = v.texcoord;//计算邻域的纹理坐标(横向5维向量)o.uv[0] = uv;o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;//右移1个单位o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;//左移1个单位o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;//右移2个单位o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;//左移2个单位return o;
}fixed4 fragBlur(v2fBlur i): SV_TARGET0{float weight[3] = {0.4026, 0.2442, 0.0545};//高斯核的权重值fixed3 sum;//5个权重值之和sum = tex2D(_MainTex, i.uv[0]).rbg * weight[0];for(int it = 1; it < 3; it++){sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];}return fixed4(sum, 1.0);
}

(3)图像混合 - pass4

这就是将原图的颜色直接与模糊图的亮度进行一个叠加啦,用的是加法。

v2fBloom vertBloom (appdata_img v){v2fBloom o;o.pos = UnityObjectToClipPos(v.vertex);o.uv.xy = v.texcoord;//xy存储_MainTex的纹理坐标o.uv.zw = v.texcoord;//zw存储_Bloom的纹理坐标//平台差异兼容,做翻转处理#if UNITY_UV_STARTS_AT_TOPif(_MainTex_TexelSize.y < 0.0)o.uv.w = 1.0 - o.uv.w;#endifreturn o;
}fixed4 fragBloom(v2fBloom i): SV_TARGET0{return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}

3. C#调用流程

Graphics.Blit(src, buffer0, m, 0): 先将图片降采样,用降采样后的宽高 创建临时的RenderTexture - buffer0,提取亮部存于 buffer0 中;
 Graphics.Blit(buffer0, buffer1, m, 1):之后就可以对 buffer0 进行纵向的高斯模糊,将计算结果存于新创建的buffer1
Graphics.Blit(buffer0, buffer1, m, 2)将buffer1给到buffer0,继续对 buffer0 进行横向的高斯模糊,将计算结果存于buffer1;
Graphics.Blit(buffer0, dest, m, 3)将buffer1给到buffer0,对buffer0进行原图叠加,显示到屏幕上。

每次交换缓冲区时,代码为:
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
为什么要先释放再交换?因为 buffer 只是引用变量,后面的 “=” 不是赋值,而是只改变了引用指向,所以如果不先进行释放,原指向数据就会永远保留在内存中,有可能会引起内存泄漏。

public class BloomEffect : MonoBehaviour
{Material m;[Range(0, 4)] public int iterations = 3;//高斯模糊迭代次数[Range(0.2f, 3.0f)] public float blurSpread = 0.6f;//每次迭代模糊范围的增长速度[Range(1, 8)] public int downSample = 2;//将图片像素量减少的降采样系数,能减少需要处理的像素量,提高性能[Range(0.0f, 4.0f)] public float luminaceThreshold = 0.6f;//模糊阈值private void Awake(){m = new Material(Shader.Find("Hidden/Bloom"));}void OnRenderImage(RenderTexture src, RenderTexture dest){Debug.Log("OnRenderImage called");//降采样int rtW = src.width / downSample;int rtH = src.height / downSample;RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);buffer0.filterMode = FilterMode.Bilinear;//pass1,提取亮区m.SetFloat("_LuminaceThreshold", luminaceThreshold);Graphics.Blit(src, buffer0, m, 0);//pass2&3,高斯for(int i = 0; i < iterations; i++){m.SetFloat("_BlurSize", 1.0f + i * blurSpread);RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);Graphics.Blit(buffer0, buffer1, m, 1);//纵向RenderTexture.ReleaseTemporary(buffer0);buffer0 = buffer1;buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);Graphics.Blit(buffer0, buffer1, m, 2);//横向RenderTexture.ReleaseTemporary(buffer0);buffer0 = buffer1;}//pass4,混合m.SetTexture("_Bloom", buffer0);Graphics.Blit(buffer0, dest, m, 3);RenderTexture.ReleaseTemporary(buffer0);Graphics.Blit(src, dest, m);}
}


http://www.ppmy.cn/server/170963.html

相关文章

每日一题——LRU缓存机制的C语言实现详解

LRU缓存机制的C语言实现详解 参考1. 数据结构设计双向链表节点哈希表节点哈希表LRU缓存结构 2. 初始化哈希表和双向链表哈希函数初始化哈希表初始化双向链表创建LRU缓存 3. 更新双向链表4. 实现Get操作5. 实现Put操作更新节点值删除最久未使用节点插入或更新节点 6. 释放缓存释…

内容中台的企业CMS架构是什么?

企业CMS模块化架构 现代企业内容管理系统的核心在于模块化架构设计&#xff0c;通过解耦内容生产、存储、发布等环节构建灵活的技术栈。动态/静态发布引擎整合技术使系统既能处理实时更新的产品文档&#xff0c;也能生成高并发的营销落地页&#xff0c;配合版本控制机制确保内…

20.<Spring图书管理系统①(登录+添加图书)>

PS&#xff1a;关于接口定义 接口定义&#xff0c;通常由服务器提供方来定义。 1.路径&#xff1a;自己定义 2.参数&#xff1a;根据需求考虑&#xff0c;我们这个接口功能完成需要哪些信息。 3.返回结果&#xff1a;考虑我们能为对方提供什么。站在对方角度考虑。 我们使用到的…

JAVA面试常见题_基础部分_springboot面试题

问题一 什么是 Spring Boot&#xff1f; 多年来&#xff0c;随着新功能的增加&#xff0c;spring 变得越来越复杂。只需访问 https://spring.io/projects 页面&#xff0c;我们就会看到可以在我们的应用程序中使用的所有 Spring 项目的不同功能。如果必须启动一个新的 Sprin…

C++ STL(二)deque

目录 deque&#xff08;双端队列&#xff09; 内存结构 验证地址不连续 使用详解 构造函数 元素访问 容量操作 修改 迭代器 迭代器失效 code实例 实现一个简单的deque deque&#xff08;双端队列&#xff09; std::deque&#xff08;双端队列&#xff09;是 C 标准…

word中对插入的图片修改背景色

关于对word中插入的图片修改背景色的问题&#xff0c;网上查了好多都无效&#xff0c;可能是由于word版本的问题&#xff0c;本人word版本为2019版&#xff0c;亲测有效的修改图片背景色为透明的小技巧&#xff1a; 选中图片-设置图片格式-最右面图标&#xff0c;选择图片校正…

JavaEE 编写Java程序,实现简单的echo程序(网络编程TCP实践练习)

Java TCP 客户端/服务器开发深度解析 一、客户端代码全注释 public class TcpClient {private Socket socket null; // TCP通信核心对象// 构造函数&#xff1a;建立TCP连接public TcpClient(String ip, int port) throws IOException {// 创建Socket时会自动进行三次握手s…

20250221 NLP

1.向量和嵌入 https://zhuanlan.zhihu.com/p/634237861 encoder的输入就是向量&#xff0c;提前嵌入为向量 二.多模态文本嵌入向量过程 1.文本预处理 文本tokenizer之前需要预处理吗&#xff1f; 是的&#xff0c;文本tokenizer之前通常需要对文本进行预处理。预处理步骤可…