Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented GizmoHelper, ViewportGizmo and ViewcubeGizmo #303

Merged
merged 12 commits into from
Feb 27, 2021
47 changes: 47 additions & 0 deletions .storybook/stories/GizmoViewcube.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { number, select, withKnobs } from '@storybook/addon-knobs'
import * as React from 'react'
import { Vector3 } from 'three'
import { GizmoHelper, OrbitControls, useGLTF, GizmoViewcube } from '../../src'
import { Setup } from '../Setup'

export default {
title: 'Gizmos/GizmoViewcube',
component: GizmoViewcube,
decorators: [
(storyFn) => (
<Setup controls={false} cameraPosition={new Vector3(0, 0, 10)}>
{storyFn()}
</Setup>
),
withKnobs,
],
}

const GizmoViewcubeStory = () => {
const controlsRef = React.useRef<OrbitControls>()
const { scene } = useGLTF('LittlestTokyo.glb')

return (
<>
<primitive object={scene} scale={[0.01, 0.01, 0.01]} />
<GizmoHelper
alignment={select('alignment', ['top-left', 'top-right', 'bottom-right', 'bottom-left'], 'bottom-right')}
margin={[number('marginX', 80), number('marginY', 80)]}
onTarget={() => controlsRef?.current?.target as Vector3}
onUpdate={() => controlsRef.current?.update!()}
>
<GizmoViewcube />
</GizmoHelper>

<OrbitControls ref={controlsRef} />
</>
)
}

export const DefaultStory = () => (
<React.Suspense fallback={null}>
<GizmoViewcubeStory />
</React.Suspense>
)

DefaultStory.storyName = 'Default'
49 changes: 49 additions & 0 deletions .storybook/stories/GizmoViewport.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { color, number, select, withKnobs } from '@storybook/addon-knobs'
import * as React from 'react'
import { Vector3 } from 'three'
import { GizmoViewport, GizmoHelper, OrbitControls, useGLTF } from '../../src'
import { Setup } from '../Setup'

export default {
title: 'Gizmos/GizmoViewport',
component: GizmoViewport,
decorators: [
(storyFn) => (
<Setup controls={false} cameraPosition={new Vector3(0, 0, 10)}>
{storyFn()}
</Setup>
),
withKnobs,
],
}

const GizmoViewportStory = () => {
const controlsRef = React.useRef<OrbitControls>()
const { scene } = useGLTF('LittlestTokyo.glb')

return (
<>
<primitive object={scene} scale={[0.01, 0.01, 0.01]} />
<GizmoHelper
alignment={select('alignment', ['top-left', 'top-right', 'bottom-right', 'bottom-left'], 'bottom-right')}
margin={[number('marginX', 80), number('marginY', 80)]}
onTarget={() => controlsRef?.current?.target as Vector3}
onUpdate={() => controlsRef.current?.update!()}
>
<GizmoViewport
axisColors={[color('colorX', 'red'), color('colorY', 'green'), color('colorZ', 'blue')]}
labelColor={color('labelColor', 'black')}
/>
</GizmoHelper>

<OrbitControls ref={controlsRef} />
</>
)
}
export const DefaultStory = () => (
<React.Suspense fallback={null}>
<GizmoViewportStory />
</React.Suspense>
)

DefaultStory.storyName = 'Default'
11 changes: 4 additions & 7 deletions .storybook/stories/useCamera.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function UseCameraScene() {
const virtualCam = React.useRef<THREE.Camera>(null!)
const ref = React.useRef<THREE.Mesh>()

const [hover, set] = React.useState<null | number>(null)
const [hover, setHover] = React.useState<null | number>(null)

const { gl, scene, camera } = useThree()

Expand All @@ -39,16 +39,13 @@ function UseCameraScene() {
gl.render(virtualScene, virtualCam.current)
}, 1)

const handlePointerOut = () => setHover(null)
const handlePointerMove = (e: THREE.Event) => setHover(Math.floor(e.faceIndex ?? 0 / 2))
return (createPortal(
<>
<OrthographicCamera ref={virtualCam} makeDefault={false} position={[0, 0, 100]} zoom={2} />

<mesh
ref={ref}
raycast={useCamera(virtualCam)}
onPointerOut={() => set(null)}
onPointerMove={(e) => set(Math.floor(e.faceIndex ?? 0 / 2))}
>
<mesh ref={ref} raycast={useCamera(virtualCam)} onPointerOut={handlePointerOut} onPointerMove={handlePointerMove}>
{[...Array(6)].map((_, index) => (
<meshLambertMaterial attachArray="material" key={index} color="hotpink" wireframe={hover !== index} />
))}
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
<li><a href="#line">Line</a></li>
<li><a href="#positionalaudio">PositionalAudio</a></li>
<li><a href="#billboard">Billboard</a></li>
<li><a href="#gizmoHelper">GizmoHelper</a></li>
<li><a href="#environment">Environment</a></li>
<li><a href="#effects">Effects</a></li>
<li><a href="#useanimations">useAnimations</a></li>
Expand Down Expand Up @@ -277,6 +278,27 @@ Adds a `<Plane />` that always faces the camera.
/>
```

#### GizmoHelper

Used by widgets that visualize and control camera position.

Two example gizmos are included: GizmoViewport and GizmoViewcube, and `useGizmoContext` makes it easy to create your own.

```jsx
<GizmoHelper
alignment="bottom-right" // widget alignment within scene
margin={[80, 80]} // widget margins (X, Y)
onUpdate={/* called during camera animation */}
onTarget={/* return current camera target (e.g. from orbit controls) to center animation */}
>
<GizmoViewport
axisColors={['red', 'green', 'blue']}
labelColor="black"
/>
{/* alternative: <GizmoViewcube /> */}
w3dot0 marked this conversation as resolved.
Show resolved Hide resolved
</GizmoHelper>
```

#### Environment

[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.react-spring.io/?path=/story/abstractions-environment--environment-st)
Expand Down
118 changes: 118 additions & 0 deletions src/core/GizmoHelper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as React from 'react'
import { createPortal, useFrame, useThree } from 'react-three-fiber'
import { Camera, Group, Intersection, Matrix4, Object3D, Quaternion, Raycaster, Scene, Vector3 } from 'three'
import { OrthographicCamera } from './OrthographicCamera'
import { useCamera } from './useCamera'

type GizmoHelperContext = {
tweenCamera: (direction: Vector3) => void
raycast: (raycaster: Raycaster, intersects: Intersection[]) => void
}

const Context = React.createContext<GizmoHelperContext>({} as GizmoHelperContext)

export const useGizmoContext = () => {
return React.useContext<GizmoHelperContext>(Context)
}

const turnRate = 2 * Math.PI // turn rate in angles per second
const dummy = new Object3D()
const matrix = new Matrix4()
const [q1, q2] = [new Quaternion(), new Quaternion()]
const target = new Vector3()
const targetPosition = new Vector3()
const targetQuaternion = new Quaternion()

export type GizmoHelperProps = JSX.IntrinsicElements['group'] & {
alignment?: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left'
margin?: [number, number]
onUpdate: () => void // update controls during animation
onTarget: () => Vector3 // return the target to rotate around
}

export const GizmoHelper = ({
alignment = 'bottom-right',
margin = [80, 80],
onUpdate,
onTarget,
children: GizmoHelperComponent,
}: GizmoHelperProps): any => {
const { gl, camera: mainCamera, size } = useThree()
const gizmoRef = React.useRef<Group>()
const virtualCam = React.useRef<Camera>(null!)
const [virtualScene] = React.useState(() => new Scene())

const animating = React.useRef(false)
const radius = React.useRef(0)
const focusPoint = React.useRef(new Vector3(0, 0, 0))

const tweenCamera = (direction: Vector3) => {
animating.current = true
focusPoint.current = onTarget()
radius.current = mainCamera.position.distanceTo(target)

// Rotate from current camera orientation
dummy.position.copy(target)
dummy.lookAt(mainCamera.position)
q1.copy(dummy.quaternion)

// To new current camera orientation
targetPosition.copy(direction).multiplyScalar(radius.current).add(target)
dummy.lookAt(targetPosition)
q2.copy(dummy.quaternion)
}

const animateStep = (delta: number) => {
if (!animating.current) return

const step = delta * turnRate

// animate position by doing a slerp and then scaling the position on the unit sphere
q1.rotateTowards(q2, step)
mainCamera.position.set(0, 0, 1).applyQuaternion(q1).multiplyScalar(radius.current).add(focusPoint.current)

// animate orientation
mainCamera.quaternion.rotateTowards(targetQuaternion, step)
mainCamera.updateProjectionMatrix()
onUpdate && onUpdate()

if (q1.angleTo(q2) < 0.01) {
animating.current = false
}
}

const beforeRender = () => {
// Sync gizmo with main camera orientation
matrix.copy(mainCamera.matrix).invert()
gizmoRef.current?.quaternion.setFromRotationMatrix(matrix)
}

useFrame((_, delta) => {
if (virtualCam.current && gizmoRef.current) {
animateStep(delta)
beforeRender()
gl.autoClear = false
gl.clearDepth()
gl.render(virtualScene, virtualCam.current)
}
})

const gizmoHelperContext = {
tweenCamera,
raycast: useCamera(virtualCam),
}

// Position gizmo component within scene
const [marginX, marginY] = margin
const x = alignment.endsWith('-left') ? -size.width / 2 + marginX : size.width / 2 - marginX
const y = alignment.startsWith('top-') ? size.height / 2 - marginY : -size.height / 2 + marginY
return createPortal(
<Context.Provider value={gizmoHelperContext}>
<OrthographicCamera ref={virtualCam} makeDefault={false} position={[0, 0, 100]} />
<group ref={gizmoRef} position={[x, y, 0]}>
{GizmoHelperComponent}
</group>
</Context.Provider>,
virtualScene
)
}
Loading