实现原理是使用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. 【后端面经-数据库】Redis详解——Redis基本概念和特点

    目录 1. Redis基本概念 2. Redis特点 2.1 优点 2.2 缺点 3. Redis的应用场景 面试模拟 参考资料 声明:Redis的相关知识是面试的一大热门知识点,同时也是一个庞大的体 ...

  2. 为什么 API 治理需要内部倡导

    API 治理旨在帮助人们通过 API 实现最大价值.但是,只有了解 API 是什么以及 API 的重要性,并且认识到 API 治理是在帮助他们而不是监管他们,才能实现这一目标.这就是为什么在任何 AP ...

  3. 3.你不知道的go语言控制语句

    目录 本篇前瞻 Leetcode习题9 题目描述 题目分析 代码编写 知识点归纳 控制结构 顺序结构(Sequence) 声明和赋值 算术运算符 位运算符 逻辑运算 分支结构 if 语句 switch ...

  4. Unity UGUI的Slider(滑动条)件组的介绍及使用

    Unity UGUI的Slider(滑动条)件组的介绍及使用 1. 什么是Slider组件? Slider(滑动条)是Unity UGUI中的一种常用UI组件用,于在用户界面中实现滑动选择的功能.通过 ...

  5. 原神盲盒风格:AI绘画Stable Diffusion原神人物公仔实操:核心tag+lora模型汇总

    本教程收集于:AIGC从入门到精通教程汇总 在这篇文章中,我们将深入探讨原神盲盒的艺术风格,以及如何运用AI绘画技术(Stable Diffusion)--来创造原神角色公仔.我们将通过实践操作让读者 ...

  6. openNebula集群搭建

    openNebula集群搭建 目录 openNebula集群搭建 OpenNebula概述 环境介绍及部署前准备 1. 安装步骤 1.关闭防火墙 2.配置epel源地和opennebula源 3.安装 ...

  7. Electron创建项目并打包生成exe

    安装nodejs 访问这个网站去下载 http://nodejs.cn/download/ 创建项目 创建项目 git clone https://github.com/electron/electr ...

  8. [htmlayout] picture标签替代img, 解决更新图片数据后依然显示原图片数据

    在hl中, 你可能遇到过这样的情况.   给img标签设置了一个图片路径.   在软件使用过程中对这个路径的数据进行过重写, 删除等等 但img依然还是显示最初载入的图片数据. 解决办法: 用&quo ...

  9. 扩展ABP的Webhook功能,推送数据到第三方接口(企业微信群、钉钉群等)

    前言 在上一篇文章[基于ASP.NET ZERO,开发SaaS版供应链管理系统]中有提到对Webhook功能的扩展改造,本文详细介绍一下具体过程. Webhook功能操作说明,请参见此文档链接:Web ...

  10. Go运算操作符全解与实战:编写更高效的代码!

    本文全面探讨了Go语言中的各类运算操作符,从基础的数学和位运算到逻辑和特殊运算符.文章旨在深入解析每一种运算操作符的工作原理.应用场景和注意事项,以帮助开发者编写更高效.健壮和可读的Go代码. 简介 ...