跳到主要内容

改变视角

上一章回顾: 展示三维空间

  • 你了解了什么是 Work, 以及如何获取和加载。
  • 如何展示三维空间,并且再次基础上开发组件来控制三维空间。
本章你可以学习到
  • 了解什么是 State。
  • 如何改变三维空间观察的方向 / 位置。
  • 了解上一章的代码,比如 currentState setState 及其他响应的是如何工作的。
  • 通过 State 完成了自动环视的功能。

准备工作

和上一章节一样,我们新建一个目录(src/2.knowing-state)以及对应的 html 文件 以及 jsxtsx 文件。

jsxtsx 文件可以先拷贝上一章节的内容。

<!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>
  <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>

src/2.knowing-state/withFetchWork.jsx

import { useState, useEffect } from "react";
import { parseWork } from "@realsee/five";

/**
 * React Hook: 通过 work.json 的地址 获取 work 对象
 * @param url work.json 的数据地址
 * @returns work 对象,如果获取中,返回 null
 */
 function useFetchWork(url) {
  const [work, setWork] = useState(null);
  useEffect(() => {
    setWork(null);
    fetch(url)
      .then(response => response.text())
      .then(text => setWork(parseWork(text)));
  },[url]);
  return work;
}

export { useFetchWork };

src/2.knowing-state/withWindowDimensions.js

import React, { Component } from "react";

/**
* React HOC: 获取当前窗口的尺寸
*/
function withWindowDimensions() {
return function(Compnent) {
return class extends Component {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = { width: this.state.width, height: this.state.height };
return <Compnent windowDimensions={dimensions} {...this.props}/>;
}
}
}
}

export { withWindowDimensions };

src/2.knowing-state/ModeController.jsx

import React, { Component } from "react";
import { Five } from "@realsee/five";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";

const FEATURES = createFiveFeature("currentState", "setState");

/**
* React Component: 模态控制
*/
const ModeController = compose(
withFive(FEATURES)
)(class extends Component {
render() {
return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={this.props.$five.currentState.mode}
onChange={(_, newValue) => {
this.props.$five.setState({ mode: newValue });
}}
>
<BottomNavigationAction label="全景漫游" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
<BottomNavigationAction label="空间总览" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
</BottomNavigation>
</Paper>;
}
})

export { ModeController };

src/2.knowing-state/App.jsx

import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";

/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(class extends Component {
render() {
const { work, windowDimensions } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
<ModeController/>;
</FiveProvider>;
}
});

export { App };

src/2.knowing-state/index.jsx

import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App/>, document.querySelector("#app"));

export {};

启动服务 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 试试。这次我们开发一个自动环视的功能。通过一个按钮控制激活自动环视的功能,该功能会自动水平旋转镜头。

书写环视组件

  1. 添加一个 LookAroundController 文件,用于编写组件。
  2. 设计 active React state 用于控制环视的功能是否开启的按钮展示状态。
  3. 环视功能的实现为:通过 setInterval 定时触发函数,修改 five 的 state
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";

const FEATURES = createFiveFeature("currentState", "setState");

/**
 * ReactComponent: 自动环视按钮
 */
const LookAroundController = compose(
  withFive(FEATURES)
)(class extends Component {

  timer;
  state = { active: false };

  toggleActive(active) {
    window.clearInterval(this.timer);
    this.setState({ active });
    if (active === true) {
      this.timer = window.setInterval(() => {
        this.props.$five.setState({
          longitude: this.props.$five.currentState.longitude + Math.PI / 360
        });
      }, 16);
    } else {
      delete this.timer;
    }
  }
  render() {
    return <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
    {this.state.active ?
      <IconButton onClick={() => this.toggleActive(false)}><PauseIcon/></IconButton>:
      <IconButton onClick={() => this.toggleActive(true)}><FlipCameraAndroidIcon/></IconButton>
    }
    </Paper>;
  }
});

export { LookAroundController };

使用自动环视组件

插入到 App 文件的 FiveProvider

import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { LookAroundController } from "./LookAroundController";
// highlight-end

/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>;
      // highlight-start
      <LookAroundController/>;
      // highlight-end
    </FiveProvider>;
  }
});

export { App };

回到你的浏览器查看,会发现你的页面右上角出现一个环视按钮。点击可以触发和关闭环视。

真是一个不错的功能🥳 !

下一章节你会学到

下一章我们再使用 State 做一些复杂的功能,深入体会一下 State 的能力。
  • 通过 State 完成用户操作的录制
  • 通过 State 还原用户操作画面