gasp动画 threejs camera 相机位置 相机变换 动态视角

发布时间 2023-10-19 21:35:30作者: 虎虎生威啊

gasp动画控制相机的位置变换

关键代码threejs/Cameras.ts

  //通过gsap 动画移动相机,从而实现看似物体在运动的效果
  // 1.添加动画
  private changePerspectiveCameraPosition() {
    // 添加一个动画
    gsap.to(this.perspectiveCamera.position, {
      x: 5,
      z: 5,
      duration: 5,
    });
  }


  //2. 通过timeline 添加一组动画
  // 参考连接: https://juejin.cn/post/7041862990622605349
  private changePerspectiveCameraPositionGroup() {

    const tl = gsap.timeline();

    // 添加一组动画
    tl.to(this.perspectiveCamera.position, {
      x: 5,
      z: 5,
      duration: 3,
    })
      .to(this.perspectiveCamera.position, {
        x: -5,
        z: 5,
        duration: 3,
      })
      .to(this.perspectiveCamera.position, {
        x: 5,
        z: 5,
        duration: 3,
      });
  }

threejs/Cameras.ts

import { PerspectiveCamera, Scene } from "three";
import Base from "./Base";
import type Size from "./Size";
import { gsap } from "gsap";

export default class Camera {
  private size: Size;
  private scene: Scene;
  private canvas: HTMLCanvasElement;
  public perspectiveCamera: PerspectiveCamera;

  constructor() {
    const base = Base.getInstance();
    this.size = base.size;
    this.scene = base.scene;
    this.canvas = base.canvas;

    this.perspectiveCamera = this.createPerspectiveCamera();
    this.scene.add(this.perspectiveCamera);

    this.changePerspectiveCameraPositionGroup();
  }


  private createPerspectiveCamera() {
    const perspectiveCamera = new PerspectiveCamera(
      35,
      this.size.aspect,
      0.1,
      1000
    );
    perspectiveCamera.position.set(0, 10, 0);
    return perspectiveCamera;
  }


  //通过gsap 动画移动相机,从而实现看似物体在运动的效果
  // 1.添加动画
  private changePerspectiveCameraPosition() {
    // 添加一个动画
    gsap.to(this.perspectiveCamera.position, {
      x: 5,
      z: 5,
      duration: 5,
    });
  }


  //2. 通过timeline 添加一组动画
  // 参考连接: https://juejin.cn/post/7041862990622605349
  private changePerspectiveCameraPositionGroup() {

    const tl = gsap.timeline();

    // 添加一组动画
    tl.to(this.perspectiveCamera.position, {
      x: 5,
      z: 5,
      duration: 3,
    })
      .to(this.perspectiveCamera.position, {
        x: -5,
        z: 5,
        duration: 3,
      })
      .to(this.perspectiveCamera.position, {
        x: 5,
        z: 5,
        duration: 3,
      });
  }


  public resize() {
    // Updating Perspective Camera on Resize
    this.perspectiveCamera.aspect = this.size.aspect;
    this.perspectiveCamera.updateProjectionMatrix();
  }

}

// /Users/song/Code/threejs_learn_vanilla_class_singleton/threejs_learn_vanilla_ts_class_singleton/src/threejs/Cameras.ts


threejs/Base.ts

import { Object3D, Scene } from "three";

import Camera from "./Cameras";
import Helper from "./Helpers";
import Renderer from "./Renderer";
import Size from "./Size";
import Time from "./Time";
import Control from "./Controls";
import Lights from "./Lights";
import EventManger from "./Events";

import Box from "../threejs/World/Box";

export default class Base {
  public static base: Base;
  public canvas: HTMLCanvasElement;
  public scene: Scene;
  public cameras: Camera;
  public size: Size;
  public time: Time;
  public renderer: Renderer;
  public control: Control;
  public helper: Helper;
  public eventManger: EventManger;
  public noTransfromControlObjs: Object3D[] = [];

  constructor(canvas: HTMLCanvasElement) {
    Base.base = this;

    this.canvas = canvas as HTMLCanvasElement;
    this.scene = new Scene();
    this.size = new Size();
    this.cameras = new Camera();
    this.time = new Time();
    this.renderer = new Renderer();
    new Lights();

    // helper
    const helper = new Helper();
    this.helper = helper;
    this.noTransfromControlObjs.push(helper.gridHelper, helper.axesHelper);

    // control
    const control = new Control();
    this.control = control;
    this.noTransfromControlObjs.push(this.control.transformControl);
    // events
    this.eventManger = new EventManger();

    // 使用EventManager实例注册的事件,就是相当于全局事件,
    this.eventManger.addEventListener("click", (e) => {
      console.log("点击射线穿过了下面这些物体");
      console.log(e);
    });

    // 添加box----------------------
    new Box();

    this.time.on("update", () => {
      this.update();
    });
    this.size.on("resize", () => {
      this.resize();
    });
  }

  public static getInstance(canvas?: HTMLCanvasElement) {
    if (canvas === undefined && Base.base !== undefined) {
      return Base.base;
    }
    Base.base = new Base(canvas as HTMLCanvasElement);
    return Base.base;
  }

  private resize() {
    this.cameras.resize();
    this.renderer.resize();
  }

  private update() {
    this.control.update();
    this.renderer.update();
  }
}


threejs/Helpers.ts

import { AxesHelper, GridHelper } from "three";
import Base from "./Base";

export default class {
  public axesHelper: AxesHelper;
  public gridHelper: GridHelper;
  constructor() {
    const base = Base.getInstance();
    const axesHelper = new AxesHelper(5);
    this.axesHelper = axesHelper;
    base.scene.add(axesHelper);

    const size = 10;
    const divisions = 10;

    const gridHelper = new GridHelper(size, divisions);
    this.gridHelper = gridHelper;
    base.scene.add(gridHelper);
  }
}

threejs/Lights.ts

import { AmbientLight, DirectionalLight } from "three";
import Base from "./Base";

export default class Environment {
  private sunLight: DirectionalLight;
  private ambientLight: AmbientLight;
  constructor() {
    const base = Base.getInstance();
    this.sunLight = new DirectionalLight("#ffffff", 2);
    this.sunLight.castShadow = true;
    this.sunLight.shadow.camera.far = 20;
    this.sunLight.shadow.mapSize.set(2048, 2048);
    this.sunLight.shadow.normalBias = 0.05;

    this.sunLight.position.set(-1.5, 7, 10);
    base.scene.add(this.sunLight);

    this.ambientLight = new AmbientLight("#ffffff", 0.1);
    base.scene.add(this.ambientLight);
  }
}

threejs/Size.ts

import EventEmitter from "eventemitter2";
export default class Size extends EventEmitter {
  public width: number;
  public height: number;
  public aspect: number;
  public pixelRatio: number;

  constructor() {
    super();
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.aspect = this.width / this.height;
    this.pixelRatio = Math.min(window.devicePixelRatio, 2);

    window.addEventListener("resize", () => {
      this.width = window.innerWidth;
      this.height = window.innerHeight;
      this.aspect = this.width / this.height;
      this.pixelRatio = Math.min(window.devicePixelRatio, 2);
      this.emit("resize");
    });
  }
}

threejs/Events.ts

import {
  AxesHelper,
  Camera,
  EventDispatcher,
  GridHelper,
  Line,
  Object3D,
  Raycaster,
  Scene,
  Vector2,
} from "three";
import {
  TransformControlsGizmo,
  TransformControlsPlane,
} from "three/examples/jsm/controls/TransformControls";
import Base from "./Base";

export default class EventManger extends EventDispatcher {
  private base: Base;
  private raycaster: Raycaster = new Raycaster();
  private mouse: Vector2 = new Vector2();
  private dom: HTMLCanvasElement;
  private scene: Scene;
  private camera: Camera;
  constructor() {
    super();
    const base = Base.getInstance();
    this.base = base;

    this.dom = base.canvas;
    this.scene = base.scene;
    this.camera = base.cameras.perspectiveCamera;

    const mouse = this.mouse;
    const raycaster = this.raycaster;
    const dom = this.dom;

    let cacheObject: Object3D | null = null;

    dom.addEventListener("mousedown", (event) => {
      // 选取物体的操作
      raycaster.setFromCamera(mouse, this.camera);
      const intersection = raycaster.intersectObjects(this.scene.children);

      // 这里将事件派发到this上面,相当于注册全局事件,也就是将事件派发到EventManager的实例上面
      this.dispatchEvent({
        type: "mousedown",
        intersection,
      });

      if (intersection.length) {
        const object = intersection[0].object;
        // 派发事件到3d物体上面
        object.dispatchEvent({
          type: "mousedown",
        });
      }
    });

    dom.addEventListener("mousemove", (event) => {
      mouse.x = (event.offsetX / dom.offsetWidth) * 2 - 1;
      mouse.y = (-event.offsetY * 2) / dom.offsetHeight + 1;

      // 选取物体的操作
      raycaster.setFromCamera(mouse, this.camera);

      const intersection = raycaster.intersectObjects(this.scene.children);

      // 全局派发事件
      this.dispatchEvent({
        type: "mousemove",
        intersection,
      });

      if (intersection.length) {
        const object = intersection[0].object;

        if (object !== cacheObject) {
          if (cacheObject) {
            cacheObject.dispatchEvent({
              type: "mouseleave",
            });
          }
          object.dispatchEvent({
            type: "mouseenter",
          });
        } else if (object === cacheObject) {
          object.dispatchEvent({
            type: "mousemove",
          });
        }
        cacheObject = object;
      } else {
        if (cacheObject) {
          cacheObject.dispatchEvent({
            type: "mouseleave",
          });
        }
        cacheObject = null;
      }
    });

    dom.addEventListener("mouseup", (event) => {
      // 选取物体的操作
      raycaster.setFromCamera(mouse, this.camera);
      const intersection = raycaster.intersectObjects(this.scene.children);

      this.dispatchEvent({
        type: "mouseup",
        intersection,
      });
      if (intersection.length) {
        const object = intersection[0].object;
        object.dispatchEvent({
          type: "mouseup",
        });
      }
    });

    dom.addEventListener("click", (event) => {
      // 选取物体的操作
      raycaster.setFromCamera(mouse, this.camera);
      let intersections = raycaster.intersectObjects(this.scene.children);

      this.dispatchEvent({
        type: "click",
        intersections,
      });

      //移出变换控制器本身,并不想要其,会干扰
      this.scene.remove(...this.base.noTransfromControlObjs);
      intersections = raycaster.intersectObjects(this.scene.children);
      //使用完要给添加回去
      this.scene.add(...this.base.noTransfromControlObjs);

      const targetIntersection = intersections.filter((intersection) => {
        return (
          // 除了上面的移出添加方法,也可是使用过滤的方法,来过滤掉一些不需要的内容
          !(intersection.object instanceof TransformControlsPlane) &&
          !(intersection.object instanceof AxesHelper) &&
          !(intersection.object instanceof GridHelper) &&
          !(intersection.object instanceof Line) &&
          !(intersection.object.parent instanceof TransformControlsGizmo)
          //   !(intersection.object instanceof Line2) &&
        );
      });

      if (targetIntersection.length) {
        const object = targetIntersection[0].object;

        console.log(object);

        object.dispatchEvent({
          type: "click",
        });
      }
    });
  }
}

threejs/Renderer.ts

import * as THREE from "three";
import Base from "./Base";
import { Scene, WebGLRenderer } from "three";
import Camera from "./Cameras";
import Size from "./Size";
import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer";

export default class Renderer {
  private size: Size;
  private scene: Scene;
  private canvas: HTMLCanvasElement;
  private camera: Camera;
  private labelRenderer: CSS2DRenderer;
  private renderer: WebGLRenderer;

  constructor() {
    const base = Base.getInstance();

    this.size = base.size;
    this.scene = base.scene;
    this.canvas = base.canvas;
    this.camera = base.cameras;

    this.renderer = this.setRenderer();

    const labelRenderer = this.setLabelRenderer();
    this.labelRenderer = labelRenderer;
  }

  setRenderer() {
    const renderer = new WebGLRenderer({
      canvas: this.canvas,
      antialias: true,
    });

    renderer.physicallyCorrectLights = true;
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.toneMapping = THREE.CineonToneMapping;
    renderer.toneMappingExposure = 1.75;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setSize(this.size.width, this.size.height);
    renderer.setPixelRatio(this.size.pixelRatio);

    return renderer;
  }
  private setLabelRenderer() {
    const labelRenderer = new CSS2DRenderer();
    labelRenderer.setSize(this.size.width, this.size.height);
    document.body.appendChild(labelRenderer.domElement);
    return labelRenderer;
  }

  resize() {
    this.renderer.setSize(this.size.width, this.size.height);
    this.renderer.setPixelRatio(this.size.pixelRatio);
    // 重新渲染dom元素的画布
    this.labelRenderer.setSize(this.size.width, this.size.height);
  }

  update() {
    this.renderer.render(this.scene, this.camera.perspectiveCamera);
    this.labelRenderer.render(this.scene, this.camera.perspectiveCamera);
  }
}

threejs/Controls.ts

import {
  TransformControls,
  TransformControlsGizmo,
  TransformControlsPlane,
} from "three/examples/jsm/controls/TransformControls";
import Base from "./Base";
import {
  AxesHelper,
  GridHelper,
  Line,
  MOUSE,
  Object3D,
  PerspectiveCamera,
  Raycaster,
  Scene,
  Vector2,
} from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

export default class Control {
  public transformControl: TransformControls;
  public orbitControl: OrbitControls;
  private base: Base;
  private camera: PerspectiveCamera;
  private canvas: HTMLCanvasElement;
  private scene: Scene;
  private transing: boolean;
  constructor() {
    const base = Base.getInstance();
    this.base = base;

    this.camera = base.cameras.perspectiveCamera;
    this.canvas = base.canvas;
    this.scene = base.scene;

    // 正在变换的标志位
    this.transing = false;
    this.transformControl = this.createTransformControl();
    this.initTransformControlEvent();
    this.scene.add(this.transformControl);
    this.orbitControl = this.createOrbitControl();
  }

  private createTransformControl() {
    const transformControls = new TransformControls(this.camera, this.canvas);
    return transformControls;
  }

  private initTransformControlEvent() {
    this.transformControl.addEventListener("mouseDown", (e) => {
      this.transing = true;
    });
  }

  public update() {
    this.orbitControl.update();
  }

  private createOrbitControl() {
    const controls = new OrbitControls(this.camera, this.canvas);
    controls.enableDamping = true;
    controls.enableZoom = true;
    // 取消鼠标控制器的鼠标左键旋转功能
    // controls.mouseButtons = {
    //   LEFT: null as unknown as MOUSE,
    //   MIDDLE: MOUSE.DOLLY,
    //   RIGHT: MOUSE.PAN,
    // };

    return controls;
  }
}


threejs/Time.ts

import EventEmitter from "eventemitter2";
import { Clock } from "three";

export default class Time extends EventEmitter {
  public clock: Clock;
  constructor() {
    super();
    this.clock = new Clock();

    this.update();
  }

  update() {
    this.emit("update");
    window.requestAnimationFrame(() => this.update());
  }
}

threejs/World/Box.ts

import {
  BoxGeometry,
  MeshBasicMaterial,
  Mesh,
  MeshLambertMaterial,
  Color,
} from "three";
import Base from "../Base";

export default class Box {
  private cube: Mesh;
  private cubeMaterial: MeshBasicMaterial;

  constructor() {
    const base = Base.getInstance();
    this.cubeMaterial = new MeshBasicMaterial({ color: 0x00ff00 });
    const cube = this.createCube();
    this.cube = cube;
    base.scene.add(cube);
  }
  private createCube() {
    const cube = new Mesh(new BoxGeometry(1, 1, 1), this.cubeMaterial);
    cube.position.y = 0.5;
    return cube;
  }
}