一、物理运动原理
在 Three.js 中实现物理运动的核心思想是:
- Three.js 负责渲染:Three.js 是一个图形库,用于创建和渲染 3D 场景。
- 物理引擎负责计算:物理引擎(如 Cannon.js或 Ammo.js, 本文用的是前一种)模拟现实世界的物理规则,例如重力、碰撞、反弹等。
- 数据同步:在每一帧中,将物理引擎计算的结果(如物体的位置、速度、旋转等)同步到 Three.js 的对象上。
二、实现步骤详解
1. 初始化 Three.js 场景
在 initScene 方法中,创建 Three.js 的基本组件(场景、相机、渲染器等)。
function initScene() {
if (!canvas.value) return
makeRenderer(canvas.value.clientWidth, canvas.value.clientHeight)
makeScene()
makeCamera(canvas.value.clientWidth, canvas.value.clientHeight)
orbitControls()
addSky()
addGround()
}
- 初始化 WebGL 渲染器,并设置画布大小和设备像素比。
makeRenderer - 创建 Three.js 场景,并添加光源。
makeScene - 创建透视相机,并设置初始位置。
makeCamera - 添加轨道控制器,用于控制相机视角。
orbitControls - 和 :分别添加天空和地面。
addSky``addGround
2. 创建物理世界
使用 Cannon.js 创建物理世界,并设置重力。
world = new Cannon.World()
world.gravity.set(0, -9.82, 0) // 设置重力 (m/s²)
world.gravity.set(0, -9.82, 0):设置重力方向为向下(Y 轴负方向),模拟地球重力。
3. 创建物体并同步到物理引擎
在 Three.js 和物理引擎中分别创建物体,并保持它们之间的同步。
创建球体(Three.js 对象)
sphere = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xff11ff }),
)
sphere.position.set(0, 10, 0)
scene.add(sphere)
- 使用
THREE.SphereGeometry创建一个半径为 2 的球体。 - 将球体添加到 Three.js 场景中。
创建对应的物理刚体(Cannon.js 对象)
const sphereBody = new Cannon.Body({
mass: 1,
shape: new Cannon.Sphere(2),
position: new Cannon.Vec3(sphere.position.x, sphere.position.y, sphere.position.z),
})
world.addBody(sphereBody)
- 使用
Cannon.Sphere创建一个物理刚体,并设置质量为 1。 - 将物理刚体添加到物理世界中。
添加地面
const groundBody = new Cannon.Body({
mass: 0,
shape: new Cannon.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
- 创建一个静态的平面作为地面(
mass: 0表示静态物体)。 - 使用
setFromEuler旋转平面使其水平。
4. 动画循环
在每一帧中更新物理引擎的状态,并同步到 Three.js 场景。
function animate() {
animationFrameId = requestAnimationFrame(animate)
// 更新物理引擎
updatePhysic()
// 渲染场景
renderer.render(scene, camera)
}
function updatePhysic() {
world.step(1 / 60) // 模拟 60 帧每秒
sphere.position.copy(sphereBody.position) // 同步位置
}
world.step(1 / 60):更新物理引擎的世界状态,模拟 60 帧每秒。sphere.position.copy(sphereBody.position):将物理引擎计算的结果同步到 Three.js 的球体对象上。
5. 处理窗口大小调整
当窗口大小发生变化时,重新调整渲染器和相机的参数。
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
- 调整相机的宽高比,并更新投影矩阵。
- 重新设置渲染器的尺寸。
三、完整流程总结
- 初始化 Three.js 场景:
- 创建场景、相机、渲染器。
- 添加光源、天空、地面等元素。
- 初始化物理引擎:
- 创建物理世界,并设置重力。
- 添加地面和物体的物理刚体。
- 动画循环:
- 在每一帧中更新物理引擎的状态。
- 将物理引擎的结果同步到 Three.js 对象上。
- 渲染场景。
- 处理窗口大小调整:
- 监听窗口大小变化事件,动态调整相机和渲染器的参数。
四、关键点解析
1. 数据同步
- 物理引擎中的物体(如
sphereBody)和 Three.js 中的物体(如sphere)需要保持同步。 - 在每一帧中,通过
sphere.position.copy(sphereBody.position)实现位置同步。
2. 性能优化
- 使用
requestAnimationFrame创建动画循环,确保流畅的动画效果。 - 设置物理引擎的更新频率(如
world.step(1 / 60)),避免过度计算。
3. 物理材质
- 在代码中,定义了两种物理材质(
groundMaterial和sphereMaterial),并通过 设置摩擦系数和弹性系数。ContactMaterial - 这些参数影响物体的碰撞行为,例如反弹高度和滑动距离。
五、源码
<template>
<el-container style="width: 100%; height: 100%">
<!-- 头部标题 -->
<el-header class="header"> 物理运动</el-header>
<!-- 主要内容区域,包含 Three.js 画布 -->
<el-main class="canvas-container">
<canvas ref="canvas" class="canvas"></canvas>
</el-main>
</el-container>
</template>
<script lang="ts">
export default {
name: 'PhysicalMotion', // 使用多词名称
}
</script>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { Sky } from 'three/examples/jsm/objects/Sky'
import { ShaderMaterial, MathUtils } from 'three'
import * as Cannon from 'cannon'
// Three.js 相关变量声明
// 场景
let scene: THREE.Scene
// 透视相机
let camera: THREE.PerspectiveCamera
// WebGL渲染器
let renderer: THREE.WebGLRenderer
// 天空
let sky: THREE.Mesh
// 动画帧ID
let animationFrameId: number
let controls: InstanceType<typeof OrbitControls>
let world: InstanceType<typeof Cannon.World>
let sphere: THREE.Mesh
let sphereBody: InstanceType<typeof Cannon.Body>
// 画布引用
const canvas = ref<HTMLCanvasElement>()
// 组件挂载时初始化场景并开始动画
onMounted(() => {
initScene()
animate()
// 添加窗口大小改变事件监听
window.addEventListener('resize', onWindowResize)
})
// 初始化场景
function initScene() {
if (!canvas.value) return
makeRenderer(canvas.value.clientWidth, canvas.value.clientHeight)
makeScene()
// 创建圆形
sphere = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xff11ff }),
)
sphere.position.set(0, 10, 0)
scene.add(sphere)
world = new Cannon.World()
world.gravity.set(0, -9.82, 0)
//创建物理材料
const groundMaterial = new Cannon.Material("groundMaterial")
const sphereMaterial = new Cannon.Material("sphereMaterial")
const contactMaterial = new Cannon.ContactMaterial(groundMaterial, sphereMaterial, {
friction: 0.3,
restitution: 0.5,
})
world.addContactMaterial(contactMaterial)
sphereBody = new Cannon.Body({
mass: 1,
shape: new Cannon.Sphere(2),
position: new Cannon.Vec3(sphere.position.x, sphere.position.y, sphere.position.z),
material: sphereMaterial,
})
world.addBody(sphereBody)
const groundBody = new Cannon.Body({
mass: 0,
shape: new Cannon.Plane(),
material: groundMaterial,
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
makeCamera(canvas.value.clientWidth, canvas.value.clientHeight)
orbitControls()
addSky()
addGround()
}
function updatePhysic(){
world.step(1 / 60)
sphere.position.copy(sphereBody.position)
}
function makeScene() {
// 创建场景
scene = new THREE.Scene()
//添加光源
const light = new THREE.DirectionalLight(0xffffff, 1)
scene.add(light)
}
function makeRenderer(width: number, height: number) {
// 创建WebGL渲染器
renderer = new THREE.WebGLRenderer({ canvas: canvas.value, antialias: true })
renderer.setSize(width, height)
// 设置设备像素比,确保在高分辨率屏幕上清晰显示
renderer.setPixelRatio(window.devicePixelRatio)
}
function makeCamera(width: number, height: number) {
// 创建透视相机
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
// 设置相机位置
camera.position.set(0, 10, 20)
camera.lookAt(0, 0, 0)
}
function orbitControls() {
controls = new OrbitControls(camera, canvas.value)
// 限制摄像机的极角范围(单位是弧度)
controls.minPolarAngle = 0 // 最小角度,0 表示水平视角
controls.maxPolarAngle = MathUtils.degToRad(85) // 最大角度,例如 85 度
// 可选:限制摄像机的缩放范围
controls.minDistance = 5 // 最小距离
controls.maxDistance = 100 // 最大距离
}
function addSky() {
sky = new Sky()
sky.scale.setScalar(450000) // 设置天空的缩放比例
scene.add(sky)
const sun = new THREE.Vector3()
// 配置天空参数
const effectController = {
turbidity: 10,
rayleigh: 2,
mieCoefficient: 0.005,
mieDirectionalG: 0.8,
elevation: 2,
azimuth: 180,
exposure: renderer.toneMappingExposure,
}
const uniforms = (sky.material as ShaderMaterial).uniforms
uniforms['turbidity'].value = effectController.turbidity
uniforms['rayleigh'].value = effectController.rayleigh
uniforms['mieCoefficient'].value = effectController.mieCoefficient
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG
const phi = THREE.MathUtils.degToRad(90 - effectController.elevation)
const theta = THREE.MathUtils.degToRad(effectController.azimuth)
sun.setFromSphericalCoords(1, phi, theta)
uniforms['sunPosition'].value.copy(sun)
}
// 添加大地
function addGround() {
const groundGeometry = new THREE.PlaneGeometry(100, 100)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x7ec850, // 绿色草地颜色
side: THREE.DoubleSide,
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = Math.PI / 2 // 旋转平面使其水平
scene.add(ground)
}
// 动画循环函数
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
updatePhysic()
// 渲染场景
renderer.render(scene, camera)
}
// 清理函数
function cleanup() {
// 取消动画帧`
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
// 清理渲染器
if (renderer) {
// 移除所有事件监听器
renderer.domElement.removeEventListener('resize', onWindowResize)
// 释放渲染器资源
renderer.dispose()
// 清空渲染器
renderer.forceContextLoss()
// 移除画布
renderer.domElement.remove()
}
// 清理场景
if (scene) {
// 遍历场景中的所有对象
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
// 释放几何体资源
object.geometry.dispose()
// 释放材质资源
if (Array.isArray(object.material)) {
object.material.forEach((material) => material.dispose())
} else {
object.material.dispose()
}
}
})
// 清空场景
scene.clear()
}
// 清理相机
if (camera) {
camera.clear()
}
}
// 窗口大小改变时的处理函数
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
console.log('onWindowResize', width, height)
// 更新相机
camera.aspect = width / height
camera.updateProjectionMatrix()
// 更新渲染器
renderer.setSize(width, height)
// 确保渲染器的像素比与设备匹配
renderer.setPixelRatio(window.devicePixelRatio)
}
// 组件卸载时清理资源
onUnmounted(() => {
cleanup()
// 移除窗口大小改变事件监听
window.removeEventListener('resize', onWindowResize)
})
</script>
<style scoped>
/* 头部样式 */
.header {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
/* 画布容器样式 */
.canvas-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 画布样式 // 保持画布比例*/
.canvas {
width: 100%;
height: 100%;
}
</style>