👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
一、🍀前言
本文详细介绍如何基于threejs在三维场景中构建三维汽车模拟器,场景有树、云、山等,亲测可用。希望能帮助到您。一起学习,加油!加油!
1.1 ☘️THREE.BoxGeometry
THREE.BoxGeometry是四边形的原始几何类,它通常使用构造函数所提供的“width”、“height”、“depth”参数来创建立方体或者不规则四边形。
代码示例:
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
1.1.1 ☘️构造函数
BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
width — X轴上面的宽度,默认值为1。
height — Y轴上面的高度,默认值为1。
depth — Z轴上面的深度,默认值为1。
widthSegments — (可选)宽度的分段数,默认值是1。
heightSegments — (可选)高度的分段数,默认值是1。
depthSegments — (可选)深度的分段数,默认值是1。
1.1.2 ☘️属性
共有属性请参见其基类BufferGeometry。
.parameters : Object
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.1.3 ☘️方法
共有方法请参见其基类BufferGeometry。
1.2 ☘️THREE.CylinderGeometry
THREE.CylinderGeometry一个用于生成圆柱几何体的类。
代码示例:
const geometry = new THREE.CylinderGeometry( 5, 5, 20, 32 );
const material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
const cylinder = new THREE.Mesh( geometry, material );
scene.add( cylinder );
1.2.1 ☘️构造函数
CylinderGeometry(radiusTop : Float, radiusBottom : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)
radiusTop — 圆柱的顶部半径,默认值是1。
radiusBottom — 圆柱的底部半径,默认值是1。
height — 圆柱的高度,默认值是1。
radialSegments — 圆柱侧面周围的分段数,默认为32。
heightSegments — 圆柱侧面沿着其高度的分段数,默认值为1。
openEnded — 一个Boolean值,指明该圆锥的底面是开放的还是封顶的。默认值为false,即其底面默认是封顶的。
thetaStart — 第一个分段的起始角度,默认为0。(three o’clock position)
thetaLength — 圆柱底面圆扇区的中心角,通常被称为“θ”(西塔)。默认值是2*Pi,这使其成为一个完整的圆柱。
1.2.2 ☘️属性
共有属性请参见其基类BufferGeometry。
.parameters : Object
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.2.3 ☘️方法
共有方法请参见其基类BufferGeometry。
1.3 ☘️THREE.ConeGeometry
THREE.ConeGeometry一个用于生成圆锥几何体的类。
代码示例:
const geometry = new THREE.ConeGeometry( 5, 20, 32 );
const material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
const cone = new THREE.Mesh( geometry, material );
scene.add( cone );
1.3.1 ☘️构造函数
ConeGeometry(radius : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)
radius — 圆锥底部的半径,默认值为1。
height — 圆锥的高度,默认值为1。
radialSegments — 圆锥侧面周围的分段数,默认为32。
heightSegments — 圆锥侧面沿着其高度的分段数,默认值为1。
openEnded — 一个Boolean值,指明该圆锥的底面是开放的还是封顶的。默认值为false,即其底面默认是封顶的。
thetaStart — 第一个分段的起始角度,默认为0。(three o’clock position)
thetaLength — 圆锥底面圆扇区的中心角,通常被称为“θ”(西塔)。默认值是2*Pi,这使其成为一个完整的圆锥。
1.3.2 ☘️属性
同THREE.CylinderGeometry一致
.parameters : Object
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.3.3 ☘️方法
同THREE.CylinderGeometry一致
1.4 ☘️THREE.SphereGeometry
THREE.SphereGeometry一个用于生成球体的类。
代码示例:
const geometry = new THREE.SphereGeometry( 15, 32, 16 );
const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
const sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
1.4.1 ☘️构造函数
SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)
radius — 球体半径,默认为1。
widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为32。
heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为16。
phiStart — 指定水平(经线)起始角度,默认值为0。。
phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
thetaStart — 指定垂直(纬线)起始角度,默认值为0。
thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。
该几何体是通过扫描并计算围绕着Y轴(水平扫描)和X轴(垂直扫描)的顶点来创建的。 因此,不完整的球体(类似球形切片)可以通过为phiStart,phiLength,thetaStart和thetaLength设置不同的值来创建, 以定义我们开始(或结束)计算这些顶点的起点(或终点)。
1.4.2 ☘️属性
共有属性请参见其基类BufferGeometry。
.parameters : Object
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.4.3 ☘️方法
共有方法请参见其基类BufferGeometry。
1.5 ☘️THREE.PlaneGeometry
THREE.PlaneGeometry一个用于生成平面几何体的类。
const geometry = new THREE.PlaneGeometry( 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
const plane = new THREE.Mesh( geometry, material );
scene.add( plane );
1.5.1 ☘️构造函数
PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)
width — 平面沿着X轴的宽度。默认值是1。
height — 平面沿着Y轴的高度。默认值是1。
widthSegments — (可选)平面的宽度分段数,默认值是1。
heightSegments — (可选)平面的高度分段数,默认值是1。
1.5.2 ☘️属性
共有属性请参见其基类BufferGeometry。
.parameters : Object
一个包含着构造函数中每个参数的对象。在对象实例化之后,对该属性的任何修改都不会改变这个几何体。
1.5.3 ☘️方法
共有方法请参见其基类BufferGeometry。
二、🍀构建三维汽车模拟器,场景有树、云、山等
1. ☘️实现思路
使用THREE.PlaneGeometry平面几何体构建场景地面;
使用THREE.BoxGeometry立方体构建汽车主体、车厢,THREE.CylinderGeometry圆柱体构建汽车轮子;
使用THREE.ConeGeometry圆锥构建山;
使用THREE.CylinderGeometry圆柱、THREE.ConeGeometry圆锥构建树;
使用THREE.SphereGeometry球体构建云;
使用THREE.BoxGeometry立方体、THREE.CylinderGeometry圆柱构建小火车;
绑定键盘方向键,控制汽车移动。
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>学习threejs,三维汽车模拟器,场景有树、云、山等</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #87ceeb;
}
canvas {
display: block;
}
/* Mobile Controls Styling */
.controls {
position: fixed;
bottom: 20px;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
z-index: 10;
pointer-events: none; /* Allow clicks/touches to pass through container */
}
.controls button {
pointer-events: auto; /* Enable interaction for buttons */
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 15px 20px;
font-size: 18px;
border-radius: 5px;
touch-action: manipulation; /* Prevents zooming on double tap */
user-select: none; /* Prevent text selection */
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE */
}
.controls .left-controls,
.controls .right-controls {
display: flex;
gap: 10px;
}
.controls .left-controls {
justify-content: flex-start;
}
.controls .right-controls {
justify-content: flex-end;
}
/* Hide controls on desktop */
@media (min-width: 769px) {
.controls {
display: none;
}
}
</style>
</head>
<body>
<!-- On-screen Mobile Controls -->
<div class="controls">
<div class="left-controls">
<button id="btn-left">Left</button>
<button id="btn-right">Right</button>
</div>
<div class="right-controls">
<button id="btn-fwd">Fwd</button>
<button id="btn-bwd">Bwd</button>
</div>
</div>
<!-- Import map for Three.js ES Modules -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
let scene, camera, renderer, clock;
let car, ground, road, train;
const mountains = [];
const trees = [];
const clouds = [];
// Movement state
const keyboard = {};
const touchControls = {
forward: false,
backward: false,
left: false,
right: false,
};
const carSpeed = 0.15;
const turnSpeed = 0.05;
const trainSpeed = 0.01;
let trainAngle = 0;
const trainRadius = 30;
init();
animate();
function init() {
// Basic Scene Setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // Sky blue
scene.fog = new THREE.Fog(0x87ceeb, 50, 150); // Add fog
clock = new THREE.Clock();
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 5, -10); // Initial position slightly behind where the car will be
camera.lookAt(0, 0, 0);
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // Enable shadows
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 25);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 200;
scene.add(directionalLight);
// --- Create Objects ---
// Ground
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x55aa55,
side: THREE.DoubleSide,
}); // Green
ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // Rotate flat
ground.receiveShadow = true;
scene.add(ground);
// Road
const roadGeometry = new THREE.PlaneGeometry(8, 200); // Narrow and long
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x444444 }); // Dark grey
road = new THREE.Mesh(roadGeometry, roadMaterial);
road.rotation.x = -Math.PI / 2;
road.position.y = 0.01; // Slightly above ground
road.receiveShadow = true;
scene.add(road);
// Car
car = createCar();
car.position.set(0, 0.3, 0); // Start on the road
scene.add(car);
// Mountains
createMountains(15);
// Trees
createTrees(50);
// Clouds
createClouds(20);
// Train
train = createTrain();
train.position.y = 0.2; // Slightly above ground
scene.add(train);
// Event Listeners
window.addEventListener("resize", onWindowResize, false);
window.addEventListener("keydown", (event) => {
keyboard[event.key.toLowerCase()] = true;
});
window.addEventListener("keyup", (event) => {
keyboard[event.key.toLowerCase()] = false;
});
// Touch Controls Listeners
setupTouchControls();
}
function createCar() {
const carGroup = new THREE.Group();
// Body
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.6, 3);
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Red
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.3;
body.castShadow = true;
carGroup.add(body);
// Cabin
const cabinGeometry = new THREE.BoxGeometry(1.3, 0.5, 1.5);
const cabinMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Light grey
const cabin = new THREE.Mesh(cabinGeometry, cabinMaterial);
cabin.position.set(0, 0.75, -0.3); // y = body.y + body.height/2 + cabin.height/2
cabin.castShadow = true;
carGroup.add(cabin);
// Wheels
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.3, 16);
const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x222222 }); // Dark grey/black
const wheelPositions = [
{ x: 0.8, y: 0, z: 1.0 }, // Front right
{ x: -0.8, y: 0, z: 1.0 }, // Front left
{ x: 0.8, y: 0, z: -1.0 }, // Back right
{ x: -0.8, y: 0, z: -1.0 }, // Back left
];
wheelPositions.forEach((pos) => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.rotation.z = Math.PI / 2; // Rotate to stand upright
wheel.position.set(pos.x, pos.y + 0.15, pos.z); // Adjust y based on radius
wheel.castShadow = true;
carGroup.add(wheel);
});
// Add invisible object for camera tracking point slightly behind the car
const cameraTarget = new THREE.Object3D();
cameraTarget.position.set(0, 2, -5); // Behind and slightly above
carGroup.add(cameraTarget);
carGroup.userData.cameraTarget = cameraTarget; // Store reference
return carGroup;
}
function createMountains(count) {
const mountainMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 }); // Brownish
const snowMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); // White snow caps
for (let i = 0; i < count; i++) {
const height = Math.random() * 30 + 10;
const radius = Math.random() * 10 + 5;
const mountainGeometry = new THREE.ConeGeometry(radius, height, 8); // Low poly cone
const mountain = new THREE.Mesh(mountainGeometry, mountainMaterial);
mountain.position.x = (Math.random() - 0.5) * 180; // Spread them out
mountain.position.z = (Math.random() - 0.5) * 180;
// Ensure mountains are far from the central road area
if (Math.abs(mountain.position.x) < 20) mountain.position.x += Math.sign(mountain.position.x) * 20;
if (Math.abs(mountain.position.z) < 20) mountain.position.z += Math.sign(mountain.position.z) * 20;
mountain.position.y = height / 2 - 0.1; // Base on the ground plane
mountain.castShadow = true;
mountain.receiveShadow = true;
scene.add(mountain);
mountains.push(mountain);
// Add snow cap
if (height > 25) {
const snowHeight = height * 0.3;
const snowRadius = radius * (snowHeight / height) * 0.8; // Tapered snow cap
const snowGeometry = new THREE.ConeGeometry(snowRadius, snowHeight, 8);
const snowCap = new THREE.Mesh(snowGeometry, snowMaterial);
snowCap.position.y = height - snowHeight / 2; // Position on top
mountain.add(snowCap); // Add as child
}
}
}
function createTrees(count) {
const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 }); // Brown
const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228b22 }); // Forest Green
for (let i = 0; i < count; i++) {
const tree = new THREE.Group();
const trunkHeight = Math.random() * 3 + 1;
const trunkRadius = trunkHeight * 0.1;
const trunkGeometry = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8);
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.position.y = trunkHeight / 2;
trunk.castShadow = true;
tree.add(trunk);
const leavesHeight = Math.random() * 4 + 2;
const leavesRadius = leavesHeight * 0.4;
const leavesGeometry = new THREE.ConeGeometry(leavesRadius, leavesHeight, 6);
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
leaves.position.y = trunkHeight + leavesHeight / 2 - 0.2; // Sit on top of trunk
leaves.castShadow = true;
tree.add(leaves);
// Position the tree randomly, avoiding the road
tree.position.x = (Math.random() - 0.5) * 150;
tree.position.z = (Math.random() - 0.5) * 150;
// Ensure trees are off the road (road width is 8, give some buffer)
if (Math.abs(tree.position.x) < 6) {
tree.position.x += Math.sign(tree.position.x || 1) * 6; // Move it away if too close
}
tree.position.y = 0; // Base at ground level
scene.add(tree);
trees.push(tree);
}
}
function createClouds(count) {
const cloudMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8,
});
for (let i = 0; i < count; i++) {
const cloud = new THREE.Group();
const numSpheres = Math.floor(Math.random() * 5) + 3; // 3 to 7 spheres per cloud
for (let j = 0; j < numSpheres; j++) {
const sphereSize = Math.random() * 5 + 2;
const sphereGeometry = new THREE.SphereGeometry(sphereSize, 8, 8); // Low poly spheres
const sphere = new THREE.Mesh(sphereGeometry, cloudMaterial);
// Offset spheres slightly to form cloud shape
sphere.position.set((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 3, (Math.random() - 0.5) * 5);
sphere.castShadow = true; // Clouds can cast subtle shadows
cloud.add(sphere);
}
// Position the cloud group high up and spread out
cloud.position.x = (Math.random() - 0.5) * 180;
cloud.position.z = (Math.random() - 0.5) * 180;
cloud.position.y = Math.random() * 20 + 30; // Height range
scene.add(cloud);
clouds.push(cloud);
}
}
function createTrain() {
const trainGroup = new THREE.Group();
const colors = [0x4444ff, 0xffaa00, 0x44ff44]; // Blue engine, orange, green cars
const carLength = 5;
const carWidth = 2;
const carHeight = 1.8;
const gap = 0.5;
for (let i = 0; i < 3; i++) {
const carGeometry = new THREE.BoxGeometry(carWidth, carHeight, carLength);
const carMaterial = new THREE.MeshStandardMaterial({ color: colors[i] });
const trainCar = new THREE.Mesh(carGeometry, carMaterial);
trainCar.position.z = -(i * (carLength + gap)); // Position cars behind each other
trainCar.castShadow = true;
trainCar.receiveShadow = true;
trainGroup.add(trainCar);
// Simple wheels for each car
const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.2, 8);
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const wheelPositions = [
{ x: carWidth / 2 + 0.1, z: carLength / 2 - 0.5 },
{ x: carWidth / 2 + 0.1, z: -carLength / 2 + 0.5 },
{ x: -carWidth / 2 - 0.1, z: carLength / 2 - 0.5 },
{ x: -carWidth / 2 - 0.1, z: -carLength / 2 + 0.5 },
];
wheelPositions.forEach((pos) => {
const wheel = new THREE.Mesh(wheelGeo, wheelMat);
wheel.rotation.x = Math.PI / 2;
wheel.position.set(pos.x, -carHeight / 2 + 0.4, trainCar.position.z + pos.z);
wheel.castShadow = true;
trainGroup.add(wheel);
});
}
return trainGroup;
}
function setupTouchControls() {
const btnFwd = document.getElementById("btn-fwd");
const btnBwd = document.getElementById("btn-bwd");
const btnLeft = document.getElementById("btn-left");
const btnRight = document.getElementById("btn-right");
// Touch start events
btnFwd.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
touchControls.forward = true;
},
{ passive: false }
);
btnBwd.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
touchControls.backward = true;
},
{ passive: false }
);
btnLeft.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
touchControls.left = true;
},
{ passive: false }
);
btnRight.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
touchControls.right = true;
},
{ passive: false }
);
// Touch end events (using 'touchend' and 'touchcancel')
const touchEndHandler = (control) => (e) => {
// Check if any remaining touches are on the *same* button
let stillTouching = false;
if (e.touches) {
for (let i = 0; i < e.touches.length; i++) {
if (e.touches[i].target === e.target) {
stillTouching = true;
break;
}
}
}
if (!stillTouching) {
touchControls[control] = false;
}
};
btnFwd.addEventListener("touchend", touchEndHandler("forward"));
btnBwd.addEventListener("touchend", touchEndHandler("backward"));
btnLeft.addEventListener("touchend", touchEndHandler("left"));
btnRight.addEventListener("touchend", touchEndHandler("right"));
btnFwd.addEventListener("touchcancel", touchEndHandler("forward"));
btnBwd.addEventListener("touchcancel", touchEndHandler("backward"));
btnLeft.addEventListener("touchcancel", touchEndHandler("left"));
btnRight.addEventListener("touchcancel", touchEndHandler("right"));
// Prevent scrolling on the controls themselves
document.querySelector(".controls").addEventListener(
"touchmove",
(e) => {
e.preventDefault();
},
{ passive: false }
);
}
function updateCarMovement(deltaTime) {
const effectiveSpeed = carSpeed * (deltaTime * 60); // Normalize speed based on 60fps
const effectiveTurnSpeed = turnSpeed * (deltaTime * 60);
let moveForward = keyboard["arrowup"] || keyboard["w"] || touchControls.forward;
let moveBackward = keyboard["arrowdown"] || keyboard["s"] || touchControls.backward;
let turnLeft = keyboard["arrowleft"] || keyboard["a"] || touchControls.left;
let turnRight = keyboard["arrowright"] || keyboard["d"] || touchControls.right;
if (moveForward) {
car.translateZ(effectiveSpeed);
}
if (moveBackward) {
car.translateZ(-effectiveSpeed * 0.7); // Slower reverse
}
if (turnLeft) {
car.rotateY(effectiveTurnSpeed);
}
if (turnRight) {
car.rotateY(-effectiveTurnSpeed);
}
}
function updateTrainMovement(deltaTime) {
trainAngle += trainSpeed * (deltaTime * 60); // Normalize speed
if (trainAngle > Math.PI * 2) {
trainAngle -= Math.PI * 2; // Loop the angle
}
const trainX = Math.cos(trainAngle) * trainRadius;
const trainZ = Math.sin(trainAngle) * trainRadius;
train.position.x = trainX;
train.position.z = trainZ;
// Make train face forward
const nextAngle = trainAngle + 0.01; // Look slightly ahead
const nextX = Math.cos(nextAngle) * trainRadius;
const nextZ = Math.sin(nextAngle) * trainRadius;
train.lookAt(nextX, train.position.y, nextZ);
}
function updateCamera() {
if (!car || !car.userData.cameraTarget) return;
const targetPosition = new THREE.Vector3();
// Get the world position of the invisible target object added to the car group
car.userData.cameraTarget.getWorldPosition(targetPosition);
// Smoothly interpolate camera position towards the target
camera.position.lerp(targetPosition, 0.05);
// Always look at the car's main body position
const lookAtPosition = new THREE.Vector3();
car.getWorldPosition(lookAtPosition); // Get car's world position
lookAtPosition.y += 0.5; // Look slightly above the car's base
camera.lookAt(lookAtPosition);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
updateCarMovement(deltaTime);
updateTrainMovement(deltaTime);
updateCamera();
renderer.render(scene, camera);
}
</script>
</body>
</html
效果如下:
参考:Three.js 汽车模拟器