Skip to content

Commit

Permalink
fix: stage can be free from the camera, accumulative support
Browse files Browse the repository at this point in the history
  • Loading branch information
drcmda committed Nov 20, 2022
1 parent 0f34354 commit afa4a63
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 93 deletions.
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2635,12 +2635,32 @@ This component makes its contents float or hover.

[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.pmnd.rs/?path=/story/staging-stage--stage-st)

Creates a "stage" with proper studio lighting, content centered and planar, shadows and ground-contact shadows.
Creates a "stage" with proper studio lighting, content centered and planar, model-shadows and ground-shadows. Make sure to set `makeDefault` on your controls when `adjustCamera` is true!

Make sure to set the `makeDefault` prop on your controls, in that case you do not need to provide `controls` via prop.
```tsx
type StageShadows = Partial<AccumulativeShadowsProps> &
Partial<ContactShadowsProps> & {
type: 'contact' | 'accumulative'
bias?: number
size?: number
}
type StageProps = JSX.IntrinsicElements['group'] & {
/** Lighting setup, default: "rembrandt" */
preset?: keyof typeof presets
/** Controls the ground shadows, default: "accumulative" */
shadows?: boolean | 'contact' | 'accumulative' | StageShadows
/** Optionally wraps and thereby centers the models using <Bounds>, can also be a margin, default: false */
adjustCamera?: boolean | number
/** The default environment, default: "city" */
environment?: PresetsType | null
/** The lighting intensity, default: 0.5 */
intensity?: number
}
```

```jsx
<Stage contactShadow shadows adjustCamera intensity={1} environment="city" preset="rembrandt" controls={controlsRef}>
<Stage>
<mesh />
</Stage>
```
Expand Down
4 changes: 2 additions & 2 deletions src/core/AccumulativeShadows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function isGeometry(object: any): object is THREE.Mesh {
return !!object.geometry
}

type AccumulativeShadowsProps = JSX.IntrinsicElements['group'] & {
export type AccumulativeShadowsProps = {
/** How many frames it can render, more yields cleaner results but takes more time, 40 */
frames?: number
/** If frames === Infinity blend controls the refresh ratio, 100 */
Expand Down Expand Up @@ -121,7 +121,7 @@ export const AccumulativeShadows = React.forwardRef(
resolution = 1024,
toneMapped = true,
...props
}: AccumulativeShadowsProps,
}: JSX.IntrinsicElements['group'] & AccumulativeShadowsProps,
forwardRef: React.ForwardedRef<AccumulativeContext>
) => {
extend({ SoftShadowMaterial })
Expand Down
35 changes: 24 additions & 11 deletions src/core/Clone.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as THREE from 'three'
import * as React from 'react'
import pick from 'lodash.pick'
import { MeshProps } from '@react-three/fiber'
import { SkeletonUtils } from 'three-stdlib'

type Props = Omit<JSX.IntrinsicElements['group'], 'children'> & {
/** Any pre-existing THREE.Object3D (groups, meshes, ...), or an array of objects */
Expand All @@ -17,6 +19,7 @@ type Props = Omit<JSX.IntrinsicElements['group'], 'children'> & {
castShadow?: boolean
/** Short access receiveShadow, applied to every mesh within */
receiveShadow?: boolean
isChild?: boolean
}

function createSpread(
Expand Down Expand Up @@ -45,6 +48,10 @@ function createSpread(
'scale',
'up',
'userData',
'bindMode',
'bindMatrix',
'bindMatrixInverse',
'skeleton',
],
deep,
inject,
Expand All @@ -63,7 +70,7 @@ function createSpread(
else spread = { ...spread, ...(inject as any) }
}

if (child.type === 'Mesh') {
if (child instanceof THREE.Mesh) {
if (castShadow) spread.castShadow = true
if (receiveShadow) spread.receiveShadow = true
}
Expand All @@ -72,10 +79,21 @@ function createSpread(

export const Clone = React.forwardRef(
(
{ object, children, deep, castShadow, receiveShadow, inject, keys, ...props }: Props,
{ isChild = false, object, children, deep, castShadow, receiveShadow, inject, keys, ...props }: Props,
forwardRef: React.Ref<THREE.Group>
) => {
const config = { keys, deep, inject, castShadow, receiveShadow }
object = React.useMemo(() => {
if (isChild === false && !Array.isArray(object)) {
let isSkinned = false
object.traverse((object) => {
if ((object as any).isSkinnedMesh) isSkinned = true
})
if (isSkinned) return SkeletonUtils.clone(object)
}
return object
}, [object, isChild])

// Deal with arrayed clones
if (Array.isArray(object)) {
return (
Expand All @@ -87,21 +105,16 @@ export const Clone = React.forwardRef(
</group>
)
}

// Singleton clones
const { children: injectChildren, ...spread } = createSpread(object, config)
const Element = object.type[0].toLowerCase() + object.type.slice(1)

return (
<Element {...spread} {...props} ref={forwardRef}>
{(object?.children).map((child) => {
let spread: any = {}
let Element: string | typeof Clone = child.type[0].toLowerCase() + child.type.slice(1)
if (Element === 'group' || Element === 'object3D') {
Element = Clone
spread = { object: child, ...config }
} else {
spread = createSpread(child, config)
}
return <Element key={child.uuid} {...spread} />
if (child.type === 'Bone') return <primitive key={child.uuid} object={child} {...config} />
return <Clone key={child.uuid} object={child} {...config} isChild />
})}
{children}
{injectChildren}
Expand Down
4 changes: 2 additions & 2 deletions src/core/ContactShadows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as THREE from 'three'
import { useFrame, useThree } from '@react-three/fiber'
import { HorizontalBlurShader, VerticalBlurShader } from 'three-stdlib'

type Props = Omit<JSX.IntrinsicElements['group'], 'scale'> & {
export type ContactShadowsProps = {
opacity?: number
width?: number
height?: number
Expand Down Expand Up @@ -36,7 +36,7 @@ export const ContactShadows = React.forwardRef(
depthWrite = false,
renderOrder,
...props
}: Props,
}: Omit<JSX.IntrinsicElements['group'], 'scale'> & ContactShadowsProps,
ref
) => {
const scene = useThree((state) => state.scene)
Expand Down
154 changes: 79 additions & 75 deletions src/core/Stage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react'
import * as THREE from 'three'
import { useThree } from '@react-three/fiber'
import { Environment } from './Environment'
import { ContactShadows } from './ContactShadows'
import { ContactShadowsProps, ContactShadows } from './ContactShadows'
import { Center } from './Center'
import { AccumulativeShadowsProps, AccumulativeShadows, RandomizedLight } from './AccumulativeShadows'
import { useBounds, Bounds } from './Bounds'
import { PresetsType } from '../helpers/environment-assets'

const presets = {
Expand All @@ -24,100 +25,103 @@ const presets = {
},
}

type ControlsProto = { update(): void; target: THREE.Vector3 }
type StageShadows = Partial<AccumulativeShadowsProps> &
Partial<ContactShadowsProps> & {
type: 'contact' | 'accumulative'
bias?: number
size?: number
}

type Props = JSX.IntrinsicElements['group'] & {
shadows?: boolean
adjustCamera?: boolean
type StageProps = JSX.IntrinsicElements['group'] & {
/** Lighting setup, default: "rembrandt" */
preset?: keyof typeof presets
/** Controls the ground shadows, default: "accumulative" */
shadows?: boolean | 'contact' | 'accumulative' | StageShadows
/** Optionally wraps and thereby centers the models using <Bounds>, can also be a margin, default: false */
adjustCamera?: boolean | number
/** The default environment, default: "city" */
environment?: PresetsType | null
/** The lighting intensity, default: 0.5 */
intensity?: number
ambience?: number
// TODO: in a new major state.controls should be the only means of consuming controls, the
// controls prop can then be removed!
controls?: React.MutableRefObject<ControlsProto>
preset?: keyof typeof presets
shadowBias?: number
contactShadow?:
| {
blur: number
opacity?: number
position?: [x: number, y: number, z: number]
}
| false
}

function Refit({ radius, adjustCamera }) {
const api = useBounds()
React.useEffect(() => {
if (adjustCamera) api.refresh().clip().fit()
}, [radius, adjustCamera])
return null
}

export function Stage({
children,
controls,
shadows = true,
adjustCamera = true,
adjustCamera,
intensity = 0.5,
shadows = 'accumulative',
environment = 'city',
intensity = 1,
preset = 'rembrandt',
shadowBias = 0,
contactShadow = {
blur: 2,
opacity: 0.5,
position: [0, 0, 0],
},
...props
}: Props) {
}: StageProps) {
const config = presets[preset]
const camera = useThree((state) => state.camera)
// @ts-expect-error new in @react-three/fiber@7.0.5
const defaultControls = useThree((state) => state.controls) as ControlsProto
const outer = React.useRef<THREE.Group>(null!)
const inner = React.useRef<THREE.Group>(null!)
const [{ radius, width, height }, set] = React.useState({ radius: 0, width: 0, height: 0 })

React.useLayoutEffect(() => {
outer.current.position.set(0, 0, 0)
outer.current.updateWorldMatrix(true, true)
const box3 = new THREE.Box3().setFromObject(inner.current)
const center = new THREE.Vector3()
const sphere = new THREE.Sphere()
const height = box3.max.y - box3.min.y
const width = box3.max.x - box3.min.x
box3.getCenter(center)
box3.getBoundingSphere(sphere)
set({ radius: sphere.radius, width, height })
outer.current.position.set(-center.x, -center.y + height / 2, -center.z)
}, [children])

React.useLayoutEffect(() => {
if (adjustCamera) {
const y = radius / (height > width ? 1.5 : 2.5)
camera.position.set(0, radius * 0.5, radius * 2.5)
camera.near = 0.1
camera.far = Math.max(5000, radius * 4)
camera.lookAt(0, y, 0)
const ctrl = defaultControls || controls?.current
if (ctrl) {
ctrl.target.set(0, y, 0)
ctrl.update()
}
}
}, [defaultControls, radius, height, width, adjustCamera])

const [{ radius, height }, set] = React.useState({ radius: 0, width: 0, height: 0, depth: 0 })
const shadowBias = (shadows as StageShadows)?.bias ?? -0.0001
const shadowSize = (shadows as StageShadows)?.size ?? 1024
const contactShadow = shadows === 'contact' || (shadows as StageShadows)?.type === 'contact'
const accumulativeShadow = shadows === 'accumulative' || (shadows as StageShadows)?.type === 'accumulative'
const shadowSpread = { ...(typeof shadows === 'object' ? shadows : {}) }
console.log(height)
return (
<group {...props}>
<group ref={outer}>
<group ref={inner}>{children}</group>
</group>
{contactShadow && <ContactShadows scale={radius * 2} far={radius / 2} {...contactShadow} />}
{environment && <Environment preset={environment} />}
<>
<ambientLight intensity={intensity / 3} />
<spotLight
penumbra={1}
position={[config.main[0] * radius, config.main[1] * radius, config.main[2] * radius]}
intensity={intensity * 2}
castShadow={shadows}
castShadow={!!shadows}
shadow-bias={shadowBias}
shadow-mapSize={shadowSize}
/>
<pointLight
position={[config.fill[0] * radius, config.fill[1] * radius, config.fill[2] * radius]}
intensity={intensity}
/>
</group>
<Bounds fit={!!adjustCamera} clip={!!adjustCamera} margin={Number(adjustCamera)} observe {...props}>
<Refit radius={radius} adjustCamera={adjustCamera} />
<Center
onCentered={({ width, height, depth, boundingSphere, ...data }) =>
set({ radius: boundingSphere.radius, width, height, depth })
}
>
{children}
</Center>
</Bounds>
<group position={[0, -height / 2, 0]}>
{contactShadow && (
<ContactShadows scale={radius * 4} far={radius} blur={2} {...(shadowSpread as ContactShadowsProps)} />
)}
{accumulativeShadow && (
<AccumulativeShadows
temporal
frames={100}
alphaTest={0.9}
toneMapped={true}
scale={radius * 4}
{...(shadowSpread as AccumulativeShadowsProps)}
>
<RandomizedLight
amount={8}
radius={radius}
ambient={0.5}
intensity={1}
position={[config.main[0] * radius, config.main[1] * radius, config.main[2] * radius]}
size={radius * 4}
bias={-shadowBias}
mapSize={shadowSize}
/>
</AccumulativeShadows>
)}
</group>
{environment && <Environment preset={environment} />}
</>
)
}

1 comment on commit afa4a63

@vercel
Copy link

@vercel vercel bot commented on afa4a63 Nov 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.