☰
用 Processing 从零渲染 Mandelbulb:三维 Mandelbrot 分形的点云可视化实战
2026/10/13 6:09:59 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

导读

本文以 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 次迭代后仍未逃逸,则视为集合内部 } } }

关键参数一览:

参数取值作用与影响
DIM128(Cloud)/ 256(Color)/ 64(Shape)体素网格分辨率,越大表面越精细,但三重循环的计算量按立方增长
n8三重数幂次,决定花瓣数量与整体造型
maxiterations20最大迭代次数,判定"是否属于集合"的阈值
空间范围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):

  1. 对每个像素构造归一化方向射线ray;
  2. 从相机位置pos = (0, 0, -10)出发;
  3. 每步计算dist = boxSDF(pos, ...),沿射线前进dist距离;
  4. 当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)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

相关推荐

上一篇:ECS集群安全更新策略:AMI更新与实例替换最佳实践指南
下一篇:React Grab 实战指南:把浏览器里的 UI 元素一键变成 Agent 可直接使用的源码上下文

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询