乐趣区

关于three.js:Threejs-进阶之旅全景漫游初阶移动相机版

申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。

摘要

3D 全景技术能够实现日常生活中的很多性能需要,比方地图的街景全景模式、数字展厅、在线看房、社交媒体的全景图预览、短视频直播平台的全景直播等。Three.js 实现全景性能也是非常不便的,当然了目前曾经有很多相干内容的文章,我之前就写过一篇《Three.js 实现 3D 全景侦探小游戏》。因而本文内容及此专栏下一篇文章探讨的重点不是如何实现 3D 全景图性能,而是 如何一步步优雅实现在多个 3D 全景中穿梭漫游,达到如在真实世界中后退后退的视觉效果

全景漫游系列文章将分为 高低两篇,本篇内容咱们先介绍如何通过挪动相机的办法来达到场景切换的目标。通过本文的学习,你将学到的知识点包含:在 Three.js 中创立全景图的几种形式、在 3D 全景图中增加交互热点、利用 Tween.js 实现相机切换动画、多个全景图之间的切换等。

成果

本文最终将实现如下的成果,左右管制鼠标旋转屏幕能够预览室内三维全景图,同时全景图内有多个交互热点,它们标识着三维场景内的一些物体,比方沙发 🛋、电视机 📺 等,交互热点会随着场景的旋转而旋转,点击热点 🔘 能够弹出交互反馈提示框。

点击屏幕上有其余场景名称的按钮比方 客厅 卧室 书房 时,能够从以后场景切换到指标场景全景图,交互热点也会同时切换。

关上以下链接,在线预览成果,大屏拜访成果更佳。

  • 👁‍🗨 在线预览地址:https://dragonir.github.io/panorama-basic/

本专栏系列代码托管在 Github 仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

原理

咱们先来简略总结下在 Three.js 中实现三维全景性能的有哪些形式:

球体

在球体内增加 HDR 全景照片能够实现三维全景性能,全景照片是一张用球形相机拍摄的图片,如下图所示:

const geometry = new THREE.SphereGeometry(500, 60, 40);
geometry.scale(- 1, 1, 1);
const texture = new THREE.TextureLoader().load( 'textures/hdr.jpg');
const material = new THREE.MeshBasicMaterial({map: texture});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

🔗 球体全景图 Three.js 官网示例

立方体

在立方体内增加全景图贴图的形式也能够实现三维全景图性能,此时须要对 HDR 全景照片进行裁切,宰割成 6 张来别离对应立方体的 6 个面。

const textures = cubeTextureLoader.load([
  '/textures/px.jpg',
  '/textures/nx.jpg',
  '/textures/py.jpg',
  '/textures/ny.jpg',
  '/textures/pz.jpg',
  '/textures/nz.jpg'
]);

const materials = [];
for (let i = 0; i < 6; i ++) {materials.push( new THREE.MeshBasicMaterial( { map: textures[ i] } ) );
}
const skyBox = new THREE.Mesh(new THREE.BoxGeometry( 1, 1, 1), materials );
skyBox.geometry.scale(1, 1, - 1);
scene.add(skyBox);

🔗 立方体全景图 Three.js 官网示例

环境贴图

应用环境贴图也能够实现全景图性能,像上面这样加载全景图片,而后将它赋值给 scene.backgroundscene.environment 即可:

const environmentMap = cubeTextureLoader.load([
    '/textures/px.jpg',
    '/textures/nx.jpg',
    '/textures/py.jpg',
    '/textures/ny.jpg',
    '/textures/pz.jpg',
    '/textures/nz.jpg'
]);
environmentMap.encoding = THREE.sRGBEncoding;
scene.background = environmentMap;
scene.environment = environmentMap;

🔗 具体原理和实现形式就不具体介绍了,可查看我往期的文章《Three.js 进阶之旅:多媒体利用 -3D Iphone》,环境贴图段落中有具体实现介绍。

其余

除了应用 Three.js 本人实现全景图性能之外,也有一些其余性能齐备的全景图库能够很不便的实现三维全景场景,比方上面几个就比拟不错,其中后两个是 GUI 客户端,能够在客户端内十分不便的在全景图上增加交互热点、实现多个场景的漫游门路等,大家感兴趣的话都能够试试。

  • panolens.js
  • pannellum
  • Photo-Sphere-Viewer
  • krpano
  • Pano2VR

工具

全景图生成工具

  • 应用球形全景相机拍摄。
  • 应用 Blender 等建模软件相机 360 度旋转渲染。

全景图编辑工具

上面两个网站提供丰盛的三维全景背景照片及将 hdr 图片裁切成上述须要的 6 张贴图的能力,大家能够按本人须要下载和编辑。

🔗 HDR 全景背景照片下载网站:polyhaven

🔗 HDR 立方体材质转换工具:HDRI-to-CubeMap

实现

当初,咱们应用第一种球体 全景图的形式,来实现示例中介绍的内容。

〇 场景初始化

创立全景图前先做一些惯例三维场景筹备工作,因为三维全景图性能并不会波及到新的技术点,因而像上面这样简略实现就能够。

<canvas class="webgl"></canvas>

在文件顶部引入以下资源,其中 OrbitControls 用于旋转全景图时的镜头鼠标管制;TWEEN 用于创立流程的场景切换动画,Animations 是应用 TWEEN 来管制摄像机和控制器切换的办法的封装,能够疾速实现镜头的丝滑切换;rooms 是自定义的一个数组,用来保留多个全景图的信息。

import * as THREE from 'three';
import {OrbitControls} from '@/utils/OrbitControls.js';
import {TWEEN} from 'three/examples/jsm/libs/tween.module.min.js';
import Animations from '@/utils/animations';
import {rooms} from '@/views/home/data';

而后初始化渲染器、场景、相机、控制器、页面缩放适配、页面重绘动画等。

const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({canvas});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// 初始化场景
const scene = new THREE.Scene();

// 初始化相机
const camera = new THREE.PerspectiveCamera(65, sizes.width / sizes.height, 0.1, 1000);
camera.position.z = data.cameraZAxis;
scene.add(camera);

// 镜头控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);

// 页面缩放监听
window.addEventListener('resize', () => {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;
  // 更新渲染
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  // 更新相机
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();});

// 动画
const tick = () => {controls && controls.update();
  TWEEN && TWEEN.update();
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

① 创立一个球体

当初,像上面这样,咱们往场景中增加一个三维球体 ,作为第一个全景图的载体。其中 THREE.SphereGeometry(radius, segmentsWidth, segmentsHeight, phiStart, phiLength, thetaStart, thetaLength) 接管 7 个参数,咱们应用前 3 个参数半径、经度上的面数切片数、纬度上的切片数即可,数值可按本人的需要自行调整。

const geometry = new THREE.SphereGeometry(16, 256, 256);
const material = new THREE.MeshBasicMaterial({color: 0xffffff,});
const room = new THREE.Mesh(geometry, material);
scene.add(room);

② 创立全景图

当初咱们对球体进行全景图片贴图,并将 side 属性设置为 THREE.DoubleSide 或者 THREE.BackSide 而后通过设置 geometry.scale(1, 1, -1) 将球体内外翻转,就能失去上面所示的成果。

const geometry = new THREE.SphereGeometry(16, 256, 256);
const material = new THREE.MeshBasicMaterial({map: textLoader.load(map),
  side: THREE.DoubleSide,
});
geometry.scale(1, 1, -1);
const room = new THREE.Mesh(geometry, material);

此时,咱们通过鼠标放大球体,进入到 球体外部,上下左右旋转球体,就能察看到全景成果了。

③ 创立其余场景的全景图

对于数量较少,简略的场景咱们能够创立多个球体全景图来实现,这种形式尽管轻便,然而管制多个场景很不便,代码也非常容易了解,下篇文章将通过 另一种更优雅的形式 来实现多个全景图场景,以适应更加简单的需要。

咱们先对创立球体 全景图的办法加以封装,通过 createRoom 办法批量创立多个全景图场景,它接管的名称 name、地位 position 以及 贴图 map 三个参数是通过上述引入的 rooms 数值配置的。

const createRoom = (name, position, map) => {const geometry = new THREE.SphereGeometry(16, 256, 256);
  geometry.scale(1, 1, -1);
  const material = new THREE.MeshBasicMaterial({map: textLoader.load(map),
    side: THREE.DoubleSide,
  });
  const room = new THREE.Mesh(geometry, material);
  room.name = name;
  room.position.set(position.x, position.y, position.z);
  room.rotation.y = Math.PI / 2;
  scene.add(room);
  return room;
};

// 批量创立
rooms.map((item) => {const room = createRoom(item.key, item.position, item.map);
  return room;
});

咱们按房间地位的和贴图的配置,创立如下所示的三个房间客厅、卧室和书房。

④ 限度旋转角度

依据本人的需要,咱们能够对镜头控制器 📹 做以下限度,比方开启转动惯性、禁止整个场景通过鼠标右键产生平移、设置缩放的最大级别避免暴露出球体、限度垂直方向旋转等,以加强用户体验。

// 转动惯性
controls.enableDamping = true;
// 禁止平移
controls.enablePan = false;
// 缩放限度
controls.maxDistance = 12;
// 垂直旋转限度
controls.minPolarAngle = Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2;

⑤ 实现多个场景穿梭漫游

本文中实现多个场景穿梭漫游的办法原理:次要是通过挪动相机和控制器的中点地位来实现的,咱们先用用于生成多个场景的 rooms 数值在页面上增加一些示意切换房间的按钮,点击按钮时拿到须要跳转的指标场景信息,而后通过 Animations.animateCamera 办法 将像机和控制器从以后地位平滑挪动到指标地位

// 点击切换场景
const handleSwitchButtonClick = async (key) => {const room = rooms.filter((item) => item.key === key)[0];
  if (data.camera) {
    const x = room.position.x;
    const y = room.position.y;
    const z = room.position.z;
    Animations.animateCamera(data.camera, data.controls, { x, y, z: data.cameraZAxis}, {x, y, z}, 1600, () => {});
    data.controls.update();}
};

其中 Animations.animateCamera 办法是应用 TWEEN.js 封装的一个挪动相机 📷 和控制器 🖱 的办法,应用它能够实现丝滑的镜头补间动画,不仅能够像本文中这样来实现 多个场景的切换 ,还能够实现像 镜头从远处拉近 、点击交互点后 镜头聚焦放大到某个部分 镜头场景巡航 等成果。残缺代码能够查看本篇文章的示例代码:

animateCamera: (camera, controls, newP, newT, time = 2000, callBack) => {
  const tween = new TWEEN.Tween({
    x1: camera.position.x, // 相机 x
    y1: camera.position.y, // 相机 y
    z1: camera.position.z, // 相机 z
    x2: controls.target.x, // 控制点的中心点 x
    y2: controls.target.y, // 控制点的中心点 y
    z2: controls.target.z, // 控制点的中心点 z
  });
  tween.to(
    {
      x1: newP.x,
      y1: newP.y,
      z1: newP.z,
      x2: newT.x,
      y2: newT.y,
      z2: newT.z,
    },
    time,
  );
  // ...
}

⑥ 增加交互点

场景漫游穿梭的性能曾经实现了,当初咱们来在全景场景中增加一些交互热点 ,用于实现场景物体标注和鼠标点击交互,比方咱们在这个示例中,在客厅中增加了 电视机📺沙发🛋冰箱❄️ 等交互点,咱们能够当初创立场景的数组中增加这些交互点的信息 interactivePoints,以不便批量创立,依据本人的需要咱们能够增加一些可选的配置参数,本文中的参数含意别离是:

  • key:惟一标识符。
  • value:显示名称。
  • description:形容文案。
  • cover:配图。
  • position:在三维空间中的地位。
const rooms = [
  {
    name: '客厅',
    key: 'living-room',
    map: new URL('@/assets/images/map/map_living_room.jpg', import.meta.url).href,
    position: new Vector3(0, 0, 0),
    interactivePoints: [
      {
        key: 'tv',
        value: '电视机',
        description: '智能电视',
        cover: new URL('@/assets/images/home/cover_living_room_tv.png', import.meta.url).href,
        position: new Vector3(-6, 2, -8),
      },
      // ...
    ],
  },

而后在页面上利用 rooms 数组的 interactivePoints 来批量创立交互点的 DOM 节点:

<div
  class="point"
  v-for="(point, index) in interactivePoints"
  :key="index"
  :class="[`point-${index}`, `point-${point.key}`]"
  @click="handleReactivePointClick(point)"
  v-show="point.room === data.currentRoom"
>
  <div class="label" :class="[`label-${index}`, `label-${point.key}`]">
    <label class="label-tips">
      <div class="cover">
        <i
          class="icon"
          :style="{background: `url(${point.cover}) no-repeat center`,
            'background-size': 'contain',
          }"
        ></i>
      </div>
      <div class="info">
        <p class="p1">{{point.value}}</p>
        <p class="p2">{{point.description}}</p>
      </div>
    </label>
  </div>
</div>

用样式表把交互点设置成本人喜爱的款式 🤩,须要留神的一点是,交互点 🔘 初始的款式中设置了 transform: scale(0, 0),即它的宽高都为 0,是暗藏看不见的,这样设置的目标是为了实现 只有交互点呈现在相机可视区域时才显示在场景中 ,其余转动到相机反面时应该暗藏掉。当交互点被增加 .visible 类时,交互点变为显示状态。本示例中还应用交互点内 .label::before.label::after 等伪元素和子元素增加了一些波纹扩散动画及其其余文案信息等。

.point
  position: fixed
  top: 50%
  left: 50%
  .label
    position: absolute
    &::before, &::after
      display inline-block
      content ''
    &::before
      animation: bounce-wave 1.5s infinite
    &::after
      animation: bounce-wave 1.5s -0.4s infinite
    .label-tips
      height 88px
      width 200px
      position absolute
  &.visible .label
    transform: scale(1, 1)

🚩 暗藏显示的交互也能够通过 display:nonevisibility:hidden、及应用 js 变量管制元素暗藏显示等形式来实现。

创立完交互点 🔘 元素之后,咱们还须要在页面重绘办法 tick() 中像上面这样增加一个办法,来将交互点显示在三维场景中,并依据与相机的关系来管制每个交互点的显示与暗藏,原理是应用 THREE.Raycaster 来检测元素是否被遮挡:

const raycaster = new THREE.Raycaster();

const tick = () => {for (const point of _points) {
    // 获取 2D 屏幕地位
    const screenPosition = point.position.clone();
    const pos = screenPosition.project(camera);
    raycaster.setFromCamera(screenPosition, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);
    if (intersects.length === 0) {
      // 未找到相交点,显示
      point.element.classList.add('visible');
    } else {
      // 获取相交点的间隔和点的间隔
      const intersectionDistance = intersects[0].distance;
      const pointDistance = point.position.distanceTo(camera.position);
      // 相交点间隔比点间隔近,暗藏;相交点间隔比点距离远,显示
      intersectionDistance < pointDistance
        ? point.element.classList.remove('visible')
        : point.element.classList.add('visible');
    }
    pos.z > 1
      ? point.element.classList.remove('visible')
      : point.element.classList.add('visible');
    const translateX = screenPosition.x * sizes.width * 0.5;
    const translateY = -screenPosition.y * sizes.height * 0.5;
    point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
  }
  // ...
};

🚩 对于应用 Raycaster 来检测元素是否被遮挡的具体介绍,能够看看我的这篇文章《Three.js 打造缤纷夏日 3D 梦中情岛》。

⑦ 页面优化和加载进度治理

最初,因为创立多个三维全景图场景须要加载很多张图片,而且全景图的图片个别比拟大,咱们能够事后加载完所有图片后再进行渲染,本文应用的是本人增加的一个预加载办法,也能够应用像 preload.js 等其余库来预加载图片。除了加载进度显示之外,事实开发场景中应该还有很多个性化的需要,比方能够在点击交互点的时候弹出一个具体弹窗、点击电视的时候开始播放一段视频、点击沙发的时候镜头聚焦放大到沙发、点击开关的时候变为夜间模式……这些交互的原理和本文中的交互点是差不多的 😂

🔗 源码地址:https://github.com/dragonir/threejs-odessey

总结

本文中次要蕴含的知识点包含:

  • Three.js 中实现全景图的原理和多种实现形式。
  • 与全景图相干的生成工具、编辑工具的应用。
  • 创立多个全景图并实现多个场景间的漫游穿梭性能。
  • 在三维全景图中增加交互热点。

本文到这里就完结了,本文中通过挪动相机镜头和管制的办法来实现几个全景图之间漫游穿梭成果还是不错的,然而它的毛病也是很显著的,就是当全景场景数量特地多时,就须要创立十分多的球体,此时计算出每个场景的地位十分艰难,并且会造成页面性能耗损问题,因而须要进行优化。下篇文章将会介绍另一种 更加优雅的形式 来实现全景图之间的漫游性能,过渡动画也会更加晦涩丝滑。

想理解其余前端常识或其余未在本文中详细描述的 Web 3D 开发技术相干常识,可浏览我往期的文章。如果有疑难能够在评论中 留言 ,如果感觉文章对你有帮忙,不要忘了 一键三连哦 👍

附录

  • [1]. 🌴 Three.js 打造缤纷夏日 3D 梦中情岛
  • [2]. 🔥 Three.js 实现炫酷的赛博朋克格调 3D 数字地球大屏
  • [3]. 🐼 Three.js 实现 2022 冬奥主题 3D 趣味页面,含冰墩墩
  • [4]. 🦊 Three.js 实现 3D 凋谢世界小游戏:阿狸的多元宇宙
  • [5]. 🏆 掘金 1000 粉!应用 Three.js 实现一个创意留念页面
  • ...
  • 【Three.js 进阶之旅】系列专栏拜访 👈
  • 更多往期【3D】专栏拜访 👈
  • 更多往期【前端】专栏拜访 👈

参考

  • [1]. threejs.org
退出移动版