Best approach for fading in/out of materials with a dynamic scene?

Hello wonderful BJS community,

I have project where the scene is made up of meshes which can change material properties dynamically at runtime (alpha & color + shader specific properties like scrolling uvs etc), often fading in or out, and we use a variety of shaders made via the NME that are implemented as NodeMaterials at runtime.

To get suitable rendering & alpha sorting order we’ve been swapping between 2 material variants (regular and fade) on each mesh, where if the object has a final alpha value less than 1, we switch to the fade material variant for the mesh. And we perform the opposite if the alpha value reaches 1/opaque.

Here’s an example of the material settings we have set differently between the regular and fade variants.

    const transparentMaterial =
        loadingFadeVariant ||
        isFlowShader ||
        shaderFilePath.includes('Glass')
      if (transparentMaterial) {
        nodeMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHATESTANDBLEND
        nodeMaterial.alphaMode = BABYLON.Engine.ALPHA_COMBINE
        nodeMaterial.needDepthPrePass = !isFlowShader
        nodeMaterial.separateCullingPass = true
        nodeMaterial.useAlphaFromDiffuseTexture = true
      } else {
        nodeMaterial.transparencyMode = BABYLON.Material.MATERIAL_OPAQUE
        nodeMaterial.alphaMode = BABYLON.Engine.ALPHA_DISABLE
        nodeMaterial.needDepthPrePass = false
        nodeMaterial.separateCullingPass = false
        nodeMaterial.disableDepthWrite = false
      }

What we’re finding is that we suffer poor FPS drops whenever we trigger the dynamic changes to the scene and material swapping takes place, and then once everything is recalculated/changed the FPS speeds up again.

We already during a loading screen, call material.forceCompilationAsync(...) on each material used in the scene to help avoid run-time stalls. Our meshes and material properties are fairly unique per use case so my understanding is that material and/or mesh instancing isn’t useful. As is material freezing because of the alpha/color property changes.

So my question is, for a fairly dynamic scene with fading in/out of meshes, and glass/transparency usage on various meshes, what would you recommend as the best approach to managing materials and scene state to achieve optimum performance out of BJS?

1 Like

Will bring @Deltakosh and @sebavan on board for this one :slight_smile:

I did something similar in a project but not with node materials. However, this might apply to you as well.
From the above, I understand that it is when you perform the swap of materials that you get the lag.
What about creating an instance or clone of mesh, and not switch material, simply bringing down one to alpha 0 while the other goes to alpha 1? Of course, by saying this, I don’t know just how many meshes are affected in your project.

Using render pass ids may be what you need: Render pass ids | Babylon.js Documentation

In your case, you would need to create two different materials instead of modifying the existing one. Then, you can use AbstractMesh.setMaterialForRenderPass(material, id) to set the right material for the right render pass id (which probably is scene.activeCamera.renderPassId).

3 Likes

Smart use for it :slight_smile: @Evgeni_Popov

Thanks for all the thoughtful replies everyone.

@Evgeni_Popov , I read this render pass tech before I asked the initial question and didn’t think it was applicable to my use case, as I wanted to have user controlled fading of some objects in the scene, while keeping other opaque. But after re-reading the docs and playground code I see that when a non-default render pass is in use, and the mesh doesn’t have a material for that render pass, it falls back to the default render passes material. This is a key/important point.

So with this in mind I made a modification to the doco example playground trying out this approach: https://playground.babylonjs.com/#6XIT28#2279

Even though I’m setting/unsetting a material for a render pass on 4000 boxes it’s still pretty quick. But I wonder if this is because there’s only 2 materials in use here.

Also I do see that my PG and even the one in the documentation that I extended from, has a second or two FPS drop when the very first change in render pass takes places

camera.renderPassId = renderPassSand;

and then in subsequent toggling this FPS drop isn’t observed. Why is this?

From further testing, the issue appears directly linked to the number of cubes/objects I have in the viewport when I trigger the initial change to the sand material (mat2). If I have all 8000 cubes, I get the initial FPS drop. But if I have 2000 or less I don’t see the drop in FPS. 8000 cubes is 96000 triangles. Which isn’t what I would thought is a really heavy load in raw triangles alone.

That’s because the first time a material is in use by a mesh it must be compiled for that mesh, there’s no way around that.

What you can try is pre-compiling the materials beforehand (at program starting), so that you don’t experience the hiccup at the time you switch materials. To do that, you should set the current render pass id to the right one (engine.currentRenderPassId = id) and then loop over the meshes and call mat.forceCompilation(mesh). Don’t forget to reset engine.currentRenderPassId to the value it had before you changed it!

Thanks for the suggestion. I’ve coded it up in the PG example I have, but I’m not seeing any noticeable difference/improvement in the initial material swap speed.

Here’s my updated PG: https://playground.babylonjs.com/#6XIT28#2280

Is this the approach you had in mind (lines 46-51)?

Yes that’s what I had in mind but I would do it like this instead, else you may have some code that executes with the wrong engine.renderPassId because forceCompilation() is using a setTimeout to execute some code in case some resources are not available right away:

However, it does not work fully because:

  • for the PBR materials, the forceCompilation method is not updating the subMeshes with an up to date DrawWrapper, it is simply creating/compiling the effect. So, when rendering for the first time the sub meshes there are still some work to be done, which takes a little time
  • for the standard materials, the forceCompilation method (in Material.forceCompilation) is creating a temporary sub mesh instead of using the one(s) from the mesh (I don’t know why), so there’s the same problem than with the PBR materials, the DrawWrapper is not created for the sub mesh(es) at that time but when rendering the sub mesh(es) for the first time.

I will let @sebavan / @Deltakosh deal with those ones as it could be a bit involved to fix them (or at least we must be sure of the fixes - and also because I’m in holidays :slight_smile: ).

2 Likes

Thank you.

In my project I use the async approach for material forceCompilation (although not with a render pass approach) and I believe I run into the same problems on first time rendering still as in your updated PG there. So It would be great if I could get this issue solved somehow so I can do all my loading/preparing behind a loading screen and then get a nice smooth user experience. I am happy to contribute coding time if I’m pointed at the right place/approach to take for the solution to this problem.

Force compilation is here to only speed up the shader compilation not the full process.

In your case checking for the full scene readyness might be the best.

You could try smthg like this:

// Force compile materials for "sand" render pass...
    console.log("compiled materials start");
    engine.currentRenderPassId = renderPassSand;
    await scene.whenReadyAsync(true);
    console.log("First pass done");
    
    engine.currentRenderPassId = renderPassCO;
    await scene.whenReadyAsync(true);
    console.log("Second pass done");

    console.log("compiled materials finished");
1 Like

I think @sebavan forgot to save the PG with the changes. Here’s the updated PG with his changes:

aHaH yes I forgot … Thanks a lot :slight_smile:

Thank you for this. Using the scene.whenReadyAsync during my scene loading, which runs through the 2 permutations of the regular and fade materials per mesh that we use, has made a great improvement.

So far, I am still just doing regular material swapping on the mesh. Do you think I will see a performance improvement if I change my code around to use the renderPassId and setMaterialForRenderPass approach instead?

Yes, each time you change the mesh.material property, a number of operations are performed under the hood that can slow things down.

If you do it a single time there’s no problem, but if you do it regularly in the course of your program you should use renderPassId and setMaterialForRenderPass instead.

1 Like