type
Post
status
Published
date
Mar 12, 2026
slug
summary
threejs在页面内的应用
tags
Three.js
category
技术学习笔记
icon
password
threejs在页面内的应用
透明背景
默认情况下,使用
threejs 的 renderer 渲染出来的背景是黑色的,可以通过给 renderer 设置 alpha: true 来让其背景透明通常情况下我们应该给页面
html、body 元素设置背景色或 background-img ,而不是在 threejs 里给 scene 设置背景色或背景图片,否则可能会某些用户的电脑上出现滚动 bug页面滚动摄像机跟随

在这里插入图片描述
核心代码:
- 监听滚动事件,记录下滚动的像素
- 计算滚动的比例,移动摄像机位置
objectDistance 代表几何体与几何体之间的距离,初始生成几何体时,它们均坐落在原点位置,上例中,修改几何体的位置为:第一个几何体:
(2, 0, 0)
第二个几何体: (-2, -4, 0)
第三个几何体: (2, -8, 0)所以,随着页面滚动,摄像机移动的数值应为
-已滚动的像素 / 视口高度 * 几何体间距初始时,摄像机在Y轴的位置为0,页面滚动的值是从0 -> ∞的,所以滚动行为一开始,就要对摄像机移动的距离取负数
物体进入视野判断
:

在当前案例中,几何体之间的间距是固定的,在页面上的体现约等于视口的高度,所以要判断屏幕滚动到了第几个几何体,可以通过对
滚动距离 / 视口高度 进行四舍五入来判断,如:鼠标移动摄像机跟随
鼠标移动时,场景跟随移动
在这里插入图片描述
鼠标移动的同步也是通过修改摄像机的位置来实现的,但是上例中已经在每一帧里修改了
camera.position ,如果鼠标移动也修改 camera.position 就无法兼容同时滚动且鼠标移动的场景要解决这个问题,可以将摄像机放进一个
Group 里,在鼠标移动时,修改这个 Group 的位置,即可避免页面滚动时修改摄像机位置的冲突,如:然后到核心实现,要实现鼠标移动的同步,本质就是监听
mousemove 事件,将屏幕的鼠标位移转为 threejs 坐标系内的移动幅度,如:在页面中,鼠标从左上角开始,水平和垂直方向的移动都是从
0 到正 ∞ ,但在 threejs 坐标系中,从左上角开始水平和垂直方向应该是负 ∞ 到正 ∞ ,所以应该用 鼠标在屏幕上的位移 / 视口宽高 获取到鼠标移动的幅度(0到1),减去 0.5 即为鼠标在 threejs 坐标系内实际移动的幅度所以,每一帧中要处理的代码如下
注意在Y轴方向的取负数操作,因为我们移动的是摄像机,所以相对于原点而言,水平方向
鼠标往左 = 摄像机往左 = 物体往右 移动假如Y轴不取反,相对于原点而言,就等于垂直方向
鼠标往上 = 摄像机往下 = 物体往上 移动所以为了便于用户理解,要么对X轴取反,要么对Y轴取反,但是对Y轴取反才更符合人对于视角的认知
平滑移动
按照上面的代码实现后会发现,视角移动起来比较“僵硬”,如图:

在现实生活中,人移动摄像机或者将摄像机架在滑轨上移动都不可能实现
一步到位 的效果,甚至会因为惯性的存在,让摄像机移动后有一点滞后性的效果所以,为了优化用户体验,我们可以优化摄像机在每一帧移动的实现,比如:
对代码的解释如下:
首先,平滑移动其实就是在移动速度上呈现出
先快后慢,越来越慢 的效果是上一帧到当前帧的时间差,基本上等于一个固定值,通过控制台可以看到其变化:

计算的基准量是
parallexX - cameraGroup.position.x ,开始时, parallexX = 0.5 , cameraGroup.position.x = 0 ,此时 (parallaxX - cameraGroup.position.x) * deltaTime; 计算出来的值最大,摄像机在第一帧 要加上的移动的距离最大随着逐帧的计算进行,因为每次加上的值是一个“差值”,差值会越来愈小,所以每帧移动的幅度会越来越小,从控制台打印也可以看出这种趋势:

先快后慢,越来越慢 的效果实现了,但是快与慢的区别还不够明显,所以这里可以再调整一下变化的幅度,通过乘上一个 倍数 来实现,比如5倍:效果是很明显的:

完整的代码实现:
物理效果
一般的物理效果都可以理解成物体的
位置 以某种规律或运动曲线进行运动,从而看起来像是现实世界中的物理效果所以,页面上的物理效果实际上就是在每一帧更新物体的位置从而实现物理效果,关键就在于如何获取到每一帧物体的位置
目前市面上成熟的方案是模拟一个
物理世界 ,在物理世界中模拟 物体 的碰撞、运动等行为,然后在每一时刻获物体模拟取该行为运动的位置,同步更新到 threejs 的场景中以实现
在这里插入图片描述
比如在上面这张图中,左边是
threejs 中的场景,右边是假想的一个物理世界,我们在物理世界中模拟小球从空中落下,砸到平面后停止的行为。在这段时间中,我们可以通过物理第三方库提供的API获取到每一帧或每一时刻小球的位置,将位置更新到我们实际的 threejs 场景中,即可实现所谓的物理效果下面是常用的一些3D或2D物理效果的库:
3D物理效果:
Ammo.js :没文档, https://github.com/kripken/ammo.jsCannon.js(推荐) :有文档,但是全英,体积小,基于 Ammojs 和 threejs 实现的物理库, https://schteppe.github.io/cannon.js/Oimo.js :没文档, https://lo-th.github.io/Oimo.js/#basic2D物理效果:
Matter.js(推荐) : 比较完善且轻量,全英文档 https://brm.io/matter-js/P2.jsPlanck.jsBox2D.js3D - 小球落到平面
使用
作为示例,实现以下效果:

首先,按照其实现原理,场景中初始时有一个平面当作”地板“,有一个位于
的小球,其半径是
(这样就可以让小球初始时处于平面之上),如图:

然后,按照原理,需要一个物理世界来模拟物体运动,项目中执行
npm install cannon 安装 cannon.js ,然后进行初始化:这里创建了一个物理世界,然后给物理世界设置垂直方向也就是Y轴的加速度为
9.82 ,取负数是因为Y轴向下为负然后在物理世界中需要创建我们的物理对象,和
threejs 的场景一样,也需要一个平面和小球,代码如下:现在,在屏幕中,就存在一个
threejs 的场景(肉眼可见)和一个物理世界了(不可见)然后,我们需要
threejs 渲染的每一帧里,在物理世界中触发运动,添加如下代码:world.step 用于推进物理世界的状态,第一个参数表示 固定的逻辑时间步长 ,即每次 step 调用之间模拟的时间间隔,通常设为 1 / 60 ,表示每秒60次更新,即符合屏幕的刷新率第二个参数代表实际经过的时间,通常设为上一帧与当前帧的时间差,可以用来更准确地模拟物理状态的变化
第三个参数是可选的, 表示在每一帧内进行的子步骤数量,增加这个数量可以在物理模拟过程中提供更高的精度
通过打印物理世界中小球的位置(
)可以看到具体数值的变化,它的值从3一直无线递减:

这是因为当前物理世界中,并没有东西将他挡住或,所以它会无限地朝着虚空进行下落,虽然我们之前创建了一个地板
floor ,但是 floor 默认情况是下垂直于X轴的,这个和 threejs 是一样的(初始化创建的 plane 也是竖直的)所以,我们需要在物理世界中对屏幕进行旋转,使其能够平行于X轴,添加代码如下:
在物理世界旋转物体比较复杂,需要使用
来实现,不懂暂时也没事,这时候再打印就会发现数值是从3到0.5左右保持了:

最后,将这个数值和
threejs 里的数值关联起来,即可实现小球的掉落效果了:材质连接
cannon 提供了 contactMaterial 这个API来定义当两种材质发生接触时会产生怎样的效果对于上面的小球示例,假设小球是塑料小球,而平面是正常的水泥地,小球肯定会弹起来,接下来我们就来实现这一效果:
首先,使用
CANNON.Material 创建两个物理世界中的材质,第一个参数是我们自定义的材质名称接着通过
ContactMaterial 来定义这两个材质接触时的摩擦力、弹跳力等因素,最后将这一”限制“添加到物理世界中然后,给小球、地板绑定上这两种材质即可:

在这里插入图片描述
在这个例子里,两个
material 除了名字不同,其他都是相同的,所以可以简单地设置一个 defaultMaterial 来节省代码,因为通常在业务场景中,我们只需要一种统一的物理碰撞效果即可添加外力
两个
API , body.applyLocalForce 或 body.applyForce例如模拟一道往X轴正半轴方向吹的风,风力为300,风从原点开始吹,可以使用
applyLocalForce :也可以在每一帧对物体施加一个力来达成这种效果:

在这里插入图片描述
到目前为止的所有代码:
多个物体
我们可以将球状几何体的创建封装成函数,然后绑定到一个按钮上,通过多次点击来创建多个球状几何体,如:
将该函数绑定到
gui 按钮上:然后在每一帧里,更新球几何体的位置即可:

在这里插入图片描述
现在试着创建盒子的物理效果,到这里就有几个要注意的点了
在
threejs 中,创建几何体是通过设置指定的长宽高实现但是在
里,创建几何体不是从边缘顶点算起,而是从中心向四周扩散,如图:

所以它只需要一半的长宽高:
仿造球状几何体的创建,实现一个盒子几何体的创建函数,然后绑定到按钮上即可
这时候看效果,会发现物理效果差那么点意思:

因为按理说,盒子在碰撞到物体之后,应该会发生一定比例的旋转的,而不是这样”直上直下”的
所幸的是,
CANNONJS 的物理效果已经涵盖了这种旋转的效果,直接在 update 方法中使用 CANNONJS 的四元数进行更新即可:
在这里插入图片描述
物理性能
BroadPhase
在物理引擎中,
broadphase 是一个重要的概念,它负责检测哪些物体之间可能存在碰撞。 broadphase 的主要目的是提高碰撞检测的效率,避免不必要的细粒度碰撞检测,以此来提高性能CANNONJS 提供了三种 broadphase :NaiveBroadphase :默认的情况,会对所有物体进行碰撞检测,每次添加一个物体,这个新物体会和之前所有的物体进行一次碰撞检测,时间复杂度为 O(n^2)适用于物体数量较少的场景
GridBroadphase :基于网格进行检测,相当于把物理世界用网格区分开,每个网格内从存储位于该网格内的物体,只对有物体的网格和其相邻的网格进行检测,通常情况下时间复杂度为 O(n) ,但是某些情况下可能退化成 O(n^2)适用于物体分布均匀的大规模场景
SAPBroadphase :基于排序和修剪的算法,它会维护一个排序列表,记录每个物体在各个轴上的边界。其内部通过定期更新排序列表来判断相邻物体是否相交,平均情况下时间复杂度为 O(n * log(n)) ,某些情况下可以优化到 O(n)适用于物体多且变化较大的场景
在
CANNONJS 里修改 Broadphase 的方式:sleep
通过设置
world.allowSleep = true 来让物理引擎自动判断场景中的物体是否处于 休眠 状态,处于 休眠 状态的物体不会参与各种计算,如果性能出现了问题,可以试着把这个打开交互事件
CANNONJS 提供了事件监听,可以对物体使用 addEventlistener 来实现监听最常用的是碰撞的事件,通过
collide 事件实现基于之前的
demo 代码,可以实现一个 碰撞后发出声音 的需求:更进一步的,每一次碰撞都应该产生新的声音,所以在每次
playSound 的时候应该将声音的播放位置重置:既然是碰撞,那就肯定存在强度之分,物体
的回调也同样存在一个
参数:

这里可以通过
获取本次碰撞的强度:

所以,为了实现更逼真的音效模拟,我们可以对声音的大小使用碰撞强度来控制
通过控制台的打印会发现,每次碰撞实际上会触发4次
eventListener 的回调,这个是因为物体间的接触取决于 接触点 的多少,所以我们也可以适当的添加防抖来解决该问题demo 地址
git@github.com:JohnWicc/threejs-demo.git约束
cannonjs 里提供了 Constraint 类来实现物体与物体间的约束比如假设
threejs 场景里要实现一根绳子连着两个物体,那么在物理世界中就需要通过 constraint 来进行配置,模拟出两个物体相连的物理效果你只需要知道它能做到这些,不需要知道它怎么做到的,实际需要的时候再去深入阅读文档对应内容来实现即可;这一方法同样适用于cannonjs里的其他API
卡死的问题
当往场景中添加物体过多时,可能会导致页面卡死,比如:

这是因为
cannonjs 物理世界的运算是在CPU里跑的,而不是GPU,当前浏览器页面默认只会使用一条CPU的主线程来执行,所以卡死是因为CPU计算超载了要解决这个问题,可以使用
来单独为物理世界的运算分配一个进程处理,参考官网提供的例子来实现即可:

cannon-es
cannon.js 已经很久没有更新了, cannon-es 是由社区在其基础上继续维护的库,同时它支持 typescript ,开发时的提示更加友好physijs
physijs 是与 threejs 高度整合的一个 物理插件 ,本身是基于 Ammo.js 的二次开发,用他可以更快速地给 threejs 场景添加上物理效果,但是它目前还处于起步阶段,并且可自定义的配置没那么多https://chandlerprall.github.io/Physijs/three-to-cannon
一个可以快速将
threejs 的几何体转为 cannonjs 里的 shape 的库,比如未来我们有了一个外部导入的模型,要对其实现一些物理效果,就可以使用这个库快速地在物理世界构建出对应形状得物体来进行模拟:https://github.com/donmccurdy/three-to-cannon