實現原理是使用TWEEN.Tween實現動畫效果
實現
汽車模型載入
使用Promise編寫模型的非同步載入方法
引數position是汽車初始位置,引數rotation是汽車初始朝向
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 EAST = { x: 0, y: 1.5707963, z: 0 };
let SOUTH = { x: 0, y: 0, z: 0 };
let WEST = { x: 0, y: -1.5707963, z: 0 };
let NORTH = { x: 0, y: 3.1415926, z: 0 };
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 },
[WEST, SOUTH],
{ x: -130.5, y: 1.5, z: 4 },
[SOUTH, EAST],
{ x: -82, y: 1.5, z: 4 },
[EAST, SOUTH],
{ x: -82, y: 1.5, z: 14.7 },
[SOUTH, EAST],
{ x: -18.8, y: 1.5, z: 14.7 },
[EAST, SOUTH],
{ x: -18.8, y: 1.5, z: 70 },
];
let speed = 5;
setTimeout(async () => {
await car.loadCar(
positions[0],
WEST);
car.showLabel(positions[0], "皖A67893");
for (let i = 1; i < positions.length; i++) {
if (positions[i].length) {
await car.rotateCar(positions[i][0], positions[i][1]);
} else {
let start = positions[i - 1].length ? positions[i - 2] : positions[i - 1];
await car.moveCar(start, positions[i], speed);
}
}
car.unloadCar();
this.carLine1(2000);
}, 5000);
}
呼叫
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 () {
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 () {
// 汽車朝向
let EAST = { x: 0, y: 1.5707963, z: 0 };
let SOUTH = { x: 0, y: 0, z: 0 };
let WEST = { x: 0, y: -1.5707963, z: 0 };
let NORTH = { x: 0, y: 3.1415926, z: 0 };
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 },
[WEST, SOUTH],
{ x: -130.5, y: 1.5, z: 4 },
[SOUTH, EAST],
{ x: -82, y: 1.5, z: 4 },
[EAST, SOUTH],
{ x: -82, y: 1.5, z: 14.7 },
[SOUTH, EAST],
{ x: -18.8, y: 1.5, z: 14.7 },
[EAST, SOUTH],
{ x: -18.8, y: 1.5, z: 70 },
];
let speed = 5;
setTimeout(async () => {
await car.loadCar(
positions[0],
WEST);
car.showLabel(positions[0], "皖A67893");
for (let i = 1; i < positions.length; i++) {
if (positions[i].length) {
await car.rotateCar(positions[i][0], positions[i][1]);
} else {
let start = positions[i - 1].length ? positions[i - 2] : positions[i - 1];
await car.moveCar(start, positions[i], 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],
NORTH);
car.showLabel(positions[0], "皖AD887U");
await car.moveCar(
positions[0],
positions[1],
speed);
await car.rotateCar(
NORTH,
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物件中的
- 汽車行駛和轉向等方法都是非同步方法,可以避免回撥地獄,這樣汽車多段行駛的程式碼會寫的比較清晰
- 在實現並完善功能的過程中不斷重構:回撥地獄的實現方式-->呼叫moveCar和rotateCar時直接傳遞座標,很多座標及轉向資料和方法呼叫摻和在一起,看著眼花-->座標和轉向資料和方法呼叫邏輯分離,看著很清晰