阅读视图

发现新文章,点击刷新页面。

学习Three.js--轨道+火车自动行进

学习Three.js--轨道+火车自动行进

前置核心说明

开发目标

基于Three.js实现带真实纹理的火车轨道 + 沿轨道自动循环行进的简易火车,核心能力包括:

  1. 轨道模块化构建(钢轨分段创建+枕木间隔排布,支持参数化配置);
  2. 简易火车模型搭建(车身+4个车轮,车轮精准贴合钢轨);
  3. 火车沿Z轴自动行进,到达轨道末端后循环重置;
  4. 自然光照系统+纹理贴图(金属钢轨、木质枕木);
  5. 基础交互(拖拽旋转视角、滚轮缩放)。

在这里插入图片描述

核心技术栈(关键知识点)

技术点 作用
THREE.Group 分组管理轨道/火车的子组件(钢轨/枕木/车身/车轮),方便整体控制
BoxGeometry/CylinderGeometry 分别创建轨道(立方体)、车轮(圆柱体)的基础几何形状
MeshStandardMaterial PBR物理材质,支持纹理、金属度、粗糙度,模拟真实钢轨/车轮质感
纹理重复配置(wrapS/wrapT/repeat 避免纹理拉伸,适配轨道分段尺寸,提升视觉真实度
动画循环(requestAnimationFrame 实时更新火车位置,实现自动行进+循环逻辑
OrbitControls 基础视角交互(旋转/缩放/阻尼)
几何体旋转/定位 车轮旋转适配钢轨方向、轨道/火车精准贴地/居中

核心开发流程

A[初始化场景/相机/渲染器/控制器] --> B[配置轨道+火车参数(可复用)]
B --> C[加载纹理并配置重复规则]
C --> D[创建轨道(钢轨分段+枕木间隔)]
D --> E[创建火车模型(车身+车轮精准定位)]
E --> F[创建地面+添加所有元素到场景]
F --> G[窗口适配+动画循环(火车行进+循环)]

分步开发详解

步骤1:基础环境搭建(场景/相机/渲染器/控制器/光照)

1.1 核心代码
// 1. 场景初始化(所有元素的容器)
const scene = new THREE.Scene();

// 2. 透视相机(模拟人眼视角,适合3D场景)
const camera = new THREE.PerspectiveCamera(
  60, // 视角(FOV)
  window.innerWidth / window.innerHeight, // 宽高比
  1, // 近裁切面
  3000 // 远裁切面
);
camera.position.set(5, 3, 10); // 调整视角,俯视轨道

// 3. 渲染器(抗锯齿+高清适配)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

// 4. 轨道控制器(交互:旋转/缩放/阻尼)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 阻尼(惯性),交互更顺滑
controls.dampingFactor = 0.05; // 阻尼系数

// 5. 光照系统(模拟自然光照)
// 环境光:补充暗部,避免纯黑
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// 方向光:模拟太阳光,提升立体感
const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
dirLight.position.set(200, 400, 300); // 高位斜向光照
dirLight.castShadow = false; // 简化场景,禁用阴影
scene.add(dirLight);
1.2 关键说明
  • 相机视角position.set(5, 3, 10) 采用俯视视角,既能看到轨道延伸,又能清晰观察火车行进;
  • 光照组合AmbientLight + DirectionalLight 是Three.js基础光照方案,兼顾暗部细节和主体立体感;
  • 控制器阻尼:启用阻尼后,视角拖拽/缩放有惯性,交互体验更自然。

步骤2:核心参数配置

2.1 核心代码
// 轨道参数配置(集中管理,方便修改)
const TRACK_PARAMS = {
  railWidth: 0.2,       // 钢轨宽度
  railHeight: 0.1,      // 钢轨高度
  railSpacing: 1.0,     // 两条钢轨中心距
  sleeperWidth: 0.1,    // 枕木宽度(沿Z轴)
  sleeperHeight: 0.05,  // 枕木高度
  sleeperLength: 1.5,   // 枕木长度(超过钢轨间距,覆盖两侧)
  sleeperGap: 1.0,      // 枕木间距
  trackLength: 50,      // 轨道总长度
  segmentLength: 1.0    // 钢轨分段长度(避免单个几何体过长)
};

// 火车参数配置(集中管理)
const TRAIN_PARAMS = {
  speed: 0.01,          // 火车行进速度(每帧移动距离)
  wheelRadius: 0.1,     // 车轮半径(刚好贴合钢轨高度)
  wheelWidth: 0.1,      // 车轮宽度
  bodyLength: 2.0,      // 车身长度
  bodyWidth: 0.8,       // 车身宽度(小于钢轨间距,居中)
  bodyHeight: 0.5       // 车身高度
};
2.2 关键设计思路
  • 参数集中化:将轨道/火车的尺寸、间距、速度等参数集中定义,后续修改无需遍历代码,符合「高内聚低耦合」原则;
  • 尺寸匹配wheelRadius = railHeight 保证车轮刚好贴合钢轨上表面,bodyWidth < railSpacing 保证车身居中在两条钢轨之间。

步骤3:纹理加载与配置(避免拉伸,提升真实度)

3.1 核心代码
const texLoader = new THREE.TextureLoader();

// 钢轨纹理(生锈金属,适配轨道质感)
const railTexture = texLoader.load('./rusty_metal_05_diff_1k.jpg', () => {
  renderer.render(scene, camera); // 纹理加载完成后重绘
});
// 纹理关键配置:避免拉伸
railTexture.colorSpace = THREE.SRGBColorSpace; // 正确的颜色空间
railTexture.wrapS = THREE.RepeatWrapping;      // 水平(X轴)重复
railTexture.wrapT = THREE.RepeatWrapping;      // 垂直(Y轴)重复
railTexture.repeat.set(2, 10);                 // 重复次数(适配钢轨尺寸)
railTexture.anisotropy = renderer.capabilities.getMaxAnisotropy(); // 提升斜向清晰度

// 枕木纹理(木质纹理)
const sleeperTexture = texLoader.load('./bark_willow_02_diff_1k.jpg');
sleeperTexture.colorSpace = THREE.SRGBColorSpace;
sleeperTexture.wrapS = THREE.RepeatWrapping;
sleeperTexture.wrapT = THREE.RepeatWrapping;
sleeperTexture.repeat.set(1, 1); // 单个枕木单次纹理
3.2 核心技术点
  • 纹理重复(RepeatWrapping):默认纹理是ClampToEdgeWrapping(拉伸),设置为RepeatWrapping后,纹理会按repeat值重复排列,避免长钢轨纹理拉伸模糊;
  • 各向异性(anisotropy):提升纹理在斜向视角下的清晰度,尤其适合轨道这种长条状物体;
  • 颜色空间SRGBColorSpace 是纹理的标准颜色空间,保证纹理颜色显示正确。

步骤4:轨道创建(核心逻辑:分段钢轨+间隔枕木)

4.1 核心代码
function createTrainTrack() {
  const trackGroup = new THREE.Group(); // 轨道组,统一管理钢轨/枕木

  // 1. 创建钢轨(两条平行,分段构建)
  const railMaterial = new THREE.MeshStandardMaterial({ 
    map: railTexture,
    metalness: 0.8, // 高金属度,模拟钢轨质感
    roughness: 0.2  // 低粗糙度,有反光
  });

  // 左侧钢轨(分段创建)
  for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.segmentLength) {
    const railGeometry = new THREE.BoxGeometry(
      TRACK_PARAMS.railWidth,
      TRACK_PARAMS.railHeight,
      TRACK_PARAMS.segmentLength
    );
    const rail = new THREE.Mesh(railGeometry, railMaterial);
    // 定位:左侧钢轨(X负方向),底部贴地
    rail.position.set(
      -TRACK_PARAMS.railSpacing / 2, // 左侧钢轨X坐标
      TRACK_PARAMS.railHeight / 2,   // Y轴:底部贴地(高度/2)
      z + TRACK_PARAMS.segmentLength / 2 // Z轴:分段居中
    );
    trackGroup.add(rail);
  }

  // 右侧钢轨(分段创建,逻辑同左侧)
  for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.segmentLength) {
    const railGeometry = new THREE.BoxGeometry(
      TRACK_PARAMS.railWidth,
      TRACK_PARAMS.railHeight,
      TRACK_PARAMS.segmentLength
    );
    const rail = new THREE.Mesh(railGeometry, railMaterial);
    rail.position.set(
      TRACK_PARAMS.railSpacing / 2, // 右侧钢轨X坐标
      TRACK_PARAMS.railHeight / 2,
      z + TRACK_PARAMS.segmentLength / 2
    );
    trackGroup.add(rail);
  }

  // 2. 创建枕木(横向间隔排布)
  const sleeperMaterial = new THREE.MeshStandardMaterial({ map: sleeperTexture });
  for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.sleeperGap) {
    const sleeperGeometry = new THREE.BoxGeometry(
      TRACK_PARAMS.sleeperLength, // 长度(X轴)覆盖两条钢轨
      TRACK_PARAMS.sleeperHeight,
      TRACK_PARAMS.sleeperWidth
    );
    const sleeper = new THREE.Mesh(sleeperGeometry, sleeperMaterial);
    // 定位:居中,底部贴地,沿Z轴间隔排布
    sleeper.position.set(
      0, // X轴居中
      TRACK_PARAMS.sleeperHeight / 2, // Y轴贴地
      z // Z轴间隔排布
    );
    trackGroup.add(sleeper);
  }

  return trackGroup;
}
4.2 核心技术点解析
  • 钢轨分段创建:若直接创建长度为50的钢轨几何体,顶点数过多且纹理拉伸严重;分段(segmentLength=1.0)后,每个钢轨段尺寸小、纹理重复合理,性能更优;
  • Group分组管理:将所有钢轨、枕木添加到trackGroup,后续可通过操作trackGroup整体移动/旋转轨道,便于扩展;
  • 定位逻辑:所有几何体的Y轴位置为「高度/2」,保证底部贴地(Y=0),避免悬浮/埋地。

步骤5:火车模型创建(车身+车轮,精准贴合轨道)

5.1 核心代码
function createTrain() {
  const trainGroup = new THREE.Group(); // 火车组,统一管理车身/车轮

  // 1. 创建车身(立方体)
  const bodyMaterial = new THREE.MeshStandardMaterial({ 
    color: 0xff4444, // 红色车身
    metalness: 0.1,
    roughness: 0.8
  });
  const bodyGeometry = new THREE.BoxGeometry(
    TRAIN_PARAMS.bodyWidth,
    TRAIN_PARAMS.bodyHeight,
    TRAIN_PARAMS.bodyLength
  );
  const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
  // 车身定位:居中在钢轨之间,Y轴=车轮半径+车身高度/2
  body.position.set(
    0, // X轴居中
    TRAIN_PARAMS.wheelRadius + TRAIN_PARAMS.bodyHeight / 2, // Y轴高度
    0 // Z轴初始位置
  );
  trainGroup.add(body);

  // 2. 创建车轮(圆柱体,共4个:前左/前右/后左/后右)
  const wheelMaterial = new THREE.MeshStandardMaterial({ 
    color: 0x333333, // 黑色车轮
    metalness: 0.8,
    roughness: 0.2
  });
  const wheelGeometry = new THREE.CylinderGeometry(
    TRAIN_PARAMS.wheelRadius, // 顶部半径
    TRAIN_PARAMS.wheelRadius, // 底部半径
    TRAIN_PARAMS.wheelWidth,  // 圆柱体高度(车轮宽度)
    16 // 分段数,越多越圆
  );

  // 前左车轮
  const frontLeftWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
  frontLeftWheel.rotation.z = Math.PI / 2; // 旋转90°,横向贴合钢轨
  frontLeftWheel.position.set(
    -TRACK_PARAMS.railSpacing / 2 + TRACK_PARAMS.railWidth / 2, // 对齐左侧钢轨
    TRAIN_PARAMS.wheelRadius, // Y轴=车轮半径(贴钢轨)
    TRAIN_PARAMS.bodyLength / 2 - 0.2 // 车身前部位置
  );
  trainGroup.add(frontLeftWheel);

  // 前右车轮(逻辑同前左,X轴反向)
  const frontRightWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
  frontRightWheel.rotation.z = Math.PI / 2;
  frontRightWheel.position.set(
    TRACK_PARAMS.railSpacing / 2 - TRACK_PARAMS.railWidth / 2,
    TRAIN_PARAMS.wheelRadius,
    TRAIN_PARAMS.bodyLength / 2 - 0.2
  );
  trainGroup.add(frontRightWheel);

  // 后左车轮(Z轴反向)
  const backLeftWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
  backLeftWheel.rotation.z = Math.PI / 2;
  backLeftWheel.position.set(
    -TRACK_PARAMS.railSpacing / 2 + TRACK_PARAMS.railWidth / 2,
    TRAIN_PARAMS.wheelRadius,
    -TRAIN_PARAMS.bodyLength / 2 + 0.2
  );
  trainGroup.add(backLeftWheel);

  // 后右车轮(X/Z轴反向)
  const backRightWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
  backRightWheel.rotation.z = Math.PI / 2;
  backRightWheel.position.set(
    TRACK_PARAMS.railSpacing / 2 - TRACK_PARAMS.railWidth / 2,
    TRAIN_PARAMS.wheelRadius,
    -TRAIN_PARAMS.bodyLength / 2 + 0.2
  );
  trainGroup.add(backRightWheel);

  // 初始化火车位置(轨道起点)
  trainGroup.position.z = 0;

  return trainGroup;
}
5.2 核心技术点解析
  • 车轮旋转CylinderGeometry 默认是垂直方向(Y轴),通过rotation.z = Math.PI / 2 旋转为水平方向(X轴),贴合钢轨延伸方向;
  • 车轮定位-TRACK_PARAMS.railSpacing / 2 + TRACK_PARAMS.railWidth / 2 精准对齐钢轨内侧,避免车轮偏移;
  • 车身高度wheelRadius + bodyHeight / 2 保证车身底部与车轮顶部贴合,模拟真实火车结构。

步骤6:地面创建+元素组装

6.1 核心代码
// 创建地面(大平面,衬托轨道)
const groundGeometry = new THREE.PlaneGeometry(100, 100); // 足够大的地面
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // 旋转90°,从垂直变为水平(贴地)
ground.position.y = 0;
scene.add(ground);

// 创建轨道和火车,添加到场景
const track = createTrainTrack();
const train = createTrain();
scene.add(track);
scene.add(train);
6.2 关键说明
  • 地面旋转PlaneGeometry 默认是XY平面(垂直),rotation.x = -Math.PI / 2 旋转为XZ平面(水平),作为场景地面;
  • 地面尺寸100x100 远大于轨道长度(50),保证轨道完全覆盖在地面上。

步骤7:窗口适配+动画循环(火车行进核心)

7.1 核心代码
// 窗口适配(响应式)
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix(); // 必须更新投影矩阵
  renderer.setSize(window.innerWidth, window.innerHeight);
});

// 渲染循环(火车行进+循环逻辑)
function animate() {
  requestAnimationFrame(animate);
  
  // 核心:火车沿Z轴行进
  train.position.z += TRAIN_PARAMS.speed;
  
  // 循环逻辑:到达轨道末端后重置到起点
  if (train.position.z > TRACK_PARAMS.trackLength) {
    train.position.z = -TRAIN_PARAMS.bodyLength; // 起点略超前,避免突兀
  }

  controls.update(); // 更新控制器(阻尼)
  renderer.render(scene, camera); // 渲染场景
}
animate();
7.2 核心技术点解析
  • 火车行进逻辑:通过每帧修改train.position.z 实现沿轨道(Z轴)行进,speed 控制行进速度;
  • 循环重置train.position.z > TRACK_PARAMS.trackLength 时,重置为-TRAIN_PARAMS.bodyLength(车身长度负值),保证火车从轨道起点外进入,循环更自然;
  • 窗口适配:相机宽高比修改后,必须调用updateProjectionMatrix() 使修改生效。

核心技术点深度解析

1. 钢轨分段的必要性

  • 性能层面:单个长度为50的钢轨几何体,顶点数=(宽度分段×高度分段×长度分段),远多于50个长度为1的分段几何体总和;
  • 纹理层面:分段后每个钢轨段的纹理可通过repeat 精准控制,避免长钢轨纹理拉伸模糊;
  • 扩展层面:分段轨道更容易实现弯曲轨道(后续可通过修改每个分段的旋转/位置实现曲线)。

2. 车轮与钢轨的精准贴合

参数匹配 效果
wheelRadius = railHeight 车轮半径等于钢轨高度,车轮底部刚好落在钢轨上表面
车轮X坐标 = ±railSpacing/2 ± railWidth/2 车轮内侧对齐钢轨外侧,避免偏移
车轮Y坐标 = wheelRadius 车轮中心Y轴高度=半径,底部贴钢轨上表面

3. 动画循环的核心逻辑

A[每帧执行animate] --> B[train.position.z += speed]
B --> C{z > trackLength?}
C -- 是 --> D[z = -bodyLength(重置)]
C -- 否 --> E[继续行进]
D --> F[渲染场景]
E --> F[渲染场景]

完整优化代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Three.js 轨道+火车自动行进</title>
  <style>
    body { margin: 0; overflow: hidden; }
    #info {
      position: absolute;
      top: 10px;
      width: 100%;
      text-align: center;
      color: white;
      font-family: Arial, sans-serif;
      text-shadow: 0 0 5px rgba(0,0,0,0.8);
      pointer-events: none;
      z-index: 10;
    }
  </style>
</head>
<body>
  <div id="info">火车沿轨道自动行进 | 拖拽旋转视角 | 滚轮缩放</div>

<script type="module">
  import * as THREE from 'https://esm.sh/three@0.174.0';
  import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';

  // ========== 1. 基础环境初始化 ==========
  // 场景:所有元素容器
  const scene = new THREE.Scene();

  // 透视相机:俯视视角观察轨道+火车
  const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000);
  camera.position.set(5, 3, 10); // 俯视视角参数

  // 渲染器:抗锯齿+高清适配
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  document.body.appendChild(renderer.domElement);

  // 轨道控制器:启用阻尼,交互更顺滑
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;

  // 光照系统:环境光+方向光,兼顾暗部和立体感
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambientLight);
  const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
  dirLight.position.set(200, 400, 300);
  dirLight.castShadow = false; // 简化场景,禁用阴影
  scene.add(dirLight);

  // ========== 2. 核心参数配置(集中管理,方便修改) ==========
  // 轨道参数
  const TRACK_PARAMS = {
    railWidth: 0.2,       // 钢轨宽度
    railHeight: 0.1,      // 钢轨高度
    railSpacing: 1.0,     // 两条钢轨中心距
    sleeperWidth: 0.1,    // 枕木宽度(沿Z轴)
    sleeperHeight: 0.05,  // 枕木高度
    sleeperLength: 1.5,   // 枕木长度(覆盖钢轨两侧)
    sleeperGap: 1.0,      // 枕木间距
    trackLength: 50,      // 轨道总长度
    segmentLength: 1.0    // 钢轨分段长度(避免单个几何体过长)
  };

  // 火车参数
  const TRAIN_PARAMS = {
    speed: 0.01,          // 火车行进速度(每帧移动距离)
    wheelRadius: 0.1,     // 车轮半径(贴合钢轨高度)
    wheelWidth: 0.1,      // 车轮宽度
    bodyLength: 2.0,      // 车身长度
    bodyWidth: 0.8,       // 车身宽度(小于钢轨间距,居中)
    bodyHeight: 0.5       // 车身高度
  };

  // ========== 3. 纹理加载与配置(避免拉伸,提升真实度) ==========
  const texLoader = new THREE.TextureLoader();

  // 钢轨纹理:生锈金属,配置重复规则
  const railTexture = texLoader.load('./rusty_metal_05_diff_1k.jpg', () => {
    renderer.render(scene, camera); // 纹理加载完成后重绘
  });
  railTexture.colorSpace = THREE.SRGBColorSpace;
  railTexture.wrapS = THREE.RepeatWrapping;
  railTexture.wrapT = THREE.RepeatWrapping;
  railTexture.repeat.set(2, 10); // 纹理重复次数
  railTexture.anisotropy = renderer.capabilities.getMaxAnisotropy(); // 提升斜向清晰度

  // 枕木纹理:木质纹理
  const sleeperTexture = texLoader.load('./bark_willow_02_diff_1k.jpg');
  sleeperTexture.colorSpace = THREE.SRGBColorSpace;
  sleeperTexture.wrapS = THREE.RepeatWrapping;
  sleeperTexture.wrapT = THREE.RepeatWrapping;
  sleeperTexture.repeat.set(1, 1);

  // ========== 4. 轨道创建(分段钢轨+间隔枕木) ==========
  function createTrainTrack() {
    const trackGroup = new THREE.Group(); // 轨道组,统一管理

    // 钢轨材质:高金属度,模拟钢轨质感
    const railMaterial = new THREE.MeshStandardMaterial({ 
      map: railTexture,
      metalness: 0.8,
      roughness: 0.2
    });

    // 左侧钢轨:分段创建
    for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.segmentLength) {
      const railGeometry = new THREE.BoxGeometry(
        TRACK_PARAMS.railWidth,
        TRACK_PARAMS.railHeight,
        TRACK_PARAMS.segmentLength
      );
      const rail = new THREE.Mesh(railGeometry, railMaterial);
      // 定位:左侧+贴地+分段居中
      rail.position.set(
        -TRACK_PARAMS.railSpacing / 2,
        TRACK_PARAMS.railHeight / 2,
        z + TRACK_PARAMS.segmentLength / 2
      );
      trackGroup.add(rail);
    }

    // 右侧钢轨:分段创建
    for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.segmentLength) {
      const railGeometry = new THREE.BoxGeometry(
        TRACK_PARAMS.railWidth,
        TRACK_PARAMS.railHeight,
        TRACK_PARAMS.segmentLength
      );
      const rail = new THREE.Mesh(railGeometry, railMaterial);
      // 定位:右侧+贴地+分段居中
      rail.position.set(
        TRACK_PARAMS.railSpacing / 2,
        TRACK_PARAMS.railHeight / 2,
        z + TRACK_PARAMS.segmentLength / 2
      );
      trackGroup.add(rail);
    }

    // 枕木材质:木质纹理
    const sleeperMaterial = new THREE.MeshStandardMaterial({ map: sleeperTexture });
    // 枕木:间隔创建
    for (let z = 0; z < TRACK_PARAMS.trackLength; z += TRACK_PARAMS.sleeperGap) {
      const sleeperGeometry = new THREE.BoxGeometry(
        TRACK_PARAMS.sleeperLength,
        TRACK_PARAMS.sleeperHeight,
        TRACK_PARAMS.sleeperWidth
      );
      const sleeper = new THREE.Mesh(sleeperGeometry, sleeperMaterial);
      // 定位:居中+贴地+间隔排布
      sleeper.position.set(
        0,
        TRACK_PARAMS.sleeperHeight / 2,
        z
      );
      trackGroup.add(sleeper);
    }

    return trackGroup;
  }

  // ========== 5. 火车创建(车身+车轮,精准贴合轨道) ==========
  function createTrain() {
    const trainGroup = new THREE.Group(); // 火车组,统一管理

    // 车身材质:红色,低金属度
    const bodyMaterial = new THREE.MeshStandardMaterial({ 
      color: 0xff4444,
      metalness: 0.1,
      roughness: 0.8
    });
    // 车身几何体
    const bodyGeometry = new THREE.BoxGeometry(
      TRAIN_PARAMS.bodyWidth,
      TRAIN_PARAMS.bodyHeight,
      TRAIN_PARAMS.bodyLength
    );
    const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
    // 车身定位:居中+贴合车轮顶部
    body.position.set(
      0,
      TRAIN_PARAMS.wheelRadius + TRAIN_PARAMS.bodyHeight / 2,
      0
    );
    trainGroup.add(body);

    // 车轮材质:黑色,高金属度
    const wheelMaterial = new THREE.MeshStandardMaterial({ 
      color: 0x333333,
      metalness: 0.8,
      roughness: 0.2
    });
    // 车轮几何体:圆柱体
    const wheelGeometry = new THREE.CylinderGeometry(
      TRAIN_PARAMS.wheelRadius,
      TRAIN_PARAMS.wheelRadius,
      TRAIN_PARAMS.wheelWidth,
      16
    );

    // 前左车轮
    const frontLeftWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
    frontLeftWheel.rotation.z = Math.PI / 2; // 旋转为横向
    frontLeftWheel.position.set(
      -TRACK_PARAMS.railSpacing / 2 + TRACK_PARAMS.railWidth / 2,
      TRAIN_PARAMS.wheelRadius,
      TRAIN_PARAMS.bodyLength / 2 - 0.2
    );
    trainGroup.add(frontLeftWheel);

    // 前右车轮
    const frontRightWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
    frontRightWheel.rotation.z = Math.PI / 2;
    frontRightWheel.position.set(
      TRACK_PARAMS.railSpacing / 2 - TRACK_PARAMS.railWidth / 2,
      TRAIN_PARAMS.wheelRadius,
      TRAIN_PARAMS.bodyLength / 2 - 0.2
    );
    trainGroup.add(frontRightWheel);

    // 后左车轮
    const backLeftWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
    backLeftWheel.rotation.z = Math.PI / 2;
    backLeftWheel.position.set(
      -TRACK_PARAMS.railSpacing / 2 + TRACK_PARAMS.railWidth / 2,
      TRAIN_PARAMS.wheelRadius,
      -TRAIN_PARAMS.bodyLength / 2 + 0.2
    );
    trainGroup.add(backLeftWheel);

    // 后右车轮
    const backRightWheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
    backRightWheel.rotation.z = Math.PI / 2;
    backRightWheel.position.set(
      TRACK_PARAMS.railSpacing / 2 - TRACK_PARAMS.railWidth / 2,
      TRAIN_PARAMS.wheelRadius,
      -TRAIN_PARAMS.bodyLength / 2 + 0.2
    );
    trainGroup.add(backRightWheel);

    // 初始化火车位置(轨道起点)
    trainGroup.position.z = 0;

    return trainGroup;
  }

  // ========== 6. 地面创建+元素组装 ==========
  // 地面:大平面,衬托轨道
  const groundGeometry = new THREE.PlaneGeometry(100, 100);
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
  ground.rotation.x = -Math.PI / 2; // 旋转为水平地面
  ground.position.y = 0;
  scene.add(ground);

  // 创建轨道和火车,添加到场景
  const track = createTrainTrack();
  const train = createTrain();
  scene.add(track);
  scene.add(train);

  // ========== 7. 窗口适配 ==========
  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

  // ========== 8. 动画循环(火车行进+循环) ==========
  function animate() {
    requestAnimationFrame(animate);
    
    // 火车沿Z轴行进
    train.position.z += TRAIN_PARAMS.speed;
    
    // 循环逻辑:到达末端后重置到起点
    if (train.position.z > TRACK_PARAMS.trackLength) {
      train.position.z = -TRAIN_PARAMS.bodyLength; // 起点略超前,避免突兀
    }

    controls.update(); // 更新控制器阻尼
    renderer.render(scene, camera); // 渲染场景
  }
  animate();
</script>
</body>
</html>

总结与扩展建议

核心总结

  1. 模块化设计:通过Group分组管理轨道/火车的子组件,参数集中化配置,便于维护和扩展;
  2. 几何体精准定位:所有元素的Y轴位置为「高度/2」保证贴地,车轮通过旋转+坐标计算精准贴合钢轨;
  3. 纹理优化:使用RepeatWrapping避免纹理拉伸,anisotropy提升斜向清晰度,PBR材质(MeshStandardMaterial)模拟真实质感;
  4. 动画核心:通过requestAnimationFrame每帧更新火车position.z实现行进,结合边界判断实现循环逻辑。

扩展建议

  1. 车轮旋转动画:在animate中添加wheel.rotation.x += 0.1,让车轮随行进旋转,更真实;
  2. 弯曲轨道:修改钢轨分段的rotation.yposition,实现曲线轨道(需计算圆弧坐标);
  3. 火车细节增强:添加车窗、烟囱、车头等细节,或加载3D模型替代简易几何体;
  4. 轨道材质优化:启用阴影(castShadow/receiveShadow),添加钢轨反光、枕木磨损效果;
  5. 交互增强:添加速度控制(滑块调整TRAIN_PARAMS.speed)、轨道开关、火车启停按钮;
  6. 性能优化:使用InstancedMesh替代重复创建的钢轨/枕木,减少DrawCall。
❌