news 2026/10/13 2:52:18

Planck.js 单位体系详解:MKS 长度约定、lengthUnitsPerMeter 缩放与弧度制角度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Planck.js 单位体系详解:MKS 长度约定、lengthUnitsPerMeter 缩放与弧度制角度
  • 游戏开发

【免费下载链接】planck.js

2D JavaScript Physics Engine

项目地址:https://gitcode.com/gh_mirrors/pl/planck.js
点击查看免费下载

Planck.js 是一个基于浮点数运算的 2D JavaScript 物理引擎,它的碰撞、约束求解与休眠判定都依赖一组为米-千克-秒(MKS)单位精心调校的容差参数。本文将围绕 units.md 展开,系统讲解 Planck.js 的推荐物体尺寸范围、为何不能直接用像素作物理单位、更换长度单位的两种可行方案(渲染层缩放与Settings.lengthUnitsPerMeter),以及弧度制角度管理与归一化技巧。读完本文,你将能正确为游戏/仿真场景选定量纲,避免因单位不当导致的抖动、穿模等异常模拟行为。

为什么 Planck.js 依赖浮点容差与 MKS 单位

Planck.js 的物理核心建立在浮点数计算之上。与整数运算不同,浮点数的精度是相对的:数值越接近 0 和极大值时,运算的相对误差会急剧放大。因此引擎内部大量使用"容差"(tolerance)来判定接触、允许微小穿透、决定物体是否进入休眠,而这些容差都是按 MKS 单位校准的。

在 src/Settings.ts 中可以看到这些容差的定义与注释:

/** @internal */ const math_PI = Math.PI; export class Settings { /** * You can use this to change the length scale used by your game. * * For example for inches you could use 39.4. */ static lengthUnitsPerMeter = 1.0; // 碰撞相关 static aabbExtension: number = 0.1; // 动态树 AABB 膨胀量,单位:米 static linearSlop: number = 0.005; // 碰撞与约束容差长度,单位:米 // ... }

其中linearSlop(线性容差,默认 0.005 米)、aabbExtension(AABB 膨胀量,默认 0.1 米)、maxTranslation(最大线速度,默认 2.0)等都是绝对长度量。这些值一旦脱离米量级的世界尺寸,就会出现两类典型问题:

  • 物体过小:相对误差变大,容差相对物体尺寸过大,物体表现为"软塌塌"、容易穿透或抖动;
  • 物体过大:浮点精度不足以表达细微位移,约束求解需要更多迭代,且maxTranslation、velocityThreshold等限制会让高速运动物体出现异常表现。

这正是文档强调"Planck.js 的容差被调校为在 MKS 单位下工作良好"的根本原因。

推荐的物体尺寸范围:0.1 ~ 10 米

移动物体:0.1 ~ 10 米,1 米为最佳

文档给出的核心建议是:让移动物体保持在 0.1 ~ 10 单位(米)之间,其中 1 米是"甜蜜点"(sweet spot)。

  • 0.1 米 ≈ 一个汤罐头(soup can)的尺寸;
  • 10 米 ≈ 一辆公交车(bus)的尺寸;
  • 1 米 ≈ 一个桶(barrel)的高度,是容差设计得最舒服的量级。

hello-world.md 也印证了这一点:

Planck.js by default is tuned for meters, kilograms, and seconds. So you can consider the dimensions to be in meters. Planck.js generally works best when objects are the size of typical real world objects. For example, a barrel is about 1 meter tall.

同时文档提醒:受浮点运算限制,用 Planck.js 模拟冰川或尘埃颗粒这类极端尺度的物体不是好主意——冰川太大、尘埃太小,都超出了容差调校的有效区间。

静态形状:最长 50 米

静态物体(staticbody)不参与动力学求解,因此可以容忍更大的尺寸。文档明确:静态形状最长可以达到 50 米而不会出问题。这解释了为什么示例代码中常用超大静态地面:例如 hello-world.md 里创建的 Edge 地面横跨 ±50 个单位(100 米):

platform.createFixture({ shape: new Edge({x: -50, y: 0}, {x: +50, y: 0}), });

不过即便静态形状放宽到 50 米,也建议尽量贴近推荐区间,以减小数值误差。

为什么像素单位会导致模拟异常

作为 2D 引擎,开发者很容易把"像素"直接当作物理单位使用。文档明确警告这是错误的做法,会"导致糟糕的模拟,甚至诡异的行为"。

文档给出的直观换算例子非常震撼:一个长度为 200 像素的物体,在 Planck.js 眼中相当于一栋 45 层楼的大小。以每层约 4 米估算,200 像素 ≈ 180 米,远超 10 米的推荐上限。此时:

  • 重力加速度(默认-10米/秒²)作用在"180 米高的物体"上,每帧位移量极大,容易发生穿透(tunneling);
  • linearSlop = 0.005米的容差相对 180 米尺寸几乎可以忽略,接触判定变得脆弱;
  • 性能上动态树(Dynamic Tree)与宽相位(broad-phase)也会因巨大 AABB 而退化。

换用其他长度单位的两条路径

如果项目确实需要采用非米单位(例如像素、英寸、厘米),文档给出了两条可行路径:

方案一:在渲染层做缩放(推荐)

Use some scaling system when you render your environment and actors. It is highly recommended to use this approach to keep your physics code portable.

方案一是强烈推荐的:物理世界始终使用米(MKS),仅在渲染时通过一个缩放系数把米换算成像素。这样做的好处是物理代码保持"可移植"——换渲染后端、换分辨率都不影响物理逻辑。

rendering.md 中也有同样的主张:

Planck.js uses MKS (meters, kilograms, and seconds) units and radians for angles, however rendering is done in pixels. So you need to transformation world geometry into screen and vice versa. You should consider using MKS units in your game code and find a scale to convert to pixels when you render.

Planck.js 自带的Testbed正是采用这一思路:它通过 stage.js 的viewbox 变换在物理坐标与屏幕像素之间做转换。见 testbed.md:

// Viewbox center(物理单位) testbed.x = 0; testbed.y = 0; // Viewbox size(物理单位) testbed.width = 30; testbed.height = 20;

Testbed will calculate and set rendering scale and offset to match provided dimensions and center.

也就是说,你只需告诉 Testbed 视口中心的物理坐标和视口的物理尺寸(例如 30 × 20 米),它会自动算出渲染缩放比例和偏移量,把物理世界映射到像素画布。对应的接口定义在 src/util/Testbed.ts:width、height(视口宽高)、x、y(视口中心偏移)以及scaleY等字段。

渲染层缩放的落地方式可以很简单:在每帧渲染时对body.getPosition()等物理坐标乘以一个全局比例因子,例如const PIXELS_PER_METER = 50; ctx.drawRect(x * PIXELS_PER_METER, y * PIXELS_PER_METER, ...)。注意文档强调这个比例因子应该在全局统一调整,以"确保不出问题"并可用它来改善稳定性。

方案二:设置Settings.lengthUnitsPerMeter(备选)

Set Settings.lengthUnitsPerMeter accordingly. For example if a car height which is around 1.6 meter is 80 units (pixels) in in your game, value of lengthUnitsPerMeter should be set to 50 (80 / 1.6).

如果不方便在渲染层做缩放,Planck.js 提供了备选方案:修改全局设置Settings.lengthUnitsPerMeter,让引擎内部所有长度相关容差自动按比例缩放。

计算方式:lengthUnitsPerMeter = 你游戏中的单位数 ÷ 对应真实米数。

文档给出的实例:一辆汽车的真实高度约 1.6 米,在游戏里用 80 个像素(单位)表示,则:

lengthUnitsPerMeter = 80 / 1.6 = 50

再举一例(来自 hello-world.md):若用像素单位且一个桶的高度是 80 像素(真实约 1 米),则设为 80。而在 src/Settings.ts 的注释中还给出了英寸的例子:若采用英寸为单位,可设为39.4(1 米 ≈ 39.4 英寸)。

默认值在源码中定义为static lengthUnitsPerMeter = 1.0;,即默认 1 单位 = 1 米。

该设置的实际生效机制:从源码看,lengthUnitsPerMeter并不是简单地把所有数字放大,而是只作用于长度类容差。src/Settings.ts 中的内部类SettingsInternal会将这些长度相关的容差全部乘以lengthUnitsPerMeter:

内部容差公式说明
aabbExtensionSettings.aabbExtension × lengthUnitsPerMeter动态树 AABB 膨胀量
linearSlopSettings.linearSlop × lengthUnitsPerMeter碰撞/约束线性容差
velocityThresholdSettings.velocityThreshold × lengthUnitsPerMeter弹性碰撞速度阈值
maxLinearCorrectionSettings.maxLinearCorrection × lengthUnitsPerMeter最大线性位置修正
maxTranslationSettings.maxTranslation × lengthUnitsPerMeter最大线速度限制
linearSleepToleranceSettings.linearSleepTolerance × lengthUnitsPerMeter休眠线速度容差
maxTranslationSquared/linearSleepToleranceSqr对应值的平方用于避免开方的平方比较

而角度类容差(angularSlop、maxRotation、angularSleepTolerance)以及无量纲参数(aabbMultiplier、baumgarte等)则不参与缩放——因为它们与长度单位无关。这一点从源码可以清楚看到:角度类 getter 只是原样返回Settings.angularSlop、Settings.maxRotation等,没有乘以lengthUnitsPerMeter。

需要说明的是:方案二虽然能工作,但文档将其定位为备选。它改变了引擎内部的容差语义,可能引入未预期的数值行为;而方案一(渲染层缩放)让物理代码保持标准 MKS 语义,更稳妥、可移植性更好。

角度单位:弧度(radians)

Planck.js 一律使用弧度

Planck.js 中所有角度均以弧度为单位,包括:

  • 创建 body 时的初始角度(见 body.md):
world.createBody({ position: {x: 0, y: 2}, // 物体原点位置 angle: 0.25 * Math.PI // 物体角度,单位:弧度 })
  • 关节(joint)角度,如 revolute-joint.md 所述"Like all angles in Planck.js, the revolute angle is measured in radians";
  • body.getAngle()/body.setAngle()的返回值与入参。

切记:Planck.js 使用弧度,而不是角度(度)。用度数值直接传入会导致旋转行为完全错误。换算关系为标准公式:弧度 = 角度 × π / 180。

角度可能无界增长,需要归一化

文档特别强调:

The body rotation is stored in radians and may grow unbounded.

物体旋转角度在模拟中会不断累积,如果物体持续旋转(例如旋转关节驱动),内部存储的角度值会变得非常大(几百甚至上千弧度)。这在数值上虽然仍可表示,但:

  • 大角度参与三角函数(sin/cos)运算时精度下降;
  • 长时间运行后数值可能溢出或产生漂移。

因此文档建议:当角度绝对值变得过大时,考虑对物体角度做归一化,使用body.setAngle把它折回到合理区间:

// 把角度归一化到 [-π, π] const angle = body.getAngle(); body.setAngle(angle - Math.round(angle / (2 * Math.PI)) * 2 * Math.PI);

引擎内部其实也有类似机制:在 TOI(碰撞时间)计算中,Sweep 会记录角度的"扫描"轨迹,其normalize()方法会把角度归一化到-π到+π之间(见 src/common/Sweep.ts):

/** * normalize the angles in radians to be between -pi and pi. */ normalize(): void { const a0 = mod(this.a0, -math_PI, +math_PI); this.a -= this.a0 - a0; this.a0 = a0; }

这是引擎在碰撞扫描阶段的内部保障;而应用层的 body 角度并不会被自动归一化,所以文档建议开发者自己留意角度量级,必要时用body.setAngle手动处理(对应实现见 src/dynamics/Body.ts)。

与渲染层的配合

在 rendering.md 的"Pixels and Coordinate Systems"一节中同样提醒:渲染使用像素、角度使用弧度,需要在渲染代码中做好换算(例如调用body.getAngle()后,若渲染 API 接受角度制,需先转换为度)。

小结:一套可落地的单位规范

综合 units.md 与相关源码,推荐的项目实践可以归纳为:

  1. 物理世界始终使用 MKS 单位:移动物体控制在 0.1 ~ 10 米(1 米最佳),静态形状不超过 50 米;
  2. 绝不把像素直接当物理单位:在渲染层用一个全局缩放系数(如 50 px/m)做换算,参考 Testbed 的 viewbox 方案(testbed.md);
  3. 万不得已才用Settings.lengthUnitsPerMeter:按游戏单位数 ÷ 真实米数计算(如 80px / 1.6m = 50),并清楚它只缩放长度类容差,角度容差不受影响(src/Settings.ts);
  4. 角度一律用弧度,长时间旋转的物体留意角度累积,过大时用body.setAngle归一化;
  5. 若使用固定时间步长(推荐),在 WorldComponent.ts 中可以看到 Testbed 的做法:timeStep = 1 / hz(默认 60Hz),每帧用world.step(timeStep)推进模拟——固定步长配合合理的单位体系,是稳定模拟的两大基石。

相关文档可直接继续阅读:units.md、hello-world.md(单位与入门示例)、rendering.md(像素与坐标系转换)、body.md(角度与位置设置)。

  • 游戏开发

【免费下载链接】planck.js

2D JavaScript Physics Engine

项目地址:https://gitcode.com/gh_mirrors/pl/planck.js
点击查看免费下载

相关推荐

上一篇:3分钟掌握Windows"此电脑"终极清理术:MyComputerManager完全指南
下一篇:GraphQL 快速参考速查表:Schema 语言与类型系统实战指南(Reference 开源项目)

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 2:51:38

波士顿房价预测:小样本深度学习建模的完整实践指南

1. 项目概述:为什么波士顿房价预测是深度学习入门的“黄金标本”你打开任何一本深度学习入门教程,十有八九会在前五章看到“波士顿房价预测”这个案例。它不像ImageNet那样动辄百万张图、几十GB数据,也不像自然语言处理任务需要复杂的分词和上…

作者头像 李华
网站建设 2026/10/13 2:49:36

WinForm SQL Server服务端分页控件实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华