实现原理是使用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();

总结

  1. 解耦:依赖的scene, renderer参数是通过构造函数传到Car和Cars对象中的
  2. 汽车行驶和转向等方法都是异步方法,可以避免回调地狱,这样汽车多段行驶的代码会写的比较清晰

改进

运行效果

three.js 汽车行驶效果的更多相关文章

  1. React.js实现原生js拖拽效果及思考

    一.起因&思路 不知不觉,已经好几天没写博客了...近来除了研究React,还做了公司官网... 一直想写一个原生js拖拽效果,又加上近来学react学得比较嗨.所以就用react来实现这个拖 ...

  2. 用js实现动画效果核心方式

    为了做好导航菜单,有时候需要在菜单下拉的时候实现动画效果,所以这几天就研究了研究如何用js实现动画效果,实现动画核心要用到两个函数,一个是setTimeOut,另一个是setInterval. 下边我 ...

  3. js拖拽效果

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 原生JS实现分页效果2.0(新增了上一页和下一页,添加当前元素样式)

    虽然写的很烂,但至少全部都是自己写的,因为这个没有固定的顺序,所以就没有封装,如果你技术好的话,可以你写的分享给我,谢谢. <!DOCTYPE html><html lang=&qu ...

  5. 原生JS实现分页效果1.0

    不太完整,写的太急,等等加上完整注释,写起来还是有些难度的,写的有点水,后面再改进改进. <!DOCTYPE html><html lang="en">&l ...

  6. 使用JS实现手风琴效果

    想要实现简单的手风琴切换效果,需要使用JS实现,如下是使用javascript源码实现,后续会更新使用jQuery实现. 1. 先进行简单的布局:我们可以再ul下添加几个li实现html的简单布局,再 ...

  7. js弹窗登录效果(源码)--web前端

    1.JS弹窗登录效果 <!DOCTYPE html><html lang="en"><head> <meta charset=" ...

  8. 原生JS实现弹幕效果

    纯属无聊写的,可能有很多问题,欢迎批评指教. 效果图:图一是预设的一些弹幕,图二是自己发射的弹幕,效果是一样的.   首先是弹幕的位置,是要从最右滑到最左,为了防止随机高度弹幕会覆盖的问题,设置了通道 ...

  9. JS添加标签效果

    JS添加标签效果 在豆瓣网上添加自己的标签是一种常见的效果,今天也就做了一个简单的demo.由于时间的问题 我不多原理,大家可以试着操作几遍就能明白其中的原理了. JSFiddle的效果如下: 点击我 ...

  10. js实现手风琴效果

    之前在慕课网上有练习手风琴效果,但是老师使用jquery简简单单的两三行实现了,今天自己用js练习一下效果 <div id="divbox"> <ul> & ...

随机推荐

  1. 【技术积累】Linux中的命令行【理论篇】【七】

    atrm命令 命令介绍 atrm命令是Linux系统中的一个命令行工具,用于取消或删除已经安排的at命令.at命令是一种用于在指定时间执行一次性任务的工具. 命令说明 atrm命令的语法如下: atr ...

  2. 21.1 使用PEfile分析PE文件

    PeFile模块是Python中一个强大的便携式第三方PE格式分析工具,用于解析和处理Windows可执行文件.该模块提供了一系列的API接口,使得用户可以通过Python脚本来读取和分析PE文件的结 ...

  3. 论文解读(TAT)《 Transferable Adversarial Training: A General Approach to Adapting Deep Classifiers》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:Transferable Adversarial Training: A General Approach ...

  4. aspnetcore 注册中心 consul

    consul启动 . http://192.168.1.6:8500/ #以server方式启动,UI可以访问,boot引导自己选为leader,网段内PC可访问 consul agent -serv ...

  5. 【HCDG城市行东莞站】松山湖开发者村助力企业释放数字新动能

    本文分享自华为云社区<[HCDG城市行东莞站]从"数据治理"洞察"数字化运营之道",松山湖开发者村助力企业释放数字新动能!>,作者:华为云社区精选. ...

  6. 《Kali渗透基础》06. 主动信息收集(三)

    @ 目录 1:服务识别 1.1:NetCat 1.2:Socket 1.3:dmitry 1.4:nmap 2:操作系统识别 2.1:Scapy 2.2:nmap 2.3:p0f 3:SNMP 扫描 ...

  7. 给你的 SpringBoot 工程部署的 jar 包瘦瘦身吧!

    之前有写过一篇有关maven插件的文章:spring-boot-maven-plugin插件详解 一.需求背景 我们知道Spring Boot项目,是可以通过java -jar 包名 启动的. 那为什 ...

  8. 部分网页中仅供浏览的pdf文件下载方法

    现在越来越多的网站提供的PDF资料只能在线浏览,不提供下载功能,实际上仅仅是通过网页PDF浏览插件来访问文件资源,如果能够获取到该文件的访问地址,就可以访问下载. 以Firefox浏览器访问某大学网站 ...

  9. Ubuntu22.04 编译安装nginx

    1.下载nginx软件包 https://nginx.org/en/download.html 2.压缩包上传服务器并解压缩 tar xf nginx-1.22.1.tar.gz 3.进入解压目录,编 ...

  10. Dubbo3应用开发—Dubbo序列化方案(Kryo、FST、FASTJSON2、ProtoBuf序列化方案的介绍和使用)

    Dubbo序列化方案(Kryo.FST.FASTJSON2.ProtoBuf序列化方案的介绍和使用) 序列化简介 序列化是Dubbo在RPC中非常重要的一个组成部分,其核心作用就是把网络传输中的数据, ...