diff --git a/example/src/demos/Update.tsx b/example/src/demos/Update.tsx deleted file mode 100644 index af15651da9..0000000000 --- a/example/src/demos/Update.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState, useRef, useEffect, useMemo, useLayoutEffect } from 'react' -import { Canvas, FixedStage, Stage, useFrame, useThree, useUpdate, Stages as Standard } from '@react-three/fiber' -import { a, useSpring } from '@react-spring/three' -import { OrbitControls } from '@react-three/drei' -import * as THREE from 'three' - -const colorA = new THREE.Color('#6246ea') -const colorB = new THREE.Color('#e45858') - -const InputStage = new Stage() -const PhysicsStage = new FixedStage(1 / 30) -const HudStage = new Stage() -const lifecycle = [ - Standard.Early, - InputStage, - Standard.Fixed, - PhysicsStage, - Standard.Update, - Standard.Late, - Standard.Render, - HudStage, - Standard.After, -] - -const Stages = { - Early: Standard.Early, - Input: InputStage, - Fixed: Standard.Fixed, - Physics: PhysicsStage, - Update: Standard.Update, - Late: Standard.Late, - Render: Standard.Render, - Hud: HudStage, - After: Standard.After, -} - -function Update() { - const groupRef = useRef(null!) - const matRef = useRef(null!) - const [fixed] = useState(() => ({ scale: new THREE.Vector3(), color: new THREE.Color() })) - const [prev] = useState(() => ({ scale: new THREE.Vector3(), color: new THREE.Color() })) - - const interpolate = true - const [active, setActive] = useState(0) - - // create a common spring that will be used later to interpolate other values - const { spring } = useSpring({ - spring: active, - config: { mass: 5, tension: 400, friction: 50, precision: 0.0001 }, - }) - // interpolate values from common spring - const scale = spring.to([0, 1], [1, 2]) - const rotation = spring.to([0, 1], [0, Math.PI]) - - useUpdate(({ clock }) => { - if (groupRef.current) { - const t = clock.getElapsedTime() - const scalar = (Math.sin(t) + 2) / 2 - prev.scale.copy(fixed.scale) - fixed.scale.set(scalar, scalar, scalar) - } - - if (matRef.current) { - const t = clock.getElapsedTime() - const alpha = Math.sin(t) + 1 - prev.color.copy(fixed.color) - fixed.color.lerpColors(colorA, colorB, alpha) - } - }, Stages.Fixed) - - useUpdate((state) => { - // With interpolation of the fixed stage - const alpha = Stages.Fixed.alpha - - if (interpolate) { - groupRef.current.scale.lerpVectors(prev.scale, fixed.scale, alpha) - matRef.current.color.lerpColors(prev.color, fixed.color, alpha) - } else { - groupRef.current.scale.copy(fixed.scale) - matRef.current.color.copy(fixed.color) - } - }) - - // For backwards compatability, useFrame gets executed in the update stage - // A positive priority switches rendering to manual - useFrame(() => { - if (groupRef.current) { - groupRef.current.rotation.x = groupRef.current.rotation.y += 0.005 - } - }) - - // Use our own render function by setting render to 'manual' - useUpdate(({ gl, scene, camera }) => { - if (gl.autoClear) gl.autoClear = false - gl.clear() - gl.render(scene, camera) - }, Stages.Render) - - // Modify the fixed stage's step at runtime. - useEffect(() => { - Stages.Fixed.fixedStep = 1 / 15 - }, []) - - return ( - - setActive(Number(!active))}> - - - - - - ) -} - -export default function App() { - return ( - - - - ) -} diff --git a/example/src/demos/index.tsx b/example/src/demos/index.tsx index 42ae2d5f5b..e2c31327e7 100644 --- a/example/src/demos/index.tsx +++ b/example/src/demos/index.tsx @@ -24,7 +24,6 @@ const Test = { Component: lazy(() => import('./Test')) } const Viewcube = { Component: lazy(() => import('./Viewcube')) } const Portals = { Component: lazy(() => import('./Portals')) } const ViewTracking = { Component: lazy(() => import('./ViewTracking')) } -const Update = { Component: lazy(() => import('./Update')) } export { Animation, @@ -51,5 +50,4 @@ export { MultiView, Portals, ViewTracking, - Update, } diff --git a/packages/fiber/src/core/hooks.tsx b/packages/fiber/src/core/hooks.tsx index f5df469fe2..bada964a2d 100644 --- a/packages/fiber/src/core/hooks.tsx +++ b/packages/fiber/src/core/hooks.tsx @@ -1,9 +1,8 @@ import * as THREE from 'three' import * as React from 'react' import { suspend, preload, clear } from 'suspend-react' -import { context, RootState, RenderCallback, UpdateCallback, StageTypes, RootStore } from './store' +import { context, RootState, RenderCallback, RootStore } from './store' import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect, isObject3D } from './utils' -import { Stages } from './stages' import type { Instance } from './reconciler' /** @@ -54,21 +53,6 @@ export function useFrame(callback: RenderCallback, renderPriority: number = 0): return null } -/** - * Executes a callback in a given update stage. - * Uses the stage instance to identify which stage to target in the lifecycle. - */ -export function useUpdate(callback: UpdateCallback, stage: StageTypes = Stages.Update): void { - const store = useStore() - const stages = store.getState().internal.stages - // Memoize ref - const ref = useMutableCallback(callback) - // Throw an error if a stage does not exist in the lifecycle - if (!stages.includes(stage)) throw new Error(`An invoked stage does not exist in the lifecycle.`) - // Subscribe on mount, unsubscribe on unmount - useIsomorphicLayoutEffect(() => stage.add(ref, store), [stage]) -} - /** * Returns a node graph of an object with named nodes & materials. * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usegraph diff --git a/packages/fiber/src/core/index.tsx b/packages/fiber/src/core/index.tsx index e3dc0aeb1f..3708548008 100644 --- a/packages/fiber/src/core/index.tsx +++ b/packages/fiber/src/core/index.tsx @@ -24,23 +24,15 @@ export type { export { extend, reconciler } from './reconciler' export type { ReconcilerRoot, GLProps, CameraProps, RenderProps, InjectState } from './renderer' export { _roots, render, createRoot, unmountComponentAtNode, createPortal } from './renderer' -export type { UpdateSubscription } from './stages' -export { Stage, FixedStage, Stages } from './stages' export type { Subscription, Dpr, Size, Viewport, RenderCallback, - UpdateCallback, - LegacyAlways, - FrameloopMode, - FrameloopRender, - FrameloopLegacy, Frameloop, Performance, Renderer, - StageTypes, XRManager, RootState, RootStore, diff --git a/packages/fiber/src/core/loop.ts b/packages/fiber/src/core/loop.ts index 48d2502a74..ac68dd9251 100644 --- a/packages/fiber/src/core/loop.ts +++ b/packages/fiber/src/core/loop.ts @@ -1,5 +1,5 @@ import { _roots } from './renderer' -import type { RootState } from './store' +import type { RootState, Subscription } from './store' export type GlobalRenderCallback = (timestamp: number) => void interface SubItem { @@ -54,22 +54,31 @@ export function flushGlobalEffects(type: GlobalEffectType, timestamp: number): v } } +let subscribers: Subscription[] +let subscription: Subscription + function update(timestamp: number, state: RootState, frame?: XRFrame) { // Run local effects let delta = state.clock.getDelta() + // In frameloop='never' mode, clock times are updated using the provided timestamp if (state.frameloop === 'never' && typeof timestamp === 'number') { delta = timestamp - state.clock.elapsedTime state.clock.oldTime = state.clock.elapsedTime state.clock.elapsedTime = timestamp - } else { - delta = Math.max(Math.min(delta, state.internal.maxDelta), 0) } - // Call subscribers (useUpdate) - for (const stage of state.internal.stages) { - stage.frame(delta, frame) + + // Call subscribers (useFrame) + subscribers = state.internal.subscribers + for (let i = 0; i < subscribers.length; i++) { + subscription = subscribers[i] + subscription.ref.current(subscription.store.getState(), delta, frame) } + // Render content + if (!state.internal.priority && state.gl.render) state.gl.render(state.scene, state.camera) + + // Decrease frame count state.internal.frames = Math.max(0, state.internal.frames - 1) return state.frameloop === 'always' ? 1 : state.internal.frames } diff --git a/packages/fiber/src/core/renderer.tsx b/packages/fiber/src/core/renderer.tsx index f3b084b47d..736df94d28 100644 --- a/packages/fiber/src/core/renderer.tsx +++ b/packages/fiber/src/core/renderer.tsx @@ -13,7 +13,6 @@ import { Size, Dpr, Performance, - Subscription, Frameloop, RootStore, } from './store' @@ -34,7 +33,6 @@ import { getColorManagement, } from './utils' import { useStore } from './hooks' -import { Stage, Lifecycle, Stages } from './stages' // Shim for OffscreenCanvas since it was removed from DOM types // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/54988 @@ -107,9 +105,6 @@ export interface RenderProps void /** Response for pointer clicks that have missed any target */ onPointerMissed?: (event: MouseEvent) => void - /** Create a custom lifecycle of stages */ - stages?: Stage[] - render?: 'auto' | 'manual' } const createRendererInstance = ( @@ -128,39 +123,6 @@ const createRendererInstance = { - const state = store.getState() - let subscribers: Subscription[] - let subscription: Subscription - - const _stages = stages ?? Lifecycle - - if (!_stages.includes(Stages.Update)) throw 'The Stages.Update stage is required for R3F.' - if (!_stages.includes(Stages.Render)) throw 'The Stages.Render stage is required for R3F.' - - state.set(({ internal }) => ({ internal: { ...internal, stages: _stages } })) - - // Add useFrame loop to update stage - const frameCallback = { - current(state: RootState, delta: number, frame?: XRFrame | undefined) { - subscribers = state.internal.subscribers - for (let i = 0; i < subscribers.length; i++) { - subscription = subscribers[i] - subscription.ref.current(subscription.store.getState(), delta, frame) - } - }, - } - Stages.Update.add(frameCallback, store) - - // Add render callback to render stage - const renderCallback = { - current(state: RootState) { - if (state.internal.render === 'auto' && state.gl.render) state.gl.render(state.scene, state.camera) - }, - } - Stages.Render.add(renderCallback, store) -} - export interface ReconcilerRoot { configure: (config?: RenderProps) => ReconcilerRoot render: (element: React.ReactNode) => RootStore @@ -247,7 +209,6 @@ export function createRoot( raycaster: raycastOptions, camera: cameraOptions, onPointerMissed, - stages, } = props let state = store.getState() @@ -426,9 +387,6 @@ export function createRoot( if (performance && !is.equ(performance, state.performance, shallowLoose)) state.set((state) => ({ performance: { ...state.performance, ...performance } })) - // Create update stages. Only do this once on init - if (state.internal.stages.length === 0) createStages(stages, store) - // Set locals onCreated = onCreatedCallback configured = true diff --git a/packages/fiber/src/core/stages.ts b/packages/fiber/src/core/stages.ts deleted file mode 100644 index cb8d812d13..0000000000 --- a/packages/fiber/src/core/stages.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { Subscription, RootStore } from './store' - -// TODO: Remove deprecated fields in `Subscription` -export type UpdateSubscription = Omit - -/** - * Class representing a stage that updates every frame. - * Stages are used to build a lifecycle of effects for an app's frameloop. - */ -export class Stage { - private subscribers: UpdateSubscription[] - private _frameTime: number - - constructor() { - this.subscribers = [] - this._frameTime = 0 - } - - /** - * Executes all callback subscriptions on the stage. - * @param delta - Delta time between frame calls. - * @param [frame] - The XR frame if it exists. - */ - frame(delta: number, frame?: XRFrame) { - const subs = this.subscribers - const initialTime = performance.now() - - for (let i = 0; i < subs.length; i++) { - subs[i].ref.current(subs[i].store.getState(), delta, frame) - } - - this._frameTime = performance.now() - initialTime - } - - /** - * Adds a callback subscriber to the stage. - * @param ref - The mutable callback reference. - * @param store - The store to be used with the callback execution. - * @returns A function to remove the subscription. - */ - add(ref: UpdateSubscription['ref'], store: RootStore) { - this.subscribers.push({ ref, store }) - - return () => { - this.subscribers = this.subscribers.filter((sub) => { - return sub.ref !== ref - }) - } - } - - get frameTime() { - return this._frameTime - } -} - -// Using Unity's fixedStep default. -const FPS_50 = 1 / 50 - -/** - * Class representing a stage that updates every frame at a fixed rate. - * @param name - Name of the stage. - * @param [fixedStep] - Fixed step rate. - * @param [maxSubsteps] - Maximum number of substeps. - */ -export class FixedStage extends Stage { - private _fixedStep: number - private _maxSubsteps: number - private _accumulator: number - private _alpha: number - private _fixedFrameTime: number - private _substepTimes: number[] - - constructor(fixedStep?: number, maxSubSteps?: number) { - super() - - this._fixedStep = fixedStep ?? FPS_50 - this._maxSubsteps = maxSubSteps ?? 6 - this._accumulator = 0 - this._alpha = 0 - this._fixedFrameTime = 0 - this._substepTimes = [] - } - - /** - * Executes all callback subscriptions on the stage. - * @param delta - Delta time between frame calls. - * @param [frame] - The XR frame if it exists. - */ - frame(delta: number, frame?: XRFrame) { - const initialTime = performance.now() - let substeps = 0 - this._substepTimes = [] - - this._accumulator += delta - - while (this._accumulator >= this._fixedStep && substeps < this._maxSubsteps) { - this._accumulator -= this._fixedStep - substeps++ - - super.frame(this._fixedStep, frame) - this._substepTimes.push(super.frameTime) - } - - this._fixedFrameTime = performance.now() - initialTime - - // The accumulator will only be larger than the fixed step if we had to - // bail early due to hitting the max substep limit or execution time lagging. - // In that case, we want to shave off the excess so we don't fall behind next frame. - this._accumulator = this._accumulator % this._fixedStep - this._alpha = this._accumulator / this._fixedStep - } - - get frameTime() { - return this._fixedFrameTime - } - - get substepTimes() { - return this._substepTimes - } - - get fixedStep() { - return this._fixedStep - } - - set fixedStep(fixedStep: number) { - this._fixedStep = fixedStep - } - - get maxSubsteps() { - return this._maxSubsteps - } - - set maxSubsteps(maxSubsteps: number) { - this._maxSubsteps = maxSubsteps - } - - get accumulator() { - return this._accumulator - } - - get alpha() { - return this._alpha - } -} - -const Early = /*#__PURE__*/ new Stage() -const Fixed = /*#__PURE__*/ new FixedStage() -const Update = /*#__PURE__*/ new Stage() -const Late = /*#__PURE__*/ new Stage() -const Render = /*#__PURE__*/ new Stage() -const After = /*#__PURE__*/ new Stage() - -export const Stages = { Early, Fixed, Update, Late, Render, After } -export const Lifecycle = [Early, Fixed, Update, Late, Render, After] diff --git a/packages/fiber/src/core/store.ts b/packages/fiber/src/core/store.ts index ca14d93a6c..b3c51e2b29 100644 --- a/packages/fiber/src/core/store.ts +++ b/packages/fiber/src/core/store.ts @@ -4,7 +4,6 @@ import { type StoreApi } from 'zustand' import { createWithEqualityFn, type UseBoundStoreWithEqualityFn } from 'zustand/traditional' import type { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events' import { calculateDpr, type Camera, isOrthographicCamera, updateCamera } from './utils' -import type { FixedStage, Stage } from './stages' export interface Intersection extends THREE.Intersection { eventObject: THREE.Object3D @@ -23,6 +22,7 @@ export interface Size { top: number left: number } +export type Frameloop = 'always' | 'demand' | 'never' export interface Viewport extends Size { /** The initial pixel ratio */ initialDpr: number @@ -37,13 +37,6 @@ export interface Viewport extends Size { } export type RenderCallback = (state: RootState, delta: number, frame?: XRFrame) => void -export type UpdateCallback = RenderCallback - -export type LegacyAlways = 'always' -export type FrameloopMode = LegacyAlways | 'auto' | 'demand' | 'never' -export type FrameloopRender = 'auto' | 'manual' -export type FrameloopLegacy = 'always' | 'demand' | 'never' -export type Frameloop = FrameloopLegacy | { mode?: FrameloopMode; render?: FrameloopRender; maxDelta?: number } export interface Performance { /** Current performance normal, between min and max */ @@ -63,8 +56,6 @@ export interface Renderer { } export const isRenderer = (def: any) => !!def?.render -export type StageTypes = Stage | FixedStage - export interface InternalState { interaction: THREE.Object3D[] hovered: Map> @@ -76,12 +67,6 @@ export interface InternalState { active: boolean priority: number frames: number - /** The ordered stages defining the lifecycle. */ - stages: StageTypes[] - /** Render function flags */ - render: 'auto' | 'manual' - /** The max delta time between two frames. */ - maxDelta: number subscribe: (callback: React.RefObject, priority: number, store: RootStore) => () => void } @@ -121,9 +106,8 @@ export interface RootState { linear: boolean /** Shortcut to gl.toneMapping = NoTonemapping */ flat: boolean - /** Update frame loop flags */ - frameloop: FrameloopLegacy - /** Adaptive performance interface */ + /** Render loop flags */ + frameloop: Frameloop performance: Performance /** Reactive pixel-size of the canvas */ size: Size @@ -265,20 +249,9 @@ export const createStore = ( const resolved = calculateDpr(dpr) return { viewport: { ...state.viewport, dpr: resolved, initialDpr: state.viewport.initialDpr || resolved } } }), - setFrameloop: (frameloop: Frameloop) => { - const state = get() - const mode: FrameloopLegacy = - typeof frameloop === 'string' - ? frameloop - : frameloop?.mode === 'auto' - ? 'always' - : frameloop?.mode ?? state.frameloop - const render = - typeof frameloop === 'string' ? state.internal.render : frameloop?.render ?? state.internal.render - const maxDelta = - typeof frameloop === 'string' ? state.internal.maxDelta : frameloop?.maxDelta ?? state.internal.maxDelta - - const clock = state.clock + setFrameloop: (frameloop: Frameloop = 'always') => { + const clock = get().clock + // if frameloop === "never" clock.elapsedTime is updated using advance(timestamp) clock.stop() clock.elapsedTime = 0 @@ -287,7 +260,7 @@ export const createStore = ( clock.start() clock.elapsedTime = 0 } - set(() => ({ frameloop: mode, internal: { ...state.internal, render, maxDelta } })) + set(() => ({ frameloop })) }, previousRoot: undefined, internal: { @@ -303,34 +276,23 @@ export const createStore = ( // Updates active: false, frames: 0, - stages: [], - render: 'auto', - maxDelta: 1 / 10, priority: 0, subscribe: (ref: React.RefObject, priority: number, store: RootStore) => { - const state = get() - const internal = state.internal + const internal = get().internal // If this subscription was given a priority, it takes rendering into its own hands // For that reason we switch off automatic rendering and increase the manual flag // As long as this flag is positive there can be no internal rendering at all // because there could be multiple render subscriptions internal.priority = internal.priority + (priority > 0 ? 1 : 0) - // We use the render flag and deprecate priority - if (internal.priority && state.internal.render === 'auto') - set(() => ({ internal: { ...state.internal, render: 'manual' } })) internal.subscribers.push({ ref, priority, store }) // Register subscriber and sort layers from lowest to highest, meaning, // highest priority renders last (on top of the other frames) internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority) return () => { - const state = get() - const internal = state.internal + const internal = get().internal if (internal?.subscribers) { // Decrease manual flag if this subscription had a priority internal.priority = internal.priority - (priority > 0 ? 1 : 0) - // We use the render flag and deprecate priority - if (!internal.priority && state.internal.render === 'manual') - set(() => ({ internal: { ...state.internal, render: 'auto' } })) // Remove subscriber from list internal.subscribers = internal.subscribers.filter((s) => s.ref !== ref) } diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index e4bcd8e20b..a3ae0752de 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -41,7 +41,6 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( scene, onPointerMissed, onCreated, - stages, ...props }, forwardedRef, @@ -110,7 +109,6 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( performance, raycaster, camera, - stages, scene, // expo-gl can only render at native dpr/resolution // https://github.com/expo/expo-three/issues/39 diff --git a/packages/fiber/src/web/Canvas.tsx b/packages/fiber/src/web/Canvas.tsx index 89c5934c45..b88e0e4995 100644 --- a/packages/fiber/src/web/Canvas.tsx +++ b/packages/fiber/src/web/Canvas.tsx @@ -58,7 +58,6 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef(func scene, onPointerMissed, onCreated, - stages, ...props }, forwardedRef, diff --git a/packages/fiber/tests/__snapshots__/index.test.tsx.snap b/packages/fiber/tests/__snapshots__/index.test.tsx.snap index ee3995d0f8..5c2aaab5bd 100644 --- a/packages/fiber/tests/__snapshots__/index.test.tsx.snap +++ b/packages/fiber/tests/__snapshots__/index.test.tsx.snap @@ -25,11 +25,7 @@ Array [ "Events", "Extensions", "FilterFunction", - "FixedStage", "Frameloop", - "FrameloopLegacy", - "FrameloopMode", - "FrameloopRender", "GLProps", "GlobalEffectType", "GlobalRenderCallback", @@ -38,7 +34,6 @@ Array [ "InstanceProps", "Intersection", "Layers", - "LegacyAlways", "Loader", "LoaderProto", "LoaderResult", @@ -63,15 +58,10 @@ Array [ "RootState", "RootStore", "Size", - "Stage", - "StageTypes", - "Stages", "Subscription", "ThreeElement", "ThreeElements", "ThreeEvent", - "UpdateCallback", - "UpdateSubscription", "Vector2", "Vector3", "Vector4", @@ -106,6 +96,5 @@ Array [ "useLoader", "useStore", "useThree", - "useUpdate", ] `;