react-spring: @react-spring/three useSpring stops updating in XR session

🐛 Bug Report

Springs stop updating when starting an active XR session.

Not sure if this a bug related to @react-spring/three, @react-three/fiber, @react-three/xr or a combination of all three.

To Reproduce

  1. Load repro link below on an Oculus Quest 2
  2. The mesh changes scale on a timer ever 1s - this is animated with a spring
  3. Start a VR session
  4. The mesh stops animating scale and stays at the scale it was when the xr session was started

Expected behavior

The mesh should continue to animate scale inside the XR session.

Link to repro (highly encouraged)

https://codesandbox.io/s/react-xr-react-spring-bug-fkzt3?file=/src/index.tsx

Environment

  • @react-spring/three >= v9.0.0
  • @react-three/fiber >= v6.0.3
  • @react-three/xr >= v2.1.0
  • react v17.x

Works as expected with the following old versions:

  • @react-spring/three v9.0.0-rc.3
  • react-three-fiber v5.x.x
  • @react-three/xr v2.0.1

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 16 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Also, great investigation so far! ⭐

I spent a few hours remote debugging my Oculus Quest 2 browser. This is what I could see:

No active XR session

package function is executed?
react-spring/three FrameLoop.advance YES
react-spring/shared FrameLoop.advance YES
react-spring/core SpringValue.advance YES

In active XR session

package function is executed?
react-spring/three FrameLoop.advance YES
react-spring/shared FrameLoop.advance NO
react-spring/core SpringValue.advance NO

I’m not familiar with the internals of react-spring so I’m not sure what that means. I was a bit surprised to see separate frame loop logic for each target tbh. And I’m not sure I understand where the shared FrameLoop gets called from the three FrameLoop.

Hope this helps nail down the issue. I will wait for someone who actually knows the internals of react-spring to comment 😅

Cool, so after some digging this weekend, it turns out that r3f was not driving the animation of react-spring when using @react-spring/three. So that made it easier to remove the FrameLoop class from @react-spring/three.

I then looked at the code for rafz and there’s only two places we call the native RequestAnimationFrame in the start function and the loop. Taking inspiration from the work I did with react-three-fiber around frameLoop and when to call it, i’ve decided to pull this into rafz so that @react-spring/three will change rafz.frameLoop to demand, this will mean nothing using rafz will run unless we manually advance the timer, we can do this by adding an effect function to r3f, since r3f chooses the correct loop function depending on if its xr or not, this should work. Keep eyes pealed for the PR into rafz and the fix for react-spring!

Edit: I want to say a real thank you to @ffdead, great research helped isolate the issue and wrote up incredibly clearly so I could think about how to solve it 🙌🏼

this has been released on v9.2.2 in theory it should all be working, but we can reopen if it’s not 🙏🏼

Thanks for the research! It’s very helpful ⭐ it’s also not easy to resolve (but when is it?).

So it feels like step1 is getting everything to use a set global AnimationLoop function e.g in the context of web this would be rafz. Step 2 would be to be able to swap this default global AnimationLoop function out for a func that can run as an effect in r3f…

Ok, here’s what I think is going on:

The main issue

  • window.requestAnimationFrame doesn’t run in an XR session on the Oculus Quest browser. This is expected as we need to use WebGLRenderer.setAnimationLoop to run animations in XR.

  • The three target’s FrameLoop is not being used at all currently. The solution is to let r3f drive the frame loop which uses WebGLRenderer.setAnimationLoop internally. The three target’s FrameLoop is hooked up to r3f via addEffect properly, but the FrameLoop itself is not being used to drive the springs.

  • Globals.assign({ frameLoop }) is a noop because:

    1. shared Globals don’t actually contain an assign or export for frameLoop
    2. All components that use the FrameLoop import it from import { frameLoop } from '@react-spring/shared' instead of something like Globals.frameLoop
    3. Some components like withAnimated use import { raf } from '@react-spring/shared' directly without using the FrameLoop to schedule the frames.

Temporary workaround

My temporary workaround is to monkey patch the three target to run rafz frames via r3f addEffect. I didn’t find a way to request only 1 frame from r3f so I cache the current frame callback in a temporary variable.

// YourApp.js
import { addEffect } from '@react-three/fiber'
import { Globals } from '@react-spring/shared'
let nextFrame: Function | undefined = undefined
addEffect(() => {
  if (nextFrame) nextFrame()
  return true
})
Globals.assign({
  requestAnimationFrame: (cb) => (nextFrame = cb)
})

Workaround sandbox

Problems with the workaround

  • Only one frame callback is saved. This might cause issues if we have multiple rafz frames scheduled for each r3f frame, but hopefully, that’s not happening.

  • r3f invalidate needs to be called manually while springs are active. It’s important that apps can stop the r3f render loop when not needed to preserve battery / performance by passing <Canvas frameloop="demand"/>

  • the rafz internal loop seem to run all the time which would cause r3f to invalidate continuously if we did something like this:

requestAnimationFrame: cb => {
 nextFrame = cb;
 invalidate();
},

Conclusions

  1. The three target’s FrameLoop is not being used currently so it could be removed. The issue mentioned with three frame loop should not have been solved by adding the current FrameLoop, is the issue still there?
  2. All components that need a frame should go via the FrameLoop - never use rafz directly since it relies on window.requestAnimationFrame.

This feels like fundamental change so I suspect a core maintainer would be better suited to make the necessary changes.

Side note: a friend has one I was supposed to borrow a while back! annoying I never got it.

I’m really sorry, I simply can’t help actually look into this issue. I would be surprised if this were an issue with react-spring because it works fine with regular WebGL, i’m also happy to be proven completely wrong.

You could open an issue in react-xr sure! Equally, if you were interested in looking into I’m happy to support you however I can // answer questions etc. 👍🏼