pmndrs / use-cannon

👋💣 physics based hooks for @react-three/fiber
https://cannon.pmnd.rs
2.8k stars 156 forks source link

Provide way to query if the simulation is sleeping, or add invalidateFrameloop prop #49

Closed RobRendell closed 4 years ago

RobRendell commented 4 years ago

My app uses <Canvas invalidateFrameloop={true}/> in react-three-fiber to avoid rendering every frame, because most of the time the scene is static (it's a virtual gaming space for tabletop roleplaying games, and if no-one is moving anything around, there's no need to consume power by re-rendering the scene).

I'm adding physics-based dice rolling using use-cannon, but obviously when the physics simulation is running the app needs to re-render every frame. At the moment, the dice's render() methods call useFrame(({invalidate}) => {invalidate();}); so they cause the app to render every frame while they are mounted.

use-cannon defaults to useSleep={true}, so presumably when the die/dice settle, the physics simulation goes to sleep. However, I can't find a way to query the sleep state of the simulation from user-land, so my useFrame call can stop calling invalidate(). If such a getter is not available, I'd like to request that it be added (presumably to the WorkerAPI).

Given that your physics state is hidden away in the workers, it might be hard to make a getter of the sleep status available through the API. As an alternative, since invalidateFrameloop is a standard (if seldom used) feature of react-three-fiber, perhaps a similar prop could be added to <Physics/>, defaulting to false, but if true, use-cannon itself could call invalidate() each frame that the physics sim is not sleeping, and stop calling it when it sleeps.

drcmda commented 4 years ago

you're right, it should call invalidate. but it's a bit tricky atm because use-cannon itself is useFrame based. i did try to bring the mutation stuff into the main loop, but got into some scope problems, there's data which only the hooks have. but i think it could call invalidate in the main loop if it detects a change. @codynova is it possible to know outright if anything was moving without going through complex vector old-new checks?

codynova commented 4 years ago

The World's internalStep function always sets the dirty flag on the Broadphase, so as far as the Broadphase is concerned, the simulation is always running. I think the easiest way to check for movement would be something like this:

world.bodies.some(body => body.sleepState !== Body.SLEEPING)

which could be used in conjunction with sleepTimeLimit and sleepSpeedLimit for greater control.

drcmda commented 4 years ago

it seems to work, that was easier than i thought it would be :-)

codynova commented 4 years ago

Published in 0.2.8

drcmda commented 4 years ago

my only worry is, what if we're dealing with hundredes (thousands?) of objects. it would have to climb through that loop every frame. is there maybe a subscription at the body level we can use to flag active state globally?

codynova commented 4 years ago

There's no reason we couldn't add one. The World (and sometimes the Solver) already iterate through every body at least one on each frame.

What do you think makes the most sense? The most basic solution would be having bodies add/remove themselves from a World.activeBodies array (or Map), then just checking that length > 0.

I wonder what effect that would have on memory? Schteppe did quite a bit of memory optimization, so if there is a more memory-efficient solution I'd love to know!

drcmda commented 4 years ago

that sounds great! yeah, i tend to forget, we can change cannon now. :-D i think mem impact for a bunch of flags won't be a problem.

codynova commented 4 years ago

I was wondering more about the memory impact of the array of bodies, but I'm not really sure how that works since it's all pass-by-reference, I'd need to actually test it. I will take a stab at the solution I proposed above...

codynova commented 4 years ago

Newer frame validation which simply checks the length of World.activeBodies each frame by referencing getter World.hasActiveBodies released in 0.2.9

drcmda commented 4 years ago

nice one! thanks a lot

codynova commented 4 years ago

Happy to do it! There is probably a more efficient way, by assuming hasActiveBodies = false and checking for non-sleeping bodies in the World step - but it's a little trickier to figure out when it's safe to read a body's sleepState in the step. I may try to revisit it a little later, while keeping the forward-facing API the same (World.hasActiveBodies)...

RobRendell commented 4 years ago

You guys are amazingly quick! I get up this morning to find all this extra activity :)

When I started reading the reopening comments, my thought was that if you had memory concerns about the array of active bodies, then a simple counter would also work for detecting any activity... each body would increment it when it transitions from asleep to awake (or when it's added in an awake state) and decrement it when it transitions from awake to asleep.

However, I imagine the array of active bodies will be useful for other optimisations too.

codynova commented 4 years ago

@RobRendell That's a good idea. The counter is closer to what I was imagining doing in the World step. I think we could push it one step further. Rather than keeping a count, just check the body's sleepState in the World step and switch the flag to true if any body is awake - otherwise default to false. The counter would be easier to implement because we can simply call it from the body's wakeUp or sleep methods. The flag is probably (slightly) more performant, but it requires being more careful during the integration with the World step method. I'm wondering if we can come up with any other potential uses for the activeBodies - I can't think of a good reason to keep it.

codynova commented 4 years ago

Revisit for performance improvements

RobRendell commented 4 years ago

Well, presumably you only need to iterate over the active bodies when simulating updates? If the scene has lots of sleeping physics objects, not having to iterate over the entire list would be more efficient. I imagine that in "real world" type scenes, most physics objects are at rest...

codynova commented 4 years ago

The World already has to iterate over every body multiple times in each step to do integrations, solve contact equations, evaluate and alter waking state, etc.

codynova commented 4 years ago

The new method of updating flags in the World step was released in cannon-es@0.8.7 and use-cannon@0.2.10. I'll wait to close this time until we confirm it's working properly :)

codynova commented 4 years ago

Seems to work fine based on testing in CompoundBody demo

RobRendell commented 4 years ago

@codynova Sorry to bug you yet again, but this appears to have regressed (checked with both versions 0.2.11 and 0.3.0). The invalidate function is being called all the time, even after the bodies are sleeping. I even checked by adding a useFrame() callback to the CompoundBody demo, and the callback continues to be invoked, apparently indefinitely.

codynova commented 4 years ago

@RobRendell Do you mind creating a minimum viable example codesandbox? I just tested locally with invalidateFrameloop on the Canvas and a callback useFrame(() => console.log('test')) - and it seems to work fine, unless I'm just missing something

drcmda commented 4 years ago

maybe you forgot to build? that is, if you're working with the examples repo. that happens to me all the time. simply updating it wont do.

RobRendell commented 4 years ago

I didn't find the examples repo, so I pulled this repo and copy/pasted the CompoundBody demo into my project.

However, I discovered that the problem was with my configuration - I misunderstood the sleepTimeLimit parameter, and assumed it was in milliseconds. The simulation was able to sleep, I just wasn't waiting long enough :)

Apologies for the false report! Thank you so much for being so responsive!