我们都知道苹果从树上脱落后会掉下来而不是飘在空中是因为万有引力,苹果掉下来不会在地面上无限滚动,因为有摩擦。如果掉下来的是个乒乓球,大概还会弹上几下,但如果是掉在苹果地里,也可能弹不起来,因为地面的材质不一样,摩擦力也不一样。还有很多物理因素决定了或约束了物体的运动或行为。
在3D渲染中我们要给物体设置如材质,摩擦力、弹力、重力等物理因素等来模拟物理效果,来创建真实的3D场景。而Threejs是一个3D图形库,可以渲染出炫酷3D场景,但物体间的交互效果,如乒乓球与地面碰撞效果,threejs没有内置的解决方案,需要与物理引擎结合使用,本文使用的是物理引擎是Cannon.js。
一、 举个例子:
使用three.js渲染一个场景,一个球体和一个平面,使用cannon.js模拟小球的真实的受力与运动,以及与地面碰撞效果。
1、搭建一个场景
搭建目录结构看这里
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 10);
//将相机添加到
scene.add(camera);
//创建环境光,环境光会均匀的照亮场景中的所有物体。
const light = new THREE.AmbientLight(0x404040);
//将环境光添加到场景
scene.add(light);
// 创建平行光
const directionalLight = new THREE.DirectionalLight();
//设置光源位置
directionalLight.position.set(0, 5, 0);
//添加到场景
scene.add(directionalLight);
//设置光源投射阴影
directionalLight.castShadow= true
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
//开启渲染器阴影计算
renderer.shadowMap.enabled = true;
//将canvas添加到body中
document.body.appendChild(renderer.domElement);
// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 轨道控制器的阻尼感
controls.enableDamping = true;
//辅助坐标轴
const axesHelp = new THREE.AxesHelper();
scene.add(axesHelp);
const clock = new THREE.Clock()
//渲染函数
function render() {
//阻尼
controls.update()
let time = clock.getDelta();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
// 初始化渲染函数
render();
// 监听浏览器窗口尺寸变化
window.addEventListener('resize',() => {
//重新设置相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
//更新相机投影矩阵
camera.updateProjectionMatrix();
//重新设置渲染器尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
//设置设备像素比
renderer.setPixelRatio(window.devicePixelRatio)
})
效果:
2、添加物体(小球和平面)
// 创建一个小球
const sphereGeometry = new THREE.SphereGeometry(1, 20,20);
//创建一个标准网格材质
const material = new THREE.MeshStandardMaterial();
//创建物体
const sphere = new THREE.Mesh(sphereGeometry, material);
//物体添加到场景中
scene.add(sphere)
//开启物体投射阴影
sphere.castShadow = true
// 创建一个平面
const planeGeometry = new THREE.PlaneGeometry(30,30);
const plane = new THREE.Mesh(planeGeometry,material)
scene.add(plane)
//设置平面位置
plane.position.set(0,-5,0);
//设置平面角度
plane.rotation.x = -Math.PI / 2;
//接收阴影
plane.receiveShadow = true;
效果:
我们可以看到一个球静止在空中,在真实世界这是不可能的,小球一定会掉下来,就像苹果会砸到牛顿,小球也会砸到地面。所以需要引入物理引擎来实现这样的效果。
3、引入cannon.js模拟物理效果
想象一下,你想要创建一个虚拟的现实世界,threejs负责渲染并呈现画面。而cannon.js负责各个元素之间的相互作用和动作表现。例如,当一个球体在斜坡上滚动时,Cannon.js 负责计算球体的速度、方向和受到的力,以及球体与斜坡之间的碰撞情况等。
上面已经渲染出了画面,现在需要创建一个物理环境,来处理物体之间的碰撞、受力和运动。然后将这些物理效果添加给threejs创建的物体,呈现效果。
安装:npm i cannon-es
引入:
// 引入物理引擎
import * as CANNON from 'cannon-es';
创建物理世界,并在物理世界创建一个小球和平面,计算它们的运动与碰撞,将物体的运动效果复制到我们能看到的threejs创建的物体中。写法跟three.js差不多。
物理世界小球:
// 创建物理世界
const world = new CANNON.World()
// 设置重力
world.gravity.set(0,-9.8,0)
//世界的小球
const sphereWorld = new CANNON.Sphere(1)
//材质
const worldSphereMaterial = new CANNON.Material()
const sphereBody = new CANNON.Body({
shape: sphereWorld,
material:worldSphereMaterial,
position: new CANNON.Vec3(0,0,0),
// 小球的质量
mass: 1
})
// 添加到世界
world.addBody(sphereBody)
物理世界平面:
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material()
floorBody.material = floorMaterial;
// 设置质量为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 clock = new THREE.Clock()
function render() {
// 获取每一帧的时间间隔
let time = clock.getDelta();
// 根据指定的时间步长来更新物体的物体的位置、旋转,同时计算碰撞、应用力和其他物理效果。来推荐物体运动。
// 这里的1/120是时间步长,也就是多久更新一次物体的物理状态。time是上次调用step到当前的时间间隔。
// 时间步长要根据准确性和性能来设置
world.step(1/120,time);
sphere.position.copy(sphereBody.position)
}
效果:
还可以设置小球和平面两种材质的碰撞参数,比如摩擦力,弹力等。
// 设置碰撞材质的参数。将两种材质关联
const defaultContactMaterial = new CANNON.ContactMaterial(
worldSphereMaterial,
floorMaterial,
{
// 摩擦力
friction:0.1,
// 弹力
restitution:0.8
}
)
// 这些API直接看官网就好了,不用都记住
world.addContactMaterial(defaultContactMaterial)
再看下效果:
还可以监听物体碰撞,添加个碰撞音效
// 添加碰撞音效
const listener = new THREE.AudioListener();
camera.add(listener)
const sound = new THREE.Audio(listener);
scene.add( sound );
const audioLoader = new THREE.AudioLoader()
audioLoader.load('collision.mp3',function(buffer){
sound.setBuffer(buffer)
sound.play();
})
二、多个物体碰撞
实现这样一个效果,每次点击都创建一个立方体,给每个立方体添加运动和碰撞效果。
封装一个函数实现创建一个立方体并给立方体添加运动和碰撞效果,监听点击事件,点击创建立方体。和上面那个例子差不多。
// 创建物理世界
const world = new CANNON.World()
// 设置重力
world.gravity.set(0,-9.8,0)
const cubeWorldMaterial = new CANNON.Material('cube')
// 立方体数组
let cubeArr = []
// 创建立方体并添加物理状态
function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1,1,1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry,cubeMaterial);
cube.castShadow = true;
scene.add(cube);
//创建物理世界的物体
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5,0.5,0.5));
const cubeBody = new CANNON.Body(
{
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
mass: 1,
material:cubeWorldMaterial
}
);
//在物体的本地坐标系(相对于物体自身坐标系)下施加力。本地坐标系可以让我们更直观的控制物体的运动和相互作用。全局坐标系是一个统一的坐标系,描述物体在场景下的具体位置。
cubeBody.applyLocalForce(
new CANNON.Vec3(300,0,0), // 添加的力的方向和大小
new CANNON.Vec3(0,0,0), // 力的作用点
)
world.addBody(cubeBody);
cubeArr.push({
mesh: cube,
body: cubeBody
})
}
平面的创建和物体碰撞参数的设置和上面一样
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material()
floorBody.material = floorMaterial;
// 设置质量为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 defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction:0.1,
// 弹力
restitution:0.7
}
)
world.addContactMaterial(defaultContactMaterial)
// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;
监听点击事件,创建立方体。
window.addEventListener('click',createCube)
渲染
//渲染函数
function render() {
let deltaTime = clock.getDelta();
world.step(1/120,deltaTime);
// 遍历立方体,给每个立方体添加运动、旋转、碰撞状态
cubeArr.forEach(item => {
item.mesh.position.copy(item.body.position)
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
})
renderer.render(scene, camera);
requestAnimationFrame(render);
}
效果: