Tresjs / tres

Declarative ThreeJS using Vue Components
https://tresjs.org
MIT License
2.16k stars 102 forks source link

Multiple instances of `useRenderLoop` per page share state #252

Closed callumacrae closed 4 months ago

callumacrae commented 1 year ago

Is your feature request related to a problem? Please describe.

Use case is having multiple <TresCanvas> instances on one page which are individually pausable. Currently, pausing one instance results in all instances being paused.

Describe the solution you'd like

I think there's two possible solutions here:

Each approach has their own pros and cons. I think each <TresCanvas> could have its own problems when third party Tres components try to do their own thing and affect the main app behaviour. It's also harder to implement and possibly involves an API change!

Happy to contribute a fix, especially if the first solution is chosen as it's a lot easier :D

andretchen0 commented 7 months ago

Reviving this.

I agree with @callumacrae that each useRenderLoop should have its own state.

StackBlitz

Here's a StackBlitz showcasing the bug.

Context

Most uses I've come across of useRenderLoop are for animating attributes. In that context, calling pause() and then not rerendering, e.g., when the screen is resized is counter to my intuition.

It seems like we need a separate update loop.

Suggestions

useRenderLoop

New prop: :on-update-loop="(clock)=>{}"

Example

Here's a class-based code example from a game library (in the Haxe language) that implements these features. (It's from one of the guys who worked on the game Dead Cells, fwiw. And it's a really great library for cranking out 2D games.)

Here, Process.hx corresponds to something like our Object3D or a VNode. Like those, a Process is a node in a graph. It might have a parent and might have children. What we need is simple graph traversal.

Fwiw, Haxe compiles to JS to run in the browser. It does several tree traversals at 30 fps and 60 fps in the browser without a problem.

Fwiw, here's a post from the author about how tmod works.

andretchen0 commented 7 months ago

Another shared state issue here: #560

alvarosabu commented 6 months ago

Hi @callumacrae @andretchen0 to give a couple of thoughts regarding useRenderLoop

The original idea of the composable was to provide the user with an abstraction to replace the internal clock of ThreeJS that uses performance.now and make it somehow reactive with useRafFn which uses requestAnimationFrame, so any abstraction in the ecosystem (cientos, postprocessing, physics) can react to changes and animate their objects or scenes. Any Tres component inside a unique TresCanvas should have the exact same loop`

Ex: If I have 2 sub-components called SphereA.vue and SphereB.vue , both uses the onLoop which triggers every frame.

What's certainly a bug is that useRenderLoop should be distinct per TresCanvas, similar to #560 which I think would be easy to fix. Which I think is what you are mentioning in this thread @callumacrae

Multiple loops on the scene graph

Create a separate update loop that is distinct per user component

@andretchen0 The thing is, ThreeJS renders all the scene graph at once when using renderer.render(scene, camera) and that method is called with onLoop every frame. To achieve the level of granularity where you can pause individual components from rendering, we would need to create a onLoop instance per object, attach it to their localstate and then overwrite the onBeforeRender method of the THREE.object class to conditionally render based on the paused state https://threejs.org/docs/#api/en/core/Object3D.onBeforeRender

This is certainly something I'm not eager to do because it adds a level of complexity and possible side effects that would make it really difficult to mantain, but to consider it I would like to understand how the end user would benefit from having individual loops and if there are not end-user alternatives, for example

Imagine a scene with 100 of objects, all of them with event listeners to handle.

Most uses I've come across of useRenderLoop are for animating attributes. In that context, calling pause() and then not rerendering, e.g., when the screen is resized is counter to my intuition.

Wouldn't be easier to just not render the object with a v-if instead of using the individual onLoop to stop the rendering?

isPaused = ref(false)

<Box v-if="isPaused" />

Allow a user component to specify a speed multiplier on self and children – this is necessary if, say, we have a physics simulation we want to slow down, but we want to keep the GUI running at normal speed

Physics simulations are in the scope of the @tresjs/rapier package, but the speed modifier couldn't be just a ref on the end user component?

const speedFactor = ref(1)
onLoop(({ delta, elapsed}) => {
   boxRef.value.position.x += delta * speedFactor
})

Looking forward for your feedback

andretchen0 commented 6 months ago

Multiple loops on the scene graph

Moved my reply to the discussion here.

andretchen0 commented 6 months ago

A related issue of shared state that it would be handy to fix: "elapsed" is shared.

The value for "elapsed" starts ticking up whenever the first TresCanvas or subcomponent does useRenderLoop. It then shares that same "elapsed" value with other TresCanvases, even if they were created later.

StackBlitz showing the behavior.

alvarosabu commented 4 months ago

This functionality is now available by using the new useLoop composable which is bound to the state https://docs.tresjs.org/api/composables.html#useloop