Three.js 汽车行驶动画教程

569 阅读5分钟

Three.js 汽车动画教程

简介

本教程将指导你使用 Three.js 创建一个简单的汽车在公路上行驶的动画。通过这个示例,你将学习到如何创建场景、相机、渲染器,以及如何创建和动画化 3D 对象。

环境准备

在开始之前,你需要一个文本编辑器和一个浏览器。可以使用任何文本编辑器,如 Visual Studio Code、Sublime Text 等。

代码实现步骤

1. 创建 HTML 基础结构

首先,创建一个基本的 HTML 文件,设置文档类型、语言和页面标题。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js Car Animation</title>
    <style>
        body {
            margin: 0;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
</body>
</html>

在 标签中,我们将 body 的外边距设置为 0,并将 canvas 元素设置为块级元素,以确保 Three.js 渲染的画布占据整个屏幕。

2. 引入 Three.js 库

使用 CDN 引入 Three.js 库。在 标签内添加以下脚本标签:

<script src="https://cdn.jsdelivr.net/npm/three/build/three.min.js"></script>
3. 创建场景、相机和渲染器

在 <script> 标签中,我们将创建场景、相机和渲染器。

// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(
    75, // 视野角度
    window.innerWidth / window.innerHeight, // 宽高比
    0.1, // 近裁剪面
    1000 // 远裁剪面
);
camera.position.set(0, 1.5, 10); // 将相机放置在汽车后面,稍微高一点
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
  • 场景(Scene) :是 Three.js 中所有对象的容器。
  • 相机(Camera) :定义了观察者的视角。这里我们使用 PerspectiveCamera 创建一个透视相机。
  • 渲染器(Renderer) :负责将场景和相机的内容渲染到屏幕上。我们使用 WebGLRenderer 进行渲染,并将其添加到 body 元素中。
4. 创建公路

我们将创建多个公路段,每个公路段的宽度逐渐变窄,以模拟透视效果。

const numRoadSegments = 5; // 公路段数量
const roadLength = 20; // 每段公路长度
const maxRoadWidth = 10; // 公路最大宽度
const minRoadWidth = 2; // 公路最小宽度
const roadSegments = [];
// 创建多个公路段
for (let i = 0; i < numRoadSegments; i++) {
    const width = maxRoadWidth - (maxRoadWidth - minRoadWidth) * (i / numRoadSegments);
    const roadGeometry = new THREE.PlaneGeometry(width, roadLength);
    const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x666666 });
    const road = new THREE.Mesh(roadGeometry, roadMaterial);
    road.rotation.x = -Math.PI / 2;
    road.position.z = i * roadLength;
    scene.add(road);
    roadSegments.push(road);
}
  • numRoadSegments 定义了公路段的数量。
  • roadLength 定义了每段公路的长度。
  • maxRoadWidth 和 minRoadWidth 分别定义了公路的最大和最小宽度。
  • 在循环中,我们根据当前段的索引计算公路的宽度,并创建一个平面几何体和材质,然后将它们组合成一个网格对象添加到场景中。
5. 创建汽车

创建一个简单的汽车几何体,并将其添加到场景中。

// 创建汽车几何体
const carGeometry = new THREE.BoxGeometry(1, 0.5, 2);
// 创建汽车材质
const carMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// 创建汽车网格
const car = new THREE.Mesh(carGeometry, carMaterial);
car.position.z = 0; // 将汽车放置在公路的起始位置
car.position.y = 0.25; // 将汽车稍微抬高一点
scene.add(car);
  • carGeometry 是一个立方体几何体,代表汽车的形状。
  • carMaterial 是汽车的材质,这里我们使用红色。
  • car 是一个网格对象,将几何体和材质组合在一起,并添加到场景中。
6. 动画循环

我们将创建一个动画循环,在每一帧中更新汽车的位置、相机的位置和公路的位置。

let carPosition = 0; // 汽车的初始位置
const carSpeed = 0.1; // 汽车的移动速度
// 检查公路是否即将移出视野,提前进行重置
function checkAndResetRoad(road, index) {
    if (road.position.z + roadLength < camera.position.z) {
        const lastRoad = roadSegments[(index - 1 + numRoadSegments) % numRoadSegments];
        road.position.z = lastRoad.position.z + roadLength;
        const width = maxRoadWidth - (maxRoadWidth - minRoadWidth) * (index / numRoadSegments);
        road.geometry = new THREE.PlaneGeometry(width, roadLength);
    }
}
function animate() {
    requestAnimationFrame(animate);
    // 更新汽车位置
    carPosition += carSpeed;
    car.position.z = carPosition;
    // 更新相机位置和方向
    camera.position.z = carPosition - 5; // 相机始终在汽车后面 5 个单位
    camera.lookAt(car.position); // 相机始终看向汽车
    // 更新公路位置
    roadSegments.forEach(road => {
        road.position.z -= carSpeed;
    });
    // 检查并重置公路位置
    roadSegments.forEach((road, index) => {
        checkAndResetRoad(road, index);
    });
    // 渲染场景
    renderer.render(scene, camera);
}
animate();
  • carPosition 记录汽车的当前位置,carSpeed 定义了汽车的移动速度。
  • checkAndResetRoad 函数用于检查公路是否即将移出视野,如果是,则将其重置到公路的末尾。
  • animate 函数是动画循环的核心,它使用 requestAnimationFrame 递归调用自身,在每一帧中更新汽车、相机和公路的位置,并调用 renderer.render 方法渲染场景。

完整代码

   
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js Car Animation</title>
    <style>
        body {
            margin: 0;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <!-- 使用替代 CDN -->
    <script src="https://cdn.jsdelivr.net/npm/three/build/three.min.js"></script>
    <script>
        // 创建场景
        const scene = new THREE.Scene();
        // 创建相机
        const camera = new THREE.PerspectiveCamera(
            75, // 视野角度
            window.innerWidth / window.innerHeight, // 宽高比
            0.1, // 近裁剪面
            1000 // 远裁剪面
        );
        camera.position.set(0, 1.5, 10); // 将相机放置在汽车后面,稍微高一点
        // 创建渲染器
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const numRoadSegments = 5; // 公路段数量
        const roadLength = 20; // 每段公路长度
        const maxRoadWidth = 10; // 公路最大宽度
        const minRoadWidth = 2; // 公路最小宽度
        const roadSegments = [];
        // 创建多个公路段
        for (let i = 0; i < numRoadSegments; i++) {
            const width = maxRoadWidth - (maxRoadWidth - minRoadWidth) * (i / numRoadSegments);
            const roadGeometry = new THREE.PlaneGeometry(width, roadLength);
            const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x666666 });
            const road = new THREE.Mesh(roadGeometry, roadMaterial);
            road.rotation.x = -Math.PI / 2;
            road.position.z = i * roadLength;
            scene.add(road);
            roadSegments.push(road);
        }
        // 创建汽车几何体
        const carGeometry = new THREE.BoxGeometry(1, 0.5, 2);
        // 创建汽车材质
        const carMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        // 创建汽车网格
        const car = new THREE.Mesh(carGeometry, carMaterial);
        car.position.z = 0; // 将汽车放置在公路的起始位置
        car.position.y = 0.25; // 将汽车稍微抬高一点
        scene.add(car);
        let carPosition = 0; // 汽车的初始位置
        const carSpeed = 0.1; // 汽车的移动速度
        // 检查公路是否即将移出视野,提前进行重置
        function checkAndResetRoad(road, index) {
            if (road.position.z + roadLength < camera.position.z) {
                const lastRoad = roadSegments[(index - 1 + numRoadSegments) % numRoadSegments];
                road.position.z = lastRoad.position.z + roadLength;
                const width = maxRoadWidth - (maxRoadWidth - minRoadWidth) * (index / numRoadSegments);
                road.geometry = new THREE.PlaneGeometry(width, roadLength);
            }
        }
        function animate() {
            requestAnimationFrame(animate);
            // 更新汽车位置
            carPosition += carSpeed;
            car.position.z = carPosition;
            // 更新相机位置和方向
            camera.position.z = carPosition - 5; // 相机始终在汽车后面 5 个单位
            camera.lookAt(car.position); // 相机始终看向汽车
            // 更新公路位置
            roadSegments.forEach(road => {
                road.position.z -= carSpeed;
            });
            // 检查并重置公路位置
            roadSegments.forEach((road, index) => {
                checkAndResetRoad(road, index);
            });
            // 渲染场景
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

运行代码

将上述代码保存为一个 HTML 文件,例如 car_animation.html,然后在浏览器中打开该文件,你将看到一个汽车在公路上行驶的动画。

扩展和优化

  • 可以尝试使用更复杂的几何体和材质来创建更逼真的汽车和公路。
  • 添加光照效果,使场景更加真实。
  • 实现用户交互,例如通过键盘控制汽车的速度和方向。