跳到主要内容

三维空间中的点

上一章回顾: 状态录制

  • 你了解了如何通过 State 完成操作的录制和还原。
  • 熟悉地使用 State 相关的方法和事件。
本章你可以学习到
  • 了解 Five SDK 的事件系统。
  • 获取到点的三维位置。

准备工作

我们新建一个目录(src/4.points-in-3d)以及对应的 html 文件 以及 jsxtsx 文件。 携带着上一章的 State 代码,太过于繁琐,那我们从 展示三维空间 章的内容的基础开发。

<!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>三维中的点 | Points in 3d</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>
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/4.points-in-3d/useWindowDimensions.js
import { useState, useEffect } from "react";

/**
* 获取当前窗口的尺寸
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}

/**
* React Hook: 获取当前窗口的尺寸
*/
function useWindowDimensions() {
const [size, setSize] = useState(getWindowDimensions);
useEffect(() => {
const listener = () => setSize(getWindowDimensions());
window.addEventListener("resize", listener, false);
return () => window.removeEventListener("resize", listener, false);
});
return size;
}

export { useWindowDimensions };
src/4.points-in-3d/ModeController.jsx
import React from "react";
import { Five } from "@realsee/five";
import { useFiveCurrentState } from "@realsee/five/react";
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";

/**
* React Component: 模态控制
*/
const ModeController = () => {
const [state, setState] = useFiveCurrentState();
return (
<Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={state.mode}
onChange={(_, newValue) => {
setState({ mode: newValue });
}}
>
<BottomNavigationAction
label="全景漫游"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="空间总览"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
};

export { ModeController };
src/4.points-in-3d/App.jsx
import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
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 = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return (
work && (
<FiveProvider initialWork={work}>
<FiveCanvas {...size} />
<ModeController />
</FiveProvider>
)
);
};

export { App };
src/4.points-in-3d/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/4.points-in-3d/index.html"。

信息

请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。

事件系统

在屏幕上点击时,five SDK 的默认行为是选择点击位置附近最合适的 观察点(observer) 移动过去。大部分用户的动作都是如此,就好比浏览器对于 A 标签 的处理逻辑大多都是链接跳转。上述就是 five SDK 的内建 tapGesture事件。

内建事件

five SDK 内建的事件有如下:

  • tapGesture: 鼠标左键点击或者手指点击。默认行为是点位移动。
  • panGesture: 鼠标按住一栋或者手指在屏幕上拖拽移动。相机旋转(Topview 下是相机平移)。
  • pinchGesture: 手指双指做捏的手势。默认行为是修改相机可视角度。
  • mouseWheel: 鼠标滚轮。默认行为是修改相机可视角度。
  • gesture: 上述的任意事件。

阻止默认行为

所有事件和 浏览器对于 A 标签 的处理逻辑 一样,都可以阻止默认事件,你只需要监听 wants 开头的事件,在回调函数中 return false 即可。比如想阻止 tapGesture 的默认点位移动行为。可以做如下操作。

useFiveEventCallback("wantsTapGesture", () => {
  // highlight-start
  // 阻止 tapGesture 触发
  return false;
  // highlight-end
});

具体每个事件的 API 可以查看详细文档

从 tapGesture 获取坐标

我们制作一个简单功能,用于标记在画布上点击的三维位置。 但是为了和 点位移动 功能不冲突,我们用一个 Switch 按钮来控制标记状态是否开启。

头部添加依赖

本章需要介绍一下 three.js 了。three.js 是一个三维图形库,Five SDK 使用 three.js 的数学库和渲染器。本章涉及 three.js 有两个内容,在这里做一些说明,你不必完全了解 three.js, 我做一些说明你就可以理解。

  • THREE.Vector3: 你可以就认为是一个 { x: number, y: number, z: number } 的结构体,并且多加了一些数学方法(本次不会用到数学方法,只是记录 xyz)
  • THREE.Raycaster: 光线投射类。你可以简单的理解为屏幕上的一个点对应到三维空间是一条射线。

REYCASTER

射线有很多作用,比如:通过射线和模型之前的相交性检测,就可以判断是否对象被选中。

编写 MarkController 组件

  1. 添加一个 MarkController 文件,用于编写组件。
  2. 我们通过 active React state 状态来控制当前应用是否在标记的模式下。
  3. 通过 tapGesture 第一个参数是 raycaster,通过 modelIntersectRaycaster 可以获取到焦点信息 intersectintersect.point 就是交点的坐标。
  4. 通过 marks React state 来存储所有交点,并且实现收集和删除。
/**
 * React Component: 标记坐标点
 */
import React, { useState, useEffect } from "react";
import {
  useFiveEventCallback,
  useFiveModelIntersectRaycaster,
} from "@realsee/five/react";
import Button from "@mui/material/Button";
import Switch from "@mui/material/Switch";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";

/**
 * React Component: 标记坐标点
 */
const MarkController = () => {
  const [active, toggleActive] = useState(false);
  const [marks, setMarks] = useState([]);
  const modelIntersectRaycaster = useFiveModelIntersectRaycaster();
  useFiveEventCallback(
    "wantsTapGesture",
    (raycaster) => {
      if (active) {
        const [intersect] = modelIntersectRaycaster(raycaster);
        if (intersect) setMarks((marks) => marks.concat(intersect.point));
        return false;
      }
    },
    [active]
  );
  return (
    <Paper sx={{ position: "fixed", top: 10, left: 10, padding: 1 }}>
      <Stack>
        <Stack direction="row">
          <Switch
            checked={active}
            onChange={(event, checked) => toggleActive(checked)}
          />{" "}
          <Button disabled>开启点击记录坐标</Button>
        </Stack>
        <Stack spacing={1}>
          {marks.map((point, index) => {
            const { x, y, z } = point;
            return (
              <Chip
                key={index}
                label={`x=${x.toFixed(2)} y=${y.toFixed(2)} z=${z.toFixed(2)}`}
                onDelete={() =>
                  setMarks((marks) =>
                    marks.filter((_, index_) => index_ !== index)
                  )
                }
              />
            );
          })}
        </Stack>
      </Stack>
    </Paper>
  );
};

export { MarkController };

使用标记组件

插入到 App 文件的 FiveProvider

import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { MarkController } from "./MarkController";
// 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 = () => {
  const work = useFetchWork(workURL);
  const size = useWindowDimensions();
  return (
    work && (
      <FiveProvider initialWork={work}>
        <FiveCanvas {...size} />
        <ModeController />
        // highlight-start
        <MarkController />
        // highlight-end
      </FiveProvider>
    )
  );
};

export { App };

回到你的浏览器查看,会发现你的页面左上角出现一个选择开关。打开开关,点击画布内容,会输出点击位置的坐标。

真棒,一下子就理解和获取到了三维坐标 🥳 。

下一章节你会学到

下一章我们将实现一个空间标签功能,不要错过哦。