09 物理与交互
学习 Three.js 中的点击选中、碰撞检测和简单物理
点击选中物体
通过射线(Raycaster)检测鼠标点击。
js
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
// 转换鼠标坐标到 NDC(标准化设备坐标)
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
}
// 点击选中
window.addEventListener('click', () => {
// 设置射线起点和方向
raycaster.setFromCamera(mouse, camera)
// 检测与物体交叉
const intersects = raycaster.intersectObjects(scene.children)
if (intersects.length > 0) {
const selected = intersects[0]
console.log('物体名称:', selected.object.name)
console.log('距离:', selected.distance.toFixed(2))
console.log('交点位置:', selected.point)
// 高亮效果
selected.object.material.emissive.setHex(0xff0000)
// 触发事件
onObjectClicked(selected.object)
}
})
window.addEventListener('mousemove', onMouseMove)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
选中后拖拽
js
let isDragging = false
let selectedObject = null
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
const intersection = new THREE.Vector3()
window.addEventListener('mousedown', () => {
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(selectableObjects)
if (intersects.length > 0) {
selectedObject = intersects[0].object
isDragging = true
controls.enabled = false // 禁用轨道控制器
}
})
window.addEventListener('mousemove', () => {
if (!isDragging || !selectedObject) return
raycaster.setFromCamera(mouse, camera)
// 将物体限制在平面上移动
if (raycaster.ray.intersectPlane(plane, intersection)) {
selectedObject.position.copy(intersection)
}
})
window.addEventListener('mouseup', () => {
isDragging = false
selectedObject = null
controls.enabled = true // 恢复轨道控制器
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
包围盒(Bounding Box)
用于快速粗略检测碰撞:
js
// 创建包围盒
const box = new THREE.Box3().setFromObject(mesh)
// 获取包围盒尺寸
const size = new THREE.Vector3()
box.getSize(size)
console.log('尺寸:', size)
// 获取包围盒中心
const center = new THREE.Vector3()
box.getCenter(center)
console.log('中心:', center)
// 检测两个物体是否碰撞
function checkCollision(meshA, meshB) {
const boxA = new THREE.Box3().setFromObject(meshA)
const boxB = new THREE.Box3().setFromObject(meshB)
return boxA.intersectsBox(boxB)
}
// 检测点是否在物体内
function isInsideMesh(point, mesh) {
const box = new THREE.Box3().setFromObject(mesh)
return box.containsPoint(point)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
包围球(Bounding Sphere)
js
const sphere = new THREE.Sphere()
const box = new THREE.Box3().setFromObject(mesh)
box.getBoundingSphere(sphere)
console.log('球心:', sphere.center)
console.log('半径:', sphere.radius)1
2
3
4
5
6
2
3
4
5
6
简单重力模拟
js
const gravity = -9.8
const objects = []
// 创建地面
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x333333 })
)
ground.rotation.x = -Math.PI / 2
ground.receiveShadow = true
scene.add(ground)
// 物理状态
class PhysicsObject {
constructor(mesh) {
this.mesh = mesh
this.velocity = new THREE.Vector3()
this.acceleration = new THREE.Vector3()
this.mass = 1
this.restitution = 0.6 // 弹性系数
}
applyForce(force) {
this.acceleration.add(force.clone().divideScalar(this.mass))
}
update(delta) {
// 应用重力
this.velocity.y += gravity * delta
// 应用加速度
this.velocity.add(this.acceleration.clone().multiplyScalar(delta))
// 更新位置
this.mesh.position.add(
this.velocity.clone().multiplyScalar(delta)
)
// 地面碰撞
if (this.mesh.position.y < 0) {
this.mesh.position.y = 0
this.velocity.y *= -this.restitution // 反弹
}
// 重置加速度
this.acceleration.set(0, 0, 0)
}
}
// 创建掉落物体
const boxMesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0xff6b6b })
)
boxMesh.castShadow = true
scene.add(boxMesh)
const physicsBox = new PhysicsObject(boxMesh)
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
physicsBox.update(delta)
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
点击涟漪效果
js
import { Refractor } from 'three/addons/objects/Refractor.js'
// 点击创建涟漪动画
function createRipple(point) {
const geometry = new THREE.RingGeometry(0.1, 0.2, 32)
const material = new THREE.MeshBasicMaterial({
color: 0x4ecdc4,
transparent: true,
opacity: 1,
side: THREE.DoubleSide
})
const ripple = new THREE.Mesh(geometry, material)
ripple.position.copy(point)
ripple.rotation.x = -Math.PI / 2
scene.add(ripple)
// 涟漪扩散动画
const startTime = Date.now()
function animateRipple() {
const elapsed = (Date.now() - startTime) / 1000
if (elapsed < 1) {
const scale = 1 + elapsed * 5
ripple.scale.set(scale, scale, scale)
ripple.material.opacity = 1 - elapsed
requestAnimationFrame(animateRipple)
} else {
scene.remove(ripple)
}
}
animateRipple()
}
window.addEventListener('click', (event) => {
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObject(ground)
if (intersects.length > 0) {
createRipple(intersects[0].point)
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
简单弹簧效果
js
class Spring {
constructor(target, stiffness = 0.1, damping = 0.8) {
this.target = target
this.velocity = new THREE.Vector3()
this.stiffness = stiffness
this.damping = damping
}
update(current) {
// 弹力 = -k * 位移
const force = current.clone().sub(this.target).multiplyScalar(-this.stiffness)
// 阻尼力
this.velocity.add(force)
this.velocity.multiplyScalar(this.damping)
// 更新位置
this.target.add(this.velocity)
}
}
// 使用:让相机平滑跟随
const cameraTarget = new THREE.Vector3()
const cameraSpring = new Spring(cameraTarget, 0.05, 0.9)
function animate() {
requestAnimationFrame(animate)
// 每帧设置目标位置
cameraTarget.copy(mesh.position)
cameraTarget.y += 2
// 更新弹簧
cameraSpring.update(camera.position)
// 相机朝向目标
camera.lookAt(mesh.position)
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
摄像机平滑跟随
js
const cameraOffset = new THREE.Vector3(0, 5, 10)
const lookAtOffset = new THREE.Vector3(0, 0, 0)
function updateCamera() {
// 计算期望相机位置
const desiredPosition = mesh.position.clone().add(cameraOffset)
// 平滑插值
camera.position.lerp(desiredPosition, 0.05)
// 平滑朝向
const lookTarget = mesh.position.clone().add(lookAtOffset)
camera.lookAt(lookTarget)
}
function animate() {
requestAnimationFrame(animate)
mesh.rotation.y += 0.01
updateCamera()
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
键盘控制物体移动
js
const keys = {
forward: false,
backward: false,
left: false,
right: false
}
const moveSpeed = 0.1
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW': keys.forward = true; break
case 'KeyS': keys.backward = true; break
case 'KeyA': keys.left = true; break
case 'KeyD': keys.right = true; break
}
})
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW': keys.forward = false; break
case 'KeyS': keys.backward = false; break
case 'KeyA': keys.left = false; break
case 'KeyD': keys.right = false; break
}
})
function animate() {
requestAnimationFrame(animate)
if (keys.forward) mesh.position.z -= moveSpeed
if (keys.backward) mesh.position.z += moveSpeed
if (keys.left) mesh.position.x -= moveSpeed
if (keys.right) mesh.position.x += moveSpeed
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
性能优化
js
// 1. 使用 InstancedMesh 优化大量相同物体
const count = 100
const instancedMesh = new THREE.InstancedMesh(geometry, material, count)
// 2. 离屏物体不渲染
mesh.visible = false
// 3. 使用 frustum culling(默认开启)
mesh.frustumCulled = true
// 4. LOD(多细节层次)
import { LOD } from 'three'
const lod = new LOD()
lod.addLevel(highDetailMesh, 0)
lod.addLevel(mediumDetailMesh, 50)
lod.addLevel(lowDetailMesh, 100)
scene.add(lod)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18