1

I'm looking for advice on updating the JavaFX scene graph while an animation is running. What I have is a custom level of detail node in a 3D scene that loads and creates a MeshView in a background thread. That's fine, it works without affecting the JavaFX application thread. But the issue is that when I update the node (I actually just replace a child in a Group node) that is live in the scene graph like this (running this part on the JavaFX application thread):

Group group = (some value that is a live node in the scene graph ...)
group.getChildren().set (0, response.view);   <-- response.view is my MeshView instance

it works, but a large number of changes like this being pushed to the scene in a small span of time makes the animation of the 3D scene shudder. I know why it's happening -- the pulse is doing a lot of work to update the scene graph to the graphics card. But I'd like some advice and/or code samples on how best to handle this. One thing I can think of would be to use a producer/consumer model where the scene graph changes are produced and placed into a queue, and then the consumer would only consume so many at a time. But there are times when the animation stops and isn't running anymore, and the scene graph changes can be pushed to the scene as fast as they become available.

Is there an example of handling this well online somewhere that I haven't found? Or some standard practices / solutions that are useful? Basically it's like I want to ensure the frame rate of the animation and only push changes at a rate that can be handled without disrupting the animation rate, but I have no idea how to determine what rate that would be. I don't know how to measure the length of time that each of my scene graph modifications are actually taking behind the scenes, or the rate of pulses that are falling below the usual 60 Hz so that I can throttle back my impact if needed.

  • 1
    Many people have already complained about the fact that all scene graph updates are very time consuming. I've done a similar thing as you proposed for Canvas updates where you basically have the same problem. But these are all hacks which try to get around the real problem. If you are in the mood you could do the community a favour and investigate the real cause of the underlying problem. That might improve the performance of the platform as a whole. – mipa Jun 27 '20 at 16:50
  • I did a little bit of research into what's causing the delays when the scene graph is changed. It seems that the JavaFX engine has to flush the changes to the hardware rendering side of things and that just takes some amount of time. I believe it's the case that no matter what kind of optimizations are done, there will always be a threshold N for example at which performing more than N changes to the scene graph requires greater than the default animation pulse of 1/60 seconds. During some animation pulses, I perform just a few scene graph changes, but during others I perform hundreds. – Peter Hollemans Jun 27 '20 at 17:24
  • I believe I've come up with a strategy to meter out the scene graph changes that I need, I'll post when I have something working. – Peter Hollemans Jun 27 '20 at 17:25

1 Answers1

1

So I've got something that works well. Whenever I need to perform a scene graph update and I don't know if an animation is running or not (because I'm in another class), I wrap the scene graph update in a Runnable that's accepted by a Consumer:

private Consumer<Runnable> updateConsumer;
...
updateConsumer.accept (() -> group.getChildren().set (0, response.view));

Then in the class that is the final destination of the consumer, I add the scene graph update to queue:

private ConcurrentLinkedQueue<Runnable> sceneGraphChangeQueue;
...
public void addSceneGraphChange (Runnable change) {
  sceneGraphChangeQueue.add (change);
}

When an animation is being performed, I watch a variable that I know is changing for each step of the animation, and attach a listener to it so that it can track the animation is happening. In my case, I'm tracking my camera position and I hold onto the count of camera position updates since I last reset the counter (which I'll explain the purpose for below):

private int cameraUpdatesSinceReset;
...
view.cameraPositionProperty().addListener ((obs, oldVal, newVal) -> {
  cameraUpdatesSinceReset++;
  performSceneChanges (SCENE_CHANGES_PER_PULSE);
});
...
private void performSceneChanges (
  int limit
) {

  int count = 0;
  while (!sceneGraphChangeQueue.isEmpty() && count < limit) {
    Runnable change = sceneGraphChangeQueue.remove();
    change.run();
    count++;
   } // while

  if (count != 0) LOGGER.finer ("Performed " + count + " scene graph changes");

} // performSceneChanges

So that lets the scene graph changes trickle through during each animation pulse, and I found that setting SCENE_CHANGES_PER_PULSE=2 was about all my machine can handle before the animation starts to shudder. Finally, to make sure the scene change queue is flushed when there are no animations going on anymore, I poll the queue periodically with a ScheduledService and check in on my cameraUpdatesSinceReset counter:

ScheduledService<Void> service = new ScheduledService<Void>() {
  protected Task<Void> createTask() {
    return new Task<Void>() {
      protected Void call() {
        Platform.runLater (() -> {
          if (cameraUpdatesSinceReset == 0) {
            performSceneChanges (Integer.MAX_VALUE);
          } // if
          else cameraUpdatesSinceReset = 0;
        });
        return (null);
      } // call
    };
  } // createTask
};
service.setPeriod (Duration.millis (333));
service.start();

The service polls the queue three times per second, which is enough to have 20 pulses pass by and if the camera hasn't updated in that time, I assume there's no animation or user interaction going on. The result is that the animation is smooth and the system does as many scene graph updates as it can when it can.