- A+
所属分类:Web前端
实现原理是使用TWEEN.Tween实现动画效果
实现
汽车模型加载
使用Promise编写模型的异步加载方法
Car.prototype.loadCar = function (position, rotation) { let onProgress = function (xhr) { }; return new Promise((resolve, reject) => { if (!this.model) { let loader = new THREE.GLTFLoader(); loader.load(this.url, gltf => { const model = gltf.scene || gltf.scenes[0]; model.position.x = position.x; model.position.y = position.y; model.position.z = position.z; model.scale.set(0.25, 0.25, 0.25); model.rotation.set(rotation.x, rotation.y, rotation.z); this.model = model; this.scene.add(model); resolve(model); }, onProgress, xhr => { console.error(xhr); console.info('模型 ' + url + ' 加载失败'); reject(xhr); }); } else { resolve(this.model); } }); }
调用:
第1个参数是初始位置,第2个参数表示汽车朝向西
await car.loadCar(positions[0], car.WEST);
汽车行驶
参数start是行驶起点位置,参数end是行驶终点位置,参数speed是速度
this.model是汽车模型,onUpdate事件中,不断更新它的position
this.label是汽车车牌号标签,onUpdate事件中,不断更新它的position
Car.prototype.moveCar = function (start, end, speed) { let distance = this.distance(start, end); let time = distance / speed * 1000; return new Promise((resolve, reject) => { this.tween = new TWEEN.Tween({ x: start.x, y: start.y, z: start.z }).to({ x: end.x, y: end.y, z: end.z }, time).start().onUpdate(e => { if (this.model) { this.model.position.x = e.x; this.model.position.y = e.y; this.model.position.z = e.z; } if (this.label) { this.label.position.x = e.x; this.label.position.y = e.y + 1.2; this.label.position.z = e.z; } }).onComplete(() => { TWEEN.remove(this.tween); resolve(); }); }); }
汽车转弯
参数start是动画开始时的汽车朝向,end是动画结束时的汽车朝向
Car.prototype.rotateCar = function (start, end) { return new Promise((resolve, reject) => { this.tween = new TWEEN.Tween({ x: start.x, y: start.y, z: start.z }).to({ x: end.x, y: end.y, z: end.z }, 300).start().onUpdate(e => { if (this.model) { this.model.rotation.set(e.x, e.y, e.z); } }).onComplete(() => { TWEEN.remove(this.tween); resolve(); }); }); }
汽车行驶一段路线
上述汽车行驶和汽车转弯方法都是异步方法,所以避免了回调地狱,不然下面的多段行驶及转弯就不好写了
Cars.prototype.carLine1 = function () { if (!this.run) return; let car = new Car(this.scene, this.renderer, './models/车红.glb'); this.cars.push(car); let positions = [ { x: -121, y: 1.5, z: -16 }, { x: -130.5, y: 1.5, z: -16 }, { x: -130.5, y: 1.5, z: 4 }, { x: -82, y: 1.5, z: 4 }, { x: -82, y: 1.5, z: 14.7 }, { x: -18.8, y: 1.5, z: 14.7 }, { x: -18.8, y: 1.5, z: 70 }, ]; let speed = 5; setTimeout(async () => { await car.loadCar( positions[0], car.WEST); car.showLabel(positions[0], "皖A67893"); await car.moveCar( positions[0], positions[1], speed); await car.rotateCar( car.WEST, car.SOUTH); await car.moveCar( positions[1], positions[2], speed); await car.rotateCar( car.SOUTH, car.EAST); await car.moveCar( positions[2], positions[3], speed); await car.rotateCar( car.EAST, car.SOUTH); await car.moveCar( positions[3], positions[4], speed); await car.rotateCar( car.SOUTH, car.EAST); await car.moveCar( positions[4], positions[5], speed); await car.rotateCar( car.EAST, car.SOUTH); await car.moveCar( positions[5], positions[6], speed); car.unloadCar(); this.carLine1(2000); }, 5000); } Cars.prototype.carLine2 = function () { if (!this.run) return; let car = new Car(this.scene, this.renderer, './models/车蓝.glb'); this.cars.push(car); let positions = [ { x: -5, y: 1.5, z: 70 }, { x: -5, y: 1.5, z: 14.7 }, { x: 70, y: 1.5, z: 14.7 } ]; let speed = 5; setTimeout(async () => { await car.loadCar( positions[0], car.NORTH); car.showLabel(positions[0], "皖AD887U"); await car.moveCar( positions[0], positions[1], speed); await car.rotateCar( car.NORTH, car.EAST); await car.moveCar( positions[1], positions[2], speed); car.unloadCar(); this.carLine2(3000); }, 6000); }
调用
let cars = new Cars(app.scene, app.renderer); cars.carLine1(); cars.carLine2();
显示车牌号
Car.prototype.showLabel = function (position, text) { let canvasDraw = new CanvasDraw(); let canvasTexture = canvasDraw.drawCarLabel(THREE, this.renderer, text, '#006688'); //标签 let spriteMaterial = new THREE.SpriteMaterial({ map: canvasTexture, color: 0xffffff, depthTest: false, side: THREE.DoubleSide, sizeAttenuation: false, transparent: true, opacity: 0.8 }); let sprite = new THREE.Sprite(spriteMaterial); sprite.scale.set(0.2, 0.1, 0.2) sprite.position.x = position.x; sprite.position.y = position.y + 1.2; sprite.position.z = position.z; this.label = sprite; this.scene.add(sprite); return sprite; }
完整代码
car.js
// 汽车 let Car = (function () { // 汽车朝向 Car.prototype.EAST = { x: 0, y: 1.5707963, z: 0 }; Car.prototype.SOUTH = { x: 0, y: 0, z: 0 }; Car.prototype.WEST = { x: 0, y: -1.5707963, z: 0 }; Car.prototype.NORTH = { x: 0, y: 3.1415926, z: 0 }; function Car(scene, renderer, url) { this.scene = scene; this.renderer = renderer; this.url = url; this.clock = new THREE.Clock(); } Car.prototype.loadCar = function (position, rotation) { let onProgress = function (xhr) { }; return new Promise((resolve, reject) => { if (!this.model) { let loader = new THREE.GLTFLoader(); loader.load(this.url, gltf => { const model = gltf.scene || gltf.scenes[0]; model.position.x = position.x; model.position.y = position.y; model.position.z = position.z; model.scale.set(0.25, 0.25, 0.25); model.rotation.set(rotation.x, rotation.y, rotation.z); this.model = model; this.scene.add(model); resolve(model); }, onProgress, xhr => { console.error(xhr); console.info('模型 ' + url + ' 加载失败'); reject(xhr); }); } else { resolve(this.model); } }); } Car.prototype.unloadCar = function () { this.stopTween(); this.removeModel(); this.removeLabel(); } Car.prototype.stopTween = function () { if (this.tween) { TWEEN.remove(this.tween); } else { setTimeout(() => { this.stopTween(); }, 100); } } Car.prototype.removeModel = function () { if (this.model) { this.scene.remove(this.model); } else { setTimeout(() => { this.removeModel(); }, 100); } } Car.prototype.removeLabel = function () { if (this.label) { this.scene.remove(this.label); } else { setTimeout(() => { this.removeLabel(); }, 100); } } Car.prototype.moveCar = function (start, end, speed) { let distance = this.distance(start, end); let time = distance / speed * 1000; return new Promise((resolve, reject) => { this.tween = new TWEEN.Tween({ x: start.x, y: start.y, z: start.z }).to({ x: end.x, y: end.y, z: end.z }, time).start().onUpdate(e => { if (this.model) { this.model.position.x = e.x; this.model.position.y = e.y; this.model.position.z = e.z; } if (this.label) { this.label.position.x = e.x; this.label.position.y = e.y + 1.2; this.label.position.z = e.z; } }).onComplete(() => { TWEEN.remove(this.tween); resolve(); }); }); } Car.prototype.rotateCar = function (start, end) { return new Promise((resolve, reject) => { this.tween = new TWEEN.Tween({ x: start.x, y: start.y, z: start.z }).to({ x: end.x, y: end.y, z: end.z }, 300).start().onUpdate(e => { if (this.model) { this.model.rotation.set(e.x, e.y, e.z); } }).onComplete(() => { TWEEN.remove(this.tween); resolve(); }); }); } Car.prototype.showLabel = function (position, text) { let canvasDraw = new CanvasDraw(); let canvasTexture = canvasDraw.drawCarLabel(THREE, this.renderer, text, '#006688'); //标签 let spriteMaterial = new THREE.SpriteMaterial({ map: canvasTexture, color: 0xffffff, depthTest: false, side: THREE.DoubleSide, sizeAttenuation: false, transparent: true, opacity: 0.8 }); let sprite = new THREE.Sprite(spriteMaterial); sprite.scale.set(0.2, 0.1, 0.2) sprite.position.x = position.x; sprite.position.y = position.y + 1.2; sprite.position.z = position.z; this.label = sprite; this.scene.add(sprite); return sprite; } Car.prototype.distance = function (p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2) + Math.pow(p1.z - p2.z, 2)); } return Car; })();
cars.js
// 多个车辆 let Cars = (function () { function Cars(scene, renderer) { this.scene = scene; this.renderer = renderer; this.cars = []; this.run = true; } Cars.prototype.carLine1 = function () { if (!this.run) return; let car = new Car(this.scene, this.renderer, './models/车红.glb'); this.cars.push(car); let positions = [ { x: -121, y: 1.5, z: -16 }, { x: -130.5, y: 1.5, z: -16 }, { x: -130.5, y: 1.5, z: 4 }, { x: -82, y: 1.5, z: 4 }, { x: -82, y: 1.5, z: 14.7 }, { x: -18.8, y: 1.5, z: 14.7 }, { x: -18.8, y: 1.5, z: 70 }, ]; let speed = 5; setTimeout(async () => { await car.loadCar( positions[0], car.WEST); car.showLabel(positions[0], "皖A67893"); await car.moveCar( positions[0], positions[1], speed); await car.rotateCar( car.WEST, car.SOUTH); await car.moveCar( positions[1], positions[2], speed); await car.rotateCar( car.SOUTH, car.EAST); await car.moveCar( positions[2], positions[3], speed); await car.rotateCar( car.EAST, car.SOUTH); await car.moveCar( positions[3], positions[4], speed); await car.rotateCar( car.SOUTH, car.EAST); await car.moveCar( positions[4], positions[5], speed); await car.rotateCar( car.EAST, car.SOUTH); await car.moveCar( positions[5], positions[6], speed); car.unloadCar(); this.carLine1(2000); }, 5000); } Cars.prototype.carLine2 = function () { if (!this.run) return; let car = new Car(this.scene, this.renderer, './models/车蓝.glb'); this.cars.push(car); let positions = [ { x: -5, y: 1.5, z: 70 }, { x: -5, y: 1.5, z: 14.7 }, { x: 70, y: 1.5, z: 14.7 } ]; let speed = 5; setTimeout(async () => { await car.loadCar( positions[0], car.NORTH); car.showLabel(positions[0], "皖AD887U"); await car.moveCar( positions[0], positions[1], speed); await car.rotateCar( car.NORTH, car.EAST); await car.moveCar( positions[1], positions[2], speed); car.unloadCar(); this.carLine2(3000); }, 6000); } Cars.prototype.clear = function () { this.run = false; this.cars.forEach(car => { car.unloadCar(); }); } return Cars; })();
调用
// 显示汽车 function showCars() { cars = new Cars(app.scene, app.renderer); cars.carLine1(); cars.carLine2(); } // 清除汽车 function clearCars() { cars.clear(); } // 显示汽车 showCars();
总结
- 解耦:依赖的scene, renderer参数是通过构造函数传到Car和Cars对象中的
- 汽车行驶和转向等方法都是异步方法,可以避免回调地狱,这样汽车多段行驶的代码会写的比较清晰