添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

我的位置: ljn1998codeing.love

该网页是我模仿一位国外大佬的网页实现,网页功能大概模仿实现了 7 成,如果想看原网页效果可以前往 传送门

下面简单聊聊在该 demo 中我做了什么以及各位能学到什么:

  • 二维方面实现了网页滚动翻页和元素展示动画
  • 三维方面实现了天空图、相机模型位置根据滚动位置变化
  • 大模型预览
  • 使用精灵实现标点
  • 精灵标点被模型遮挡隐藏
  • 鼠标移到标点处弹出提示框
  • 点击标点进入下一场景
  • 演示地址(在 github 上有点卡,请见谅)

    二维实现滚动翻页和元素展示动画

    // 页面滚动数据
    const pageScrollingData = reactive({
      scrollviewHeight: 0, // 滚动视图高度
      pageHeight: 0, // 每页高度
      totalPage: 5, // 总页数
      currentPage: 1, // 当前页面
      isScrolling: false, // 是否正在滚动
      scrollPos: 0, // 滚轮滚动位置
      ending: false, // 是否滚动到底部
    // 控制页面元素数据
    const elementStatus = reactive({
      pageOnetitle: false,
      pageOneStart: false,
      pageTwoText: false,
      pageThreeLeftImage: false,
      pageThreeHeader: false,
      pageThreeRightText: false,
      pageFourALeftText: false,
      pageFourArightText: false,
      quitButton: false,
    // 初始化滚动视图数据
    const initScrollViewData = (): void => {
      // 每一页高度 = 浏览器窗口viewport的高度
      pageScrollingData.pageHeight = window.innerHeight;
      // 滚动视图总高度 = 每页高度 * 总页数
      pageScrollingData.scrollviewHeight = pageScrollingData.pageHeight * pageScrollingData.totalPage;
    // 鼠标滚轮滚动控制
    const mouseWheelHandle = (event: any): void | boolean => {
      const evt = event || window.event;
      // 阻止默认事件
      if (evt.stopPropagation) {
        evt.stopPropagation();
      } else {
        evt.returnValue = false;
      // 当前正在滚动中则不做任何操作
      if (pageScrollingData.isScrolling) {
        return false;
      const e = event.originalEvent || event;
      // 记录滚动位置
      pageScrollingData.scrollPos = e.deltaY || e.detail;
      if (pageScrollingData.scrollPos > 0) { // 当鼠标滚轮向上滚动时
        pageTurning(true);
      } else if (pageScrollingData.scrollPos < 0) { // 当鼠标滚轮向下滚动时
        pageTurning(false);
    // 页面移动方向处理
    const pageTurning = (direction: boolean): void => {
      if (direction) {
        // 往上滚动时,判断当前页码 + 1 是否 <= 总页码 ?? 页码 + 1,执行页面滚动操作,
        if (pageScrollingData.currentPage + 1 <= pageScrollingData.totalPage) {
          pageScrollingData.currentPage += 1;
          pageMove(pageScrollingData.currentPage);
      } else {
        // 同样往下滚动时,判断当前页码 - 1 是否 > 0 ?? 页码 - 1,执行页面滚动操作
        if (pageScrollingData.currentPage - 1 > 0) {
          pageScrollingData.currentPage -= 1;
          pageMove(pageScrollingData.currentPage);
    // 页面滚动
    const pageMove = (pageNo: number): void => {
      // 设置滚动状态
      pageScrollingData.isScrolling = true;
      // 计算滚动高度
      const scrollHeight = -(pageNo - 1) * pageScrollingData.pageHeight + "px";
      // 设置css样式
      scrollview.value.style.transform = `translateY(${scrollHeight})`;
      // 重新设置下当前页码
      pageScrollingData.currentPage = pageNo;
      handingElementshow();
      // 定时器做一个防抖,避免一秒内多次触发
      setTimeout(() => {
        pageScrollingData.isScrolling = false;
      }, 1500);
    // 处理元素出现或隐藏
    const handingElementshow = (): void => {
      setTimeout(() => {
        switch (pageScrollingData.currentPage) {
          case 1:
            elementStatus.pageOnetitle = true;
            elementStatus.pageOneStart = true;
            elementStatus.pageTwoText = false;
            break;
          case 2:
            elementStatus.pageOnetitle = false;
            elementStatus.pageOneStart = false;
            elementStatus.pageTwoText = true;
            elementStatus.pageThreeLeftImage = false;
            elementStatus.pageThreeHeader = false;
            elementStatus.pageThreeRightText = false;
            break;
          case 3:
            elementStatus.pageTwoText = false;
            elementStatus.pageThreeLeftImage = true;
            elementStatus.pageThreeHeader = true;
            elementStatus.pageThreeRightText = true;
            elementStatus.pageFourALeftText = false;
            elementStatus.pageFourArightText = false;
            break;
          case 4:
            elementStatus.pageThreeLeftImage = false;
            elementStatus.pageThreeHeader = false;
            elementStatus.pageThreeRightText = false;
            elementStatus.pageFourALeftText = true;
            elementStatus.pageFourArightText = true;
            break;
          case 5:
            elementStatus.pageFourALeftText = false;
            elementStatus.pageFourArightText = false;
            break;
      }, 1000);
    

    配合 CSS3 动画可以达到以下效果:

    三维效果实现

    初始化场景、相机、渲染器、灯光

    使用three.js里面的CubeTextureLoader渲染一个天空图

    // 初始化场景
    const initScene = (): void => {
      scene = new THREE.Scene();
      // 天空图图片集合,指定顺序pos-x, neg-x, pos-y, neg-y, pos-z, neg-z
      const skyBg = [
        getAssetsFile("sky/px.jpg"),
        getAssetsFile("sky/nx.jpg"),
        getAssetsFile("sky/py.jpg"),
        getAssetsFile("sky/ny.jpg"),
        getAssetsFile("sky/pz.jpg"),
        getAssetsFile("sky/nz.jpg"),
      const cubeLoader: THREE.CubeTextureLoader = new THREE.CubeTextureLoader();
      skyEnvMap = cubeLoader.load(skyBg);
      // 设置场景背景
      scene.background = skyEnvMap;
    // 初始化相机
    const initCamera = (width: number, height: number): void => {
      camera = new THREE.PerspectiveCamera(cameraFov, width / height, 1, 1000);
      cameraPostion = new THREE.Vector3(0, -13, 48);
      camera.position.copy(cameraPostion);
      scene.add(camera);
    // 初始化渲染器
    const initRenderer = (width: number, height: number): void => {
      renderer = new THREE.WebGLRenderer({
        antialias: true, // 抗锯齿
      renderer.setSize(width, height);
      // 指定输出编码格式,当设置renderer.outputEncoding为sRGBEncoding时,渲染器会将输出的颜色值转换为sRGB格式,以便正确呈现在屏幕上
      renderer.outputEncoding = THREE.sRGBEncoding;
      canvas.value.appendChild(renderer.domElement);
      renderer.render(scene, camera);
    // 初始化灯光
    const initLight = (): void => {
      // 环境光
      const ambientLight: THREE.AmbientLight = new THREE.AmbientLight(
        new THREE.Color("rgb(255, 255, 255)")
      // 平行光
      const directionalLight: THREE.DirectionalLight = new THREE.DirectionalLight(
        new THREE.Color("rgb(255, 99, 71)"),
        2 // 光照强度为2
      directionalLight.position.set(-220, 30, 50);
      scene.add(ambientLight, directionalLight);
    

    模型加载需要使用three.js里面的DRACOLoaderGLTFLoader两个类,需要从node_modules中把draco拷贝出来放到项目的public目录中

    使用DRACOLoader是因为glTF 模型使用了DRACO压缩算法进行了压缩

    DRACO是一种用于压缩 3D 几何数据的算法,它可以将 3D 模型文件的大小减小到原来的 10%到 30%之间,从而提高加载和渲染速度。在使用DRACO进行压缩后,模型文件将被转换为DRACO 格式,这意味着Three.js需要使用DRACOLoader来读取和解压缩模型文件。

    const dracoLoader: DRACOLoader = new DRACOLoader();
    dracoLoader.setDecoderPath("draco/");
    dracoLoader.preload();
    const gltfLoader: GLTFLoader = new GLTFLoader();
    gltfLoader.setDRACOLoader(dracoLoader);
    // 加载建筑模型
    const loadBuildingModel = (): void => {
      gltfLoader.load(getAssetsFile("building/building.glb"), (gltf) => {
        // 保存模型初始位置
        originalModelPos.value = new THREE.Vector3(14, -40.8, 0);
        // 设置模型位置
        gltf.scene.position.copy(originalModelPos.value);
        // 设置模型旋转角度
        const currentRotation = gltf.scene.rotation.clone();
        const newRotation = new THREE.Euler(
          currentRotation.x,
          currentRotation.y - (131 * Math.PI) / 180,
          currentRotation.z,
          currentRotation.order
        gltf.scene.rotation.copy(newRotation);
        // 循环模型内Mesh并找到窗户所属的Mesh,设置该Mesh中材质的环境贴图以及环境贴图的强度
        const ObjectGroup = gltf.scene.children;
        for (let i = 0; i < ObjectGroup.length; i++) {
            ObjectGroup[i] instanceof THREE.Group &&
            ObjectGroup[i].name === "AB1_OBJ_02"
            ObjectGroup[i].children &&
              ObjectGroup[i]
    
    
    
    
        
    .children.forEach((item) => {
                if (item instanceof THREE.Mesh && item.name === "AB1_OBJ_02_1") {
                  item.material.envMap = skyEnvMap;
                  item.material.envMapIntensity = 0.5;
        // 保存模型数据,后面设置动画会直接使用到
        buildingModel = gltf.scene;
        scene.add(buildingModel);
    

    模型动画,模型根据页面滚动设置动画

    在切换页面的同时,我们需要让模型做出相应动画来进行滚动交互

    // 滚动时相机和模型动画
    const handingScrolling = (): void => {
      // 判断是否滚动到最后一页,因为第3、4页模型的位置是不需要改变,也就是没有相对应地模型动画,所以当前页面是最后一页时,那么只能玩上滚动,并且需要执行第二页的模型动画
      const pos = pageScrollingData.ending ? 2 - 1 : pageScrollingData.currentPage - 1;
      // 计算新的模型位置
      const newModelPos: THREE.Vector3 = originalModelPos.value && originalModelPos.value.clone().add(new THREE.Vector3(pos * 10, pos * 8.6, pos * 13));
      // 当前为第一页时,模型位置设置为初始值
      if (pageScrollingData.currentPage === 1) {
        newModelPos.copy(originalModelPos.value);
      if (pageScrollingData.currentPage <= 2 || pageScrollingData.ending) { // 当前页码 <= 第2页时 或者 页面滚动到最底部,执行该动画
        gsap.to(camera.position, {
          x: pos * 18,
          y: cameraPostion.y + pos * 14,
          ease: "Power2.inOut",
          duration: 1,
        gsap.to(buildingModel.position, {
          x: newModelPos.x,
          y: newModelPos.y,
          z: newModelPos.z,
          ease: "Power2.inOut",
          duration: 1,
        pageScrollingData.ending = false;
      } else if (pageScrollingData.currentPage === 5) { // 当前页码 === 第5页时,执行该动画
        gsap.to(camera.position, {
          x: -24,
          y: -30,
          ease: "Power2.inOut",
          duration: 1,
        gsap.to(buildingModel.position, {
          x: -6,
          y: -59,
          z: 18,
          ease: "Power2.inOut",
          duration: 1,
        pageScrollingData.ending = true;
      // 控制页面元素显示隐藏
      handingElementshow();
    

    配合上面的pageMove函数,可以达到以下效果:

    模型探索与退出探索

    模型探索所做的操作就是将页面三维容器层级设置到最高,同时设置相机和模型的动画,并开启控制器交互

    退出探索则是把相机模型位置设置回第5页时的状态,并且把控制器属性设置回原来的状态

    // 探索模型
    const explorarModel = (): void => {
      // 设置三维容器层级
      canvas.value.style.zIndex = 1;
      // 相机动画改变相机位置
      const cameraGasp: gsap.core.Tween = gsap.to(camera.position, {
        x: -6,
        y: 6,
        z: 80,
        ease: "Power0.inOut",
        duration: 2,
      // 模型动画改变模型位置
      const buildingGasp: gsap.core.Tween = gsap.to(buildingModel.position, {
        x: 0,
        y: -22,
        z: 0,
        ease: "Power0.inOut",
        duration: 2,
      // 等待执行
      const delayedCall: Promise<unknown> = new Promise((resolve) => {
        gsap.delayedCall(1, resolve);
      // 当所有动画执行完成时的操作
      Promise.all([cameraGasp, buildingGasp, delayedCall])
        .then(() => {
          elementStatus.quitButton = true; // 展示退出探索按钮
          controls.enabled = true; // 开启控制器交互
          controls.maxPolarAngle = Math.PI / 2 - 0.01; // 设置垂直旋转的角度的上限
          controls.autoRotate = true; // 开启自动旋转
          controls.minDistance = 40; // 设置相机向内移动上限
          controls.maxDistance = 86; // 设置相机向外移动上限
        .catch((err) => {
          console.log(err);
    // 退出探索模型
    const quitExporarModel = (key: number): void => {
      // 移除标点
      scene.remove(pointGroup);
      // 设置三维容器层级
      canvas.value.style.zIndex = -1;
      // 隐藏退出按钮
      elementStatus.quitButton = false;
      // 把控制器一些参数设置回初始值
      controls.maxPolarAngle = Math.PI;
      controls.enabled = false;
      controls.autoRotate = false;
      controls.minDistance = 0;
      controls.maxDistance = Infinity;
      // 执行动画操作
      gsap.to(camera.position, {
        x: -24,
        y: -30,
        z: 48,
        ease: "Power0.inOut",
        duration: 1,
      gsap.to(buildingModel.position, {
        x: -6,
        y: -59,
        z: 18,
        ease: "Power0.inOut",
        duration: 1,
      gsap.to(controls.target, {
        x: 0,
        y: 0,
        z: 0,
        ease: "Power0.inOut",
        duration: 1,
    

    得到当前如下效果:

    给模型添加标点

    在开发时我在项目中写了一个方法利用three.js中的Raycaster类拾取了3个坐标,下面直接看方法

    // 给模型添加标点
    const addPointWithModel = (): void => {
      // 标点数据
      const pointArr: PointType[] = [
          x: -16.979381448617573,
          y: -19.167911412787436,
          z: 1.4417293738365617,
          text: "aaaaa",
          x: 4.368890112320235,
          y: -12.020210823358955,
          z: 10.590562296036955,
          text: "bbbbb",
          x: -4.655517564465063,
          y: 12.146541899849993,
          z: 11.879293977258593,
          ware: true, // 是否展示涟漪动画
          otherScene: true, // 是否可以前往下一个场景
          text: "ccccc",  // 弹框展示的文字
      // 贴图加载
      const circleTexture: THREE.Texture = textureLoader.load(
        getAssetsFile("building/sprite.png")
      const waveTexture: THREE.Texture = textureLoader.load(
        getAssetsFile("wave.png")
      // 遍历标点数据创建精灵标点
      pointArr.forEach((item: PointType) => {
        const spriteMaterial: THREE.SpriteMaterial = new THREE.SpriteMaterial({
          map: circleTexture,
        const sprite: THREE.Sprite & Info = new THREE.Sprite(spriteMaterial);
        sprite.name = "point";
        sprite.text = item.text;
        sprite.otherScene = item.otherScene;
        sprite.position.set(item.x, item.y + 0.2, item.z + 2);
        sprite.scale.set(1.4, 1.4, 1);
        // 需要涟漪动画则要创建一个涟漪精灵
        if (item.ware) {
          const waveMaterial: THREE.SpriteMaterial = new THREE.SpriteMaterial({
            map: waveTexture,
            color: new THREE.Color("rgb(255, 255, 255)"),
            transparent: true,
            opacity: 1.0,
            side: THREE.DoubleSide,
            depthWrite: false,
          let waveSprite: THREE.Sprite & Info = new THREE.Sprite(waveMaterial);
          waveSprite.name = "wave";
          waveSprite.text = item.text;
          waveSprite.otherScene = item.otherScene;
          waveSprite.size = 8 * 0.3;
          waveSprite._s = Math.random() * 1.0 + 1.0;
          waveSprite.position.set(item.x, item.y + 0.2, item.z + 2);
          pointGroup.add(waveSprite);
        pointGroup.add(sprite);
      scene.add(pointGroup);
    

    render函数中我们需要添加如下代码,来实现涟漪动画

    // 涟漪动画
    const pointGroup = scene.children.find((item) => item.name === "pointGroup"); // 查找标点组合
    if (pointGroup) { // 组合存在
      const wave: any = pointGroup.children.length && pointGroup.children.find((sprite) => sprite.name === "wave"); // 找到涟漪精灵
      if (wave) {
        // 修改精灵的大小和材质的透明度达到涟漪的效果
        wave._s += 0.01;
        wave.scale.set(
          wave.size * wave._s,
          wave.size * wave._s,
          wave.size * wave._s
        if (wave._s <= 1.5) {
          //mesh._s=1,透明度=0 mesh._s=1.5,透明度=1
          wave.material.opacity = (wave._s - 1) * 2;
        } else if (wave._s > 1.5 && wave._s <= 2) {
          //mesh._s=1.5,透明度=1 mesh._s=2,透明度=0
          wave.material.opacity = 1 - (wave._s - 1.5) * 2;
        } else {
          wave._s = 1.0;
    

    效果如下图:

    建筑遮挡隐藏标点

    当建筑遮挡标点时,因为精灵是一个总是面朝着摄像机的平面,所以即便被建筑遮挡,射线依旧能选中这个标点,这不利于后面的功能

    three.js中,Raycaster可以用于检测鼠标或者屏幕上某个点是否与场景中的物体相交

    Raycaster的原理是基于3D空间中的射线投射,它会从一个起点(例如相机位置)发出一条射线,直到它与场景中的某个物体相交。Raycaster并不会遮挡检测,但是通过检测物体与射线相交的顺序,可以判断它们之间是否存在遮挡关系

    在这个功能中,我们把标点作为射线的起点,相机为终点,当射线检测到的对象是不是精灵标点sprite,则隐藏标点,具体原理如下图

    // 判断模型是否遮挡精灵
    const spriteVisible = (): void => {
      // 创建一个Raycaster对象
      const raycaster = new THREE.Raycaster();
      raycaster.camera = camera;
      // 精灵标点集合
      const spriteArr: THREE.Object3D<THREE.Event>[] = [];
      pointGroup.children.forEach((sprite) => {
        spriteArr.push(sprite);
      for (let i = 0; i < spriteArr.length; i++) {
        const sprite: THREE.Object3D<THREE.Event> = spriteArr[i];
        // 将Sprite的位置作为射线的起点
        // 创建一个新的 Vector3 对象,然后使用 setFromMatrixPosition 方法将该对象设置为 Sprite 对象在世界坐标系下的位置
        // 最终得到一个 Vector3 对象,表示了 Sprite 对象在世界坐标系下的位置。这个位置可以用于计算精灵与相机的相对位置,或者用于计算精灵的旋转方向
        const spritePosition: THREE.Vector3 = new THREE.Vector3().setFromMatrixPosition(
          sprite.matrixWorld
        const rayOrigin: THREE.Vector3 = spritePosition.clone();
        // 将摄像机位置作为射线的终点
        const cameraPosition: THREE.Vector3 = new THREE.Vector3().setFromMatrixPosition(
          camera.matrixWorld
        // 计算spritePosition指向cameraPosition的单位向量代码
        // ameraPosition.clone() 将 cameraPosition 对象进行克隆,得到一个新的 Vector3 对象。这么做是为了避免修改原始的 cameraPosition 对象
        // sub(spritePosition) 将 spritePosition 对象从上一步得到的新的 Vector3 对象中减去,得到一个指向 spritePosition 的向量
        // normalize():将上一步得到的指向 spritePosition 的向量进行标准化,得到一个单位向量,即长度为 1 的向量
        const rayDirection: THREE.Vector3 = cameraPosition.clone().sub(spritePosition).normalize();
        // 设置射线的起点和方向
        raycaster.set(rayOrigin, rayDirection);
        // 检查是否存在与Sprite相交的物体
        const intersects = raycaster.intersectObjects(buildingModel.children, true);
        let isOccluded = false;
        for (let j = 0; j < intersects.length; j++) {
          const intersection = intersects[j];
          const object = intersection.object;
          if (object !== sprite && object.name !== "Plane") {
            // 当前相交对象不是Sprite,那Sprite被遮挡了
            isOccluded = true;
            break;
        // 如果Sprite被遮挡了,将其隐藏,因为不能直接用gasp操作sprite.visible属性,所以只能改变opacity属性,并且当执行完成时需要隐藏精灵,要不然射线还会选到
        if (isOccluded) {
          gsap.to((sprite as THREE.Sprite).material, {
            opacity: 0,
            ease: "Power0.inOut",
            duration: 0.5,
            onComplete: () => {
              sprite.visible = false;
        } else {
          gsap.to((sprite as THREE.Sprite).material, {
            opacity: 1,
            ease: "Power0.inOut",
            duration: 0.5,
            onComplete: () => {
              sprite.visible = true;
    

    鼠标移到标点出弹出信息框

    在上面的标点数据中已经存在了信息框数据,这边主要是操作document元素来创建或移除元素,然后监听鼠标的移动事件,配合Raycaster射线拾取来实现

    // 检测鼠标与模型标点相交
    const detectionMouseIntersectPoint = (event: any): void => {
      if (!elementStatus.quitButton) return;
      // 创建射线
      const raycaster = new THREE.Raycaster();
      // 将终点设置为固定的点
      const rayEndpoint = new THREE.Vector3(0, 0, 0);
      // 创建鼠标向量
      const mouse = new THREE.Vector2();
      // 计算鼠标点击位置的归一化设备坐标(NDC)
      // NDC 坐标系的范围是 [-1, 1],左下角为 (-1, -1),右上角为 (1, 1)
      if (!canvas.value) return;
      mouse.x = (event.clientX / canvas.value.clientWidth) * 2 - 1;
      mouse.y = -(event.clientY / canvas.value.clientHeight) * 2 + 1;
      // 更新射线的起点和方向
      raycaster.setFromCamera(mouse, camera);
      // 将终点设置为距离相机100的位置
      raycaster.ray.at(100, rayEndpoint);
      // 计算射线与场景中的所有标点相交
      const intersects = raycaster.intersectObjects(pointGroup.children, true);
      // 如果存在相交点,则获取第一个相交点的坐标
      if (intersects.length > 0) {
        const object: NewObject3d = intersects[0].object;
        // 获取标点在屏幕上的位置
        const point = new THREE.Vector3().copy(object.position);
        // 标点从三维空间投影到二维屏幕上
        point.project(camera);
        // 判断下如果标点是隐藏状态就不做任何操作
        if(!object.visible) return
        addTipElementOrRemove(object, point, true);
      } else {
        if (isClick) return;
        addTipElementOrRemove(null, null, false);
    // 添加或移除提示信息框
    const addTipElementOrRemove = (
      object: NewObject3d | null, // 鼠标拾取到的对象
      point: THREE.Vector3 | null, // 对象在屏幕上的位置
      status: boolean // 状态 添加true  移除false
    ): void => {
      // 获取文档中ID为tooltip的元素
      const tooltipElement: HTMLElement | null = document.getElementById("tooltip");
      // 状态是true并且元素已存在,就不再执行添加操作
      if (status && tooltipElement) return;
      // 状态是true并且元素不存在执行添加操作
      if (!tooltipElement && status) {
        const tooltipDiv: HTMLElement = document.createElement("div");
        tooltipDiv.innerHTML = (object && object.text) || "";
        tooltipDiv.setAttribute("id", "tooltip");
        tooltipDiv.style.position = "absolute";
        tooltipDiv.style.left = `${
          point && ((point.x + 1) * canvas.value.clientWidth) / 2 + 10
        }px`;
        tooltipDiv.style.top = `${
          point && ((-point.y + 1) * canvas.value.clientHeight) / 2 + 10
        }px`;
        tooltipDiv.style.zIndex = "100";
        tooltipDiv.style.padding = "4px 6px";
        tooltipDiv.style.fontSize = "12px";
        tooltipDiv.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
        tooltipDiv.style.border = "1px solid #ffffff";
        tooltipDiv.style.borderRadius = "6px";
        canvas.value.appendChild(tooltipDiv);
      } else {
        // 状态为false并且元素存在执行移除操作
        if (!status && tooltipElement) {
          canvas.value.removeChild(tooltipElement);
    // 监听鼠标移动事件
    window.addEventListener("mousemove", detectionMouseIntersectPoint, false);
    

    点击前往第二场景

    前往第二场景,主要操作是通过修改相机位置来实现,下面直接看代码

    // 点击前往第二个场景
    const goOtherScene = (object: NewObject3d): void => {
      // 设置控制器属性
      controls.enabled = false;
      controls.enableZoom = false;
      controls.autoRotate = false;
      controls.minDistance = 0;
      controls.maxDistance = Infinity;
      // 遍历建筑模型,找到第二场景的位置
      buildingModel.traverse((child) => {
        if (child.name === "Area002") {
          const newPosition = new THREE.Vector3();
          child.updateMatrixWorld();
          newPosition.setFromMatrixPosition(child.matrixWorld);
          // 设置controls的中心点
          controls.target.set(newPosition.x, newPosition.y, newPosition.z);
          elementStatus.quitButton = false;
          // 相机动画
          gsap.to(camera.position, {
            x: newPosition.x - 4,
            y: newPosition.y + 2,
            z: newPosition.z,
            ease: "Power0.inOut",
            duration: 1,
            onUpdate: () => {
              // 设置相机的广角
              if (cameraFov < 50) {
                cameraFov += 1;
                camera.fov = cameraFov;
                camera.updateProjectionMatrix();
            onComplete: () => {
              controls.enabled = true;
              elementStatus.quitButton = true;
              buttonText.key = 2;
              buttonText.value = "OUT";
    

    查看完整代码请移步

    链接: pan.baidu.com/s/1BJCOx2CS… 提取码: 7xc6

    本文暂时就到这了,在文章中我贴了大量的代码加上注释以及效果图,在需要讲解的地方我也加上了自己的理解,希望大家能看明白,不明白之处或者觉得处理的不好的地方可以评论区留言,期待和各位大佬的交流😊

    原文链接:https://juejin.cn/post/7239744255674105913 作者:半个糯米鸡