- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
导读
本文以 Coding Train 第 168 期编程挑战《The Mandelbulb》为核心,完整讲解"曼德博集合(Mandelbrot Set)如何扩展到三维空间成为 Mandelbulb(曼德博球)",并基于仓库中的 Processing 源码,从三重数(Triplex Number)数学原理、球坐标迭代公式、体素(Voxel)遍历,到带 PeasyCam 交互与颜色/贴图优化的点云渲染,给出可直接运行、可逐步调参的完整实现路径。读完本文,你将掌握 Mandelbulb 的核心迭代公式、点云边界提取算法,以及如何用 Processing(Java)搭建一个可旋转、可着色的三维分形查看器。
关联文档位于 _CodingChallenges/168-mandelbulb.md,配套完整源码位于 CodingChallenges/CC_168_Mandelbulb/Processing,包含 5 个可运行的 Processing 工程。
一、从 Mandelbrot 到 Mandelbulb:问题定义
1.1 二维 Mandelbrot 集的迭代本质
Mandelbrot 集的定义基于复数平面上的迭代:
zₙ₊₁ = zₙ² + c其中c是复平面上的一个点,z₀ = 0。如果迭代序列zₙ不发散(模长始终不超过 2),则该点属于 Mandelbrot 集;否则属于集合外部,迭代次数n决定了其"逃逸速度",可用于着色。
仓库中该主题的前置挑战 CC_021_Mandelbrot 用 p5.js 实现了这一经典算法,其核心循环是:
var aa = a * a - b * b; // 实部平方减虚部平方 var bb = 2 * a * b; // 交叉项 a = aa + ca; b = bb + cb; if (a * a + b * b > 16) { // 模长平方超过 16 即视为逃逸 break; }注意逃逸判定用的是a*a + b*b > 16,即模长r > 4就中断迭代;而在 Mandelbulb 版本中判定阈值取r > 2,两者是等价的判定思路(临界逃逸半径),只是系数习惯不同。
1.2 三维化面临的数学难题
二维复数的乘法可以用"模长相乘、辐角相加"来描述,但三维空间中没有与复数完全等价的"三重数(triplex number)"代数系统,直接定义z² + c的三维乘法是数学上不成立的。因此,社区(尤其是 Daniel White 与 Paul Nylander 的探索,参见视频中引用的 skytopia 资料)采用了一个工程上可行的替代方案:
在三维空间中,用球坐标来描述迭代点,把复数的"辐角相乘"替换为"球坐标角度乘以幂次 n"。
这正是 Mandelbulb 的核心思想,也是视频时间轴中"Triplex numbers(4:31)"与"Spherical coordinates(7:19)"两节所讲解的内容。
二、核心数学:三重数幂公式与球坐标转换
2.1 幂运算的球坐标形式
令三维迭代点p = (x, y, z),将其转换到球坐标:
r = sqrt(x² + y² + z²) θ = atan2(sqrt(x² + y²), z) // 与 z 轴的夹角 φ = atan2(y, x) // 与 x 轴的方位角则幂pⁿ的计算公式为:
pⁿ = (rⁿ·sin(nθ)·cos(nφ), rⁿ·sin(nθ)·sin(nφ), rⁿ·cos(nθ))这就是仓库中 MandelBulb_Cloud.pde 的迭代核心:
Spherical c = spherical(zeta.x, zeta.y, zeta.z); float newx = pow(c.r, n) * sin(c.theta*n) * cos(c.phi*n); float newy = pow(c.r, n) * sin(c.theta*n) * sin(c.phi*n); float newz = pow(c.r, n) * cos(c.theta*n); zeta.x = newx + x; zeta.y = newy + y; zeta.z = newz + z;这里n = 8,是 Mandelbulb 最经典的幂次("power 8"),不同n值会生成不同形状的花瓣状分形。zeta是迭代变量(对应二维中的z),每轮迭代后都要加上初始点(x, y, z)(对应二维中的c)。
2.2 笛卡尔坐标转球坐标的实现
仓库三个主要变体都复用了同一个转换函数(以 MandelBulb_Color.pde 为例):
Spherical spherical(float x, float y, float z) { float r = sqrt(x*x + y*y + z*z); float theta = atan2( sqrt(x*x+y*y), z); float phi = atan2(y, x); return new Spherical(r, theta, phi); }要点说明:
θ使用atan2(sqrt(x²+y²), z):分母是z而非常见的atan2(y, x)式的极角,这是以z 轴为极轴的球坐标约定;φ使用atan2(y, x):方位角以 x 轴为参考;atan2两个参数顺序不能写反,否则角度会反转,生成的形状将发生镜像;- 逃逸判据是
c.r > 2(模长超过逃逸半径),一旦逃逸立即跳出循环,不再加入点云。
三、体素化遍历:如何"挖出"分形表面
3.1 三维网格遍历策略
Mandelbulb 是三维点集,无法像二维那样逐像素扫描。仓库采用体素(Voxel)扫描 + 边界提取的策略:把[-1, 1]³的空间均匀切分为DIM³个小格子,对每个格子中心点执行迭代,判断其是否属于集合内部。
以 MandelBulb_Cloud.pde 的三重循环为例:
for (int i = 0; i < DIM; i++) { for (int j = 0; j < DIM; j++) { boolean edge = false; for (int k = 0; k < DIM; k++) { float x = map(i, 0, DIM, -1, 1); float y = map(j, 0, DIM, -1, 1); float z = map(k, 0, DIM, -1, 1); // ... 迭代 zeta,若 20 次迭代后仍未逃逸,则视为集合内部 } } }关键参数一览:
| 参数 | 取值 | 作用与影响 |
|---|---|---|
DIM | 128(Cloud)/ 256(Color)/ 64(Shape) | 体素网格分辨率,越大表面越精细,但三重循环的计算量按立方增长 |
n | 8 | 三重数幂次,决定花瓣数量与整体造型 |
maxiterations | 20 | 最大迭代次数,判定"是否属于集合"的阈值 |
| 空间范围 | map(i, 0, DIM, -1, 1) | 把体素索引映射到[-1, 1]的立方体空间 |
| 逃逸半径 | c.r > 2 | 超过即逃逸,停止迭代 |
3.2 边界提取的巧妙之处
注意三重循环的排列顺序:i(x)与j(y)在外层,k(z)在最内层,且edge标志在j循环内重置。每扫描完一整条 z 轴方向上的体素线后,只有"首次进入集合内部"的格子才会被记录:
if (iteration > maxiterations) { if (!edge) { edge = true; mandelbulb.add(new PVector(x, y, z)); } break; }这里的逻辑是:只有当某个体素经 20 次迭代仍未逃逸(判定为集合内部)时,才将其加入点云;由于edge标志的存在,同一条扫描线上重复命中的内部格子不会重复添加,从而只保留分形表面的"薄壳"轮廓。正是这种仅渲染表面点云的策略,让 128³ ≈ 209 万个体素中只有约 3.4 万个点被渲染(仓库生成的 mandelbulb.txt 实际包含 34252 行坐标数据),既保证了形状完整性,又控制了绘制开销。
从源码结构看,这种"扫线取边界"的方式与体素扫描本身是耦合的:如果改为只判断iteration > maxiterations而不加edge标志,会得到实心点云,同样能看,但绘制点数与耗时都会显著上升。
四、绘制与交互:PeasyCam + 点云渲染
4.1 加入 PeasyCam 实现三维漫游
三个主要变体都通过 PeasyCam 库实现鼠标拖拽旋转:
import peasy.*; int DIM = 128; PeasyCam cam; void setup() { size(600, 600, P3D); cam = new PeasyCam(this, 600); // 视点距离 600 }- 需要 Processing 3D 渲染模式
P3D,否则无法显示三维点; new PeasyCam(this, 600)的第二个参数是相机初始距离;- 使用前需通过 Processing 库管理器安装 PeasyCam 库(
Sketch → Import Library)。
4.2 点云的绘制循环
setup()阶段完成全部体素迭代与数据导出,draw()阶段则只负责绘制,保证每帧开销极低:
void draw() { background(0); rotateX(PI/4); rotateY(-PI/3); for (PVector v : mandelbulb) { stroke(255); strokeWeight(1); point(v.x*200, v.y*200, v.z*200); // 放大 200 倍显示 } }其中v.x*200等比例放大把[-1,1]的模型坐标映射到 600×600 画布的可视范围。rotateX(PI/4); rotateY(-PI/3);给了一个固定视角,PeasyCam 的交互旋转在此基础上叠加。
4.3 体素渲染的另一种形态
仓库中的 Voxels.pde 展示了完全不同的可视化思路——不生成点云,而是直接绘制体素立方体:
int DIM = 16; float angle = -PI/3; void draw() { translate(width/2, height/2, 100); rotateY(angle); angle += 0.02; // 自动旋转 for (int i = 0; i < DIM; i++) for (int j = 0; j < DIM; j++) for (int k = 0; k < DIM; k++) { float x = map(i, 0, DIM, -range, range); // ... if (y > x && y > z) { // 只绘制满足条件的体素 translate(x, y, z); box(2*range/DIM); } } }它演示了三个概念:
box(2*range/DIM)让相邻体素无缝拼接,是"体素渲染"的通用写法;if (y > x && y > z)这类条件过滤可以快速裁剪掉不关心的体素区域;angle += 0.02实现无需交互的自动旋转动画。
这段代码不涉及 Mandelbulb 迭代,是视频时间轴 5:11 处用于铺垫"体素概念"的辅助演示,可作为理解 Mandelbulb 点云渲染原理的入门对照。
五、变体进阶:颜色、贴图与光影
5.1 按迭代次数着色的点云(MandelBulb_Color)
MandelBulb_Color.pde 在 Cloud 版基础上引入了MandelPoint数据结构,把逃逸前的最后一次迭代次数lastIteration随坐标一起保存:
class MandelPoint { PVector v; float i; // 逃逸时的迭代次数 MandelPoint(PVector v, float i) { this.v = v; this.i = i; } }在while循环的逃逸分支记录迭代次数:
if (c.r > 2) { lastIteration = iteration; // 记录逃逸时的迭代次数 if (edge) { edge = false; } break; }绘制时用 HSB 色环映射迭代次数,让表面的"环带结构"随颜色呈现:
colorMode(HSB, 255); for (MandelPoint m : mandelbulb) { stroke(map(m.i, 0, maxiterations, 255, 0), 255, 255); point(m.v.x, m.v.y, m.v.z); }其中map(m.i, 0, maxiterations, 255, 0)把[0, 20]的迭代次数反向映射到[255, 0]的色相区间——迭代越慢逃逸(数值越小)的位置偏暖色,迭代越接近 20 的位置偏冷色,形成从外到内的渐变环带,这正是二维 Mandelbrot 逃逸时间着色的三维对应物。
5.2 用贴图点精灵代替小点(MandelBulb_Shape)
MandelBulb_Shape.pde 是"点云 + 贴图"的优化版,把单个point()换成带纹理的四边形(billboard 点精灵),让每个点呈现柔和光斑效果:
dot = loadImage("dot.png"); // 320×320 径向渐变圆点贴图 for (MandelPoint m : mandelbulb) { push(); translate(m.v.x, m.v.y, m.v.z); imageMode(CENTER); image(dot, 0, 0, 400/(DIM), 400/(DIM)); // 贴图尺寸随分辨率自适应 // box(400/(DIM)); // 也可替换为立方体体素 pop(); }细节要点:
400/(DIM)使点精灵尺寸与体素分辨率成反比:DIM = 64时每个点约6.25像素,密集排布后近似连续表面;imageMode(CENTER)让贴图以点坐标为中心;- 同一位置可用
box(400/(DIM))替换贴图,退化回体素渲染,便于两种效果对比; - 配合
lights(); ambientLight(255, 100, 200);开启光照,点精灵会受环境光着色,产生立体感; - 该贴图即仓库中的 dot.png,是一张 320×320 的径向渐变圆形纹理。
5.3 光线追踪 SDF 演示(RayMarching_SDF)
仓库还包含一个独立的 SDF(有符号距离场)光线追踪演示 RayMarching_SDF.pde,用于铺垫"如何用 raymarching 渲染三维分形":
float boxSDF(PVector b, PVector r) { PVector d = abs(b); d.sub(r); float m = max(d, new PVector(0, 0, 0)).mag(); return m + min(max(d.x, max(d.y, d.z)), 0.0); }其渲染流程(注释中注明灵感来自 observablehq 的@leon196/raymarch):
- 对每个像素构造归一化方向射线
ray; - 从相机位置
pos = (0, 0, -10)出发; - 每步计算
dist = boxSDF(pos, ...),沿射线前进dist距离; - 当
dist < 0.001时判定命中表面,用剩余步数n/count作为明暗shade写入像素。
float count = 20; float shade = 0; for (float n = count; n > 0; --n) { float dist = boxSDF(pos, new PVector(2, 2, 2)); if (dist < .001) { shade = n / count; // 命中越早越亮 break; } pos.add(PVector.mult(ray, dist)); }此外文件中还预留了wackySDF(对空间做 mod 取模后减去 1.0 的球体距离场)与注释掉的rotateY(ray, ...)/rotateX(ray, ...),用于实验运动网格与射线旋转。需要说明的是:该文件实现的是 SDF raymarching 的概念演示(球体/立方体距离场),并未包含 Mandelbulb 的完整迭代距离场,是理解"分形 + 光线追踪"组合方向的起点,社区贡献中出现的 "Mandelbulb Ray Marching in p5.js based on shadertoy.com/view/wdjGWR" 等作品即沿此路线延伸。
六、参数调节指南与运行环境
6.1 运行前提
- 环境:Processing 3.x(Java 模式),需安装 PeasyCam 库(仅点云变体需要,Voxels 与 RayMarching_SDF 不依赖外部库);
- 数据文件:点云变体会在草图目录生成
mandelbulb.txt,内含x y z三列空格分隔的坐标数据,格式见 MandelBulb_Cloud 的产物; - 计算开销:
DIM = 256意味着 256³ ≈ 1678 万次体素迭代,在setup()中一次性完成,首次运行会有数秒到数十秒的等待,属于正常现象(Color 版注释即提示"注意计算量")。
6.2 参数调参建议
| 目标 | 参数调整 | 说明 |
|---|---|---|
| 提升表面精度 | 增大DIM(如 256→512) | 分辨率每翻倍,体素数变为 8 倍,计算量陡增,需权衡 |
| 改变花瓣造型 | 调整n(如 6、8、10) | n决定绕 z 轴的花瓣数量与纵向分层 |
| 更清晰的集合判定 | 增大maxiterations | 阈值越高,边界越精细,但更慢 |
| 点云密度/疏密 | 调整绘制时的缩放系数(如v.x*200) | 仅影响显示尺度,不影响几何 |
| 观察视角 | 修改rotateX(PI/4); rotateY(-PI/3); | 调整默认视角,配合 PeasyCam 拖拽旋转 |
6.3 衍生方向(来自文档的 contributions 记录)
关联文档记录了社区在该主题上的多种衍生实现,可作为继续探索的路线图:
- p5.js 移植:如 "Rainbow Mandelbulb Cloud (p5js)"、"Mandelbulb with raymarching and shaders"、"Cinderella mandelbulb rendered in p5.js with phong illumination lighting",把体素迭代或 SDF raymarching 迁移到浏览器端;
- WebGL/Three.js:如 "Three.js (React) Mandelbulb Port using Web Worker",用 Web Worker 分担计算;
- 边缘检测优化:如 "Mandelbulb with better edge detection",改进边界提取算法以减少噪点。
这些社区作品表明:本挑战的核心产出(幂公式、体素扫描、逃逸着色)是一套可复用到任意渲染栈的分形生成管线。
七、结语:一条从 2D 到 3D 的完整分形管线
回看整个挑战,可以提炼出一条清晰的实现链路:
二维 Mandelbrot(z² + c) → 三维扩展(球坐标幂公式 p⁸ + c) → 体素网格扫描 + 边界提取 → 点云/体素/贴图精灵渲染 → PeasyCam 交互 + 逃逸次数着色 →(可选)SDF raymarching 光照渲染每一步在仓库中都有可运行的对应工程:MandelBulb_Cloud 是最简版本,MandelBulb_Color 增加迭代着色,MandelBulb_Shape 增加贴图与光照,Voxels 演示体素渲染概念,RayMarching_SDF 铺垫光线追踪路线。若想从二维起点温习,可对照 CC_021 的 p5.js 实现 与 CC_025 的球坐标几何(后者与本文的球坐标转换一脉相承)。
推荐实验路径:先以DIM = 64、n = 8跑通最简版,再逐步提高到 128、256,观察花瓣结构与边界精度随分辨率的变化;随后把n改为 6 或 10,对比造型差异,即可直观理解"幂次决定分形拓扑"这一核心结论。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
Unity高斯点云渲染实战手册:从零构建实时3D可视化系统
想要在Unity中实现惊艳的实时3D点云渲染效果吗?Unity Gaussian Splatting项目正是你需要的利器!这个基于SIGGRAPH 2023重磅
图形学3D渲染咖啡烘焙师的数字实验室:5个关键功能让你从直觉烘焙迈向数据驱动
咖啡烘焙师的数字实验室:5个关键功能让你从直觉烘焙迈向数据驱动 你是否曾经在烘焙咖啡时,只能依靠"感觉"来判断豆子的状态?当一爆声响起时,你是否会犹豫是否应该提
桌面应用数据可视化数据分析UnityGaussianSplatting 完整使用指南:从零开始掌握三维高斯点云渲染
UnityGaussianSplatting 完整使用指南:从零开始掌握三维高斯点云渲染 想要在Unity中实现令人惊艳的实时三维场景渲染吗?UnityGaus
图形学3D渲染
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考