Three.js物理效果实践
这段代码通过 Three.js 和 cannon-es 库,创建了一个有趣的交互式 3D 物理模拟场景。当你在页面上点击鼠标时,会发射出一个小球,它会在重力作用下弹跳并与地面发生碰撞。
以下是这段代码的逐步解析,以及它如何协同工作来构建这个模拟场景:
一、设置 3D 场景基础
首先,代码导入了所需的库:
-
THREE:用于创建和渲染 3D 图形。 -
CANNON:一个轻量级的 JavaScript 物理引擎,用于处理碰撞、重力和运动。 -
OrbitControls:来自 Three.js 的一个辅助工具,让你能够通过鼠标拖动、缩放和旋转来控制相机视角。
接下来,代码设置了 Three.js 的核心组件:
-
scene(场景):3D 世界的容器,所有物体、灯光和相机都放在这里。 -
camera(相机):你观察世界的视点。这里使用PerspectiveCamera创建了一个透视相机,模拟了人眼的观察方式。 -
renderer(渲染器):负责将场景中的 3D 对象渲染到你的浏览器窗口中。它还启用了阴影贴图 (renderer.shadowMap.enabled = true;),让场景看起来更真实。 -
directionalLight(平行光):模拟了太阳光,它从上方照射,并启用了投射阴影。为了提高阴影质量,代码还调整了阴影的分辨率和相机范围。
import * as THREE from 'three'; import * as CANNON from 'cannon-es'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // 场景 const scene = new THREE.Scene(); //相机 const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 100, 30); camera.lookAt(0, 0, 0); //渲染 const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);
二、创建地面
-
视觉上的地面:使用
THREE.PlaneGeometry和THREE.MeshStandardMaterial创建了一个 Three.js 平面。这个平面被旋转到水平,并设置了receiveShadow = true,使其能够接收其他物体投射的阴影。 -
物理上的地面:使用
CANNON.Plane和CANNON.Body创建了一个物理世界的平面。它的mass被设置为0,这意味着它是一个不可移动的静态物体。这个物理平面与视觉平面被旋转到相同的方向,从而在物理世界和视觉世界中对齐。
//加载器 const loader = new THREE.TextureLoader(); const texture = loader.load('./images/01.jpg'); //创建一个平面 const planeGeometry = new THREE.PlaneGeometry(100, 100, 100, 100); const planeMaterial = new THREE.MeshStandardMaterial({ map: texture }); const plane = new THREE.Mesh(planeGeometry, planeMaterial); plane.rotation.x = -Math.PI / 2; scene.add(plane); // 创建一个物理世界 const world = new CANNON.World(); world.gravity.set(0, -9.82, 0); // 创建物理材质 const groundPhysMat = new CANNON.Material(); const ballPhysMat = new CANNON.Material(); // 配置接触材质 const contactMaterial = new CANNON.ContactMaterial( groundPhysMat, ballPhysMat, { restitution: 0.9, // 弹性系数 0-1(1为完全弹性碰撞) friction: 0.8 // 摩擦系数 } ); world.addContactMaterial(contactMaterial); // 创建物理地面 const planeShape = new CANNON.Plane() const planeBody = new CANNON.Body({ mass: 0, shape: planeShape, material: groundPhysMat }) planeBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up world.addBody(planeBody)
三、创建小球
定义一个 createSphere 函数,同时创建了 Three.js 的视觉小球和 cannon-es 的物理小球:
-
视觉小球:一个
THREE.SphereGeometry加上THREE.MeshStandardMaterial,用于在场景中显示。 -
物理小球:一个
CANNON.Sphere加上CANNON.Body。它的mass被设置为0.5,这意味着它会受到重力和其他力的影响。
当你在页面上点击鼠标时,一个事件监听器会触发:
-
Raycaster:Three.js 的一个工具,用于从相机位置发射一条射线,来确定鼠标点击了 3D 空间中的哪个位置和方向。 -
createSphere被调用:根据射线的信息,在点击的位置创建一个新的小球。 -
applyLocalForce:在物理小球创建后,立即给它一个力,使其沿着你点击的方向飞出去。
const createSphere = (position, direction) => { //创建一个小球 const radio = 10; const ballGeometry = new THREE.SphereGeometry(radio, 30, 30); const ballMaterial = new THREE.MeshStandardMaterial({ map: texture }); const ball = new THREE.Mesh(ballGeometry, ballMaterial); ball.position.copy(position); ball.castShadow = true; scene.add(ball); // 创建一个物理小球 const ballShape = new CANNON.Sphere(radio); const ballBody = new CANNON.Body({ mass: 0.5, position: ball.position, shape: ballShape, material: ballPhysMat }); //给物理小球添加力 ballBody.applyLocalForce( direction.scale(600), new CANNON.Vec3(0, 0, 0) ); world.addBody(ballBody); spheres.push({ three: ball, cannon: ballBody }) } renderer.domElement.addEventListener('click', (event) => { //坐标归化 const mouse = new THREE.Vector2(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const pos = new THREE.Vector3(); pos.copy(raycaster.ray.direction); pos.add(raycaster.ray.origin); const direction = new CANNON.Vec3( raycaster.ray.direction.x, raycaster.ray.direction.y, raycaster.ray.direction.z ); createSphere({ x: pos.x, y: pos.y, z: pos.z }, direction) })
四、物理世界与视觉世界的同步
这是整个模拟的核心部分。物理引擎和渲染引擎是独立运行的,你需要将它们的结果同步起来。
-
world.step(1 / 60):这是物理世界的关键步骤。它告诉 cannon-es 引擎,时间已经过去了 1/60 秒。在这个时间步长内,引擎会计算所有物体的运动、碰撞、重力等物理效果。 -
spheres.forEach(...):这段代码遍历了所有创建的小球。对于每个小球,它会将物理世界中的位置 (sphere.cannon.position) 和旋转 (sphere.cannon.quaternion) 拷贝给 Three.js 对应的视觉小球 (sphere.three.position和sphere.three.quaternion)。 -
animate()函数:使用requestAnimationFrame循环调用animate()函数。在这个循环里,它会:-
更新相机控制器 (
controls.update())。 -
调用
update()函数,从而同步物理和视觉世界。 -
重新渲染场景 (
renderer.render(scene, camera))。
-
// 物理世界和threejs世界同步 function update() { world.step(1 / 60); spheres.forEach(sphere => { sphere.three.position.copy(sphere.cannon.position); // 球面朝向滚动 sphere.three.quaternion.copy(sphere.cannon.quaternion); }) } //创建一个轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; function animate() { requestAnimationFrame(animate); controls.update(); update() renderer.render(scene, camera); } animate();
五、碰撞效果
小球的弹跳效果是通过 CANNON.ContactMaterial 配置的。
-
groundPhysMat和ballPhysMat:分别定义了地面和小球的物理材质。 -
contactMaterial:这个对象定义了这两种材质相互接触时的行为。-
restitution: 0.9:这是弹性系数。0.9表示碰撞后会损失一点能量,但小球会弹起得很高(1是完全弹性碰撞,0是完全非弹性碰撞,不会弹起)。 -
friction: 0.8:这是摩擦系数。它决定了小球在地面上滚动时受到的阻力。
-
// 创建物理材质 const groundPhysMat = new CANNON.Material(); const ballPhysMat = new CANNON.Material(); // 配置接触材质 const contactMaterial = new CANNON.ContactMaterial( groundPhysMat, ballPhysMat, { restitution: 0.9, // 弹性系数 0-1(1为完全弹性碰撞) friction: 0.8 // 摩擦系数 } ); world.addContactMaterial(contactMaterial);
全部代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> body { margin: 0; padding: 0; } </style> </head> <body> <script type="module"> import * as THREE from 'three'; import * as CANNON from 'cannon-es'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // 场景 const scene = new THREE.Scene(); //相机 const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 100, 30); camera.lookAt(0, 0, 0); //渲染 const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); renderer.shadowMap.enabled = true; // 从上方照射的白色平行光,强度为 0.5。 const directionalLight = new THREE.DirectionalLight(0xffffff, 3); directionalLight.position.set(20, 20, 0); directionalLight.castShadow = true; // 增加阴影质量设置 directionalLight.shadow.mapSize.width = 4096; // 提高阴影分辨率 directionalLight.shadow.mapSize.height = 4096; directionalLight.shadow.camera.near = 0.5; // 调整阴影相机范围 directionalLight.shadow.camera.far = 500; directionalLight.shadow.camera.left = -100; // 扩展阴影计算区域 directionalLight.shadow.camera.right = 100; directionalLight.shadow.camera.top = 100; directionalLight.shadow.camera.bottom = -100; scene.add(directionalLight); //加载器 const loader = new THREE.TextureLoader(); const texture = loader.load('./images/01.jpg'); //创建一个平面 const planeGeometry = new THREE.PlaneGeometry(100, 100, 100, 100); const planeMaterial = new THREE.MeshStandardMaterial({ map: texture }); const plane = new THREE.Mesh(planeGeometry, planeMaterial); plane.rotation.x = -Math.PI / 2; plane.receiveShadow = true; scene.add(plane); // 创建一个物理世界 const world = new CANNON.World(); world.gravity.set(0, -9.82, 0); // 创建物理材质 const groundPhysMat = new CANNON.Material(); const ballPhysMat = new CANNON.Material(); // 配置接触材质 const contactMaterial = new CANNON.ContactMaterial( groundPhysMat, ballPhysMat, { restitution: 0.9, // 弹性系数 0-1(1为完全弹性碰撞) friction: 0.8 // 摩擦系数 } ); world.addContactMaterial(contactMaterial); // 创建物理地面 const planeShape = new CANNON.Plane() const planeBody = new CANNON.Body({ mass: 0, shape: planeShape, material: groundPhysMat }) planeBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up world.addBody(planeBody) const spheres = [] const createSphere = (position, direction) => { //创建一个小球 const radio = 10; const ballGeometry = new THREE.SphereGeometry(radio, 30, 30); const ballMaterial = new THREE.MeshStandardMaterial({ map: texture }); const ball = new THREE.Mesh(ballGeometry, ballMaterial); ball.position.copy(position); ball.castShadow = true; scene.add(ball); // 创建一个物理小球 const ballShape = new CANNON.Sphere(radio); const ballBody = new CANNON.Body({ mass: 0.5, position: ball.position, shape: ballShape, material: ballPhysMat }); //给物理小球添加力 ballBody.applyLocalForce( direction.scale(600), new CANNON.Vec3(0, 0, 0) ); world.addBody(ballBody); spheres.push({ three: ball, cannon: ballBody }) } renderer.domElement.addEventListener('click', (event) => { //坐标归化 const mouse = new THREE.Vector2(); mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const pos = new THREE.Vector3(); pos.copy(raycaster.ray.direction); pos.add(raycaster.ray.origin); const direction = new CANNON.Vec3( raycaster.ray.direction.x, raycaster.ray.direction.y, raycaster.ray.direction.z ); createSphere({ x: pos.x, y: pos.y, z: pos.z }, direction) }) // 物理世界和threejs世界同步 function update() { world.step(1 / 60); spheres.forEach(sphere => { sphere.three.position.copy(sphere.cannon.position); // 球面朝向滚动 sphere.three.quaternion.copy(sphere.cannon.quaternion); }) } //创建一个轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; function animate() { requestAnimationFrame(animate); controls.update(); update() renderer.render(scene, camera); } animate(); </script> </body> </html>
效果展示:

六、总结
这段代码通过结合 Three.js 和 cannon-es,成功地创建了一个实时的 3D 物理模拟场景。Three.js 负责所有你看得到的视觉效果,而 cannon-es 则在幕后处理所有复杂的物理计算。通过将这两个世界的位置和旋转同步起来,我们就看到了一个逼真且富有交互性的物理世界。

浙公网安备 33010602011771号