概要
React Three Fiberで、ドラッグ可能なメッシュの実装方法をまとめました。
この記事は、以下のコースの受講に伴って、ドラッグ操作について内容を整理して改めてまとめたものです。
このコースは、少し情報(使用しているライブラリのバージョン)が古いです。
受講してみたい方で、最新のライブラリのバージョンを使用したい方、TypeScriptで実装したい方は、私が受講した時のリポジトリが役に立つかもしれないです。ご参考の際は、Read Meを読んでください。
実装
import React, { Suspense, useEffect, useRef, VFC } from 'react';
import * as THREE from 'three';
import { DragControls } from 'three-stdlib';
import { OrbitControls, Sphere, Stats, TorusKnot, useHelper, useTexture } from '@react-three/drei';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
export const App: VFC = () => {
return (
<Canvas
camera={{
position: [0, 10, 8],
fov: 50,
aspect: window.innerWidth / window.innerHeight,
near: 0.1,
far: 2000
}}
dpr={window.devicePixelRatio}
shadows>
{/* canvas color */}
<color attach="background" args={['#1e1e1e']} />
{/* fps */}
<Stats />
{/* camera controller */}
<OrbitControls attach="orbitControls" />
{/* lights */}
<Draggable>
<PointLight />
</Draggable>
{/* objects */}
<Suspense fallback={null}>
<Draggable>
<ToonTorusKnot textureName="threeTone" color="#22cefa" position={[1, 2, 0]} />
</Draggable>
<Draggable>
<ToonTorusKnot textureName="fiveTone" color="#ea7600" position={[-1, 2, 0]} />
</Draggable>
</Suspense>
{/* helper */}
<axesHelper />
<gridHelper position={[0, 0.01, 0]} args={[10, 10, 'red', 'black']} />
</Canvas>
)
}
// ==============================================
type ToonTorusKnotProps = {
textureName: string
color: string
position: [number, number, number]
}
const ToonTorusKnot: VFC<ToonTorusKnotProps> = props => {
const { textureName, color, position } = props
const ref = useRef<THREE.Mesh>(null)
const toneMap = useTexture(`/assets/textures/${textureName}.jpg`)
toneMap.minFilter = THREE.NearestFilter
toneMap.magFilter = THREE.NearestFilter
useFrame(() => {
ref.current!.rotation.x += 0.005
ref.current!.rotation.y += 0.005
})
return (
<TorusKnot ref={ref} args={[0.5, 0.2, 128, 128]} position={position}>
<meshToonMaterial color={color} gradientMap={toneMap} />
</TorusKnot>
)
}
// ==============================================
const PointLight: VFC = () => {
const lightRef = useRef<THREE.Light>()
useHelper(lightRef, THREE.PointLightHelper, [0.5])
return (
<group position={[0, 5, 0]}>
<Sphere scale={0.2}>
<meshBasicMaterial color="#f3f3f3" />
</Sphere>
<pointLight
ref={lightRef}
intensity={1}
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
castShadow
/>
</group>
)
}
// ==============================================
type DraggableProps = {
children: React.ReactNode
}
const Draggable: VFC<DraggableProps> = ({ children }) => {
const ref = useRef<THREE.Group>(null)
const { camera, gl, scene } = useThree()
useEffect(() => {
const controls = new DragControls(ref.current!.children, camera, gl.domElement)
controls.transformGroup = true
const orbitControls = (scene as any).orbitControls
controls.addEventListener('dragstart', () => {
orbitControls.enabled = false
})
controls.addEventListener('dragend', () => {
orbitControls.enabled = true
})
}, [camera, gl.domElement, scene])
return <group ref={ref}>{children}</group>
}
Draggable Componentの実装
実装にはDragControlsを使用します。
type DraggableProps = {
children: React.ReactNode
}
const Draggable: VFC<DraggableProps> = ({ children }) => {
const ref = useRef<THREE.Group>(null)
const { camera, gl, scene } = useThree()
useEffect(() => {
const controls = new DragControls(ref.current!.children, camera, gl.domElement)
controls.transformGroup = true
// console.log(scene)
const orbitControls = (scene as any).orbitControls
controls.addEventListener('dragstart', () => {
orbitControls.enabled = false
})
controls.addEventListener('dragend', () => {
orbitControls.enabled = true
})
}, [camera, gl.domElement, scene])
return <group ref={ref}>{children}</group>
}
インスタンスを作成するには、引数に以下の3つが必要です。
- objects: An array of draggable 3D objects.
- camera: The camera of the rendered scene.
- domElement: The HTML element used for event listeners.
この内、camera
とdomElement
は、useThreeフック経由で取得することができます。
objects
には、ドラッグさせたいオブジェクトの配列を渡します。配列なので、引数で渡されたコンポーネント(children)をgroup化して、それを引数に渡します。ドラッグは、配列で渡されたオブジェクトそれぞれについて適用されます。
ただし、ドラッグできるオブジェクトは、そもそもドラッグが可能なオブジェクトに限ります。今回の例で言えば、Meshオブジェクトはドラッグ可能で、Lightオブジェクトはドラッグ不可です。
groupに対してドラッグを適用したい場合は、controls.transformGroup = true
を設定します。groupにドラッグ可能なオブジェクトが1つでも含まれていれば、group自体がドラッグされるようになります。
このため、PointLightオブジェクトはドラッグ不可ですが、PointLightオブジェクトと一緒に渡されているSphereジオメトリがドラッグ可能で、それらをまとめてgroup化しているので、結果的にPointLightオブジェクトもドラッグ操作で移動します。
orbitControlsの無効化
orbitControls
を追加している場合、ドラッグ操作でカメラも移動してしまします。そのため、ドラッグをしているときはカメラの操作を無効にする必要があります。
orbitControlsオブジェクトは、sceneオブジェクトから取得することができます。ただし、orbitControls追加時に明示的にattachする必要があります。
<OrbitControls attach="orbitControls" />
Scene
//・・・
> orbitControls: OrbitControls {object: PerspectiveCamera, domElement: canvas, enabled: true, target: Vector3, minDistance: 0, …}
あとは、dragstart
・dragend
イベントで、orbitControlsのtrue/falseを切り替えます。
const orbitControls = (scene as any).orbitControls
controls.addEventListener('dragstart', () => {
orbitControls.enabled = false
})
controls.addEventListener('dragend', () => {
orbitControls.enabled = true
})
MeshToonMaterial
meshのmaterialには、MeshToonMaterialを使用しています。
const ToonTorusKnot: VFC<ToonTorusKnotProps> = props => {
const { textureName, color, position } = props
const ref = useRef<THREE.Mesh>(null)
const toneMap = useTexture(`/assets/textures/${textureName}.jpg`)
toneMap.minFilter = THREE.NearestFilter
toneMap.magFilter = THREE.NearestFilter
useFrame(() => {
ref.current!.rotation.x += 0.005
ref.current!.rotation.y += 0.005
})
return (
<TorusKnot ref={ref} args={[0.5, 0.2, 128, 128]} position={position}>
<meshToonMaterial color={color} gradientMap={toneMap} />
</TorusKnot>
)
}
gradientMapを指定して、影を3段階、5段階にしています。
texture画像は、three.jsの公式サイトから拝借しました。ダウンロード方法はおまけを参照してください。
PointLight
ライトは、位置がわかりやすいようにSphereジオメトリも追加しています。また、Sphereジオメトリを追加したことでPointLightもドラッグすることができます。
const PointLight: VFC = () => {
const lightRef = useRef<THREE.Light>()
useHelper(lightRef, THREE.PointLightHelper, [0.5])
return (
<group position={[0, 5, 0]}>
<Sphere scale={0.2}>
<meshBasicMaterial color="#f3f3f3" />
</Sphere>
<pointLight
ref={lightRef}
intensity={1}
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
castShadow
/>
</group>
)
}
ライト自体(pointLight)が本当にドラッグできているのか確認するために、helperを追加しています。
useHelperを使用することで簡単に実装することができます。特に、第3引数([0.5])にはPointLightHelperのインスタンス生成時に渡すsphereSize
を渡します。
成果物
おまけ
Texture 画像のダウンロード方法
MeshToonMaterialのページから、新規Windowでサンプルを開きます。
[F12]を押して、ソースのテクスチャファイルを選択して、新しいタブで開きます。
画像が新規画面で表示されるので、保存します。