Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Don't create default camera, scene, and render loop #61

Closed
AndrewRayCode opened this issue Apr 7, 2019 · 16 comments
Closed

Comments

@AndrewRayCode
Copy link

I don't think this library should hard code a scene, camera, or render loop, for example hard coding the default camera to a perspectivecamera at position 0,0,5. I think this library should be more general than that. A Three setup can have 0 to many cameras of different types, 0 to many scenes, and itself doesn't start a requestAnimationFrame loop. These seem anti-React too, to hard code components into a library.

@drcmda
Copy link
Member

drcmda commented Apr 7, 2019

you don't have to use the default camera, you can useRender(..., true) and render your own scene and your own camera, at that point the defaults dont matter: https://github.com/drcmda/react-three-fiber#heads-up-display-rendering-multiple-scenes

Also the frameloop is optional, even the renderer: https://codesandbox.io/s/yq90n32zmx

But, this isn't what most people would go for. Auto frameloop is far more effective and it meshes well with other libs like react-spring, etc. I couldn't imagine a single reason why you'd want to be the one that calls requestAnimationLoop. There's no benefit, just downsides. useRender(..., true) essentially empties everything that would be done internally, letting you define your own update effects (camera.update(), etc) and render calls. But you could of course skip all of this and just use render/unmountComponentAtNode.

The thing about useRender is that it allows components to affect the render cycle in a uniform way, so that you can compose. This isn't easily possible in Threejs. Even simple stuff like

function Effects({ factor }) {
  const { gl, scene, camera, size } = useThree()
  const composer = useRef()
  useEffect(() => void composer.current.setSize(size.width, size.height), [size])
  // This takes over as the main render-loop (when 2nd arg is set to true)
  useRender(() => composer.current.render(), true)
  return (
    <effectComposer ref={composer} args={[gl]}>
      <renderPass attachArray="passes" args={[scene, camera]} />
      <glitchPass attachArray="passes" factor={factor} renderToScreen />
    </effectComposer>
  )
}

// Mount effects conditionally
{effectsActive && <Effects />

would mean so much complexity in Threejs. But here the effects component is self managed and controls its own render logic. But once it's unmounted everything goes back to normal. In Threejs your render loop would have to know about effects in order to switch it on/off, which creates a dependency you don't want to have.

@AndrewRayCode
Copy link
Author

AndrewRayCode commented Apr 7, 2019

I couldn't imagine a single reason why you'd want to be the one that calls requestAnimationLoop.

You don't need to imagine it, it's what I already do! 😃 I pause my animation loop on window blur, and on focus, start it up again and track the elapsed time delta. I also use the value passed to the rAF callback, which I can't do with the current loop because it's abstracted away.

In your example of manual looping, you have to drop into more imperative code to achieve it, which I'd prefer not to do if there's a declarative component that can do it (but that doesn't introduce default behavior). It also requires exposing the guts of the library to the user. React-three-render achieved this with a parameter to a trigger created callback.

@drcmda
Copy link
Member

drcmda commented Apr 7, 2019

I pause my animation loop on window blur, and on focus, start it up again and track the elapsed time delta

But can't you do this with the invalidate function? Im not so familiar with three-renderer, but that trigger looks familar.

<Canvas invalidateFrameloop={state.paused}>
  <SomeComponent />
</Canvas>

function SomeComponent() {
  const { invalidate } = useThree()

  useEffect(() => {
    // trigger a single frame, if invalidateFrameloop is true rendering is manual
    invalidate()
  }, [])

  return ...
}

There's also a universal one, which would render all canvases (if you had multiple) for a single frame.

import { invalidate } from 'react-three-fiber'

invalidate()

Another (perhaps cleaner?) way would be:

useRender(({ gl, camera, scene }) => {
  if (!state.pause) gl.render(camera.scene)
}, true)

I also use the value passed to the rAF callback, which I can't do with the current loop because it's abstracted away.

That's no problem, i'll add it.

@drcmda
Copy link
Member

drcmda commented Apr 7, 2019

The timetag is merged, next patch you can get it like this:

useRender((state, t) => ...)

@AndrewRayCode
Copy link
Author

AndrewRayCode commented Apr 9, 2019

I want to rAF outside of the canvas component because I need to update many UI elements surrounding the canvas as part of my game state, like overlayed vanilla html menus. I have a big game state object, I run it through one frame (like physics), then I pass all that data down to UI and <Canvas /> elements so everything updates, then I re-render.

I tried to recreate this scenario in a codesandbox, where <Game /> is that top level controlling component. I can't figure out why the renderFn() method on line 83 never gets set. It seems unlikely this is from a bug in r3f, but I'm stuck. Also do you have any input on this pattern? I think it suits my purpose of controlling the render loop, but I don't know if there's a simpler way.

@drcmda
Copy link
Member

drcmda commented Apr 9, 2019

You can try using the global invalidate, don't need to pass the local one up. Maybe that makes it easier.

import { invalidate } from 'react-three-fiber'

Here's the codesandbox with global invalidate: https://codesandbox.io/s/zn7o3z0k33

What i don't like about the current solution is that it seems to put React through the update loop with that useState trigger, it re-renders the Game component 60fps. I would avoid that at all cost. Even if some UI elements have to reflect something, can't this be reduced? Or written outside of React? Running React in a game loop is a bit controversial i think.

That's the whole purpose, if the rotation would be in useRender with a clean ongoing game loop it would be butter smooth. But "animating" that rotation through React will kill performance.

Personally i would prefer the loop to be in canvas. I'd communicate change to the surrounding UI in there, probably via animatable values (react-spring), because they don't re-render anything.

@AndrewRayCode
Copy link
Author

I'll try the global invalidate, I generally prefer to keep things "in the tree" so I'll also try to figure out why my demo isn't working.

What I do now for Charisma is steps a physics engine (p2.js) which updates all in-game positions, then I pass down those positions to the respective components as props. I loop over all "dynamic" entity data in one component and re-render each child, like <Player />, with the updated <Player position={physicsPosition} /> data. I'm new to hooks, is there a way to do this without passing props? I understand how the hooks work for fairly static scenes, like the demo scenes in this library, but I need to update most game data which affects many components at 60fps.

@drcmda
Copy link
Member

drcmda commented Apr 9, 2019

no, if you need realtime it will come down to prop passing. i just hope physics won't take too long to computer, because three + p2 + react + dom paint all in a 15ms window isn't much time to do things. If you use react-spring for positioning, it will at least be outside of react, you would pass down props once and change them in the parent, which informs the views that cling to it.

@AndrewRayCode
Copy link
Author

AndrewRayCode commented Apr 13, 2019

I've found two things:

My setup doesn't work if useRender() is called inside the scene component. As in this works:

      <Canvas>
        <ExposeContextToParent
          setRenderTrigger={setRenderTrigger}
        />
        <scene ref={sceneRef}>

but this doesn't:

      <Canvas>
        <scene ref={sceneRef}>
          <ExposeContextToParent
            setRenderTrigger={setRenderTrigger}
          />

Secondly, there doesn't appear to be a way to fully control the render loop? If I use invalidateFrameLoop and useRender(..., true) then setting props on my scene elements still triggers a re-render.

Are either of these bugs?

@drcmda
Copy link
Member

drcmda commented Apr 13, 2019

invalidateFrameLoop means that it only renders on prop changes or manual trigger (by calling invalidate). if you need a fully manual loop, perhaps we can expose some of the code around canvas and make it re-usable so that people can build whatever they like around the basic render/unmountComponentAtNode reconciler functionality. can i interest you in looking into it, making a PR?

@AndrewRayCode
Copy link
Author

I'm still struggling with this. I put my game state on the useRender callback state ref, and I can update my game state inside the first/"top level" useRender callback and things are working.

But now I want to click on a <button> outside my canvas and have it affect the game state. Because I don't have access to the context outside of the canvas I can't do useRender(). I need the opposite too, some action inside the canvas needs to affect state that the <button> uses.

Do you have a suggestion of how to approach this? Is there a way to move the <stateContext.Provider> higher up / into userland, so I can use useRender() outside of r3f? That way the existing renderloop logic could still run, but non-canvas components could play with the state as well, and branch if gl/camera aren't created yet

@drcmda
Copy link
Member

drcmda commented Apr 22, 2019

You could use a global effect. Check the exports, it’s among them. This can be done outside of canvas.

@linonetwo
Copy link
Contributor

linonetwo commented Jun 21, 2019

re-renders the Game component 60fps

Why is this bad? How to redraw things if you don't rerender react component? I also need a better way to do this.

I'm writing a binding to an ECS framework, upon drawing, I sync components to react, and trigger rerender:

https://github.com/linonetwo/react-encompass-ecs/blob/4748245cd85801918f55e2db2eaadec9a2ce9c9c/src/updator.ts#L6-L8

I can see reconciler takes a long time, while gl drawing is very fast.

@drcmda
Copy link
Member

drcmda commented Jun 21, 2019

See: https://twitter.com/0xca0a/status/1135465164504031233

And: https://twitter.com/0xca0a/status/1133329007800397824 (this one's using three-fiber)

It all depends on how big the rendering effort is. In edge cases you can use useRender, react-spring, or something like zustand. All three go outside of React.

@linonetwo
Copy link
Contributor

linonetwo commented Jun 21, 2019

Thanks, your example helps, I've already got the reference to component (the component in ECS) which gets transient updates by ECS framework (I guess "transient updates" means the reference to the data object is not changed so it won't trigger rerender, but the data inside object change rapidly, am I right?)

The key here is <mesh ref={mesh}> and useRender(() => mesh.current && mesh.current.rotation.set(...coords.current)), so the only way to avoid performance issue is update things imperatively. There is no way to fastly do declearative update.

Also, hope you can expand https://github.com/react-spring/zustand#transient-updates-for-often-occuring-state-changes to describe what does "transient updates" means...

@drcmda
Copy link
Member

drcmda commented Jun 21, 2019

well, yes, it means what you said. you are basically notified of a state change via callback and now you can apply updates to the component by directly mutating the view "transiently".

react-spring solves this in a purely declarative way. but it's focussed on animation, which probably isn't what you need. so zustand + useRender cuts a good middleground, because the api is formed in a way that suits useEffect, so everything gets cleaned up after the component unmounts.

also interesting, react will get something like this officially. dan abramov has mentioned this on twitter. something like a fast path-through mode where, if you agree to not change the structure of the view, you can blow props changes through React without diffing, which would be as fast as applying them natively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants