LoginSignup
3
5

More than 1 year has passed since last update.

【React Three Fiber】DraggableなMeshの実装

Posted at

概要

React Three Fiberで、ドラッグ可能なメッシュの実装方法をまとめました。

https://rgn91.csb.app/
output(video-cutter-js.com).gif

この記事は、以下のコースの受講に伴って、ドラッグ操作について内容を整理して改めてまとめたものです。

このコースは、少し情報(使用しているライブラリのバージョン)が古いです。 受講してみたい方で、最新のライブラリのバージョンを使用したい方TypeScriptで実装したい方は、私が受講した時のリポジトリが役に立つかもしれないです。ご参考の際は、Read Meを読んでください。

実装

App.tsx
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.

この内、cameradomElementは、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" />
console.log
Scene
//・・・
> orbitControls: OrbitControls {object: PerspectiveCamera, domElement: canvas, enabled: true, target: Vector3, minDistance: 0, …}

あとは、dragstartdragendイベントで、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]を押して、ソースのテクスチャファイルを選択して、新しいタブで開きます。

画像が新規画面で表示されるので、保存します。

3
5
0

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
3
5