(1)はじめに
普段はソーシャルワーカーとして障がいをお持ちの方の支援をしています。ReactやReact Nativeでアプリ作成を楽しんでおり、最近は3D表示に関心がありThree.jsなどを使っています。前回はReact Nativeで3D表示してみようの続き で三人称視点のコードを紹介しました。Advent Calendarに合わせてバージョンアップさせたのでまとめてみました。
(2)3Dキャラクターを動かしてみよう
![20211215_010511.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635004/e479b1d4-44e6-000e-2007-3e4aaa3401d7.gif)前回までは四角いオブジェクトを表示していましたが、今回はキャラクターを表示してみます。また、バーチャルスティックを円形に改良しましたので、そちらもコードを書いてみたいと思います。
参考にしたサイト
Three.js公式
Expo公式 GLView
Mixamo公式
@nemutasさんの記事
【React】react-three-fibarで3D表現をする(Mixamoを使ったアニメーションモデル)
(3)使った技術
・react-native
・expo 42.0.1
・expo-gl 10.4.2
・expo-three 6.0.1
・gsap 3.6.0
・three 0.132.0 ← 今回のコードはバージョンによってはエラーになります
・base-64 1.0.0
・expo-asset 8.3.3
(4)バーチャルスティック
まずはバーチャルスティックについて、前回は四角形バックが四角形だったので可動範囲の座標が決まっていたのですが、今回は円形のため可動範囲は計算が必要になります。
//Position.js
import React, { useState } from "react";
import { View, Text } from "react-native";
export default function Position(props) {
// onMoveは画面にタッチして指を動かした時に発火するイベント App.jsに渡す
// onEndは画面から指を話した時に発火するイベント App.jsに渡す
const { onMove, onEnd } = props;
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,
};
// atan2でラジアンを算出する
const radian = Math.atan2(y, x);
// 大きい円と小さい円の中心が接している座標を算出
// ラジアンからコサインを算出し、大きい円の半径と乗算し座標を算出する
let limitX = largeRadius - smallRadius + largeRadius * Math.cos(radian);
let limitY = largeRadius - smallRadius + largeRadius * Math.sin(radian);
// タッチした座標と大きい円の座標の小さい値をx座標、y座標にセットする
setX(Math.min(coordinates.x, limitX));
setY(Math.min(coordinates.y, limitY));
onMove({ x: x, y: y });
};
// 指を話した時に実行される関数、座標を中心(初期座標)に戻しonEndイベントを渡す
const handleTouchEnd = () => {
setX(largeRadius - smallRadius);
setY(largeRadius - smallRadius);
onEnd();
};
return (
<>
<View>
<Text>
x:{Math.trunc(x - 60)}, y:{Math.trunc(y - 60)}
</Text>
<View
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: 2 * largeRadius, // 半径 * 2
height: 2 * largeRadius,
borderRadius: largeRadius, // 円形にする
backgroundColor: "black",
}}
>
<View
pointerEvents="none"
style={{
height: 2 * smallRadius, // 半径 * 2
width: 2 * smallRadius,
borderRadius: smallRadius, // 円形にする
backgroundColor: "blue",
position: "absolute",
transform: [{ translateX: x }, { translateY: y }],
}}
/>
</View>
</View>
</>
);
}
const radian = Math.atan2(y, x);
ラジアン(角度の単位)はMath関数で算出することができます。
let limitX = largeRadius - smallRadius + largeRadius * Math.cos(radian)
半径にコサインを乗算(largeRadius * Math.cos(radian))し
原点(largeRadius - smallRadius)を加算することで、そのラジアンにおけるx座標が算出されます。
setX(Math.min(coordinates.x, limitX));
Math.minで小さい値を返します。y座標も同様に算出することで円内の可動範囲を制限することができます。
小さい円の座標を飛ぶときがありますが、おおむね期待通り動いてくれます。
(5)3Dモデル表示
Mixamoからデータを作成しfbxファイルをBlenderに読み込ませてglbファイルで書き出すところは、@nemutasさんの記事を参照いただければと思います。metro.config.jsを作成し、"glb", "gltf" を読み込めるように追記してください。
```ts //metro.config.js module.exports = { resolver: { sourceExts: ["js", "jsx", "json", "ts", "tsx", "cjs"], assetExts: ["db", "mp3", "ttf", "obj", "png", "jpg", "glb", "gltf"], }, }; ``` React Nativeでglbファイルを表示して動かせるコードになります。できる限りコメントしました。//App.js
import React, { useState } from "react";
import { View } from "react-native";
import { GLView } from "expo-gl";
import { Renderer } from "expo-three";
import { TweenMax } from "gsap";
import {
PointLight,
GridHelper,
PerspectiveCamera,
Scene,
AnimationMixer, // アニメーションのため追加
Clock, // アニメーションのため追加
} from "three";
import Positon from "./Position"; // バーチャルスティックのjs
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Asset } from "expo-asset"; // ファイル読み込みのため追加
export default function App() {
const [cameras, setCameras] = useState(null);
const [models, setModels] = useState(null); // 3Dモデルをセットする変数
const [walk, setWalk] = useState(true); // アニメーションをセットする変数
// TweenMax.to(何が, 何秒で, { z軸に distance 分移動 })
const move = (props) => {
walk.paused = false; // .paused = アニメーションを一時停止解除
walk.play(); // アニメーションである変数walkを再生
TweenMax.to(models.position, 0.1, {
z: models.position.z + props.y,
x: models.position.x + props.x,
});
TweenMax.to(cameras.position, 0.1, {
z: cameras.position.z + props.y,
x: cameras.position.x + props.x,
});
// y座標を反転させ radian に +1.5 加算し前後左右にいい感じで振り向かせる
models.rotation.y = Math.atan2(-props.y, props.x) + 1.5;
};
// Position.jsから受け取ったonEnd(画面から指を離すことで発火する)で実行
const end = () => {
walk.paused = true; // .paused = アニメーションを一時停止解除
};
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)); //グリッドを表示
// GLTFをロードする
const loader = new GLTFLoader();
// アセットフォルダのglbファイルを読み込み
const asset = Asset.fromModule(require("./assets/test.glb")); // Mixamoで作成したtest.glb
await asset.downloadAsync();
let mixer;
let clock = new Clock();
loader.load(
// glbファイルをロードして3D空間に表示させる
asset.uri || "",
(gltf) => {
const model = gltf.scene;
model.position.set(0, 0, 0); // 配置される座標 (x,y,z)
model.rotation.y = Math.PI;
const animations = gltf.animations;
//Animation Mixerインスタンスを生成
mixer = new AnimationMixer(model);
// glbファイルのアニメーション
let animation = animations[0];
// アニメーションを変数walkにセット
setWalk(mixer.clipAction(animation));
// test.glbを3D空間に追加
scene.add(model);
setModels(model);
},
(xhr) => {
console.log("ロード中");
},
(error) => {
console.error("読み込めませんでした");
}
);
// 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);
// カメラの初期座標
let cameraInitialPositionX = 0;
let cameraInitialPositionY = 2;
let cameraInitialPositionZ = 7;
// カメラの座標
camera.position.set(
cameraInitialPositionX,
cameraInitialPositionY,
cameraInitialPositionZ
);
const render = () => {
requestAnimationFrame(render); // アニメーション moveUd関数、moveLr関数でカメラ座標が移動
renderer.render(scene, camera); // レンダリング
//Animation Mixerを実行
if (mixer) {
mixer.update(clock.getDelta());
}
gl.endFrameEXP(); // 現在のフレームを表示する準備ができていることをコンテキストに通知するpresent (Expo公式)
};
render();
}}
/>
</View>
<View style={{ flexDirection: "row", alignSelf: "center" }}>
<Positon
// Position.jsからonMoveを受け取ってmove関数を実行
onMove={(data) => {
move({
x: (data.x - 60) / 1000,
y: (data.y - 60) / 1000,
});
}}
// Position.jsからonEndを受け取ってend関数を実行
onEnd={end}
/>
</View>
</>
);
}
(6)終わりに
3Dキャラクターになると一気にそれらしくなりますね。React Nativeでの3D表示は手探りで面白いです。Mixamoにvrmファイルを読み込ませるとVroidを表示させることもできるので、新たなアプリの可能性が広がりそうです