作为一名图形程序员,我经常需要实现各种视觉效果来增强场景的真实感和艺术性。其中Bokeh散景效果是我个人非常喜欢的一种技术,它能为画面带来电影级的景深效果。今天我要分享的是一个基于黄金角采样的Bokeh着色器实现,这个方案在性能和效果之间取得了很好的平衡。
Bokeh效果本质上模拟了真实相机镜头的物理特性。当镜头对焦时,焦平面以外的点光源会形成美丽的光斑。这些光斑的形状和分布特性取决于镜头的光圈结构。在计算机图形学中,我们通过着色器程序来模拟这一物理现象。
这个着色器特别适合以下场景:
这个着色器的核心创新在于使用了黄金角进行螺旋采样。黄金角是一个数学常数,约等于137.5度。它的精妙之处在于:
glsl复制const float GOLDEN_ANGLE = 2.3999632; // 约等于137.5度的弧度值
黄金角的数学定义源自黄金比例:
code复制Golden Angle = (3 - √5) × π ≈ 2.39996 弧度
为什么选择黄金角而不是其他角度?这背后有深刻的数学原理:
在实际实现中,我们使用一个旋转矩阵来应用这个角度:
glsl复制const mat2 rot = mat2(
cos(GOLDEN_ANGLE), sin(GOLDEN_ANGLE),
-sin(GOLDEN_ANGLE), cos(GOLDEN_ANGLE)
);
这个矩阵每次迭代都会将采样方向旋转黄金角度数,确保采样点在螺旋路径上均匀分布。
采样点沿着费马螺线(Fermat's Spiral)分布,其数学表达式为:
code复制r(n) = √n
θ(n) = n × φ (φ是黄金角)
在代码中,我们使用了一个巧妙的近似来实现平方根增长:
glsl复制r += 1. / r;
这个递推公式模拟了√n的增长特性,但避免了昂贵的平方根计算。让我们看看它的增长序列:
| 迭代次数 | 半径值 |
|---|---|
| 0 | 1.000 |
| 1 | 2.000 (+1.000) |
| 2 | 2.500 (+0.500) |
| 3 | 2.900 (+0.333) |
| 4 | 3.245 (+0.250) |
可以看到,随着迭代次数增加,半径的增长速度逐渐减缓,非常接近√n函数的特性。
Bokeh效果的主函数结构如下:
glsl复制vec3 Bokeh(sampler2D tex, vec2 uv, float radius) {
vec3 acc = vec3(0), div = acc;
float r = 1.;
vec2 vangle = vec2(0.0, radius*.01 / sqrt(float(ITERATIONS)));
for (int j = 0; j < ITERATIONS; j++) {
// 更新螺旋半径
r += 1. / r;
// 旋转采样方向
vangle = rot * vangle;
// 采样纹理
vec3 col = texture(tex, uv + (r-1.) * vangle).xyz;
// 对比度增强
col = col * col * 1.8;
// 计算权重
vec3 bokeh = pow(col, vec3(4));
// 累加加权颜色
acc += col * bokeh;
div += bokeh;
}
return acc / div;
}
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
| ITERATIONS | int | 采样迭代次数 | 300-500(桌面端) |
| radius | float | 模糊半径 | 0.5-3.0 |
| 对比度系数 | float | 高光增强 | 1.8-2.5 |
| 权重指数 | float | 亮度权重 | 4.0 |
这个着色器使用了一种智能的加权混合策略:
glsl复制vec3 bokeh = pow(col, vec3(4));
acc += col * bokeh;
div += bokeh;
这种设计模拟了真实镜头的两个特性:
这种处理使得明亮区域会产生更明显的光斑效果,与真实物理现象一致。
根据模糊半径动态调整采样次数可以显著提升性能:
glsl复制int actualIterations = mix(100, 500, smoothstep(0.0, 1.0, radius));
先对低分辨率图像应用Bokeh效果,再上采样:
glsl复制vec2 uv_low = fragCoord / (iResolution.x * 0.5);
vec3 result = Bokeh(iChannel0, uv_low, rad);
将旋转矩阵声明为常量,让编译器进行优化:
glsl复制const mat2 rot = mat2(...);
在游戏中实现动态景深时,可以结合深度图:
glsl复制float depth = texture(depthMap, uv).r;
float coc = abs(depth - focusDepth) * aperture;
color = Bokeh(sceneTex, uv, coc);
迭代次数:
对比度系数:
glsl复制col = col * col * 1.8; // 1.8是较好的平衡点
权重指数:
glsl复制vec3 bokeh = pow(col, vec3(4)); // 4是推荐值
症状:模糊区域出现明显的环形图案
解决方案:
症状:明亮区域过度泛白
调整方法:
glsl复制vec3 bokeh = pow(col, vec3(3)); // 降低指数
col = col * col * 1.2; // 减小对比度增强
对于移动端或性能敏感场景:
glsl复制vec3 col = textureLod(tex, uv + offset, 1.0).xyz;
模拟真实镜头的多边形光圈:
glsl复制const float HEX_ANGLE = 1.0472; // π/3 (60度)
mat2 rot = mat2(cos(HEX_ANGLE), sin(HEX_ANGLE), -sin(HEX_ANGLE), cos(HEX_ANGLE));
添加镜头色差模拟:
glsl复制float r = texture(tex, uv + offset * 1.02).r;
float g = texture(tex, uv + offset).g;
float b = texture(tex, uv + offset * 0.98).b;
结合运动矢量实现动态模糊:
glsl复制vec2 motionVec = texture(motionTex, uv).xy;
vec3 col = texture(tex, uv + (r-1.) * (vangle + motionVec)).xyz;
以下是经过优化和注释的完整实现:
glsl复制// 基于黄金角的Bokeh散景效果
// 优化版本 - 包含性能改进和参数调整
const float GOLDEN_ANGLE = 2.3999632; // ~137.5度
#define ITERATIONS 500 // 桌面端推荐值
// 预计算旋转矩阵
const mat2 rot = mat2(
cos(GOLDEN_ANGLE), sin(GOLDEN_ANGLE),
-sin(GOLDEN_ANGLE), cos(GOLDEN_ANGLE)
);
vec3 Bokeh(sampler2D tex, vec2 uv, float radius) {
vec3 acc = vec3(0), div = acc;
float r = 1.;
// 半径归一化处理,确保不同迭代次数下效果一致
vec2 vangle = vec2(0.0, radius * 0.01 / sqrt(float(ITERATIONS)));
for (int j = 0; j < ITERATIONS; j++) {
// 近似平方根增长的半径更新
r += 1. / r;
// 黄金角旋转
vangle = rot * vangle;
// 纹理采样
vec3 col = texture(tex, uv + (r-1.) * vangle).xyz;
// 对比度增强(突出高光区域)
col = col * col * 1.8;
// 亮度权重计算(4次方)
vec3 bokeh = pow(col, vec3(4));
// 加权累加
acc += col * bokeh;
div += bokeh;
}
// 归一化输出
return max(vec3(0), acc / div);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.x;
// 动态模糊半径(0.0-0.8循环)
float time = mod(iTime * 0.2 + 0.25, 3.0);
float rad = 0.8 - 0.8 * cos(time * 6.283);
// 鼠标交互覆盖
if (iMouse.z > 0.0) {
rad = (iMouse.x / iResolution.x) * 3.0;
}
// 多纹理切换演示
if (time < 1.0) {
fragColor = vec4(Bokeh(iChannel0, uv, rad), 1.0);
} else if (time < 2.0) {
fragColor = vec4(Bokeh(iChannel1, uv, rad), 1.0);
} else {
fragColor = vec4(Bokeh(iChannel2, uv, rad), 1.0);
}
}
这个Bokeh着色器虽然代码量不大,但蕴含了许多精妙的设计:
在实际项目中,我通常会根据具体需求进行以下调整:
这个算法的框架还可以扩展到其他效果,如: