Skip to content

Conversation

almarklein
Copy link
Member

@almarklein almarklein commented Mar 23, 2023

New features:

  • Controller smoothing.
  • A system to configure the mouse and key bindings.
  • Key bindings can have 3 modes: apply a change (drag), apply change while it is pressed (peak), apply a change on each tick (push). The latter will be used by the fly camera.
  • Can can control the controller programmatically.
  • Multiple control actions can occur at once.

It needs some work:

  • Clean up.
  • Make tick-update take the passed time into account.
  • Docs.
  • Fix Orbit controller.
  • Example with custom controller config.
  • Example with controller but no external draw requests.
  • Fix before_draw (also in relation to viewports) in a seperate PR.

How does the controller work?

When an interaction starts, an action object is created. In some cases (e.g. dragging) the action's target value is updated as new events come in. There can be multiple actions simultaneously, e.g. you can zoom while panning. Though only one drag action is allowed at one time, otherwise that just gets weird.

During each "tick" a number of things happen:

  • The real action values are moved towards the target value. This is the smoothing part.
  • The action's corresponding update method is called. This updates a camera-state object using the action's value.
  • Then all cameras that the controller controls are updated with that state.

A bit more on that state object: the camera's state is obtained using camera.get_state(), and is kept as a private var on the controller. It is updated at each tick (possibly by multiple actions). This controller's internal state is used as long as there are active actions. This means that changes to the cameras (e.g. pushing a button to rotate it), have no effect while the controller is doing its thing. In practice the inertia from the smoothing is short, so this should not be a problem for changes invoked by user interaction.

So how is this kept up-to date? When an event occurs that changes the state, a new draw is requested (unless auto_update is false).

The tick mentioned above happens right before each draw, using a new event emitted by the renderer. The tick itself also requests a new draw when there are still active actions.

When you don't make use of controller.register_events(), this would work somewhat differently. We may need to change a few things around to make that work well.

@Korijn
Copy link
Collaborator

Korijn commented Mar 24, 2023

@berendkleinhaneveld we need to test this branch with your project before merging. I'm a little concerned it won't be compatible with your push-based render loop.

@almarklein almarklein marked this pull request as ready for review March 28, 2023 11:56
@almarklein almarklein changed the title Refactor controllers, WIP messy Make controllers configurable and apply damping Mar 29, 2023
@almarklein
Copy link
Member Author

almarklein commented Mar 29, 2023

This is ready (and no longer requires a change in wgpu-py).

@almarklein
Copy link
Member Author

I added an example with a very wide texture, and a camera config that only allows horizontal panning. Funnily enough, the horizontal-only panning revealed that the panning also moved vertically, due to a bug where we calculate the panning vectors.

@Korijn
Copy link
Collaborator

Korijn commented Mar 29, 2023

Another question: is it possible to disable damping?

@almarklein
Copy link
Member Author

Another question: is it possible to disable damping?

Yes, just set it to 0.

@Korijn
Copy link
Collaborator

Korijn commented Mar 29, 2023

In that case, I'd like a test from berend to see how well this works on his project before we merge.

@berendkleinhaneveld
Copy link
Contributor

I'll try it out, but it will have to wait till next week. Unless we are in a hurry to get this in?

@almarklein
Copy link
Member Author

I'll try it out, but it will have to wait till next week. Unless we are in a hurry to get this in?

No, not in a hurry.

BTW, once this is merged I'll add a Trackball controller (which is just a few lines) and a FlyController (which also should be easy now).

@Korijn Korijn mentioned this pull request Apr 4, 2023
10 tasks
@almarklein
Copy link
Member Author

@berendkleinhaneveld I added an explanation in the top post. If needed we can sit together to see how we can tie things together.

@Korijn
Copy link
Collaborator

Korijn commented Apr 5, 2023

So I discussed this with Berend and here's what would need to change in order for collagraph to retain full control over the update-render loop:

  • don't use the before_render event
  • don't run update_cameras
  • controller.auto_update = False
  • call handle_tick on regular intervals via event loop (e.g. every 16.7ms), independently from calling render
    • in that loop, copy controller._last_cam_state to camera state object (not the pygfx camera instance, but an observ camera state object)

There's two essential requirements here:

  • Controllers do not mutate cameras
  • Handle_tick is not triggered by before_render (this trigger prevents running state updates independently from draws)

@almarklein
Copy link
Member Author

don't use the before_render event
don't run update_cameras

Is the controller hooked up to the renderer events in the collagraph case? If not, a lot of the requirements are met, which means that we can keep the controller Just Work when using register_events, and with just a few tweaks provide the flexibility needed by e.g. collagraph.

call handle_tick on regular intervals

Would it work if we made handle_tick public, and leave it to the application? It could return the camera state, so the application can process it the way it wants.

@Korijn
Copy link
Collaborator

Korijn commented Apr 5, 2023

Is the controller hooked up to the renderer events in the collagraph case?

The controller is listening to mouse/keyboard events, but nothing else. I guess that's what you mean?

If not, a lot of the requirements are met, which means that we can keep the controller Just Work when using register_events, and with just a few tweaks provide the flexibility needed by e.g. collagraph.

Yes, I agree.

Would it work if we made handle_tick public, and leave it to the application? It could return the camera state, so the application can process it the way it wants.

Yes, that would work. We could also make the camera state a property, instead of returning it. I think that's a little more flexible.

@almarklein
Copy link
Member Author

The controller is listening to mouse/keyboard events, but nothing else. I guess that's what you mean?

How are these events fed into the controller? By calling handle_event() explicitly?

We could also make the camera state a property, instead of returning it. I think that's a little more flexible.

I'd rather not, because the internal camera state is only valid as long as any actions are active, and we'd have to figure out whether to e.g. set it to None when there are no actions, etc.

@almarklein
Copy link
Member Author

I just pushed a commit that I think should do the trick. The tick() method is not public. And when auto_update is False, the controller won't update the camera either.

@almarklein
Copy link
Member Author

I added the reactive example and adjusted it for the new controller.

@Korijn Korijn merged commit e87d379 into main Apr 8, 2023
@Korijn Korijn deleted the controllers branch April 8, 2023 21:04
@almarklein
Copy link
Member Author

Would this call not be necessary if controller tick was executed on a timer?

Exactly. Because of the inertia, it needs to be called somehow in addition to (mouse) events. A timer would work. At draw-time works too.

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

Successfully merging this pull request may close these issues.

3 participants