Hard FPS lost when movin meshes

I’m facing a very weird error that my FPS drastically drops when I don’t remove the beforeRenderObsevable listener.

I’ve tried to simulate the error on the playground without success so I’ll provide the package: GitHub - Alecell/catrun

The whole point is on /src/scenes/scene-1/obstacles/obstacles.ts at the method createObstacle I have a onBeforeRenderObservable, the purpose of this is to move the obstacle across the screen, but if I comment the line 53 after the first one obstacle I’m facing a hard FPS lost. Also, if I comment the movement line (obstacle.position.x += -0.05 * animationRatio;) the FPS do not drop anymore.

I thought that was because I’m generating too much onBeforeRenderObservable calls (since it’s called inside another onBeforeRenderObservable) so I moved this to the class constructor and created an array of obstacles, having only one onBeforeRenderObservable that inside it I iterate the array, moving the obstacles, but even with this I’m facing the FPS lost and I can’t find why.

The new class would look like this:

import { Mesh, MeshBuilder, Scene } from '@babylonjs/core';
import { IObstacles } from './types';

export class Obstacles implements IObstacles {
  private prevTime = new Date().getTime();
  private currentTime = new Date().getTime();
  private trySpawnTime = new Date().getTime();
  private trySpawnTimeout: number = 0;
  private obstacles: Mesh[] = []

  constructor(
    private scene: Scene, 
    private player: Mesh,
  ) {
    this.scene.onBeforeRenderObservable.add(() => {
      const animationRatio = this.scene.getAnimationRatio();

      this.obstacles.forEach(obstacle => {
        obstacle.position.x += -0.05 * animationRatio;

        if (this.player.intersectsMesh(obstacle)) {
          obstacle.dispose(true);
        }
      })
    });
  }

  private canSpawn(minDelay: number, maxDelay: number) {
    const isMaxDelay = this.currentTime - this.prevTime > maxDelay;
    const isMinDelay = this.currentTime - this.prevTime > minDelay;
    const canTry = this.currentTime - this.trySpawnTime > this.trySpawnTimeout;
    let shouldSpawn = false;

    if (canTry) {
      this.trySpawnTime = this.currentTime;

      if (isMinDelay) {
        shouldSpawn = Math.random() > 0.5;
        
        if (isMaxDelay) shouldSpawn = true;
      }
    }

    if (shouldSpawn) this.prevTime = this.currentTime;

    return shouldSpawn;
  }

  private generateObstacle() {
    return MeshBuilder.CreateBox('obstacle', { width: 0.1, height: Math.random(), depth: 1 })
  }

  private createObstacle() {
    const obstacle = this.generateObstacle();
  
    obstacle.position.z = 3.55;
    obstacle.position.y = -0.75;
    obstacle.position.x = 1.5;

    this.obstacles.push(obstacle);
  }

  spawnWithDelay(minDelay: number, maxDelay: number) {
    this.currentTime = new Date().getTime();
    this.trySpawnTimeout = Math.floor(minDelay / 4);

    if (this.canSpawn(minDelay, maxDelay)) {
      this.createObstacle();
    }
  }
}

The weirdest thing about this is if comment the line that makes the movement (obstacle.position.x += -0.05 * animationRatio;) the FPS don’t drop anymore.

Does anyone know why the FPS are falling just because of the movement?

Hi!
I don’t know what exactly can be the case here. However you should remove the obstacle from this.obstacles when you dispose it.

I’ve created a small PG to check whether there is a bug in the intersects method which can be the most CPU consumpting task here. BJS 5 is in development so we should be aware of every possibility:

It’s not the case. 144 FPS.

Is it performing poorly on 4.2.1 as well? EDIT: Sorry just had a look at the repo. You are on 4.2.1.

I’ll examine your code thoroughly and try to reproduce the issue.

2 Likes

Heya, one suggestion is to try upgrading to V5 because the code to compute the world matrix is much faster now. Alternatively you could try moving half or a fourth of them per frame, for example, to limit the amount of matrices needed to be computed each frame.

Since all of the meshes are moving by the same amount, another solution could be to use a solid particle system so that there’s only one mesh to move and so only one matrix to compute. Also SPS can help with batching the updates if you need to move them by different amounts.

Also the profiler can be very helpful in these situations to see where the time is being spent.

2 Likes

He uses plain boxes and the player mesh only. It must perform veeery fast! So what will be the issue here? :slight_smile:

Do you register only one observer? Don’t you have many of them? How many boxes are on the scene?

I had FPS issue with lots of boxes on v4 once upon a time and end upping finding the issue through profiling and found that hacking computeWorldMatrix to be more efficient solved the issue for me. That’s why I think updating to v5 could help fix the issue, it has for me in similar projects… :slight_smile:

@Alecell I’ve just tried your code from the repo. It runs at 100 FPS / 4k:

@blake look at the values, everything seems to be ok. Very low values for the frame steps duration.

There is actually one box at a time on the scene :stuck_out_tongue: The one in the middle.

It looks like that’s not a repro of the issue then. The problem only arise when there are more obstacles right?

@blake @Alecell Didn’t try to comment the line at line 53 yet though, pardon me :smiley:
EDIT: @Alecell ok actually what’s the point of not removing the observer here? That’s the case I was asking about. You must clean up your observers because they will stack up.

He is not removing the observers so they stack up and the system obviously dies :cry:

2 Likes

But there are only few meshes. It must work with BJS 4.2.1. I’ll dig deeper :sunglasses:

1 Like

But I still don’t understand why it would be wanted to keep running code that updates a disposed mesh’s position and checks it for intersections before calling dispose again… Isn’t the solution just to not comment that line out and remove the observer? I don’t understand what problem commenting out that line is trying to solve I guess…

@Alecell I’ve found a couple of flaws in your code. Fixing. However I already rewrote your code so it is not dropping FPS anymore:

obstacles.ts

import { Mesh, MeshBuilder, Nullable, Observer, Scene } from "@babylonjs/core";
import { IObstacles } from "./types";

export class Obstacles implements IObstacles {
  private prevTime = new Date().getTime();
  private currentTime = new Date().getTime();
  private trySpawnTime = new Date().getTime();
  private trySpawnTimeout: number = 0;

  constructor(private scene: Scene, private player: Mesh) {}

  private static _Obstacles: Mesh[] = [];
  private static _Observer: Nullable<Observer<Scene>>;

  public static Start(scene: Scene, player: Mesh) {
    Obstacles._Observer = scene.onBeforeRenderObservable.add(() => {
      Obstacles._Obstacles.forEach((obstacle, idx) => {
        const animationRatio = scene.getAnimationRatio();
        obstacle.position.x += -0.05 * animationRatio;

        if (player.intersectsMesh(obstacle)) {
          obstacle.dispose(true);
          Obstacles._Obstacles.splice(idx, 1);
        }
      });
    });
  }

  public static End(scene: Scene) {
    // remove observer
    if (Obstacles._Observer) {
      scene.onBeforeRenderObservable.remove(Obstacles._Observer);
    }
  }

  private createObstacle() {
    const obstacle = this.generateObstacle();

    obstacle.position.z = 3.55;
    obstacle.position.y = -0.75;
    obstacle.position.x = 1.5;

    Obstacles._Obstacles.push(obstacle);
  }

  
  private canSpawn(minDelay: number, maxDelay: number) {
    const isMaxDelay = this.currentTime - this.prevTime > maxDelay;
    const isMinDelay = this.currentTime - this.prevTime > minDelay;
    const canTry = this.currentTime - this.trySpawnTime > this.trySpawnTimeout;
    let shouldSpawn = false;

    if (canTry) {
      this.trySpawnTime = this.currentTime;

      if (isMinDelay) {
        shouldSpawn = Math.random() > 0.5;

        if (isMaxDelay) shouldSpawn = true;
      }
    }

    if (shouldSpawn) this.prevTime = this.currentTime;

    return shouldSpawn;
  }

  private generateObstacle() {
    return MeshBuilder.CreateBox("obstacle", {
      width: 0.1,
      height: Math.random(),
      depth: 1,
    });
  }


  spawnWithDelay(minDelay: number, maxDelay: number) {
    this.currentTime = new Date().getTime();
    this.trySpawnTimeout = Math.floor(minDelay / 4);

    if (this.canSpawn(minDelay, maxDelay)) {
      this.createObstacle();
    }
  }
}

index.tsx
Insert at line 30:

  Obstacles.Start(scene, player)
3 Likes

Here you go! :vulcan_salute: Live long and prosper!

catrun.zip (1.2 MB)

EDIT: Feel free to ask after you have examined the code.

EDIT2: I’ve just solved the loading of the landscape. I don’t know what’s your purpose with it, but your code was throwing an exception and didn’t load it at all.

There wasn’t a point exactly on don’t remove the obstacle nor the observable I was just testing things when I fall on this error and I wanted to understand it :sweat_smile:

But why when I put the onBeforeRenderObservable on the constructor I still with the error? I mean, adding it on the constructor wasn’t suppose to avoid the stacking, adding only one observer? :thinking:

(sorry for the late response, I was on a meeting)

No worries, we are enjoying helping the lost souls here :stuck_out_tongue:

Getting of the animation ratio should be moved one level up. Anyway in all the examples I saw until now you’re the first one who uses this method. Congratulations!

Many demos are not playable on higher framerates because the dudes are not using it. :sunglasses: I use it in all my projects as well.

1 Like

Hey @roland I didn’t provided the Solution badge because the biggest question was more of a why things got the FPS lost, you said that was because of the huge stacking of the onBeforeRenderObservable but if I do the example below I’m not stacking them, but got the error anyway.

Do you know why? :thinking:

Here I add the onBeforeRenderObservable inside the constructor and I generate the object only one time, so, despite the several code mistakes, I believe I’m not stacking, am I?

import { Mesh, MeshBuilder, Scene } from '@babylonjs/core';
import { IObstacles } from './types';

export class Obstacles implements IObstacles {
  private prevTime = new Date().getTime();
  private currentTime = new Date().getTime();
  private trySpawnTime = new Date().getTime();
  private trySpawnTimeout: number = 0;
  private obstacles: Mesh[] = []

  constructor(
    private scene: Scene, 
    private player: Mesh,
  ) {
    this.scene.onBeforeRenderObservable.add(() => {
      const animationRatio = this.scene.getAnimationRatio();

      this.obstacles.forEach(obstacle => {
        obstacle.position.x += -0.05 * animationRatio;

        if (this.player.intersectsMesh(obstacle)) {
          obstacle.dispose(true);
        }
      })
    });
  }

  private canSpawn(minDelay: number, maxDelay: number) {
    const isMaxDelay = this.currentTime - this.prevTime > maxDelay;
    const isMinDelay = this.currentTime - this.prevTime > minDelay;
    const canTry = this.currentTime - this.trySpawnTime > this.trySpawnTimeout;
    let shouldSpawn = false;

    if (canTry) {
      this.trySpawnTime = this.currentTime;

      if (isMinDelay) {
        shouldSpawn = Math.random() > 0.5;
        
        if (isMaxDelay) shouldSpawn = true;
      }
    }

    if (shouldSpawn) this.prevTime = this.currentTime;

    return shouldSpawn;
  }

  private generateObstacle() {
    return MeshBuilder.CreateBox('obstacle', { width: 0.1, height: Math.random(), depth: 1 })
  }

  private createObstacle() {
    const obstacle = this.generateObstacle();
  
    obstacle.position.z = 3.55;
    obstacle.position.y = -0.75;
    obstacle.position.x = 1.5;

    this.obstacles.push(obstacle);
  }

  spawnWithDelay(minDelay: number, maxDelay: number) {
    this.currentTime = new Date().getTime();
    this.trySpawnTimeout = Math.floor(minDelay / 4);

    if (this.canSpawn(minDelay, maxDelay)) {
      this.createObstacle();
    }
  }
}

EDIT: I forgot to mention that, on this example also, if I comment obstacle.position.x += -0.05 * animationRatio; the FPS don’t drop too

Ok, I get the point. However a Half solution badge would be cool :smiley:
Can you make a video like I did where I can see the issue? This is running using your code from the previous post.

https://misc.babylonjs.xyz/catrun.mkv