React-three-fiber: Panning / Orbit Controls

Created on 14 Mar 2019  路  20Comments  路  Source: pmndrs/react-three-fiber

I'm trying to implement a panning system on scroll events, and I have managed to get something working but it appears I've done it in the slowest way possible.

I've noticed you're using orbitalControls here (https://codesandbox.io/embed/mo0xrqrj79) and I'm trying to jam it into my system with little success. Are you able to explain a little how that all works?

It looks like the orbital controls tell the renderer how to position the camera, and you need to use useRender to tell the objects to rerender, and. Is that correct?

Here's my very slow attempt a panning (sorry, I couldn't get it rendering correctly on CodeSandbox)

import React, { useMemo, useState } from 'react';
import * as THREE from 'three/src/Three'
import { Canvas } from 'react-three-fiber'
import _ from 'lodash'

function Extrusion({ x, y, width, height, radius, ...props }) {
  const roundedRect = useMemo(() => {
    const roundedRect = new THREE.Shape()
    roundedRect.moveTo(x, y + radius);
    roundedRect.lineTo(x, y + height - radius);
    roundedRect.quadraticCurveTo(x, y + height, x + radius, y + height);
    roundedRect.lineTo(x + width - radius, y + height);
    roundedRect.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
    roundedRect.lineTo(x + width, y + radius);
    roundedRect.quadraticCurveTo(x + width, y, x + width - radius, y);
    roundedRect.lineTo(x + radius, y);
    roundedRect.quadraticCurveTo(x, y, x, y + radius);
    return roundedRect
  }, [x, y, width, height, radius])

  return (
    <mesh>
      <extrudeGeometry name="geometry" args={[roundedRect, props]} />
      <meshNormalMaterial name="material" />
    </mesh>
  )
}

function Bar({ start, end, yIndex }) {
  return <Extrusion
    x={start}
    y={yIndex}
    height={10}
    width={end - start}
    radius={3}
    bevelEnabled={false}
    depth={10}
  />
}

const makeRandomBar = () => {
  const start = _.random(-1000, 1000)
  const length = _.random(50, 500)
  const yIndex = _.random(-1000, 1000)
  return [start, start + length, yIndex]
}

const bars = _.range(0, 250).map(n => makeRandomBar())

function App() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  const scrollFunc = e => {
    setX(x + e.deltaX)
    setY(y + e.deltaY)
  }
  return (
    <div style={{ width: "100%", height: "100%", position: "absolute" }}>
      <Canvas camera={{ position: [0, 0, 300] }} onWheel={scrollFunc}>
        <group position={[x, y, 0]}>
          {bars.map(b => <Bar start={b[0]} end={b[1]} yIndex={b[2]} />)}
        </group>
      </Canvas>
    </div>
  );
}

export default App;

Most helpful comment

Ok, got it working thanks! Interestingly you need to add the following css otherwise the orbit / pan breaks if any parent container has position: absolute.

html, body {
  height: 100%;
  width: 100%;
} 

All 20 comments

I would recommend three js controls for sure. There exist a couple, orbit is just one of them. It sucks that three isn't fully modular still, so i just copy stuff from this PR: https://github.com/mrdoob/three.js/pull/15832

Controls need a reference to the camera, you can either create your own camera or use the default camera and fetch it via useThree, and it needs to be updated every frame, for this you should useRender. So basically:

import { apply, Canvas, useRender, useThree } from 'react-three-fiber'
import { OrbitControls } from '../resources/controls/OrbitControls'

// Make OrbitControls known as <orbitControls />
apply({ OrbitControls })

function Controls(props) {
  const ref = useRef()
  const { camera } = useThree()
  useRender(() => ref.current.obj.update())
  return <orbitControls ref={ref} args={[camera]} {...props} />
}

function App() {
  return (
    <div style={{ width: '100%', height: '100%', position: 'absolute' }}>
      <Canvas camera={{ position: [0, 0, 300] }}>
        <Controls />
        <group position={[x, y, 0]}>
          {bars.map(b => (
            <Bar start={b[0]} end={b[1]} yIndex={b[2]} />
          ))}
        </group>
      </Canvas>
    </div>
  )
}

Orbit has lots of options: https://threejs.org/docs/index.html#examples/controls/OrbitControls you can switch off rotation and zoom so that only pan is allowed.

Awesome, thanks! I've managed to get a few different control systems working.
However, everything is disappearing on my during pan and orbit no matter which system I use. I've logged the camera during the useRender function and it appears to be getting a few NaN values.

It doesn't appear to be doing it with your example in CodeSandbox. I have the exact same OrbitControl code so maybe it has something to do with the default camera? I'll play around with setting up a custom camera.

function Controls(props) {
  const ref = useRef()
  const { camera } = useThree()
  useRender(() => {
    console.log('Controls -> useRender -> camera', camera)
    ref.current.obj.update()
  })
  return <orbitControls ref={ref} args={[camera]} {...props} />
}

function App() {
  return (
    <div style={{ width: "100%", height: "100%", position: "absolute" }}>
      <Canvas camera={{ position: [0, 0, 300] }}>
        <Controls />
        <group>
            {bars.map((b,i) => <Bar key={i} start={b[0]} end={b[1]} yIndex={b[2]} />)}
        </group>
      </Canvas>
    </div>
  );
}

Nope didn't seem to make a difference. Any ideas?

function CanvasContent() {
  const camera = useRef()
  const controls = useRef()
  const { size, setDefaultCamera } = useThree()
  useEffect(() => void setDefaultCamera(camera.current), [])
  useRender(() => {
    console.log('cam', camera.current)
    controls.current.obj.update()
  })
  return (
    <group>
      <perspectiveCamera
        ref={camera}
        aspect={size.width / size.height}
        radius={(size.width + size.height) / 4}
        fov={55}
        position={[0, 0, 300]}
        onUpdate={self => self.updateProjectionMatrix()}
      />
      {camera.current && (
        <group>
          <orbitControls ref={controls} args={[camera.current]} enableDamping dampingFactor={0.1} rotateSpeed={0.1} />
          <group>
            {bars.map((b, i) => <Bar key={i} start={b[0]} end={b[1]} yIndex={b[2]} />)}
          </group>
        </group>
      )}
    </group>
  )
}

function App() {
  return (
    <div style={{ width: "100%", height: "100%", position: "absolute" }}>
      <Canvas>
        <CanvasContent />
      </Canvas>
    </div>
  );
}

Camera log
Screen Shot 2019-03-14 at 11 54 59 am

I haven't tried the code i posted, but doing it now ... and seems fine: https://codesandbox.io/s/387z7o2zrq it doesn't seem to pan up/down, but i think that is related to how these controls work, probably they need a specific up vector or something like that, it's purely threejs at that point.

As for NaN, controls write into the camera, if they are set up wrong or are missing any critical data, they can screw up the camera. Must be a reason somewhere why it's doing that. If you put that into a sandbox, i can investigate - would also be interesting to me if it's three-fibers fault.

Code sandbox is failing to load dependencies for me atm but I'm sure this is working https://codesandbox.io/s/14r8y3k144

I haven't been able to test the code above in sandbox, but strangely I'm having the same problems locally. The only difference I can possibly think of is the wrapper div <div style={{ width: "100%", height: "100%", position: "absolute" }}>.

Ok, the div is definitely this issue! But I need it to stop the canvas from expanding in size indefinitely. Trying to figure out another approach. Specifically, it's the position: absolute style, no problems with fixed dimensions.

your box worked for me, i upgraded three-fiber to latest and changed three/src/Three to just 'three'

https://codesandbox.io/s/o5mj1l8y0z

But i can pan the contents.

Ok, the div is definitely this issue! But I need it to stop the canvas from expanding in size indefinitely. Trying to figure out another approach.

The expanding happens when the container doesn't have a size. needs to be relative/absolute to something and have a width/height, percentages, fixed values, everything goes. Otherwise the resize observer will start to push the bounds.

Ok, got it working thanks! Interestingly you need to add the following css otherwise the orbit / pan breaks if any parent container has position: absolute.

html, body {
  height: 100%;
  width: 100%;
} 

I don't know yet - you think it's a good idea to have a resize observer inside the canvas? I can understand it might look like magic, and usually that's not a good thing. On the other hand, three canvas isn't responsive, if you resize the browser the view stays put and you would have to await resize events (which isn't enough if it's inside a dynamically sized div) and update the context renderer manually.

Yeah, I had a bit of trouble with it at first - but in saying that I would love things to resize automatically, and would end up trying to implement it myself anyway.

I've been trying to think if there's a way to stop the auto-expanding from inside react-three-fibre but it seems like maybe you need to know what's happening all the way up the dom tree.

Perhaps you could add an autoResize flag to the Canvas, and have it off by default. That will stop people from running into trouble when they first use the library, but they have the option to turn it on when they start diving deeper.

that would be an option. maybe also cutting out the resize-observer-polyfill and letting users decide to include that on their own. it's just additional weight if it's running in evergreen browsers.

Yeah, that could work. Say the polyfill was included, another approach would be to just throw an error if the container is dimensionless. It's not that it's a hard issue to fix, but because you don't know that react-three-fiber auto-resizes it feels like it's doing something weird on its own, that you never told it to do.

If you just say error: Canvas inside dimensionless container. Please ensure Canvas container has dimensions. then I would be able to fix things in 2 seconds. But when it grows on its own, it becomes a bit of a puzzle as to why it's doing that.

Please help! <orbitControls /> don't work here. OrbitControls.js throws TypeError: this.object is undefined

Here's the code I wrote:

import {
    DodecahedronBufferGeometry, EdgesGeometry,
    LineSegments, LineBasicMaterial
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import React, { useRef } from 'react'
import { Canvas, extend, useThree, useRender } from 'react-three-fiber'

const geometry = new DodecahedronBufferGeometry(1, 1)
const edges = new EdgesGeometry(geometry)
const lines = new LineSegments(edges, new LineBasicMaterial({ color: 'red' }))

extend({ OrbitControls })

const Sphere = () => {
    // Setup OrbitControls
    const { camera } = useThree()
    const ref = useRef()
    useRender(() => ref.current.obj.update())

    return (
        <Canvas>
            <orbitControls args={[camera]} ref={ref} />
            <primitive object={lines} />
        </Canvas>
      )
}

export default Sphere

What should I do?

useThree is context based, it doesn't work outside of canvas, plus, you can have multiple canvases. best wrap it into a component, here's an easy example: https://codesandbox.io/s/xvvn4vxqnz

Thank you @drcmda for help! Now it works. I didn't know that useThree is content-based cos' I only started using the library.

The code above works pretty fine:

import {
    DodecahedronBufferGeometry, EdgesGeometry,
    LineSegments, LineBasicMaterial
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import React, { useRef } from 'react'
import { Canvas, extend, useThree, useRender } from 'react-three-fiber'

const geometry = new DodecahedronBufferGeometry(1, 1)
const edges = new EdgesGeometry(geometry)
const lines = new LineSegments(edges, new LineBasicMaterial({ color: 'red' }))

extend({ OrbitControls })

const Model = () => <primitive object={lines} />

const Controls = props => {
  const { camera } = useThree()
  const controls = useRef()
  useRender(() => controls.current && controls.current.update())
  return <orbitControls ref={controls} args={[camera]} {...props} />
}

const Sphere = () => {
    return (
        <Canvas>
            <Controls enableDamping rotateSpeed={0.3} dampingFactor={0.1} />
            <Model />
        </Canvas>
  )
}

export default Sphere

seems that useRender is not anymore available, how to use orbitcontrols in latest v4 version?

You need useFrame().

@gino8080 you can also import OrbitalControl from drei

I needed to pass the domElement in args too, else it was undefined and throwing an error:

const {
  camera,
  gl: { domElement },
} = useThree();

This article helped https://codeworkshop.dev/blog/2020-04-03-adding-orbit-controls-to-react-three-fiber/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

regalstreak picture regalstreak  路  5Comments

jamestubman picture jamestubman  路  6Comments

dbismut picture dbismut  路  3Comments

janvarsa picture janvarsa  路  3Comments

gtolarc picture gtolarc  路  4Comments