改变视角
上一章回顾: 展示三维空间
- 你了解什么是 Work, 以及如何获取和加载。
 - 如何展示三维空间,并且再次基础上开发组件来控制三维空间。
 
- 了解什么是 State。
 - 如何改变三维空间观察的方向 / 位置。
 - 了解上一章的代码,比如 
useCurrentState及其他响应的是如何工作的。 - 通过 State 完成了自动环视的功能。
 
准备工作
和上一章节一样,我们新建一个目录(src/2.knowing-state)以及对应的 html 文件 以及 js 或 ts 文件。
js 或 ts 文件可以先拷贝上一章节的内容。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>改变视角 | Knowing state</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      crossorigin="anonymous"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
      rel="stylesheet"
    />
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html,
      body,
      #app {
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./index"></script>
  </body>
</html>
- JavaScript
 - TypeScript
 
import { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
  const width = ref(window.innerWidth);
  const height = ref(window.innerHeight);
  const listener = () => {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  };
  window.addEventListener("resize", listener, false);
  onBeforeUnmount(() => {
    window.removeEventListener("resize", listener, false);
  });
  return { width, height };
}
export { useWindowDimensions };
<template>
  <nav class="navbar fixed-bottom navbar-light bg-light">
    <div class="container-fluid justify-content-center">
      <div class="btn-group">
        <button
          :class="
            state.mode == 'Panorama'
              ? 'btn btn-primary active'
              : 'btn btn-primary'
          "
          @click="() => setState({ mode: Five.Mode.Panorama })"
        >
          全景漫游
        </button>
        <button
          :class="
            state.mode == 'Panorama'
              ? 'btn btn-primary'
              : 'btn btn-primary active'
          "
          @click="() => setState({ mode: Five.Mode.Floorplan })"
        >
          空间总览
        </button>
      </div>
    </div>
  </nav>
</template>
<script setup>
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script>
<template>
  <FiveProvider :work="work">
    <FiveCanvas :width="width" :height="height" />
    <ModeController />
  </FiveProvider>
</template>
<script setup>
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
const work = ref();
const workURL =
  "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
  .then((response) => response.text())
  .then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>
import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
import { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
  const width = ref<number>(window.innerWidth);
  const height = ref<number>(window.innerHeight);
  const listener = () => {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  };
  window.addEventListener("resize", listener, false);
  onBeforeUnmount(() => {
    window.removeEventListener("resize", listener, false);
  });
  return { width, height };
}
export { useWindowDimensions };
<template>
  <nav class="navbar fixed-bottom navbar-light bg-light">
    <div class="container-fluid justify-content-center">
      <div class="btn-group">
        <button
          :class="
            state.mode == 'Panorama'
              ? 'btn btn-primary active'
              : 'btn btn-primary'
          "
          @click="() => setState({ mode: Five.Mode.Panorama })"
        >
          全景漫游
        </button>
        <button
          :class="
            state.mode == 'Panorama'
              ? 'btn btn-primary'
              : 'btn btn-primary active'
          "
          @click="() => setState({ mode: Five.Mode.Floorplan })"
        >
          空间总览
        </button>
      </div>
    </div>
  </nav>
</template>
<script setup lang="ts">
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script>
<template>
  <FiveProvider :work="work">
    <FiveCanvas :width="width" :height="height" />
    <ModeController />
  </FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
const work = ref();
const workURL =
  "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
  .then((response) => response.text())
  .then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>
import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
启动服务 npm run dev。 并跳转到当前页面 "http://localhost:3000/src/2.knowing-state/index.html"。
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
什么是 State
由来了解概念了,我保证这你是现阶段需要了解的最后一个理论知识了。
State 简介
State 用于描述状态的数据结构。上一章我们了解了 Work, Work 是用于描述一个三维空间,那么 State 则是描述在这个三维空间内,目前正在处于什么状态。他包含了模态、位于的采集点位、相机的方向、相机可视角度的信息。
State 的数据结构及说明
interface State {
  mode: Five.Mode;
  panoIndex: number;
  longitude: number;
  latitude: number;
  fov: number;
  offset: THEEE.Vector3;
}
State的数据描述
mode: 当前的模态 Five 常用有 5 种模态,可以使用Five.Mode获得- Panorama: 全景游走模态,该模态下视图将在采集点间游走,手势操作可以旋转/放大视角/切换采集点,适合查看采集的全景信息。
 - Floorplan: 空间总览模态, 该模态下视图以模型为中心,手势操作可以旋转/放大模型/切换楼层,适合查看模型的整体效果。
 - Topview: 户型图模态,该模态下视图以模型为中心,垂直俯视模型,手势操作可以平移/放大模型/切换楼层,适合查看模型平面结构。
 - Model: 模型游走模态,该模态下视图将在模型中自由游走,手势操作可以旋转/放大视角/位移,适合查看模型的细节,做一些定位操作。
 - VRPanorama: VR 眼镜模态,该模态下可以使用 Cardboard 眼镜 或者他的第三方衍生产品,实现 VR 虚拟显示效果。
 
panoIndex: 采集点位,即在 Panorama 模态可以落脚的位置。是一个work[observers]的一个索引。longitude/latitude: 相机的水平角 / 相机的偏航角(弧度),我们使用类似经纬度的方式描述相机位置。整个模型场景为一个右手笛卡尔坐标系,
XZ平面平行于地面,Y轴垂直于地面。初始相机方向为原点看向 Z 轴负方向。
- 增加 
longitude相机向左旋转 - 减少 
longitude相机向右旋转 - 增加 
latitude相机向下旋转 - 减少 
latitude相机向上旋转 
- 增加 
 fov: 相机垂直方向的可视角度 (角度)offset: 当前相机的三维坐标
state 相关的 api 有什么
可以通过 state currentState 获取当前状态,通过 setState setCurrentState 设置状态。
state / currentState 的区别
currentState 是当前状态,就是画面中的状态,目前展示的状态。 state 是目标状态或者说下一时间的稳定状态。
可以简单的这样认为: 
当 setSate 被调用后,state 当即会变成 setState 的值,而 currentState 则不会马上改变,他会在动画过渡的过程中逐步逼近 state 并最终变成和 state 一样的值。就和你在画面中看到的动画一样。
在上一章的代码示例中,我们已经使用了 mode 属性,来切换 Panorama 以及 Floorplan 模态。你也可以尝试把其他的几种模态都补充上,看看各种模态下有什么区别。
VRPanorama 模态需要使用设备陀螺仪信息,需要使用移动设备。 并且如果在 iOS 设备下,需要服务处于
https下,否则 iOS 不允许访问陀螺仪信息。
开发一个自动环视的功能
我们已经获取和设置过了 mode,这次我们修改下 longitude / latitude 试试。这次我们开发一个自动环视的功能。通过一个按钮控制激活自动环视的功能,该功能会自动水平旋转镜头。
书写环视组件
- 添加一个 LookAroundController 文件,用于编写组件。
 - 设计 active Vue Ref 用于控制环视的功能是否开启。
 - 环视功能的实现为:通过 setInterval 定时触发函数,修改 five 的 state。
 
- JavaScript
 - TypeScript
 
<template>
  <div class="card position-fixed m-2 top-0 end-0">
    <button class="btn btn-light" v-show="isShow" @click="startFunc">
      <i class="bi bi-arrow-repeat"></i>
    </button>
    <button class="btn btn-light" v-show="!isShow" @click="stopFunc">
      <i class="bi bi-pause"></i>
    </button>
  </div>
</template>
<script setup>
import { ref } from "vue";
import { useFiveCurrentState } from "@realsee/five/vue";
const [currentState, setState] = useFiveCurrentState();
const isShow = ref(true);
let timer;
const startFunc = () => {
  window.clearInterval(timer);
  isShow.value = false;
  timer = window.setInterval(() => {
    setState({ longitude: currentState.value.longitude + Math.PI / 360 });
  }, 16);
};
const stopFunc = () => {
  window.clearInterval(timer);
  isShow.value = true;
};
</script>
<template>
  <div class="card position-fixed m-2 top-0 end-0">
    <button class="btn btn-light" v-show="isShow" @click="startFunc">
      <i class="bi bi-arrow-repeat"></i>
    </button>
    <button class="btn btn-light" v-show="!isShow" @click="stopFunc">
      <i class="bi bi-pause"></i>
    </button>
  </div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useFiveCurrentState } from "@realsee/five/vue";
const [currentState, setState] = useFiveCurrentState();
const isShow = ref(true);
let timer: number | undefined;
const startFunc = () => {
  window.clearInterval(timer);
  isShow.value = false;
  timer = window.setInterval(() => {
    setState({ longitude: currentState.value.longitude + Math.PI / 360 });
  }, 16);
};
const stopFunc = () => {
  window.clearInterval(timer);
  isShow.value = true;
};
</script>
使用自动环视组件
插入到 App 文件的 FiveProvider 中
- JavaScript
 - TypeScript
 
<template>
  <FiveProvider :work="work">
    <FiveCanvas :width="width" :height="height" />
    <ModelControl />
    <LookAroundController />
  </FiveProvider>
</template>
<script setup>
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModelControl from "./ModelControl.vue";
import LookAroundController from "./LookAroundController.vue";
const work = ref();
const workURL =
  "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
  .then((response) => response.text())
  .then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>
<template>
  <FiveProvider :work="work">
    <FiveCanvas :width="width" :height="height" />
    <ModelControl />
    <LookAroundController />
  </FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork, Work } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModelControl from "./ModelControl.vue";
import LookAroundController from "./LookAroundController.vue";
const work = ref<Work>();
const workURL =
  "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
  .then((response) => response.text())
  .then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>
回到你的浏览器查看,会发现你的页面右上角出现一个环视按钮。点击可以触发和关闭环视。
真是一个不错的功能 🥳 !
下一章节你会学到
- 通过 State 完成用户操作的录制。
 - 通过 State 还原用户操作画面。