学习Three.js--轨道+火车自动行进
2026年1月27日 14:17
学习Three.js--轨道+火车自动行进
前置核心说明
开发目标
基于Three.js实现带真实纹理的火车轨道 + 沿轨道自动循环行进的简易火车,核心能力包括:
- 轨道模块化构建(钢轨分段创建+枕木间隔排布,支持参数化配置);
- 简易火车模型搭建(车身+4个车轮,车轮精准贴合钢轨);
- 火车沿Z轴自动行进,到达轨道末端后循环重置;
- 自然光照系统+纹理贴图(金属钢轨、木质枕木);
- 基础交互(拖拽旋转视角、滚轮缩放)。
![]()
核心技术栈(关键知识点)
| 技术点 | 作用 |
|---|---|
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>
总结与扩展建议
核心总结
-
模块化设计:通过
Group分组管理轨道/火车的子组件,参数集中化配置,便于维护和扩展; - 几何体精准定位:所有元素的Y轴位置为「高度/2」保证贴地,车轮通过旋转+坐标计算精准贴合钢轨;
-
纹理优化:使用
RepeatWrapping避免纹理拉伸,anisotropy提升斜向清晰度,PBR材质(MeshStandardMaterial)模拟真实质感; -
动画核心:通过
requestAnimationFrame每帧更新火车position.z实现行进,结合边界判断实现循环逻辑。
扩展建议
-
车轮旋转动画:在
animate中添加wheel.rotation.x += 0.1,让车轮随行进旋转,更真实; -
弯曲轨道:修改钢轨分段的
rotation.y和position,实现曲线轨道(需计算圆弧坐标); - 火车细节增强:添加车窗、烟囱、车头等细节,或加载3D模型替代简易几何体;
-
轨道材质优化:启用阴影(
castShadow/receiveShadow),添加钢轨反光、枕木磨损效果; -
交互增强:添加速度控制(滑块调整
TRAIN_PARAMS.speed)、轨道开关、火车启停按钮; -
性能优化:使用
InstancedMesh替代重复创建的钢轨/枕木,减少DrawCall。