Svelte で3D表示を行うライブラリ Threlte #5: XR への対応

# シリーズ

### Main

* [Svelte で 3D 表示を行うライブラリ Threlte #1: パッケージの作成とライブラリ概要](https://qiita.com/takavfx/items/0d4c6b831628d628b468)
* [Svelte で 3D 表示を行うライブラリ Threlte #2: エクストラパッケージを使用する](https://qiita.com/takavfx/items/e71db089198bba1b122b)
* [Svelte で 3D 表示を行うライブラリ Threlte #3: glTF シーンを扱う](https://qiita.com/takavfx/items/aa05a313ef9e450b3731)
* [Svelte で 3D 表示を行うライブラリ Threlte #4: シーンコントロールツールセット Theatre.js for Threlte](https://qiita.com/takavfx/items/b91ba3c47e5137723a5f)
* Svelte で3D表示を行うライブラリ Threlte #5: XR への対応

### Extra

* [Svelte で 3D 表示を行うライブラリ Threlte Ex1: Svelte5 対応を試す](https://qiita.com/takavfx/items/13feec35d60fe629eb26)

# Threlte での XR 対応

Three.js には [XR 実装](https://threejs.org/examples/?q=xr)がある。
ということで、Threlte でも XR の実装のラップが存在する。それが `@threlte/xr`。

```bash
npm install @threlte/xr
```

https://threlte.xyz/docs/reference/xr/getting-started

:::note warn
他の実装部分でもそうだが、特に XR 実装についてはβ版という立ち位置。内部実装は今後ごろっと変わる可能性もあるのでドキュメントを注視。
:::

## 開発環境について

幸いなことに、Threlte は Svelte/SvelteKit (Vite) のフレームワークに乗っかった Web ライブラリであるので、開発サーバーを立ち上げ、そのマシンの `IP アドレス+ポート` をデバイスのブラウザで開くだけでテストが出来る。

`npm create threlte` でパッケージを作成している場合、`npm run dev` をすると、次の画像の様に、如何にローカルネットワークに露出しているかが分かるので、例えば https://192.168.1.114:5173 で開発環境にアクセスすることが出来る。

僕は現状、これを Meta Quest 2 で開いたりしてテストしている。 (Quest 3 欲しくなってきた)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/3eaa5195-d667-2de6-206a-2c3901276af3.png)


# VR

## VR シーンの作成

通常の 3D シーンを XR 化させる (WebXR のセッションを準備する) ために `<XR>` コンポーネントを適用する。

また、カメラには、`<T.PerspectiveCamera>` を設定し、 `on:create={({ ref }) => ref.lookAt(0, 1.8, 0)}` と設定し、視点方向にカメラを向けるように設定する。

```svelte:Scene.svelte
<script lang="ts">
   import { T } from '@threlte/core'
   import { XR } from '@threlte/xr'
</script>

<XR />

<T.AmbientLight />
<T.DirectionalLight />

<T.PerspectiveCamera
	makeDefault
	position={[0, 1.8, 1]}
	on:create={({ ref }) => ref.lookAt(0, 1.8, 0)}
/>

<!-- floor -->
<T.Mesh position.y={-50}>
  <T.CylinderGeometry args={[2, 2, 100]} />
  <T.MeshStandardMaterial color="white" />
</T.Mesh>
```

![output1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/c857326c-e9b3-90b7-c631-2a60def3c59a.gif)


### VR セッションの切り替えを実装する

`<VRButton>` コンポーネントを使用して、VR 上でクリックできるボタンを追加する。
これにより、XR 対応のデバイスであれば XR セッションを開始するボタンを配置でき、さらには、XR セッション時に、XR セッションを終了するボタンを配置できる。

```diff_vue:App.svelte
<script lang="ts">
	import { Canvas } from '@threlte/core';
+	import { VRButton } from '@threlte/xr';
	import Scene from './Scene.svelte';
</script>

<div>
	<Canvas>
        <Scene />
	</Canvas>
+	<VRButton />
</div>
```

## コントローラ―の有効化

まずはコントローラー用のコンポーネントを作成する。
`@threlte/er` の `<Controller>` コンポーネントを利用して適用する。

```svelte:Controllers.svelte
<script lang="ts">
	import { Controller } from '@threlte/xr';
</script>

<Controller left />
<Controller right />
```

そしてこのコントローラーをシーンの XR セッション作成部分に内包する。

```diff_svelte:Scene.svelte
<script lang="ts">
	import { T } from '@threlte/core';
	import { XR } from '@threlte/xr';
	import Controllers from './Controllers.svelte';
</script>

<XR>
+	<Controllers />
</XR>

<!-- 省略 -->
```

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/96ac20bd-4a5f-ca08-1166-0c1b7b7544a3.gif)


(コントローラ―が表示されてないタイミングは、ハンドトラッキング状態になっている。)

たったこれだけでコントローラー表示できるようになるのすごい。

### ハンドトラッキングに対応する

ハンドトラッキングに対応する場合は、同様に `@threlte/xr` から `<Hand>` コンポーネントを取得してきて使用する。
コンポーネント上には二種類が並ぶことになるが、デバイス側の切り替えを認識しているので、コントローラー操作かハンドトラッキング操作かに応じて自動で表示が切り替わる。

```diff_svelte:Controllers.svelte
<script lang="ts">
+	import { Controller, Hand } from '@threlte/xr';
</script>

<Controller left />
<Controller right />

+<Hand left />
+<Hand right />
```

![output4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/0d1accc5-828e-4149-6cfd-7a86fdbd2178.gif)



これだけっ！すごい。

### コントローラ―にオブジェクトをアタッチする

コントローラ―に対してオブジェクトをアタッチする場合は、`<Controller>` や `<Hand>` コンポーネントに内包する。

例えば、左側のコントローラーのみに適用する場合は、次の様に `<Controlller left>` の方にだけ適用する。

```diff_svelte:Controllers.svelte
<script lang="ts">
	import { Controller, Hand } from '@threlte/xr';
+	import { T } from '@threlte/core';
</script>

<Controller left>
+	<T.Mesh>
+		<T.BoxGeometry args={[0.2, 0.2, 0.2]} />
+		<T.MeshStandardMaterial roughness={0} color="green" />
+	</T.Mesh>
</Controller>
<Controller right />

<Hand left />
<Hand right />
```

![output5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/f509e85a-6b0f-cb6c-dc26-fb90550333c8.gif)


## ジオメトリのインタラクティブ化

「ジオメトリのインタラクティブ化」(「インタラクティブメッシュの配置」などではなく)と書いたのは、配置ジオメトリは簡単にインタラクティブ化できるから。

まずジオメトリに対しては、 `<T.Mesh>` コンポーネントの `on:` プロップに対して次の様に渡す。
例えばクリックした際の処理を追加する場合は以下のように `on:click` プロップに関数を渡す。

```diff_svelte
<script lang="ts">
    import { T } from "@threlte/core";

    // マテリアル周りの値と変更イベント用関数
    let color = 'yellow'
    function onClicked() {
        if (color === 'yellow') {
            color = 'blue'
        } else {
            color = 'yellow'
        }
    }
</script>

<T.Mesh
+	on:click={onClicked}
	position.z={-10}
>
	<T.SphereGeometry />
    <!-- メッシュをクリックしたさいに色を変更 -->
	<T.MeshStandardMaterial bind:color />
</T.Mesh>
```

:::note info
**ポインタコントロールのイベントの種類**

ドキュメントにも書いているものが大半だが、一部例題実装の方にのみ見つけられるものもあるので、要確認。

* **click**
    * クリックイベント（ポインターでの押し込みと離しの両方の操作）
* **pointerup**
    * ポインターの押し込みを話した際のイベント
* **pointerdown**
    * ポインターの押し込みがあった際のイベント
* **pointerover**
    * ポインターのレイがオブジェクトに被った際のイベント
* **pointerout**
    * ポインターのレイがオブジェクトへの被りから外れた祭のイベント
* **pointerenter**
    * ポインターのレイが対象のオブジェクトの境界から入った際のイベント
* **pointerleave**
    * ポインターのレイが対象のオブジェクトの境界から外れた祭のイベント
* **pointermove**
    * ポインターのレイが対象のオブジェクトに被っている状態で動いてる際のイベント
* **pointermissed**
    * ポインターのレイが対象のオブジェクトから外れた祭のイベント
    * (pointerout/pointerleave との違いは未調査)
:::

### ポインタコントロールを追加する

ポインタでのコントロールを有効にするためには、`pointerControls()` を使って有効にする。

```svelte
<script lang="ts">
	// 省略
	import { XR, pointerControls } from '@threlte/xr';

	// ポインタコントロールを有効に
	pointerControls('right');
	pointerControls('left');
</script>
```

:::note info
**シーン内でポインタコントロールの有効／無効を切り替えたい場合**

次の方法で、有効／無効を切り替えることもできる。

```svelte
<script>
  import { pointerControls } from '@threlte/xr'
  const { enabled } = pointerControls('left', { enabled: false })

  enabled.set(true)
</script>

```
:::


### サンプル

これらを用いて以下のように、ポインタをオブジェクトにかぶせた際、 `roughness` を変更し、クリックした際には色 (`color`) を変更すると言ったことも実装できる。

```svelte:Scene.svelte
<script lang="ts">
	import { T } from '@threlte/core';
	import { XR, pointerControls } from '@threlte/xr';
	import { Environment } from '@threlte/extras';
	import Controllers from './Controllers.svelte';

	// ポインタコントロールを有効に
	pointerControls('right');
	pointerControls('left');

	// マテリアル周りの値と変更イベント用関数
	let roughness = 0;
	let color = 'yellow';
 
	function onClicked() {
		if (color === 'yellow') {
			color = 'blue';
		} else {
			color = 'yellow';
		}
	}
 
	function onPointerOververd() {
		roughness = 1;
	}

	function onPointerOut() {
		roughness = 0;
	}
</script>

<XR>
	<Controllers />
</XR>

<T.AmbientLight />
<T.DirectionalLight />

<Environment files="./textures/pedestrian_overpass_1k.hdr" isBackground={true} />

<T.PerspectiveCamera
	makeDefault
	position={[0, 1.8, 1]}
	on:create={({ ref }) => ref.lookAt(0, 1.8, 0)}
/>

<T.Mesh
	on:click={onClicked}
	on:pointerover={onPointerOververd}
	on:pointerout={onPointerOut}
	position.z={-10}
>
	<T.SphereGeometry />
	<T.MeshStandardMaterial bind:roughness metalness={1} bind:color />
</T.Mesh>
```

![output3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/d71e312e-e216-1b25-bed8-fe8c095af423.gif)


## VR セッション外でもインタラクティビティを有効化する

VR セッションに入ってない場合、基本的にはポインタの操作イベントなどは認識されない。
しかしながら、 `@threlte/extras` の `intaractivity()` を併用すると、VR セッションに入っていなくても操作することが可能になる。

こうした VR ページの場合、どうしてもセッション内に入っていないとページが固まっている様に見えてしまう。これを回避するための方法としても有効。

```diff_svelte:Scene.svelte
<script lang="ts">
	import { T } from '@threlte/core';
+	import { interactivity } from '@threlte/extras';
	import { XR, pointerControls } from '@threlte/xr';

    // VR セッション外でも操作を可能にする
+    interactivity();

 	// ポインタコントロールを有効に
	pointerControls('right');
	pointerControls('left');
</script>
```

# AR

:::note warn
AR については現在明確な使用方法がドキュメントにないので、要調査。
:::