一种基于边缘Bloom的溶解shader的实现

溶解效果在游戏中十分常见,然而普通的溶解效果往往并不好看,本文将实现一种基于边缘bloom的溶解效果的实现

                           (图片来自Trifox)

先上最终效果图


整体思路:将溶解的边缘提取出来,bloom之后再与原图像混合。

首先我们实现一下最基础的溶解,下面给出关键代码:

fixed4 frag(v2f i) : SV_Target

{

  fixed cutout = tex2D(_NoiseTex, i.uvNoiseTex).r;

  clip(cutout - _Threshold);

//溶解边缘颜色

  if (cutout - _Threshold < _EdgeLength)

  return _EdgeColor;

  fixed4 col = tex2D(_MainTex, i.uvMainTex);

  return col;

}

_Threshold是控制溶解程度的值,思路是噪音纹理和透明度测试这里不再赘述,这里用纯色表示边缘,当然我们可以对溶解的边缘进行更多的处理比如颜色混合和渐变纹理等,不过这不是我们的重点。得到效果如下。


好的接下来我们想要单独获取得到溶解的边缘(DissolveEdge.shader),思路也很简单,既然我们通过clip处理了cutout - _Threshold小于0的部分,我们同样也可以将cout - _Threshold > _EdgeLength的部分clip掉,if能少用就少用所以将参数移到<的左边可得clip(_EdgeLength - cutout + _Threshold)效果是一样的。处理后得到效果如下。


等等...这个效果EMM...我们给噪音纹理加上一个流动控制,用_SpeedX,_SpeedY控制流动速度,然后在控制脚本中不断改变_Threshold的值,得到的效果如下。(和塞尔达里面神庙的激光柱的效果有点像有木有)。

Fixed cutout = tex2D(_NoiseTex,fixed2(i.uvNoiseTex.x + _Time.x * _SpeedX,i.uvNoiseTex.y + _Time.x * _SpeedY));

咳咳..扯远了,回到主线上来,当前我们得到了溶解的边缘,接下来我们需要考虑的就是如何单独对这个边缘进行bloom的处理了,提起bloom,大多数情况下bloom是基于全屏后处理的一种效果实现,用阈值采样当前场景中亮度较高的部分进行处理,然而这里我们想实现的是只对溶解的边缘进行bloom的处理,这样就不能用上算方法处理,这里我们选择使用将溶解边缘渲染到一张单独的RenderTexture上进行处理,我们创建一个新的摄像机BloomRTCam和一张RTBloomRT),使新相机的位置和角度和主相机完全一致,将其的TargetTexture设置为BloomRT,那么如何只将溶解边缘渲染到BloomRT上呢,这里我们会用到Camera.SetReplacementShader(Shader XX,string XX);Camera.RenderWithShader(Shader XX,string XX);这两个函数,官网对其的解释(https://docs.unity3d.com/Manual/SL-ShaderReplacement.html),简言之,它们可以使用特定的shader来渲染摄像机中的物体,而决定渲染哪些物体则由string来指明,string中的值为一个标签,系统会遍历视口中的所有物体,如果物体的shadersubpass中有对应这个标签的值,则摄像机会将其渲染,若没有则不渲染,通常我们会将string设置为”RenderType”或自定义的RenderType,因为所有的shader都带有”RenderType”标签。其次,RenderWithShader只会在一帧中替换shader,而SetReplacementShader会在调用后始终用替换后的shader渲染相应物体。

对应到我们的场景,我们将DissolveEdge Subpass的"RenderType”=  "Bloom”,我们在BloomRTCam上挂一个控制RT渲染的脚本,然后传入DissolveEdge,在Start中调用SetReplacementShader(DissolveShader,”RenderType”)

void Start () {
        this.GetComponent<Camera>().SetReplacementShader(DissolveShader, "RenderType");
}

这样开始时遍历视口中的物体,只要该物体的shader”RenderType” = “Bloom”就会被渲染到BloomRT,所以我们将需要溶解的物体的shader如此设置,这样得到的BloomRT上就只有需要溶解物体的溶解边缘,效果如下:

接下来的处理就和常规bloom一样了,我们需要在主相机上挂载Bloom.cs脚本和一个材质BlurAndBlend用于做屏幕后处理。流程:降采样→高斯模糊→混合。

关于高斯模糊:

高斯模糊总体上就是图像与正态分布做卷积的过程,每一个像素点的值由本身和周围的像素值通过加权平均后得到。

关于正态分布,上学期的《概率论》课程中有所提及


很明显高斯卷积核的原点处μ=0,因为中心点分配的权重最大,公式可化成:


其对应二维方程为:


其中,μx的均值(方差),σx的标准差(均方差),当μ=0σ=1时,称之为标准正态分布,当x=μ时取得最大值。如果固定μ,改变σ,当σ越小时图形变得越尖,固x落在附近的概率越大,相应模糊程度越弱,σ越大相应模糊程度更强。

计算平均值时我们将高斯卷积核的中心点做原点,周围的点按照其在正态分布曲线上的位置分配权重即可,这里我们使用标准正态分布(σ=1)获取一个5X5的高斯卷积核。

-22

-12

02

12

22

-21

-11

01

11

21

-20

-10

00

10

20

-2-1

-1-1

0-1

1-1

2-1

-2-2

-1-2

0-2

1-2

2-2

带入上述二维方程可得

0.002915

0.013064

0.021539

0.013064

0.002915

0.013064

0.058550

0.096532

0.058550

0.013064

0.021539

0.096532

0.159155

0.096532

0.021539

0.013064

0.058550

0.096532

0.058550

0.013064

0.002915

0.013064

0.021539

0.013064

0.002915

表中所有值的和为0.981811,为计算加权平均,故将表中每个值都要除以0.981811以让权重和为1

最终得到的高斯卷积核为

0.002969

0.013306

0.021938

0.013306

0.002969

0.013306

0.059635

0.098320

0.059635

0.013306

0.021938

0.098320

0.162104

0.098320

0.021938

0.013306

0.059635

0.098320

0.059635

0.013306

0.002969

0.013306

0.021938

0.013306

0.002969

正如我们之前说过的高斯模糊总体上就是图像与正态分布做卷积的过程,得到高斯卷积核之后我们就要进行高斯核与BloomRT图像的卷积操作了,关于卷积这篇文章讲得很好,我们要将BloomRT中的每一个像素点与高斯卷积核做卷积操作得出新的像素值,由于能将二维高斯函数拆分成两个一维函数进行处理,拆分出的一维高斯核为(0.0544880.2442020.402620.2442020.054488),所以主摄像机上挂载的BlurAndBlendshader中含三个pass,第一个用于处理竖直方向的模糊,第二个用于处理水平方向的模糊,第三个用于合并最后的图像。

下面是Bloom.cs的实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//挂载在主摄像机上
public class Bloom : MonoBehaviour {
    //传入BloomRT
    public RenderTexture BloomRT;
    public Material BlurAndBlendMat;

    //控制Bloom强度
    [Range(1,5)]
    public float BloomStrength = 1;

    //迭代次数
    [Range(0, 4)]
    public int iterations = 3;
    //模糊范围大小
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;
    //缩放系数
    [Range(1, 5)]
    public int downSample = 2;

    void Update()
    {
        BlurAndBlendMat.SetFloat("_BloomStrength", BloomStrength);
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        int rtW = BloomRT.width / downSample;
        int rtH = BloomRT.height / downSample;
        //申请RT用于降低分辨率并存入BloomRT
        RenderTexture RT0 = RenderTexture.GetTemporary(rtW,rtH,0);
        RT0.filterMode = FilterMode.Bilinear;
        Graphics.Blit(BloomRT,RT0);

        for (int i =0; i < iterations;i++) {
            BlurAndBlendMat.SetFloat("_BlurSize", 1.0f + i * blurSpread);
            RenderTexture RT1 = RenderTexture.GetTemporary(rtW, rtH, 0);
            //竖直方向模糊处理
            Graphics.Blit(RT0,RT1,BlurAndBlendMat,0);
            RenderTexture.ReleaseTemporary(RT0);
            RT0 = RT1;
            RT1 = RenderTexture.GetTemporary(rtW,rtH,0);
            //水平方向模糊处理
            Graphics.Blit(RT0,RT1,BlurAndBlendMat,1);
            RenderTexture.ReleaseTemporary(RT0);
            RT0 = RT1;
        }
        BlurAndBlendMat.SetTexture("_BloomTex",RT0);
        //将原图像与处理后的BloomRT混合
        Graphics.Blit(source,destination,BlurAndBlendMat,2);
        RenderTexture.ReleaseTemporary(RT0);

    }

}

下面是BlurAndBlend.shader的实现

Shader "Dissolve/BlurAndBlend"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_BloomTex("BloomTex",2D) = "white"{}
		_BlurSize("BlurSize",Float) = 1
		_BloomStrength("BloomStrength",Range(1,5)) = 1
	}
		SubShader
	{
		CGINCLUDE

	#include "UnityCG.cginc"

	sampler2D _MainTex;
	half4 _MainTex_TexelSize;
	sampler2D _BloomTex;
	float _BlurSize, _BloomStrength;

	//用于高斯模糊
	struct v2f
	{
		float4 pos : SV_POSITION;
		half2 uv[5] : TEXCOORD0;
	};

	//由于最后图像混合
	struct v2fBloom {
		float4 pos: SV_POSITION;
		half2 uv: TEXCOORD0;
		half2 uv2: TEXCOORD1;
	};

	//竖直方向模糊处理
	v2f vertBlurVertical(appdata_img v) {
		v2f o;
		o.pos = UnityObjectToClipPos(v.vertex);
		half2 uv = v.texcoord;
		//定义的纹理数组用于存储采样时的像素点,由于顶点着色器到片元着色器差值线性,所以在顶点着色器中计算节省性能
		o.uv[0] = uv;
		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;
		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;
	}

	
	v2fBloom vertBloom(appdata_img v) {
		v2fBloom o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		o.uv2 = v.texcoord.xy;
		return o;
	}


	fixed4 fragBlur(v2f i) : SV_Target
	{
		//一维高斯核
		float weight[3] = { 0.40262,0.244202,0.054488 };
	    //最终颜色=各像素点*权重值之和
		fixed3 sum = tex2D(_MainTex,i.uv[0]).rgb * 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);
	}

	fixed4 fragBloom(v2fBloom i) :SV_Target{
		//调用第三个pass前传入模糊过的BloomRT与原图像混合
		return tex2D(_MainTex,i.uv) + tex2D(_BloomTex,i.uv2) * _BloomStrength;
	}
		ENDCG

		ZTest Always Cull Off ZWrite Off

		Pass {
			CGPROGRAM
			#pragma vertex vertBlurVertical
			#pragma fragment fragBlur
			ENDCG
	    }

		Pass {
			CGPROGRAM
			#pragma vertex vertBlurHorizontal
			#pragma fragment fragBlur
			ENDCG
	    }

		Pass{
			CGPROGRAM
			#pragma vertex vertBloom
			#pragma fragment fragBloom
			ENDCG
		}
	}
		FallBack  Off
}

学生党一枚,文章中的错误,不足诚请各位指点!!

学习资料:

https://en.wikipedia.org/wiki/Gaussian_blur

https://www.jianshu.com/p/d8b535efa9db

http://www.cnblogs.com/wantnon/p/4542172.html

https://blog.youkuaiyun.com/u011047171/article/details/47977441


<think>嗯,用户现在想了解如何编写Bloom效果的Shader。之前他问过关于_sourceTexture和_BlitTexture的问题,可能是在进行后处理效果开发,尤其是自定义的Bloom效果。现在他需要具体的实现步骤,可能已经有一定的UnityShader基础,但需要指导具体的实现方法。 首先,我需要回忆Bloom效果的基本原理。Bloom通常包括几个步骤:亮度提取、模糊(高斯模糊多次迭代)、混合。用户可能对每个步骤的实现细节不太清楚,需要分步骤讲解。 接下来,我需要考虑用户的使用环境。他之前提到URP,所以应该是在Unity的URP管线中实现。需要确保提供的Shader代码与URP兼容,比如使用HLSL而不是GLSL,并且包含正确的头文件和渲染管线接口。 然后,要确定用户可能的疑问点。比如如何提取亮度区域,如何实现高斯模糊,如何控制Bloom的强度、阈值等参数。此外,还需要考虑性能优化,比如降采样和升采样的处理,以及多Pass协作。 用户可能没有提到但需要注意的点包括:如何在后处理流程中正确插入Bloom效果,是否需要自定义Renderer Feature,以及如何处理多个相机的渲染。不过根据问题,主要焦点是Shader本身的编写,所以可能暂时不需要深入Renderer Feature的设置,但可能需要简要提及。 另外,需要提供代码示例,但必须确保代码的正确性和可读性。使用URP中的CommandBuffer和Blitter API进行纹理处理,同时管理临时RT的分配和释放,避免内存泄漏。 可能还需要解释一些关键函数,比如高斯模糊的权重计算,以及双线性采样的优化方法。此外,参数如阈值、强度、散射系数的调节对最终效果的影响,帮助用户理解如何调整参数以达到理想效果。 最后,总结实现步骤,强调核心要点,比如多Pass协作、性能优化、参数调节,并提醒常见问题和排查方法,比如纹理未正确绑定或模糊效果不明显的情况。</think>Bloom 效果的核心是通过**提取高亮区域 + 多级模糊 + 叠加混合**实现的。以下是基于 Unity URP 的 Bloom Shader 实现思路和关键代码示例: --- ### **Bloom 实现步骤** 1. **亮度提取**:从原图中分离超过阈值的高亮区域 2. **降采样 + 高斯模糊**:通过多级降采样进行横向/纵向模糊 3. **升采样 + 叠加混合**:将模糊后的光晕与原图混合 --- ### **完整 Shader 框架(HLSL)** ```hlsl Shader "Custom/Bloom" { Properties { _MainTex ("Base Texture", 2D) = "white" {} _BloomThreshold ("Bloom Threshold", Range(0,1)) = 0.7 _BloomIntensity ("Bloom Intensity", Range(0,5)) = 1.2 _Scattering ("Scattering", Range(0,1)) = 0.7 } SubShader { // 亮度提取 Pass Pass { Name "BrightnessFilter" HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float _BloomThreshold; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(Attributes v) { // 常规顶点处理... } half4 frag(v2f i) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); // 计算亮度:Luminance = 0.2126*R + 0.7152*G + 0.0722*B float brightness = dot(col.rgb, float3(0.2126, 0.7152, 0.0722)); // 阈值处理 + 平滑过渡 float soft = brightness - _BloomThreshold; soft = saturate(soft / (1 - _BloomThreshold)); return col * soft; } ENDHLSL } // 高斯模糊 Pass(横向/纵向各一次) Pass { Name "GaussianBlur" HLSLPROGRAM float2 _BlurDirection; // (1,0) 或 (0,1) float _Scattering; half4 frag(v2f i) : SV_Target { static const float weights[5] = {0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216}; half4 result = 0; float2 texelSize = _MainTex_TexelSize.xy * _Scattering; for(int j = -4; j <= 4; j++) { float2 offset = _BlurDirection * j * texelSize; result += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv + offset) * weights[abs(j)]; } return result; } ENDHLSL } // 最终混合 Pass Pass { Name "Composite" HLSLPROGRAM TEXTURE2D(_BloomTexture); float _BloomIntensity; half4 frag(v2f i) : SV_Target { half4 scene = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); half4 bloom = SAMPLE_TEXTURE2D(_BloomTexture, sampler_MainTex, i.uv); return scene + bloom * _BloomIntensity; } ENDHLSL } } } ``` --- ### **C# 脚本关键控制逻辑** ```csharp // Bloom 渲染流程控制 public void RenderBloom( CommandBuffer cmd, RenderTargetIdentifier source, RenderTextureDescriptor descriptor) { // 1. 创建临时RT int brightPass = Shader.PropertyToID("_BrightPass"); cmd.GetTemporaryRT(brightPass, descriptor, FilterMode.Bilinear); // 2. 亮度提取 cmd.Blit(source, brightPass, bloomMaterial, 0); // 使用第一个Pass // 3. 多级模糊(示例为2次迭代) int blurRT1 = Shader.PropertyToID("_BlurRT1"); cmd.GetTemporaryRT(blurRT1, descriptor, FilterMode.Bilinear); // 横向模糊 material.SetVector("_BlurDirection", new Vector2(1, 0)); cmd.Blit(brightPass, blurRT1, bloomMaterial, 1); // 纵向模糊 cmd.Blit(blurRT1, brightPass, bloomMaterial, 1); // 4. 最终混合 cmd.SetGlobalTexture("_BloomTexture", brightPass); cmd.Blit(source, destination, bloomMaterial, 2); // 5. 释放临时RT cmd.ReleaseTemporaryRT(brightPass); cmd.ReleaseTemporaryRT(blurRT1); } ``` --- ### **关键参数解析** | 参数 | 作用 | 典型值 | |------|------|-------| | `_BloomThreshold` | 控制哪些亮度区域被提取 | 0.6~0.8 | | `_BloomIntensity` | 光晕叠加强度 | 1.0~2.0 | | `_Scattering` | 模糊散射范围 | 0.5~1.0 | | 模糊迭代次数 | 影响光晕柔和度 | 通常2~4次 | --- ### **性能优化技巧** 1. **降采样优化**:在模糊前先将 RT 缩小到 1/2 或 1/4 分辨率 2. **双线性采样**:利用硬件插值减少采样次数 3. **权重预计算**:提前计算高斯核权重值 4. **分帧渲染**:对低优先级效果分帧处理 --- ### **常见问题排查** 1. **光晕边缘有锯齿**: - 增加模糊迭代次数 - 检查降采样是否正确 2. **高亮区域不连续**: - 调整亮度提取的平滑过渡公式 ```glsl soft = smoothstep(_BloomThreshold, _BloomThreshold + 0.1, brightness); ``` 3. **性能消耗过高**: - 减少模糊迭代次数 - 使用更小的 RT 分辨率 --- 如果需要更完整的工程示例(包含 Renderer Feature 集成),可以基于这个框架继续扩展。实际项目中还需要考虑 HDR 颜色空间处理、镜头脏迹(dirt texture)叠加等高级特性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值