在 Three.js 中,实现相机视角的平滑过渡和点击模型切换到查看模型视角是一个常见且有用的功能。这种效果不仅能提升用户体验,还能为场景互动添加更多的动态元素。
1. 基本设置
首先,我们需要创建一个基本的 Three.js 场景,包括相机、渲染器、光源以及一些示例模型。
创建场景和相机
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 10, 10);
scene.add(light);
添加示例模型
// 创建一个简单的几何体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1, 0);
scene.add(cube);
// 创建另一个几何体
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(2, 1, 0);
scene.add(sphere);
2. 引入动画库
为了实现平滑过渡,我们引入 tween.js 动画库。
npm install @tweenjs/tween.js
import * as TWEEN from '@tweenjs/tween.js'
3. 实现相机视角的平滑切换
定义相机切换函数
function smoothCameraTransition(targetPosition, targetLookAt) {
// 保存当前相机的位置和朝向
const startPosition = camera.position.clone();
const startLookAt = new THREE.Vector3();
camera.getWorldDirection(startLookAt);
// 创建 tween 动画
new TWEEN.Tween(startPosition)
.to(targetPosition, 2000) // 动画持续时间为2000毫秒
.easing(TWEEN.Easing.Quadratic.InOut) // 使用缓动函数
.onUpdate(() => {
camera.position.copy(startPosition);
})
.start();
new TWEEN.Tween(startLookAt)
.to(targetLookAt, 2000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => {
camera.lookAt(startLookAt);
})
.start();
}
更新渲染循环
确保在渲染循环中更新 tween 动画。
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
renderer.render(scene, camera);
}
animate();
4. 实现点击模型切换视角
添加射线投射器
我们需要添加射线投射器来检测用户点击的模型。
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(event) {
// 将鼠标点击位置转换为标准化设备坐标
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 更新射线投射器
raycaster.setFromCamera(mouse, camera);
// 计算交互对象
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const intersectedObject = intersects[0].object;
// 切换相机视角到点击的模型
const targetPosition = new THREE.Vector3().copy(intersectedObject.position).add(new THREE.Vector3(0, 2, 5));
const targetLookAt = intersectedObject.position.clone();
smoothCameraTransition(targetPosition, targetLookAt);
}
}
window.addEventListener('click', onMouseClick, false);
5. 完整代码示例
将上述代码片段整合在一起,形成一个完整的示例。
<template>
<div ref="rendererContainer" class="renderer-container"></div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import TWEEN from '@tweenjs/tween.js';
// 引用模板中的 DOM 元素
const rendererContainer = ref(null);
// 初始化场景、相机和渲染器
onMounted(() => {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
rendererContainer.value.appendChild(renderer.domElement);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 10, 10);
scene.add(light);
// 添加示例模型
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1, 0);
scene.add(cube);
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(2, 1, 0);
scene.add(sphere);
// 相机平滑过渡函数
function smoothCameraTransition(targetPosition, targetLookAt) {
const startPosition = camera.position.clone();
const startLookAt = new THREE.Vector3();
camera.getWorldDirection(startLookAt);
new TWEEN.Tween(startPosition)
.to(targetPosition, 2000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => {
camera.position.copy(startPosition);
})
.start();
new TWEEN.Tween(startLookAt)
.to(targetLookAt, 2000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => {
camera.lookAt(startLookAt);
})
.start();
}
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
renderer.render(scene, camera);
}
animate();
// 添加射线投射器
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const intersectedObject = intersects[0].object;
const targetPosition = new THREE.Vector3().copy(intersectedObject.position).add(new THREE.Vector3(0, 2, 5));
const targetLookAt = intersectedObject.position.clone();
smoothCameraTransition(targetPosition, targetLookAt);
}
}
window.addEventListener('click', onMouseClick, false);
});
</script>
<style>
.renderer-container {
width: 100%;
height: 100vh;
}
</style>