☰
用 p5.js 与 Processing 模拟黑洞:光子绕行轨迹 2D 可视化实战(Coding Challenge 144)
2026/10/13 2:54:10 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

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

2019 年 4 月,事件视界望远镜(Event Horizon Telescope)公布了 M87 星系中心黑洞 Powehi 的首张直接影像,这张照片启发了 Coding Challenge #144:用牛顿引力近似在 2D 画布上模拟光子(光线)沿弯曲时空环绕黑洞的轨迹。本文以仓库中 144-black-hole-visualization.md 的挑战说明为骨架,结合其完整源码,从物理模型、p5.js 实现拆解、Processing 移植到参数调优,一步步带你复现一个可运行的黑洞光线偏折模拟器。

一、挑战背景:从 M87 黑洞照片到 2D 模拟

原文档开篇点明了整个挑战的缘起:

Inspired by the recent image of the Powehi m87 black hole from the Event Horizon Telescope, I attempt to visualize the behavior or light (photons) following the path of spacetime around the black hole.

即:受 EHT 发布的 M87 黑洞(Powehi)影像启发,尝试可视化光子沿黑洞周围弯曲时空运动的轨迹。这张照片首次直观展示了"事件视界"(黑洞边界)与周围发光吸积盘的轮廓——中央暗区、明亮的光环,正是本项目在 2D 画布上努力复现的核心视觉要素。

围绕这一主题,原文档还关联了两个重要的参考视频与若干扩展资料,可作为理解物理模型的背景铺垫:

  • Veritasium 的 "How to Understand the Image of a Black Hole":解释为什么黑洞"照片"看起来是那个样子,涉及引力透镜、光子球等概念;
  • STEM Coding 的 "Slingshot with Gravity!":展示如何用万有引力做"弹弓"加速,与本项目"引力偏折光子"的思路一脉相承;
  • "Simulating a Black Hole!"与"How to draw a Black Hole"等文章,讨论了黑洞模拟与绘制的不同方法;
  • 文档中的相关链接还区分了牛顿版本与相对论修正版本(后者使用不同的积分方式,见下文"从牛顿近似到广义相对论"一节)。

在本仓库中,该挑战的实现位于 CodingChallenges/CC_144_Black_Hole_Newtonian,同时提供p5.js与Processing两套等价实现,源码完全对应,适合对照学习。

二、物理模型:牛顿引力近似下的光线偏折

在真正进入代码前,先建立模拟所依赖的物理模型。本项目刻意采用牛顿引力近似(仓库目录名CC_144_Black_Hole_Newtonian即表明这一点),其核心假设是:

  1. 光子是携带质量的质点,会被黑洞的引力吸引——用牛顿万有引力公式计算引力加速度;
  2. 光速恒定:无论引力如何改变光子的运动方向,其速度大小始终等于光速常量c;
  3. 事件视界即"终点":一旦光子进入黑洞的史瓦西半径(Schwarzschild radius)之内,就认为它被黑洞吞噬,停止运动。

对应地,源码在 sketch.js 顶部定义了三个全局常量:

const c = 30; // 模拟世界中的"光速" const G = 2; // 万有引力常量 const dt = 0.1; // 时间步长(欧拉积分步长)

这里c、G都不是真实物理常数,而是经过缩放、便于在画布像素尺度上呈现动画的"模拟常量"——这是本挑战的一个重要设计取向:视觉真实性优先于数值保真度。

黑洞的史瓦西半径由 blackhole.js 的构造函数计算:

class Blackhole { constructor(x, y, m) { this.pos = createVector(x, y); this.mass = m; this.rs = (2 * G * this.mass) / (c * c); } // ... }

即经典的史瓦西半径公式:

rs = 2GM / c²

在主程序中(sketch.js),黑洞被放置在画布正中心,质量取 10000:

function setup() { createCanvas(windowWidth, windowHeight); m87 = new Blackhole(width / 2, height / 2, 10000); // ... }

m87这个变量名直接致敬了 M87 星系中心的真实黑洞。结合G = 2、c = 30,可算出该黑洞的史瓦西半径约为:

rs = (2 × 2 × 10000) / (30 × 30) ≈ 44.4(像素)

也就是说,黑洞事件视界在屏幕上是一个直径约 89 像素的黑色圆盘,这个尺度既保证了视觉冲击力,又为光子轨迹留出了充足的偏折空间。

三、p5.js 实现拆解:三个文件、三个职责

p5.js 版本由三个源文件构成(加载顺序见 index.html):photon.js(光子)、blackhole.js(黑洞)、sketch.js(主流程)。这种划分清晰体现了面向对象的设计思路:光子是被引力作用的个体,黑洞是引力场源,主程序负责把它们组织到一起。

3.1 主流程:光子的发射与参考线

sketch.js 的setup()完成两件事:创建黑洞;在画布左缘垂直方向上、从顶部到画布中线之间,每隔 10 像素发射一束光子,所有光子初始速度均为(-c, 0),即向左匀速运动:

start = height / 2; end = height / 2 - m87.rs * 2.6; for (let y = 0; y < start; y += 10) { particles.push(new Photon(width - 20, y)); }

draw()每一帧执行标准的三步循环:黑洞对每个光子施力 → 光子更新位置 → 光子绘制;同时画出两条水平参考线(start与end),方便观察光子轨迹被引力"拉弯"的程度:

function draw() { background(255); stroke(0); strokeWeight(1); line(0, start, width, start); line(0, end, width, end); for (let p of particles) { m87.pull(p); p.update(); p.show(); } m87.show(); }

注意end = height / 2 - m87.rs * 2.6:这条线位于黑洞上方约 2.6 倍史瓦西半径处,作为"引力影响范围"的视觉标尺。由于光子从左向右发射时只被施加以指向黑洞的加速度,最终轨迹会呈现出不同程度的弯折——离黑洞越近的光子弯得越厉害,直至坠入视界。

3.2 Blackhole 类:引力施力与事件视界判定

黑洞的核心逻辑集中在pull(photon)方法(blackhole.js):

pull(photon) { const force = p5.Vector.sub(this.pos, photon.pos); const r = force.mag(); const fg = (G * this.mass) / (r * r); force.setMag(fg); photon.vel.add(force); photon.vel.setMag(c); if (r < this.rs) { photon.stop(); } }

这段代码按顺序完成了五件事:

  1. 求引力方向:用p5.Vector.sub(this.pos, photon.pos)得到从光子指向黑洞的向量;
  2. 求距离:force.mag()即光子到黑洞中心的距离r;
  3. 牛顿引力加速度:fg = G * mass / r²,与距离平方成反比;
  4. 改变方向但不改变速率:先photon.vel.add(force)让引力改变速度向量,再photon.vel.setMag(c)把速度大小强制拉回光速c。这一步是"光速恒定"假设的实现关键——引力只弯曲光的传播方向,不改变光速;
  5. 事件视界吞噬:当r < rs(光子进入史瓦西半径内部)时调用photon.stop(),光子从此不再运动、不再累积轨迹。

show()方法(blackhole.js)用三层同心圆绘制黑洞的视觉形态:

show() { ellipseMode(RADIUS); fill(0); noStroke(); ellipse(this.pos.x, this.pos.y, this.rs); // 黑色事件视界圆盘 noFill(); stroke(100, 100); strokeWeight(64); ellipse(this.pos.x, this.pos.y, this.rs * 3 + 32); // 灰色"引力透镜"光晕 stroke(255, 150, 0, 100); strokeWeight(32); ellipse(this.pos.x, this.pos.y, this.rs * 1.5 + 16);// 橙色吸积盘发光 }
  • 第一层:半径rs的纯黑圆盘,代表事件视界本身(连光都无法逃脱的区域);
  • 第二层:半径rs*3 + 32、线宽 64 的半透明灰色圆环,模拟光子球与强引力透镜造成的光晕;
  • 第三层:半径rs*1.5 + 16、线宽 32 的橙色半透明圆环,对应真实黑洞照片中最醒目的高温吸积盘辐射。

三者的叠色与半透明描边在白色背景上能产生类似"黑洞周围发光"的视觉层次,这正是对 M87 照片"暗影 + 亮环"结构的高度抽象。

3.3 Photon 类:匀速运动、轨迹记录与停止

photon.js 定义了光子对象,构造函数初始化位置、速度((-c, 0))、轨迹历史数组与停止标志:

class Photon { constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(-c, 0); this.history = []; this.stopped = false; this.theta = 0; } // ... }

update()实现欧拉积分(Euler method)推进位置,并维护一条最多 500 个点的轨迹:

update() { if (!this.stopped) { this.history.push(this.pos.copy()); const deltaV = this.vel.copy(); deltaV.mult(dt); this.pos.add(deltaV); } if (this.history.length > 500) { this.history.splice(0, 1); } }
  • 位置更新公式即pos += vel × dt,其中vel的方向在每一帧已被Blackhole.pull()修正过;
  • 源码中保留了// if (frameCount % 5 == 0)的注释,提示可以按帧间隔采样轨迹点以降低轨迹密度;
  • 历史数组超过 500 个点时从头部移除,保证轨迹线长度可控、内存占用稳定。

show()先用 4 像素红色圆点绘制光子当前位置,再用 2 像素红色折线把整条历史轨迹画出来:

show() { strokeWeight(4); stroke(255, 0, 0); point(this.pos.x, this.pos.y); strokeWeight(2); noFill(); beginShape(); for (let v of this.history) { vertex(v.x, v.y); } endShape(); }

红色轨迹线正是整个可视化最直观的输出:读者可以清楚看到左侧发射的一排光子,在接近黑洞时轨迹逐渐弯曲,靠近视界的光子被吸入,稍远的光子则绕出一个弧线后继续向左下方飞出。

四、Processing 移植:逻辑一致、API 替换

除了 p5.js,仓库还提供了完整的 Processing 版本,位于 Processing/CC_114_Black_Hole_Newtonian(目录名沿用了历史编号 CC_114),包含CC_114_Black_Hole_Newtonian.pde、blackhole.pde、photon.pde三个文件,与 p5.js 版本逐行对应。两者差异集中在 API 与数据类型层面:

方面p5.js 版本Processing 版本
画布createCanvas(windowWidth, windowHeight)fullScreen()(主文件)
向量p5.VectorPVector
粒子容器JS 数组[]+push()ArrayList<Photon>+add()(主文件)
椭圆绘制ellipse(x, y, r)(RADIUS 模式)ellipse(x, y, r, r)

以黑洞的pull()为例,blackhole.pde 与 p5.js 版几乎逐行一致:

void pull(Photon photon) { PVector force = PVector.sub(this.pos, photon.pos); float r = force.mag(); float fg = G * this.mass / (r * r); force.setMag(fg); photon.vel.add(force); photon.vel.setMag(c); if (r < this.rs) { photon.stop(); } }

p5.js 与 Processing 双版本并存的价值在于:同一套物理与视觉设计,可以在浏览器中零依赖运行,也可以在 Processing IDE 中以全屏模式演示,方便不同场景下的教学与二次开发。

五、运行方式

5.1 在浏览器中运行 p5.js 版本

仓库中的 index.html 已经通过 CDN 引入 p5.js 1.4.1,并把三个脚本按photon.js → blackhole.js → sketch.js的顺序加载。只需用浏览器打开该 HTML 文件即可看到效果:

<script src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script> <script src="photon.js"></script> <script src="blackhole.js"></script> <script src="sketch.js"></script>

运行前提:需要联网加载 p5.js CDN;若离线环境,可将 p5.min.js 下载到本地并修改src指向本地文件。页面样式设置canvas { display: block; },避免画布下方出现多余空白。

5.2 运行 Processing 版本

将 Processing/CC_114_Black_Hole_Newtonian 目录作为 Processing sketch 打开(保持三个.pde文件在同一目录),点击运行即可全屏演示。注意主文件使用fullScreen(),运行环境需支持全屏窗口。

六、参数调优指南

模拟的"观感"完全由c、G、dt与黑洞质量m四个参数决定,理解它们的作用即可自由调出不同的视觉效果:

参数位置作用调大后效果
c(光速)sketch.js光子速度常量,也是rs = 2GM/c²的分母光子飞得更快,轨迹更平直,史瓦西半径变小
G(引力常量)sketch.js引力加速度GM/r²的系数偏折更剧烈,光子更容易被吸入视界
m(黑洞质量)sketch.js直接线性放大rs与引力视界变大,光晕与吸积盘随之放大
dt(时间步长)sketch.js欧拉积分的步长运动速度变快;过大时轨迹会出现明显锯齿失真
光子间距 10sketch.js左侧发射光子的垂直间隔调小则光束更密集,弯曲"束流"感更强

调参建议:若想让弯曲更明显,可先增大G或减小c;若想让事件视界更醒目,直接增大质量m。由于牛顿模型下光子速度被强制恒定为c,参数组合只要保持rs在几十像素量级,通常都能得到稳定的动画。

需要说明的限制:欧拉积分是显式一阶方法,dt过大或引力过强(fg过大)时,光子在靠近黑洞的强场区域可能发生明显数值漂移。这也是原文档专门附上"相对论修正版本"(web_editor: 4DvaeH0Ur)的原因——更精确的模拟需要更换积分策略。

七、从牛顿近似到广义相对论:社区扩展方向

原文档在contributions一节收录了大量社区衍生作品,展示了这一模拟可以继续深挖的多个方向,也印证了"牛顿近似只是起点":

  • 相对论修正:使用测地线方程等广义相对论方法替代牛顿引力,轨迹更贴近真实时空弯曲;
  • 光子球与吸积盘细节:模拟光子在黑洞周围的多圈环绕(光子球轨道),复现 M87 照片中光环的精细结构;
  • 3D 化:多个贡献者将模拟从 2D 画布扩展到 3D 场景,在 Processing 与 p5.js 中渲染黑洞的立体形态;
  • 旋转黑洞(Kerr)轨道:加入角动量,模拟旋转黑洞周围的光子轨道;
  • 跨界融合:有贡献者将 flocking 群体行为与黑洞引力结合(FlockingBlackHole),把"粒子被引力偏折"的模型复用到群体模拟中,证明该引力-粒子框架具有通用性。

这些方向与原文档"visualize the behavior of light following the path of spacetime"的主旨一脉相承——本项目的价值不仅是画出一张"黑洞照片",更是为理解引力透镜、事件视界、光子球等概念提供了一个可交互、可修改的最小模型。

八、小结

Coding Challenge #144 用约百行代码完成了三件事:以rs = 2GM/c²定义事件视界,以fg = GM/r²计算引力偏折,以"速度恒定、方向可变"模拟光速不变原理。通过阅读 sketch.js、blackhole.js 与 photon.js,你可以清楚地看到一条从物理公式到像素渲染的完整链路;而 Processing 版本则提供了脱离浏览器、全屏演示的替代方案。在此基础上,无论是调整参数观察偏折变化,还是沿"相对论修正 / 3D 化 / 旋转黑洞"方向二次开发,本项目都是一个理想的起点。

  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载
上一篇:nixpkgs 中 nexusmods-app 的 vendored 游戏数据:games.json 生成、缓存与构建集成机制解析
下一篇:网盘直链下载助手:解锁9大网盘高速下载的终极免费方案

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

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

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

立即咨询