Three.js 进阶之旅:全景漫游-初阶移动相机版

Three.js 进阶之旅:全景漫游-初阶移动相机版

声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

摘要

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

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

效果

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

Three.js 进阶之旅:全景漫游-初阶移动相机版

点击屏幕上有其他场景名称的按钮比如 客厅卧室书房 时,可以从当前场景切换到目标场景全景图,交互热点也会同时切换。

Three.js 进阶之旅:全景漫游-初阶移动相机版

打开以下链接,在线预览效果,大屏访问效果更佳。

本专栏系列代码托管在 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 进阶之旅:全景漫游-初阶移动相机版

? 球体全景图 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 进阶之旅:全景漫游-初阶移动相机版

? 立方体全景图 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 客户端,可以在客户端内非常方便的在全景图上添加交互热点、实现多个场景的漫游路径等,大家感兴趣的话都可以试试。

Three.js 进阶之旅:全景漫游-初阶移动相机版

工具

全景图生成工具

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

全景图编辑工具

下面两个网站提供丰富的三维全景背景照片及将 hdr 图片裁切成上述需要的 6 张贴图的能力,大家可以按自己需要下载和编辑。

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

Three.js 进阶之旅:全景漫游-初阶移动相机版

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

Three.js 进阶之旅:全景漫游-初阶移动相机版

实现

现在,我们使用第一种球体 全景图的方式,来实现示例中介绍的内容。

〇 场景初始化

创建全景图前先做一些常规三维场景准备工作,由于三维全景图功能并不会涉及到新的技术点,因此像下面这样简单实现就可以。

<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);

Three.js 进阶之旅:全景漫游-初阶移动相机版

② 创建全景图

现在我们对球体进行全景图片贴图,并将 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);

Three.js 进阶之旅:全景漫游-初阶移动相机版

此时,我们通过鼠标放大球体,进入到球体内部,上下左右旋转球体,就能观察到全景效果了。

Three.js 进阶之旅:全景漫游-初阶移动相机版

③ 创建其他场景的全景图

对于数量较少,简单的场景我们可以创建多个球体全景图来实现,这种方式虽然笨重,但是控制多个场景很方便,代码也非常容易理解,下篇文章将通过另一种更优雅的方式来实现多个全景图场景,以适应更加复杂的需求。

我们先对创建球体 全景图的方法加以封装,通过 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;
});

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

Three.js 进阶之旅:全景漫游-初阶移动相机版

④ 限制旋转角度

根据自己的需求,我们可以对镜头控制器 ? 做以下限制,比如开启转动惯性、禁止整个场景通过鼠标右键发生平移、设置缩放的最大级别防止暴露出球体、限制垂直方向旋转等,以增强用户体验。

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

Three.js 进阶之旅:全景漫游-初阶移动相机版

⑤ 实现多个场景穿梭漫游

本文中实现多个场景穿梭漫游的方法原理:主要是通过移动相机和控制器的中点位置来实现的,我们先用用于生成多个场景的 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,
  );
  // ...
}

Three.js 进阶之旅:全景漫游-初阶移动相机版

⑥ 添加交互点

场景漫游穿梭的功能已经实现了,现在我们来在全景场景中添加一些交互热点 ,用于实现场景物体标注和鼠标点击交互,比如我们在这个示例中,在客厅中添加了 电视机?沙发?冰箱❄️ 等交互点,我们可以现在创建场景的数组中添加这些交互点的信息 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梦中情岛》

Three.js 进阶之旅:全景漫游-初阶移动相机版

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

最后,因为创建多个三维全景图场景需要加载很多张图片,而且全景图的图片一般比较大,我们可以预先加载完所有图片后再进行渲染,本文使用的是自己添加的一个预加载方法,也可以使用像 preload.js 等其他库来预加载图片。除了加载进度显示之外,现实开发场景中应该还有很多个性化的需求,比如可以在点击交互点的时候弹出一个详细弹窗、点击电视的时候开始播放一段视频、点击沙发的时候镜头聚焦放大到沙发、点击开关的时候变为夜间模式……这些交互的原理和本文中的交互点是差不多的 ?

Three.js 进阶之旅:全景漫游-初阶移动相机版

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

总结

本文中主要包含的知识点包括:

  • Three.js 中实现全景图的原理和多种实现方式。
  • 与全景图相关的生成工具、编辑工具的使用。
  • 创建多个全景图并实现多个场景间的漫游穿梭功能。
  • 在三维全景图中添加交互热点。

本文到这里就结束了,本文中通过移动相机镜头和控制的方法来实现几个全景图之间漫游穿梭效果还是不错的,但是它的缺点也是很明显的,就是当全景场景数量特别多时,就需要创建非常多的球体,此时计算出每个场景的位置非常困难,并且会造成页面性能耗损问题,因此需要进行优化。下篇文章将会介绍另一种更加优雅的方式来实现全景图之间的漫游功能,过渡动画也会更加流畅丝滑。

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 ?

附录

参考

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17263717.html

原文链接:https://www.cnblogs.com/dragonir/p/17263717.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Three.js 进阶之旅:全景漫游-初阶移动相机版 - Python技术站

(0)
上一篇 2023年4月18日
下一篇 2023年4月18日

相关文章

  • 解决JS内存泄露之js对象和dom对象互相引用问题

    解决JS内存泄露(memory leak)之js对象和dom对象互相引用问题需要我们重视,因为它可能导致页面性能下降以及系统崩溃等问题。本文将通过以下几个方面来探讨此问题的解决方案: 什么是JS内存泄露问题? 为什么JS对象和DOM对象互相引用会造成内存泄露? 如何解决JS对象和DOM对象互相引用的问题? 1. 什么是JS内存泄露问题? JavaScript…

    JavaScript 2023年6月10日
    00
  • javascript RegExp multiline多行匹配影响的^

    JavaScript的正则表达式是一类对文本进行模式匹配的工具,其中RegExp对象是正则表达式的表示。 在正则表达式中,有一些特殊字符和元字符,用于匹配特定的文本,只要掌握这些特殊字符和元字符,就可以更加灵活和高效的处理文本。 其中^是表示字符串的起始位置,在单行模式中表示匹配以目标字符串开始的字符串,而在多行模式中,^匹配每行的起始位置。 multili…

    JavaScript 2023年6月11日
    00
  • 详解JavaScript实现动态的轮播图效果

    完整攻略:详解JavaScript实现动态的轮播图效果 背景介绍 轮播图是现代网站中常见的交互元素之一,作为一种动态的展示形式,可以吸引用户的注意力,增强用户体验。因此,对于前端开发人员来说,能够熟练掌握实现轮播图的技术是非常重要的。 本篇文章将会详细介绍如何使用JavaScript实现动态的轮播图效果,让读者掌握这项技术。 实现方法 步骤一:HTML结构 …

    JavaScript 2023年6月11日
    00
  • 也说JavaScript中String类的replace函数

    请允许我详细讲解“也说JavaScript中String类的replace函数”的完整攻略。 了解replace函数 首先,我们需要了解一下JavaScript中,String类的replace函数是什么。 replace函数是一个用于替换字符串的工具函数,它的用法如下: str.replace(regexp|substr, newSubStr|functi…

    JavaScript 2023年5月28日
    00
  • JavaScript实现获取某个元素相邻兄弟节点的prev与next方法

    要实现获取某个元素的相邻兄弟节点,我们可以使用JavaScript提供的DOM API来实现。可以通过元素节点的 previousSibling 和 nextSibling 方法来获取相邻的兄弟节点。 以下是实现获取某个元素相邻兄弟节点的完整攻略: 步骤一:获取元素节点 首先,我们需要获取要查找相邻兄弟节点的元素节点。可以使用 document.getEle…

    JavaScript 2023年6月10日
    00
  • 详解Node.js中的事件机制

    详解Node.js中的事件机制 Node.js作为基于事件驱动的后端框架,事件机制非常重要。在Node.js中,事件分为两个主要部分:事件触发器和事件监听器。事件触发器通过emit()函数来触发事件,事件监听器通过on()函数来监听事件。下面将对事件机制进行详细讲解。 事件触发器 事件触发器是指当某个事件发生时,会调用emit()函数来发出一个事件。emit…

    JavaScript 2023年5月28日
    00
  • javascript之嵌套函数使用方法

    下面我来详细讲解一下“JavaScript之嵌套函数使用方法”的完整攻略。 什么是嵌套函数 嵌套函数是一种定义在另一个函数内部的函数,可以在外部函数范围内使用。嵌套函数有时候也称为内部函数、嵌套函数、局部函数或私有函数。 以下是一个简单的嵌套函数示例: function outerFunction() { console.log("这是外部函数&q…

    JavaScript 2023年5月27日
    00
  • JS 中在严格模式下 this 的指向问题

    JS 中的 this 表示函数执行时所在的上下文对象,在不同的情况下,this 指向的对象是不同的,这是 JS 中一个比较重要,也比较复杂的概念。 在严格模式下,this 指向的对象与非严格模式下不同。下面我们通过两个示例来详细讲解在严格模式下 this 的指向问题。 示例一 ‘use strict’; function showThis() { conso…

    JavaScript 2023年6月10日
    00
合作推广
合作推广
分享本页
返回顶部