更新记录

1.0.0(2026-07-19)

首发。3D 物理引擎——真实刚体碰撞 + 相机批量投影,掷骰子 / 抽奖动效 / 商品 3D 展示 / 轻量小游戏。

  • 刚体:球 / 盒 / 胶囊 / 圆柱 / 凸包 + 静态盒(地面墙体);弹性、摩擦、密度、阻尼、锁轴、重力倍率、传感器、防穿透(ccd)逐体可调
  • 关节:球铰(三自由度)/ 铰链(马达 + 角度限位)/ 滑轨(限位 + 马达)/ 焊接
  • 查询:射线检测 / 点查询 / 包围盒查询(未命中给安全默认值,不抛错)
  • 碰撞事件drainEvents() 每帧取空开始/结束接触对
  • 相机 + 批量投影setCamera 配机位,projectBodies() 把全部刚体一次投成屏幕像素点集 (sx, sy, depth)——不引 3D 渲染引擎就能画 3D
  • 物理单位:米/秒/牛顿,y 轴向上,四元数姿态(xyzw 序)
  • 三端:Android / iOS / H5 同一原生内核,42 个 API 三端全量可用
  • 性能实测:千刚体单步 ~1.3ms、千刚体批量投影 <1ms(60fps 预算余量约 7 倍)
  • 上限保护:刚体/关节各 10,000、凸包 4,096 点,超限抛可捕获错误不闪退

平台兼容性

uni-app x(5.14)

Chrome Safari Android iOS 鸿蒙 微信小程序
5.0 12 × ×

nex-physics3d 3D 物理引擎(掷骰子 · 真实碰撞 · 3D 展示)

真实 3D 碰撞搬进 uni-app——掷个骰子试试。原生内核负责刚体仿真与透视投影,把 3D 世界直接算成屏幕像素坐标吐给你,JS 只管拿画布/图片把点画出来——不需要引入任何 3D 渲染引擎。实测:千刚体世界单步 ~1.3ms、千刚体批量投影 <1ms,60fps 预算(16.6ms)余量约 7 倍。骰子 / 抽奖动效 / 商品 3D 展示 / 轻量小游戏,一个插件配齐。

特性

  • 5 种刚体:球 / 盒 / 胶囊 / 圆柱 / 凸包(任意凸多面体),外加静态盒(地面/墙体);弹性、摩擦、密度、阻尼、锁轴、重力倍率、传感器、防穿透(ccd)逐体可调
  • 4 种关节:球铰(三自由度)/ 铰链(可带马达与角度限位)/ 滑轨(可带限位与马达)/ 焊接
  • 查询三件套:射线检测(拾取/瞄准)、点查询、包围盒查询——未命中给安全默认值,不抛错
  • 碰撞事件:开始/结束接触对,每帧 drainEvents() 一次取空,做音效/震动/计分触发
  • 独家:相机 + 批量投影——setCamera 配好机位,projectBodies() 一次把全部刚体投成 (sx, sy, depth) 屏幕点集(含地面墙体);深度排序画远近,画布线框、贴图精灵随你
  • 物理单位、真实手感:米/秒/牛顿,y 轴向上,默认重力 -9.81——参数即真实世界直觉
  • 三端同源:Android / iOS / H5 同一原生内核,42 个 API 三端全量可用(H5 无阉割)
  • 零权限、纯本地:无网络、无存储、无隐私权限,全部计算在端上完成

快速上手:掷骰子(完整可抄)

import { PhysicsWorld3d } from '@/uni_modules/nex-physics3d'

// H5 端内核异步初始化:建世界前等一次(App 端可跳过)
// #ifdef H5
import { ensureReady } from '@/uni_modules/nex-physics3d'
await ensureReady()   // await 需在 async 上下文内(如 async onLoad);非 async 处改用 ensureReady().then(...)
// #endif

// 1) 建世界 + 地面 + 一颗骰子(单位:米;y 轴向上)
const w = new PhysicsWorld3d({ gravityY: -9.81 })
w.addStaticBox({ x: 0, y: -0.5, z: 0, hx: 50, hy: 0.5, hz: 50 })      // 地面(hx/hy/hz 是半边长)
const dice = w.addBox({
  bodyType: 'dynamic', x: 0, y: 5, z: 0, hx: 0.5, hy: 0.5, hz: 0.5,   // 1m 见方骰子从 5m 高落下
  restitution: 0.35, friction: 0.6, ccd: true                          // 弹一弹 + 防高速穿透
})

// 2) 掷:给随机初速度 + 角速度(rand 自备:(min,max) 均匀随机)
w.setLinearVelocity(dice, rand(-2, 2), 0, rand(-2, 2))
w.setAngularVelocity(dice, rand(-10, 10), rand(-10, 10), rand(-10, 10))

// 3) 相机(配一次):视口像素尺寸 + 机位(眼/看向/视场角[度])
w.setViewport(375, 300)
w.setCamera(0, 6, 9, 0, 0, 0, 60)   // 从 (0,6,9) 米俯看原点

// 4) 每帧:步进 → 批量投影 → 画;骰子睡着 = 落定
const timer = setInterval(() => {
  w.step(16.7)                       // 推进 16.7ms
  drawWireframe(w.projectBodies())   // 屏幕点集直接画(解析见下)
  if (w.isSleeping(dice)) { clearInterval(timer); console.log('点数', diceValue()) }
}, 16)

// 5) 解析投影结果:[handle, 形状编码, 顶点数, (sx,sy,depth)×顶点数] 逐体排列
function drawWireframe(flat: number[]) {
  let i = 0
  while (i < flat.length) {
    const kind = flat[i + 1]; const n = flat[i + 2]; i += 3
    const pts: number[][] = []
    for (let k = 0; k < n; k++) { pts.push([flat[i], flat[i + 1], flat[i + 2]]); i += 3 }
    // kind:0=球(24 点) 1=盒(8 角) 2=胶囊(18) 3=圆柱(16) 4=凸包(原点数) 5=静态盒(8 角)
    // 盒/静态盒:8 角连 12 条棱;球:3 个纬度环各 8 点连环……画法随你的视觉风格
  }
}

// 6) 读点数:四元数判哪个面朝上(对面点数和恒为 7)
function diceValue(): number {
  const t = w.getTransform(dice)                       // [h, x,y,z, qx,qy,qz,qw]
  const qx = t[4], qy = t[5], qz = t[6], qw = t[7]
  const faces: number[][] = [                          // [局部法向 x,y,z, 该面点数]
    [0, 1, 0, 2], [0, -1, 0, 5], [1, 0, 0, 3], [-1, 0, 0, 4], [0, 0, 1, 1], [0, 0, -1, 6],
  ]
  let best = 1, bestDot = -2
  for (const f of faces) {
    const wy = rotatedY(qx, qy, qz, qw, f[0], f[1], f[2])  // 面法向转到世界系后的 y 分量
    if (wy > bestDot) { bestDot = wy; best = f[3] as number }
  }
  return best                                          // y 分量最大的面 = 朝上的面
}
function rotatedY(qx: number, qy: number, qz: number, qw: number, vx: number, vy: number, vz: number): number {
  const tx = 2 * (qy * vz - qz * vy), ty = 2 * (qz * vx - qx * vz), tz = 2 * (qx * vy - qy * vx)
  return vy + qw * ty + (qz * tx - qx * tz)            // v' = v + qw·t + q×t,只要 y
}

// 7) 重掷:先 clear() 清场(复用同一个世界;clear 后重建也可,见 FAQ)
function reroll() {
  w.clear()   // 保留重力配置,清掉全部刚体/关节/事件
  // 重新 addStaticBox + addBox + 设随机速度即可
}

多骰同掷:循环 addBox 存下多个 handle,逐个 isSleeping 全真即全部落定。示例工程里有完整的掷骰子游戏页(画布线框着色 + 重掷按钮)可整页照抄。

API 速览(42 个,全部在 PhysicsWorld3d 对象上)

分组 API
世界(7) new PhysicsWorld3d(config) · step(dtMs) · transforms() · getTransform(h) · setGravity · bodyCount() · clear()
刚体增删(8) addBall · addBox · addCapsule · addCylinder · addConvexHull · addStaticBox · removeBody · bodyExists
刚体操控(14) setLinearVelocity/getLinearVelocity · setAngularVelocity/getAngularVelocity · applyImpulse · applyTorqueImpulse · applyForce · applyTorque · setTransform · setBodyType · wakeUp · isSleeping · setRestitution · setFriction
关节(5) addSphericalJoint · addRevoluteJoint · addPrismaticJoint · addFixedJoint · removeJoint
查询(3) castRay(命中 [handle,x,y,z,dist])· pointQuery(未命中 -1)· queryAabb
事件(1) drainEvents()[h1,h2,started]×M,取走即清空)
相机投影(4) setViewport · setCamera · projectPoint · projectBodies

step() / transforms() / getTransform() 返回 8 宽位姿 [handle, x,y,z, qx,qy,qz,qw](位置米 + 四元数 xyzw 序);错误可 try/catch(参数非法 / handle 不存在 / 超上限,message 双端一致)。

常见问题

  • 画面里什么都没有? 九成是没配相机:先 setViewport(宽px, 高px)setCamera(...)(默认机位只是「能跑不崩」的占位,不对准你的场景);另查每帧是否真的在调 step()
  • 怎么知道骰子停了? 建世界保持默认 allowSleep: true,逐帧 isSleeping(handle);多骰全 true 即全部落定。落定后用 getTransform 的四元数判朝上面(上方完整代码可抄)。
  • 重掷几十次后内存涨? 世界是显式生命周期,引擎内存不归 JS 垃圾回收管:重建前必须先 clear() 释放旧世界;之后复用同一个世界、或 clear 后重新 new PhysicsWorld3d 皆可(两种写法都不泄漏——上方快速上手用前者,示例工程的掷骰子页用后者);页面卸载(onUnload)也记得 clear()
  • 骰子偶尔穿过地面? 高速小物体开 ccd: true(示例已开);另注意 step(dtMs) 是单次步进,掉帧时别把 100ms 一口气传进去——切成多次 step(16.7) 碰撞质量更好。
  • 3D 坐标怎么对到我的画布? 不用你换算:projectBodies() 直接吐屏幕像素 (sx, sy, depth),按 depth 从大到小排序先画远的即可;单点换算用 projectPoint(x,y,z)
  • 单位到底是什么? 全部真实物理单位:米、秒、米/秒、牛顿;y 轴向上、默认重力 (0, -9.81, 0)。骰子建 0.5~1 米见方手感最稳(纯像素思维建 100「米」的骰子会觉得「飘」——自由落体 100 米当然久)。
  • 旋转四元数怎么用? 顺序固定 (qx,qy,qz,qw)(标量在最后),可直接喂 three.js new Quaternion(x,y,z,w) 等主流库;只想判朝向抄上方 rotatedY 即可,不必引数学库。
  • H5 端首次调用报「尚未初始化完成」? H5 内核是异步初始化:应用启动或页面 onLoadawait ensureReady() 一次,再 new PhysicsWorld3d
  • Android 打包报「找不到 jna」? 本插件含原生内核库,须制作自定义调试基座(HBuilderX 运行 → 制作自定义调试基座),标准基座跑不了。
  • 能做布娃娃/吊桥吗? 能:球铰 + 铰链把多个胶囊/盒串起来即可(关节 body1/body2 + 各自局部锚点)。软体/布料/流体不支持(超出刚体范围)。

平台说明

说明
Android 原生内核库(arm64-v8a / armeabi-v7a / x86_64 全覆盖);需自定义基座运行
iOS 原生内核库(真机 + 模拟器);HBuilderX 打包勾选「支持 Swift」
H5 同一原生内核在浏览器内直接运行(免安装、免服务端);首次调用前 await ensureReady(),此后 42 个 API 与 App 端完全一致

上限保护:刚体/关节各 10,000、凸包单次 4,096 点——超限抛可捕获错误,绝不静默截断或闪退。

技术支持 / 问题反馈:插件评论区或联系作者。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据。所有计算/处理均在本地完成,无任何网络请求、不发送数据到任何服务器。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。