首页
/ D3 forceCollide 碰撞力详解:radius、strength 与 iterations 的机制、参数与实战

D3 forceCollide 碰撞力详解:radius、strength 与 iterations 的机制、参数与实战

2026-09-04 17:12:35作者:俞予舒Fleming

本文围绕 D3 官方文档 docs/d3-force/collide.md 展开,系统讲解 d3-force 模块中的碰撞力 forceCollide:它如何把节点当作圆而非点来处理,三个 API(forceCollide(*radius*)*collide*.radius*collide*.strength*collide*.iterations)的默认值与调用时机,以及其“软约束 + 迭代松弛”的工作原理。读完你可以直接复现仓库内置的 200 圆交互示例,并理解碰撞力在模拟 tick 循环中的位置与调优思路。

碰撞力是什么:圆形节点与软约束

碰撞力把节点视为具有给定半径的圆,而不是点,从而阻止节点相互重叠。文档给出的严格定义是:两个节点 ab 会被分开,使得二者之间的距离至少为 radius(a) + radius(b)。

需要注意的关键设计:为了减少抖动(jitter),碰撞力默认是一个“软”约束(soft constraint),而不是硬性排斥——它的松紧程度由两个可配置项控制:

  • strength:控制重叠修正的衰减强度,默认 1;
  • iterations:控制每次施加力时的迭代次数,默认 1。

也就是说,碰撞力并不保证每一步都“完全推开”,而是通过迭代松弛逐步趋近于稳定解;这正是它与 d3-pack 这类确定性圆形打包布局的本质区别。碰撞力的典型应用场景是气泡图(bubble chart)一类的可视化,d3-force 模块文档 也把“碰撞解决”列为其核心用途之一。

API 总览

API 形式 默认值 作用
d3.forceCollide(*radius*) 构造函数 半径默认为常量 1 创建一个新的圆形碰撞力
*collide*.radius(*radius*) 数值或 (d, i) => number () => 1 设置(或读取)每个节点的半径访问器
*collide*.strength(*strength*) 数值 1 设置(或读取)重叠修正的衰减强度,范围 [0,1]
*collide*.iterations(*iterations*) 整数 1 设置(或读取)每次施加力时的迭代次数

三个访问器方法都遵循“传入参数则设置并返回 this、不传参数则返回当前值”的读写器惯例,因此可以自由链式调用。

forceCollide(radius)

创建一个带有指定 radius 的圆形碰撞力;如果不指定 radius,则默认为常量 1(对所有节点相同)。

const collide = d3.forceCollide((d) => d.r);

半径可以直接写常量数字(所有节点同半径),也可以写成函数以按节点取值,例如上例中取节点数据上的 d.r 字段。构造函数中传入的 radius 与之后调用 *collide*.radius(*radius*) 完全等价,官方文档给出的示例正是函数形式。

collide.radius(radius)

  • 传入 radius:把半径访问器设置为给定的数值或函数,立即对每个节点重新求值一次,并返回 this,可继续链式调用;
  • 不传参数:返回当前的半径访问器,其默认值为:
function radius() {
  return 1;
}

半径访问器会在模拟的每个节点(node)上被调用,并接收 node 本身和它的从零开始下标 index 两个参数;求值结果随后被存储(缓存)在力内部。因此每个节点的半径只在两种时刻被重新计算:

  1. 该力被初始化时(force.initialize,即力绑定到模拟、或模拟节点发生变化时);
  2. 调用 *collide*.radius() 传入了新的 radius 时。

每次施加力(即每个 tick)时不会重新调用访问器。这个设计意味着:即使你的半径函数里有较昂贵的计算(查表、比例尺换算等),其成本也只发生在初始化阶段,而不是每个 tick 都付一遍。反过来它也有一个隐含约束——如果你在模拟运行中直接修改了节点上的半径源数据(例如 d.r),必须再调用一次 collide.radius(collide.radius()) 之类的操作触发重新求值,碰撞检测才会采用新值。

collide.strength(strength)

传入 strength 时,把力的强度设置为给定数值并返回 this;不传参数时返回当前强度。取值范围是 [0,1],默认值为 1。

重叠节点通过迭代松弛(iterative relaxation)来消解。具体过程是:对每个节点,先确定在下一个 tick 的预测位置x + vx, y + vy⟩ 下预计会与之重叠的其他节点;然后修改该节点的速度,把它从每个与之重叠的节点处推开。速度的变化量会被力的 strength 所衰减(dampened),使得同一时刻发生的多重重叠可以彼此“混合”,从而收敛到一个稳定解,而不是互相打架产生抖动。

由此可以推断出 strength 的调优含义:strength 越接近 1,约束越“硬”,重叠被更坚决地推开;调小 strength(如 0.3~0.7)则让碰撞更像软弹簧,节点之间允许轻微“渗透”,视觉上更柔和,但静态布局下的最终重叠程度也会更高。

collide.iterations(iterations)

传入 iterations 时,把每次施加力时的迭代次数设置为给定数值并返回 this;不传参数时返回当前迭代次数,默认值为 1。

文档明确给出了代价与收益的权衡:

  • 增大迭代次数会显著提高约束的刚性,避免节点的部分重叠(partial overlap);
  • 但同时增加每次求值该力的运行时间成本,因为每一轮迭代都要重新查询邻接关系并修正速度。

实践中常见取值是 2~8 之间的整数:节点数不大、对“绝对不重叠”要求严格时可以提高 iterations;节点数很大或追求动画流畅时则保持 1~2,并通过适当降低 strength 换取观感上的平滑。

实战:官方示例的完整实现

下面这段完整可运行的示例改编自仓库文档站点中的官方演示组件 ExampleCollideForce.vue:200 个不等半径的圆在低摩擦下漂浮、相互避让,且第一个节点可以被指针“拖住”,其余节点实时让位。

import * as d3 from "d3";

const width = 688;
const height = 540;
const k = width / 200;                          // 半径基准,与画布宽度挂钩
const r = d3.randomUniform(k, k * 4);          // 半径在 k 与 4k 之间均匀分布
const nodes = Array.from({length: 200}, () => ({r: r()}));

const svg = d3.select("svg")
    .on("touchmove", (event) => event.preventDefault())
    .on("pointerenter", () => simulation.alphaTarget(0.3).restart()) // 进入:重新加热
    .on("pointerleave", () => simulation.alphaTarget(0))            // 离开:恢复冷却
    .on("pointermove", (event) => ([nodes[0].fx, nodes[0].fy] = d3.pointer(event))); // 固定第一个节点

const circle = svg.selectAll("circle")
  .data(nodes.slice(1))
  .join("circle")
  .attr("r", (d) => d.r);

const simulation = d3.forceSimulation(nodes)
  .velocityDecay(0.1)                                       // 低摩擦
  .force("x", d3.forceX().strength(0.01))                   // 极弱的向心定位
  .force("y", d3.forceY().strength(0.01))
  .force("collide", d3.forceCollide()
      .radius((d) => d.r + 1)                               // 半径加 1px 间隙
      .iterations(4))                                       // 提高刚性,避免部分重叠
  .force("charge", d3.forceManyBody()
      .strength((d, i) => i ? 0 : -width * 2 / 3))          // 仅节点 0 带电荷
  .on("tick", ticked);

function ticked() {
  circle.attr("cx", (d) => d.x)
        .attr("cy", (d) => d.y);
}

逐点拆解其中的碰撞力配置与配套技巧:

  1. .radius((d) => d.r + 1):半径访问器取节点数据中的 d.r 再加 1 个像素的固定间隙。这体现了 radius 作为函数的灵活性——它可以是任意 (d, i) => number 表达式,间隙也可以改成比例(如 d.r * 1.1)。
  2. .iterations(4):把每次施加力的迭代次数从默认 1 提到 4,换取更强的刚性,避免 200 个圆在密集区出现部分重叠。
  3. velocityDecay(0.1)(低摩擦):默认速度衰减是 0.4,这里刻意调低让节点“滑”得更久,配合弱定位力产生漂浮感。低摩擦会让软约束的混合过程更明显,也更容易暴露 jitter——这正是文档中引入 strength 作为“软”约束旋钮的动机。
  4. force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 3)):多体力也支持按节点取值的强度函数,这里只有下标 0 的节点带强负电荷,其余节点电荷为 0,于是形成“一个中心 + 一群圆”的结构。
  5. 交互重热pointerentersimulation.alphaTarget(0.3).restart() 把目标 alpha 抬到 0.3 并重启内部计时器,模拟进入“加热”状态;pointerleave 时恢复 alphaTarget(0) 让其正常冷却。配合 nodes[0].fx / nodes[0].fy 的固定位置机制(每个 tick 末尾,fx/fy 有定义的节点其位置会被重置为该值、对应速度置零),第一个节点始终跟随指针,其余节点被碰撞力实时推开。这些机制分别见 simulation 文档simulation.alphaTargetsimulation.restartsimulation.nodes 的说明。
  6. 渲染侧遵循力模拟的标准模式:只给 nodes.slice(1) 做数据绑定(节点 0 不参与普通圆绑定,它由指针直接驱动),在 tick 事件里按 d.x / d.y 更新 cx / cy

机制深入:预测位置、软约束与缓存

综合 collide 文档力模拟文档 的描述,可以把碰撞力在每次施加时的行为归纳为三个要点。

1. 使用预测位置做重叠判定。 碰撞力不是用当前位置 ⟨x, y⟩ 判定重叠,而是“向前看”到下一步 ⟨x + vx, y + vy⟩。simulation 文档的 Custom forces 一节 明确说明:力可以“窥探”节点的下一预测位置,这是通过迭代松弛解决几何约束所必需的——只看当前位置的碰撞判定在高速运动下会“追不上”节点的移动,从而出现穿过现象。

2. strength 是速度的阻尼系数。 对每个预测到重叠的邻居对,力会修正节点速度把二者推开,而修正量乘以 strength 后写入 vx / vy。多个重叠同时存在时,各方向的修正互相叠加并被统一衰减,这就是文档所说“simultaneous overlaps can be blended together to find a stable solution”的含义:strength < 1 时相当于在多个冲突修正之间做加权平均,越小的 strength 越“温顺”,越不容易抖动,但静态收敛后的剩余重叠也越多。

3. iterations 与运行成本的线性权衡。 iterations 控制的是每次施加力时“查询邻接 → 修正速度”这一整轮过程重复几遍。每一轮都会让布局更接近“完全不相交”的状态,因此刚性随 iterations 提升;代价是每次 tick 的碰撞力求值成本随之增长(邻接查询与速度修正都按轮数重复)。

4. 半径只算一次(初始化时缓存)。radius 小节 所述,访问器在 force.initialize(nodes) 阶段对每个节点求值并缓存,每次施加力只读缓存。对照 simulation 文档*force*.initialize 的说明——力可以在初始化时完成求值每个节点参数之类的工作,避免在每次施加力时重复付出——碰撞力正是这一优化原则的典型实现。

5. 碰撞力只改速度,不改位置。 从源码结构看(力通过 simulation.force("collide", ...) 绑定),碰撞力遵循 d3-force 的自定义力接口:力函数接收当前 alpha 参数,读取节点位置、改写节点速度;而位置本身由模拟在每步末尾统一按“速度更新位置”推进(每个 tick 的完整顺序是:alpha 更新 → 依次施加各力 → 速度乘以 1 - velocityDecay → 位置加上速度,见 simulation.tick 说明)。因此碰撞力的“软”特性最终体现在速度层面:它给节点注入的是逐渐衰减的修正速度,而不是瞬移,这既保证了与其他力(电荷、定位、链接)的自然协同,也是低摩擦(低 velocityDecay)配置下需要配合 strength / iterations 共同调参的原因。

参数选择与排查清单

  • 想保证布局严格不重叠:提高 iterations(如 4~8),并保持 strength 接近 1;代价是每 tick 更贵。
  • 追求动画柔和、允许轻微间隙重叠:降低 strengthiterations 保持 1。
  • 半径变化后布局“不生效”:确认已重新调用 collide.radius(...)(或重新初始化力),否则内部仍是旧缓存值。
  • 需要离线/静态布局:按 simulation 文档 的建议,simulation.stop() 后用 simulation.tick(*iterations*) 手动步进若干次直至稳定,而不是依赖内部计时器。
  • 需要确定性结果:d3-force 6.0 起内置力(包括碰撞相关的初始化抖动处理)是确定性的,见 CHANGES.md 的 d3-force 小节;未指定位置的新节点按黄金角螺旋(phyllotaxis)初始化,保证跨运行一致。
  • 版本前提:本文对应仓库版本 d3 v7.9.0"d3-force": "^3.0.0"),D3 7 为纯 ES 模块、要求 Node 12+;上述 API 与默认值均以此版本为准。

更多相关入口:d3-force 模块总览力模拟 simulation多体力 many-body定位力 position,以及 完整 API 索引 中对 d3.forceCollide 的条目(“create a circle collision force”)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384