はじめに
PLATEAUを使ったwebXRアプリ開発のために、Next.jsのApp Routerを使おうと思ったのですが、これまでに使っている記事を見受けられなかったので、備忘録的に残しておきます
基本的には以下の記事の通りです
より詳細な実装について知りたい場合にはそちらの記事をご覧ください
今回のプロジェクトではPWAとwebXRを導入しているので、それらについての記載が混ざっているかもしれません
デモ
こんな感じで視点がくるくる移動できるようなものを作ります
Next.js,ESLint,Prettier などの設定
この Public template を使っています
自作テンプレートなので抜けがあるかもしれません
デプロイにはVercelを利用しています
ファイル構造
.
├── README.md
├── app
│ ├── _components
│ │ ├── PlateauTileset.tsx
│ │ └── PlateauTilesetTransform.tsx
│ ├── _hooks
│ │ └── PlateauTilesetTransformContext.ts
│ ├── _types
│ │ ├── PlateauTilesetProps.ts
│ │ └── PlateauTilesetTransformProps.ts
│ ├── _utils
│ │ └── CesiumRTCPlugin.ts
│ ├── api
│ │ └── ...
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── next-env.d.ts
├── next.config.mjs
├── node_modules
├── package.json
├── public
│ └── ...
├── tsconfig.json
└── yarn.lock
Next.jsでPWAをするための設定については省略しています
_hooksディレクトリの使い方が間違ってるかもしれないです
補足:PLATEAU が提供しているデータの利用について
利用に関する情報は以下のレポジトリにて説明されています
今回の用途に沿ったデータの利用ついてより、詳しく記した記事を書きましたので合わせてご確認ください
実装
package.json
{
"name": "test-next",
"version": "0.1.0",
"private": true,
"resolutions": {
"postprocessing": "~6.28.7"
},
"scripts": {
"dev": "next dev",
"dev:https": "next dev --experimental-https",
"build": "next build",
"start": "next start",
"lint": "next lint --dir app",
"lint:fix": "yarn lint --fix",
"format": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json}'"
},
"dependencies": {
"3d-tiles-renderer": "^0.3.13",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@react-three/drei": "^9.17.1",
"@react-three/fiber": "^8.2.0",
"@react-three/postprocessing": "~2.6.2",
"@react-three/xr": "^5.7.1",
"next": "14.1.4",
"next-pwa": "^5.6.0",
"react": "^18",
"react-dom": "^18",
"three": "^0.142.0",
"three-stdlib": "^2.12.1"
},
"devDependencies": {
"@babel/core": "^7.19.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/three": "^0.141.0",
"@types/webxr": "^0.5.15",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-unused-imports": "^3.1.0",
"prettier": "^3.2.5",
"typescript": "^5"
}
}
page.tsx
'use client';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { EffectComposer, SSAO } from '@react-three/postprocessing';
import React from 'react';
import { PlateauTileset } from './_components/PlateauTileset';
import { PlateauTilesetTransform } from './_components/PlateauTilesetTransform';
const Home: React.FC = () => {
return (
<Canvas>
<fogExp2 attach='fog' color='white' density={0.0002} />
<PerspectiveCamera makeDefault position={[-1600, 450, -1400]} near={10} far={1e5} />
<OrbitControls target={[-1200, 0, -800]} />
<ambientLight intensity={0.5} />
<directionalLight
position={[500, 1000, 1000]}
intensity={1}
castShadow
shadow-mapSize={[8192, 8192]}
>
<orthographicCamera attach='shadow-camera' args={[-2500, 2500, 2500, -2500, 1, 5000]} />
</directionalLight>
<PlateauTilesetTransform>
<PlateauTileset path='bldg/23100_nagoya/23101_chikusa-ku/notexture' center />
</PlateauTilesetTransform>
<EffectComposer>
<SSAO intensity={5000} />
</EffectComposer>
</Canvas>
);
};
export default Home;
カメラ・レンダリング
<PerspectiveCamera makeDefault position={[0,0,0]} near={1} far={1000} />
<OrbitControls target={[-1200, 0, -800]} />
canvas
に描画される要素のレンダリングを行うカメラの設定
three.js単体とは異なり、Reactのコンポーネントのように扱う
Props
Prop | summary | Default |
---|---|---|
makeDefault | カメラをシステムデフォルトとして登録し、ファイバーはそのカメラでレンダリングを開始する。 | - |
manual | 手動にすると応答性がなくなるので、アスペクト比を自分で計算する | - |
children | カメラに追従するか、関数を渡すと撮影時に非表示になります | - |
frames | レンダリングするフレーム数 | 0 |
resolution | FBOの解像度 | 256 |
envMap | 機能的に使用するためのオプションの環境マップ | - |
Prop | summary | Default |
---|---|---|
fov | カメラのフラスタムの垂直視野角 | 50 |
aspect | カメラのフラスタムのアスペクト比 | 1 |
near | カメラのフラスタムの近い平面 | 0.1 |
far | カメラのフラスタムの遠い平面 | 2000 |
OrbitControls
マウスによるカメラの操作を可能にする
特定オブジェクトを中心としたカメラの回転や、スクロールによるzoom
の変更など
ライト
<ambientLight intensity={0.5} />
<directionalLight
position={[500, 1000, 1000]}
intensity={1}
castShadow
shadow-mapSize={[8192, 8192]}
>
<orthographicCamera
attach='shadow-camera'
args={[-2500, 2500, 2500, -2500, 1, 5000]}
{/* [left?: number, right?: number, top?: number, bottom?: number, near?: number, far?: number]*/}
/>
</directionalLight>
_components
'use client';
import React, { useCallback, useMemo, useState } from 'react';
import { Quaternion, Vector3 } from 'three';
import { PlateauTilesetTransformContext } from '../_hooks/PlateauTilesetTransformContext';
import { PlateauTilesetTransformProps } from '../_types/PlateauTilesetTransformProps';
export const PlateauTilesetTransform: React.FC<PlateauTilesetTransformProps> = ({ children }) => {
const [{ offset, rotation }, setState] = useState<{
offset?: Vector3;
rotation?: Quaternion;
}>({});
const setCenter = useCallback((center: Vector3) => {
const direction = center.clone().normalize();
const up = new Vector3(0, 1, 0);
const rotation = new Quaternion();
rotation.setFromUnitVectors(direction, up);
setState({
offset: new Vector3(0, -center.length(), 0),
rotation,
});
}, []);
const context = useMemo(() => ({ setCenter }), [setCenter]);
return (
<PlateauTilesetTransformContext.Provider value={context}>
<group position={offset} quaternion={rotation}>
{children}
</group>
</PlateauTilesetTransformContext.Provider>
);
};
'use client';
import { TilesRenderer } from '3d-tiles-renderer';
import { useFrame, useThree } from '@react-three/fiber';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Box3, Matrix4, Mesh, MeshStandardMaterial, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib';
import { PlateauTilesetTransformContext } from '../_hooks/PlateauTilesetTransformContext';
import { PlateauTilesetProps } from '../_types/PlateauTilesetProps';
import { CesiumRTCPlugin } from '../_utils/CesiumRTCPlugin';
const gltfLoader = new GLTFLoader();
gltfLoader.register((parser) => new CesiumRTCPlugin(parser));
const material = new MeshStandardMaterial({
metalness: 0.5,
});
export const PlateauTileset: React.FC<PlateauTilesetProps> = ({ path, center = false }) => {
const { setCenter } = useContext(PlateauTilesetTransformContext);
const centerRef = useRef(center);
centerRef.current = center;
const createTiles = useCallback(
(path: string) => {
const tiles = new TilesRenderer(
`https://plateau.geospatial.jp/main/data/3d-tiles/${path}/tileset.json`,
);
tiles.manager.addHandler(/\.gltf$/, gltfLoader);
// `center` が指定されているとき、タイルの境界ボックスの底面の中央を
// PlateauTilesetTransform の位置として指定する。
tiles.onLoadTileSet = () => {
if (centerRef.current) {
const box = new Box3();
const matrix = new Matrix4();
tiles.getOrientedBoundingBox(box, matrix);
box.min.z = box.max.z = Math.min(box.min.z, box.max.z);
box.applyMatrix4(matrix);
const center = new Vector3();
box.getCenter(center);
setCenter(center);
}
};
// タイル内のすべてのオブジェクトに影とマテリアルを適用する。
tiles.onLoadModel = (scene) => {
scene.traverse((object) => {
object.castShadow = true;
object.receiveShadow = true;
if (object instanceof Mesh) {
object.material = material;
}
});
};
return tiles;
},
[setCenter],
);
// TilesRenderer のライフサイクル
const [tiles, setTiles] = useState(() => createTiles(path));
const pathRef = useRef(path);
useEffect(() => {
if (path !== pathRef.current) {
pathRef.current = path;
setTiles(createTiles(path));
}
}, [path, createTiles]);
useEffect(() => {
return () => {
tiles.dispose();
};
}, [tiles]);
const camera = useThree(({ camera }) => camera);
const gl = useThree(({ gl }) => gl);
// TilesRenderer と React の状態を同期する。
useEffect(() => {
tiles.setCamera(camera);
}, [tiles, camera]);
useEffect(() => {
tiles.setResolutionFromRenderer(camera, gl);
}, [tiles, camera, gl]);
useFrame(() => {
tiles.update();
});
return <primitive object={tiles.group} />;
};
_types
export interface PlateauTilesetProps {
path: string;
center?: boolean;
}
import { ReactNode } from 'react';
export interface PlateauTilesetTransformProps {
children: ReactNode;
}
_hooks
'use client';
import { createContext } from 'react';
import { Vector3 } from 'three';
export const PlateauTilesetTransformContext = createContext({
// eslint-disable-next-line no-unused-vars
setCenter: (center: Vector3): void => {},
});
_utils
import { GLTF, GLTFLoaderPlugin, GLTFParser } from 'three-stdlib';
export class CesiumRTCPlugin implements GLTFLoaderPlugin {
readonly name = 'CESIUM_RTC';
// eslint-disable-next-line no-unused-vars
constructor(private readonly parser: GLTFParser) {}
afterRoot(result: GLTF): null {
if (this.parser.json.extensions?.CESIUM_RTC?.center != null) {
const center: [number, number, number] = this.parser.json.extensions.CESIUM_RTC.center as [
number,
number,
number,
];
result.scene.position.set(...center);
}
return null;
}
}
終わりに
three.js と cesium が全くの初心者の状態でしたので、コードの読み込みが足りないのですが、とりあえず動くものになったので記事にしました。本当にとりあえず動くだけですが...。
PLATEAUのAR表示が可能になったので、そこから面白い体験を生み出せるように検証を続けてみようと思います!
参考文献