36
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React (Typescript) + react-three-fiber + three-vrmでVRMモデルを表示してみる

Last updated at Posted at 2020-04-10

手軽に3Dモデルを使ってみたい。でもUnityは学習コスト高そうだし、手慣れたWeb系の言語でどうにかできないものか。
というわけで今回、タイトルにあるライブラリを使ってVRMモデルを表示させてみました。

なおここでは、React環境の作り方などの説明は割愛します。

環境

Package Version Explanation
React 16.13 りあくと
Typescript 3.8.3 たいぷすくりぷと
Three 0.115.0 WebGLを使ってJSで3Dモデルを描画できるライブラリ
react-three-fiber 4.0.27 React上でThree.jsを手軽に使えるやーつ
@pixiv/three-vrm 0.3.3 Three.js上で簡単にVRMファイルを扱える便利なやつ

react-three-rendererというのもあるが凍結しているようで、開発陣も「代わりにfiber応援してネ」って言ってる。ちなみにreact-three-fiberはReactNativeにも対応しているらしい。

そもそもVRMファイルとは

ドワンゴが作った、人型3Dデータ用のファイル形式です。
最近VRChatを始めいろんなところで3Dモデルの需要が増えている中で、ソフトごとに形式が違ってはせっかく作った3Dキャラもったいない!ということで作られたフォーマットらしいです。画像の jpgとかpngとか、ハードウェアで言うならUSBとかと同じですね。
共通フォーマットができたことで、Vroid Hubみたいな、有志の方が作成した3Dキャラを手軽に見ることができるようになったり、中には無償で配布されているVRMファイルもあります!

やってみる

0. React環境を作る

create-react-appでも大丈夫です。

1. パッケージを追加

とりあえずパッケージを追加する。

yarn add -D three react-three-fiber @pixiv/three-vrm @types/three

2. 一旦react-three-fiberに慣れる

初めて使うので、一旦適当なオブジェクトを表示してみよう。と言うことで、立方体を表示してみます。

App.tsx
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'

import SampleBox from 'components/SampleBox'

export default function App() {
  return (
    <Container>
      <Canvas>
        <SampleBox />
      </Canvas>
    </Container>
  )
}

const Container = styled.div`
  width: 100vw;
  height: 100vh;
`
SampleBox.tsx
import React, { useRef } from 'react'
import { Mesh } from 'three'
import { useFrame } from 'react-three-fiber'

export default function SampleBox() {
  const ref = useRef({} as Mesh)
  useFrame(() => (ref.current.rotation.z += 0.01))

  return (
   <mesh ref={ref}>
      <boxBufferGeometry attach='geometry' />
      <meshBasicMaterial attach='material' color='hotpink' opacity={0.5} transparent />
    </mesh>
  )
}

SampleBoxコンポーネントが立方体です。
useFrameを使うことで、毎フレームごとにオブジェクトの位置や向きを変えられます。今回は向きのz座標を0.01ずつ足すことで回転を加えています。

ちなみにStyledComponentは、コンポーネントに直接styleを適用出来るライブラリです。個人的に可読性が爆上がりするのでオススメ。

これを実行したのがこちら
image

ピンクの立方体(?)がくるくる回っていますね。
しかしこのままだと立方体なのか平面なのかよくわからないので、カメラの位置をマウスで動かせるようにしましょう。

3. OrbitControlを追加する

マウスで拡大したり、カメラのアングルを自由に変えられるようにしましょう。
って言うのがOrbitControlです。

実はreact-three-fiberにあるんですが、これをtypescriptで使うと型参照できずエラーに。そこで型を定義しつつコンポーネントを作ってラップします。
https://github.com/react-spring/react-three-fiber/issues/27

Controls.tsx
import React, { useRef } from 'react'
import { extend, ReactThreeFiber, useThree, useFrame } from 'react-three-fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

extend({ OrbitControls })

declare global {
  namespace JSX {
    interface IntrinsicElements {
      readonly orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
    }
  }
}

export default function Controls(props: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>) {
  const {
    camera,
    gl: { domElement }
  } = useThree()
  const controls = useRef({} as OrbitControls)
  useFrame(() => controls.current.update())
  return <orbitControls {...props} ref={controls} args={[camera, domElement]} />
}
App.tsx
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'

import SampleBox from 'components/SampleBox'
+ import Controls from 'utils/Controls'

export default function App() {
  return (
    <Container>
      <Canvas>
        <SampleBox />
+        <Controls />
+        <gridHelper /> {/* わかりやすいようにGridPanelを表示 */}
      </Canvas>
    </Container>
  )
}

const Container = styled.div`
  width: 100vw;
  height: 100vh;
`

使い方としては、<Canvas>内で呼び出すだけ。実行してみましょう。
Image from Gyazo
マウスで動かすことができました!

4. VRMを描画する

ここからがやっと本題。

4-1. モデルファイルを配置する

まずは描画するためのVRMを用意しましょう。
今回はサンプルとして、three-vrmに入っている女の子をお借りします。可愛い。黒髪ロング最高。
https://github.com/pixiv/three-vrm/blob/dev/examples/models/three-vrm-girl.vrm
image.png

用意したVRMファイルはsrcではなく、distやpublicなどの配信ディレクトリに配置しましょう。
イメージとしては以下のような感じ。

├── public/
│   ├── models/
│   │   └── [ ファイル名 ].vrm	<-- ここ
│   ├── bundle.js
│   └── index.html
├── src/
│   ├── components/
│   ├── App.tsx
│   └── index.tsx
├── package.json
├── webpack.config.js
└── tsconfig.json

4-2. VRMコンポーネントを作成

VRMファイルを表示するためのコンポーネントを作ります。

VRMAsset.tsx
import React, { useState, useEffect } from 'react'
import { useLoader } from 'react-three-fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM, VRMUtils, VRMSchema } from '@pixiv/three-vrm'
import { Scene, Group } from 'three'

interface Props {
  url: string
}

export default function VRMAsset({ url }: Props) {
  const [scene, setScene] = useState<Scene | Group | null>(null)
  const gltf = useLoader(GLTFLoader, url)

  useEffect(() => {
    VRMUtils.removeUnnecessaryJoints(gltf.scene)
    VRM.from(gltf).then(vrm => {
      setScene(vrm.scene)
      // 初期描画で背中が映ってしまうので向きを変えてあげる
      const boneNode = vrm.humanoid?.getBoneNode(VRMSchema.HumanoidBoneName.Hips)
      boneNode?.rotateY(Math.PI)
    })
  }, [gltf, setScene])

  if (scene === null) {
    return null
  }

  return <primitive object={scene} dispose={null} />
}
SampleModel.tsx
import React, { Suspense } from 'react'
import VRMAsset from 'utils/VRMAsset'

export default function SampleModel() {
  return (
    <Suspense fallback={null}>
      <VRMAsset url='./models/model.vrm' />
    </Suspense>
  )
}

Propsで、VRMファイルのurlを受け取ります。ここに、先ほど配置したファイルのパスを指定します。
またVRM.fromはPromiseを返すので、Suspenseで囲ってあげましょう(本当はここでローディングを出すとそれっぽくなったりする)

App.tsx
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'
+ import SampleModel from 'components/SampleBox' 
- import SampleBox from 'components/SampleBox'

import Controls from 'utils/Controls'

export default function App() {
  return (
    <Container>
      <Canvas>
+        <SampleModel />
-        <SampleBox />
        <Controls />
+       <directionalLight position={[1, 1, 1]} />
        <gridHelper />
      </Canvas>
    </Container>
  )
}

const Container = styled.div`
  width: 100vw;
  height: 100vh;
`

さっきまで試してたSampleBoxを、新しく作ったSampleModelに置き換えます。

それからもう一つ。照明を追加しましょう。
<directionalLight>ってやつです。これがないと、真っ黒いモデルが出てきます。僕はこれを忘れて、小一時間「ポケモンだ〜れだ」をやってました。

それではいざ、実行。
Image from Gyazo

できたー! :tada: :tada: :tada:

番外編

今のままだと、カメラの初期位置が低すぎたり遠すぎたりするので、そこら辺を調整しました。最終的なコードがこちら。

Controls.tsx
import React, { useRef, useEffect } from 'react'
import { extend, ReactThreeFiber,  useThree, useFrame } from 'react-three-fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

extend({ OrbitControls })

declare global {
  namespace JSX {
    interface IntrinsicElements {
      readonly orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
    }
  }
}

interface Props extends ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls> {
  defaultCameraPosition?: [number, number, number]
}

export default function Controls(props: Props) {
  const {
    camera,
    gl: { domElement }
  } = useThree()
  const controls = useRef<OrbitControls>()
  const { defaultCameraPosition } = props

  useFrame(() => controls.current?.update())

  useEffect(() => {
    if (defaultCameraPosition !== undefined) {
      camera.position.set(...defaultCameraPosition)
    }
  }, [camera, defaultCameraPosition])

  return <orbitControls ref={controls} args={[camera, domElement]} screenSpacePanning {...props} />
}
App.tsx
import React from 'react'
import { Canvas } from 'react-three-fiber'
import styled from 'styled-components'

import Controls from 'utils/Controls'
import SampleModel from 'components/SampleModel'
import { Vector3 } from 'three'

export default function App() {
  return (
    <Container>
      <Canvas>
        <SampleModel />
        <Controls defaultCameraPosition={[0, 1.25, 1]} target={new Vector3(0, 1, 0)} />
        <directionalLight position={[1, 1, 1]} />
        <gridHelper />
      </Canvas>
    </Container>
  )
}

const Container = styled.div`
  width: 100vw;
  height: 100vh;
`

defaultCameraPosition(名前長い)を追加して、初期カメラ位置を指定出来るように。

最終結果

Image from Gyazo
モデルとちょうどいい位置にカメラをおくことができました :tada: :tada: :tada:

まとめ

VRコンテンツが増える中で、3Dモデルはもっと身近なものになっていく気がします。というかすでに乗り遅れている気が……。
そんな3Dモデルを手軽に扱えて、しかもブラウザ上で動くなんていいですね!

今度はこの3Dモデルを動かしてみたいと思うので、また進展があれば記事にしてみたいと思います。ではでは〜

36
24
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?