效果预览一个由迭代函数系统IFS生成的三维分形结构通过 Ray Marching 渲染出体积光 glow 效果。中心分形球体呈现紫蓝粉渐变的发光碎片结构周围环绕深紫红色星空背景。相机围绕物体持续旋转分形本身也在多轴上同时旋转产生迷幻的动态视觉效果。 点击查看《分形金字塔的》完整源码与效果演示Shader 实现原理1. 整体思路 — Ray Marching IFS 分形传统多边形渲染需要显式定义几何体顶点、面片但分形具有无限细节的特性无法用最多数万个三角形表达。这里的解决方案是隐式曲面定义一个距离场函数map(p)返回空间中任意点p到分形表面的距离从相机位置沿每条视线射出 ray通过距离场逐步前进Sphere Tracing每步积累发光颜色距离场值越小积累越强形成体积光 glow远离分形的区域用星空背景填充这不是表面渲染而是体积渲染光线穿过发光介质时的能量积累。2. 调色板 — 线性插值的色彩空间vec3 palette(float d) { return mix(vec3(0.2, 0.7, 0.9), vec3(1.0, 0.0, 1.0), d); }mix(a, b, t)在 GLSL 中是线性插值a * (1-t) b * t。t 0时输出vec3(0.2, 0.7, 0.9)——青色高绿、高蓝t 1时输出vec3(1.0, 0.0, 1.0)——品红色高红、高蓝0 t 1时输出两者的线性过渡这个调色板的选择刻意避开了完整的 RGB 色轮只在青-品红这条线上扫过形成冷色调的科技感。参数d length(p) * 0.1以到原点的距离为索引越远越偏品红越近越偏青。3. 二维旋转矩阵vec2 rotate(vec2 p, float a) { float c cos(a); float s sin(a); return p * mat2(c, s, -s, c); }mat2(c, s, -s, c)是标准的二维旋转矩阵。数学上逆时针旋转角度a的变换矩阵为| cos(a) -sin(a) | | sin(a) cos(a) |GLSL 的mat2构造函数按列优先存储mat2(c, s, -s, c)等价于| c -s | | s c |注意p * mat2(...)是行向量左乘矩阵数学上等价于标准的列向量变换。rotate函数被复用在多个地方分形内部的自旋转、相机围绕物体的公转。4. 分形距离场 — Kaleidoscopic IFSfloat map(vec3 p) { for (int i 0; i 8; i) { float t iTime * 0.2; p.xz rotate(p.xz, t); p.xy rotate(p.xy, t * 1.89); p.xz abs(p.xz); p.xz - 0.5; } return dot(sign(p), p) / 5.0; }这是**Kaleidoscopic Iterated Function System万花筒迭代函数系统**的经典结构每一轮迭代包含四个操作4.1 双轴旋转p.xz rotate(p.xz, t); p.xy rotate(p.xy, t * 1.89);XZ平面和XY平面分别绕不同角速度旋转。1.89是一个无理数倍率接近2 - 1/e确保两轴旋转不会形成周期性锁定产生准周期运动。4.2 绝对值折叠 — 对称性生成p.xz abs(p.xz);abs()将XZ平面的负半轴折叠到正半轴这是镜像对称操作。单次折叠产生 2 重对称8 次迭代后形成2^8 256重对称碎片。这个操作是分形自相似性的核心每次折叠都把空间掰成更小的镜像副本无限迭代下去会产生分形维度大于 2 的曲面。4.3 平移 — 缩放与位移p.xz - 0.5;每次折叠后向内收缩0.5个单位。这个位移量直接控制分形的密度值越大 → 碎片越分散内部更空旷值越小 → 碎片越密集趋向实心球体0.5是一个经验值在视觉上形成碎片环结构 —— 不是实心球而是有镂空的发光环面。4.4 距离场输出return dot(sign(p), p) / 5.0;sign(p)提取p各分量的符号-1 或 1dot(sign(p), p)等价于|p.x| |p.y| |p.z|—— 这是L1 范数曼哈顿距离。除以5.0是缩放因子把距离场的尺度压缩使 ray marching 的步长更细密。如果没有这个缩放分形会太大相机在z -50处只能看到局部。5. Ray Marching — 体积光积累vec4 rm(vec3 ro, vec3 rd) { float t 0.0; vec3 col vec3(0.0); float d; for (float i 0.0; i 64.0; i) { vec3 p ro rd * t; d map(p) * 0.5; if (d 0.02) break; if (d 100.0) break; col palette(length(p) * 0.1) / (400.0 * d); t d; } return vec4(col, 1.0 / (d * 100.0)); }5.1 Sphere Tracing 步进t是 ray 上已走过的距离p ro rd * t是当前采样点。d map(p) * 0.5是该点到分形表面的安全距离。d 0.02时认为已经 hit 到表面退出循环。0.02是容差阈值太小 → 步数增加可能穿透表面太大 → 提前终止丢失细节d 100.0时认为 ray 已经远离分形退出循环节省计算。5.2 体积光积累 — 反比发光模型col palette(length(p) * 0.1) / (400.0 * d);这不是传统光照光源 → 表面 → 相机而是发光介质的体积吸收palette(length(p) * 0.1)—— 根据到原点的距离选取颜色1 / d—— 距离场越小越接近分形表面发光越强1 / 400.0—— 整体衰减系数防止过曝从物理角度这近似于等离子体或星云的发射密度越高距离场越小的区域光子发射越强。1/d的衰减规律不是真实物理真实物理是1/d²但在视觉上更讨喜因为1/d的衰减更慢能产生更柔和的光晕。5.3 Alpha 通道的含义return vec4(col, 1.0 / (d * 100.0));Alpha 不是传统的不透明度而是深度权重hit 时d ≈ 0.02alpha ≈ 0.5远离时d 100alpha ≈ 0这个值在 main 函数中用于区分前景分形和背景星空。6. 星空背景 — Hash 与闪烁float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } vec3 background(vec3 rd) { vec3 bg vec3(0.05, 0.015, 0.04); float sky max(rd.y, 0.0); bg mix(bg, vec3(0.08, 0.025, 0.07), sky * sky); vec2 su rd.xz / (abs(rd.y) 0.5); float s hash(floor(su * 200.0)); float tw sin(iTime * 2.0 s * 50.0) * 0.5 0.5; bg vec3(0.55, 0.40, 0.50) * smoothstep(0.997, 1.0, s) * tw * 0.5; return bg; }6.1 Hash 函数 — 确定性伪随机hash(vec2)是 GPU 上最常用的廉价随机数生成器。核心操作dot(p, vec2(127.1, 311.7))—— 把二维坐标投影到一维sin(...)—— 利用正弦的混沌特性放大微小差异* 43758.5453—— 进一步打散周期fract(...)—— 取小数部分归一化到[0, 1)127.1和311.7的选择原则是与像素坐标的整数部分没有简单有理数关系避免摩尔纹。43758.5453同理。6.2 球面坐标到平面的映射vec2 su rd.xz / (abs(rd.y) 0.5);rd是单位方向向量球面坐标。直接把它当随机数输入会出问题球面上均匀分布的方向在xz平面上的投影不均匀两极密度高。除以abs(rd.y) 0.5是一种近似等面积投影让星星在球面上分布更均匀。6.3 闪烁 — 相位偏移的正弦float tw sin(iTime * 2.0 s * 50.0) * 0.5 0.5;每颗星星的闪烁频率相同iTime * 2.0但相位不同s * 50.0。这模拟了大气扰动造成的星光闪烁每颗星的明暗周期相同但起始时刻随机。6.4 阈值化 — 从噪声到离散星星smoothstep(0.997, 1.0, s)hash输出在[0, 1)均匀分布smoothstep(0.997, 1.0, s)把 0.997的极少数值映射到(0, 1]其余全部压为 0。这是一个硬阈值把连续噪声变成稀疏的离散点 —— 即星星。概率上每200 * 200 40000个 grid 点中约有40000 * 0.003 120颗可见星密度适中。7. 相机矩阵 — Look-At 的简化版vec3 ro vec3(0.0, 0.0, -50.0); ro.xz rotate(ro.xz, iTime); vec3 cf normalize(-ro); vec3 cs normalize(cross(cf, vec3(0.0, 1.0, 0.0))); vec3 cu normalize(cross(cf, cs));这里构建了相机坐标系的三个基向量roray origin— 相机位置初始在(0, 0, -50)绕 Y 轴旋转形成轨道运动cfcamera forward— 相机朝向始终指向原点-ro归一化cscamera side— 相机右方向cross(forward, world_up)cucamera up— 相机上方向cross(forward, side)这是Look-At 矩阵的手动构建版本。cross顺序很重要cross(cf, vec3(0,1,0))产生右向量cross(cf, cs)产生修正后的上向量确保三者正交。vec3 uuv ro cf * 3.0 uv.x * cs uv.y * cu; vec3 rd normalize(uuv - ro);cf * 3.0把成像平面放在相机前方 3 个单位处。uv.x * cs uv.y * cu在成像平面上铺展像素。最终rd是从相机指向每个像素的方向向量。8. 前景与背景合成vec4 col rm(ro, rd); vec3 bg background(rd); float fg length(col.rgb); float bgMask smoothstep(0.008, 0.0, fg); col.rgb mix(col.rgb, bg, bgMask);合成策略基于亮度阈值fg length(col.rgb)—— 分形输出的亮度smoothstep(0.008, 0.0, fg)—— 亮度低于0.008时完全显示背景高于0.008时完全显示分形0.008这个阈值的选择依据ray marching 在远离分形区域 64 步积累后的 rgb 长度约0.005 ~ 0.01刚好落在阈值边缘。这样分形球体的发光区域和星空背景有硬边过渡不会互相渗透。9. 性能分析阶段每像素开销瓶颈map()距离场8 次旋转 8 次折叠 8 次平移ALU纯计算Ray Marching 循环最多 64 次map()调用分支发散不同像素 hit/未 hit体积光积累1 次palette() 除法 加法ALU背景星空2 次hash()sin()ALU在现代桌面 GPU 上这个 shader 在 1080p 下可以稳定 60fps。主要瓶颈是map()中的循环每个像素最多执行 8 次rotate含 2 次sin 2 次cos64 步就是 512 次三角函数调用。GPU 的sin/cos是硬件指令但仍有明显延迟这是限制分辨率的主要因素。总结这个特效展示了隐式分形 体积渲染的强大组合分形不是画出来的是折叠出来的 —— 8 轮旋转、镜像、平移把空间拧成一个自相似的无穷结构光照不是照出来的是积累出来的 —— 每步 ray marching 往颜色 buffer 里加一点点 glow距离越近加得越猛星空不是贴图是筛出来的 —— Hash 噪声经过阈值化99.7% 的区域变黑0.3% 的区域变成星星