diff --git a/.prettierrc b/.prettierrc index 8599b1aa9..4c11c7ad4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "singleQuote": true, "tabWidth": 2, "printWidth": 120, - "useTabs": false + "useTabs": false, + "endOfLine": "auto" } diff --git a/.storybook/public/gelatinous_cube.glb b/.storybook/public/gelatinous_cube.glb new file mode 100644 index 000000000..153d9b281 Binary files /dev/null and b/.storybook/public/gelatinous_cube.glb differ diff --git a/.storybook/stories/MeshTransmissionMaterial.stories.tsx b/.storybook/stories/MeshTransmissionMaterial.stories.tsx new file mode 100644 index 000000000..0173a6858 --- /dev/null +++ b/.storybook/stories/MeshTransmissionMaterial.stories.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' +import * as THREE from 'three' +import { Setup } from '../Setup' +import { + useGLTF, + MeshTransmissionMaterial, + AccumulativeShadows, + RandomizedLight, + Environment, + OrbitControls, + Center, +} from '../../src' + +export default { + title: 'Shaders/MeshTransmissionMaterial', + component: MeshTransmissionMaterial, + decorators: [ + (storyFn) => ( + + {storyFn()} + + ), + ], +} + +// https://sketchfab.com/3d-models/gelatinous-cube-e08385238f4d4b59b012233a9fbdca21 +export function GelatinousCube() { + const { nodes, materials } = useGLTF('/gelatinous_cube.glb') as any + return ( + + + + + + + + + + + + + ) +} + +export const TransmissionSt = () => ( + + + +
+ +
+ + + +
+ + +
+) +TransmissionSt.storyName = 'Default' diff --git a/.storybook/stories/Shadow.stories.tsx b/.storybook/stories/Shadow.stories.tsx index 0e1583ed1..9fa3d31e1 100644 --- a/.storybook/stories/Shadow.stories.tsx +++ b/.storybook/stories/Shadow.stories.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { useFrame } from '@react-three/fiber' -import { Mesh } from 'three' import { Setup } from '../Setup' -import { Shadow, Icosahedron, Plane } from '../../src' +import { Shadow, Icosahedron, Plane, type ShadowType } from '../../src' +import { type Mesh } from 'three' export default { title: 'Misc/Shadow', @@ -13,7 +13,7 @@ export default { } function ShadowScene() { - const shadow = React.useRef(null!) + const shadow = React.useRef(null!) const mesh = React.useRef(null!) useFrame(({ clock }) => { diff --git a/.storybook/stories/ShadowAlpha.stories.tsx b/.storybook/stories/ShadowAlpha.stories.tsx new file mode 100644 index 000000000..003259a73 --- /dev/null +++ b/.storybook/stories/ShadowAlpha.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' + +import { Setup } from '../Setup' + +import { useFrame } from '@react-three/fiber' +import { BufferGeometry, MeshStandardMaterial, type Mesh } from 'three' +import { Icosahedron, Plane, ShadowAlpha } from '../../src' + +export default { + title: 'Misc/ShadowAlpha', + component: ShadowAlpha, + decorators: [(storyFn) => {storyFn()}], +} + +function ShadowAlphaScene() { + const mesh = React.useRef>(null!) + + useFrame(({ clock }) => { + const time = clock.elapsedTime + mesh.current.material.opacity = Math.sin(time * 2) * 0.5 + 0.5 + }) + + return ( + <> + + + + + + + + + + + + + ) +} + +export const ShadowAlphaSt = () => +ShadowAlphaSt.storyName = 'Default' diff --git a/.storybook/stories/useBVH.stories.tsx b/.storybook/stories/useBVH.stories.tsx index c9e7b018b..98fc13229 100644 --- a/.storybook/stories/useBVH.stories.tsx +++ b/.storybook/stories/useBVH.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Setup } from '../Setup' -import { MeshBVHVisualizer } from 'three-mesh-bvh' +import { MeshBVHHelper } from 'three-mesh-bvh' import { useHelper, useBVH, TorusKnot, OrbitControls } from '../../src' import { useFrame, useThree } from '@react-three/fiber' @@ -24,7 +24,7 @@ function TorusBVH({ bvh, ...props }) { }) const debug = boolean('vizualize bounds', true) - useHelper(debug ? mesh : dummy, MeshBVHVisualizer) + useHelper(debug ? mesh : dummy, MeshBVHHelper) const [hovered, setHover] = React.useState(false) return ( diff --git a/README.md b/README.md index 43d3d59a5..4c571a41c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
  • PresentationControls
  • KeyboardControls
  • FaceControls
  • +
  • MotionPathControls
  • Gizmos
  • Shaders
  • Modifiers
  • @@ -223,6 +227,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
  • useEnvironment
  • useMatcapTexture
  • useNormalTexture
  • +
  • ShadowAlpha
  • @@ -297,7 +302,7 @@ A responsive [THREE.OrthographicCamera](https://threejs.org/docs/#api/en/cameras ``` -You can use the OrthographicCamera to film contents into a RenderTarget, it has the same API as OrthographicCamera. +You can use the OrthographicCamera to film contents into a RenderTarget, it has the same API as PerspectiveCamera. ```jsx @@ -750,6 +755,118 @@ useFrame((_, delta) => { }) ``` +#### MotionPathControls + +

    + Demo +

    + +Motion path controls, it takes a path of bezier curves or catmull-rom curves as input and animates the passed `object` along that path. It can be configured to look upon an external object for staging or presentation purposes by adding a `focusObject` property (ref). + +```tsx +type MotionPathProps = JSX.IntrinsicElements['group'] & { + /** An optional array of THREE curves */ + curves?: THREE.Curve[] + /** Show debug helpers */ + debug?: boolean + /** The target object that is moved, default: null (the default camera) */ + object?: React.MutableRefObject + /** An object where the target looks towards, can also be a vector, default: null */ + focus?: [x: number, y: number, z: number] | React.MutableRefObject + /** Position between 0 (start) and end (1), if this is not set useMotion().current must be used, default: null */ + offset?: number + /** Optionally smooth the curve, default: false */ + smooth?: boolean | number + /** Damping tolerance, default: 0.00001 */ + eps?: number + /** Damping factor for movement along the curve, default: 0.1 */ + damping?: number + /** Damping factor for lookAt, default: 0.1 */ + focusDamping?: number + /** Damping maximum speed, default: Infinity */ + maxSpeed?: number +} +``` + +You can use MotionPathControls with declarative curves. + +```jsx +function App() { + const poi = useRef() + return ( + + + + + + +``` + +Or with imperative curves. + +```jsx + +``` + +You can exert full control with the `useMotion` hook, it allows you to define the current position along the path for instance, or define your own lookAt. Keep in mind that MotionPathControls will still these values unless you set damping and focusDamping to 0. Then you can also employ your own easing. + +```tsx +type MotionState = { + /** The user-defined, mutable, current goal position along the curve, it may be >1 or <0 */ + current: number + /** The combined curve */ + path: THREE.CurvePath + /** The focus object */ + focus: React.MutableRefObject> | [x: number, y: number, z: number] | undefined + /** The target object that is moved along the curve */ + object: React.MutableRefObject> + /** The automated, 0-1 normalised and damped current goal position along curve */ + offset: number + /** The current point on the curve */ + point: THREE.Vector3 + /** The current tangent on the curve */ + tangent: THREE.Vector3 + /** The next point on the curve */ + next: THREE.Vector3 +} + +const state: MotionState = useMotion() +``` + +```jsx +function Loop() { + const motion = useMotion() + useFrame((state, delta) => { + // Set the current position along the curve, you can increment indiscriminately for a loop + motion.current += delta + // Look ahead on the curve + motion.object.current.lookAt(motion.next) + }) +} + + + + +``` + # Gizmos #### GizmoHelper @@ -969,6 +1086,7 @@ A box buffer geometry with rounded corners, done with extrusion. args={[1, 1, 1]} // Width, height, depth. Default is [1, 1, 1] radius={0.05} // Radius of the rounded corners. Default is 0.05 smoothness={4} // The number of curve segments. Default is 4 + bevelSegments={4} // The number of bevel segments. Default is 4, setting it to 0 removes the bevel, as a result the texture is applied to the whole geometry. creaseAngle={0.4} // Smooth normals everywhere except faces that meet at an angle greater than the crease angle {...meshProps} // All THREE.Mesh props are valid > @@ -1134,13 +1252,13 @@ export type FacemeshProps = { /** a landmark index (to get the position from) or a vec3 to be the origin of the mesh. default: undefined (ie. the bbox center) */ origin?: number | THREE.Vector3 /** A facial transformation matrix, as returned by FaceLandmarkerResult.facialTransformationMatrixes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */ - facialTransformationMatrix?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes[0] + facialTransformationMatrix?: (typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes)[0] /** Apply position offset extracted from `facialTransformationMatrix` */ offset?: boolean /** Offset sensitivity factor, less is more sensible */ offsetScalar?: number /** Fface blendshapes, as returned by FaceLandmarkerResult.faceBlendshapes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */ - faceBlendshapes?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes[0] + faceBlendshapes?: (typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes)[0] /** whether to enable eyes (nb. `faceBlendshapes` is required for), default: true */ eyes?: boolean /** Force `origin` to be the middle of the 2 eyes (nb. `eyes` is required for), default: false */ @@ -1184,6 +1302,7 @@ api.eyeRightRef.current.irisDirRef.current.localToWorld(new THREE.Vector3(0, 0, #### Image

    + Horizontal tiles Horizontal tiles useIntersect Infinite scroll @@ -1192,10 +1311,26 @@ api.eyeRightRef.current.irisDirRef.current.localToWorld(new THREE.Vector3(0, 0, A shader-based image component with auto-cover (similar to css/background: cover). +```tsx +export type ImageProps = Omit & { + segments?: number + scale?: number | [number, number] + color?: Color + zoom?: number + radius?: number + grayscale?: number + toneMapped?: boolean + transparent?: boolean + opacity?: number + side?: THREE.Side +} +``` + ```jsx function Foo() { const ref = useRef() useFrame(() => { + ref.current.material.radius = ... // between 0 and 1 ref.current.material.zoom = ... // 1 and higher ref.current.material.grayscale = ... // between 0 and 1 ref.current.material.color.set(...) // mix-in color @@ -1210,6 +1345,20 @@ To make the material transparent: ``` +You can have custom planes, for instance a rounded-corner plane. + +```jsx +import { extend } from '@react-three/fiber' +import { Image } from '@react-three/drei' +import { easing, geometry } from 'maath' + +extend({ RoundedPlaneGeometry: geometry.RoundedPlaneGeometry }) + + + + +``` + #### Text [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/abstractions-text--text-st) ![](https://img.shields.io/badge/-suspense-brightgreen) @@ -1402,12 +1551,14 @@ Abstracts [THREE.EdgesGeometry](https://threejs.org/docs/#api/en/geometries/Edge Demo

    -An ornamental component that extracts the geometry from its parent and displays an [inverted-hull outline](https://bnpr.gitbook.io/bnpr/outline/inverse-hull-method). +An ornamental component that extracts the geometry from its parent and displays an [inverted-hull outline](https://bnpr.gitbook.io/bnpr/outline/inverse-hull-method). Supported parents are ``, `` and ``. ```tsx type OutlinesProps = JSX.IntrinsicElements['group'] & { /** Outline color, default: black */ color: ReactThreeFiber.Color + /** Line thickness is independent of zoom, default: false */ + screenspace: boolean /** Outline opacity, default: 1 */ opacity: number /** Outline transparency, default: false */ @@ -1651,10 +1802,12 @@ The decal box has to intersect the surface, otherwise it will not be visible. if position={[0, 0, 0]} // Position of the decal rotation={[0, 0, 0]} // Rotation of the decal (can be a vector or a degree in radians) scale={1} // Scale of the decal - polygonOffset - polygonOffsetFactor={-1} // The mesh should take precedence over the original > - + ``` @@ -1729,6 +1882,46 @@ type AsciiRendererProps = { ``` +#### Splat + +

    + Demo +

    + +A declarative abstraction around [antimatter15/splat](https://github.com/antimatter15/splat). It supports re-use, multiple splats with correct depth sorting, splats can move and behave as a regular object3d's, supports alphahash & alphatest, and stream-loading. + +```tsx +type SplatProps = { + /** Url towards a *.splat file, no support for *.ply */ + src: string + /** Whether to use tone mapping, default: false */ + toneMapped?: boolean + /** Alpha test value, , default: 0 */ + alphaTest?: number + /** Whether to use alpha hashing, default: false */ + alphaHash?: boolean + /** Chunk size for lazy loading, prevents chokings the worker, default: 25000 (25kb) */ + chunkSize?: number +} & JSX.IntrinsicElements['mesh'] +``` + +```jsx + +``` + +In order to depth sort multiple splats correectly you can either use alphaTest, for instance with a low value. But keep in mind that this can show a slight outline under some viewing conditions. + +```jsx + + +``` + +You can also use alphaHash, but this can be slower and create some noise, you would typically get rid of the noise in postprocessing with a TAA pass. You don't have to use alphaHash on all splats. + +```jsx + +``` + # Shaders #### MeshReflectorMaterial @@ -2251,6 +2444,16 @@ Enable shadows using the `castShadow` and `recieveShadow` prop. > Note: Html 'blending' mode only correctly occludes rectangular HTML elements by default. Use the `geometry` prop to swap the backing geometry to a custom one if your Html has a different shape. +If transform mode is enabled, the dimensions of the rendered html will depend on the position relative to the camera, the camera fov and the distanceFactor. For example, an Html component placed at (0,0,0) and with a distanceFactor of 10, rendered inside a scene with a perspective camera positioned at (0,0,2.45) and a FOV of 75, will have the same dimensions as a "plain" html element like in [this example](https://codesandbox.io/s/drei-html-magic-number-6mzt6m). + +A caveat of transform mode is that on some devices and browsers, the rendered html may appear blurry, as discussed in [#859](https://github.com/pmndrs/drei/issues/859). The issue can be at least mitigated by scaling down the Html parent and scaling up the html children: + +```jsx + +
    Some text
    + +``` + #### CycleRaycast ![](https://img.shields.io/badge/-Dom only-red) @@ -2349,7 +2552,7 @@ type Props = { onLoopEnd?: Function /** Event callback when each frame changes */ onFrame?: Function - /** Control when the animation runs */ + /** @deprecated Control when the animation runs*/ play?: boolean /** Control when the animation pauses */ pause?: boolean @@ -2357,6 +2560,18 @@ type Props = { flipX?: boolean /** Sets the alpha value to be used when running an alpha test. https://threejs.org/docs/#api/en/materials/Material.alphaTest */ alphaTest?: number + /** Displays the texture on a SpriteGeometry always facing the camera, if set to false, it renders on a PlaneGeometry */ + asSprite?: boolean + /** Allows for manual update of the sprite animation e.g: via ScrollControls */ + offset?: number + /** Allows the sprite animation to start from the end towards the start */ + playBackwards: boolean + /** Allows the animation to be paused after it ended so it can be restarted on demand via auto */ + resetOnEnd?: boolean + /** An array of items to create instances from */ + instanceItems?: any[] + /** The max number of items to instance (optional) */ + maxItems?: number } ``` @@ -2380,6 +2595,37 @@ Notes: /> ``` +ScrollControls example + +```jsx +; + + + + + +function FireScroll() { + const sprite = useSpriteAnimator() + const scroll = useScroll() + const ref = React.useRef() + useFrame(() => { + if (sprite && scroll) { + sprite.current = scroll.offset + } + }) + + return null +} +``` + #### Stats [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/misc-stats--default-story) @@ -2795,6 +3041,8 @@ useGLTF(url, '/draco-gltf') useGLTF.preload(url) ``` +If you want to use your own draco decoder globally, you can pass it through `useGLTF.setDecoderPath(path)`: + > **Note**
    If you are using the CDN loaded draco binaries, you can get a small speedup in loading time by prefetching them. > > You can accomplish this by adding two `` tags to your `` tag, as below. The version in those URLs must exactly match what [useGLTF](src/core/useGLTF.tsx#L18) uses for this to work. If you're using create-react-app, `public/index.html` file contains the `` tag. @@ -3110,7 +3358,12 @@ A wrapper around [THREE.LineSegments](https://threejs.org/docs/#api/en/objects/L ##### Prop based: ```jsx - + @@ -3222,6 +3475,14 @@ export interface BVHOptions { maxDepth?: number /** The number of triangles to aim for in a leaf node, default: 10 */ maxLeafTris?: number + /** If false then an index buffer is created if it does not exist and is rearranged */ + /** to hold the bvh structure. If false then a separate buffer is created to store the */ + /** structure and the index buffer (or lack thereof) is retained. This can be used */ + /** when the existing index layout is important or groups are being used so a */ + /** single BVH hierarchy can be created to improve performance. */ + /** default: false */ + /** Note: This setting is experimental */ + indirect?: boolean } export type BvhProps = BVHOptions & @@ -3300,7 +3561,7 @@ function App() { const [dpr, setDpr] = useState(1.5) return ( - setDpr(2)} onDecline={() => setDpr(1)} > + setDpr(2)} onDecline={() => setDpr(1)} /> ``` You can also use the `onChange` callback to get notified when the average changes in whichever direction. This allows you to make gradual changes. It gives you a `factor` between 0 and 1, which is increased by incline and decreased by decline. The `factor` is initially 0.5 by default. If your app starts with lowest defaults and gradually increases quality set `factor` to 0. If it starts with highest defaults and decreases quality, set it to 1. If it starts in the middle and can either increase or decrease, set it to 0.5. @@ -3308,18 +3569,16 @@ You can also use the `onChange` callback to get notified when the average change The following starts at the highest dpr (2) and clamps the gradual dpr between 0.5 at the lowest and 2 at the highest. If the app is in trouble it will reduce `factor` by `step` until it is either 0 or the app has found its sweet spot above that. ```jsx -import round from 'lodash/round' - -const [dpr, set] = useState(2) +const [dpr, setDpr] = useState(2) return ( - setDpr(round(0.5 + 1.5 * factor, 1))} > + setDpr(Math.floor(0.5 + 1.5 * factor, 1))} /> ``` If you still experience flip flops despite the bounds you can define a limit of `flipflops`. If it is met `onFallback` will be triggered which typically sets a lowest possible baseline for the app. After the fallback has been called PerformanceMonitor will shut down. ```jsx - setDpr(1)}> + setDpr(1)} /> ``` PerformanceMonitor can also have children, if you wrap your app in it you get to use `usePerformanceMonitor` which allows individual components down the nested tree to respond to performance changes on their own. @@ -3379,6 +3638,7 @@ type HudProps = {

    Demo + Demo Demo Demo

    @@ -3394,30 +3654,53 @@ integrations into your view. > versions of `@react-three/fiber`. ```tsx - +export type ViewProps = { + /** Root element type, default: div */ + as?: string + /** CSS id prop */ + id?: string + /** CSS classname prop */ + className?: string + /** CSS style prop */ + style?: React.CSSProperties + /** If the view is visible or not, default: true */ + visible?: boolean /** Views take over the render loop, optional render index (1 by default) */ index?: number - /** If you know your view is always at the same place set this to 1 to avoid needless getBoundingClientRect overhead. The default is Infinity, which is best for css animations */ + /** If you know your view is always at the same place set this to 1 to avoid needless getBoundingClientRect overhead */ frames?: number /** The scene to render, if you leave this undefined it will render the default scene */ children?: React.ReactNode -/> + /** The tracking element, the view will be cut according to its whereabouts + * @deprecated + */ + track: React.MutableRefObject +} + +export type ViewportProps = { Port: () => React.ReactNode } & React.ForwardRefExoticComponent< + ViewProps & React.RefAttributes +> ``` +You can define as many views as you like, directly mix them into your dom graph, right where you want them to appear. `View` is an unstyled HTML DOM element (by default a div, and it takes the same properties as one). Use `View.Port` inside the canvas to output them. The canvas should ideally fill the entire screen with absolute positioning, underneath HTML or on top of it, as you prefer. + ```jsx -const container = useRef() -const tracking = useRef() return (

    Html content here

    -
    + + + + + + + + - - - - + + +
    +) ``` #### RenderTexture @@ -3463,6 +3746,90 @@ type Props = JSX.IntrinsicElements['texture'] & { ``` +#### RenderCubeTexture + +This component allows you to render a live scene into a cubetexture which you can then apply to a material, for instance as an environment map (via the envMap property). The contents of it run inside a portal and are separate from the rest of the canvas, therefore you can have events in there, environment maps, etc. + +```tsx +export type RenderCubeTextureProps = Omit & { + /** Optional stencil buffer, defaults to false */ + stencilBuffer?: boolean + /** Optional depth buffer, defaults to true */ + depthBuffer?: boolean + /** Optional generate mipmaps, defaults to false */ + generateMipmaps?: boolean + /** Optional render priority, defaults to 0 */ + renderPriority?: number + /** Optional event priority, defaults to 0 */ + eventPriority?: number + /** Optional frame count, defaults to Infinity. If you set it to 1, it would only render a single frame, etc */ + frames?: number + /** Optional event compute, defaults to undefined */ + compute?: ComputeFunction + /** Flip cubemap, see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLCubeRenderTarget.js */ + flip?: boolean + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be rendered into a portal */ + children: React.ReactNode + near?: number + far?: number + position?: ReactThreeFiber.Vector3 + rotation?: ReactThreeFiber.Euler + scale?: ReactThreeFiber.Vector3 + quaternion?: ReactThreeFiber.Quaternion + matrix?: ReactThreeFiber.Matrix4 + matrixAutoUpdate?: boolean +} + +export type RenderCubeTextureApi = { + scene: THREE.Scene + fbo: THREE.WebGLCubeRenderTarget + camera: THREE.CubeCamera +} +``` + +```jsx +const api = useRef(null!) +// ... + + + + + +``` + +### Fisheye + +

    + Demo +

    + +```tsx +export type FisheyeProps = JSX.IntrinsicElements['mesh'] & { + /** Zoom factor, 0..1, 0 */ + zoom?: number + /** Number of segments, 64 */ + segments?: number + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be projected into the fisheye */ + children: React.ReactNode + /** Optional render priority, defaults to 1 */ + renderPriority?: number +} +``` + +This component will take over system rendering. It portals its children into a cubemap which is then projected onto a sphere. The sphere is rendered out on the screen, filling it. You can lower the resolution to increase performance. Six renders per frame are necessary to construct a full fisheye view, and since each facet of the cubemap only takes a portion of the screen full resolution is not necessary. You can also reduce the amount of segments (resulting in edgier rounds). + +```jsx + + + + + +``` + #### Mask

    @@ -3723,15 +4090,19 @@ For instance, one could want the Html component to be pinned to `positive x`, `p Demo

    -Calculates a boundary box and centers the camera accordingly. If you are using camera controls, make sure to pass them the `makeDefault` prop. `fit` fits the current view on first render. `clip` sets the cameras near/far planes. `observe` will trigger on window resize. +Calculates a boundary box and centers the camera accordingly. If you are using camera controls, make sure to pass them the `makeDefault` prop. `fit` fits the current view on first render. `clip` sets the cameras near/far planes. `observe` will trigger on window resize. To control the damping animation, use `maxDuration` to set the animation length in seconds, and `interpolateFunc` to define how the animation changes over time (should be an increasing function in [0, 1] interval, `interpolateFunc(0) === 0`, `interpolateFunc(1) === 1`). ```jsx - +const interpolateFunc = (t: number) => 1 - Math.exp(-5 * t) + 0.007 * t // Matches the default Bounds behavior +const interpolateFunc1 = (t: number) => -t * t * t + 2 * t * t // Start smoothly, finish linearly +const interpolateFunc2 = (t: number) => -t * t * t + t * t + t // Start linearly, finish smoothly + + ``` -The Bounds component also acts as a context provider, use the `useBounds` hook to refresh the bounds, fit the camera, clip near/far planes, go to camera orientations or focus objects. `refresh(object?: THREE.Object3D | THREE.Box3)` will recalculate bounds, since this can be expensive only call it when you know the view has changed. `clip` sets the cameras near/far planes. `to` sets a position and target for the camera. `fit` zooms and centers the view. +The Bounds component also acts as a context provider, use the `useBounds` hook to refresh the bounds, fit the camera, clip near/far planes, go to camera orientations or focus objects. `refresh(object?: THREE.Object3D | THREE.Box3)` will recalculate bounds, since this can be expensive only call it when you know the view has changed. `reset` centers the view. `moveTo` changes the camera position. `lookAt` changes the camera orientation, with the respect to up-vector, if specified. `clip` sets the cameras near/far planes. `fit` centers the view for non-orthographic cameras (same as reset) or zooms the view for orthographic cameras. ```jsx function Foo() { @@ -3744,10 +4115,17 @@ function Foo() { // bounds.refresh(ref.current).clip().fit() // bounds.refresh(new THREE.Box3()).clip().fit() - // Or, send the camera to a specific orientatin - // bounds.to({position: [0, 10, 10], target: {[5, 5, 0]}}) + // Or, move the camera to a specific position, and change its orientation + // bounds.moveTo([0, 10, 10]).lookAt({ target: [5, 5, 0], up: [0, -1, 0] }) + + // For orthographic cameras, reset has to be used to center the view (fit would only change its zoom to match the bounding box) + // bounds.refresh().reset().clip().fit() + }, [...]) +} + + ``` #### CameraShake @@ -3865,7 +4243,7 @@ For a little more realistic results enable accumulative shadows, which requires ```jsx - + ``` @@ -4351,21 +4729,63 @@ attribute vec3 color; [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.pmnd.rs/?path=/story/staging-cloud--cloud-st) ![](https://img.shields.io/badge/-suspense-brightgreen)

    + Demo Demo +

    Particle based cloud. -👉 Note: `` component is not meant to be used in production environments as it relies on third-party CDN. +```tsx +type CloudsProps = JSX.IntrinsicElements['group'] & { + /** Optional cloud texture, points to a default hosted on rawcdn.githack */ + texture?: string + /** Maximum number of segments, default: 200 (make this tight to save memory!) */ + limit?: number + /** How many segments it renders, default: undefined (all) */ + range?: number + /** Which material it will override, default: MeshLambertMaterial */ + material?: typeof Material +} + +type CloudProps = JSX.IntrinsicElements['group'] & { + /** A seeded random will show the same cloud consistently, default: Math.random() */ + seed?: number + /** How many segments or particles the cloud will have, default: 20 */ + segments?: number + /** The box3 bounds of the cloud, default: [5, 1, 1] */ + bounds?: ReactThreeFiber.Vector3 + /** How to arrange segment volume inside the bounds, default: inside (cloud are smaller at the edges) */ + concentrate?: 'random' | 'inside' | 'outside' + /** The general scale of the segments */ + scale?: ReactThreeFiber.Vector3 + /** The volume/thickness of the segments, default: 6 */ + volume?: number + /** The smallest volume when distributing clouds, default: 0.25 */ + smallestVolume?: number + /** An optional function that allows you to distribute points and volumes (overriding all settings), default: null + * Both point and volume are factors, point x/y/z can be between -1 and 1, volume between 0 and 1 */ + distribute?: (cloud: CloudState, index: number) => { point: Vector3; volume?: number } + /** Growth factor for animated clouds (speed > 0), default: 4 */ + growth?: number + /** Animation factor, default: 0 */ + speed?: number + /** Camera distance until the segments will fade, default: 10 */ + fade?: number + /** Opacity, default: 1 */ + opacity?: number + /** Color, default: white */ + color?: ReactThreeFiber.Color +} +``` + +Use the `` provider to glob all clouds into a single, instanced draw call. ```jsx - + + + + ``` #### useEnvironment @@ -4451,3 +4871,21 @@ return ( ... ) ``` + +#### ShadowAlpha + +Makes an object's shadow respect its opacity and alphaMap. + +```jsx + + + + + + +``` + +> Note: This component uses Screendoor transparency using a dither pattern. This pattern is notacible when the camera gets close to the shadow. diff --git a/package.json b/package.json index b9c6274f5..d011f854c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "main": "index.cjs.js", "module": "index.js", "types": "index.d.ts", + "react-native": "native/index.cjs.js", "sideEffects": false, "commitlint": { "extends": [ @@ -49,34 +50,34 @@ "test": "npm run eslint:ci && (cd test/e2e; ./e2e.sh)", "typecheck": "tsc --noEmit --emitDeclarationOnly false --strict --jsx react", "typegen": "tsc --emitDeclarationOnly", - "storybook": "NODE_OPTIONS=\"--openssl-legacy-provider\" storybook dev -p 6006", - "build-storybook": "NODE_OPTIONS=\"--openssl-legacy-provider\" storybook build", + "storybook": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider\" storybook dev -p 6006", + "build-storybook": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider\" storybook build", "copy": "copyfiles package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"", "release": "semantic-release" }, "dependencies": { "@babel/runtime": "^7.11.2", - "@mediapipe/tasks-vision": "0.10.2", + "@mediapipe/tasks-vision": "0.10.8", "@react-spring/three": "~9.6.1", "@use-gesture/react": "^10.2.24", "camera-controls": "^2.4.2", + "cross-env": "^7.0.3", "detect-gpu": "^5.0.28", "glsl-noise": "^0.0.0", - "lodash.clamp": "^4.0.3", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", - "maath": "^0.6.0", + "maath": "^0.10.7", "meshline": "^3.1.6", "react-composer": "^5.0.3", "react-merge-refs": "^1.1.0", - "stats-gl": "^1.0.4", + "stats-gl": "^2.0.0", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", - "three-mesh-bvh": "^0.6.0", - "three-stdlib": "^2.23.9", + "three-mesh-bvh": "^0.7.0", + "three-stdlib": "^2.29.4", "troika-three-text": "^0.47.2", + "tunnel-rat": "^0.1.2", "utility-types": "^3.10.0", - "zustand": "^3.5.13" + "uuid": "^9.0.1", + "zustand": "^3.7.1" }, "devDependencies": { "@babel/core": "^7.14.3", @@ -103,9 +104,8 @@ "@storybook/react-vite": "^7.0.12", "@storybook/theming": "^7.0.12", "@types/jest": "^26.0.10", - "@types/lodash-es": "^4.17.3", - "@types/react": "^17.0.5", - "@types/react-dom": "^17.0.5", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "@types/three": "^0.149.0", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", diff --git a/src/core/AccumulativeShadows.tsx b/src/core/AccumulativeShadows.tsx index a91f2fe1a..5d7786d37 100644 --- a/src/core/AccumulativeShadows.tsx +++ b/src/core/AccumulativeShadows.tsx @@ -4,6 +4,7 @@ import { extend, ReactThreeFiber, useFrame, useThree } from '@react-three/fiber' import { shaderMaterial } from './shaderMaterial' import { DiscardMaterial } from '../materials/DiscardMaterial' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' function isLight(object: any): object is THREE.Light { return object.isLight @@ -72,11 +73,13 @@ declare global { } } -export const accumulativeContext = React.createContext(null as unknown as AccumulativeContext) +export const accumulativeContext = /* @__PURE__ */ React.createContext( + null as unknown as AccumulativeContext +) -const SoftShadowMaterial = shaderMaterial( +const SoftShadowMaterial = /* @__PURE__ */ shaderMaterial( { - color: new THREE.Color(), + color: /* @__PURE__ */ new THREE.Color(), blend: 2.0, alphaTest: 0.75, opacity: 0, @@ -97,14 +100,14 @@ const SoftShadowMaterial = shaderMaterial( vec4 sampledDiffuseColor = texture2D(map, vUv); gl_FragColor = vec4(color * sampledDiffuseColor.r * blend, max(0.0, (1.0 - (sampledDiffuseColor.r + sampledDiffuseColor.g + sampledDiffuseColor.b) / alphaTest)) * opacity); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) export const AccumulativeShadows: ForwardRefComponent< JSX.IntrinsicElements['group'] & AccumulativeShadowsProps, AccumulativeContext -> = React.forwardRef( +> = /* @__PURE__ */ React.forwardRef( ( { children, @@ -253,7 +256,7 @@ export type RandomizedLightProps = { export const RandomizedLight: ForwardRefComponent< JSX.IntrinsicElements['group'] & RandomizedLightProps, AccumulativeLightContext -> = React.forwardRef( +> = /* @__PURE__ */ React.forwardRef( ( { castShadow = true, @@ -266,7 +269,7 @@ export const RandomizedLight: ForwardRefComponent< position = [0, 0, 0], radius = 1, amount = 8, - intensity = parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 155 ? Math.PI : 1, + intensity = version >= 155 ? Math.PI : 1, ambient = 0.5, ...props }: JSX.IntrinsicElements['group'] & RandomizedLightProps, @@ -304,8 +307,8 @@ export const RandomizedLight: ForwardRefComponent< React.useImperativeHandle(forwardRef, () => api, [api]) React.useLayoutEffect(() => { const group = gLights.current - if (parent) parent.lights.set(group.uuid, api) - return () => void parent.lights.delete(group.uuid) + if (parent) parent.lights?.set(group.uuid, api) + return () => void parent?.lights?.delete(group.uuid) }, [parent, api]) return ( diff --git a/src/core/ArcballControls.tsx b/src/core/ArcballControls.tsx index 4c0e79daa..3c8c3eb38 100644 --- a/src/core/ArcballControls.tsx +++ b/src/core/ArcballControls.tsx @@ -23,55 +23,55 @@ export type ArcballControlsProps = Omit< 'ref' > -export const ArcballControls: ForwardRefComponent = forwardRef< - ArcballControlsImpl, - ArcballControlsProps ->(({ camera, makeDefault, regress, domElement, onChange, onStart, onEnd, ...restProps }, ref) => { - const invalidate = useThree((state) => state.invalidate) - const defaultCamera = useThree((state) => state.camera) - const gl = useThree((state) => state.gl) - const events = useThree((state) => state.events) as EventManager - const set = useThree((state) => state.set) - const get = useThree((state) => state.get) - const performance = useThree((state) => state.performance) - const explCamera = camera || defaultCamera - const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement - const controls = useMemo(() => new ArcballControlsImpl(explCamera), [explCamera]) +export const ArcballControls: ForwardRefComponent = + /* @__PURE__ */ forwardRef( + ({ camera, makeDefault, regress, domElement, onChange, onStart, onEnd, ...restProps }, ref) => { + const invalidate = useThree((state) => state.invalidate) + const defaultCamera = useThree((state) => state.camera) + const gl = useThree((state) => state.gl) + const events = useThree((state) => state.events) as EventManager + const set = useThree((state) => state.set) + const get = useThree((state) => state.get) + const performance = useThree((state) => state.performance) + const explCamera = camera || defaultCamera + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + const controls = useMemo(() => new ArcballControlsImpl(explCamera), [explCamera]) - useFrame(() => { - if (controls.enabled) controls.update() - }, -1) + useFrame(() => { + if (controls.enabled) controls.update() + }, -1) - useEffect(() => { - controls.connect(explDomElement) - return () => void controls.dispose() - }, [explDomElement, regress, controls, invalidate]) + useEffect(() => { + controls.connect(explDomElement) + return () => void controls.dispose() + }, [explDomElement, regress, controls, invalidate]) - useEffect(() => { - const callback = (e: Event) => { - invalidate() - if (regress) performance.regress() - if (onChange) onChange(e) - } + useEffect(() => { + const callback = (e: Event) => { + invalidate() + if (regress) performance.regress() + if (onChange) onChange(e) + } - controls.addEventListener('change', callback) - if (onStart) controls.addEventListener('start', onStart) - if (onEnd) controls.addEventListener('end', onEnd) + controls.addEventListener('change', callback) + if (onStart) controls.addEventListener('start', onStart) + if (onEnd) controls.addEventListener('end', onEnd) - return () => { - controls.removeEventListener('change', callback) - if (onStart) controls.removeEventListener('start', onStart) - if (onEnd) controls.removeEventListener('end', onEnd) - } - }, [onChange, onStart, onEnd]) + return () => { + controls.removeEventListener('change', callback) + if (onStart) controls.removeEventListener('start', onStart) + if (onEnd) controls.removeEventListener('end', onEnd) + } + }, [onChange, onStart, onEnd]) - useEffect(() => { - if (makeDefault) { - const old = get().controls - set({ controls }) - return () => set({ controls: old }) - } - }, [makeDefault, controls]) + useEffect(() => { + if (makeDefault) { + const old = get().controls + set({ controls }) + return () => set({ controls: old }) + } + }, [makeDefault, controls]) - return -}) + return + } + ) diff --git a/src/core/BBAnchor.tsx b/src/core/BBAnchor.tsx index ad1c16eab..a1bcb6e42 100644 --- a/src/core/BBAnchor.tsx +++ b/src/core/BBAnchor.tsx @@ -2,15 +2,15 @@ import * as React from 'react' import * as THREE from 'three' import { useFrame, GroupProps } from '@react-three/fiber' -const boundingBox = new THREE.Box3() -const boundingBoxSize = new THREE.Vector3() +const boundingBox = /* @__PURE__ */ new THREE.Box3() +const boundingBoxSize = /* @__PURE__ */ new THREE.Vector3() export interface BBAnchorProps extends GroupProps { anchor: THREE.Vector3 | [number, number, number] } export const BBAnchor = ({ anchor, ...props }: BBAnchorProps) => { - const ref = React.useRef(null!) + const ref = React.useRef(null!) const parentRef = React.useRef(null) // Reattach group created by this component to the parent's parent, @@ -29,9 +29,9 @@ export const BBAnchor = ({ anchor, ...props }: BBAnchorProps) => { boundingBox.getSize(boundingBoxSize) ref.current.position.set( - parentRef.current.position.x + (boundingBoxSize.x * anchor[0]) / 2, - parentRef.current.position.y + (boundingBoxSize.y * anchor[1]) / 2, - parentRef.current.position.z + (boundingBoxSize.z * anchor[2]) / 2 + parentRef.current.position.x + (boundingBoxSize.x * (Array.isArray(anchor) ? anchor[0] : anchor.x)) / 2, + parentRef.current.position.y + (boundingBoxSize.y * (Array.isArray(anchor) ? anchor[1] : anchor.y)) / 2, + parentRef.current.position.z + (boundingBoxSize.z * (Array.isArray(anchor) ? anchor[2] : anchor.z)) / 2 ) } }) diff --git a/src/core/Billboard.tsx b/src/core/Billboard.tsx index 140bc28aa..b5b10e714 100644 --- a/src/core/Billboard.tsx +++ b/src/core/Billboard.tsx @@ -1,7 +1,6 @@ import * as React from 'react' -import { Group } from 'three' +import { Group, Quaternion } from 'three' import { useFrame } from '@react-three/fiber' -import mergeRefs from 'react-merge-refs' import { ForwardRefComponent } from '../helpers/ts-utils' export type BillboardProps = { @@ -20,23 +19,36 @@ export type BillboardProps = { * * ``` */ -export const Billboard: ForwardRefComponent = React.forwardRef( - function Billboard({ follow = true, lockX = false, lockY = false, lockZ = false, ...props }, ref) { - const localRef = React.useRef() - useFrame(({ camera }) => { - if (!follow || !localRef.current) return +export const Billboard: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + Group, + BillboardProps +>(function Billboard({ children, follow = true, lockX = false, lockY = false, lockZ = false, ...props }, fref) { + const inner = React.useRef(null!) + const localRef = React.useRef(null!) + const q = new Quaternion() - // save previous rotation in case we're locking an axis - const prevRotation = localRef.current.rotation.clone() + useFrame(({ camera }) => { + if (!follow || !localRef.current) return - // always face the camera - camera.getWorldQuaternion(localRef.current.quaternion) + // save previous rotation in case we're locking an axis + const prevRotation = localRef.current.rotation.clone() - // readjust any axis that is locked - if (lockX) localRef.current.rotation.x = prevRotation.x - if (lockY) localRef.current.rotation.y = prevRotation.y - if (lockZ) localRef.current.rotation.z = prevRotation.z - }) - return - } -) + // always face the camera + localRef.current.updateMatrix() + localRef.current.updateWorldMatrix(false, false) + localRef.current.getWorldQuaternion(q) + camera.getWorldQuaternion(inner.current.quaternion).premultiply(q.invert()) + + // readjust any axis that is locked + if (lockX) localRef.current.rotation.x = prevRotation.x + if (lockY) localRef.current.rotation.y = prevRotation.y + if (lockZ) localRef.current.rotation.z = prevRotation.z + }) + + React.useImperativeHandle(fref, () => localRef.current, []) + return ( + + {children} + + ) +}) diff --git a/src/core/Bounds.tsx b/src/core/Bounds.tsx index 96b7399c3..73a1b38cd 100644 --- a/src/core/Bounds.tsx +++ b/src/core/Bounds.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import * as THREE from 'three' + import { useFrame, useThree } from '@react-three/fiber' export type SizeProps = { @@ -11,19 +12,28 @@ export type SizeProps = { export type BoundsApi = { getSize: () => SizeProps - refresh(object?: THREE.Object3D | THREE.Box3): any - clip(): any - fit(): any - to: ({ position, target }: { position: [number, number, number]; target?: [number, number, number] }) => any + refresh(object?: THREE.Object3D | THREE.Box3): BoundsApi + reset(): BoundsApi + moveTo(position: THREE.Vector3 | [number, number, number]): BoundsApi + lookAt({ + target, + up, + }: { + target?: THREE.Vector3 | [number, number, number] + up?: THREE.Vector3 | [number, number, number] + }): BoundsApi + to({ position, target }: { position: [number, number, number]; target: [number, number, number] }): BoundsApi + fit(): BoundsApi + clip(): BoundsApi } export type BoundsProps = JSX.IntrinsicElements['group'] & { - damping?: number + maxDuration?: number + margin?: number + observe?: boolean fit?: boolean clip?: boolean - observe?: boolean - margin?: number - eps?: number + interpolateFunc?: (t: number) => number onFit?: (data: SizeProps) => void } @@ -35,49 +45,83 @@ type ControlsProto = { removeEventListener: (event: string, callback: (event: any) => void) => void } +type OriginT = { + camPos: THREE.Vector3 + camRot: THREE.Quaternion + camZoom: number +} + +type GoalT = { + camPos: THREE.Vector3 | undefined + camRot: THREE.Quaternion | undefined + camZoom: number | undefined + camUp: THREE.Vector3 | undefined + target: THREE.Vector3 | undefined +} + +// eslint-disable-next-line no-shadow +enum AnimationState { + NONE = 0, + START = 1, + ACTIVE = 2, +} + const isOrthographic = (def: THREE.Camera): def is THREE.OrthographicCamera => def && (def as THREE.OrthographicCamera).isOrthographicCamera const isBox3 = (def: any): def is THREE.Box3 => def && (def as THREE.Box3).isBox3 +const interpolateFuncDefault = (t: number) => { + // Imitates the previously used THREE.MathUtils.damp + return 1 - Math.exp(-5 * t) + 0.007 * t +} + const context = React.createContext(null!) -export function Bounds({ children, damping = 6, fit, clip, observe, margin = 1.2, eps = 0.01, onFit }: BoundsProps) { +export function Bounds({ + children, + maxDuration = 1.0, + margin = 1.2, + observe, + fit, + clip, + interpolateFunc = interpolateFuncDefault, + onFit, +}: BoundsProps) { const ref = React.useRef(null!) - const { camera, invalidate, size, controls: controlsImpl } = useThree() - const controls = controlsImpl as unknown as ControlsProto + + const { camera, size, invalidate } = useThree() + const controls = useThree((state) => state.controls as unknown as ControlsProto) const onFitRef = React.useRef<((data: SizeProps) => void) | undefined>(onFit) onFitRef.current = onFit - function equals(a, b) { - return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps && Math.abs(a.z - b.z) < eps - } - - function damp(v, t, lambda, delta) { - v.x = THREE.MathUtils.damp(v.x, t.x, lambda, delta) - v.y = THREE.MathUtils.damp(v.y, t.y, lambda, delta) - v.z = THREE.MathUtils.damp(v.z, t.z, lambda, delta) - } - - const [current] = React.useState(() => ({ - animating: false, - focus: new THREE.Vector3(), - camera: new THREE.Vector3(), - zoom: 1, - })) - const [goal] = React.useState(() => ({ focus: new THREE.Vector3(), camera: new THREE.Vector3(), zoom: 1 })) + const origin = React.useRef({ + camPos: new THREE.Vector3(), + camRot: new THREE.Quaternion(), + camZoom: 1, + }) + const goal = React.useRef({ + camPos: undefined, + camRot: undefined, + camZoom: undefined, + camUp: undefined, + target: undefined, + }) + const animationState = React.useRef(AnimationState.NONE) + const t = React.useRef(0) // represent animation state from 0 to 1 const [box] = React.useState(() => new THREE.Box3()) const api: BoundsApi = React.useMemo(() => { function getSize() { - const size = box.getSize(new THREE.Vector3()) + const boxSize = box.getSize(new THREE.Vector3()) const center = box.getCenter(new THREE.Vector3()) - const maxSize = Math.max(size.x, size.y, size.z) + const maxSize = Math.max(boxSize.x, boxSize.y, boxSize.z) const fitHeightDistance = isOrthographic(camera) ? maxSize * 4 : maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360)) const fitWidthDistance = isOrthographic(camera) ? maxSize * 4 : fitHeightDistance / camera.aspect const distance = margin * Math.max(fitHeightDistance, fitWidthDistance) - return { box, size, center, distance } + + return { box, size: boxSize, center, distance } } return { @@ -95,109 +139,153 @@ export function Bounds({ children, damping = 6, fit, clip, observe, margin = 1.2 box.setFromCenterAndSize(new THREE.Vector3(), new THREE.Vector3(max, max, max)) } - if (controls?.constructor.name === 'OrthographicTrackballControls') { - // Put camera on a sphere along which it should move - const { distance } = getSize() - const direction = camera.position.clone().sub(controls.target).normalize().multiplyScalar(distance) - const newPos = controls.target.clone().add(direction) - camera.position.copy(newPos) - } + origin.current.camPos.copy(camera.position) + origin.current.camRot.copy(camera.quaternion) + isOrthographic(camera) && (origin.current.camZoom = camera.zoom) + + goal.current.camPos = undefined + goal.current.camRot = undefined + goal.current.camZoom = undefined + goal.current.camUp = undefined + goal.current.target = undefined return this }, - clip() { - const { distance } = getSize() - if (controls) controls.maxDistance = distance * 10 - camera.near = distance / 100 - camera.far = distance * 100 - camera.updateProjectionMatrix() - if (controls) controls.update() - invalidate() + reset() { + const { center, distance } = getSize() + + const direction = camera.position.clone().sub(center).normalize() + goal.current.camPos = center.clone().addScaledVector(direction, distance) + goal.current.target = center.clone() + const mCamRot = new THREE.Matrix4().lookAt(goal.current.camPos, goal.current.target, camera.up) + goal.current.camRot = new THREE.Quaternion().setFromRotationMatrix(mCamRot) + + animationState.current = AnimationState.START + t.current = 0 + return this }, - to({ position, target }: { position: [number, number, number]; target?: [number, number, number] }) { - current.camera.copy(camera.position) - const { center } = getSize() - goal.camera.set(...position) + moveTo(position: THREE.Vector3 | [number, number, number]) { + goal.current.camPos = Array.isArray(position) ? new THREE.Vector3(...position) : position.clone() - if (target) { - goal.focus.set(...target) - } else { - goal.focus.copy(center) - } + animationState.current = AnimationState.START + t.current = 0 - if (damping) { - current.animating = true + return this + }, + lookAt({ + target, + up, + }: { + target: THREE.Vector3 | [number, number, number] + up?: THREE.Vector3 | [number, number, number] + }) { + goal.current.target = Array.isArray(target) ? new THREE.Vector3(...target) : target.clone() + if (up) { + goal.current.camUp = Array.isArray(up) ? new THREE.Vector3(...up) : up.clone() } else { - camera.position.set(...position) + goal.current.camUp = camera.up.clone() } + const mCamRot = new THREE.Matrix4().lookAt( + goal.current.camPos || camera.position, + goal.current.target, + goal.current.camUp + ) + goal.current.camRot = new THREE.Quaternion().setFromRotationMatrix(mCamRot) + + animationState.current = AnimationState.START + t.current = 0 return this }, + /** + * @deprecated Use moveTo and lookAt instead + */ + to({ position, target }: { position: [number, number, number]; target?: [number, number, number] }) { + return this.moveTo(position).lookAt({ target }) + }, fit() { - current.camera.copy(camera.position) - if (controls) current.focus.copy(controls.target) + if (!isOrthographic(camera)) { + // For non-orthographic cameras, fit should behave exactly like reset + return this.reset() + } - const { center, distance } = getSize() - const direction = center.clone().sub(camera.position).normalize().multiplyScalar(distance) - - goal.camera.copy(center).sub(direction) - goal.focus.copy(center) - - if (isOrthographic(camera)) { - current.zoom = camera.zoom - - let maxHeight = 0, - maxWidth = 0 - const vertices = [ - new THREE.Vector3(box.min.x, box.min.y, box.min.z), - new THREE.Vector3(box.min.x, box.max.y, box.min.z), - new THREE.Vector3(box.min.x, box.min.y, box.max.z), - new THREE.Vector3(box.min.x, box.max.y, box.max.z), - new THREE.Vector3(box.max.x, box.max.y, box.max.z), - new THREE.Vector3(box.max.x, box.max.y, box.min.z), - new THREE.Vector3(box.max.x, box.min.y, box.max.z), - new THREE.Vector3(box.max.x, box.min.y, box.min.z), - ] - // Transform the center and each corner to camera space - center.applyMatrix4(camera.matrixWorldInverse) - for (const v of vertices) { - v.applyMatrix4(camera.matrixWorldInverse) - maxHeight = Math.max(maxHeight, Math.abs(v.y - center.y)) - maxWidth = Math.max(maxWidth, Math.abs(v.x - center.x)) - } - maxHeight *= 2 - maxWidth *= 2 - const zoomForHeight = (camera.top - camera.bottom) / maxHeight - const zoomForWidth = (camera.right - camera.left) / maxWidth - goal.zoom = Math.min(zoomForHeight, zoomForWidth) / margin - if (!damping) { - camera.zoom = goal.zoom - camera.updateProjectionMatrix() - } + // For orthographic cameras, fit should only modify the zoom value + let maxHeight = 0, + maxWidth = 0 + const vertices = [ + new THREE.Vector3(box.min.x, box.min.y, box.min.z), + new THREE.Vector3(box.min.x, box.max.y, box.min.z), + new THREE.Vector3(box.min.x, box.min.y, box.max.z), + new THREE.Vector3(box.min.x, box.max.y, box.max.z), + new THREE.Vector3(box.max.x, box.max.y, box.max.z), + new THREE.Vector3(box.max.x, box.max.y, box.min.z), + new THREE.Vector3(box.max.x, box.min.y, box.max.z), + new THREE.Vector3(box.max.x, box.min.y, box.min.z), + ] + + // Transform the center and each corner to camera space + const pos = goal.current.camPos || camera.position + const target = goal.current.target || controls?.target + const up = goal.current.camUp || camera.up + const mCamWInv = target + ? new THREE.Matrix4().lookAt(pos, target, up).setPosition(pos).invert() + : camera.matrixWorldInverse + for (const v of vertices) { + v.applyMatrix4(mCamWInv) + maxHeight = Math.max(maxHeight, Math.abs(v.y)) + maxWidth = Math.max(maxWidth, Math.abs(v.x)) } + maxHeight *= 2 + maxWidth *= 2 + const zoomForHeight = (camera.top - camera.bottom) / maxHeight + const zoomForWidth = (camera.right - camera.left) / maxWidth - if (damping) { - current.animating = true - } else { - camera.position.copy(goal.camera) - camera.lookAt(goal.focus) - if (controls) { - controls.target.copy(goal.focus) - controls.update() - } + goal.current.camZoom = Math.min(zoomForHeight, zoomForWidth) / margin + + animationState.current = AnimationState.START + t.current = 0 + + onFitRef.current && onFitRef.current(this.getSize()) + + return this + }, + clip() { + const { distance } = getSize() + + camera.near = distance / 100 + camera.far = distance * 100 + camera.updateProjectionMatrix() + + if (controls) { + controls.maxDistance = distance * 10 + controls.update() } - if (onFitRef.current) onFitRef.current(this.getSize()) + invalidate() + return this }, } - }, [box, camera, controls, margin, damping, invalidate]) + }, [box, camera, controls, margin, invalidate]) React.useLayoutEffect(() => { if (controls) { // Try to prevent drag hijacking - const callback = () => (current.animating = false) + const callback = () => { + if (controls && goal.current.target && animationState.current !== AnimationState.NONE) { + const front = new THREE.Vector3().setFromMatrixColumn(camera.matrix, 2) + const d0 = origin.current.camPos.distanceTo(controls.target) + const d1 = (goal.current.camPos || origin.current.camPos).distanceTo(goal.current.target) + const d = (1 - t.current) * d0 + t.current * d1 + + controls.target.copy(camera.position).addScaledVector(front, -d) + controls.update() + } + + animationState.current = AnimationState.NONE + } + controls.addEventListener('start', callback) return () => controls.removeEventListener('start', callback) } @@ -208,35 +296,49 @@ export function Bounds({ children, damping = 6, fit, clip, observe, margin = 1.2 React.useLayoutEffect(() => { if (observe || count.current++ === 0) { api.refresh() - if (fit) api.fit() + if (fit) api.reset().fit() if (clip) api.clip() } }, [size, clip, fit, observe, camera, controls]) useFrame((state, delta) => { - if (current.animating) { - damp(current.focus, goal.focus, damping, delta) - damp(current.camera, goal.camera, damping, delta) - current.zoom = THREE.MathUtils.damp(current.zoom, goal.zoom, damping, delta) - camera.position.copy(current.camera) - - if (isOrthographic(camera)) { - camera.zoom = current.zoom + // This [additional animation step START] is needed to guarantee that delta used in animation isn't absurdly high (2-3 seconds) which is actually possible if rendering happens on demand... + if (animationState.current === AnimationState.START) { + animationState.current = AnimationState.ACTIVE + invalidate() + } else if (animationState.current === AnimationState.ACTIVE) { + t.current += delta / maxDuration + + if (t.current >= 1) { + goal.current.camPos && camera.position.copy(goal.current.camPos) + goal.current.camRot && camera.quaternion.copy(goal.current.camRot) + goal.current.camUp && camera.up.copy(goal.current.camUp) + goal.current.camZoom && isOrthographic(camera) && (camera.zoom = goal.current.camZoom) + + camera.updateMatrixWorld() camera.updateProjectionMatrix() - } - if (!controls) { - camera.lookAt(current.focus) + if (controls && goal.current.target) { + controls.target.copy(goal.current.target) + controls.update() + } + + animationState.current = AnimationState.NONE } else { - controls.target.copy(current.focus) - controls.update() + const k = interpolateFunc(t.current) + + goal.current.camPos && camera.position.lerpVectors(origin.current.camPos, goal.current.camPos, k) + goal.current.camRot && camera.quaternion.slerpQuaternions(origin.current.camRot, goal.current.camRot, k) + goal.current.camUp && camera.up.set(0, 1, 0).applyQuaternion(camera.quaternion) + goal.current.camZoom && + isOrthographic(camera) && + (camera.zoom = (1 - k) * origin.current.camZoom + k * goal.current.camZoom) + + camera.updateMatrixWorld() + camera.updateProjectionMatrix() } invalidate() - if (isOrthographic(camera) && !(Math.abs(current.zoom - goal.zoom) < eps)) return - if (!isOrthographic(camera) && !equals(current.camera, goal.camera)) return - if (controls && !equals(current.focus, goal.focus)) return - current.animating = false } }) diff --git a/src/core/CameraControls.tsx b/src/core/CameraControls.tsx index cc3375de9..f1bfdc5f7 100644 --- a/src/core/CameraControls.tsx +++ b/src/core/CameraControls.tsx @@ -38,7 +38,7 @@ export type CameraControlsProps = Omit< 'ref' > -export const CameraControls: ForwardRefComponent = forwardRef< +export const CameraControls: ForwardRefComponent = /* @__PURE__ */ forwardRef< CameraControlsImpl, CameraControlsProps >((props, ref) => { diff --git a/src/core/CameraShake.tsx b/src/core/CameraShake.tsx index f356ab3f8..37d23cf48 100644 --- a/src/core/CameraShake.tsx +++ b/src/core/CameraShake.tsx @@ -28,77 +28,75 @@ export interface CameraShakeProps { rollFrequency?: number } -export const CameraShake: ForwardRefComponent = React.forwardRef< - ShakeController | undefined, - CameraShakeProps ->( - ( - { - intensity = 1, - decay, - decayRate = 0.65, - maxYaw = 0.1, - maxPitch = 0.1, - maxRoll = 0.1, - yawFrequency = 0.1, - pitchFrequency = 0.1, - rollFrequency = 0.1, - }, - ref - ) => { - const camera = useThree((state) => state.camera) - const defaultControls = useThree((state) => state.controls) as unknown as ControlsProto - const intensityRef = React.useRef(intensity) - const initialRotation = React.useRef(camera.rotation.clone()) - const [yawNoise] = React.useState(() => new SimplexNoise()) - const [pitchNoise] = React.useState(() => new SimplexNoise()) - const [rollNoise] = React.useState(() => new SimplexNoise()) +export const CameraShake: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + intensity = 1, + decay, + decayRate = 0.65, + maxYaw = 0.1, + maxPitch = 0.1, + maxRoll = 0.1, + yawFrequency = 0.1, + pitchFrequency = 0.1, + rollFrequency = 0.1, + }, + ref + ) => { + const camera = useThree((state) => state.camera) + const defaultControls = useThree((state) => state.controls) as unknown as ControlsProto + const intensityRef = React.useRef(intensity) + const initialRotation = React.useRef(camera.rotation.clone()) + const [yawNoise] = React.useState(() => new SimplexNoise()) + const [pitchNoise] = React.useState(() => new SimplexNoise()) + const [rollNoise] = React.useState(() => new SimplexNoise()) - const constrainIntensity = () => { - if (intensityRef.current < 0 || intensityRef.current > 1) { - intensityRef.current = intensityRef.current < 0 ? 0 : 1 + const constrainIntensity = () => { + if (intensityRef.current < 0 || intensityRef.current > 1) { + intensityRef.current = intensityRef.current < 0 ? 0 : 1 + } } - } - React.useImperativeHandle( - ref, - () => ({ - getIntensity: (): number => intensityRef.current, - setIntensity: (val: number): void => { - intensityRef.current = val - constrainIntensity() - }, - }), - [] - ) + React.useImperativeHandle( + ref, + () => ({ + getIntensity: (): number => intensityRef.current, + setIntensity: (val: number): void => { + intensityRef.current = val + constrainIntensity() + }, + }), + [] + ) - React.useEffect(() => { - if (defaultControls) { - const callback = () => void (initialRotation.current = camera.rotation.clone()) - defaultControls.addEventListener('change', callback) - callback() - return () => void defaultControls.removeEventListener('change', callback) - } - }, [camera, defaultControls]) + React.useEffect(() => { + if (defaultControls) { + const callback = () => void (initialRotation.current = camera.rotation.clone()) + defaultControls.addEventListener('change', callback) + callback() + return () => void defaultControls.removeEventListener('change', callback) + } + }, [camera, defaultControls]) - useFrame((state, delta) => { - const shake = Math.pow(intensityRef.current, 2) - const yaw = maxYaw * shake * yawNoise.noise(state.clock.elapsedTime * yawFrequency, 1) - const pitch = maxPitch * shake * pitchNoise.noise(state.clock.elapsedTime * pitchFrequency, 1) - const roll = maxRoll * shake * rollNoise.noise(state.clock.elapsedTime * rollFrequency, 1) + useFrame((state, delta) => { + const shake = Math.pow(intensityRef.current, 2) + const yaw = maxYaw * shake * yawNoise.noise(state.clock.elapsedTime * yawFrequency, 1) + const pitch = maxPitch * shake * pitchNoise.noise(state.clock.elapsedTime * pitchFrequency, 1) + const roll = maxRoll * shake * rollNoise.noise(state.clock.elapsedTime * rollFrequency, 1) - camera.rotation.set( - initialRotation.current.x + pitch, - initialRotation.current.y + yaw, - initialRotation.current.z + roll - ) + camera.rotation.set( + initialRotation.current.x + pitch, + initialRotation.current.y + yaw, + initialRotation.current.z + roll + ) - if (decay && intensityRef.current > 0) { - intensityRef.current -= decayRate * delta - constrainIntensity() - } - }) + if (decay && intensityRef.current > 0) { + intensityRef.current -= decayRate * delta + constrainIntensity() + } + }) - return null - } -) + return null + } + ) diff --git a/src/core/CatmullRomLine.tsx b/src/core/CatmullRomLine.tsx index 0d44f41cb..1c3040327 100644 --- a/src/core/CatmullRomLine.tsx +++ b/src/core/CatmullRomLine.tsx @@ -11,41 +11,43 @@ type Props = Omit & { segments?: number } -export const CatmullRomLine: ForwardRefComponent = React.forwardRef(function CatmullRomLine( - { points, closed = false, curveType = 'centripetal', tension = 0.5, segments = 20, vertexColors, ...rest }, - ref -) { - const curve = React.useMemo(() => { - const mappedPoints = points.map((pt) => - pt instanceof Vector3 ? pt : new Vector3(...(pt as [number, number, number])) - ) - - return new CatmullRomCurve3(mappedPoints, closed, curveType, tension) - }, [points, closed, curveType, tension]) - - const segmentedPoints = React.useMemo(() => curve.getPoints(segments), [curve, segments]) - - const interpolatedVertexColors = React.useMemo(() => { - if (!vertexColors || vertexColors.length < 2) return undefined - - if (vertexColors.length === segments + 1) return vertexColors - - const mappedColors = vertexColors.map((color) => - color instanceof Color ? color : new Color(...(color as [number, number, number])) - ) - if (closed) mappedColors.push(mappedColors[0].clone()) - - const iColors: Color[] = [mappedColors[0]] - const divisions = segments / (mappedColors.length - 1) - for (let i = 1; i < segments; i++) { - const alpha = (i % divisions) / divisions - const colorIndex = Math.floor(i / divisions) - iColors.push(mappedColors[colorIndex].clone().lerp(mappedColors[colorIndex + 1], alpha)) - } - iColors.push(mappedColors[mappedColors.length - 1]) - - return iColors - }, [vertexColors, segments]) - - return -}) +export const CatmullRomLine: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( + function CatmullRomLine( + { points, closed = false, curveType = 'centripetal', tension = 0.5, segments = 20, vertexColors, ...rest }, + ref + ) { + const curve = React.useMemo(() => { + const mappedPoints = points.map((pt) => + pt instanceof Vector3 ? pt : new Vector3(...(pt as [number, number, number])) + ) + + return new CatmullRomCurve3(mappedPoints, closed, curveType, tension) + }, [points, closed, curveType, tension]) + + const segmentedPoints = React.useMemo(() => curve.getPoints(segments), [curve, segments]) + + const interpolatedVertexColors = React.useMemo(() => { + if (!vertexColors || vertexColors.length < 2) return undefined + + if (vertexColors.length === segments + 1) return vertexColors + + const mappedColors = vertexColors.map((color) => + color instanceof Color ? color : new Color(...(color as [number, number, number])) + ) + if (closed) mappedColors.push(mappedColors[0].clone()) + + const iColors: Color[] = [mappedColors[0]] + const divisions = segments / (mappedColors.length - 1) + for (let i = 1; i < segments; i++) { + const alpha = (i % divisions) / divisions + const colorIndex = Math.floor(i / divisions) + iColors.push(mappedColors[colorIndex].clone().lerp(mappedColors[colorIndex + 1], alpha)) + } + iColors.push(mappedColors[mappedColors.length - 1]) + + return iColors + }, [vertexColors, segments]) + + return + } +) diff --git a/src/core/Caustics.tsx b/src/core/Caustics.tsx index fdea4d809..e75b36c10 100644 --- a/src/core/Caustics.tsx +++ b/src/core/Caustics.tsx @@ -11,6 +11,7 @@ import { shaderMaterial } from './shaderMaterial' import { Edges } from './Edges' import { FullScreenQuad } from 'three-stdlib' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' type CausticsMaterialType = THREE.ShaderMaterial & { cameraMatrixWorld?: THREE.Matrix4 @@ -99,13 +100,13 @@ function createNormalMaterial(side = THREE.FrontSide) { }) } -const CausticsProjectionMaterial = shaderMaterial( +const CausticsProjectionMaterial = /* @__PURE__ */ shaderMaterial( { causticsTexture: null, causticsTextureB: null, - color: new THREE.Color(), - lightProjMatrix: new THREE.Matrix4(), - lightViewMatrix: new THREE.Matrix4(), + color: /* @__PURE__ */ new THREE.Color(), + lightProjMatrix: /* @__PURE__ */ new THREE.Matrix4(), + lightViewMatrix: /* @__PURE__ */ new THREE.Matrix4(), }, `varying vec3 vWorldPosition; void main() { @@ -128,22 +129,22 @@ const CausticsProjectionMaterial = shaderMaterial( vec3 back = texture2D(causticsTextureB, lightSpacePos.xy).rgb; gl_FragColor = vec4((front + back) * color, 1.0); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) -const CausticsMaterial = shaderMaterial( +const CausticsMaterial = /* @__PURE__ */ shaderMaterial( { - cameraMatrixWorld: new THREE.Matrix4(), - cameraProjectionMatrixInv: new THREE.Matrix4(), + cameraMatrixWorld: /* @__PURE__ */ new THREE.Matrix4(), + cameraProjectionMatrixInv: /* @__PURE__ */ new THREE.Matrix4(), normalTexture: null, depthTexture: null, - lightDir: new THREE.Vector3(0, 1, 0), - lightPlaneNormal: new THREE.Vector3(0, 1, 0), + lightDir: /* @__PURE__ */ new THREE.Vector3(0, 1, 0), + lightPlaneNormal: /* @__PURE__ */ new THREE.Vector3(0, 1, 0), lightPlaneConstant: 0, near: 0.1, far: 100, - modelMatrix: new THREE.Matrix4(), + modelMatrix: /* @__PURE__ */ new THREE.Matrix4(), worldRadius: 1 / 40, ior: 1.1, bounces: 0, @@ -276,9 +277,9 @@ const CAUSTICPROPS = { generateMipmaps: true, } -const causticsContext = React.createContext(null) +const causticsContext = /* @__PURE__ */ React.createContext(null) -export const Caustics: ForwardRefComponent = React.forwardRef( +export const Caustics: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { debug, diff --git a/src/core/Center.tsx b/src/core/Center.tsx index 654bdb496..cf160816b 100644 --- a/src/core/Center.tsx +++ b/src/core/Center.tsx @@ -42,77 +42,75 @@ export type CenterProps = { cacheKey?: any } -export const Center: ForwardRefComponent = React.forwardRef< - Group, - JSX.IntrinsicElements['group'] & CenterProps ->(function Center( - { - children, - disable, - disableX, - disableY, - disableZ, - left, - right, - top, - bottom, - front, - back, - onCentered, - precise = true, - cacheKey = 0, - ...props - }, - fRef -) { - const ref = React.useRef(null!) - const outer = React.useRef(null!) - const inner = React.useRef(null!) - React.useLayoutEffect(() => { - outer.current.matrixWorld.identity() - const box3 = new Box3().setFromObject(inner.current, precise) - const center = new Vector3() - const sphere = new Sphere() - const width = box3.max.x - box3.min.x - const height = box3.max.y - box3.min.y - const depth = box3.max.z - box3.min.z - box3.getCenter(center) - box3.getBoundingSphere(sphere) - const vAlign = top ? height / 2 : bottom ? -height / 2 : 0 - const hAlign = left ? -width / 2 : right ? width / 2 : 0 - const dAlign = front ? depth / 2 : back ? -depth / 2 : 0 +export const Center: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef(function Center( + { + children, + disable, + disableX, + disableY, + disableZ, + left, + right, + top, + bottom, + front, + back, + onCentered, + precise = true, + cacheKey = 0, + ...props + }, + fRef + ) { + const ref = React.useRef(null!) + const outer = React.useRef(null!) + const inner = React.useRef(null!) + React.useLayoutEffect(() => { + outer.current.matrixWorld.identity() + const box3 = new Box3().setFromObject(inner.current, precise) + const center = new Vector3() + const sphere = new Sphere() + const width = box3.max.x - box3.min.x + const height = box3.max.y - box3.min.y + const depth = box3.max.z - box3.min.z + box3.getCenter(center) + box3.getBoundingSphere(sphere) + const vAlign = top ? height / 2 : bottom ? -height / 2 : 0 + const hAlign = left ? -width / 2 : right ? width / 2 : 0 + const dAlign = front ? depth / 2 : back ? -depth / 2 : 0 - outer.current.position.set( - disable || disableX ? 0 : -center.x + hAlign, - disable || disableY ? 0 : -center.y + vAlign, - disable || disableZ ? 0 : -center.z + dAlign - ) + outer.current.position.set( + disable || disableX ? 0 : -center.x + hAlign, + disable || disableY ? 0 : -center.y + vAlign, + disable || disableZ ? 0 : -center.z + dAlign + ) - // Only fire onCentered if the bounding box has changed - if (typeof onCentered !== 'undefined') { - onCentered({ - parent: ref.current.parent!, - container: ref.current, - width, - height, - depth, - boundingBox: box3, - boundingSphere: sphere, - center: center, - verticalAlignment: vAlign, - horizontalAlignment: hAlign, - depthAlignment: dAlign, - }) - } - }, [cacheKey, onCentered, top, left, front, disable, disableX, disableY, disableZ, precise, right, bottom, back]) + // Only fire onCentered if the bounding box has changed + if (typeof onCentered !== 'undefined') { + onCentered({ + parent: ref.current.parent!, + container: ref.current, + width, + height, + depth, + boundingBox: box3, + boundingSphere: sphere, + center: center, + verticalAlignment: vAlign, + horizontalAlignment: hAlign, + depthAlignment: dAlign, + }) + } + }, [cacheKey, onCentered, top, left, front, disable, disableX, disableY, disableZ, precise, right, bottom, back]) - React.useImperativeHandle(fRef, () => ref.current, []) + React.useImperativeHandle(fRef, () => ref.current, []) - return ( - - - {children} + return ( + + + {children} + - - ) -}) + ) + }) diff --git a/src/core/Clone.tsx b/src/core/Clone.tsx index 6e07c25fd..b617322ab 100644 --- a/src/core/Clone.tsx +++ b/src/core/Clone.tsx @@ -1,6 +1,5 @@ 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' import { ForwardRefComponent } from '../helpers/ts-utils' @@ -60,7 +59,11 @@ function createSpread( receiveShadow, }: Omit & Partial ) { - let spread = pick(child, keys) + let spread: Record<(typeof keys)[number], any> = {} + for (const key of keys) { + spread[key] = child[key] + } + if (deep) { if (spread.geometry && deep !== 'materialsOnly') spread.geometry = spread.geometry.clone() if (spread.material && deep !== 'geometriesOnly') spread.material = spread.material.clone() @@ -79,7 +82,7 @@ function createSpread( } export const Clone: ForwardRefComponent & CloneProps, THREE.Group> = - React.forwardRef( + /* @__PURE__ */ React.forwardRef( ( { isChild = false, @@ -120,11 +123,11 @@ export const Clone: ForwardRefComponent return ( - {(object?.children).map((child) => { + {object.children.map((child) => { if (child.type === 'Bone') return return })} diff --git a/src/core/Cloud.tsx b/src/core/Cloud.tsx index d113e3225..d050178b8 100644 --- a/src/core/Cloud.tsx +++ b/src/core/Cloud.tsx @@ -1,61 +1,320 @@ import * as React from 'react' -import { Group, Texture } from 'three' -import { useFrame } from '@react-three/fiber' -import { Billboard } from './Billboard' -import { Plane } from './shapes' +import { + REVISION, + DynamicDrawUsage, + Color, + Group, + Texture, + Vector3, + InstancedMesh, + Material, + MeshLambertMaterial, + Matrix4, + Quaternion, + BufferAttribute, +} from 'three' +import { MaterialNode, extend, applyProps, useFrame, ReactThreeFiber } from '@react-three/fiber' import { useTexture } from './useTexture' +import { v4 } from 'uuid' +import { setUpdateRange } from '../helpers/deprecated' + +declare global { + namespace JSX { + interface IntrinsicElements { + cloudMaterial: MaterialNode + } + } +} const CLOUD_URL = 'https://rawcdn.githack.com/pmndrs/drei-assets/9225a9f1fbd449d9411125c2f419b843d0308c9f/cloud.png' -export function Cloud({ - opacity = 0.5, - speed = 0.4, - width = 10, - depth = 1.5, - segments = 20, - texture = CLOUD_URL, - color = '#ffffff', - depthTest = true, - ...props -}) { - const group = React.useRef() - const cloudTexture = useTexture(texture) as Texture - const clouds = React.useMemo( - () => - [...new Array(segments)].map((_, index) => ({ - x: width / 2 - Math.random() * width, - y: width / 2 - Math.random() * width, - scale: 0.4 + Math.sin(((index + 1) / segments) * Math.PI) * ((0.2 + Math.random()) * 10), - density: Math.max(0.2, Math.random()), - rotation: Math.max(0.002, 0.005 * Math.random()) * speed, - })), - [width, segments, speed] - ) - useFrame((state) => - group.current?.children.forEach((cloud, index) => { - cloud.children[0].rotation.z += clouds[index].rotation - cloud.children[0].scale.setScalar( - clouds[index].scale + (((1 + Math.sin(state.clock.getElapsedTime() / 10)) / 2) * index) / 10 - ) +type CloudState = { + uuid: string + index: number + segments: number + dist: number + matrix: Matrix4 + bounds: Vector3 + position: Vector3 + volume: number + length: number + ref: React.MutableRefObject + speed: number + growth: number + opacity: number + fade: number + density: number + rotation: number + rotationFactor: number + color: Color +} + +type CloudsProps = JSX.IntrinsicElements['group'] & { + /** Optional cloud texture, points to a default hosted on rawcdn.githack */ + texture?: string + /** Maximum number of segments, default: 200 (make this tight to save memory!) */ + limit?: number + /** How many segments it renders, default: undefined (all) */ + range?: number + /** Which material it will override, default: MeshLambertMaterial */ + material?: typeof Material +} + +type CloudProps = JSX.IntrinsicElements['group'] & { + /** A seeded random will show the same cloud consistently, default: Math.random() */ + seed?: number + /** How many segments or particles the cloud will have, default: 20 */ + segments?: number + /** The box3 bounds of the cloud, default: [5, 1, 1] */ + bounds?: ReactThreeFiber.Vector3 + /** How to arrange segment volume inside the bounds, default: inside (cloud are smaller at the edges) */ + concentrate?: 'random' | 'inside' | 'outside' + /** The general scale of the segments */ + scale?: ReactThreeFiber.Vector3 + /** The volume/thickness of the segments, default: 6 */ + volume?: number + /** The smallest volume when distributing clouds, default: 0.25 */ + smallestVolume?: number + /** An optional function that allows you to distribute points and volumes (overriding all settings), default: null + * Both point and volume are factors, point x/y/z can be between -1 and 1, volume between 0 and 1 */ + distribute?: (cloud: CloudState, index: number) => { point: Vector3; volume?: number } + /** Growth factor for animated clouds (speed > 0), default: 4 */ + growth?: number + /** Animation factor, default: 0 */ + speed?: number + /** Camera distance until the segments will fade, default: 10 */ + fade?: number + /** Opacity, default: 1 */ + opacity?: number + /** Color, default: white */ + color?: ReactThreeFiber.Color +} + +const parentMatrix = /* @__PURE__ */ new Matrix4() +const translation = /* @__PURE__ */ new Vector3() +const rotation = /* @__PURE__ */ new Quaternion() +const cpos = /* @__PURE__ */ new Vector3() +const cquat = /* @__PURE__ */ new Quaternion() +const scale = /* @__PURE__ */ new Vector3() + +const context = /* @__PURE__ */ React.createContext>(null!) +export const Clouds = /* @__PURE__ */ React.forwardRef( + ({ children, material = MeshLambertMaterial, texture = CLOUD_URL, range, limit = 200, ...props }, fref) => { + const CloudMaterial = React.useMemo(() => { + return class extends (material as typeof Material) { + constructor() { + super() + const opaque_fragment = parseInt(REVISION.replace(/\D+/g, '')) >= 154 ? 'opaque_fragment' : 'output_fragment' + this.onBeforeCompile = (shader) => { + shader.vertexShader = + `attribute float opacity; + varying float vOpacity; + ` + + shader.vertexShader.replace( + '#include ', + `#include + vOpacity = opacity; + ` + ) + shader.fragmentShader = + `varying float vOpacity; + ` + + shader.fragmentShader.replace( + `#include <${opaque_fragment}>`, + `#include <${opaque_fragment}> + gl_FragColor = vec4(outgoingLight, diffuseColor.a * vOpacity); + ` + ) + } + } + } + }, [material]) + + extend({ CloudMaterial }) + + const instance = React.useRef(null!) + const clouds = React.useRef([]) + const opacities = React.useMemo(() => new Float32Array(Array.from({ length: limit }, () => 1)), [limit]) + const colors = React.useMemo(() => new Float32Array(Array.from({ length: limit }, () => [1, 1, 1]).flat()), [limit]) + const cloudTexture = useTexture(texture) as Texture + + let t = 0 + let index = 0 + let config: CloudState + const qat = new Quaternion() + const dir = new Vector3(0, 0, 1) + const pos = new Vector3() + + useFrame((state, delta) => { + t = state.clock.getElapsedTime() + parentMatrix.copy(instance.current.matrixWorld).invert() + state.camera.matrixWorld.decompose(cpos, cquat, scale) + + for (index = 0; index < clouds.current.length; index++) { + config = clouds.current[index] + config.ref.current.matrixWorld.decompose(translation, rotation, scale) + translation.add(pos.copy(config.position).applyQuaternion(rotation).multiply(scale)) + rotation.copy(cquat).multiply(qat.setFromAxisAngle(dir, (config.rotation += delta * config.rotationFactor))) + scale.multiplyScalar(config.volume + ((1 + Math.sin(t * config.density * config.speed)) / 2) * config.growth) + config.matrix.compose(translation, rotation, scale).premultiply(parentMatrix) + config.dist = translation.distanceTo(cpos) + } + + // Depth-sort. Instances have no specific draw order, w/o sorting z would be random + clouds.current.sort((a, b) => b.dist - a.dist) + for (index = 0; index < clouds.current.length; index++) { + config = clouds.current[index] + opacities[index] = config.opacity * (config.dist < config.fade - 1 ? config.dist / config.fade : 1) + instance.current.setMatrixAt(index, config.matrix) + instance.current.setColorAt(index, config.color) + } + + // Update instance + instance.current.geometry.attributes.opacity.needsUpdate = true + instance.current.instanceMatrix.needsUpdate = true + if (instance.current.instanceColor) instance.current.instanceColor.needsUpdate = true + }) + + React.useLayoutEffect(() => { + const count = Math.min(limit, range !== undefined ? range : limit, clouds.current.length) + instance.current.count = count + setUpdateRange(instance.current.instanceMatrix, { offset: 0, count: count * 16 }) + if (instance.current.instanceColor) { + setUpdateRange(instance.current.instanceColor, { offset: 0, count: count * 3 }) + } + setUpdateRange(instance.current.geometry.attributes.opacity as BufferAttribute, { offset: 0, count: count }) }) - ) - return ( - - - {clouds.map(({ x, y, scale, density }, index) => ( - - - - - - ))} + + let imageBounds = [cloudTexture!.image.width ?? 1, cloudTexture!.image.height ?? 1] + let max = Math.max(imageBounds[0], imageBounds[1]) + imageBounds = [imageBounds[0] / max, imageBounds[1] / max] + + return ( + + + {children} + + + + + + + + - - ) -} + ) + } +) + +export const CloudInstance = /* @__PURE__ */ React.forwardRef( + ( + { + opacity = 1, + speed = 0, + bounds = [5, 1, 1], + segments = 20, + color = '#ffffff', + fade = 10, + volume = 6, + smallestVolume = 0.25, + distribute = null, + growth = 4, + concentrate = 'inside', + seed = Math.random(), + ...props + }, + fref + ) => { + function random() { + const x = Math.sin(seed++) * 10000 + return x - Math.floor(x) + } + + const parent = React.useContext(context) + const ref = React.useRef(null!) + const [uuid] = React.useState(() => v4()) + const clouds: CloudState[] = React.useMemo(() => { + return [...new Array(segments)].map( + (_, index) => + ({ + segments, + bounds: new Vector3(1, 1, 1), + position: new Vector3(), + uuid, + index, + ref, + dist: 0, + matrix: new Matrix4(), + color: new Color(), + rotation: index * (Math.PI / segments), + } as CloudState) + ) + }, [segments, uuid]) + + React.useLayoutEffect(() => { + clouds.forEach((cloud, index) => { + applyProps(cloud as any, { + volume, + color, + speed, + growth, + opacity, + fade, + bounds, + density: Math.max(0.5, random()), + rotationFactor: Math.max(0.2, 0.5 * random()) * speed, + }) + // Only distribute randomly if there are multiple segments + + const distributed = distribute?.(cloud, index) + + if (distributed || segments > 1) + cloud.position.copy(cloud.bounds).multiply( + distributed?.point ?? + ({ + x: random() * 2 - 1, + y: random() * 2 - 1, + z: random() * 2 - 1, + } as Vector3) + ) + const xDiff = Math.abs(cloud.position.x) + const yDiff = Math.abs(cloud.position.y) + const zDiff = Math.abs(cloud.position.z) + const max = Math.max(xDiff, yDiff, zDiff) + cloud.length = 1 + if (xDiff === max) cloud.length -= xDiff / cloud.bounds.x + if (yDiff === max) cloud.length -= yDiff / cloud.bounds.y + if (zDiff === max) cloud.length -= zDiff / cloud.bounds.z + cloud.volume = + (distributed?.volume !== undefined + ? distributed.volume + : Math.max( + Math.max(0, smallestVolume), + concentrate === 'random' ? random() : concentrate === 'inside' ? cloud.length : 1 - cloud.length + )) * volume + }) + }, [concentrate, bounds, fade, color, opacity, growth, volume, seed, segments, speed]) + + React.useLayoutEffect(() => { + const temp = clouds + parent.current = [...parent.current, ...temp] + return () => { + parent.current = parent.current.filter((item) => item.uuid !== uuid) + } + }, [clouds]) + + React.useImperativeHandle(fref, () => ref.current, []) + return + } +) + +export const Cloud = /* @__PURE__ */ React.forwardRef((props, fref) => { + const parent = React.useContext(context) + if (parent) return + else + return ( + + + + ) +}) diff --git a/src/core/ContactShadows.tsx b/src/core/ContactShadows.tsx index e546d01d3..c6d789879 100644 --- a/src/core/ContactShadows.tsx +++ b/src/core/ContactShadows.tsx @@ -25,7 +25,7 @@ export type ContactShadowsProps = { export const ContactShadows: ForwardRefComponent< Omit & ContactShadowsProps, THREE.Group -> = React.forwardRef( +> = /* @__PURE__ */ React.forwardRef( ( { scale = 10, diff --git a/src/core/CubeCamera.tsx b/src/core/CubeCamera.tsx index f870dbbe5..895fc631a 100644 --- a/src/core/CubeCamera.tsx +++ b/src/core/CubeCamera.tsx @@ -12,7 +12,7 @@ type Props = Omit & { } & CubeCameraOptions export function CubeCamera({ children, frames = Infinity, resolution, near, far, envMap, fog, ...props }: Props) { - const ref = React.useRef() + const ref = React.useRef(null!) const { fbo, camera, update } = useCubeCamera({ resolution, near, diff --git a/src/core/CubicBezierLine.tsx b/src/core/CubicBezierLine.tsx index 143eb34d3..db1da4c1a 100644 --- a/src/core/CubicBezierLine.tsx +++ b/src/core/CubicBezierLine.tsx @@ -12,7 +12,7 @@ type Props = Omit & { segments?: number } -export const CubicBezierLine: ForwardRefComponent = React.forwardRef( +export const CubicBezierLine: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( function CubicBezierLine({ start, end, midA, midB, segments = 20, ...rest }, ref) { const points = React.useMemo(() => { const startV = start instanceof Vector3 ? start : new Vector3(...start) diff --git a/src/core/CurveModifier.tsx b/src/core/CurveModifier.tsx index d9ca33c08..b6012113d 100644 --- a/src/core/CurveModifier.tsx +++ b/src/core/CurveModifier.tsx @@ -11,8 +11,8 @@ export interface CurveModifierProps { export type CurveModifierRef = Pick -export const CurveModifier: ForwardRefComponent = React.forwardRef( - ({ children, curve }: CurveModifierProps, ref) => { +export const CurveModifier: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef(({ children, curve }: CurveModifierProps, ref) => { const [scene] = React.useState(() => new THREE.Scene()) const [obj, set] = React.useState() const modifier = React.useRef() @@ -40,5 +40,4 @@ export const CurveModifier: ForwardRefComponent} ) - } -) + }) diff --git a/src/core/Decal.tsx b/src/core/Decal.tsx index bd6f60655..77ec4039a 100644 --- a/src/core/Decal.tsx +++ b/src/core/Decal.tsx @@ -31,81 +31,82 @@ function vecToArray(vec: number[] | FIBER.Vector3 | FIBER.Euler | number = [0, 0 } } -export const Decal: ForwardRefComponent = React.forwardRef( - function Decal( - { debug, depthTest = false, polygonOffsetFactor = -10, map, mesh, children, position, rotation, scale, ...props }, - forwardRef - ) { - const ref = React.useRef(null!) - React.useImperativeHandle(forwardRef, () => ref.current) - const helper = React.useRef(null!) +export const Decal: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + THREE.Mesh, + DecalProps +>(function Decal( + { debug, depthTest = false, polygonOffsetFactor = -10, map, mesh, children, position, rotation, scale, ...props }, + forwardRef +) { + const ref = React.useRef(null!) + React.useImperativeHandle(forwardRef, () => ref.current) + const helper = React.useRef(null!) - React.useLayoutEffect(() => { - const parent = mesh?.current || ref.current.parent - const target = ref.current - if (!(parent instanceof THREE.Mesh)) { - throw new Error('Decal must have a Mesh as parent or specify its "mesh" prop') - } + React.useLayoutEffect(() => { + const parent = mesh?.current || ref.current.parent + const target = ref.current + if (!(parent instanceof THREE.Mesh)) { + throw new Error('Decal must have a Mesh as parent or specify its "mesh" prop') + } - const state = { - position: new THREE.Vector3(), - rotation: new THREE.Euler(), - scale: new THREE.Vector3(1, 1, 1), - } + const state = { + position: new THREE.Vector3(), + rotation: new THREE.Euler(), + scale: new THREE.Vector3(1, 1, 1), + } - if (parent) { - applyProps(state as any, { position, scale }) + if (parent) { + applyProps(state as any, { position, scale }) - // Zero out the parents matrix world for this operation - const matrixWorld = parent.matrixWorld.clone() - parent.matrixWorld.identity() + // Zero out the parents matrix world for this operation + const matrixWorld = parent.matrixWorld.clone() + parent.matrixWorld.identity() - if (!rotation || typeof rotation === 'number') { - const o = new THREE.Object3D() + if (!rotation || typeof rotation === 'number') { + const o = new THREE.Object3D() - o.position.copy(state.position) - o.lookAt(parent.position) - if (typeof rotation === 'number') o.rotateZ(rotation) - applyProps(state as any, { rotation: o.rotation }) - } else { - applyProps(state as any, { rotation }) - } + o.position.copy(state.position) + o.lookAt(parent.position) + if (typeof rotation === 'number') o.rotateZ(rotation) + applyProps(state as any, { rotation: o.rotation }) + } else { + applyProps(state as any, { rotation }) + } - target.geometry = new DecalGeometry(parent, state.position, state.rotation, state.scale) - if (helper.current) { - applyProps(helper.current as any, state) - // Prevent the helpers from blocking rays - helper.current.traverse((child) => (child.raycast = () => null)) - } - // Reset parents matix-world - parent.matrixWorld = matrixWorld + target.geometry = new DecalGeometry(parent, state.position, state.rotation, state.scale) + if (helper.current) { + applyProps(helper.current as any, state) + // Prevent the helpers from blocking rays + helper.current.traverse((child) => (child.raycast = () => null)) + } + // Reset parents matix-world + parent.matrixWorld = matrixWorld - return () => { - target.geometry.dispose() - } + return () => { + target.geometry.dispose() } - }, [mesh, ...vecToArray(position), ...vecToArray(scale), ...vecToArray(rotation)]) + } + }, [mesh, ...vecToArray(position), ...vecToArray(scale), ...vecToArray(rotation)]) - // } - return ( - - {children} - {debug && ( - - - - - - )} - - ) - } -) + // } + return ( + + {children} + {debug && ( + + + + + + )} + + ) +}) diff --git a/src/core/Detailed.tsx b/src/core/Detailed.tsx index efb40f1e8..1cc78b146 100644 --- a/src/core/Detailed.tsx +++ b/src/core/Detailed.tsx @@ -10,7 +10,7 @@ type Props = JSX.IntrinsicElements['lOD'] & { distances: number[] } -export const Detailed: ForwardRefComponent = React.forwardRef( +export const Detailed: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ children, hysteresis = 0, distances, ...props }: Props, ref) => { const lodRef = React.useRef(null!) React.useLayoutEffect(() => { diff --git a/src/core/DeviceOrientationControls.tsx b/src/core/DeviceOrientationControls.tsx index 7f252591a..76ed16056 100644 --- a/src/core/DeviceOrientationControls.tsx +++ b/src/core/DeviceOrientationControls.tsx @@ -16,7 +16,7 @@ export type DeviceOrientationControlsProps = ReactThreeFiber.Object3DNode< export const DeviceOrientationControls: ForwardRefComponent< DeviceOrientationControlsProps, DeviceOrientationControlsImp -> = React.forwardRef( +> = /* @__PURE__ */ React.forwardRef( (props: DeviceOrientationControlsProps, ref) => { const { camera, onChange, makeDefault, ...rest } = props const defaultCamera = useThree((state) => state.camera) diff --git a/src/core/Edges.tsx b/src/core/Edges.tsx index f1983671a..7ffdc3cd1 100644 --- a/src/core/Edges.tsx +++ b/src/core/Edges.tsx @@ -8,7 +8,7 @@ type Props = JSX.IntrinsicElements['lineSegments'] & { color?: ReactThreeFiber.Color } -export const Edges: ForwardRefComponent = React.forwardRef( +export const Edges: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { userData, children, geometry, threshold = 15, color = 'black', ...props }: Props, fref: React.ForwardedRef diff --git a/src/core/Effects.tsx b/src/core/Effects.tsx index ea365452f..6da57bacd 100644 --- a/src/core/Effects.tsx +++ b/src/core/Effects.tsx @@ -37,7 +37,7 @@ export const isWebGL2Available = () => { } } -export const Effects: ForwardRefComponent = React.forwardRef( +export const Effects: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { children, diff --git a/src/core/Example.tsx b/src/core/Example.tsx index 3d7127170..d2bc14cd5 100644 --- a/src/core/Example.tsx +++ b/src/core/Example.tsx @@ -18,7 +18,7 @@ export type ExampleApi = { decr: (x?: number) => void } -export const Example = React.forwardRef( +export const Example = /* @__PURE__ */ React.forwardRef( ({ font, color = '#cbcbcb', bevelSize = 0.04, debug = false, children, ...props }, fref) => { const [counter, setCounter] = React.useState(0) diff --git a/src/core/FirstPersonControls.tsx b/src/core/FirstPersonControls.tsx index 482f2de88..d69c9ff0c 100644 --- a/src/core/FirstPersonControls.tsx +++ b/src/core/FirstPersonControls.tsx @@ -9,26 +9,28 @@ export type FirstPersonControlsProps = Object3DNode = - React.forwardRef(({ domElement, makeDefault, ...props }, ref) => { - const camera = useThree((state) => state.camera) - const gl = useThree((state) => state.gl) - const events = useThree((state) => state.events) as EventManager - const get = useThree((state) => state.get) - const set = useThree((state) => state.set) - const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement - const [controls] = React.useState(() => new FirstPersonControlImpl(camera, explDomElement)) + /* @__PURE__ */ React.forwardRef( + ({ domElement, makeDefault, ...props }, ref) => { + const camera = useThree((state) => state.camera) + const gl = useThree((state) => state.gl) + const events = useThree((state) => state.events) as EventManager + const get = useThree((state) => state.get) + const set = useThree((state) => state.set) + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + const [controls] = React.useState(() => new FirstPersonControlImpl(camera, explDomElement)) - React.useEffect(() => { - if (makeDefault) { - const old = get().controls - set({ controls }) - return () => set({ controls: old }) - } - }, [makeDefault, controls]) + React.useEffect(() => { + if (makeDefault) { + const old = get().controls + set({ controls }) + return () => set({ controls: old }) + } + }, [makeDefault, controls]) - useFrame((_, delta) => { - controls.update(delta) - }, -1) + useFrame((_, delta) => { + controls.update(delta) + }, -1) - return controls ? : null - }) + return controls ? : null + } + ) diff --git a/src/core/Fisheye.tsx b/src/core/Fisheye.tsx new file mode 100644 index 000000000..dfa3c2618 --- /dev/null +++ b/src/core/Fisheye.tsx @@ -0,0 +1,110 @@ +/** + * Event compute by Garrett Johnson https://twitter.com/garrettkjohnson + * https://discourse.threejs.org/t/how-to-use-three-raycaster-with-a-sphere-projected-envmap/56803/10 + */ + +import * as THREE from 'three' +import * as React from 'react' +import { useFrame, useThree } from '@react-three/fiber' +import { RenderCubeTexture, RenderCubeTextureApi } from './RenderCubeTexture' + +export type FisheyeProps = JSX.IntrinsicElements['mesh'] & { + /** Zoom factor, 0..1, 0 */ + zoom?: number + /** Number of segments, 64 */ + segments?: number + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be projected into the fisheye */ + children: React.ReactNode + /** Optional render priority, defaults to 1 */ + renderPriority?: number +} + +export function Fisheye({ + renderPriority = 1, + zoom = 0, + segments = 64, + children, + resolution = 896, + ...props +}: FisheyeProps) { + const sphere = React.useRef(null!) + const cubeApi = React.useRef(null!) + + // This isn't more than a simple sphere and a fixed orthographc camera + // pointing at it. A virtual scene is portalled into the environment map + // of its material. The cube-camera filming that scene is being synced to + // the portals default camera with the component. + + const { width, height } = useThree((state) => state.size) + const [orthoC] = React.useState(() => new THREE.OrthographicCamera()) + + React.useLayoutEffect(() => { + orthoC.position.set(0, 0, 100) + orthoC.zoom = 100 + orthoC.left = width / -2 + orthoC.right = width / 2 + orthoC.top = height / 2 + orthoC.bottom = height / -2 + orthoC.updateProjectionMatrix() + }, [width, height]) + + const radius = (Math.sqrt(width * width + height * height) / 100) * (0.5 + zoom / 2) + const normal = new THREE.Vector3() + const sph = new THREE.Sphere(new THREE.Vector3(), radius) + const normalMatrix = new THREE.Matrix3() + + const compute = React.useCallback((event, state, prev) => { + // Raycast from the render camera to the sphere and get the surface normal + // of the point hit in world space of the sphere scene + // We have to set the raycaster using the orthocam and pointer + // to perform sphere interscetions. + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) + state.raycaster.setFromCamera(state.pointer, orthoC) + if (!state.raycaster.ray.intersectSphere(sph, normal)) return + else normal.normalize() + // Get the matrix for transforming normals into world space + normalMatrix.getNormalMatrix(cubeApi.current.camera.matrixWorld) + // Get the ray + cubeApi.current.camera.getWorldPosition(state.raycaster.ray.origin) + state.raycaster.ray.direction.set(0, 0, 1).reflect(normal) + state.raycaster.ray.direction.x *= -1 // flip across X to accommodate the "flip" of the env map + state.raycaster.ray.direction.applyNormalMatrix(normalMatrix).multiplyScalar(-1) + return undefined + }, []) + + useFrame((state) => { + // Take over rendering + if (renderPriority) state.gl.render(sphere.current, orthoC) + }, renderPriority) + + return ( + <> + + + + + {children} + + + + + + ) +} + +function UpdateCubeCamera({ api }: { api: React.MutableRefObject }) { + const t = new THREE.Vector3() + const r = new THREE.Quaternion() + const s = new THREE.Vector3() + const e = new THREE.Euler(0, Math.PI, 0) + useFrame((state) => { + // Read out the cameras whereabouts, state.camera is the one *within* the portal + state.camera.matrixWorld.decompose(t, r, s) + // Apply its position and rotation, flip the Y axis + api.current.camera.position.copy(t) + api.current.camera.quaternion.setFromEuler(e).premultiply(r) + }) + return null +} diff --git a/src/core/Float.tsx b/src/core/Float.tsx index fe8c3ad87..311df711f 100644 --- a/src/core/Float.tsx +++ b/src/core/Float.tsx @@ -13,7 +13,10 @@ export type FloatProps = JSX.IntrinsicElements['group'] & { floatingRange?: [number?, number?] } -export const Float: ForwardRefComponent = React.forwardRef( +export const Float: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + THREE.Group, + FloatProps +>( ( { children, diff --git a/src/core/FlyControls.tsx b/src/core/FlyControls.tsx index 46d5ff5d1..081e4378c 100644 --- a/src/core/FlyControls.tsx +++ b/src/core/FlyControls.tsx @@ -10,7 +10,7 @@ export type FlyControlsProps = ReactThreeFiber.Object3DNode = React.forwardRef< +export const FlyControls: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< FlyControlsImpl, FlyControlsProps >(({ domElement, ...props }, fref) => { diff --git a/src/core/GizmoHelper.tsx b/src/core/GizmoHelper.tsx index fe7ff5f3b..9873a8451 100644 --- a/src/core/GizmoHelper.tsx +++ b/src/core/GizmoHelper.tsx @@ -9,18 +9,18 @@ type GizmoHelperContext = { tweenCamera: (direction: Vector3) => void } -const Context = React.createContext({} as GizmoHelperContext) +const Context = /* @__PURE__ */ React.createContext({} as GizmoHelperContext) export const useGizmoContext = () => { return React.useContext(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 dummy = /* @__PURE__ */ new Object3D() +const matrix = /* @__PURE__ */ new Matrix4() +const [q1, q2] = [/* @__PURE__ */ new Quaternion(), /* @__PURE__ */ new Quaternion()] +const target = /* @__PURE__ */ new Vector3() +const targetPosition = /* @__PURE__ */ new Vector3() type ControlsProto = { update(): void; target: THREE.Vector3 } @@ -61,7 +61,7 @@ export const GizmoHelper = ({ // @ts-ignore const defaultControls = useThree((state) => state.controls) as ControlsProto const invalidate = useThree((state) => state.invalidate) - const gizmoRef = React.useRef() + const gizmoRef = React.useRef(null!) const virtualCam = React.useRef(null!) const animating = React.useRef(false) diff --git a/src/core/GizmoViewcube.tsx b/src/core/GizmoViewcube.tsx index fb46a4b85..44fe74f60 100644 --- a/src/core/GizmoViewcube.tsx +++ b/src/core/GizmoViewcube.tsx @@ -21,7 +21,7 @@ const colors = { bg: '#f0f0f0', hover: '#999', text: 'black', stroke: 'black' } const defaultFaces = ['Right', 'Left', 'Top', 'Bottom', 'Front', 'Back'] const makePositionVector = (xyz: number[]) => new Vector3(...xyz).multiplyScalar(0.38) -const corners: Vector3[] = [ +const corners: Vector3[] = /* @__PURE__ */ [ [1, 1, 1], [1, 1, -1], [1, -1, 1], @@ -34,7 +34,7 @@ const corners: Vector3[] = [ const cornerDimensions: XYZ = [0.25, 0.25, 0.25] -const edges: Vector3[] = [ +const edges: Vector3[] = /* @__PURE__ */ [ [1, 1, 0], [1, 0, 1], [1, 0, -1], @@ -49,7 +49,7 @@ const edges: Vector3[] = [ [-1, -1, 0], ].map(makePositionVector) -const edgeDimensions = edges.map( +const edgeDimensions = /* @__PURE__ */ edges.map( (edge) => edge.toArray().map((axis: number): number => (axis == 0 ? 0.5 : 0.25)) as XYZ ) diff --git a/src/core/Gltf.tsx b/src/core/Gltf.tsx index ce4b605a7..363d0d007 100644 --- a/src/core/Gltf.tsx +++ b/src/core/Gltf.tsx @@ -9,7 +9,7 @@ type GltfProps = Omit & src: string } -export const Gltf: ForwardRefComponent = React.forwardRef( +export const Gltf: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ src, ...props }: GltfProps, ref: React.Ref) => { const { scene } = useGLTF(src) return diff --git a/src/core/Grid.tsx b/src/core/Grid.tsx index 4b3df2a8e..3e82a6344 100644 --- a/src/core/Grid.tsx +++ b/src/core/Grid.tsx @@ -10,6 +10,7 @@ import mergeRefs from 'react-merge-refs' import { extend, useFrame } from '@react-three/fiber' import { shaderMaterial } from './shaderMaterial' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' export type GridMaterialType = { /** Cell size, default: 0.5 */ @@ -49,7 +50,7 @@ declare global { } } -const GridMaterial = shaderMaterial( +const GridMaterial = /* @__PURE__ */ shaderMaterial( { cellSize: 0.5, sectionSize: 1, @@ -57,12 +58,12 @@ const GridMaterial = shaderMaterial( fadeStrength: 1, cellThickness: 0.5, sectionThickness: 1, - cellColor: new THREE.Color(), - sectionColor: new THREE.Color(), + cellColor: /* @__PURE__ */ new THREE.Color(), + sectionColor: /* @__PURE__ */ new THREE.Color(), infiniteGrid: false, followCamera: false, - worldCamProjPosition: new THREE.Vector3(), - worldPlanePosition: new THREE.Vector3(), + worldCamProjPosition: /* @__PURE__ */ new THREE.Vector3(), + worldPlanePosition: /* @__PURE__ */ new THREE.Vector3(), }, /* glsl */ ` varying vec3 localPosition; @@ -121,13 +122,13 @@ const GridMaterial = shaderMaterial( if (gl_FragColor.a <= 0.0) discard; #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> } ` ) export const Grid: ForwardRefComponent & GridProps, THREE.Mesh> = - React.forwardRef( + /* @__PURE__ */ React.forwardRef( ( { args, diff --git a/src/core/Image.tsx b/src/core/Image.tsx index 590cfc8c9..71c395a5d 100644 --- a/src/core/Image.tsx +++ b/src/core/Image.tsx @@ -1,24 +1,29 @@ import * as React from 'react' import * as THREE from 'three' -import { Color, extend } from '@react-three/fiber' +import { Color, extend, useThree } from '@react-three/fiber' import { shaderMaterial } from './shaderMaterial' import { useTexture } from './useTexture' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' export type ImageProps = Omit & { segments?: number scale?: number | [number, number] color?: Color zoom?: number + radius?: number grayscale?: number toneMapped?: boolean transparent?: boolean opacity?: number + side?: THREE.Side } & ({ texture: THREE.Texture; url?: never } | { texture?: never; url: string }) // {texture: THREE.Texture} XOR {url: string} type ImageMaterialType = JSX.IntrinsicElements['shaderMaterial'] & { scale?: number[] imageBounds?: number[] + radius?: number + resolution?: number color?: Color map: THREE.Texture zoom?: number @@ -33,22 +38,37 @@ declare global { } } -const ImageMaterialImpl = shaderMaterial( - { color: new THREE.Color('white'), scale: [1, 1], imageBounds: [1, 1], map: null, zoom: 1, grayscale: 0, opacity: 1 }, +const ImageMaterialImpl = /* @__PURE__ */ shaderMaterial( + { + color: /* @__PURE__ */ new THREE.Color('white'), + scale: /* @__PURE__ */ new THREE.Vector2(1, 1), + imageBounds: /* @__PURE__ */ new THREE.Vector2(1, 1), + resolution: 1024, + map: null, + zoom: 1, + radius: 0, + grayscale: 0, + opacity: 1, + }, /* glsl */ ` varying vec2 vUv; + varying vec2 vPos; void main() { gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); vUv = uv; + vPos = position.xy; } `, /* glsl */ ` // mostly from https://gist.github.com/statico/df64c5d167362ecf7b34fca0b1459a44 varying vec2 vUv; + varying vec2 vPos; uniform vec2 scale; uniform vec2 imageBounds; + uniform float resolution; uniform vec3 color; uniform sampler2D map; + uniform float radius; uniform float zoom; uniform float grayscale; uniform float opacity; @@ -59,6 +79,14 @@ const ImageMaterialImpl = shaderMaterial( vec2 aspect(vec2 size) { return size / min(size.x, size.y); } + + const float PI = 3.14159265; + + // from https://iquilezles.org/articles/distfunctions + float udRoundBox( vec2 p, vec2 b, float r ) { + return length(max(abs(p)-b+r,0.0))-r; + } + void main() { vec2 s = aspect(scale); vec2 i = aspect(imageBounds); @@ -68,15 +96,20 @@ const ImageMaterialImpl = shaderMaterial( vec2 offset = (rs < ri ? vec2((new.x - s.x) / 2.0, 0.0) : vec2(0.0, (new.y - s.y) / 2.0)) / new; vec2 uv = vUv * s / new + offset; vec2 zUv = (uv - vec2(0.5, 0.5)) / zoom + vec2(0.5, 0.5); - gl_FragColor = toGrayscale(texture2D(map, zUv) * vec4(color, opacity), grayscale); + + vec2 res = vec2(scale * resolution); + vec2 halfRes = 0.5 * res; + float b = udRoundBox(vUv.xy * res - halfRes, halfRes, resolution * radius); + vec3 a = mix(vec3(1.0,0.0,0.0), vec3(0.0,0.0,0.0), smoothstep(0.0, 1.0, b)); + gl_FragColor = toGrayscale(texture2D(map, zUv) * vec4(color, opacity * a), grayscale); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> } ` ) -const ImageBase: ForwardRefComponent, THREE.Mesh> = React.forwardRef( +const ImageBase: ForwardRefComponent, THREE.Mesh> = /* @__PURE__ */ React.forwardRef( ( { children, @@ -86,16 +119,35 @@ const ImageBase: ForwardRefComponent, THREE.Mesh> = Reac zoom = 1, grayscale = 0, opacity = 1, + radius = 0, texture, toneMapped, transparent, + side, ...props }: Omit, - ref: React.ForwardedRef + fref: React.ForwardedRef ) => { extend({ ImageMaterial: ImageMaterialImpl }) + const ref = React.useRef(null!) + const size = useThree((state) => state.size) const planeBounds = Array.isArray(scale) ? [scale[0], scale[1]] : [scale, scale] const imageBounds = [texture!.image.width, texture!.image.height] + const resolution = Math.max(size.width, size.height) + React.useImperativeHandle(fref, () => ref.current, []) + React.useLayoutEffect(() => { + // Support arbitrary plane geometries (for instance with rounded corners) + // @ts-ignore + if (ref.current.geometry.parameters) { + // @ts-ignore + ref.current.material.scale.set( + // @ts-ignore + planeBounds[0] * ref.current.geometry.parameters.width, + // @ts-ignore + planeBounds[1] * ref.current.geometry.parameters.height + ) + } + }, []) return ( @@ -107,8 +159,12 @@ const ImageBase: ForwardRefComponent, THREE.Mesh> = Reac opacity={opacity} scale={planeBounds} imageBounds={imageBounds} + resolution={resolution} + radius={radius} toneMapped={toneMapped} transparent={transparent} + side={side} + key={ImageMaterialImpl.key} /> {children} @@ -116,23 +172,24 @@ const ImageBase: ForwardRefComponent, THREE.Mesh> = Reac } ) -const ImageWithUrl: ForwardRefComponent = React.forwardRef( +const ImageWithUrl: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ url, ...props }: ImageProps, ref: React.ForwardedRef) => { const texture = useTexture(url!) return } ) -const ImageWithTexture: ForwardRefComponent = React.forwardRef( +const ImageWithTexture: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ url: _url, ...props }: ImageProps, ref: React.ForwardedRef) => { return } ) -export const Image: ForwardRefComponent = React.forwardRef( - (props, ref) => { - if (props.url) return - else if (props.texture) return - else throw new Error(' requires a url or texture') - } -) +export const Image: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + THREE.Mesh, + ImageProps +>((props, ref) => { + if (props.url) return + else if (props.texture) return + else throw new Error(' requires a url or texture') +}) diff --git a/src/core/Instances.tsx b/src/core/Instances.tsx index 9515b2046..9a7817cbc 100644 --- a/src/core/Instances.tsx +++ b/src/core/Instances.tsx @@ -4,6 +4,7 @@ import { ReactThreeFiber, extend, useFrame } from '@react-three/fiber' import mergeRefs from 'react-merge-refs' import Composer from 'react-composer' import { ForwardRefComponent } from '../helpers/ts-utils' +import { setUpdateRange } from '../helpers/deprecated' declare global { namespace JSX { @@ -33,10 +34,18 @@ type InstancedMesh = Omit() +function isFunctionChild( + value: any +): value is ( + props: React.ForwardRefExoticComponent & React.RefAttributes> +) => React.ReactNode { + return typeof value === 'function' +} + +const _instanceLocalMatrix = /* @__PURE__ */ new THREE.Matrix4() +const _instanceWorldMatrix = /* @__PURE__ */ new THREE.Matrix4() +const _instanceIntersects: THREE.Intersection[] = [] +const _mesh = /* @__PURE__ */ new THREE.Mesh() class PositionMesh extends THREE.Group { color: THREE.Color @@ -84,15 +93,15 @@ class PositionMesh extends THREE.Group { } } -const globalContext = /*@__PURE__*/ React.createContext(null!) -const parentMatrix = /*@__PURE__*/ new THREE.Matrix4() -const instanceMatrix = /*@__PURE__*/ new THREE.Matrix4() -const tempMatrix = /*@__PURE__*/ new THREE.Matrix4() -const translation = /*@__PURE__*/ new THREE.Vector3() -const rotation = /*@__PURE__*/ new THREE.Quaternion() -const scale = /*@__PURE__*/ new THREE.Vector3() +const globalContext = /* @__PURE__ */ React.createContext(null!) +const parentMatrix = /* @__PURE__ */ new THREE.Matrix4() +const instanceMatrix = /* @__PURE__ */ new THREE.Matrix4() +const tempMatrix = /* @__PURE__ */ new THREE.Matrix4() +const translation = /* @__PURE__ */ new THREE.Vector3() +const rotation = /* @__PURE__ */ new THREE.Quaternion() +const scale = /* @__PURE__ */ new THREE.Vector3() -export const Instance = React.forwardRef(({ context, children, ...props }: InstanceProps, ref) => { +export const Instance = /* @__PURE__ */ React.forwardRef(({ context, children, ...props }: InstanceProps, ref) => { React.useMemo(() => extend({ PositionMesh }), []) const group = React.useRef() const { subscribe, getParent } = React.useContext(context || globalContext) @@ -104,7 +113,7 @@ export const Instance = React.forwardRef(({ context, children, ...props }: Insta ) }) -export const Instances: ForwardRefComponent = React.forwardRef< +export const Instances: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< InstancedMesh, InstancesProps >(({ children, range, limit = 1000, frames = Infinity, ...props }, ref) => { @@ -129,18 +138,18 @@ export const Instances: ForwardRefComponent = Rea parentRef.current.instanceMatrix.needsUpdate = true }) + let iterations = 0 let count = 0 - let updateRange = 0 useFrame(() => { - if (frames === Infinity || count < frames) { + if (frames === Infinity || iterations < frames) { parentRef.current.updateMatrix() parentRef.current.updateMatrixWorld() parentMatrix.copy(parentRef.current.matrixWorld).invert() - updateRange = Math.min(limit, range !== undefined ? range : limit, instances.length) - parentRef.current.count = updateRange - parentRef.current.instanceMatrix.updateRange.count = updateRange * 16 - parentRef.current.instanceColor.updateRange.count = updateRange * 3 + count = Math.min(limit, range !== undefined ? range : limit, instances.length) + parentRef.current.count = count + setUpdateRange(parentRef.current.instanceMatrix, { offset: 0, count: count * 16 }) + setUpdateRange(parentRef.current.instanceColor, { offset: 0, count: count * 3 }) for (let i = 0; i < instances.length; i++) { const instance = instances[i].current @@ -153,7 +162,7 @@ export const Instances: ForwardRefComponent = Rea instance.color.toArray(colors, i * 3) parentRef.current.instanceColor.needsUpdate = true } - count++ + iterations++ } }) @@ -191,7 +200,7 @@ export const Instances: ForwardRefComponent = Rea itemSize={3} usage={THREE.DynamicDrawUsage} /> - {typeof children === 'function' ? ( + {isFunctionChild(children) ? ( {children(instance)} ) : ( {children} @@ -205,30 +214,29 @@ export interface MergedProps extends InstancesProps { children: React.ReactNode } -export const Merged: ForwardRefComponent = React.forwardRef(function Merged( - { meshes, children, ...props }, - ref -) { - const isArray = Array.isArray(meshes) - // Filter out meshes from collections, which may contain non-meshes - if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key] - return ( - - ( - - ))} - > - {(args) => - isArray - ? children(...args) - : children( - Object.keys(meshes) - .filter((key) => meshes[key].isMesh) - .reduce((acc, key, i) => ({ ...acc, [key]: args[i] }), {}) - ) - } - - - ) -}) +export const Merged: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( + function Merged({ meshes, children, ...props }, ref) { + const isArray = Array.isArray(meshes) + // Filter out meshes from collections, which may contain non-meshes + if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key] + return ( + + ( + + ))} + > + {(args) => + isArray + ? children(...args) + : children( + Object.keys(meshes) + .filter((key) => meshes[key].isMesh) + .reduce((acc, key, i) => ({ ...acc, [key]: args[i] }), {}) + ) + } + + + ) + } +) diff --git a/src/core/Lightformer.tsx b/src/core/Lightformer.tsx index eb61e85ad..178d51986 100644 --- a/src/core/Lightformer.tsx +++ b/src/core/Lightformer.tsx @@ -15,7 +15,7 @@ export type LightProps = JSX.IntrinsicElements['mesh'] & { target?: [number, number, number] | THREE.Vector3 } -export const Lightformer: ForwardRefComponent = React.forwardRef( +export const Lightformer: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { args, diff --git a/src/core/Line.tsx b/src/core/Line.tsx index 99da1ee65..965c69dbb 100644 --- a/src/core/Line.tsx +++ b/src/core/Line.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Vector2, Vector3, Color, ColorRepresentation } from 'three' +import { Vector2, Vector3, Vector4, Color, ColorRepresentation } from 'three' import { ReactThreeFiber, useThree } from '@react-three/fiber' import { LineGeometry, @@ -13,7 +13,7 @@ import { ForwardRefComponent } from '../helpers/ts-utils' export type LineProps = { points: Array - vertexColors?: Array + vertexColors?: Array lineWidth?: number segments?: boolean } & Omit & @@ -22,18 +22,19 @@ export type LineProps = { color?: ColorRepresentation } -export const Line: ForwardRefComponent = React.forwardRef< +export const Line: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< Line2 | LineSegments2, LineProps >(function Line({ points, color = 'black', vertexColors, linewidth, lineWidth, segments, dashed, ...rest }, ref) { const size = useThree((state) => state.size) const line2 = React.useMemo(() => (segments ? new LineSegments2() : new Line2()), [segments]) const [lineMaterial] = React.useState(() => new LineMaterial()) + const itemSize = (vertexColors?.[0] as number[] | undefined)?.length === 4 ? 4 : 3 const lineGeom = React.useMemo(() => { const geom = segments ? new LineSegmentsGeometry() : new LineGeometry() const pValues = points.map((p) => { const isArray = Array.isArray(p) - return p instanceof Vector3 + return p instanceof Vector3 || p instanceof Vector4 ? [p.x, p.y, p.z] : p instanceof Vector2 ? [p.x, p.y, 0] @@ -48,11 +49,11 @@ export const Line: ForwardRefComponent = React if (vertexColors) { const cValues = vertexColors.map((c) => (c instanceof Color ? c.toArray() : c)) - geom.setColors(cValues.flat()) + geom.setColors(cValues.flat(), itemSize) } return geom - }, [points, segments, vertexColors]) + }, [points, segments, vertexColors, itemSize]) React.useLayoutEffect(() => { line2.computeLineDistances() @@ -83,6 +84,7 @@ export const Line: ForwardRefComponent = React resolution={[size.width, size.height]} linewidth={linewidth ?? lineWidth} dashed={dashed} + transparent={itemSize === 4} {...rest} /> diff --git a/src/core/MapControls.tsx b/src/core/MapControls.tsx index 00ffc7ba3..83f4f8c1e 100644 --- a/src/core/MapControls.tsx +++ b/src/core/MapControls.tsx @@ -17,7 +17,7 @@ export type MapControlsProps = ReactThreeFiber.Overwrite< } > -export const MapControls: ForwardRefComponent = React.forwardRef< +export const MapControls: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< MapControlsImpl, MapControlsProps >((props = { enableDamping: true }, ref) => { diff --git a/src/core/MarchingCubes.tsx b/src/core/MarchingCubes.tsx index 6fe29ea27..27f661d38 100644 --- a/src/core/MarchingCubes.tsx +++ b/src/core/MarchingCubes.tsx @@ -10,7 +10,7 @@ type Api = { getParent: () => React.MutableRefObject } -const globalContext = React.createContext(null!) +const globalContext = /* @__PURE__ */ React.createContext(null!) export type MarchingCubesProps = { resolution?: number @@ -19,39 +19,41 @@ export type MarchingCubesProps = { enableColors?: boolean } & JSX.IntrinsicElements['group'] -export const MarchingCubes: ForwardRefComponent = React.forwardRef( - ( - { - resolution = 28, - maxPolyCount = 10000, - enableUvs = false, - enableColors = false, - children, - ...props - }: MarchingCubesProps, - ref - ) => { - const marchingCubesRef = React.useRef(null!) - const marchingCubes = React.useMemo( - () => new MarchingCubesImpl(resolution, null as unknown as THREE.Material, enableUvs, enableColors, maxPolyCount), - [resolution, maxPolyCount, enableUvs, enableColors] - ) - const api = React.useMemo(() => ({ getParent: () => marchingCubesRef }), []) +export const MarchingCubes: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + resolution = 28, + maxPolyCount = 10000, + enableUvs = false, + enableColors = false, + children, + ...props + }: MarchingCubesProps, + ref + ) => { + const marchingCubesRef = React.useRef(null!) + const marchingCubes = React.useMemo( + () => + new MarchingCubesImpl(resolution, null as unknown as THREE.Material, enableUvs, enableColors, maxPolyCount), + [resolution, maxPolyCount, enableUvs, enableColors] + ) + const api = React.useMemo(() => ({ getParent: () => marchingCubesRef }), []) - useFrame(() => { - marchingCubes.update() - marchingCubes.reset() - }, -1) // To make sure the reset runs before the balls or planes are added + useFrame(() => { + marchingCubes.update() + marchingCubes.reset() + }, -1) // To make sure the reset runs before the balls or planes are added - return ( - <> - - {children} - - - ) - } -) + return ( + <> + + {children} + + + ) + } + ) type MarchingCubeProps = { strength?: number @@ -59,7 +61,7 @@ type MarchingCubeProps = { color?: Color } & JSX.IntrinsicElements['group'] -export const MarchingCube: ForwardRefComponent = React.forwardRef( +export const MarchingCube: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ strength = 0.5, subtract = 12, color, ...props }: MarchingCubeProps, ref) => { const { getParent } = React.useContext(globalContext) const parentRef = React.useMemo(() => getParent(), [getParent]) @@ -80,7 +82,7 @@ type MarchingPlaneProps = { subtract?: number } & JSX.IntrinsicElements['group'] -export const MarchingPlane: ForwardRefComponent = React.forwardRef( +export const MarchingPlane: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ planeType: _planeType = 'x', strength = 0.5, subtract = 12, ...props }: MarchingPlaneProps, ref) => { const { getParent } = React.useContext(globalContext) const parentRef = React.useMemo(() => getParent(), [getParent]) diff --git a/src/core/Mask.tsx b/src/core/Mask.tsx index 2ef248e83..4e04769ba 100644 --- a/src/core/Mask.tsx +++ b/src/core/Mask.tsx @@ -11,7 +11,7 @@ type Props = Omit & { depthWrite?: boolean } -export const Mask: ForwardRefComponent = React.forwardRef( +export const Mask: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ id = 1, colorWrite = false, depthWrite = false, ...props }: Props, fref: React.ForwardedRef) => { const ref = React.useRef(null!) const spread = React.useMemo( diff --git a/src/core/MeshDiscardMaterial.tsx b/src/core/MeshDiscardMaterial.tsx index 083cc1aa3..fcd291771 100644 --- a/src/core/MeshDiscardMaterial.tsx +++ b/src/core/MeshDiscardMaterial.tsx @@ -12,7 +12,9 @@ declare global { } export const MeshDiscardMaterial: ForwardRefComponent = - React.forwardRef((props: JSX.IntrinsicElements['shaderMaterial'], fref: React.ForwardedRef) => { - extend({ DiscardMaterialImpl }) - return - }) + /* @__PURE__ */ React.forwardRef( + (props: JSX.IntrinsicElements['shaderMaterial'], fref: React.ForwardedRef) => { + extend({ DiscardMaterialImpl }) + return + } + ) diff --git a/src/core/MeshDistortMaterial.tsx b/src/core/MeshDistortMaterial.tsx index 7ec81a113..29f180c94 100644 --- a/src/core/MeshDistortMaterial.tsx +++ b/src/core/MeshDistortMaterial.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { MeshPhysicalMaterial, MeshPhysicalMaterialParameters, Shader } from 'three' +import { IUniform, MeshPhysicalMaterial, MeshPhysicalMaterialParameters } from 'three' import { useFrame } from '@react-three/fiber' // eslint-disable-next-line // @ts-ignore @@ -42,7 +42,8 @@ class DistortMaterialImpl extends MeshPhysicalMaterial { this._radius = { value: 1 } } - onBeforeCompile(shader: Shader) { + // FIXME Use `THREE.WebGLProgramParametersWithUniforms` type when able to target @types/three@0.160.0 + onBeforeCompile(shader: { vertexShader: string; uniforms: { [uniform: string]: IUniform } }) { shader.uniforms.time = this._time shader.uniforms.radius = this._radius shader.uniforms.distort = this._distort @@ -89,7 +90,7 @@ class DistortMaterialImpl extends MeshPhysicalMaterial { } } -export const MeshDistortMaterial: ForwardRefComponent = React.forwardRef( +export const MeshDistortMaterial: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ speed = 1, ...props }: Props, ref) => { const [material] = React.useState(() => new DistortMaterialImpl()) useFrame((state) => material && (material.time = state.clock.getElapsedTime() * speed)) diff --git a/src/core/MeshPortalMaterial.tsx b/src/core/MeshPortalMaterial.tsx index af76dce61..12f3806e5 100644 --- a/src/core/MeshPortalMaterial.tsx +++ b/src/core/MeshPortalMaterial.tsx @@ -11,15 +11,16 @@ import { useFBO } from './useFBO' import { RenderTexture } from './RenderTexture' import { shaderMaterial } from './shaderMaterial' import { FullScreenQuad } from 'three-stdlib' +import { version } from '../helpers/constants' -const PortalMaterialImpl = shaderMaterial( +const PortalMaterialImpl = /* @__PURE__ */ shaderMaterial( { blur: 0, map: null, sdf: null, blend: 0, size: 0, - resolution: new THREE.Vector2(), + resolution: /* @__PURE__ */ new THREE.Vector2(), }, `varying vec2 vUv; void main() { @@ -42,7 +43,7 @@ const PortalMaterialImpl = shaderMaterial( float alpha = 1.0 - smoothstep(0.0, 1.0, clamp(d/k + 1.0, 0.0, 1.0)); gl_FragColor = vec4(t.rgb, blur == 0.0 ? t.a : t.a * alpha); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) @@ -81,7 +82,7 @@ export type PortalProps = JSX.IntrinsicElements['shaderMaterial'] & { events?: boolean } -export const MeshPortalMaterial = React.forwardRef( +export const MeshPortalMaterial = /* @__PURE__ */ React.forwardRef( ( { children, @@ -180,11 +181,10 @@ export const MeshPortalMaterial = React.forwardRef( return ( @@ -261,9 +261,8 @@ function ManagePortalScene({ vec4 ta = texture2D(a, vUv); vec4 tb = texture2D(b, vUv); gl_FragColor = mix(tb, ta, blend); - #include <${ - parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment' - }> + #include + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }`, }) ) diff --git a/src/core/MeshReflectorMaterial.tsx b/src/core/MeshReflectorMaterial.tsx index 001710193..6bec45cfd 100644 --- a/src/core/MeshReflectorMaterial.tsx +++ b/src/core/MeshReflectorMaterial.tsx @@ -47,135 +47,158 @@ declare global { } } -extend({ MeshReflectorMaterialImpl }) - -export const MeshReflectorMaterial: ForwardRefComponent = React.forwardRef< - MeshReflectorMaterialImpl, - Props ->( - ( - { - mixBlur = 0, - mixStrength = 1, - resolution = 256, - blur = [0, 0], - minDepthThreshold = 0.9, - maxDepthThreshold = 1, - depthScale = 0, - depthToBlurRatioBias = 0.25, - mirror = 0, - distortion = 1, - mixContrast = 1, - distortionMap, - reflectorOffset = 0, - ...props - }, - ref - ) => { - const gl = useThree(({ gl }) => gl) - const camera = useThree(({ camera }) => camera) - const scene = useThree(({ scene }) => scene) - blur = Array.isArray(blur) ? blur : [blur, blur] - const hasBlur = blur[0] + blur[1] > 0 - const materialRef = React.useRef(null!) - const [reflectorPlane] = React.useState(() => new Plane()) - const [normal] = React.useState(() => new Vector3()) - const [reflectorWorldPosition] = React.useState(() => new Vector3()) - const [cameraWorldPosition] = React.useState(() => new Vector3()) - const [rotationMatrix] = React.useState(() => new Matrix4()) - const [lookAtPosition] = React.useState(() => new Vector3(0, 0, -1)) - const [clipPlane] = React.useState(() => new Vector4()) - const [view] = React.useState(() => new Vector3()) - const [target] = React.useState(() => new Vector3()) - const [q] = React.useState(() => new Vector4()) - const [textureMatrix] = React.useState(() => new Matrix4()) - const [virtualCamera] = React.useState(() => new PerspectiveCamera()) +export const MeshReflectorMaterial: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + mixBlur = 0, + mixStrength = 1, + resolution = 256, + blur = [0, 0], + minDepthThreshold = 0.9, + maxDepthThreshold = 1, + depthScale = 0, + depthToBlurRatioBias = 0.25, + mirror = 0, + distortion = 1, + mixContrast = 1, + distortionMap, + reflectorOffset = 0, + ...props + }, + ref + ) => { + extend({ MeshReflectorMaterialImpl }) + const gl = useThree(({ gl }) => gl) + const camera = useThree(({ camera }) => camera) + const scene = useThree(({ scene }) => scene) + blur = Array.isArray(blur) ? blur : [blur, blur] + const hasBlur = blur[0] + blur[1] > 0 + const materialRef = React.useRef(null!) + const [reflectorPlane] = React.useState(() => new Plane()) + const [normal] = React.useState(() => new Vector3()) + const [reflectorWorldPosition] = React.useState(() => new Vector3()) + const [cameraWorldPosition] = React.useState(() => new Vector3()) + const [rotationMatrix] = React.useState(() => new Matrix4()) + const [lookAtPosition] = React.useState(() => new Vector3(0, 0, -1)) + const [clipPlane] = React.useState(() => new Vector4()) + const [view] = React.useState(() => new Vector3()) + const [target] = React.useState(() => new Vector3()) + const [q] = React.useState(() => new Vector4()) + const [textureMatrix] = React.useState(() => new Matrix4()) + const [virtualCamera] = React.useState(() => new PerspectiveCamera()) - const beforeRender = React.useCallback(() => { - // TODO: As of R3f 7-8 this should be __r3f.parent - const parent = (materialRef.current as any).parent || (materialRef.current as any)?.__r3f.parent - if (!parent) return + const beforeRender = React.useCallback(() => { + // TODO: As of R3f 7-8 this should be __r3f.parent + const parent = (materialRef.current as any).parent || (materialRef.current as any)?.__r3f.parent + if (!parent) return - reflectorWorldPosition.setFromMatrixPosition(parent.matrixWorld) - cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld) - rotationMatrix.extractRotation(parent.matrixWorld) - normal.set(0, 0, 1) - normal.applyMatrix4(rotationMatrix) - reflectorWorldPosition.addScaledVector(normal, reflectorOffset) - view.subVectors(reflectorWorldPosition, cameraWorldPosition) - // Avoid rendering when reflector is facing away - if (view.dot(normal) > 0) return - view.reflect(normal).negate() - view.add(reflectorWorldPosition) - rotationMatrix.extractRotation(camera.matrixWorld) - lookAtPosition.set(0, 0, -1) - lookAtPosition.applyMatrix4(rotationMatrix) - lookAtPosition.add(cameraWorldPosition) - target.subVectors(reflectorWorldPosition, lookAtPosition) - target.reflect(normal).negate() - target.add(reflectorWorldPosition) - virtualCamera.position.copy(view) - virtualCamera.up.set(0, 1, 0) - virtualCamera.up.applyMatrix4(rotationMatrix) - virtualCamera.up.reflect(normal) - virtualCamera.lookAt(target) - virtualCamera.far = camera.far // Used in WebGLBackground - virtualCamera.updateMatrixWorld() - virtualCamera.projectionMatrix.copy(camera.projectionMatrix) - // Update the texture matrix - textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0) - textureMatrix.multiply(virtualCamera.projectionMatrix) - textureMatrix.multiply(virtualCamera.matrixWorldInverse) - textureMatrix.multiply(parent.matrixWorld) - // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html - // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf - reflectorPlane.setFromNormalAndCoplanarPoint(normal, reflectorWorldPosition) - reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse) - clipPlane.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant) - const projectionMatrix = virtualCamera.projectionMatrix - q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0] - q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5] - q.z = -1.0 - q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14] - // Calculate the scaled plane vector - clipPlane.multiplyScalar(2.0 / clipPlane.dot(q)) - // Replacing the third row of the projection matrix - projectionMatrix.elements[2] = clipPlane.x - projectionMatrix.elements[6] = clipPlane.y - projectionMatrix.elements[10] = clipPlane.z + 1.0 - projectionMatrix.elements[14] = clipPlane.w - }, [camera, reflectorOffset]) + reflectorWorldPosition.setFromMatrixPosition(parent.matrixWorld) + cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld) + rotationMatrix.extractRotation(parent.matrixWorld) + normal.set(0, 0, 1) + normal.applyMatrix4(rotationMatrix) + reflectorWorldPosition.addScaledVector(normal, reflectorOffset) + view.subVectors(reflectorWorldPosition, cameraWorldPosition) + // Avoid rendering when reflector is facing away + if (view.dot(normal) > 0) return + view.reflect(normal).negate() + view.add(reflectorWorldPosition) + rotationMatrix.extractRotation(camera.matrixWorld) + lookAtPosition.set(0, 0, -1) + lookAtPosition.applyMatrix4(rotationMatrix) + lookAtPosition.add(cameraWorldPosition) + target.subVectors(reflectorWorldPosition, lookAtPosition) + target.reflect(normal).negate() + target.add(reflectorWorldPosition) + virtualCamera.position.copy(view) + virtualCamera.up.set(0, 1, 0) + virtualCamera.up.applyMatrix4(rotationMatrix) + virtualCamera.up.reflect(normal) + virtualCamera.lookAt(target) + virtualCamera.far = camera.far // Used in WebGLBackground + virtualCamera.updateMatrixWorld() + virtualCamera.projectionMatrix.copy(camera.projectionMatrix) + // Update the texture matrix + textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0) + textureMatrix.multiply(virtualCamera.projectionMatrix) + textureMatrix.multiply(virtualCamera.matrixWorldInverse) + textureMatrix.multiply(parent.matrixWorld) + // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html + // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf + reflectorPlane.setFromNormalAndCoplanarPoint(normal, reflectorWorldPosition) + reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse) + clipPlane.set( + reflectorPlane.normal.x, + reflectorPlane.normal.y, + reflectorPlane.normal.z, + reflectorPlane.constant + ) + const projectionMatrix = virtualCamera.projectionMatrix + q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0] + q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5] + q.z = -1.0 + q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14] + // Calculate the scaled plane vector + clipPlane.multiplyScalar(2.0 / clipPlane.dot(q)) + // Replacing the third row of the projection matrix + projectionMatrix.elements[2] = clipPlane.x + projectionMatrix.elements[6] = clipPlane.y + projectionMatrix.elements[10] = clipPlane.z + 1.0 + projectionMatrix.elements[14] = clipPlane.w + }, [camera, reflectorOffset]) - const [fbo1, fbo2, blurpass, reflectorProps] = React.useMemo(() => { - const parameters = { - minFilter: LinearFilter, - magFilter: LinearFilter, - type: HalfFloatType, - } - const fbo1 = new WebGLRenderTarget(resolution, resolution, parameters) - fbo1.depthBuffer = true - fbo1.depthTexture = new DepthTexture(resolution, resolution) - fbo1.depthTexture.format = DepthFormat - fbo1.depthTexture.type = UnsignedShortType - const fbo2 = new WebGLRenderTarget(resolution, resolution, parameters) - const blurpass = new BlurPass({ + const [fbo1, fbo2, blurpass, reflectorProps] = React.useMemo(() => { + const parameters = { + minFilter: LinearFilter, + magFilter: LinearFilter, + type: HalfFloatType, + } + const fbo1 = new WebGLRenderTarget(resolution, resolution, parameters) + fbo1.depthBuffer = true + fbo1.depthTexture = new DepthTexture(resolution, resolution) + fbo1.depthTexture.format = DepthFormat + fbo1.depthTexture.type = UnsignedShortType + const fbo2 = new WebGLRenderTarget(resolution, resolution, parameters) + const blurpass = new BlurPass({ + gl, + resolution, + width: blur[0], + height: blur[1], + minDepthThreshold, + maxDepthThreshold, + depthScale, + depthToBlurRatioBias, + }) + const reflectorProps = { + mirror, + textureMatrix, + mixBlur, + tDiffuse: fbo1.texture, + tDepth: fbo1.depthTexture, + tDiffuseBlur: fbo2.texture, + hasBlur, + mixStrength, + minDepthThreshold, + maxDepthThreshold, + depthScale, + depthToBlurRatioBias, + distortion, + distortionMap, + mixContrast, + 'defines-USE_BLUR': hasBlur ? '' : undefined, + 'defines-USE_DEPTH': depthScale > 0 ? '' : undefined, + 'defines-USE_DISTORTION': distortionMap ? '' : undefined, + } + return [fbo1, fbo2, blurpass, reflectorProps] + }, [ gl, + blur, + textureMatrix, resolution, - width: blur[0], - height: blur[1], - minDepthThreshold, - maxDepthThreshold, - depthScale, - depthToBlurRatioBias, - }) - const reflectorProps = { mirror, - textureMatrix, - mixBlur, - tDiffuse: fbo1.texture, - tDepth: fbo1.depthTexture, - tDiffuseBlur: fbo2.texture, hasBlur, + mixBlur, mixStrength, minDepthThreshold, maxDepthThreshold, @@ -184,65 +207,44 @@ export const MeshReflectorMaterial: ForwardRefComponent 0 ? '' : undefined, - 'defines-USE_DISTORTION': distortionMap ? '' : undefined, - } - return [fbo1, fbo2, blurpass, reflectorProps] - }, [ - gl, - blur, - textureMatrix, - resolution, - mirror, - hasBlur, - mixBlur, - mixStrength, - minDepthThreshold, - maxDepthThreshold, - depthScale, - depthToBlurRatioBias, - distortion, - distortionMap, - mixContrast, - ]) + ]) - useFrame(() => { - // TODO: As of R3f 7-8 this should be __r3f.parent - const parent = (materialRef.current as any).parent || (materialRef.current as any)?.__r3f.parent - if (!parent) return + useFrame(() => { + // TODO: As of R3f 7-8 this should be __r3f.parent + const parent = (materialRef.current as any).parent || (materialRef.current as any)?.__r3f.parent + if (!parent) return - parent.visible = false - const currentXrEnabled = gl.xr.enabled - const currentShadowAutoUpdate = gl.shadowMap.autoUpdate - beforeRender() - gl.xr.enabled = false - gl.shadowMap.autoUpdate = false - gl.setRenderTarget(fbo1) - gl.state.buffers.depth.setMask(true) - if (!gl.autoClear) gl.clear() - gl.render(scene, virtualCamera) - if (hasBlur) blurpass.render(gl, fbo1, fbo2) - gl.xr.enabled = currentXrEnabled - gl.shadowMap.autoUpdate = currentShadowAutoUpdate - parent.visible = true - gl.setRenderTarget(null) - }) + parent.visible = false + const currentXrEnabled = gl.xr.enabled + const currentShadowAutoUpdate = gl.shadowMap.autoUpdate + beforeRender() + gl.xr.enabled = false + gl.shadowMap.autoUpdate = false + gl.setRenderTarget(fbo1) + gl.state.buffers.depth.setMask(true) + if (!gl.autoClear) gl.clear() + gl.render(scene, virtualCamera) + if (hasBlur) blurpass.render(gl, fbo1, fbo2) + gl.xr.enabled = currentXrEnabled + gl.shadowMap.autoUpdate = currentShadowAutoUpdate + parent.visible = true + gl.setRenderTarget(null) + }) - return ( - - ) - } -) + return ( + + ) + } + ) diff --git a/src/core/MeshTransmissionMaterial.tsx b/src/core/MeshTransmissionMaterial.tsx index e1702200e..971f43630 100644 --- a/src/core/MeshTransmissionMaterial.tsx +++ b/src/core/MeshTransmissionMaterial.tsx @@ -122,6 +122,10 @@ class MeshTransmissionMaterialImpl extends THREE.MeshPhysicalMaterial { ...this.uniforms, } + // Fix for r153-r156 anisotropy chunks + // https://github.com/mrdoob/three.js/pull/26716 + if ((this as any).anisotropy > 0) shader.defines.USE_ANISOTROPY = '' + // If the transmission sampler is active inject a flag if (transmissionSampler) shader.defines.USE_SAMPLER = '' // Otherwise we do use use .transmission and must therefore force USE_TRANSMISSION @@ -372,7 +376,7 @@ class MeshTransmissionMaterialImpl extends THREE.MeshPhysicalMaterial { export const MeshTransmissionMaterial: ForwardRefComponent< MeshTransmissionMaterialProps, JSX.IntrinsicElements['meshTransmissionMaterial'] -> = React.forwardRef( +> = /* @__PURE__ */ React.forwardRef( ( { buffer, @@ -454,7 +458,7 @@ export const MeshTransmissionMaterial: ForwardRefComponent< = React.forwardRef( +export const MeshWobbleMaterial: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ speed = 1, ...props }: Props, ref) => { const [material] = React.useState(() => new WobbleMaterialImpl()) useFrame((state) => material && (material.time = state.clock.getElapsedTime() * speed)) diff --git a/src/core/MotionPathControls.tsx b/src/core/MotionPathControls.tsx new file mode 100644 index 000000000..b1cb52349 --- /dev/null +++ b/src/core/MotionPathControls.tsx @@ -0,0 +1,184 @@ +/* eslint-disable prettier/prettier */ +import * as THREE from 'three' +import * as React from 'react' +import { useFrame, useThree } from '@react-three/fiber' +import { easing, misc } from 'maath' + +type MotionPathProps = JSX.IntrinsicElements['group'] & { + /** An optional array of THREE curves */ + curves?: THREE.Curve[] + /** Show debug helpers */ + debug?: boolean + /** The target object that is moved, default: null (the default camera) */ + object?: React.MutableRefObject + /** An object where the target looks towards, can also be a vector, default: null */ + focus?: [x: number, y: number, z: number] | React.MutableRefObject + /** Position between 0 (start) and end (1), if this is not set useMotion().current must be used, default: null */ + offset?: number + /** Optionally smooth the curve, default: false */ + smooth?: boolean | number + /** Damping tolerance, default: 0.00001 */ + eps?: number + /** Damping factor for movement along the curve, default: 0.1 */ + damping?: number + /** Damping factor for lookAt, default: 0.1 */ + focusDamping?: number + /** Damping maximum speed, default: Infinity */ + maxSpeed?: number +} + +type MotionState = { + /** The user-defined, mutable, current goal position along the curve, it may be >1 or <0 */ + current: number + /** The combined curve */ + path: THREE.CurvePath + /** The focus object */ + focus: React.MutableRefObject | [x: number, y: number, z: number] | undefined + /** The target object that is moved along the curve */ + object: React.MutableRefObject + /** The 0-1 normalised and damped current goal position along curve */ + offset: number + /** The current point on the curve */ + point: THREE.Vector3 + /** The current tangent on the curve */ + tangent: THREE.Vector3 + /** The next point on the curve */ + next: THREE.Vector3 +} + +const isObject3DRef = (ref: any): ref is React.MutableRefObject => + ref?.current instanceof THREE.Object3D + +const context = /* @__PURE__ */ React.createContext(null!) + +export function useMotion() { + return React.useContext(context) as MotionState +} + +function Debug({ points = 50 }: { points?: number }) { + const { path } = useMotion() + const [dots, setDots] = React.useState([]) + const [material] = React.useState(() => new THREE.MeshBasicMaterial({ color: 'black' })) + const [geometry] = React.useState(() => new THREE.SphereGeometry(0.025, 16, 16)) + const last = React.useRef[]>([]) + React.useEffect(() => { + if (path.curves !== last.current) { + setDots(path.getPoints(points)) + last.current = path.curves + } + }) + return ( + <> + {dots.map((item: { x: any; y: any; z: any }, index: any) => ( + + ))} + + ) +} + +export const MotionPathControls = /* @__PURE__ */ React.forwardRef( + ( + { + children, + curves = [], + object, + debug = false, + smooth = false, + focus, + offset = undefined, + eps = 0.00001, + damping = 0.1, + focusDamping = 0.1, + maxSpeed = Infinity, + ...props + }: MotionPathProps, + fref + ) => { + const { camera } = useThree() + const ref = React.useRef() + const [path] = React.useState(() => new THREE.CurvePath()) + + const pos = React.useRef(offset ?? 0) + const state = React.useMemo( + () => ({ + focus, + object: object?.current instanceof THREE.Object3D ? object : { current: camera }, + path, + current: pos.current, + offset: pos.current, + point: new THREE.Vector3(), + tangent: new THREE.Vector3(), + next: new THREE.Vector3(), + }), + [focus, object] + ) + + React.useLayoutEffect(() => { + path.curves = [] + const _curves = curves.length > 0 ? curves : ref.current?.__r3f.objects + for (var i = 0; i < _curves.length; i++) path.add(_curves[i]) + + //Smoothen curve + if (smooth) { + const points = path.getPoints(typeof smooth === 'number' ? smooth : 1) + const catmull = new THREE.CatmullRomCurve3(points) + path.curves = [catmull] + } + path.updateArcLengths() + }) + + React.useImperativeHandle(fref, () => ref.current, []) + + React.useLayoutEffect(() => { + // When offset changes, normalise pos to avoid overshoot spinning + pos.current = misc.repeat(pos.current, 1) + }, [offset]) + + let last = 0 + const [vec] = React.useState(() => new THREE.Vector3()) + + useFrame((_state, delta) => { + last = state.offset + easing.damp( + pos, + 'current', + offset !== undefined ? offset : state.current, + damping, + delta, + maxSpeed, + undefined, + eps + ) + state.offset = misc.repeat(pos.current, 1) + + if (path.getCurveLengths().length > 0) { + path.getPointAt(state.offset, state.point) + path.getTangentAt(state.offset, state.tangent).normalize() + path.getPointAt(misc.repeat(pos.current - (last - state.offset), 1), state.next) + const target = object?.current instanceof THREE.Object3D ? object.current : camera + target.position.copy(state.point) + //@ts-ignore + if (focus) { + easing.dampLookAt( + target, + isObject3DRef(focus) ? focus.current.getWorldPosition(vec) : focus, + focusDamping, + delta, + maxSpeed, + undefined, + eps + ) + } + } + }) + + return ( + + + {children} + {debug && } + + + ) + } +) diff --git a/src/core/OrbitControls.tsx b/src/core/OrbitControls.tsx index 82e5643e6..c680c5411 100644 --- a/src/core/OrbitControls.tsx +++ b/src/core/OrbitControls.tsx @@ -27,84 +27,82 @@ export type OrbitControlsProps = Omit< 'ref' > -export const OrbitControls: ForwardRefComponent = React.forwardRef< - OrbitControlsImpl, - OrbitControlsProps ->( - ( - { - makeDefault, - camera, - regress, - domElement, - enableDamping = true, - keyEvents = false, - onChange, - onStart, - onEnd, - ...restProps - }, - ref - ) => { - const invalidate = useThree((state) => state.invalidate) - const defaultCamera = useThree((state) => state.camera) - const gl = useThree((state) => state.gl) - const events = useThree((state) => state.events) as EventManager - const setEvents = useThree((state) => state.setEvents) - const set = useThree((state) => state.set) - const get = useThree((state) => state.get) - const performance = useThree((state) => state.performance) - const explCamera = (camera || defaultCamera) as THREE.OrthographicCamera | THREE.PerspectiveCamera - const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement - const controls = React.useMemo(() => new OrbitControlsImpl(explCamera), [explCamera]) +export const OrbitControls: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + makeDefault, + camera, + regress, + domElement, + enableDamping = true, + keyEvents = false, + onChange, + onStart, + onEnd, + ...restProps + }, + ref + ) => { + const invalidate = useThree((state) => state.invalidate) + const defaultCamera = useThree((state) => state.camera) + const gl = useThree((state) => state.gl) + const events = useThree((state) => state.events) as EventManager + const setEvents = useThree((state) => state.setEvents) + const set = useThree((state) => state.set) + const get = useThree((state) => state.get) + const performance = useThree((state) => state.performance) + const explCamera = (camera || defaultCamera) as THREE.OrthographicCamera | THREE.PerspectiveCamera + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + const controls = React.useMemo(() => new OrbitControlsImpl(explCamera), [explCamera]) - useFrame(() => { - if (controls.enabled) controls.update() - }, -1) + useFrame(() => { + if (controls.enabled) controls.update() + }, -1) - React.useEffect(() => { - if (keyEvents) { - controls.connect(keyEvents === true ? explDomElement : keyEvents) - } + React.useEffect(() => { + if (keyEvents) { + controls.connect(keyEvents === true ? explDomElement : keyEvents) + } - controls.connect(explDomElement) - return () => void controls.dispose() - }, [keyEvents, explDomElement, regress, controls, invalidate]) + controls.connect(explDomElement) + return () => void controls.dispose() + }, [keyEvents, explDomElement, regress, controls, invalidate]) - React.useEffect(() => { - const callback = (e: OrbitControlsChangeEvent) => { - invalidate() - if (regress) performance.regress() - if (onChange) onChange(e) - } + React.useEffect(() => { + const callback = (e: OrbitControlsChangeEvent) => { + invalidate() + if (regress) performance.regress() + if (onChange) onChange(e) + } - const onStartCb = (e: Event) => { - if (onStart) onStart(e) - } + const onStartCb = (e: Event) => { + if (onStart) onStart(e) + } - const onEndCb = (e: Event) => { - if (onEnd) onEnd(e) - } + const onEndCb = (e: Event) => { + if (onEnd) onEnd(e) + } - controls.addEventListener('change', callback) - controls.addEventListener('start', onStartCb) - controls.addEventListener('end', onEndCb) + controls.addEventListener('change', callback) + controls.addEventListener('start', onStartCb) + controls.addEventListener('end', onEndCb) - return () => { - controls.removeEventListener('start', onStartCb) - controls.removeEventListener('end', onEndCb) - controls.removeEventListener('change', callback) - } - }, [onChange, onStart, onEnd, controls, invalidate, setEvents]) + return () => { + controls.removeEventListener('start', onStartCb) + controls.removeEventListener('end', onEndCb) + controls.removeEventListener('change', callback) + } + }, [onChange, onStart, onEnd, controls, invalidate, setEvents]) - React.useEffect(() => { - if (makeDefault) { - const old = get().controls - set({ controls }) - return () => set({ controls: old }) - } - }, [makeDefault, controls]) + React.useEffect(() => { + if (makeDefault) { + const old = get().controls + set({ controls }) + return () => set({ controls: old }) + } + }, [makeDefault, controls]) - return - } -) + return + } + ) diff --git a/src/core/OrthographicCamera.tsx b/src/core/OrthographicCamera.tsx index 634b0dd08..1844d79be 100644 --- a/src/core/OrthographicCamera.tsx +++ b/src/core/OrthographicCamera.tsx @@ -23,7 +23,7 @@ type Props = Omit & { envMap?: THREE.Texture } -export const OrthographicCamera: ForwardRefComponent = React.forwardRef( +export const OrthographicCamera: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ envMap, resolution = 256, frames = Infinity, children, makeDefault, ...props }: Props, ref) => { const set = useThree(({ set }) => set) const camera = useThree(({ camera }) => camera) diff --git a/src/core/Outlines.tsx b/src/core/Outlines.tsx index 708e790c1..0541878e5 100644 --- a/src/core/Outlines.tsx +++ b/src/core/Outlines.tsx @@ -1,18 +1,27 @@ import * as THREE from 'three' import * as React from 'react' import { shaderMaterial } from './shaderMaterial' -import { extend, applyProps, dispose, ReactThreeFiber } from '@react-three/fiber' +import { extend, applyProps, ReactThreeFiber, useThree } from '@react-three/fiber' import { toCreasedNormals } from 'three-stdlib' +import { version } from '../helpers/constants' -const OutlinesMaterial = shaderMaterial( - { color: new THREE.Color('black'), opacity: 1, thickness: 0.05 }, +const OutlinesMaterial = /* @__PURE__ */ shaderMaterial( + { + screenspace: false, + color: /* @__PURE__ */ new THREE.Color('black'), + opacity: 1, + thickness: 0.05, + size: /* @__PURE__ */ new THREE.Vector2(), + }, `#include #include #include uniform float thickness; + uniform float screenspace; + uniform vec2 size; void main() { #if defined (USE_SKINNING) - #include + #include #include #include #include @@ -22,71 +31,142 @@ const OutlinesMaterial = shaderMaterial( #include #include #include - vec3 newPosition = transformed + normal * thickness; - gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); + vec4 tNormal = vec4(normal, 0.0); + vec4 tPosition = vec4(transformed, 1.0); + #ifdef USE_INSTANCING + tNormal = instanceMatrix * tNormal; + tPosition = instanceMatrix * tPosition; + #endif + if (screenspace == 0.0) { + vec3 newPosition = tPosition.xyz + tNormal.xyz * thickness; + gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); + } else { + vec4 clipPosition = projectionMatrix * modelViewMatrix * tPosition; + vec4 clipNormal = projectionMatrix * modelViewMatrix * tNormal; + vec2 offset = normalize(clipNormal.xy) * thickness / size * clipPosition.w * 2.0; + clipPosition.xy += offset; + gl_Position = clipPosition; + } }`, `uniform vec3 color; uniform float opacity; void main(){ gl_FragColor = vec4(color, opacity); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) type OutlinesProps = JSX.IntrinsicElements['group'] & { /** Outline color, default: black */ - color: ReactThreeFiber.Color + color?: ReactThreeFiber.Color + /** Line thickness is independent of zoom, default: false */ + screenspace?: boolean /** Outline opacity, default: 1 */ - opacity: number + opacity?: number /** Outline transparency, default: false */ - transparent: boolean + transparent?: boolean /** Outline thickness, default 0.05 */ - thickness: number + thickness?: number /** Geometry crease angle (0 === no crease), default: Math.PI */ - angle: number + angle?: number + toneMapped?: boolean + polygonOffset?: boolean + polygonOffsetFactor?: number + renderOrder?: number } export function Outlines({ color = 'black', opacity = 1, transparent = false, + screenspace = false, + toneMapped = true, + polygonOffset = false, + polygonOffsetFactor = 0, + renderOrder = 0, thickness = 0.05, angle = Math.PI, ...props -}) { - const ref = React.useRef(null!) +}: OutlinesProps) { + const ref = React.useRef() + const [material] = React.useState(() => new OutlinesMaterial({ side: THREE.BackSide })) + const { gl } = useThree() + const contextSize = gl.getDrawingBufferSize(new THREE.Vector2()) React.useMemo(() => extend({ OutlinesMaterial }), []) + + const oldAngle = React.useRef(0) + const oldGeometry = React.useRef() React.useLayoutEffect(() => { const group = ref.current - const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh + if (!group) return + + const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh & THREE.InstancedMesh if (parent && parent.geometry) { - let mesh - if (parent.skeleton) { - mesh = new THREE.SkinnedMesh() - mesh.material = new OutlinesMaterial({ side: THREE.BackSide }) - mesh.bind(parent.skeleton, parent.bindMatrix) - group.add(mesh) - } else { - mesh = new THREE.Mesh() - mesh.material = new OutlinesMaterial({ side: THREE.BackSide }) - group.add(mesh) - } - mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry - return () => { - dispose(mesh) - group.clear() + if (oldAngle.current !== angle || oldGeometry.current !== parent.geometry) { + oldAngle.current = angle + oldGeometry.current = parent.geometry + + // Remove old mesh + let mesh = group.children[0] as any + if (mesh) { + if (angle) mesh.geometry.dispose() + group.remove(mesh) + } + + if (parent.skeleton) { + mesh = new THREE.SkinnedMesh() + mesh.material = material + mesh.bind(parent.skeleton, parent.bindMatrix) + group.add(mesh) + } else if (parent.isInstancedMesh) { + mesh = new THREE.InstancedMesh(parent.geometry, material, parent.count) + mesh.instanceMatrix = parent.instanceMatrix + group.add(mesh) + } else { + mesh = new THREE.Mesh() + mesh.material = material + group.add(mesh) + } + mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry } } - }, [angle]) + }) React.useLayoutEffect(() => { const group = ref.current + if (!group) return + const mesh = group.children[0] as THREE.Mesh if (mesh) { - applyProps(mesh.material as any, { transparent, thickness, color, opacity }) + mesh.renderOrder = renderOrder + applyProps(mesh.material as any, { + transparent, + thickness, + color, + opacity, + size: contextSize, + screenspace, + toneMapped, + polygonOffset, + polygonOffsetFactor, + }) + } + }) + + React.useEffect(() => { + return () => { + // Dispose everything on unmount + const group = ref.current + if (!group) return + + const mesh = group.children[0] as THREE.Mesh + if (mesh) { + if (angle) mesh.geometry.dispose() + group.remove(mesh) + } } - }, [angle, transparent, thickness, color, opacity]) + }, []) - return + return } {...props} /> } diff --git a/src/core/PerformanceMonitor.tsx b/src/core/PerformanceMonitor.tsx index 9b87a20e8..1e49a29f7 100644 --- a/src/core/PerformanceMonitor.tsx +++ b/src/core/PerformanceMonitor.tsx @@ -54,7 +54,7 @@ type PerformanceMonitorProps = { children?: React.ReactNode } -const context = createContext(null!) +const context = /* @__PURE__ */ createContext(null!) export function PerformanceMonitor({ iterations = 10, diff --git a/src/core/PerspectiveCamera.tsx b/src/core/PerspectiveCamera.tsx index dee24a757..0cedd5f62 100644 --- a/src/core/PerspectiveCamera.tsx +++ b/src/core/PerspectiveCamera.tsx @@ -23,7 +23,7 @@ type Props = Omit & { envMap?: THREE.Texture } -export const PerspectiveCamera: ForwardRefComponent = React.forwardRef( +export const PerspectiveCamera: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ envMap, resolution = 256, frames = Infinity, makeDefault, children, ...props }: Props, ref) => { const set = useThree(({ set }) => set) const camera = useThree(({ camera }) => camera) diff --git a/src/core/PointMaterial.tsx b/src/core/PointMaterial.tsx index 956614c25..083d5faf0 100644 --- a/src/core/PointMaterial.tsx +++ b/src/core/PointMaterial.tsx @@ -2,6 +2,7 @@ import * as THREE from 'three' import * as React from 'react' import { PrimitiveProps } from '@react-three/fiber' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' type PointMaterialType = JSX.IntrinsicElements['pointsMaterial'] @@ -13,7 +14,7 @@ declare global { } } -const opaque_fragment = parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'opaque_fragment' : 'output_fragment' +const opaque_fragment = version >= 154 ? 'opaque_fragment' : 'output_fragment' export class PointMaterialImpl extends THREE.PointsMaterial { constructor(props) { @@ -34,7 +35,7 @@ export class PointMaterialImpl extends THREE.PointsMaterial { float mask = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r); gl_FragColor = vec4(gl_FragColor.rgb, mask * gl_FragColor.a ); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> ` ) } @@ -44,7 +45,7 @@ export class PointMaterialImpl extends THREE.PointsMaterial { export const PointMaterial: ForwardRefComponent< Omit, PointMaterialImpl -> = React.forwardRef>((props, ref) => { +> = /* @__PURE__ */ React.forwardRef>((props, ref) => { const [material] = React.useState(() => new PointMaterialImpl(null)) return }) diff --git a/src/core/PointerLockControls.tsx b/src/core/PointerLockControls.tsx index 1695a3e2d..a34e5c110 100644 --- a/src/core/PointerLockControls.tsx +++ b/src/core/PointerLockControls.tsx @@ -20,7 +20,7 @@ export type PointerLockControlsProps = ReactThreeFiber.Object3DNode< } export const PointerLockControls: ForwardRefComponent = - React.forwardRef( + /* @__PURE__ */ React.forwardRef( ({ domElement, selector, onChange, onLock, onUnlock, enabled = true, makeDefault, ...props }, ref) => { const { camera, ...rest } = props const setEvents = useThree((state) => state.setEvents) @@ -73,8 +73,8 @@ export const PointerLockControls: ForwardRefComponent { controls.removeEventListener('change', callback) - if (onLock) controls.addEventListener('lock', onLock) - if (onUnlock) controls.addEventListener('unlock', onUnlock) + if (onLock) controls.removeEventListener('lock', onLock) + if (onUnlock) controls.removeEventListener('unlock', onUnlock) elements.forEach((element) => (element ? element.removeEventListener('click', handler) : undefined)) } }, [onChange, onLock, onUnlock, selector, controls, invalidate]) diff --git a/src/core/Points.tsx b/src/core/Points.tsx index b293d659a..5e217d0e7 100644 --- a/src/core/Points.tsx +++ b/src/core/Points.tsx @@ -22,10 +22,10 @@ type PointsInstancesProps = JSX.IntrinsicElements['points'] & { limit?: number } -const _inverseMatrix = /*@__PURE__*/ new THREE.Matrix4() -const _ray = /*@__PURE__*/ new THREE.Ray() -const _sphere = /*@__PURE__*/ new THREE.Sphere() -const _position = /*@__PURE__*/ new THREE.Vector3() +const _inverseMatrix = /* @__PURE__ */ new THREE.Matrix4() +const _ray = /* @__PURE__ */ new THREE.Ray() +const _sphere = /* @__PURE__ */ new THREE.Sphere() +const _position = /* @__PURE__ */ new THREE.Vector3() export class PositionPoint extends THREE.Group { size: number @@ -82,14 +82,14 @@ export class PositionPoint extends THREE.Group { } let i, positionRef -const context = /*@__PURE__*/ React.createContext(null!) -const parentMatrix = /*@__PURE__*/ new THREE.Matrix4() -const position = /*@__PURE__*/ new THREE.Vector3() +const context = /* @__PURE__ */ React.createContext(null!) +const parentMatrix = /* @__PURE__ */ new THREE.Matrix4() +const position = /* @__PURE__ */ new THREE.Vector3() /** * Instance implementation, relies on react + context to update the attributes based on the children of this component */ -const PointsInstances: ForwardRefComponent = React.forwardRef< +const PointsInstances: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< THREE.Points, PointsInstancesProps >(({ children, range, limit = 1000, ...props }, ref) => { @@ -173,8 +173,8 @@ const PointsInstances: ForwardRefComponent = ) }) -export const Point: ForwardRefComponent = React.forwardRef( - ({ children, ...props }: JSX.IntrinsicElements['positionPoint'], ref) => { +export const Point: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef(({ children, ...props }: JSX.IntrinsicElements['positionPoint'], ref) => { React.useMemo(() => extend({ PositionPoint }), []) const group = React.useRef() const { subscribe, getParent } = React.useContext(context) @@ -184,8 +184,7 @@ export const Point: ForwardRefComponent ) - } -) + }) /** * Buffer implementation, relies on complete buffers of the correct number, leaves it to the user to update them @@ -199,7 +198,7 @@ type PointsBuffersProps = JSX.IntrinsicElements['points'] & { stride?: 2 | 3 } -export const PointsBuffer: ForwardRefComponent = React.forwardRef< +export const PointsBuffer: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< THREE.Points, PointsBuffersProps >(({ children, positions, colors, sizes, stride = 3, ...props }, forwardedRef) => { @@ -246,11 +245,9 @@ export const PointsBuffer: ForwardRefComponent ) }) -export const Points: ForwardRefComponent = React.forwardRef< - THREE.Points, - PointsBuffersProps | PointsInstancesProps ->((props, forwardedRef) => { - if ((props as PointsBuffersProps).positions instanceof Float32Array) { - return - } else return -}) +export const Points: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef((props, forwardedRef) => { + if ((props as PointsBuffersProps).positions instanceof Float32Array) { + return + } else return + }) diff --git a/src/core/PositionalAudio.tsx b/src/core/PositionalAudio.tsx index 709912cf6..659806b72 100644 --- a/src/core/PositionalAudio.tsx +++ b/src/core/PositionalAudio.tsx @@ -10,7 +10,7 @@ type Props = JSX.IntrinsicElements['positionalAudio'] & { loop?: boolean } -export const PositionalAudio: ForwardRefComponent = React.forwardRef( +export const PositionalAudio: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ url, distance = 1, loop = true, autoplay, ...props }: Props, ref) => { const sound = React.useRef() const camera = useThree(({ camera }) => camera) diff --git a/src/core/QuadraticBezierLine.tsx b/src/core/QuadraticBezierLine.tsx index 522cba2dd..b15440068 100644 --- a/src/core/QuadraticBezierLine.tsx +++ b/src/core/QuadraticBezierLine.tsx @@ -21,45 +21,44 @@ export type Line2Props = Object3DNode & { ) => void } -const v = new Vector3() -export const QuadraticBezierLine: ForwardRefComponent = React.forwardRef( - function QuadraticBezierLine({ start = [0, 0, 0], end = [0, 0, 0], mid, segments = 20, ...rest }, forwardref) { - const ref = React.useRef(null!) - const [curve] = React.useState( - () => new QuadraticBezierCurve3(undefined as any, undefined as any, undefined as any) - ) - const getPoints = React.useCallback((start, end, mid, segments = 20) => { - if (start instanceof Vector3) curve.v0.copy(start) - else curve.v0.set(...(start as [number, number, number])) - if (end instanceof Vector3) curve.v2.copy(end) - else curve.v2.set(...(end as [number, number, number])) - if (mid instanceof Vector3) { - curve.v1.copy(mid) - } else if (Array.isArray(mid)) { - curve.v1.set(...(mid as [number, number, number])) - } else { - curve.v1.copy( - curve.v0 - .clone() - .add(curve.v2.clone().sub(curve.v0)) - .add(v.set(0, curve.v0.y - curve.v2.y, 0)) - ) - } - return curve.getPoints(segments) - }, []) +const v = /* @__PURE__ */ new Vector3() +export const QuadraticBezierLine: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + Line2Props, + Props +>(function QuadraticBezierLine({ start = [0, 0, 0], end = [0, 0, 0], mid, segments = 20, ...rest }, forwardref) { + const ref = React.useRef(null!) + const [curve] = React.useState(() => new QuadraticBezierCurve3(undefined as any, undefined as any, undefined as any)) + const getPoints = React.useCallback((start, end, mid, segments = 20) => { + if (start instanceof Vector3) curve.v0.copy(start) + else curve.v0.set(...(start as [number, number, number])) + if (end instanceof Vector3) curve.v2.copy(end) + else curve.v2.set(...(end as [number, number, number])) + if (mid instanceof Vector3) { + curve.v1.copy(mid) + } else if (Array.isArray(mid)) { + curve.v1.set(...(mid as [number, number, number])) + } else { + curve.v1.copy( + curve.v0 + .clone() + .add(curve.v2.clone().sub(curve.v0)) + .add(v.set(0, curve.v0.y - curve.v2.y, 0)) + ) + } + return curve.getPoints(segments) + }, []) - React.useLayoutEffect(() => { - ref.current.setPoints = ( - start: Vector3 | [number, number, number], - end: Vector3 | [number, number, number], - mid: Vector3 | [number, number, number] - ) => { - const points = getPoints(start, end, mid) - if (ref.current.geometry) ref.current.geometry.setPositions(points.map((p) => p.toArray()).flat()) - } - }, []) + React.useLayoutEffect(() => { + ref.current.setPoints = ( + start: Vector3 | [number, number, number], + end: Vector3 | [number, number, number], + mid: Vector3 | [number, number, number] + ) => { + const points = getPoints(start, end, mid) + if (ref.current.geometry) ref.current.geometry.setPositions(points.map((p) => p.toArray()).flat()) + } + }, []) - const points = React.useMemo(() => getPoints(start, end, mid, segments), [start, end, mid, segments]) - return - } -) + const points = React.useMemo(() => getPoints(start, end, mid, segments), [start, end, mid, segments]) + return +}) diff --git a/src/core/Reflector.tsx b/src/core/Reflector.tsx index 60cd60aa2..67f8c3fcc 100644 --- a/src/core/Reflector.tsx +++ b/src/core/Reflector.tsx @@ -52,12 +52,13 @@ declare global { } } -extend({ MeshReflectorMaterial }) - /** * @deprecated Use MeshReflectorMaterial instead */ -export const Reflector: ForwardRefComponent = React.forwardRef( +export const Reflector: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + Mesh, + ReflectorProps +>( ( { mixBlur = 0, @@ -79,6 +80,8 @@ export const Reflector: ForwardRefComponent = React.forwar }, ref ) => { + extend({ MeshReflectorMaterial }) + React.useEffect(() => { console.warn( 'Reflector has been deprecated and will be removed next major. Replace it with !' diff --git a/src/core/RenderCubeTexture.tsx b/src/core/RenderCubeTexture.tsx new file mode 100644 index 000000000..2bebd43a3 --- /dev/null +++ b/src/core/RenderCubeTexture.tsx @@ -0,0 +1,144 @@ +import * as THREE from 'three' +import * as React from 'react' +import { ComputeFunction, ReactThreeFiber, createPortal, useFrame, useThree } from '@react-three/fiber' +import { ForwardRefComponent } from '../helpers/ts-utils' + +export type RenderCubeTextureProps = Omit & { + /** Optional stencil buffer, defaults to false */ + stencilBuffer?: boolean + /** Optional depth buffer, defaults to true */ + depthBuffer?: boolean + /** Optional generate mipmaps, defaults to false */ + generateMipmaps?: boolean + /** Optional render priority, defaults to 0 */ + renderPriority?: number + /** Optional event priority, defaults to 0 */ + eventPriority?: number + /** Optional frame count, defaults to Infinity. If you set it to 1, it would only render a single frame, etc */ + frames?: number + /** Optional event compute, defaults to undefined */ + compute?: ComputeFunction + /** Flip cubemap, see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLCubeRenderTarget.js */ + flip?: boolean + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be rendered into a portal */ + children: React.ReactNode + near?: number + far?: number + position?: ReactThreeFiber.Vector3 + rotation?: ReactThreeFiber.Euler + scale?: ReactThreeFiber.Vector3 + quaternion?: ReactThreeFiber.Quaternion + matrix?: ReactThreeFiber.Matrix4 + matrixAutoUpdate?: boolean +} + +export type RenderCubeTextureApi = { + scene: THREE.Scene + fbo: THREE.WebGLCubeRenderTarget + camera: THREE.CubeCamera +} + +export const RenderCubeTexture: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + children, + compute, + renderPriority = -1, + eventPriority = 0, + frames = Infinity, + stencilBuffer = false, + depthBuffer = true, + generateMipmaps = false, + resolution = 896, + near = 0.1, + far = 1000, + flip = false, + position, + rotation, + scale, + quaternion, + matrix, + matrixAutoUpdate, + ...props + }, + forwardRef + ) => { + const { size, viewport } = useThree() + + const camera = React.useRef(null!) + const fbo = React.useMemo(() => { + const fbo = new THREE.WebGLCubeRenderTarget( + Math.max((resolution || size.width) * viewport.dpr, (resolution || size.height) * viewport.dpr), + { + stencilBuffer, + depthBuffer, + generateMipmaps, + } + ) + fbo.texture.isRenderTargetTexture = !flip + fbo.texture.flipY = true + fbo.texture.type = THREE.HalfFloatType + return fbo + }, [resolution, flip]) + + React.useEffect(() => { + return () => fbo.dispose() + }, [fbo]) + + const [vScene] = React.useState(() => new THREE.Scene()) + + React.useImperativeHandle(forwardRef, () => ({ scene: vScene, fbo, camera: camera.current }), [fbo]) + + return ( + <> + {createPortal( + + {children} + {/* Without an element that receives pointer events state.pointer will always be 0/0 */} + null} /> + , + vScene, + { events: { compute, priority: eventPriority } } + )} + + + + ) + } + ) + +// The container component has to be separate, it can not be inlined because "useFrame(state" when run inside createPortal will return +// the portals own state which includes user-land overrides (custom cameras etc), but if it is executed in 's render function +// it would return the default state. +function Container({ + frames, + renderPriority, + children, + camera, +}: { + frames: number + renderPriority: number + children: React.ReactNode + camera: React.MutableRefObject +}) { + let count = 0 + useFrame((state) => { + if (frames === Infinity || count < frames) { + camera.current.update(state.gl, state.scene) + count++ + } + }, renderPriority) + return <>{children} +} diff --git a/src/core/RenderTexture.tsx b/src/core/RenderTexture.tsx index 8d5c9ae1b..3b63dfdf5 100644 --- a/src/core/RenderTexture.tsx +++ b/src/core/RenderTexture.tsx @@ -29,7 +29,7 @@ type Props = JSX.IntrinsicElements['texture'] & { children: React.ReactNode } -export const RenderTexture: ForwardRefComponent = React.forwardRef( +export const RenderTexture: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { children, @@ -111,14 +111,18 @@ function Container({ }) { let count = 0 let oldAutoClear + let oldXrEnabled useFrame((state) => { if (frames === Infinity || count < frames) { oldAutoClear = state.gl.autoClear + oldXrEnabled = state.gl.xr.enabled state.gl.autoClear = true + state.gl.xr.enabled = false state.gl.setRenderTarget(fbo) state.gl.render(state.scene, state.camera) state.gl.setRenderTarget(null) state.gl.autoClear = oldAutoClear + state.gl.xr.enabled = oldXrEnabled count++ } }, renderPriority) diff --git a/src/core/Resize.tsx b/src/core/Resize.tsx index 67e2c8c32..4f11343e7 100644 --- a/src/core/Resize.tsx +++ b/src/core/Resize.tsx @@ -14,7 +14,7 @@ export type ResizeProps = JSX.IntrinsicElements['group'] & { precise?: boolean } -export const Resize = React.forwardRef( +export const Resize = /* @__PURE__ */ React.forwardRef( ({ children, width, height, depth, box3, precise = true, ...props }, fRef) => { const ref = React.useRef(null!) const outer = React.useRef(null!) diff --git a/src/core/RoundedBox.tsx b/src/core/RoundedBox.tsx index 6ac523883..3c688f7e6 100644 --- a/src/core/RoundedBox.tsx +++ b/src/core/RoundedBox.tsx @@ -19,48 +19,52 @@ type Props = { args?: NamedArrayTuple<(width?: number, height?: number, depth?: number) => void> radius?: number smoothness?: number + bevelSegments?: number steps?: number creaseAngle?: number } & Omit -export const RoundedBox: ForwardRefComponent = React.forwardRef(function RoundedBox( - { - args: [width = 1, height = 1, depth = 1] = [], - radius = 0.05, - steps = 1, - smoothness = 4, - creaseAngle = 0.4, - children, - ...rest - }, - ref -) { - const shape = React.useMemo(() => createShape(width, height, radius), [width, height, radius]) - const params = React.useMemo( - () => ({ - depth: depth - radius * 2, - bevelEnabled: true, - bevelSegments: smoothness * 2, - steps, - bevelSize: radius - eps, - bevelThickness: radius, - curveSegments: smoothness, - }), - [depth, radius, smoothness] - ) - const geomRef = React.useRef() +export const RoundedBox: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( + function RoundedBox( + { + args: [width = 1, height = 1, depth = 1] = [], + radius = 0.05, + steps = 1, + smoothness = 4, + bevelSegments = 4, + creaseAngle = 0.4, + children, + ...rest + }, + ref + ) { + const shape = React.useMemo(() => createShape(width, height, radius), [width, height, radius]) + const params = React.useMemo( + () => ({ + depth: depth - radius * 2, + bevelEnabled: true, + bevelSegments: bevelSegments * 2, + steps, + bevelSize: radius - eps, + bevelThickness: radius, + curveSegments: smoothness, + }), + [depth, radius, smoothness] + ) + const geomRef = React.useRef(null!) - React.useLayoutEffect(() => { - if (geomRef.current) { - geomRef.current.center() - toCreasedNormals(geomRef.current, creaseAngle) - } - }, [shape, params]) + React.useLayoutEffect(() => { + if (geomRef.current) { + geomRef.current.center() + toCreasedNormals(geomRef.current, creaseAngle) + } + }, [shape, params]) - return ( - - - {children} - - ) -}) + return ( + + + {children} + + ) + } +) diff --git a/src/core/ScreenQuad.tsx b/src/core/ScreenQuad.tsx index 1bff5f645..78f481531 100644 --- a/src/core/ScreenQuad.tsx +++ b/src/core/ScreenQuad.tsx @@ -15,7 +15,7 @@ function createScreenQuadGeometry() { type Props = Omit -export const ScreenQuad: ForwardRefComponent = React.forwardRef( +export const ScreenQuad: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( function ScreenQuad({ children, ...restProps }, ref) { const geometry = React.useMemo(createScreenQuadGeometry, []) diff --git a/src/core/ScreenSpace.tsx b/src/core/ScreenSpace.tsx index 66b98a02e..b95996d66 100644 --- a/src/core/ScreenSpace.tsx +++ b/src/core/ScreenSpace.tsx @@ -8,18 +8,19 @@ export type ScreenSpaceProps = { depth?: number } & JSX.IntrinsicElements['group'] -export const ScreenSpace: ForwardRefComponent = React.forwardRef( - ({ children, depth = -1, ...rest }, ref) => { - const localRef = React.useRef(null!) +export const ScreenSpace: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + Group, + ScreenSpaceProps +>(({ children, depth = -1, ...rest }, ref) => { + const localRef = React.useRef(null!) - useFrame(({ camera }) => { - localRef.current.quaternion.copy(camera.quaternion) - localRef.current.position.copy(camera.position) - }) - return ( - - {children} - - ) - } -) + useFrame(({ camera }) => { + localRef.current.quaternion.copy(camera.quaternion) + localRef.current.position.copy(camera.position) + }) + return ( + + {children} + + ) +}) diff --git a/src/core/Segments.tsx b/src/core/Segments.tsx index 3ec7d21ce..1cea63267 100644 --- a/src/core/Segments.tsx +++ b/src/core/Segments.tsx @@ -2,10 +2,10 @@ import * as THREE from 'three' import * as React from 'react' import mergeRefs from 'react-merge-refs' import { extend, useFrame, ReactThreeFiber } from '@react-three/fiber' -import { Line2, LineSegmentsGeometry, LineMaterial } from 'three-stdlib' +import { Line2, LineSegmentsGeometry, LineMaterial, LineMaterialParameters } from 'three-stdlib' import { ForwardRefComponent } from '../helpers/ts-utils' -type SegmentsProps = { +type SegmentsProps = LineMaterialParameters & { limit?: number lineWidth?: number children: React.ReactNode @@ -22,9 +22,9 @@ type SegmentProps = Omit(null!) +const context = /* @__PURE__ */ React.createContext(null!) -const Segments: ForwardRefComponent = React.forwardRef( +const Segments: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( (props, forwardedRef) => { React.useMemo(() => extend({ SegmentObject }), []) @@ -114,16 +114,15 @@ export class SegmentObject { const normPos = (pos: SegmentProps['start']): SegmentObject['start'] => pos instanceof THREE.Vector3 ? pos : new THREE.Vector3(...(typeof pos === 'number' ? [pos, pos, pos] : pos)) -const Segment: ForwardRefComponent = React.forwardRef( - ({ color, start, end }, forwardedRef) => { - const api = React.useContext(context) - if (!api) throw 'Segment must used inside Segments component.' - const ref = React.useRef(null) - React.useLayoutEffect(() => api.subscribe(ref), []) - return ( - - ) - } -) +const Segment: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< + SegmentObject, + SegmentProps +>(({ color, start, end }, forwardedRef) => { + const api = React.useContext(context) + if (!api) throw 'Segment must used inside Segments component.' + const ref = React.useRef(null) + React.useLayoutEffect(() => api.subscribe(ref), []) + return +}) export { Segments, Segment } diff --git a/src/core/Shadow.tsx b/src/core/Shadow.tsx index 5dfa96285..81e71bf18 100644 --- a/src/core/Shadow.tsx +++ b/src/core/Shadow.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Mesh, Color, DoubleSide } from 'three' +import { Mesh, Color, DoubleSide, type PlaneGeometry, type MeshBasicMaterial } from 'three' import { ForwardRefComponent } from '../helpers/ts-utils' type Props = JSX.IntrinsicElements['mesh'] & { @@ -10,7 +10,9 @@ type Props = JSX.IntrinsicElements['mesh'] & { depthWrite?: boolean } -export const Shadow: ForwardRefComponent = React.forwardRef( +export type ShadowType = Mesh + +export const Shadow: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { fog = false, renderOrder, depthWrite = false, colorStop = 0.0, color = 'black', opacity = 0.5, ...props }: Props, ref diff --git a/src/core/ShadowAlpha.tsx b/src/core/ShadowAlpha.tsx new file mode 100644 index 000000000..1c8365d8d --- /dev/null +++ b/src/core/ShadowAlpha.tsx @@ -0,0 +1,122 @@ +/** + * Integration and compilation: Faraz Shaikh (https://twitter.com/CantBeFaraz) + * + * Based on: + * - https://gkjohnson.github.io/threejs-sandbox/screendoor-transparency/ by Garrett Johnson (https://github.com/gkjohnson) + * + * Note: + * - Must depreciate in favor of https://github.com/mrdoob/three.js/issues/10600 when it's ready. + */ + +import { useFrame } from '@react-three/fiber' +import * as React from 'react' +import * as THREE from 'three' + +interface ShadowAlphaProps { + opacity?: number + alphaMap?: THREE.Texture | boolean +} + +export function ShadowAlpha({ opacity, alphaMap }: ShadowAlphaProps) { + const depthMaterialRef = React.useRef(null!) + const distanceMaterialRef = React.useRef(null!) + + const uShadowOpacity = React.useRef({ + value: 1, + }) + + const uAlphaMap = React.useRef({ + value: null, + }) + + const uHasAlphaMap = React.useRef({ + value: false, + }) + + React.useLayoutEffect(() => { + depthMaterialRef.current.onBeforeCompile = distanceMaterialRef.current.onBeforeCompile = (shader) => { + // Need to get the "void main" line dynamically because the lines for + // MeshDistanceMaterial and MeshDepthMaterial are different 🤦‍♂️ + const mainLineStart = shader.fragmentShader.indexOf('void main') + let mainLine = '' + let ch + let i = mainLineStart + while (ch !== '\n' && i < mainLineStart + 100) { + ch = shader.fragmentShader.charAt(i) + mainLine += ch + i++ + } + mainLine = mainLine.trim() + + shader.vertexShader = shader.vertexShader.replace( + 'void main() {', + ` + varying vec2 custom_vUv; + + void main() { + custom_vUv = uv; + + ` + ) + + shader.fragmentShader = shader.fragmentShader.replace( + mainLine, + ` + uniform float uShadowOpacity; + uniform sampler2D uAlphaMap; + uniform bool uHasAlphaMap; + + varying vec2 custom_vUv; + + float bayerDither2x2( vec2 v ) { + return mod( 3.0 * v.y + 2.0 * v.x, 4.0 ); + } + + float bayerDither4x4( vec2 v ) { + vec2 P1 = mod( v, 2.0 ); + vec2 P2 = mod( floor( 0.5 * v ), 2.0 ); + return 4.0 * bayerDither2x2( P1 ) + bayerDither2x2( P2 ); + } + + void main() { + float alpha = + uHasAlphaMap ? + uShadowOpacity * texture2D(uAlphaMap, custom_vUv).x + : uShadowOpacity; + + if( ( bayerDither4x4( floor( mod( gl_FragCoord.xy, 4.0 ) ) ) ) / 16.0 >= alpha ) discard; + + ` + ) + + shader.uniforms['uShadowOpacity'] = uShadowOpacity.current + shader.uniforms['uAlphaMap'] = uAlphaMap.current + shader.uniforms['uHasAlphaMap'] = uHasAlphaMap.current + } + }, []) + + useFrame(() => { + const parent = (depthMaterialRef.current as any).__r3f?.parent + if (parent) { + const parentMainMaterial = parent.material + if (parentMainMaterial) { + uShadowOpacity.current.value = opacity ?? parentMainMaterial.opacity + + if (alphaMap === false) { + uAlphaMap.current.value = null + uHasAlphaMap.current.value = false + } else { + uAlphaMap.current.value = alphaMap || parentMainMaterial.alphaMap + uHasAlphaMap.current.value = !!uAlphaMap.current.value + } + } + } + }) + + return ( + <> + + + + ) +} diff --git a/src/core/Sky.tsx b/src/core/Sky.tsx index 3e14a9bcc..677e0aed0 100644 --- a/src/core/Sky.tsx +++ b/src/core/Sky.tsx @@ -26,7 +26,7 @@ export function calcPosFromAngles(inclination: number, azimuth: number, vector: return vector } -export const Sky: ForwardRefComponent = React.forwardRef( +export const Sky: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { inclination = 0.6, diff --git a/src/core/Sparkles.tsx b/src/core/Sparkles.tsx index b68b965ec..301d5fc8d 100644 --- a/src/core/Sparkles.tsx +++ b/src/core/Sparkles.tsx @@ -3,6 +3,7 @@ import * as THREE from 'three' import { PointsProps, useThree, useFrame, extend, Node } from '@react-three/fiber' import { shaderMaterial } from './shaderMaterial' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' interface Props { /** Number of particles (default: 100) */ @@ -21,7 +22,7 @@ interface Props { noise?: number | [number, number, number] | THREE.Vector3 | Float32Array } -const SparklesImplMaterial = shaderMaterial( +const SparklesImplMaterial = /* @__PURE__ */ shaderMaterial( { time: 0, pixelRatio: 1 }, ` uniform float pixelRatio; uniform float time; @@ -52,7 +53,7 @@ const SparklesImplMaterial = shaderMaterial( float strength = 0.05 / distanceToCenter - 0.1; gl_FragColor = vec4(vColor, strength * vOpacity); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) @@ -100,7 +101,7 @@ function usePropAsIsOrAsAttribute( }, [prop]) } -export const Sparkles: ForwardRefComponent = React.forwardRef< +export const Sparkles: ForwardRefComponent = /* @__PURE__ */ React.forwardRef< THREE.Points, Props & PointsProps >(({ noise = 1, count = 100, speed = 1, opacity = 1, scale = 1, size, color, children, ...props }, forwardRef) => { diff --git a/src/core/Splat.tsx b/src/core/Splat.tsx new file mode 100644 index 000000000..922fd32ad --- /dev/null +++ b/src/core/Splat.tsx @@ -0,0 +1,666 @@ +// Based on: +// Kevin Kwok https://github.com/antimatter15/splat +// Quadjr https://github.com/quadjr/aframe-gaussian-splatting +// Adapted by: +// Paul Henschel twitter.com/0xca0a + +import * as THREE from 'three' +import * as React from 'react' +import { extend, useThree, useFrame, useLoader, LoaderProto } from '@react-three/fiber' +import { shaderMaterial } from './shaderMaterial' + +export type SplatMaterialType = { + alphaTest?: number + alphaHash?: boolean + centerAndScaleTexture?: THREE.DataTexture + covAndColorTexture?: THREE.DataTexture + viewport?: THREE.Vector2 + focal?: number +} + +export type TargetMesh = THREE.Mesh & { + ready: boolean + sorted: boolean + pm: THREE.Matrix4 + vm1: THREE.Matrix4 + vm2: THREE.Matrix4 + viewport: THREE.Vector4 +} + +export type SharedState = { + url: string + gl: THREE.WebGLRenderer + worker: Worker + manager: THREE.LoadingManager + stream: ReadableStreamDefaultReader + loading: boolean + loaded: boolean + loadedVertexCount: number + rowLength: number + maxVertexes: number + chunkSize: number + totalDownloadBytes: number + numVertices: number + bufferTextureWidth: number + bufferTextureHeight: number + centerAndScaleData: Float32Array + covAndColorData: Uint32Array + covAndColorTexture: THREE.DataTexture + centerAndScaleTexture: THREE.DataTexture + connect(target: TargetMesh): () => void + update(target: TargetMesh, camera: THREE.Camera, hashed: boolean): void + onProgress?: (event: ProgressEvent) => void +} + +declare global { + namespace JSX { + interface IntrinsicElements { + splatMaterial: SplatMaterialType & JSX.IntrinsicElements['shaderMaterial'] + } + } +} + +type SplatProps = { + /** Url towards a *.splat file, no support for *.ply */ + src: string + /** Whether to use tone mapping, default: false */ + toneMapped?: boolean + /** Alpha test value, , default: 0 */ + alphaTest?: number + /** Whether to use alpha hashing, default: false */ + alphaHash?: boolean + /** Chunk size for lazy loading, prevents chokings the worker, default: 25000 (25kb) */ + chunkSize?: number +} & JSX.IntrinsicElements['mesh'] + +const SplatMaterial = /* @__PURE__ */ shaderMaterial( + { + alphaTest: 0, + viewport: /* @__PURE__ */ new THREE.Vector2(1980, 1080), + focal: 1000.0, + centerAndScaleTexture: null, + covAndColorTexture: null, + }, + /*glsl*/ ` + precision highp sampler2D; + precision highp usampler2D; + out vec4 vColor; + out vec3 vPosition; + uniform vec2 resolution; + uniform vec2 viewport; + uniform float focal; + attribute uint splatIndex; + uniform sampler2D centerAndScaleTexture; + uniform usampler2D covAndColorTexture; + + vec2 unpackInt16(in uint value) { + int v = int(value); + int v0 = v >> 16; + int v1 = (v & 0xFFFF); + if((v & 0x8000) != 0) + v1 |= 0xFFFF0000; + return vec2(float(v1), float(v0)); + } + + void main () { + ivec2 texSize = textureSize(centerAndScaleTexture, 0); + ivec2 texPos = ivec2(splatIndex%uint(texSize.x), splatIndex/uint(texSize.x)); + vec4 centerAndScaleData = texelFetch(centerAndScaleTexture, texPos, 0); + vec4 center = vec4(centerAndScaleData.xyz, 1); + vec4 camspace = modelViewMatrix * center; + vec4 pos2d = projectionMatrix * camspace; + + float bounds = 1.2 * pos2d.w; + if (pos2d.z < -pos2d.w || pos2d.x < -bounds || pos2d.x > bounds + || pos2d.y < -bounds || pos2d.y > bounds) { + gl_Position = vec4(0.0, 0.0, 2.0, 1.0); + return; + } + + uvec4 covAndColorData = texelFetch(covAndColorTexture, texPos, 0); + vec2 cov3D_M11_M12 = unpackInt16(covAndColorData.x) * centerAndScaleData.w; + vec2 cov3D_M13_M22 = unpackInt16(covAndColorData.y) * centerAndScaleData.w; + vec2 cov3D_M23_M33 = unpackInt16(covAndColorData.z) * centerAndScaleData.w; + mat3 Vrk = mat3( + cov3D_M11_M12.x, cov3D_M11_M12.y, cov3D_M13_M22.x, + cov3D_M11_M12.y, cov3D_M13_M22.y, cov3D_M23_M33.x, + cov3D_M13_M22.x, cov3D_M23_M33.x, cov3D_M23_M33.y + ); + + mat3 J = mat3( + focal / camspace.z, 0., -(focal * camspace.x) / (camspace.z * camspace.z), + 0., focal / camspace.z, -(focal * camspace.y) / (camspace.z * camspace.z), + 0., 0., 0. + ); + + mat3 W = transpose(mat3(modelViewMatrix)); + mat3 T = W * J; + mat3 cov = transpose(T) * Vrk * T; + vec2 vCenter = vec2(pos2d) / pos2d.w; + float diagonal1 = cov[0][0] + 0.3; + float offDiagonal = cov[0][1]; + float diagonal2 = cov[1][1] + 0.3; + float mid = 0.5 * (diagonal1 + diagonal2); + float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); + float lambda1 = mid + radius; + float lambda2 = max(mid - radius, 0.1); + vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); + vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector; + vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x); + uint colorUint = covAndColorData.w; + vColor = vec4( + float(colorUint & uint(0xFF)) / 255.0, + float((colorUint >> uint(8)) & uint(0xFF)) / 255.0, + float((colorUint >> uint(16)) & uint(0xFF)) / 255.0, + float(colorUint >> uint(24)) / 255.0 + ); + vPosition = position; + + gl_Position = vec4( + vCenter + + position.x * v2 / viewport * 2.0 + + position.y * v1 / viewport * 2.0, pos2d.z / pos2d.w, 1.0); + } + `, + /*glsl*/ ` + #include + #include + in vec4 vColor; + in vec3 vPosition; + void main () { + float A = -dot(vPosition.xy, vPosition.xy); + if (A < -4.0) discard; + float B = exp(A) * vColor.a; + vec4 diffuseColor = vec4(vColor.rgb, B); + #include + #include + gl_FragColor = diffuseColor; + #include + #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + } + ` +) + +function createWorker(self: any) { + let matrices: Float32Array = null! + let offset = 0 + + function sortSplats(view: Float32Array, hashed: boolean = false) { + const vertexCount = matrices.length / 16 + const threshold = -0.0001 + + let maxDepth = -Infinity + let minDepth = Infinity + const depthList = new Float32Array(vertexCount) + const sizeList = new Int32Array(depthList.buffer) + const validIndexList = new Int32Array(vertexCount) + + let validCount = 0 + for (let i = 0; i < vertexCount; i++) { + // Sign of depth is reversed + const depth = + view[0] * matrices[i * 16 + 12] + view[1] * matrices[i * 16 + 13] + view[2] * matrices[i * 16 + 14] + view[3] + // Skip behind of camera and small, transparent splat + if (hashed || (depth < 0 && matrices[i * 16 + 15] > threshold * depth)) { + depthList[validCount] = depth + validIndexList[validCount] = i + validCount++ + if (depth > maxDepth) maxDepth = depth + if (depth < minDepth) minDepth = depth + } + } + + // This is a 16 bit single-pass counting sort + const depthInv = (256 * 256 - 1) / (maxDepth - minDepth) + const counts0 = new Uint32Array(256 * 256) + for (let i = 0; i < validCount; i++) { + sizeList[i] = ((depthList[i] - minDepth) * depthInv) | 0 + counts0[sizeList[i]]++ + } + const starts0 = new Uint32Array(256 * 256) + for (let i = 1; i < 256 * 256; i++) starts0[i] = starts0[i - 1] + counts0[i - 1] + const depthIndex = new Uint32Array(validCount) + for (let i = 0; i < validCount; i++) depthIndex[starts0[sizeList[i]]++] = validIndexList[i] + return depthIndex + } + + self.onmessage = (e: { + data: { method: string; length: number; key: string; view: Float32Array; matrices: Float32Array; hashed: boolean } + }) => { + if (e.data.method == 'push') { + if (offset === 0) matrices = new Float32Array(e.data.length) + const new_matrices = new Float32Array(e.data.matrices) + matrices.set(new_matrices, offset) + offset += new_matrices.length + } else if (e.data.method == 'sort') { + if (matrices !== null) { + const indices = sortSplats(new Float32Array(e.data.view), e.data.hashed) + // @ts-ignore + self.postMessage({ indices, key: e.data.key }, [indices.buffer]) + } + } + } +} + +class SplatLoader extends THREE.Loader { + // WebGLRenderer, needs to be filled out! + gl: THREE.WebGLRenderer = null! + // Default chunk size for lazy loading + chunkSize: number = 25000 + load( + url: string, + onLoad: (data: SharedState) => void, + onProgress?: (event: ProgressEvent) => void, + onError?: (event: ErrorEvent) => void + ) { + const shared = { + gl: this.gl, + url: this.manager.resolveURL(url), + worker: new Worker( + URL.createObjectURL( + new Blob(['(', createWorker.toString(), ')(self)'], { + type: 'application/javascript', + }) + ) + ), + manager: this.manager, + update: (target: TargetMesh, camera: THREE.Camera, hashed: boolean) => update(camera, shared, target, hashed), + connect: (target: TargetMesh) => connect(shared, target), + loading: false, + loaded: false, + loadedVertexCount: 0, + chunkSize: this.chunkSize, + totalDownloadBytes: 0, + numVertices: 0, + rowLength: 3 * 4 + 3 * 4 + 4 + 4, + maxVertexes: 0, + bufferTextureWidth: 0, + bufferTextureHeight: 0, + stream: null!, + centerAndScaleData: null!, + covAndColorData: null!, + covAndColorTexture: null!, + centerAndScaleTexture: null!, + onProgress, + } + load(shared) + .then(onLoad) + .catch((e) => { + onError?.(e) + shared.manager.itemError(shared.url) + }) + } +} + +async function load(shared: SharedState) { + shared.manager.itemStart(shared.url) + const data = await fetch(shared.url) + + if (data.body === null) throw 'Failed to fetch file' + let _totalDownloadBytes = data.headers.get('Content-Length') + const totalDownloadBytes = _totalDownloadBytes ? parseInt(_totalDownloadBytes) : undefined + if (totalDownloadBytes == undefined) throw 'Failed to get content length' + shared.stream = data.body.getReader() + shared.totalDownloadBytes = totalDownloadBytes + shared.numVertices = Math.floor(shared.totalDownloadBytes / shared.rowLength) + const context = shared.gl.getContext() + let maxTextureSize = context.getParameter(context.MAX_TEXTURE_SIZE) + shared.maxVertexes = maxTextureSize * maxTextureSize + + if (shared.numVertices > shared.maxVertexes) shared.numVertices = shared.maxVertexes + shared.bufferTextureWidth = maxTextureSize + shared.bufferTextureHeight = Math.floor((shared.numVertices - 1) / maxTextureSize) + 1 + + shared.centerAndScaleData = new Float32Array(shared.bufferTextureWidth * shared.bufferTextureHeight * 4) + shared.covAndColorData = new Uint32Array(shared.bufferTextureWidth * shared.bufferTextureHeight * 4) + shared.centerAndScaleTexture = new THREE.DataTexture( + shared.centerAndScaleData, + shared.bufferTextureWidth, + shared.bufferTextureHeight, + THREE.RGBAFormat, + THREE.FloatType + ) + + shared.centerAndScaleTexture.needsUpdate = true + shared.covAndColorTexture = new THREE.DataTexture( + shared.covAndColorData, + shared.bufferTextureWidth, + shared.bufferTextureHeight, + THREE.RGBAIntegerFormat, + THREE.UnsignedIntType + ) + shared.covAndColorTexture.internalFormat = 'RGBA32UI' + shared.covAndColorTexture.needsUpdate = true + return shared +} + +async function lazyLoad(shared: SharedState) { + shared.loading = true + let bytesDownloaded = 0 + let bytesProcessed = 0 + const chunks: Array = [] + let lastReportedProgress = 0 + const lengthComputable = shared.totalDownloadBytes !== 0 + while (true) { + try { + const { value, done } = await shared.stream.read() + if (done) break + bytesDownloaded += value.length + + if (shared.totalDownloadBytes != undefined) { + const percent = (bytesDownloaded / shared.totalDownloadBytes) * 100 + if (shared.onProgress && percent - lastReportedProgress > 1) { + const event = new ProgressEvent('progress', { + lengthComputable, + loaded: bytesDownloaded, + total: shared.totalDownloadBytes, + }) + shared.onProgress(event) + lastReportedProgress = percent + } + } + + chunks.push(value) + const bytesRemains = bytesDownloaded - bytesProcessed + if (shared.totalDownloadBytes != undefined && bytesRemains > shared.rowLength * shared.chunkSize) { + let vertexCount = Math.floor(bytesRemains / shared.rowLength) + const concatenatedChunksbuffer = new Uint8Array(bytesRemains) + let offset = 0 + for (const chunk of chunks) { + concatenatedChunksbuffer.set(chunk, offset) + offset += chunk.length + } + chunks.length = 0 + if (bytesRemains > vertexCount * shared.rowLength) { + const extra_data = new Uint8Array(bytesRemains - vertexCount * shared.rowLength) + extra_data.set(concatenatedChunksbuffer.subarray(bytesRemains - extra_data.length, bytesRemains), 0) + chunks.push(extra_data) + } + const buffer = new Uint8Array(vertexCount * shared.rowLength) + buffer.set(concatenatedChunksbuffer.subarray(0, buffer.byteLength), 0) + const matrices = pushDataBuffer(shared, buffer.buffer, vertexCount) + shared.worker.postMessage( + { method: 'push', src: shared.url, length: shared.numVertices * 16, matrices: matrices.buffer }, + [matrices.buffer] + ) + bytesProcessed += vertexCount * shared.rowLength + + if (shared.onProgress) { + const event = new ProgressEvent('progress', { + lengthComputable, + loaded: shared.totalDownloadBytes, + total: shared.totalDownloadBytes, + }) + shared.onProgress(event) + } + } + } catch (error) { + console.error(error) + break + } + } + + if (bytesDownloaded - bytesProcessed > 0) { + // Concatenate the chunks into a single Uint8Array + let concatenatedChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)) + let offset = 0 + for (const chunk of chunks) { + concatenatedChunks.set(chunk, offset) + offset += chunk.length + } + let numVertices = Math.floor(concatenatedChunks.byteLength / shared.rowLength) + const matrices = pushDataBuffer(shared, concatenatedChunks.buffer, numVertices) + shared.worker.postMessage( + { method: 'push', src: shared.url, length: numVertices * 16, matrices: matrices.buffer }, + [matrices.buffer] + ) + } + shared.loaded = true + shared.manager.itemEnd(shared.url) +} + +function update(camera: THREE.Camera, shared: SharedState, target: TargetMesh, hashed: boolean) { + camera.updateMatrixWorld() + shared.gl.getCurrentViewport(target.viewport) + // @ts-ignore + target.material.viewport.x = target.viewport.z + // @ts-ignore + target.material.viewport.y = target.viewport.w + target.material.focal = (target.viewport.w / 2.0) * Math.abs(camera.projectionMatrix.elements[5]) + + if (target.ready) { + if (hashed && target.sorted) return + target.ready = false + const view = new Float32Array([ + target.modelViewMatrix.elements[2], + -target.modelViewMatrix.elements[6], + target.modelViewMatrix.elements[10], + target.modelViewMatrix.elements[14], + ]) + shared.worker.postMessage({ method: 'sort', src: shared.url, key: target.uuid, view: view.buffer, hashed }, [ + view.buffer, + ]) + if (hashed && shared.loaded) target.sorted = true + } +} + +function connect(shared: SharedState, target: TargetMesh) { + if (!shared.loading) lazyLoad(shared) + + target.ready = false + target.pm = new THREE.Matrix4() + target.vm1 = new THREE.Matrix4() + target.vm2 = new THREE.Matrix4() + target.viewport = new THREE.Vector4() + + let splatIndexArray = new Uint32Array(shared.bufferTextureWidth * shared.bufferTextureHeight) + const splatIndexes = new THREE.InstancedBufferAttribute(splatIndexArray, 1, false) + splatIndexes.setUsage(THREE.DynamicDrawUsage) + + const geometry = (target.geometry = new THREE.InstancedBufferGeometry()) + const positionsArray = new Float32Array(6 * 3) + const positions = new THREE.BufferAttribute(positionsArray, 3) + geometry.setAttribute('position', positions) + positions.setXYZ(2, -2.0, 2.0, 0.0) + positions.setXYZ(1, 2.0, 2.0, 0.0) + positions.setXYZ(0, -2.0, -2.0, 0.0) + positions.setXYZ(5, -2.0, -2.0, 0.0) + positions.setXYZ(4, 2.0, 2.0, 0.0) + positions.setXYZ(3, 2.0, -2.0, 0.0) + positions.needsUpdate = true + geometry.setAttribute('splatIndex', splatIndexes) + geometry.instanceCount = 1 + + function listener(e: { data: { key: string; indices: Uint32Array } }) { + if (target && e.data.key === target.uuid) { + let indexes = new Uint32Array(e.data.indices) + // @ts-ignore + geometry.attributes.splatIndex.set(indexes) + geometry.attributes.splatIndex.needsUpdate = true + geometry.instanceCount = indexes.length + target.ready = true + } + } + shared.worker.addEventListener('message', listener) + + async function wait() { + while (true) { + const centerAndScaleTextureProperties = shared.gl.properties.get(shared.centerAndScaleTexture) + const covAndColorTextureProperties = shared.gl.properties.get(shared.covAndColorTexture) + if ( + centerAndScaleTextureProperties?.__webglTexture && + covAndColorTextureProperties?.__webglTexture && + shared.loadedVertexCount > 0 + ) + break + await new Promise((resolve) => setTimeout(resolve, 10)) + } + target.ready = true + } + + wait() + return () => shared.worker.removeEventListener('message', listener) +} + +function pushDataBuffer(shared: SharedState, buffer: ArrayBufferLike, vertexCount: number) { + const context = shared.gl.getContext() + if (shared.loadedVertexCount + vertexCount > shared.maxVertexes) + vertexCount = shared.maxVertexes - shared.loadedVertexCount + if (vertexCount <= 0) throw 'Failed to parse file' + + const u_buffer = new Uint8Array(buffer) + const f_buffer = new Float32Array(buffer) + const matrices = new Float32Array(vertexCount * 16) + + const covAndColorData_uint8 = new Uint8Array(shared.covAndColorData.buffer) + const covAndColorData_int16 = new Int16Array(shared.covAndColorData.buffer) + for (let i = 0; i < vertexCount; i++) { + const quat = new THREE.Quaternion( + -(u_buffer[32 * i + 28 + 1] - 128) / 128.0, + (u_buffer[32 * i + 28 + 2] - 128) / 128.0, + (u_buffer[32 * i + 28 + 3] - 128) / 128.0, + -(u_buffer[32 * i + 28 + 0] - 128) / 128.0 + ) + quat.invert() + const center = new THREE.Vector3(f_buffer[8 * i + 0], f_buffer[8 * i + 1], -f_buffer[8 * i + 2]) + const scale = new THREE.Vector3(f_buffer[8 * i + 3 + 0], f_buffer[8 * i + 3 + 1], f_buffer[8 * i + 3 + 2]) + + const mtx = new THREE.Matrix4() + mtx.makeRotationFromQuaternion(quat) + mtx.transpose() + mtx.scale(scale) + const mtx_t = mtx.clone() + mtx.transpose() + mtx.premultiply(mtx_t) + mtx.setPosition(center) + + const cov_indexes = [0, 1, 2, 5, 6, 10] + let max_value = 0.0 + for (let j = 0; j < cov_indexes.length; j++) + if (Math.abs(mtx.elements[cov_indexes[j]]) > max_value) max_value = Math.abs(mtx.elements[cov_indexes[j]]) + + let destOffset = shared.loadedVertexCount * 4 + i * 4 + shared.centerAndScaleData[destOffset + 0] = center.x + shared.centerAndScaleData[destOffset + 1] = -center.y + shared.centerAndScaleData[destOffset + 2] = center.z + shared.centerAndScaleData[destOffset + 3] = max_value / 32767.0 + + destOffset = shared.loadedVertexCount * 8 + i * 4 * 2 + for (let j = 0; j < cov_indexes.length; j++) + covAndColorData_int16[destOffset + j] = (mtx.elements[cov_indexes[j]] * 32767.0) / max_value + + // RGBA + destOffset = shared.loadedVertexCount * 16 + (i * 4 + 3) * 4 + const col = new THREE.Color( + u_buffer[32 * i + 24 + 0] / 255, + u_buffer[32 * i + 24 + 1] / 255, + u_buffer[32 * i + 24 + 2] / 255 + ) + col.convertSRGBToLinear() + covAndColorData_uint8[destOffset + 0] = col.r * 255 + covAndColorData_uint8[destOffset + 1] = col.g * 255 + covAndColorData_uint8[destOffset + 2] = col.b * 255 + covAndColorData_uint8[destOffset + 3] = u_buffer[32 * i + 24 + 3] + + // Store scale and transparent to remove splat in sorting process + mtx.elements[15] = (Math.max(scale.x, scale.y, scale.z) * u_buffer[32 * i + 24 + 3]) / 255.0 + for (let j = 0; j < 16; j++) matrices[i * 16 + j] = mtx.elements[j] + } + + while (vertexCount > 0) { + let width = 0 + let height = 0 + const xoffset = shared.loadedVertexCount % shared.bufferTextureWidth + const yoffset = Math.floor(shared.loadedVertexCount / shared.bufferTextureWidth) + if (shared.loadedVertexCount % shared.bufferTextureWidth != 0) { + width = Math.min(shared.bufferTextureWidth, xoffset + vertexCount) - xoffset + height = 1 + } else if (Math.floor(vertexCount / shared.bufferTextureWidth) > 0) { + width = shared.bufferTextureWidth + height = Math.floor(vertexCount / shared.bufferTextureWidth) + } else { + width = vertexCount % shared.bufferTextureWidth + height = 1 + } + + const centerAndScaleTextureProperties = shared.gl.properties.get(shared.centerAndScaleTexture) + context.bindTexture(context.TEXTURE_2D, centerAndScaleTextureProperties.__webglTexture) + context.texSubImage2D( + context.TEXTURE_2D, + 0, + xoffset, + yoffset, + width, + height, + context.RGBA, + context.FLOAT, + shared.centerAndScaleData, + shared.loadedVertexCount * 4 + ) + + const covAndColorTextureProperties = shared.gl.properties.get(shared.covAndColorTexture) + context.bindTexture(context.TEXTURE_2D, covAndColorTextureProperties.__webglTexture) + context.texSubImage2D( + context.TEXTURE_2D, + 0, + xoffset, + yoffset, + width, + height, + // @ts-ignore + context.RGBA_INTEGER, + context.UNSIGNED_INT, + shared.covAndColorData, + shared.loadedVertexCount * 4 + ) + shared.gl.resetState() + + shared.loadedVertexCount += width * height + vertexCount -= width * height + } + return matrices +} + +export function Splat({ + src, + toneMapped = false, + alphaTest = 0, + alphaHash = false, + chunkSize = 25000, + ...props +}: SplatProps) { + extend({ SplatMaterial }) + + const ref = React.useRef(null!) + const gl = useThree((state) => state.gl) + const camera = useThree((state) => state.camera) + + // Shared state, globally memoized, the same url re-uses the same daza + const shared = useLoader(SplatLoader as unknown as LoaderProto, src, (loader) => { + loader.gl = gl + loader.chunkSize = chunkSize + }) as SharedState + + // Listen to worker results, apply them to the target mesh + React.useLayoutEffect(() => shared.connect(ref.current), [src]) + // Update the worker + useFrame(() => shared.update(ref.current, camera, alphaHash)) + + return ( + + 0} + blending={alphaHash ? THREE.NormalBlending : THREE.CustomBlending} + blendSrcAlpha={THREE.OneFactor} + alphaHash={!!alphaHash} + toneMapped={toneMapped} + /> + + ) +} diff --git a/src/core/SpriteAnimator.tsx b/src/core/SpriteAnimator.tsx index 08efd2515..7b31f075c 100644 --- a/src/core/SpriteAnimator.tsx +++ b/src/core/SpriteAnimator.tsx @@ -1,6 +1,7 @@ import * as React from 'react' -import { useFrame, useThree, Vector3 } from '@react-three/fiber' +import { useFrame, Vector3 } from '@react-three/fiber' import * as THREE from 'three' +import { Instances, Instance } from './Instances' export type SpriteAnimatorProps = { startFrame?: number @@ -22,320 +23,544 @@ export type SpriteAnimatorProps = { flipX?: boolean position?: Array alphaTest?: number + asSprite?: boolean + offset?: number + playBackwards?: boolean + resetOnEnd?: boolean + maxItems?: number + instanceItems?: any[] } & JSX.IntrinsicElements['group'] -export const SpriteAnimator: React.FC = ( - { - startFrame, - endFrame, - fps, - frameName, - textureDataURL, - textureImageURL, - loop, - numberOfFrames, - autoPlay, - animationNames, - onStart, - onEnd, - onLoopEnd, - onFrame, - play, - pause, - flipX, - alphaTest, - children, - ...props - }, - fref -) => { - const v = useThree((state) => state.viewport) - const spriteData = React.useRef(null) - const [isJsonReady, setJsonReady] = React.useState(false) - const matRef = React.useRef() - const spriteRef = React.useRef() - const timerOffset = React.useRef(window.performance.now()) - const textureData = React.useRef() - const currentFrame = React.useRef(startFrame || 0) - const currentFrameName = React.useRef(frameName || '') - const fpsInterval = 1000 / (fps || 30) - const [spriteTexture, setSpriteTexture] = React.useState(new THREE.Texture()) - const totalFrames = React.useRef(0) - const [aspect, setAspect] = React.useState([1, 1, 1]) - const flipOffset = flipX ? -1 : 1 - - function loadJsonAndTextureAndExecuteCallback( - jsonUrl: string, - textureUrl: string, - callback: (json: any, texture: THREE.Texture) => void - ): void { - const textureLoader = new THREE.TextureLoader() - const jsonPromise = fetch(jsonUrl).then((response) => response.json()) - const texturePromise = new Promise((resolve) => { - textureLoader.load(textureUrl, resolve) - }) +type SpriteAnimatorState = { + /** The user-defined, mutable, current goal position along the curve, it may be >1 or <0 */ + current: number | undefined + /** The 0-1 normalised and damped current goal position along curve */ + offset: number | undefined + hasEnded: boolean | undefined + ref: React.MutableRefObject | undefined | null | ((instance: any) => void) +} - Promise.all([jsonPromise, texturePromise]).then((response) => { - callback(response[0], response[1]) - }) - } +const context = React.createContext(null!) - const calculateAspectRatio = (width: number, height: number): Vector3 => { - const aspectRatio = height / width - spriteRef.current.scale.set(1, aspectRatio, 1) - return [1, aspectRatio, 1] - } +export function useSpriteAnimator() { + return React.useContext(context) as SpriteAnimatorState +} - // initial loads - React.useEffect(() => { - if (textureDataURL && textureImageURL) { - loadJsonAndTextureAndExecuteCallback(textureDataURL, textureImageURL, parseSpriteData) - } else if (textureImageURL) { - // only load the texture, this is an image sprite only +export const SpriteAnimator: React.FC = /* @__PURE__ */ React.forwardRef( + ( + { + startFrame, + endFrame, + fps, + frameName, + textureDataURL, + textureImageURL, + loop, + numberOfFrames, + autoPlay, + animationNames, + onStart, + onEnd, + onLoopEnd, + onFrame, + play, + pause, + flipX, + alphaTest, + children, + asSprite, + offset, + playBackwards, + resetOnEnd, + maxItems, + instanceItems, + ...props + }, + fref + ) => { + const ref = React.useRef() + const spriteData = React.useRef(null) + //const hasEnded = React.useRef(false) + const matRef = React.useRef() + const spriteRef = React.useRef() + const timerOffset = React.useRef(window.performance.now()) + const textureData = React.useRef() + const currentFrame = React.useRef(startFrame || 0) + const currentFrameName = React.useRef(frameName || '') + const fpsInterval = 1000 / (fps || 30) + const [spriteTexture, setSpriteTexture] = React.useState(new THREE.Texture()) + const totalFrames = React.useRef(0) + const [aspect, setAspect] = React.useState([1, 1, 1]) + const flipOffset = flipX ? -1 : 1 + const [displayAsSprite, setDisplayAsSprite] = React.useState(asSprite ?? true) + const pauseRef = React.useRef(pause) + const pos = React.useRef(offset) + const softEnd = React.useRef(false) + const frameBuffer = React.useRef([]) + // + + function reset() {} + + const state = React.useMemo( + () => ({ + current: pos.current, + offset: pos.current, + imageUrl: textureImageURL, + reset: reset, + hasEnded: false, + ref: fref, + }), + [textureImageURL] + ) + + React.useImperativeHandle(fref, () => ref.current, []) + + React.useLayoutEffect(() => { + pos.current = offset + }, [offset]) + + function loadJsonAndTextureAndExecuteCallback( + jsonUrl: string, + textureUrl: string, + callback: (json: any, texture: THREE.Texture) => void + ): void { const textureLoader = new THREE.TextureLoader() - new Promise((resolve) => { - textureLoader.load(textureImageURL, resolve) - }).then((texture) => { - parseSpriteData(null, texture) + const jsonPromise = fetch(jsonUrl).then((response) => response.json()) + const texturePromise = new Promise((resolve) => { + textureLoader.load(textureUrl, resolve) }) - } - }, []) - React.useLayoutEffect(() => { - modifySpritePosition() - }, [spriteTexture, flipX]) + Promise.all([jsonPromise, texturePromise]).then((response) => { + callback(response[0], response[1]) + }) + } - React.useEffect(() => { - if (autoPlay === false) { - if (play) { + const calculateAspectRatio = (width: number, height: number): Vector3 => { + const aspectRatio = height / width + if (spriteRef.current) { + spriteRef.current.scale.set(1, aspectRatio, 1) } + return [1, aspectRatio, 1] } - }, [pause]) - React.useEffect(() => { - if (currentFrameName.current !== frameName && frameName) { - currentFrame.current = 0 - currentFrameName.current = frameName - } - }, [frameName]) - - const parseSpriteData = (json: any, _spriteTexture: THREE.Texture): void => { - // sprite only case - if (json === null) { - if (_spriteTexture && numberOfFrames) { - //get size from texture - const width = _spriteTexture.image.width - const height = _spriteTexture.image.height - const frameWidth = width / numberOfFrames - const frameHeight = height - textureData.current = _spriteTexture - totalFrames.current = numberOfFrames - spriteData.current = { - frames: [], - meta: { - version: '1.0', - size: { w: width, h: height }, - scale: '1', - }, + // initial loads + React.useEffect(() => { + if (textureDataURL && textureImageURL) { + loadJsonAndTextureAndExecuteCallback(textureDataURL, textureImageURL, parseSpriteData) + } else if (textureImageURL) { + // only load the texture, this is an image sprite only + const textureLoader = new THREE.TextureLoader() + new Promise((resolve) => { + textureLoader.load(textureImageURL, resolve) + }).then((texture) => { + parseSpriteData(null, texture) + }) + } + }, []) + + React.useEffect(() => { + setDisplayAsSprite(asSprite ?? true) + }, [asSprite]) + + // support backwards play + React.useEffect(() => { + state.hasEnded = false + if (spriteData.current && playBackwards === true) { + currentFrame.current = spriteData.current.frames.length - 1 + } else { + currentFrame.current = 0 + } + }, [playBackwards]) + + React.useLayoutEffect(() => { + modifySpritePosition() + }, [spriteTexture, flipX]) + + React.useEffect(() => { + if (autoPlay) { + pauseRef.current = false + } + }, [autoPlay]) + + React.useEffect(() => { + if (currentFrameName.current !== frameName && frameName) { + currentFrame.current = 0 + currentFrameName.current = frameName + state.hasEnded = false + modifySpritePosition() + if (spriteData.current) { + const { w, h } = getFirstItem(spriteData.current.frames).sourceSize + const _aspect = calculateAspectRatio(w, h) + setAspect(_aspect) } + } + }, [frameName]) + + // parse sprite-data from JSON file (jsonHash or jsonArray) + const parseSpriteData = (json: any, _spriteTexture: THREE.Texture): void => { + // sprite only case + if (json === null) { + if (numberOfFrames) { + //get size from texture + const width = _spriteTexture.image.width + const height = _spriteTexture.image.height + const frameWidth = width / numberOfFrames + const frameHeight = height + textureData.current = _spriteTexture + totalFrames.current = numberOfFrames + + if (playBackwards) { + currentFrame.current = numberOfFrames - 1 + } + + spriteData.current = { + frames: [], + meta: { + version: '1.0', + size: { w: width, h: height }, + scale: '1', + }, + } - if (parseInt(frameWidth.toString(), 10) === frameWidth) { - // if it fits - for (let i = 0; i < numberOfFrames; i++) { - spriteData.current.frames.push({ - frame: { x: i * frameWidth, y: 0, w: frameWidth, h: frameHeight }, - rotated: false, - trimmed: false, - spriteSourceSize: { x: 0, y: 0, w: frameWidth, h: frameHeight }, - sourceSize: { w: frameWidth, h: height }, - }) + if (parseInt(frameWidth.toString(), 10) === frameWidth) { + // if it fits + for (let i = 0; i < numberOfFrames; i++) { + spriteData.current.frames.push({ + frame: { x: i * frameWidth, y: 0, w: frameWidth, h: frameHeight }, + rotated: false, + trimmed: false, + spriteSourceSize: { x: 0, y: 0, w: frameWidth, h: frameHeight }, + sourceSize: { w: frameWidth, h: height }, + }) + } } } + } else { + spriteData.current = json + spriteData.current.frames = Array.isArray(json.frames) ? json.frames : parseFrames() + totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length + textureData.current = _spriteTexture + + if (playBackwards) { + currentFrame.current = totalFrames.current - 1 + } + + const { w, h } = getFirstItem(json.frames).sourceSize + const aspect = calculateAspectRatio(w, h) + + setAspect(aspect) + if (matRef.current) { + matRef.current.map = _spriteTexture + } } - } else if (_spriteTexture) { - spriteData.current = json - spriteData.current.frames = Array.isArray(json.frames) ? json.frames : parseFrames() - totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length - textureData.current = _spriteTexture - - const { w, h } = getFirstItem(json.frames).sourceSize - const aspect = calculateAspectRatio(w, h) - - setAspect(aspect) - if (matRef.current) { - matRef.current.map = _spriteTexture + + // buffer for instanced + if (instanceItems) { + for (var i = 0; i < instanceItems.length; i++) { + const keys = Object.keys(spriteData.current.frames) + const randomKey = keys[Math.floor(Math.random() * keys.length)] + + frameBuffer.current.push({ + key: i, + frames: spriteData.current.frames, + selectedFrame: randomKey, + offset: { x: 0, y: 0 }, + }) + } } - } - _spriteTexture.premultiplyAlpha = false + _spriteTexture.premultiplyAlpha = false - setSpriteTexture(_spriteTexture) - } + setSpriteTexture(_spriteTexture) + } - // for frame based JSON Hash sprite data - const parseFrames = (): any => { - const sprites: any = {} - const data = spriteData.current - const delimiters = animationNames - if (delimiters) { - for (let i = 0; i < delimiters.length; i++) { - sprites[delimiters[i]] = [] - - for (let innerKey in data['frames']) { - const value = data['frames'][innerKey] - const frameData = value['frame'] - const x = frameData['x'] - const y = frameData['y'] - const width = frameData['w'] - const height = frameData['h'] - const sourceWidth = value['sourceSize']['w'] - const sourceHeight = value['sourceSize']['h'] - - if (typeof innerKey === 'string' && innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) { - sprites[delimiters[i]].push({ - x: x, - y: y, - w: width, - h: height, - frame: frameData, - sourceSize: { w: sourceWidth, h: sourceHeight }, - }) + // for frame based JSON Hash sprite data + const parseFrames = (): any => { + const sprites: any = {} + const data = spriteData.current + const delimiters = animationNames + if (delimiters) { + for (let i = 0; i < delimiters.length; i++) { + sprites[delimiters[i]] = [] + + for (const innerKey in data['frames']) { + const value = data['frames'][innerKey] + const frameData = value['frame'] + const x = frameData['x'] + const y = frameData['y'] + const width = frameData['w'] + const height = frameData['h'] + const sourceWidth = value['sourceSize']['w'] + const sourceHeight = value['sourceSize']['h'] + + if (innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) { + sprites[delimiters[i]].push({ + x: x, + y: y, + w: width, + h: height, + frame: frameData, + sourceSize: { w: sourceWidth, h: sourceHeight }, + }) + } } } + return sprites + } else if (frameName) { + const spritesArr: any[] = [] + for (const key in data.frames) { + spritesArr.push(data.frames[key]) + } + + return spritesArr } } - return sprites - } - - // modify the sprite material after json is parsed and state updated - const modifySpritePosition = (): void => { - if (!spriteData.current) return - const { - meta: { size: metaInfo }, - frames, - } = spriteData.current - - const { w: frameW, h: frameH } = Array.isArray(frames) - ? frames[0].sourceSize - : frameName - ? frames[frameName] - ? frames[frameName][0].sourceSize + // modify the sprite material after json is parsed and state updated + const modifySpritePosition = (): void => { + if (!spriteData.current) return + const { + meta: { size: metaInfo }, + frames, + } = spriteData.current + + const { w: frameW, h: frameH } = Array.isArray(frames) + ? frames[0].sourceSize + : frameName + ? frames[frameName] + ? frames[frameName][0].sourceSize + : { w: 0, h: 0 } : { w: 0, h: 0 } - : { w: 0, h: 0 } - matRef.current.map.wrapS = matRef.current.map.wrapT = THREE.RepeatWrapping - matRef.current.map.center.set(0, 0) - matRef.current.map.repeat.set((1 * flipOffset) / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH)) + matRef.current.map.wrapS = matRef.current.map.wrapT = THREE.RepeatWrapping + matRef.current.map.center.set(0, 0) + matRef.current.map.repeat.set((1 * flipOffset) / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH)) - //const framesH = (metaInfo.w - 1) / frameW - const framesV = (metaInfo.h - 1) / frameH - const frameOffsetY = 1 / framesV - matRef.current.map.offset.x = 0.0 //-matRef.current.map.repeat.x - matRef.current.map.offset.y = 1 - frameOffsetY + //const framesH = (metaInfo.w - 1) / frameW + const framesV = (metaInfo.h - 1) / frameH + const frameOffsetY = 1 / framesV + matRef.current.map.offset.x = 0.0 //-matRef.current.map.repeat.x + matRef.current.map.offset.y = 1 - frameOffsetY - setJsonReady(true) - if (onStart) onStart({ currentFrameName: frameName, currentFrame: currentFrame.current }) - } + if (onStart) onStart({ currentFrameName: frameName, currentFrame: currentFrame.current }) + } + + // run the animation on each frame + const runAnimation = (): void => { + //if (!frameName) return + const now = window.performance.now() + const diff = now - timerOffset.current + const { + meta: { size: metaInfo }, + frames, + } = spriteData.current + const { w: frameW, h: frameH } = getFirstItem(frames).sourceSize + const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : [] + const _endFrame = endFrame || spriteFrames.length - 1 + + var _offset = offset === undefined ? state.current : offset + + // conditionals to support backwards play + var endCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current > _endFrame + var onStartCondition = playBackwards ? currentFrame.current === _endFrame : currentFrame.current === 0 + var manualProgressEndCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current >= _endFrame + + if (endCondition) { + currentFrame.current = loop ? startFrame ?? 0 : 0 + + if (playBackwards) { + currentFrame.current = _endFrame + } + + if (loop) { + onLoopEnd?.({ + currentFrameName: frameName, + currentFrame: currentFrame.current, + }) + } else { + onEnd?.({ + currentFrameName: frameName, + currentFrame: currentFrame.current, + }) + + if (!_offset) { + console.log('will end') + } - // run the animation on each frame - const runAnimation = (): void => { - //if (!frameName) return - const now = window.performance.now() - const diff = now - timerOffset.current - const { - meta: { size: metaInfo }, - frames, - } = spriteData.current - const { w: frameW, h: frameH } = getFirstItem(frames).sourceSize - const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : [] - - let finalValX = 0 - let finalValY = 0 - const _endFrame = endFrame || spriteFrames.length - 1 - - if (currentFrame.current > _endFrame) { - currentFrame.current = loop ? startFrame ?? 0 : 0 - if (loop) { - onLoopEnd?.({ + state.hasEnded = resetOnEnd ? false : true + if (resetOnEnd) { + pauseRef.current = true + //calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames) + } + } + + if (!loop) return + } else if (onStartCondition) { + onStart?.({ currentFrameName: frameName, currentFrame: currentFrame.current, }) + } + + // for manual update + if (_offset !== undefined && manualProgressEndCondition) { + if (softEnd.current === false) { + onEnd?.({ + currentFrameName: frameName, + currentFrame: currentFrame.current, + }) + softEnd.current = true + } } else { - onEnd?.({ - currentFrameName: frameName, - currentFrame: currentFrame.current, - }) + // same for start? + softEnd.current = false } - if (!loop) return - } - if (diff <= fpsInterval) return - timerOffset.current = now - (diff % fpsInterval) - - calculateAspectRatio(frameW, frameH) - const framesH = (metaInfo.w - 1) / frameW - const framesV = (metaInfo.h - 1) / frameH - const { - frame: { x: frameX, y: frameY }, - sourceSize: { w: originalSizeX, h: originalSizeY }, - } = spriteFrames[currentFrame.current] - const frameOffsetX = 1 / framesH - const frameOffsetY = 1 / framesV - finalValX = - flipOffset > 0 - ? frameOffsetX * (frameX / originalSizeX) - : frameOffsetX * (frameX / originalSizeX) - matRef.current.map.repeat.x - finalValY = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY) - - matRef.current.map.offset.x = finalValX - matRef.current.map.offset.y = finalValY - - currentFrame.current += 1 - } + // clock to limit fps + if (diff <= fpsInterval) return + timerOffset.current = now - (diff % fpsInterval) - // *** Warning! It runs on every frame! *** - useFrame((state, delta) => { - if (!spriteData.current?.frames || !matRef.current?.map) { - return + calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames) } - if (pause) { - return - } + const calculateFinalPosition = ( + frameW: number, + frameH: number, + metaInfo: { w: number; h: number }, + spriteFrames: { frame: { x: any; y: any }; sourceSize: { w: any; h: any } }[] + ) => { + // get the manual update offset to find the next frame + var _offset = offset === undefined ? state.current : offset + const targetFrame = currentFrame.current + let finalValX = 0 + let finalValY = 0 + calculateAspectRatio(frameW, frameH) + const framesH = (metaInfo.w - 1) / frameW + const framesV = (metaInfo.h - 1) / frameH + if (!spriteFrames[targetFrame]) { + return + } - if (autoPlay || play) { - runAnimation() - onFrame && onFrame({ currentFrameName: currentFrameName.current, currentFrame: currentFrame.current }) + const { + frame: { x: frameX, y: frameY }, + sourceSize: { w: originalSizeX, h: originalSizeY }, + } = spriteFrames[targetFrame] + + const frameOffsetX = 1 / framesH + const frameOffsetY = 1 / framesV + finalValX = + flipOffset > 0 + ? frameOffsetX * (frameX / originalSizeX) + : frameOffsetX * (frameX / originalSizeX) - matRef.current.map.repeat.x + finalValY = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY) + + matRef.current.map.offset.x = finalValX + matRef.current.map.offset.y = finalValY + + // if manual update is active + if (_offset !== undefined && _offset !== null) { + // Calculate the frame index, based on offset given from the provider + let frameIndex = Math.floor(_offset * spriteFrames.length) + + // Ensure the frame index is within the valid range + frameIndex = Math.max(0, Math.min(frameIndex, spriteFrames.length - 1)) + + if (isNaN(frameIndex)) { + console.log('nan frame detected') + frameIndex = 0 //fallback + } + currentFrame.current = frameIndex + } else { + // auto update + if (playBackwards) { + currentFrame.current -= 1 + } else { + currentFrame.current += 1 + } + } } - }) - - // utils - const getFirstItem = (param: any): any => { - if (Array.isArray(param)) { - return param[0] - } else if (typeof param === 'object' && param !== null) { - const keys = Object.keys(param) - return param[keys[0]][0] - } else { - return { w: 0, h: 0 } + + // *** Warning! It runs on every frame! *** + useFrame((_state, _delta) => { + if (!spriteData.current?.frames || !matRef.current?.map) { + return + } + + if (pauseRef.current) { + return + } + + if (!state.hasEnded && (autoPlay || play)) { + runAnimation() + onFrame && onFrame({ currentFrameName: currentFrameName.current, currentFrame: currentFrame.current }) + } + }) + + // utils + const getFirstItem = (param: any): any => { + if (Array.isArray(param)) { + return param[0] + } else if (typeof param === 'object' && param !== null) { + const keys = Object.keys(param) + return frameName ? param[frameName][0] : param[keys[0]][0] + } else { + return { w: 0, h: 0 } + } } - } - return ( - - - - - - - {children} - - ) -} + return ( + + + + {displayAsSprite && ( + + + + )} + {!displayAsSprite && ( + + + + + {(instanceItems ?? [0]).map((item, index) => { + const texture = spriteTexture.clone() + if (matRef.current && frameBuffer.current[index]) { + texture.offset.set(frameBuffer.current[index].offset.x, frameBuffer.current[index].offset.y) // Set the offset for this item + } + + return ( + + + + ) + })} + + )} + + {children} + + + ) + } +) diff --git a/src/core/Stage.tsx b/src/core/Stage.tsx index 0f4b28a48..93460b72b 100644 --- a/src/core/Stage.tsx +++ b/src/core/Stage.tsx @@ -57,7 +57,7 @@ type StageProps = { /** Optionally wraps and thereby centers the models using , can also be a margin, default: true */ adjustCamera?: boolean | number /** The default environment, default: "city" */ - environment?: PresetsType | Partial + environment?: PresetsType | Partial | null /** The lighting intensity, default: 0.5 */ intensity?: number /** To adjust centering, default: undefined */ diff --git a/src/core/Stars.tsx b/src/core/Stars.tsx index a32f4c304..9cfd2bd46 100644 --- a/src/core/Stars.tsx +++ b/src/core/Stars.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import { ReactThreeFiber, useFrame } from '@react-three/fiber' import { Points, Vector3, Spherical, Color, AdditiveBlending, ShaderMaterial } from 'three' import { ForwardRefComponent } from '../helpers/ts-utils' +import { version } from '../helpers/constants' type Props = { radius?: number @@ -42,7 +43,7 @@ class StarfieldMaterial extends ShaderMaterial { gl_FragColor = vec4(vColor, opacity); #include - #include <${parseInt(THREE.REVISION.replace(/\D+/g, '')) >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }`, }) } @@ -60,7 +61,7 @@ const genStar = (r: number) => { return new Vector3().setFromSpherical(new Spherical(r, Math.acos(1 - Math.random() * 2), Math.random() * 2 * Math.PI)) } -export const Stars: ForwardRefComponent = React.forwardRef( +export const Stars: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ({ radius = 100, depth = 50, count = 5000, saturation = 0, factor = 4, fade = false, speed = 1 }: Props, ref) => { const material = React.useRef() const [position, color, size] = React.useMemo(() => { diff --git a/src/core/StatsGl.tsx b/src/core/StatsGl.tsx index ee968277d..2d6d7dc7e 100644 --- a/src/core/StatsGl.tsx +++ b/src/core/StatsGl.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { addEffect, addAfterEffect, useThree } from '@react-three/fiber' +import { addAfterEffect, useThree } from '@react-three/fiber' import Stats from 'stats-gl' type Props = Partial & { @@ -9,26 +9,24 @@ type Props = Partial & { } export function StatsGl({ className, parent, ...props }: Props) { - const gl = useThree((state) => state.gl) + const gl: any = useThree((state) => state.gl) const stats = React.useMemo(() => { const stats = new Stats({ ...props, }) - stats.init(gl.domElement) + stats.init(gl) return stats }, [gl]) React.useEffect(() => { if (stats) { const node = (parent && parent.current) || document.body - node?.appendChild(stats.container) + node?.appendChild(stats.dom) if (className) stats.container.classList.add(...className.split(' ').filter((cls) => cls)) - const begin = addEffect(() => stats.begin()) - const end = addAfterEffect(() => stats.end()) + const end = addAfterEffect(() => stats.update()) return () => { - node?.removeChild(stats.container) - begin() + node?.removeChild(stats.dom) end() } } diff --git a/src/core/Svg.tsx b/src/core/Svg.tsx index f9a9af762..983298161 100644 --- a/src/core/Svg.tsx +++ b/src/core/Svg.tsx @@ -16,67 +16,69 @@ export interface SvgProps extends Omit { strokeMeshProps?: MeshProps } -export const Svg: ForwardRefComponent = forwardRef(function R3FSvg( - { src, skipFill, skipStrokes, fillMaterial, strokeMaterial, fillMeshProps, strokeMeshProps, ...props }, - ref -) { - const svg = useLoader(SVGLoader, !src.startsWith(' = /* @__PURE__ */ forwardRef( + function R3FSvg( + { src, skipFill, skipStrokes, fillMaterial, strokeMaterial, fillMeshProps, strokeMeshProps, ...props }, + ref + ) { + const svg = useLoader(SVGLoader, !src.startsWith(' - skipStrokes - ? [] - : svg.paths.map((path) => - path.userData?.style.stroke === undefined || path.userData.style.stroke === 'none' - ? null - : path.subPaths.map((subPath) => SVGLoader.pointsToStroke(subPath.getPoints(), path.userData!.style)) - ), - [svg, skipStrokes] - ) + const strokeGeometries = useMemo( + () => + skipStrokes + ? [] + : svg.paths.map((path) => + path.userData?.style.stroke === undefined || path.userData.style.stroke === 'none' + ? null + : path.subPaths.map((subPath) => SVGLoader.pointsToStroke(subPath.getPoints(), path.userData!.style)) + ), + [svg, skipStrokes] + ) - useEffect(() => { - return () => strokeGeometries.forEach((group) => group && group.map((g) => g.dispose())) - }, [strokeGeometries]) + useEffect(() => { + return () => strokeGeometries.forEach((group) => group && group.map((g) => g.dispose())) + }, [strokeGeometries]) - return ( - - - {svg.paths.map((path, p) => ( - - {!skipFill && - path.userData?.style.fill !== undefined && - path.userData.style.fill !== 'none' && - SVGLoader.createShapes(path).map((shape, s) => ( - - - - - ))} - {!skipStrokes && - path.userData?.style.stroke !== undefined && - path.userData.style.stroke !== 'none' && - path.subPaths.map((_subPath, s) => ( - - - - ))} - - ))} + return ( + + + {svg.paths.map((path, p) => ( + + {!skipFill && + path.userData?.style.fill !== undefined && + path.userData.style.fill !== 'none' && + SVGLoader.createShapes(path).map((shape, s) => ( + + + + + ))} + {!skipStrokes && + path.userData?.style.stroke !== undefined && + path.userData.style.stroke !== 'none' && + path.subPaths.map((_subPath, s) => ( + + + + ))} + + ))} + - - ) -}) + ) + } +) diff --git a/src/core/Text.tsx b/src/core/Text.tsx index 48ef07c21..e7669d13c 100644 --- a/src/core/Text.tsx +++ b/src/core/Text.tsx @@ -39,7 +39,7 @@ type Props = JSX.IntrinsicElements['mesh'] & { } // eslint-disable-next-line prettier/prettier -export const Text: ForwardRefComponent = React.forwardRef( +export const Text: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { sdfGlyphSize = 64, diff --git a/src/core/Text3D.tsx b/src/core/Text3D.tsx index 7842e345c..1aa809b9a 100644 --- a/src/core/Text3D.tsx +++ b/src/core/Text3D.tsx @@ -36,7 +36,7 @@ const getTextFromChildren = (children) => { export const Text3D: ForwardRefComponent< React.PropsWithChildren, THREE.Mesh -> = React.forwardRef< +> = /* @__PURE__ */ React.forwardRef< THREE.Mesh, React.PropsWithChildren >( diff --git a/src/core/TrackballControls.tsx b/src/core/TrackballControls.tsx index 6e90351e2..377c30659 100644 --- a/src/core/TrackballControls.tsx +++ b/src/core/TrackballControls.tsx @@ -18,51 +18,54 @@ export type TrackballControlsProps = ReactThreeFiber.Overwrite< } > -export const TrackballControls: ForwardRefComponent = React.forwardRef< - TrackballControlsImpl, - TrackballControlsProps ->(({ makeDefault, camera, domElement, regress, onChange, onStart, onEnd, ...restProps }, ref) => { - const { invalidate, camera: defaultCamera, gl, events, set, get, performance, viewport } = useThree() - const explCamera = camera || defaultCamera - const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement - const controls = React.useMemo(() => new TrackballControlsImpl(explCamera as THREE.PerspectiveCamera), [explCamera]) +export const TrackballControls: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ({ makeDefault, camera, domElement, regress, onChange, onStart, onEnd, ...restProps }, ref) => { + const { invalidate, camera: defaultCamera, gl, events, set, get, performance, viewport } = useThree() + const explCamera = camera || defaultCamera + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + const controls = React.useMemo( + () => new TrackballControlsImpl(explCamera as THREE.PerspectiveCamera), + [explCamera] + ) - useFrame(() => { - if (controls.enabled) controls.update() - }, -1) + useFrame(() => { + if (controls.enabled) controls.update() + }, -1) - React.useEffect(() => { - controls.connect(explDomElement) - return () => void controls.dispose() - }, [explDomElement, regress, controls, invalidate]) + React.useEffect(() => { + controls.connect(explDomElement) + return () => void controls.dispose() + }, [explDomElement, regress, controls, invalidate]) - React.useEffect(() => { - const callback = (e: THREE.Event) => { - invalidate() - if (regress) performance.regress() - if (onChange) onChange(e) - } - controls.addEventListener('change', callback) - if (onStart) controls.addEventListener('start', onStart) - if (onEnd) controls.addEventListener('end', onEnd) - return () => { - if (onStart) controls.removeEventListener('start', onStart) - if (onEnd) controls.removeEventListener('end', onEnd) - controls.removeEventListener('change', callback) - } - }, [onChange, onStart, onEnd, controls, invalidate]) + React.useEffect(() => { + const callback = (e: THREE.Event) => { + invalidate() + if (regress) performance.regress() + if (onChange) onChange(e) + } + controls.addEventListener('change', callback) + if (onStart) controls.addEventListener('start', onStart) + if (onEnd) controls.addEventListener('end', onEnd) + return () => { + if (onStart) controls.removeEventListener('start', onStart) + if (onEnd) controls.removeEventListener('end', onEnd) + controls.removeEventListener('change', callback) + } + }, [onChange, onStart, onEnd, controls, invalidate]) - React.useEffect(() => { - controls.handleResize() - }, [viewport]) + React.useEffect(() => { + controls.handleResize() + }, [viewport]) - React.useEffect(() => { - if (makeDefault) { - const old = get().controls - set({ controls }) - return () => set({ controls: old }) - } - }, [makeDefault, controls]) + React.useEffect(() => { + if (makeDefault) { + const old = get().controls + set({ controls }) + return () => set({ controls: old }) + } + }, [makeDefault, controls]) - return -}) + return + } + ) diff --git a/src/core/Trail.tsx b/src/core/Trail.tsx index 8b3f178de..a131330cb 100644 --- a/src/core/Trail.tsx +++ b/src/core/Trail.tsx @@ -88,10 +88,10 @@ export function useTrail(target: Object3D, settings: Partial) { export type MeshLineGeometry = THREE.Mesh & MeshLineGeometryImpl -export const Trail: ForwardRefComponent, MeshLineGeometry> = React.forwardRef< - MeshLineGeometry, - React.PropsWithChildren ->((props, forwardRef) => { +export const Trail: ForwardRefComponent< + React.PropsWithChildren, + MeshLineGeometry +> = /* @__PURE__ */ React.forwardRef>((props, forwardRef) => { const { children } = props const { width, length, decay, local, stride, interval } = { ...defaults, diff --git a/src/core/TransformControls.tsx b/src/core/TransformControls.tsx index d44133d70..e9623215c 100644 --- a/src/core/TransformControls.tsx +++ b/src/core/TransformControls.tsx @@ -1,6 +1,4 @@ import { ReactThreeFiber, useThree } from '@react-three/fiber' -import omit from 'lodash.omit' -import pick from 'lodash.pick' import * as React from 'react' import * as THREE from 'three' import { TransformControls as TransformControlsImpl } from 'three-stdlib' @@ -34,108 +32,131 @@ export type TransformControlsProps = ReactThreeFiber.Object3DNode = React.forwardRef< - TransformControlsImpl, - TransformControlsProps ->(({ children, domElement, onChange, onMouseDown, onMouseUp, onObjectChange, object, makeDefault, ...props }, ref) => { - const transformOnlyPropNames = [ - 'enabled', - 'axis', - 'mode', - 'translationSnap', - 'rotationSnap', - 'scaleSnap', - 'space', - 'size', - 'showX', - 'showY', - 'showZ', - ] +export const TransformControls: ForwardRefComponent = + /* @__PURE__ */ React.forwardRef( + ( + { + children, + domElement, + onChange, + onMouseDown, + onMouseUp, + onObjectChange, + object, + makeDefault, + camera, + // Transform + enabled, + axis, + mode, + translationSnap, + rotationSnap, + scaleSnap, + space, + size, + showX, + showY, + showZ, + ...props + }, + ref + ) => { + // @ts-expect-error new in @react-three/fiber@7.0.5 + const defaultControls = useThree((state) => state.controls) as ControlsProto + const gl = useThree((state) => state.gl) + const events = useThree((state) => state.events) + const defaultCamera = useThree((state) => state.camera) + const invalidate = useThree((state) => state.invalidate) + const get = useThree((state) => state.get) + const set = useThree((state) => state.set) + const explCamera = camera || defaultCamera + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + const controls = React.useMemo( + () => new TransformControlsImpl(explCamera, explDomElement), + [explCamera, explDomElement] + ) + const group = React.useRef(null!) - const { camera, ...rest } = props - const transformProps = pick(rest, transformOnlyPropNames) - const objectProps = omit(rest, transformOnlyPropNames) - // @ts-expect-error new in @react-three/fiber@7.0.5 - const defaultControls = useThree((state) => state.controls) as ControlsProto - const gl = useThree((state) => state.gl) - const events = useThree((state) => state.events) - const defaultCamera = useThree((state) => state.camera) - const invalidate = useThree((state) => state.invalidate) - const get = useThree((state) => state.get) - const set = useThree((state) => state.set) - const explCamera = camera || defaultCamera - const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement - const controls = React.useMemo( - () => new TransformControlsImpl(explCamera, explDomElement), - [explCamera, explDomElement] - ) - const group = React.useRef() + React.useLayoutEffect(() => { + if (object) { + controls.attach(object instanceof THREE.Object3D ? object : object.current) + } else if (group.current instanceof THREE.Object3D) { + controls.attach(group.current) + } - React.useLayoutEffect(() => { - if (object) { - controls.attach(object instanceof THREE.Object3D ? object : object.current) - } else if (group.current instanceof THREE.Object3D) { - controls.attach(group.current) - } + return () => void controls.detach() + }, [object, children, controls]) - return () => void controls.detach() - }, [object, children, controls]) + React.useEffect(() => { + if (defaultControls) { + const callback = (event) => (defaultControls.enabled = !event.value) + controls.addEventListener('dragging-changed', callback) + return () => controls.removeEventListener('dragging-changed', callback) + } + }, [controls, defaultControls]) - React.useEffect(() => { - if (defaultControls) { - const callback = (event) => (defaultControls.enabled = !event.value) - controls.addEventListener('dragging-changed', callback) - return () => controls.removeEventListener('dragging-changed', callback) - } - }, [controls, defaultControls]) + const onChangeRef = React.useRef<(e?: THREE.Event) => void>() + const onMouseDownRef = React.useRef<(e?: THREE.Event) => void>() + const onMouseUpRef = React.useRef<(e?: THREE.Event) => void>() + const onObjectChangeRef = React.useRef<(e?: THREE.Event) => void>() - const onChangeRef = React.useRef<(e?: THREE.Event) => void>() - const onMouseDownRef = React.useRef<(e?: THREE.Event) => void>() - const onMouseUpRef = React.useRef<(e?: THREE.Event) => void>() - const onObjectChangeRef = React.useRef<(e?: THREE.Event) => void>() + React.useLayoutEffect(() => void (onChangeRef.current = onChange), [onChange]) + React.useLayoutEffect(() => void (onMouseDownRef.current = onMouseDown), [onMouseDown]) + React.useLayoutEffect(() => void (onMouseUpRef.current = onMouseUp), [onMouseUp]) + React.useLayoutEffect(() => void (onObjectChangeRef.current = onObjectChange), [onObjectChange]) - React.useLayoutEffect(() => void (onChangeRef.current = onChange), [onChange]) - React.useLayoutEffect(() => void (onMouseDownRef.current = onMouseDown), [onMouseDown]) - React.useLayoutEffect(() => void (onMouseUpRef.current = onMouseUp), [onMouseUp]) - React.useLayoutEffect(() => void (onObjectChangeRef.current = onObjectChange), [onObjectChange]) + React.useEffect(() => { + const onChange = (e: THREE.Event) => { + invalidate() + onChangeRef.current?.(e) + } - React.useEffect(() => { - const onChange = (e: THREE.Event) => { - invalidate() - onChangeRef.current?.(e) - } + const onMouseDown = (e: THREE.Event) => onMouseDownRef.current?.(e) + const onMouseUp = (e: THREE.Event) => onMouseUpRef.current?.(e) + const onObjectChange = (e: THREE.Event) => onObjectChangeRef.current?.(e) - const onMouseDown = (e: THREE.Event) => onMouseDownRef.current?.(e) - const onMouseUp = (e: THREE.Event) => onMouseUpRef.current?.(e) - const onObjectChange = (e: THREE.Event) => onObjectChangeRef.current?.(e) + controls.addEventListener('change', onChange) + controls.addEventListener('mouseDown', onMouseDown) + controls.addEventListener('mouseUp', onMouseUp) + controls.addEventListener('objectChange', onObjectChange) - controls.addEventListener('change', onChange) - controls.addEventListener('mouseDown', onMouseDown) - controls.addEventListener('mouseUp', onMouseUp) - controls.addEventListener('objectChange', onObjectChange) + return () => { + controls.removeEventListener('change', onChange) + controls.removeEventListener('mouseDown', onMouseDown) + controls.removeEventListener('mouseUp', onMouseUp) + controls.removeEventListener('objectChange', onObjectChange) + } + }, [invalidate, controls]) - return () => { - controls.removeEventListener('change', onChange) - controls.removeEventListener('mouseDown', onMouseDown) - controls.removeEventListener('mouseUp', onMouseUp) - controls.removeEventListener('objectChange', onObjectChange) - } - }, [invalidate, controls]) + React.useEffect(() => { + if (makeDefault) { + const old = get().controls + set({ controls }) + return () => set({ controls: old }) + } + }, [makeDefault, controls]) - React.useEffect(() => { - if (makeDefault) { - const old = get().controls - set({ controls }) - return () => set({ controls: old }) + return ( + <> + + + {children} + + + ) } - }, [makeDefault, controls]) - - return controls ? ( - <> - - - {children} - - - ) : null -}) + ) diff --git a/src/core/Wireframe.tsx b/src/core/Wireframe.tsx index c51643181..30e011064 100644 --- a/src/core/Wireframe.tsx +++ b/src/core/Wireframe.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import * as THREE from 'three' -import * as FIBER from '@react-three/fiber' +import { MaterialNode, extend } from '@react-three/fiber' import { WireframeMaterial, WireframeMaterialProps, @@ -12,13 +12,11 @@ import { declare global { namespace JSX { interface IntrinsicElements { - meshWireframeMaterial: FIBER.MaterialNode + meshWireframeMaterial: MaterialNode } } } -FIBER.extend({ MeshWireframeMaterial: WireframeMaterial }) - interface WireframeProps { geometry?: THREE.BufferGeometry | React.RefObject simplify?: boolean @@ -120,6 +118,7 @@ function WireframeWithCustomGeo({ simplify = false, ...props }: WireframeProps & WireframeMaterialProps) { + extend({ MeshWireframeMaterial: WireframeMaterial }) const [geometry, setGeometry] = React.useState(null!) React.useLayoutEffect(() => { diff --git a/src/core/index.ts b/src/core/index.ts index cf48c174d..ce07c133c 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -22,6 +22,7 @@ export * from './Decal' export * from './Svg' export * from './Gltf' export * from './AsciiRenderer' +export * from './Splat' // Cameras export * from './OrthographicCamera' @@ -39,7 +40,7 @@ export * from './TransformControls' export * from './PointerLockControls' export * from './FirstPersonControls' export * from './CameraControls' -export * from './FaceControls' +export * from './MotionPathControls' // Gizmos export * from './GizmoHelper' @@ -76,7 +77,6 @@ export * from './useTrailTexture' export * from './useCubeCamera' export * from './Example' export * from './SpriteAnimator' -export * from './FaceLandmarker' // Modifiers export * from './CurveModifier' @@ -94,7 +94,6 @@ export * from './softShadows' // Shapes export * from './shapes' -export * from './Facemesh' export * from './RoundedBox' export * from './ScreenQuad' @@ -122,6 +121,7 @@ export * from './useEnvironment' export * from './useMatcapTexture' export * from './useNormalTexture' export * from './Wireframe' +export * from './ShadowAlpha' // Performance export * from './Points' @@ -137,6 +137,8 @@ export * from './PerformanceMonitor' // Portals export * from './RenderTexture' +export * from './RenderCubeTexture' export * from './Mask' export * from './Hud' +export * from './Fisheye' export * from './MeshPortalMaterial' diff --git a/src/core/meshBounds.tsx b/src/core/meshBounds.tsx index 4e882488a..185d4ecf4 100644 --- a/src/core/meshBounds.tsx +++ b/src/core/meshBounds.tsx @@ -1,9 +1,9 @@ import { Raycaster, Matrix4, Ray, Sphere, Vector3, Intersection } from 'three' -const _inverseMatrix = new Matrix4() -const _ray = new Ray() -const _sphere = new Sphere() -const _vA = new Vector3() +const _inverseMatrix = /* @__PURE__ */ new Matrix4() +const _ray = /* @__PURE__ */ new Ray() +const _sphere = /* @__PURE__ */ new Sphere() +const _vA = /* @__PURE__ */ new Vector3() export function meshBounds(raycaster: Raycaster, intersects: Intersection[]) { const geometry = this.geometry diff --git a/src/core/shapes.tsx b/src/core/shapes.tsx index 0bd8a2ad4..2a1bdb308 100644 --- a/src/core/shapes.tsx +++ b/src/core/shapes.tsx @@ -20,25 +20,25 @@ function create(type: string, effect?: (mesh: THREE.Mesh) => void): ForwardRe }) } -export const Box = create('box') -export const Circle = create('circle') -export const Cone = create('cone') -export const Cylinder = create('cylinder') -export const Sphere = create('sphere') -export const Plane = create('plane') -export const Tube = create('tube') -export const Torus = create('torus') -export const TorusKnot = create('torusKnot') -export const Tetrahedron = create('tetrahedron') -export const Ring = create('ring') -export const Polyhedron = create('polyhedron') -export const Icosahedron = create('icosahedron') -export const Octahedron = create('octahedron') -export const Dodecahedron = create('dodecahedron') -export const Extrude = create('extrude') -export const Lathe = create('lathe') -export const Capsule = create('capsule') -export const Shape = create('shape', ({ geometry }) => { +export const Box = /* @__PURE__ */ create('box') +export const Circle = /* @__PURE__ */ create('circle') +export const Cone = /* @__PURE__ */ create('cone') +export const Cylinder = /* @__PURE__ */ create('cylinder') +export const Sphere = /* @__PURE__ */ create('sphere') +export const Plane = /* @__PURE__ */ create('plane') +export const Tube = /* @__PURE__ */ create('tube') +export const Torus = /* @__PURE__ */ create('torus') +export const TorusKnot = /* @__PURE__ */ create('torusKnot') +export const Tetrahedron = /* @__PURE__ */ create('tetrahedron') +export const Ring = /* @__PURE__ */ create('ring') +export const Polyhedron = /* @__PURE__ */ create('polyhedron') +export const Icosahedron = /* @__PURE__ */ create('icosahedron') +export const Octahedron = /* @__PURE__ */ create('octahedron') +export const Dodecahedron = /* @__PURE__ */ create('dodecahedron') +export const Extrude = /* @__PURE__ */ create('extrude') +export const Lathe = /* @__PURE__ */ create('lathe') +export const Capsule = /* @__PURE__ */ create('capsule') +export const Shape = /* @__PURE__ */ create('shape', ({ geometry }) => { // Calculate UVs (by https://discourse.threejs.org/u/prisoner849) // https://discourse.threejs.org/t/custom-shape-in-image-not-working/49348/10 const pos = geometry.attributes.position as THREE.BufferAttribute diff --git a/src/core/useBVH.tsx b/src/core/useBVH.tsx index d68243201..4cb2b6e39 100644 --- a/src/core/useBVH.tsx +++ b/src/core/useBVH.tsx @@ -15,6 +15,15 @@ export interface BVHOptions { maxDepth?: number /** The number of triangles to aim for in a leaf node, default: 10 */ maxLeafTris?: number + + /** If false then an index buffer is created if it does not exist and is rearranged */ + /** to hold the bvh structure. If false then a separate buffer is created to store the */ + /** structure and the index buffer (or lack thereof) is retained. This can be used */ + /** when the existing index layout is important or groups are being used so a */ + /** single BVH hierarchy can be created to improve performance. */ + /** default: false */ + /** Note: This setting is experimental */ + indirect?: boolean } export type BvhProps = BVHOptions & @@ -37,6 +46,7 @@ export function useBVH(mesh: React.MutableRefObject, options?: setBoundingBox: true, maxDepth: 40, maxLeafTris: 10, + indirect: false, ...options, } React.useEffect(() => { @@ -56,7 +66,7 @@ export function useBVH(mesh: React.MutableRefObject, options?: }, [mesh, JSON.stringify(options)]) } -export const Bvh: ForwardRefComponent = React.forwardRef( +export const Bvh: ForwardRefComponent = /* @__PURE__ */ React.forwardRef( ( { enabled = true, @@ -67,6 +77,7 @@ export const Bvh: ForwardRefComponent = React.forwardRef( setBoundingBox = true, maxDepth = 40, maxLeafTris = 10, + indirect = false, ...props }: BvhProps, fref @@ -78,7 +89,7 @@ export const Bvh: ForwardRefComponent = React.forwardRef( React.useEffect(() => { if (enabled) { - const options = { strategy, verbose, setBoundingBox, maxDepth, maxLeafTris } + const options = { strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect } const group = ref.current // This can only safely work if the component is used once, but there is no alternative. // Hijacking the raycast method to do it for individual meshes is not an option as it would @@ -103,7 +114,7 @@ export const Bvh: ForwardRefComponent = React.forwardRef( }) } } - }) + }, []) return ( diff --git a/src/core/useBoxProjectedEnv.tsx b/src/core/useBoxProjectedEnv.tsx index 568d9c273..1b821e76e 100644 --- a/src/core/useBoxProjectedEnv.tsx +++ b/src/core/useBoxProjectedEnv.tsx @@ -49,9 +49,17 @@ const getIBLRadiance_patch = /* glsl */ ` #endif ` -function boxProjectedEnvMap(shader: THREE.Shader, envMapPosition: THREE.Vector3, envMapSize: THREE.Vector3) { +// FIXME Replace with `THREE.WebGLProgramParametersWithUniforms` type when able to target @types/three@0.160.0 +interface MaterialShader { + vertexShader: string + fragmentShader: string + defines: { [define: string]: string | number | boolean } | undefined + uniforms: { [uniform: string]: THREE.IUniform } +} + +function boxProjectedEnvMap(shader: MaterialShader, envMapPosition: THREE.Vector3, envMapSize: THREE.Vector3) { // defines - ;(shader as any).defines.BOX_PROJECTED_ENV_MAP = true + shader.defines!.BOX_PROJECTED_ENV_MAP = true // uniforms shader.uniforms.envMapPosition = { value: envMapPosition } shader.uniforms.envMapSize = { value: envMapSize } @@ -89,7 +97,7 @@ export function useBoxProjectedEnv( const spread = React.useMemo( () => ({ ref, - onBeforeCompile: (shader: THREE.Shader) => boxProjectedEnvMap(shader, config.position, config.size), + onBeforeCompile: (shader: MaterialShader) => boxProjectedEnvMap(shader, config.position, config.size), customProgramCacheKey: () => JSON.stringify(config.position.toArray()) + JSON.stringify(config.size.toArray()), }), [...config.position.toArray(), ...config.size.toArray()] diff --git a/src/core/useGLTF.tsx b/src/core/useGLTF.tsx index a07c28b62..00f70d978 100644 --- a/src/core/useGLTF.tsx +++ b/src/core/useGLTF.tsx @@ -1,10 +1,11 @@ -import { Loader } from 'three' -// @ts-ignore -import { GLTFLoader, DRACOLoader, MeshoptDecoder, GLTF } from 'three-stdlib' -import { useLoader } from '@react-three/fiber' +import { type Loader } from 'three' +import { type GLTF, GLTFLoader, DRACOLoader, MeshoptDecoder } from 'three-stdlib' +import { type ObjectMap, useLoader } from '@react-three/fiber' let dracoLoader: DRACOLoader | null = null +let decoderPath: string = 'https://www.gstatic.com/draco/versioned/decoders/1.5.5/' + function extensions(useDraco: boolean | string, useMeshopt: boolean, extendLoader?: (loader: GLTFLoader) => void) { return (loader: Loader) => { if (extendLoader) { @@ -14,9 +15,7 @@ function extensions(useDraco: boolean | string, useMeshopt: boolean, extendLoade if (!dracoLoader) { dracoLoader = new DRACOLoader() } - dracoLoader.setDecoderPath( - typeof useDraco === 'string' ? useDraco : 'https://www.gstatic.com/draco/versioned/decoders/1.5.5/' - ) + dracoLoader.setDecoderPath(typeof useDraco === 'string' ? useDraco : decoderPath) ;(loader as GLTFLoader).setDRACOLoader(dracoLoader) } if (useMeshopt) { @@ -32,9 +31,8 @@ export function useGLTF( useDraco: boolean | string = true, useMeshOpt: boolean = true, extendLoader?: (loader: GLTFLoader) => void -) { - const gltf = useLoader(GLTFLoader, path, extensions(useDraco, useMeshOpt, extendLoader)) - return gltf +): T extends any[] ? (GLTF & ObjectMap)[] : GLTF & ObjectMap { + return useLoader(GLTFLoader, path, extensions(useDraco, useMeshOpt, extendLoader)) } useGLTF.preload = ( @@ -45,3 +43,6 @@ useGLTF.preload = ( ) => useLoader.preload(GLTFLoader, path, extensions(useDraco, useMeshOpt, extendLoader)) useGLTF.clear = (input: string | string[]) => useLoader.clear(GLTFLoader, input) +useGLTF.setDecoderPath = (path: string) => { + decoderPath = path +} diff --git a/src/core/useProgress.tsx b/src/core/useProgress.tsx index 7868057b2..5713831c4 100644 --- a/src/core/useProgress.tsx +++ b/src/core/useProgress.tsx @@ -11,7 +11,7 @@ type Data = { } let saveLastTotalLoaded = 0 -const useProgress = create((set) => { +const useProgress = /* @__PURE__ */ create((set) => { DefaultLoadingManager.onStart = (item, loaded, total) => { set({ active: true, diff --git a/src/core/useTexture.tsx b/src/core/useTexture.tsx index 964a2fa0b..a99f7288e 100644 --- a/src/core/useTexture.tsx +++ b/src/core/useTexture.tsx @@ -1,17 +1,22 @@ import { Texture, TextureLoader } from 'three' import { useLoader, useThree } from '@react-three/fiber' -import { useEffect } from 'react' -import { useLayoutEffect } from 'react' +import { useLayoutEffect, useEffect } from 'react' export const IsObject = (url: any): url is Record => url === Object(url) && !Array.isArray(url) && typeof url !== 'function' +export type MappedTextureType> = T extends any[] + ? Texture[] + : T extends Record + ? { [key in keyof T]: Texture } + : Texture + export function useTexture>( input: Url, - onLoad?: (texture: Texture | Texture[]) => void -): Url extends any[] ? Texture[] : Url extends object ? { [key in keyof Url]: Texture } : Texture { + onLoad?: (texture: MappedTextureType) => void +): MappedTextureType { const gl = useThree((state) => state.gl) - const textures = useLoader(TextureLoader, IsObject(input) ? Object.values(input) : (input as any)) + const textures = useLoader(TextureLoader, IsObject(input) ? Object.values(input) : input) as MappedTextureType useLayoutEffect(() => { onLoad?.(textures) @@ -19,18 +24,21 @@ export function useTexture { - const array = Array.isArray(textures) ? textures : [textures] - array.forEach(gl.initTexture) + if ('initTexture' in gl) { + const array = Array.isArray(textures) ? textures : [textures] + array.forEach(gl.initTexture) + } }, [gl, textures]) if (IsObject(input)) { - const keys = Object.keys(input) - const keyed = {} as any - keys.forEach((key) => Object.assign(keyed, { [key]: textures[keys.indexOf(key)] })) + const keyed = {} as MappedTextureType + let i = 0 + for (const key in input) keyed[key] = textures[i++] return keyed } else { - return textures as any + return textures } } diff --git a/src/core/useVideoTexture.tsx b/src/core/useVideoTexture.tsx index 93c734723..21fa0be30 100644 --- a/src/core/useVideoTexture.tsx +++ b/src/core/useVideoTexture.tsx @@ -38,6 +38,11 @@ export function useVideoTexture(src: string | MediaStream, props?: Partial