印峰 发表于 2023-12-6 21:46:00

three.js 汽车行驶动画效果

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

                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, 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,
            car.WEST);

      car.showLabel(positions, "皖A67893");

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.WEST,
            car.SOUTH);

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.SOUTH,
            car.EAST);

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.EAST,
            car.SOUTH);

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.SOUTH,
            car.EAST);

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.EAST,
            car.SOUTH);

      await car.moveCar(
            positions,
            positions,
            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,
            car.NORTH);

      car.showLabel(positions, "皖AD887U");

      await car.moveCar(
            positions,
            positions,
            speed);

      await car.rotateCar(
            car.NORTH,
            car.EAST);

      await car.moveCar(
            positions,
            positions,
            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 },
      ,
      { 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,
            WEST);

      car.showLabel(positions, "皖A67893");

      for (let i = 1; i < positions.length; i++) {
            if (positions.length) {
                await car.rotateCar(positions, positions);
            } else {
                let start = positions.length ? positions : positions;
                await car.moveCar(start, positions, 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;

                  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 },
            ,
            { 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,
                WEST);

            car.showLabel(positions, "皖A67893");

            for (let i = 1; i < positions.length; i++) {
                if (positions.length) {
                  await car.rotateCar(positions, positions);
                } else {
                  let start = positions.length ? positions : positions;
                  await car.moveCar(start, positions, 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,
                NORTH);

            car.showLabel(positions, "皖AD887U");

            await car.moveCar(
                positions,
                positions,
                speed);

            await car.rotateCar(
                NORTH,
                EAST);

            await car.moveCar(
                positions,
                positions,
                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时直接传递坐标,很多坐标及转向数据和方法调用掺和在一起,看着眼花-->坐标和转向数据和方法调用逻辑分离,看着很清晰
运行效果



来源:https://www.cnblogs.com/s0611163/p/17879849.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: three.js 汽车行驶动画效果