likes
comments
collection
share

基于vue3.0 + Threejs实现炫酷3D网页

作者站长头像
站长
· 阅读数 5

介绍

我的位置: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 动画可以达到以下效果:

基于vue3.0 + Threejs实现炫酷3D网页

三维效果实现

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

使用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);
};
基于vue3.0 + Threejs实现炫酷3D网页

模型加载

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

基于vue3.0 + Threejs实现炫酷3D网页

使用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++) {
      if (
        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);
  });
};
基于vue3.0 + Threejs实现炫酷3D网页

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

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

// 滚动时相机和模型动画
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函数,可以达到以下效果:

基于vue3.0 + Threejs实现炫酷3D网页

模型探索与退出探索

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

退出探索则是把相机模型位置设置回第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,
  });
};

得到当前如下效果:

基于vue3.0 + Threejs实现炫酷3D网页

给模型添加标点

在开发时我在项目中写了一个方法利用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;
    }
  }
}

效果如下图:

基于vue3.0 + Threejs实现炫酷3D网页

建筑遮挡隐藏标点

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

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

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

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

基于vue3.0 + Threejs实现炫酷3D网页
// 判断模型是否遮挡精灵
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;
        },
      });
    }
  }
};
基于vue3.0 + Threejs实现炫酷3D网页

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

在上面的标点数据中已经存在了信息框数据,这边主要是操作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);
基于vue3.0 + Threejs实现炫酷3D网页

点击前往第二场景

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

// 点击前往第二个场景
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";
        },
      });
    }
  });
};
基于vue3.0 + Threejs实现炫酷3D网页

完整代码

查看完整代码请移步

资源文件

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

总结

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

转载自:https://juejin.cn/post/7239744255674105913
评论
请登录