在摄影和影视制作领域,散景(Bokeh)效果一直是营造氛围感的秘密武器。当镜头对焦于主体时,背景中明亮的光源会自然虚化成柔和的光斑,这种光学现象如今通过Shader技术被完美复刻到数字世界。我最近完成的这个Bokeh散景Shader项目,正是要解决实时渲染中高质量景深模拟的难题——传统后处理模糊往往生硬呆板,而物理正确的Bokeh效果需要模拟真实镜头的光学特性。
这个Shader特别适合两类开发者:一是正在开发摄影模拟类应用的同仁,比如虚拟相机或电影级游戏;二是任何需要提升场景氛围感的3D项目。通过GPU加速的像素着色器,我们能在保持60fps的同时,实现可动态调整光圈形状、色散程度的高级散景。下面我将从原理拆解到Unity实现,完整分享这个让项目质感瞬间升级的技术方案。
真实镜头的散景效果源于光线在焦平面外的成像特性。当点光源不在焦平面上时,它在传感器上会形成一个弥散圆(Circle of Confusion)。我们通过着色器主要模拟三个关键光学现象:
hlsl复制float hexagonDistanceField(float2 uv, float size) {
uv = abs(uv);
return max(uv.x * 0.866025 + uv.y * 0.5, uv.y) - size;
}
math复制I(r) = I_0 \times (1 - k \cdot r^2)
其中r是归一化半径,k控制衰减强度。
hlsl复制float3 chromaticOffset = float3(
cos(angle) * strength_R,
cos(angle + 2.094) * strength_G, // 120度相位差
cos(angle + 4.188) * strength_B // 240度相位差
);
完全物理精确的模拟需要追踪每条光线的传播路径,这在实时渲染中不现实。我们的Shader采用以下折中方案:
hlsl复制float brightness = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
if (brightness < threshold) discard;
在Unity中创建后处理Shader需要这些基础配置:
RenderPassFeaturecsharp复制Camera.main.depthTextureMode |= DepthTextureMode.Depth;
csharp复制[Range(0.1, 10)] public float blurRadius = 3.0f;
[Tooltip("光圈边数(0=圆形)")] public int apertureBlades = 6;
public Texture2D bokehTexture; // 自定义光斑形状
片段着色器的处理流程分为四个阶段:
hlsl复制float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
float linearDepth = LinearEyeDepth(depth);
float focusRange = abs(linearDepth - _FocusDistance) / _FocusRange;
float blurWeight = saturate(focusRange * _Intensity);
hlsl复制float2 offset = uv - center;
float angle = atan2(offset.y, offset.x);
float radius = length(offset) * _RadiusScale;
// 多边形光圈距离场
if (_ApertureBlades > 0) {
float segmentAngle = 6.28318 / _ApertureBlades;
angle = mod(angle, segmentAngle) - segmentAngle * 0.5;
radius /= cos(angle);
}
hlsl复制float3 finalColor = 0;
for (int i = 0; i < 3; i++) {
float2 chromaUV = uv + _ChromaticAberration[i] * blurWeight;
finalColor[i] = tex2D(_MainTex, chromaUV)[i] * attenuation;
}
hlsl复制#if USE_BOKEH_TEXTURE
float4 bokeh = tex2D(_BokehTex, uv * _BokehScale);
color.rgb = lerp(color.rgb, bokeh.rgb, bokeh.a * blurWeight);
#endif
csharp复制RenderTextureDescriptor descriptor = source.descriptor;
descriptor.width /= 2;
descriptor.height /= 2;
RenderTexture halfRes = RenderTexture.GetTemporary(descriptor);
hlsl复制[numthreads(8,8,1)]
void CS_HorizontalBlur (uint3 id : SV_DispatchThreadID) {
float4 sum = 0;
for (int x = -RADIUS; x <= RADIUS; x++) {
sum += _MainTex[id.xy + int2(x, 0)] * _Kernel[x + RADIUS];
}
_Result[id.xy] = sum;
}
csharp复制float perfFactor = 1.0f - QualitySettings.GetQualityLevel() * 0.2f;
int downSample = Mathf.FloorToInt(perfFactor * _MaxDownSample);
| 参数 | 推荐值 | 视觉影响 | 性能消耗 |
|---|---|---|---|
| Blur Radius | 2-5px | 光斑大小 | 高 |
| Aperture Blades | 5-8边 | 光斑形状 | 低 |
| Chroma Strength | 0.1-0.3 | 色散程度 | 中 |
| Brightness Threshold | 0.8-1.2 | 光斑数量 | 低 |
重要提示:光圈边数超过8边后肉眼难以区分,但计算量线性增长
csharp复制descriptor.msaaSamples = 2;
hlsl复制float blendFactor = smoothstep(0.3, 0.7, focusRange);
csharp复制if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3) {
_MaxBokehPoints = 50;
}
csharp复制void Update() {
float pulse = Mathf.PingPong(Time.time * 0.5f, 1);
material.SetFloat("_ApertureRoundness", pulse);
}
hlsl复制float3 history = tex2D(_HistoryBuffer, uv).rgb;
float3 current = ComputeBokeh(uv);
return lerp(history, current, 0.2);
csharp复制[System.Serializable]
public struct LensPreset {
public string name;
public int bladeCount;
public float chromaticAberration;
public Texture2D bokehTexture;
}
public LensPreset[] presets = {
new LensPreset(){name="Helios 44-2", bladeCount=8, chromaticAberration=0.3},
new LensPreset(){name="Canon 50mm", bladeCount=6, chromaticAberration=0.1}
};
csharp复制bokeh.blurRadius = 4.5f;
bokeh.focusDistance = 1.2f; // 对焦在人物面部
bokeh.apertureBlades = 7; // 柔和七边形
bokeh.chromaticAberration = float3(0.02, 0.0, -0.02);
csharp复制bokeh.brightnessThreshold = 0.6f; // 捕捉更多微弱光源
bokeh.blurRadius = 8.0f; // 大尺寸光斑
bokeh.enableAnamorphic = true; // 启用椭圆形变形
csharp复制// 配合时间轴动态调整
void OnTimelineUpdate(float t) {
bokeh.focusDistance = Lerp(1.0f, 10.0f, t);
bokeh.blurRadius = Sin(t * PI) * 5.0f;
}
在移动端实测中,Redmi Note 10 Pro上1080p分辨率下平均帧率保持在57fps,主要瓶颈在内存带宽而非GPU计算。建议中低端设备将Max Bokeh Points参数控制在100以内,并禁用复杂的色散计算。