【图中完整效果代码位于文章末】
在Web开发中,结合Three.js的强大3D渲染能力和CANNON.js的物理模拟功能,我们可以创造出既美观又真实的物理交互场景。本文将引导你完成一个简单的项目,展示如何使用Three.js和CANNON.js创建一个带有物理碰撞的小球与平面场景。我们将从环境搭建开始,逐步深入到物体创建、物理规则设置以及渲染循环的建立。
目录
环境准备
首先,确保你的项目中已安装了Three.js和CANNON.js库。对于Vue项目,你可以在项目中通过npm安装这两个库:
npm install three cannon-es
引入依赖
在Vue组件中,我们首先引入必要的模块:
<script setup>
import * as THREE from 'three';
import { onMounted } from 'vue';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import * as CANNON from 'cannon-es';
</script>
初始化场景与相机
接下来,初始化Three.js的基本元素:场景、相机、渲染器以及OrbitControls用于视角操控:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
const controls = new OrbitControls(camera, renderer.domElement);
创建物理世界
使用CANNON.js创建物理世界,并设置重力:
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0); // 重力向下的设置
创建3D物体
我们创建一个小球和一个地面。在Three.js中,这涉及到几何体、材质以及网格的创建;而在CANNON.js中,则需要创建相应的物理形状和body:
// Three.js中的小球
const sphereGeometry = new THREE.SphereGeometry(1, 26, 26);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
sphere.position.set(0, 5, 0.5);
scene.add(sphere);
// CANNON.js中的小球物理体
const sphereShape = new CANNON.Sphere(1);
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 5, 0.5),
mass: 1,
material: new CANNON.Material('sphereMaterial'),
});
world.addBody(sphereBody);
// Three.js中的地面
const floorGeometry = new THREE.PlaneGeometry(20, 20);
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0xe4e4e4, transparent: true, opacity: 0.5 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// CANNON.js中的地面物理体
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body({
mass: 0, // 静态物体质量设为0
shape: floorShape,
material: new CANNON.Material('groundMaterial'),
});
floorBody.position.set(0, -5, 0);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
设置材质接触属性
定义两种材质并设置它们之间的接触属性,以控制摩擦和弹性:
const sphereWorldMaterial = new CANNON.Material('sphereMaterial');
const groundWorldMaterial = new CANNON.Material('groundMaterial');
const contactMaterial = new CANNON.ContactMaterial(
sphereWorldMaterial,
groundWorldMaterial,
{ friction: 0.1, restitution: 0.6 }
);
world.addContactMaterial(contactMaterial);
光照与渲染
为场景添加光照,并启动渲染循环:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);
function render() {
requestAnimationFrame(render);
const deltaTime = clock.getDelta();
world.step(1 / 120, deltaTime);
sphere.position.copy(sphereBody.position);
controls.update();
renderer.render(scene, camera);
}
render();
关于这段循环渲染代码的解释:
const deltaTime = clock.getDelta();
clock: 这是一个THREE.Clock对象的实例,它被用来测量两次连续调用之间的间隔时间。在实时渲染应用中,比如游戏和交互式3D场景,我们通常希望动画和物理效果能够以恒定的速度运行,不随系统性能波动。这就需要基于实际流逝的时间(delta time)来调整每一次迭代的更新量。
getDelta(): 这个方法返回自上一次调用以来的时间差(单位通常是秒)。deltaTime因此代表了从上次渲染到现在这段时间内实际流逝的时间。使用deltaTime可以帮助我们的动画和物理效果保持一致,无论用户的电脑速度快慢。
world.step(1 / 120, deltaTime);
world: 这是CANNON.js中CANNON.World的实例,代表了整个物理世界,在其中管理着所有物理对象和它们之间的相互作用。
step(dt, deltaTime): 这是更新物理世界状态的方法。它接受两个参数:
dt(或称为时间步长): 表示每次物理模拟计算的时间跨度。这里的1 / 120意味着每秒钟我们希望物理引擎进行120次迭代计算。选择合适的时间步长对物理模拟的稳定性和准确性至关重要。较大的时间步可能引起不稳定,而过小则会增加计算负担。
deltaTime: 我们之前计算出的实际流逝时间。传入deltaTime是为了让物理模拟与实际时间流逝保持同步。如果直接使用固定的时间步长而不考虑实际渲染时间,快速或慢速的设备都可能导致物理行为看起来不自然或出现不同步现象。
总结
通过以上步骤,我们成功创建了一个包含小球自由落体并弹跳于地面的简单物理场景。Three.js负责渲染视觉效果,而CANNON.js则处理物理模拟逻辑,两者结合让我们能够轻松实现物理碰撞效果。CANNON.js支持更多复杂的物理特性,包括但不限于刚体动力学、约束、碰撞检测等,其他功能将在后续文章中讲解。
完整代码如下
<template></template>
<script setup>
import * as THREE from 'three'
import { onMounted } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 导入connon引擎
import * as CANNON from 'cannon-es'
// 创建物理世界
const world = new CANNON.World()
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
const controls = new OrbitControls(camera, renderer.domElement)
scene.background = new THREE.Color(0x000000)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.outputEncoding = THREE.sRGBEncoding
// 设置时钟
const clock = new THREE.Clock()
onMounted(() => {
init()
})
function init() {
camera.position.set(0, 5, 10)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 26, 26) // 球的几何形状
const sphereMaterial = new THREE.MeshStandardMaterial()
// 定义两个材料
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
sphere.castShadow = true // 阴影
sphere.position.set(0, 5, 0.5) // 球的位置
scene.add(sphere)
const floorMaterial = new THREE.MeshStandardMaterial({
// map: texture,
color: 0xe4e4e4,
transparent: true,
opacity: 0.5,
})
const floor = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), floorMaterial)
floor.position.set(0, -5, 0)
floor.rotation.x = -Math.PI / 2
floor.receiveShadow = true // 接收阴影
scene.add(floor)
// 设置物理世界的重力
world.gravity.set(0, -9.8, 0)
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1)
//设置物体材质
// const sphereWorldMaterial = new CANNON.Material()
const sphereWorldMaterial = new CANNON.Material('sphereMaterial')
const groundWorldMaterial = new CANNON.Material('groundMaterial')
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 5, 0.5),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
})
// 将物体添加至物理世界
world.addBody(sphereBody)
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5)
dirLight.castShadow = true // 阴影
scene.add(dirLight)
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
// controls.enableDamping = true
// 物理世界创建地面
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body()
floorBody.material = groundWorldMaterial
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0
floorBody.addShape(floorShape)
// 地面位置
floorBody.position.set(0, -5, 0)
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(
new CANNON.Vec3(1, 0, 0),
-Math.PI / 2
)
world.addBody(floorBody)
//创建联系材质
const concretePlasticMaterial = new CANNON.ContactMaterial(
sphereWorldMaterial,
groundWorldMaterial,
{
friction: 0.1,
restitution: 0.6,
}
)
//添加联系材质
world.addContactMaterial(concretePlasticMaterial)
function render() {
let deltaTime = clock.getDelta()
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime) // 更新
controls.update()
sphere.position.copy(sphereBody.position) // 渲染引擎复制物理引擎中的数据
renderer.render(scene, camera)
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render)
}
render()
}
</script>