学习threejs,三维汽车模拟器,场景有树、云、山等
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
- 一、🍀前言
- 1.1 ☘️THREE.BoxGeometry
- 1.1.1 ☘️构造函数
- 1.1.2 ☘️属性
- 1.1.3 ☘️方法
- 1.2 ☘️THREE.CylinderGeometry
- 1.2.1 ☘️构造函数
- 1.2.2 ☘️属性
- 1.2.3 ☘️方法
- 1.3 ☘️THREE.ConeGeometry
- 1.3.1 ☘️构造函数
- 1.3.2 ☘️属性
- 1.3.3 ☘️方法
- 1.4 ☘️THREE.SphereGeometry
- 1.4.1 ☘️构造函数
- 1.4.2 ☘️属性
- 1.4.3 ☘️方法
- 1.5 ☘️THREE.PlaneGeometry
- 1.5.1 ☘️构造函数
- 1.5.2 ☘️属性
- 1.5.3 ☘️方法
- 二、🍀构建三维汽车模拟器,场景有树、云、山等
- 1. ☘️实现思路
- 2. ☘️代码样例
一、🍀前言
本文详细介绍如何基于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 stateconst 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 Setupscene = new THREE.Scene();scene.background = new THREE.Color(0x87ceeb); // Sky bluescene.fog = new THREE.Fog(0x87ceeb, 50, 150); // Add fogclock = new THREE.Clock();// Cameracamera = 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 becamera.lookAt(0, 0, 0);// Rendererrenderer = new THREE.WebGLRenderer({ antialias: true });renderer.setSize(window.innerWidth, window.innerHeight);renderer.shadowMap.enabled = true; // Enable shadowsrenderer.shadowMap.type = THREE.PCFSoftShadowMap;document.body.appendChild(renderer.domElement);// Lightingconst 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 ---// Groundconst groundGeometry = new THREE.PlaneGeometry(200, 200);const groundMaterial = new THREE.MeshStandardMaterial({color: 0x55aa55,side: THREE.DoubleSide,}); // Greenground = new THREE.Mesh(groundGeometry, groundMaterial);ground.rotation.x = -Math.PI / 2; // Rotate flatground.receiveShadow = true;scene.add(ground);// Roadconst roadGeometry = new THREE.PlaneGeometry(8, 200); // Narrow and longconst roadMaterial = new THREE.MeshStandardMaterial({ color: 0x444444 }); // Dark greyroad = new THREE.Mesh(roadGeometry, roadMaterial);road.rotation.x = -Math.PI / 2;road.position.y = 0.01; // Slightly above groundroad.receiveShadow = true;scene.add(road);// Carcar = createCar();car.position.set(0, 0.3, 0); // Start on the roadscene.add(car);// MountainscreateMountains(15);// TreescreateTrees(50);// CloudscreateClouds(20);// Traintrain = createTrain();train.position.y = 0.2; // Slightly above groundscene.add(train);// Event Listenerswindow.addEventListener("resize", onWindowResize, false);window.addEventListener("keydown", (event) => {keyboard[event.key.toLowerCase()] = true;});window.addEventListener("keyup", (event) => {keyboard[event.key.toLowerCase()] = false;});// Touch Controls ListenerssetupTouchControls();}function createCar() {const carGroup = new THREE.Group();// Bodyconst bodyGeometry = new THREE.BoxGeometry(1.5, 0.6, 3);const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Redconst body = new THREE.Mesh(bodyGeometry, bodyMaterial);body.position.y = 0.3;body.castShadow = true;carGroup.add(body);// Cabinconst cabinGeometry = new THREE.BoxGeometry(1.3, 0.5, 1.5);const cabinMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Light greyconst cabin = new THREE.Mesh(cabinGeometry, cabinMaterial);cabin.position.set(0, 0.75, -0.3); // y = body.y + body.height/2 + cabin.height/2cabin.castShadow = true;carGroup.add(cabin);// Wheelsconst wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.3, 16);const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x222222 }); // Dark grey/blackconst 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 uprightwheel.position.set(pos.x, pos.y + 0.15, pos.z); // Adjust y based on radiuswheel.castShadow = true;carGroup.add(wheel);});// Add invisible object for camera tracking point slightly behind the carconst cameraTarget = new THREE.Object3D();cameraTarget.position.set(0, 2, -5); // Behind and slightly abovecarGroup.add(cameraTarget);carGroup.userData.cameraTarget = cameraTarget; // Store referencereturn carGroup;}function createMountains(count) {const mountainMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 }); // Brownishconst snowMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); // White snow capsfor (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 coneconst mountain = new THREE.Mesh(mountainGeometry, mountainMaterial);mountain.position.x = (Math.random() - 0.5) * 180; // Spread them outmountain.position.z = (Math.random() - 0.5) * 180;// Ensure mountains are far from the central road areaif (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 planemountain.castShadow = true;mountain.receiveShadow = true;scene.add(mountain);mountains.push(mountain);// Add snow capif (height > 25) {const snowHeight = height * 0.3;const snowRadius = radius * (snowHeight / height) * 0.8; // Tapered snow capconst snowGeometry = new THREE.ConeGeometry(snowRadius, snowHeight, 8);const snowCap = new THREE.Mesh(snowGeometry, snowMaterial);snowCap.position.y = height - snowHeight / 2; // Position on topmountain.add(snowCap); // Add as child}}}function createTrees(count) {const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 }); // Brownconst leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228b22 }); // Forest Greenfor (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 trunkleaves.castShadow = true;tree.add(leaves);// Position the tree randomly, avoiding the roadtree.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 levelscene.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 cloudfor (let j = 0; j < numSpheres; j++) {const sphereSize = Math.random() * 5 + 2;const sphereGeometry = new THREE.SphereGeometry(sphereSize, 8, 8); // Low poly spheresconst sphere = new THREE.Mesh(sphereGeometry, cloudMaterial);// Offset spheres slightly to form cloud shapesphere.position.set((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 3, (Math.random() - 0.5) * 5);sphere.castShadow = true; // Clouds can cast subtle shadowscloud.add(sphere);}// Position the cloud group high up and spread outcloud.position.x = (Math.random() - 0.5) * 180;cloud.position.z = (Math.random() - 0.5) * 180;cloud.position.y = Math.random() * 20 + 30; // Height rangescene.add(cloud);clouds.push(cloud);}}function createTrain() {const trainGroup = new THREE.Group();const colors = [0x4444ff, 0xffaa00, 0x44ff44]; // Blue engine, orange, green carsconst 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 othertrainCar.castShadow = true;trainCar.receiveShadow = true;trainGroup.add(trainCar);// Simple wheels for each carconst 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 eventsbtnFwd.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* buttonlet 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 themselvesdocument.querySelector(".controls").addEventListener("touchmove",(e) => {e.preventDefault();},{ passive: false });}function updateCarMovement(deltaTime) {const effectiveSpeed = carSpeed * (deltaTime * 60); // Normalize speed based on 60fpsconst 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 speedif (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 forwardconst nextAngle = trainAngle + 0.01; // Look slightly aheadconst 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 groupcar.userData.cameraTarget.getWorldPosition(targetPosition);// Smoothly interpolate camera position towards the targetcamera.position.lerp(targetPosition, 0.05);// Always look at the car's main body positionconst lookAtPosition = new THREE.Vector3();car.getWorldPosition(lookAtPosition); // Get car's world positionlookAtPosition.y += 0.5; // Look slightly above the car's basecamera.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 汽车模拟器