-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
loop.ts
159 lines (139 loc) Β· 5.2 KB
/
loop.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import * as THREE from 'three'
import { Root } from './renderer'
import { RootState, Subscription } from './store'
export type GlobalRenderCallback = (timeStamp: number) => void
type SubItem = { callback: GlobalRenderCallback }
function createSubs(callback: GlobalRenderCallback, subs: Set<SubItem>): () => void {
const sub = { callback }
subs.add(sub)
return () => void subs.delete(sub)
}
let i
let globalEffects: Set<SubItem> = new Set()
let globalAfterEffects: Set<SubItem> = new Set()
let globalTailEffects: Set<SubItem> = new Set()
/**
* Adds a global render callback which is called each frame.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
*/
export const addEffect = (callback: GlobalRenderCallback) => createSubs(callback, globalEffects)
/**
* Adds a global after-render callback which is called each frame.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
*/
export const addAfterEffect = (callback: GlobalRenderCallback) => createSubs(callback, globalAfterEffects)
/**
* Adds a global callback which is called when rendering stops.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
*/
export const addTail = (callback: GlobalRenderCallback) => createSubs(callback, globalTailEffects)
function run(effects: Set<SubItem>, timestamp: number) {
if (!effects.size) return
for (const { callback } of effects.values()) {
callback(timestamp)
}
}
export type GlobalEffectType = 'before' | 'after' | 'tail'
export function flushGlobalEffects(type: GlobalEffectType, timestamp: number): void {
switch (type) {
case 'before':
return run(globalEffects, timestamp)
case 'after':
return run(globalAfterEffects, timestamp)
case 'tail':
return run(globalTailEffects, timestamp)
}
}
let subscribers: Subscription[]
let subscription: Subscription
function render(timestamp: number, state: RootState, frame?: THREE.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
}
// Call subscribers (useFrame)
subscribers = state.internal.subscribers
for (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
}
export function createLoop<TCanvas>(roots: Map<TCanvas, Root>) {
let running = false
let repeat: number
let frame: number
let state: RootState
function loop(timestamp: number): void {
frame = requestAnimationFrame(loop)
running = true
repeat = 0
// Run effects
flushGlobalEffects('before', timestamp)
// Render all roots
for (const root of roots.values()) {
state = root.store.getState()
// If the frameloop is invalidated, do not run another frame
if (
state.internal.active &&
(state.frameloop === 'always' || state.internal.frames > 0) &&
!state.gl.xr?.isPresenting
) {
repeat += render(timestamp, state)
}
}
// Run after-effects
flushGlobalEffects('after', timestamp)
// Stop the loop if nothing invalidates it
if (repeat === 0) {
// Tail call effects, they are called when rendering stops
flushGlobalEffects('tail', timestamp)
// Flag end of operation
running = false
return cancelAnimationFrame(frame)
}
}
function invalidate(state?: RootState, frames = 1): void {
if (!state) return roots.forEach((root) => invalidate(root.store.getState()), frames)
if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never') return
// Increase frames, do not go higher than 60
state.internal.frames = Math.min(60, state.internal.frames + frames)
// If the render-loop isn't active, start it
if (!running) {
running = true
requestAnimationFrame(loop)
}
}
function advance(
timestamp: number,
runGlobalEffects: boolean = true,
state?: RootState,
frame?: THREE.XRFrame,
): void {
if (runGlobalEffects) flushGlobalEffects('before', timestamp)
if (!state) for (const root of roots.values()) render(timestamp, root.store.getState())
else render(timestamp, state, frame)
if (runGlobalEffects) flushGlobalEffects('after', timestamp)
}
return {
loop,
/**
* Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
*/
invalidate,
/**
* Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
*/
advance,
}
}