React Nativeで3D表示してみようの続き

<h1>（１）React Native(Expo)で3D表示</h1>

<p>普段はソーシャルワーカーとして障がいをお持ちの方の支援をしています。ReactやReact Nativeでアプリ作成を楽しんでおり、最近は3D表示に関心がありThree.jsなどを使っています。前回は3D表示で簡単な一人称視点のコードを紹介しました。さすがにタップ連打で移動は大変なので、これも簡単なバーチャルスティックを追加してみようと思います
。</p>



<h1>（２）今回は三人称視点で3D空間を動いてみる</h1>

<p>今回は三人称視点で動いてみようと思います。前回は一人称だったのでカメラの位置座標を動かしていましたが、今回はバーチャルスティックで入力された数値分オブジェクトも一緒に動かしてみます。</p>

![20211211_005234.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635004/74922648-3503-ddad-2d24-c6cad619eb17.gif)



<a href="https://qiita.com/syeha/items/2c58d08ebb818458116d">React Native で3D表示してみよう（前回）</a>
<p>参考にしたサイト</a>
<a href="https://threejs.org/">Three.js公式</a>
<a href="https://docs.expo.dev/versions/latest/sdk/gl-view/">Expo公式 GLView</a>

<h1>（３）使った技術</h1>

<p>
・react-native<br>
・expo 42.0.1<br>
・expo-gl 10.4.2<br>
・expo-three 6.0.1<br>
・gsap 3.6.0<br>
・three 0.134.0

<h1>（４）コード</h1>
<p>まずは読み込み</p>
```ts
//App.js
import React from "react";
import { View } from "react-native";
import { GLView } from "expo-gl";
import { Renderer } from "expo-three";
import { TweenMax } from "gsap";
import {
  PointLight,
  GridHelper,
  Mesh,
  PerspectiveCamera,
  Scene,
  BoxGeometry,
  MeshLambertMaterial,
} from "three";
import Positon from "./Position";　// バーチャルスティック

```
可能な限りコメントしました

```ts
//App.js
export default function App() {
  const [cameras, setCameras] = useState(null); // cameraを読めるよう変数にします
  const [cubes, setCubes] = useState(null); // cubeを読めるよう変数にします

  // TweenMax.to(何が, 何秒で, { z軸に distance 分移動 })
  //オブジェクトとカメラを同時に動かすことで三人称視点にする
  const move = (distance) => {
    TweenMax.to(cubes.position, 0.1, {
      z: cubes.position.z + distance.y,
      x: cubes.position.x + distance.x,
    });
    TweenMax.to(cameras.position, 0.1, {
      z: cameras.position.z + distance.y,
      x: cameras.position.x + distance.x,
    });
  };

  return (
    <>
      <View style={{ flex: 1 }}>
        <GLView
          style={{ flex: 1 }}
          onContextCreate={async (gl) => {
            // 3D空間の準備
            const { drawingBufferWidth: width, drawingBufferHeight: height } =
              gl;
            const renderer = new Renderer({ gl }); // レンダラーの準備
            renderer.setSize(width, height); // 3D空間の幅と高さ
            renderer.setClearColor("white"); // 3D空間の配色
            const scene = new Scene(); // これが3D空間
            scene.add(new GridHelper(100, 100)); //グリッドを表示

            // 配置するオブジェクト
            const geometry = new BoxGeometry(2, 2, 2); // 四角い物体
            const material = new MeshLambertMaterial({ color: "blue" }); // 物体に光を反射させ色や影を表現する
            cube = new Mesh(geometry, material); // geometryとmaterialでオブジェクト完成
            cube.position.set(0, 1, 0); // 配置される座標 (x,y,z)
            scene.add(cube); // 3D空間に追加
            setCubes(cube); // 変数cubesにセットします

            // 3D空間の光！
            const pointLight = new PointLight(0xffffff, 2, 1000, 1); //一点からあらゆる方向への光源(色, 光の強さ, 距離, 光の減衰率)
            pointLight.position.set(0, 200, 200); //配置される座標 (x,y,z)
            scene.add(pointLight); //3D空間に追加

            // カメラが映し出す設定(視野角, アスペクト比, near, far)
            const camera = new PerspectiveCamera(45, width / height, 1, 1000);
            setCameras(camera); // 変数camerasにセットします
            // カメラの初期座標
            let cameraInitialPositionX = 0;
            let cameraInitialPositionY = 2;
            let cameraInitialPositionZ = 7;
            // カメラの座標
            camera.position.set(
              cameraInitialPositionX,
              cameraInitialPositionY,
              cameraInitialPositionZ
            );

            const render = () => {
              cube.rotation.x += 0.01; // 繰り返しオブジェクトを回転させる
              cube.rotation.y += 0.01;
              requestAnimationFrame(render); // アニメーション
              renderer.render(scene, camera); // レンダリング
              gl.endFrameEXP(); // 現在のフレームを表示する準備ができていることをコンテキストに通知するpresent (Expo公式)
            };
            render();
          }}
        />
      </View>
      <View style={{ flexDirection: "row", alignSelf: "center" }}>
        // Position.jsから取得したx座標 y座標を除算して 関数moveに渡す
        <Positon 
          onMove={(data) => {
            move({ x: (data.x - 60) / 1000, y: (data.y - 60) / 1000 });
          }}
        />
      </View>
    </>
  );
}

```

App.jsに渡すx座標、y座標をバーチャルスティックを使って取得する

```ts
// Position.js
import React, { useState } from "react";
import { View, Text } from "react-native";

export default function Position(props) {
  const { onMove } = props;　// App.jsに渡します

  const largeRadius = 90; // 外側の四角の一片の半分
  const smallRadius = largeRadius / 3; // 中の円の半径

  const [x, setX] = useState(largeRadius - smallRadius); // 四角の一片の半分 - 小さい円の半径の差 x
  const [y, setY] = useState(largeRadius - smallRadius); // 四角の一片の半分 - 小さい円の半径の差 y

  const handleTouchMove = (e) => {
    const touchX = e.nativeEvent.locationX; // 画面をタッチしたときのx座標
    const touchY = e.nativeEvent.locationY; // 画面をタッチしたときのy座標
    // 小さい円の半径を差し引く
    let coordinates = {
      x: touchX - smallRadius,
      y: touchY - smallRadius,
    };
    // 外側の四角の範囲内の数値だけを取得したいので
    setX(Math.min(120, Math.max(0, coordinates.x))); // Math関数でxの範囲を設定 0 < x < 120
    setY(Math.min(120, Math.max(0, coordinates.y))); // Math関数でyの範囲を設定 0 < y < 120
    onMove({ x: x, y: y });
 };
  // 指を外した時に実行される関数 x座標、y座標をリセットし小さい円が真ん中に戻る
  const handleTouchEnd = () => { 
    setX(largeRadius - smallRadius); 
    setY(largeRadius - smallRadius);
  };

  return (
    <>
      <View>
        <Text>
          x:{Math.trunc(x - 60)}, y:{Math.trunc(y - 60)}　// Math.truncで少数以下をまるめる
        </Text>
        <View
          onTouchMove={handleTouchMove} // タッチして指を動かした時
          onTouchEnd={handleTouchEnd} // 指を画面から外した時
          style={{
            width: 2 * largeRadius,
            height: 2 * largeRadius,
            backgroundColor: "black",
          }}
        >
          <View
            pointerEvents="none"
            style={{
              height: 2 * smallRadius, // 直径
              width: 2 * smallRadius,
              borderRadius: smallRadius, // まーるい円にする
              backgroundColor: "blue",
              position: "absolute",
              transform: [{ translateX: x }, { translateY: y }],　// 小さい円の座標
            }}
          />
        </View>
      </View>
    </>
  );
}
```

<h1>（５）終わりに</h1>

<p>Advent Calendarがあるとモチベーションが上がりますね！バーチャルスティックでできることが見つかるといいな。React NativeでのVRMモデル読み込みが実現できず、Reactよりもできることが限られてしまい残念です。いろいろなオブジェクトが表示できると広がりそう:relieved:</p>