ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

three.js RapierPhysics 物理引擎接入指南:从刚体堆叠到高度场地形仿真

three.js RapierPhysics 物理引擎接入指南:从刚体堆叠到高度场地形仿真 three.js RapierPhysics 物理引擎接入指南从刚体堆叠到高度场地形仿真【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsRapierPhysics 是 three.js 官方 Addon 提供的物理引擎适配器位于 examples/jsm/physics/RapierPhysics.js用于把 Rust 编写的轻量级刚体物理引擎 [Rapierdimforge/rapier3d-compat] 无缝接入 three.js 应用。本文将基于 three.js 仓库中该模块的源码与官方 API 文档 docs/pages/RapierPhysics.html.md完整讲解它的初始化方式、7 个公开方法的参数与用法、几何体到碰撞体的自动映射机制以及内部 60Hz 仿真循环的工作原理并借助仓库内 physics_rapier_basic、physics_rapier_instancing、physics_rapier_terrain 等真实示例带你掌握刚体堆叠、冲量弹射、实例化碰撞与地形碰撞的实战写法。RapierPhysics 是什么在 three.js 仓库中物理引擎适配器被统一放在 examples/jsm/physics 目录下与 AmmoPhysics.jsBullet/ammo.js、JoltPhysics.js 并列RapierPhysics.js 是其中的一份轻量实现。它的设计目标很明确用最少的面板化代码把 Rapier 物理世界和 three.js 场景同步起来。该组件本身不打包 Rapier 库而是在首次调用时通过动态import()从 CDN 拉取dimforge/rapier3d-compat0.17.3源码第 3 行因此必须在有网络连接的环境中使用官方文档亦作了该说明由于模块级的RAPIER单例缓存源码第 10、98~103 行同一页面多次调用RapierPhysics()只会加载与初始化 Rapier 一次但每次调用都会创建一个新的物理世界new RAPIER.World( gravity )重力默认为( 0, -9.81, 0 )。因此当你需要多个相互独立的物理空间时可多次调用需要默认的地球重力则可直接使用返回值。引入方式与模块化背景RapierPhysics 是 three.js 的 Addon插件必须显式导入官方文档的示例如下import { RapierPhysics } from three/addons/physics/RapierPhysics.js;这里的three/addons/路径由你项目中的 import map 或打包器别名决定。仓库内各示例页面使用 import map 映射例如 examples/physics_rapier_basic.htmlscript typeimportmap { imports: { three: ../build/three.module.js, three/addons/: ./jsm/ } } /script同时examples/jsm/Addons.js 中也通过export * from ./physics/RapierPhysics.js;统一导出该模块方便打包器从集中入口引入。初始化与返回值结构官方文档给出的唯一初始化方式是异步调用const physics await RapierPhysics();从源码第 96~103 行可以看到首次调用时它会动态加载 Rapier 并执行await RAPIER.init()之后构建Rapier.World并开始内部的定时仿真循环。调用完成后返回一个包含如下成员的对象成员类型说明RAPIERModule底层的 Rapier 库命名空间可用来创建关节、查询世界等进阶操作worldRAPIER.World当前物理世界实例addSceneFunction批量注册场景中的物理网格addMeshFunction注册单个网格到物理世界removeMeshFunction从物理世界移除网格addHeightfieldFunction添加高度场地形静态碰撞体setMeshPositionFunction直接设置刚体位置并清零速度setMeshVelocityFunction直接设置刚体线速度applyImpulseFunction对刚体施加冲量因为每次调用都返回全新的world示例代码中常见的写法是physics await RapierPhysics();后立刻用physics.addScene( scene )或逐个physics.addMesh(...)注册物体。注册物体的两种入口userData 约定与 addSceneaddScene( scene : Object3D )用于把整个场景或任意 Object3D 子树一次性交给物理世界。它的关键约定是只有userData.physics字段携带物理配置的 Mesh 才会被注册其他对象灯光、相机、辅助器、没有物理属性的装饰物会被自动忽略。配置对象用于保存该网格的质量与恢复系数官方文档给出的示例box.userData.physics { mass: 1, restitution: 0 };从源码第 117~135 行可见其实现scene.traverse()遍历所有子节点遇到isMesh为真的子节点时读取child.userData.physics并等价地调用addMesh( child, physics.mass, physics.restitution )。因此userData.physics支持任意数量的附加字段mass与restitution是被读取的两个约定字段。这种声明式设计在仓库示例中被大量使用例如 examples/physics_rapier_basic.html 里静态地板和动态方块都通过该字段注册// 地板mass 为 0 表示静态固定刚体 floor.userData.physics { mass: 0 }; // 动态方块单独调用 addMesh( mesh, 1, 0.5 ) physics.addMesh( mesh, 1, 0.5 );注意userData.physics是一个普通对象也可以先只写{ mass: 0 }注册后模块还会把物理句柄回写到mesh.userData.physics.body与mesh.userData.physics.collider供你在后续访问刚体详见后文“静态刚体与运行期句柄”。addMesh核心注册方法签名与参数addMesh( mesh : Mesh, mass : number, restitution : number )参数类型默认值说明meshMesh—要加入物理模拟的网格massnumber0质量单位 kg。0代表静态刚体fixed 0代表动态刚体dynamicrestitutionnumber0恢复系数弹性通常取值 0~1表示物体碰撞时的“弹跳”程度静态与动态刚体的区分源码第 205~216 行根据质量选择刚体类型const desc mass 0 ? RAPIER.RigidBodyDesc.dynamic() : RAPIER.RigidBodyDesc.fixed();mass 0注册为静态刚体fixed不会受重力、不会移动适合做地板、墙壁、障碍物mass 0注册为动态刚体dynamic受重力与碰撞力影响参与运动求解。一个值得注意的底层事实mass 0的网格会被压入模块内部的meshes数组并登记到WeakMap源码第 155~160 行供每帧仿真结束后回写变换而mass 0的静态物体只创建刚体与碰撞体不参与逐帧位置回写。这解释了为什么“地面必须用 mass 0、物体用 mass 1”是示例里的铁律。几何体到碰撞体的自动映射getShape无论addScene还是addMesh最终都调用getShape( geometry )源码第 12~80 行把 three.js 几何体转换成 Rapier 碰撞体描述ColliderDesc。映射规则如下表three.js 几何体类型Rapier 碰撞体备注RoundedBoxGeometryroundCuboid使用宽/高/深的一半减去圆角半径作为半尺寸圆角半径默认0.1BoxGeometrycuboid使用三轴尺寸的一半作为半尺寸默认0.5SphereGeometryball使用radius默认1IcosahedronGeometryball同样按球体处理这是示例中用二十面体做“球”的底层原因CylinderGeometrycylinder高的一半作为半长半径取radiusBottom默认0.5CapsuleGeometrycapsule高的一半作为半长半径默认0.5BufferGeometry通用trimesh逐顶点读取 position 属性组成三角网格若为非索引缓冲会先按顶点顺序生成索引源码第 67~70 行其他类型—console.error(RapierPhysics: Unsupported geometry type, type)并返回null该网格会被静默跳过几何体参数均从geometry.parameters中读取因此给网格设置了位移、缩放、旋转等matrixWorld变换时请确认与碰撞体构建时机匹配代码在注册瞬间读取网格当前的position/quaternion实例网格则读取instanceMatrix创建刚体。实际使用时建议把变换先在 Mesh 上设置好再调用addMesh/addScene。removeMesh反向注销removeMesh( mesh : Mesh )把指定网格从物理模拟中移除源码第 164~183 行会从内部meshes数组中查找该网格并调用world.removeRigidBody()/world.removeCollider()分别清理刚体与碰撞体。仓库中 examples/physics_rapier_basic.html 的animate()循环在物体掉出场景position.y -10时同时执行scene.remove( object )与physics.removeMesh( object )做清理避免无限增长if ( object.position.y - 10 ) { scene.remove( object ); physics.removeMesh( object ); }需要说明的是从源码结构看removeMesh只处理内部meshes数组中登记过的网格也就是通过addMesh/addScene注册且mass 0的动态刚体。若确实需要移除静态刚体可以借助注册时写回 userData 的刚体句柄自行调用底层 world 接口。运行时直接操控位置、速度与冲量下面三个方法用于在仿真进行中直接操控已注册的动态刚体它们都通过WeakMap取回刚体未注册或静态物体调用时没有对象可取。setMeshPositionsetMeshPosition( mesh : Mesh, position : Vector3, index : number )把刚体瞬移到指定位置。官方文档特别强调调用本方法会重置该刚体当前的模拟速度。源码第 254~268 行也证实了这一点——它会先body.setAngvel( ZERO )和body.setLinvel( ZERO )清零角速度与线速度再body.setTranslation( position )。适合做“传送、复位、扔回场上”等操作。setMeshVelocitysetMeshVelocity( mesh : Mesh, velocity : Vector3, index : number )直接设置刚体的线速度body.setLinvel不改动位置。常用于发射子弹、投掷物体。在 examples/webxr_xr_ballshooter.html 中XR 手柄扣扳机时即先setMeshPosition把球放到控制器位置再根据控制器朝向计算速度并setMeshVelocity抛射physics.setMeshPosition( spheres, controller.position, count ); velocity.set( ( Math.random() - 0.5 ) * 2, ( Math.random() - 0.5 ) * 2, ( Math.random() - 0.5 ) * 2 ); velocity.applyQuaternion( controller.quaternion ); physics.setMeshVelocity( spheres, velocity, count );applyImpulseapplyImpulse( mesh : Mesh, impulse : Vector3, index : number )对刚体施加一次瞬时冲量源码中body.applyImpulse( impulse, true )第二个参数true表示唤醒刚体。与设置速度不同冲量会把动量增量交给刚体效果受质量影响质量越大同样冲量引起的速度变化越小因此更接近“推一把”的物理语义。它不会重置已有速度适合做爆炸波、晃动、碰撞反馈。examples/physics_rapier_instancing.html 里的 “SHAKE” 按钮就遍历 400 个球体实例与 400 个方块实例对每个实例施加随机方向冲量impulse.set( ( Math.random() - 0.5 ) * 5, Math.random() * 5, ( Math.random() - 0.5 ) * 5 ); physics.applyImpulse( boxes, impulse, i );index 参数实例化网格的逐实例寻址上述三个方法都带index参数默认值0。当mesh是InstancedMesh时模块为它的每个实例创建独立的刚体数组源码createInstancedBody第 185~203 行遍历mesh.count从instanceMatrix.array中提取每实例位置逐一createBodyindex就是实例 ID用来锁定要操控的刚体。也就是说对普通Mesh调用时index参数可省略始终命中唯一刚体对InstancedMesh调用时必须传入目标实例下标0 ~ mesh.count-1。这是“百上千个可独立碰撞的粒子/物体”的高性能实现基础examples/physics_rapier_instancing.html 用两个InstancedMesh各 400 实例配合mass: 1的 userData 声明实现了整批刚体的实时堆叠与弹射。addHeightfield高度场地形签名与参数addHeightfield( mesh : Mesh, width : number, depth : number, heights : Float32Array, scale : Object ) : RigidBody把一段高度场地形加入物理模拟返回创建的 Rapier 刚体。参数含义如下参数类型说明meshMesh表示地形的 three.js 网格同时用于同步绘制widthnumber高度场沿宽x 轴方向的顶点数depthnumber高度场沿深z 轴方向的顶点数heightsFloat32Array每个顶点对应的高度值数组scale.xnumber宽度方向的缩放因子scale.ynumber高度方向的缩放因子scale.znumber深度方向的缩放因子源码实现与特性源码addHeightfield第 298~314 行构造RAPIER.ColliderDesc.heightfield( width, depth, heights, scale )并把它挂到一个固定刚体RigidBodyDesc.fixed()上刚体的平移与旋转取自mesh.position与mesh.quaternion。也就是说地形一旦加入就不可移动可承接一切下落物体的碰撞。官方文档还提到一个使用约定heights数组须与width × depth的顶点网格对齐。可对照 examples/physics_rapier_terrain.html 查看完整的“可视化网格 物理地形”对齐模式// 可视化网格100×100 范围、127×127 段即 128×128 个采样顶点 const geometry new THREE.PlaneGeometry( 100, 100, 127, 127 ); geometry.rotateX( - Math.PI / 2 ); // 生成 128×128 的高度采样 Float32Array heightData generateHeight( terrainWidth, terrainDepth, - 2, 8 ); // 把顶点 y 写进几何体并重建法线 for ( ... ) vertices[ j 1 ] heightData[ i ]; geometry.computeVertexNormals(); // 物理与绘制共享同一份高度数据scale 与平面范围对应 physics.addHeightfield( terrainMesh, terrainWidth - 1, terrainDepth - 1, heightData, { x: 100, y: 1.0, z: 100 } );该示例还演示了配合循环每隔一段时间在高度场上空随机生成球体/方块/圆柱physics.addMesh( obj, mass, restitution )并在物体掉出地形边界后清理场景与物理对象——与 removeMesh 用法互相印证。内部仿真循环60Hz 步进与变换回写RapierPhysics 最“隐形”的部分是仿真驱动。官方文档没有展开但从源码第 316~367 行可以看到清晰的实现模块用 three.js 的Timer计时并通过setInterval( step, 1000 / 60 )以每秒 60 步的固定节奏驱动step()每步先timer.update()取真实帧间隔timer.getDelta()把它赋给world.timestep后调用world.step()推进 Rapier 世界——也就是说物理步进与渲染循环renderer.setAnimationLoop相互独立你无需手动调用 step步进完成后遍历内部登记的动态网格把物理结果回写为 three.js 对象变换普通Meshmesh.position.copy( body.translation() )、mesh.quaternion.copy( body.rotation() )InstancedMesh用每个实例刚体的位置/旋转配合单位缩放_matrix.compose(...)逐实例写入instanceMatrix.array随后置instanceMatrix.needsUpdate true并调用mesh.computeBoundingSphere()实例化示例中对应instanceMatrix.setUsage( THREE.DynamicDrawUsage )的设定表示矩阵每帧更新。需要提醒的是由于回写方向是物理→渲染示例中通常不建议每帧再手动改动已注册动态网格的position/quaternion如需操控请走setMeshPosition/setMeshVelocity/applyImpulse否则会被物理结果覆盖。静态刚体与运行期句柄userData 的二次作用从addMesh与addHeightfield的源码可见模块在注册完成后都会把物理句柄回写到mesh.userData.physics.body与collidermesh.userData.physics.body body; mesh.userData.physics.collider collider;这意味着在注册之后你可以随时通过mesh.userData.physics.body调用 Rapier 原生刚体 API例如唤醒刚体if ( obj.userData.physics.body ) obj.userData.physics.body.wakeUp();examples/physics_rapier_terrain.html 的updatePhysics()正是基于该句柄判断刚体休眠状态并施加修正冲量。另外 examples/physics_rapier_basic.html 展示了调试可视化技巧——用返回对象中的physics.world构造RapierHelper来绘制碰撞体轮廓physicsHelper new RapierHelper( physics.world ); scene.add( physicsHelper ); // 每帧if ( physicsHelper ) physicsHelper.update();最小可用示例综合以上全部要点把上述 API 组合起来一个“地面 周期性下落物块”的最小实现大致如下import * as THREE from three; import { RapierPhysics } from three/addons/physics/RapierPhysics.js; let physics; async function init() { // 1. 初始化内部从 CDN 加载 Rapier physics await RapierPhysics(); // 2. 静态地面mass 0 const floor new THREE.Mesh( new THREE.BoxGeometry( 10, 0.5, 10 ), new THREE.MeshStandardMaterial() ); floor.position.y - 0.25; floor.userData.physics { mass: 0 }; // fixed body scene.add( floor ); // 3. 动态物块注册进物理世界 const box new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), material ); box.position.set( 0, 5, 0 ); box.userData.physics { mass: 1, restitution: 0.5 }; physics.addScene( scene ); // 或单独 physics.addMesh( box, 1, 0.5 ); // 4. 直接操控示例 physics.applyImpulse( box, new THREE.Vector3( 0, 3, 0 ) ); // 推一把 physics.setMeshVelocity( box, new THREE.Vector3( 0, 2, 0 ) ); // 给速度 physics.setMeshPosition( box, new THREE.Vector3( 0, 6, 0 ) ); // 瞬移并清零速度 // 5. 物理循环由模块内部以 60Hz 自动运行渲染循环照常即可 renderer.setAnimationLoop( animate ); }仓库内的可运行示例索引想直接体验与对照源码仓库提供了 5 个 Rapier 相关页面examples/physics_rapier_basic.html基础堆叠、循环下落方块Box/Sphere/RoundedBox配合RapierHelper显示碰撞体并演示removeMesh清理examples/physics_rapier_instancing.html400 方块 400 球体实例的逐实例碰撞applyImpulseSHAKE 按钮与setMeshPosition的逐实例寻址examples/physics_rapier_terrain.html高度场地形几何体与物理共享高度数据演示addHeightfieldexamples/webxr_xr_ballshooter.htmlXR 场景下 800 个球体实例的抛射与冲量操作setMeshVelocity、setMeshPositionphysics_rapier_joints.html、physics_rapier_character_controller.html、physics_rapier_vehicle_controller.html分别演示利用返回对象中的RAPIER/world扩展关节约束、角色控制器与载具控制器等高级用法。总结RapierPhysics 的设计哲学可以概括为一句话在 three.js 中提供“注册即模拟”的物理体验。你只需遵守userData.physics { mass, restitution }约定调用addScene/addMesh注册物体其余交给内部 60Hz 的世界步进与变换回写需要主动控制时再通过applyImpulse、setMeshVelocity、setMeshPosition干预特定网格含按实例 ID 精确寻址的 InstancedMesh不规则地面则用addHeightfield与网格共享一份高度数据即可。掌握这些 API 与 examples/jsm/physics/RapierPhysics.js 中的实现细节你便能在不离开 three.js 心智模型的前提下快速搭建从跑酷、弹球到地形沙盒的完整物理场景。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表