第一次在Unity里看到Bloom效果时,那种自发光的质感让我眼前一亮。简单来说,Bloom就是模拟现实世界中强光溢出镜头的光晕现象。比如霓虹灯在潮湿夜晚的朦胧光效,或者阳光透过树叶间隙的柔光效果,都是Bloom的典型应用场景。
实现原理可以拆解为三个关键步骤:首先是亮度提取,通过阈值筛选出需要发光的区域;接着对高亮区域进行模糊处理,模拟光线扩散;最后将模糊后的图像与原图叠加。这个过程中最有趣的是亮度提取环节——就像用筛子过滤面粉,我们通过_BrightCut参数控制"筛孔大小",只允许足够亮的像素参与后续效果。
在Shader层面,亮度提取的核心代码非常简洁:
hlsl复制float br = max(max(col.r, col.g), col.b);
br = max(0, (br - _BrightCut)) / max(br, 0.00001);
这段代码先取RGB通道的最大值,然后减去阈值参数,最后通过除法归一化。实际项目中我发现,将_BrightCut设置为0.3-0.5之间,通常能得到比较自然的发光效果。
很多新手容易忽略亮度提取的质量对最终效果的影响。经过多次项目实践,我总结出几个关键点:
首先是阈值算法的选择。基础版本使用RGB最大值,但会导致颜色失真。改进方案可以采用亮度公式:
hlsl复制float luminance = dot(col.rgb, float3(0.2126, 0.7152, 0.0722));
这个基于人眼感知的亮度计算,能更准确地保留色彩关系。
其次是抗锯齿处理。直接使用阈值切割会产生明显的边缘锯齿,我通常会增加平滑过渡:
hlsl复制float softThreshold = smoothstep(_BrightCut, _BrightCut + 0.1, luminance);
这个技巧让发光边缘产生渐变,特别适合卡通风格的游戏。参数0.1控制过渡范围,数值越大边缘越柔和。
最后是亮度增强环节。简单的线性乘法容易导致亮部过曝,我更喜欢用指数曲线控制:
hlsl复制col.rgb *= pow(br, _BrightPower);
通过_BrightPower参数(建议1.5-3.0)可以灵活调整发光强度,这个技巧在科幻场景中特别实用。
模糊处理是Bloom效果最耗能的环节。经过多次性能测试,我整理了三种常见模糊方案的对比:
| 算法类型 | 采样次数 | 效果质量 | 适用场景 |
|---|---|---|---|
| 均值模糊 | 9次/像素 | 一般 | 移动端项目 |
| 高斯模糊 | 17次/像素 | 优秀 | PC/主机游戏 |
| Kawase模糊 | 4次/像素 | 良好 | 平衡型项目 |
在Shader实现上,Kawase模糊是我的首选方案。它的核心优势在于迭代次数与模糊程度解耦:
csharp复制for(int i=0; i<iterations; i++){
Graphics.Blit(rt1, rt2, blurMat, 0);
Graphics.Blit(rt2, rt1, blurMat, 1);
}
这个循环结构允许我们通过调整RT分辨率来控制性能——首次迭代使用原图1/4分辨率,后续每次迭代分辨率减半。实测在移动设备上,4次迭代配合分辨率降采样,帧率可以提升40%以上。
基础Bloom直接将模糊结果与原图相加,这容易导致色彩失真。更专业的做法是引入混合模式控制:
hlsl复制// 线性光混合
col.rgb = lerp(col.rgb, col.rgb + brightCol.rgb, _Intensity);
// 屏幕混合
col.rgb = 1.0 - (1.0 - col.rgb) * (1.0 - brightCol.rgb * _TintColor.rgb);
第一种方案适合写实风格,第二种则能保留更多中间色调。我经常在项目中暴露_Intensity和_TintColor参数给美术人员,让他们直接调整发光色调。
针对HDR管线,还需要特别注意Tonemapping处理。建议在Bloom之后应用ACES色调映射,避免出现不自然的颜色偏移:
hlsl复制float3 ACESFilm(float3 x){
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}
将各个模块组合起来,完整的C#控制器应该包含这些关键参数:
csharp复制[Header("亮度设置")]
[Range(0,1)] public float threshold = 0.5f;
[Range(1,5)] public float intensity = 1.5f;
[Header("模糊设置")]
[Range(1,8)] public int iterations = 4;
[Range(0.5f,2f)] public float blurSize = 1.2f;
[Header("颜色控制")]
public Color tintColor = Color.white;
[Range(0,1)] public float saturation = 0.8f;
调试时我通常遵循这个流程:先设置threshold直到只有目标区域发光,然后调整intensity控制发光强度,最后用blurSize和iterations平衡效果与性能。对于风格化项目,适当降低saturation能让发光效果更符合卡通审美。
在OnRenderImage中的处理流程要特别注意RT管理:
csharp复制RenderTexture rt1 = RenderTexture.GetTemporary(width/2, height/2);
RenderTexture rt2 = RenderTexture.GetTemporary(width/4, height/4);
// 亮度提取
Graphics.Blit(source, rt1, brightMat);
// 降采样模糊
for(int i=0; i<iterations; i++){
Graphics.Blit(rt1, rt2, blurMat);
RenderTexture.ReleaseTemporary(rt1);
rt1 = rt2;
rt2 = RenderTexture.GetTemporary(width/(8*(i+1)), height/(8*(i+1)));
}
// 上采样与混合
Graphics.Blit(rt1, destination, bloomMat);
RenderTexture.ReleaseTemporary(rt1);
在大型开放世界项目中,Bloom优化至关重要。这是我总结的几个实用技巧:
首先是分帧处理策略。将Bloom计算分散到多帧执行,特别是对于高分辨率模糊:
csharp复制void Update(){
if(Time.frameCount % 2 == 0){
UpdateEvenFrameBloom();
}else{
UpdateOddFrameBloom();
}
}
其次是基于视距的动态调整。通过LOD系统控制Bloom强度:
csharp复制float distanceFactor = 1.0 - saturate(camDistance / maxDistance);
bloomIntensity = Mathf.Lerp(minIntensity, maxIntensity, distanceFactor);
对于移动平台,可以考虑使用Compute Shader加速模糊计算。一个简单的Box模糊Compute Shader示例:
hlsl复制[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID){
float4 sum = 0;
for(int x=-RADIUS; x<=RADIUS; x++){
for(int y=-RADIUS; y<=RADIUS; y++){
sum += _MainTex[id.xy + int2(x,y)];
}
}
_Result[id.xy] = sum / ((2*RADIUS+1)*(2*RADIUS+1));
}
最后要提醒的是,不同渲染管线需要适配不同的实现方式。URP中需要通过RenderFeature添加Bloom,而HDRP则内置了更复杂的Bloom控制系统。