[Solved] Blinking/flickering transparent mesh with Lighting.j3md

Hello,
I am trying to set a transparency parameter dynamically (based on the distance from camera) to several subgroups of meshes.

The scene is made up of N nodes, each node contains about 100 meshes.

The meshes use the material Lighting.j3md with a diffusemap set, and I am modifying their transparency through the parameter “UseMaterialColors” and adding a more or less transparent color to the “Diffuse” parameter (+ changing to adequate render queue, blendmode Alpha etc …)

In order to apply that transparency, I have a control added to each big node, which calculates the new alpha and applies it to the 100 meshes belonging to it inside the controlRender method

If I apply this to a scene with N around 10-15 nodes, it works very well

If N starts to go above 20+, I see onscreen that the transparency of the meshes is blinking (as if it was reset to non-transparent and then transparent again). Interesting to note that they all blink simultaneously (even though they belong to different groups). I also see some general instability, like some meshes don’t show the transparency they are supposed to have (It’s easy to see since the transparency is linked to the distance of the camera: I move back and forth and see that the transparency is different even though the camera comes back to the same place)

I was able to find that the blinking is caused precisely by inputting a color in the “Diffuse” parameter of the meshes (even if you input the same color again)

I see that this is also specific to having many nodes, not many meshes (if I create 4 nodes with 1000 meshes, I don’t have the issue)

Any idea what could be the deeper cause, and how to solve this? Any suggestion for troubleshooting more?

Thanks

Need… code…

1 Like

I don’t understand why a mesh would have a “Diffuse” parameter. Perhaps you’ve confused meshes with materials, or perhaps I’m just confused.

Indeed the parameters I am mentioning are part of the material, not the mesh. Sorry if I wasn’t clear

Ok, I tried to comment and cleanup as much as possible. If you want the full source of main and the control (it’s messier), you can also check here:
main.java
treebillboardcontrol.java

One comment regarding what I initially said

Blockquote
I see that this is also specific to having many nodes, not many meshes (if I create 4 nodes with 1000 meshes, I don’t have the issue)

Well this is not correct, going high enough on the number of meshes on just a few nodes gives me the same issue (which makes more sense actually)

public class Main extends SimpleApplication {

[...]

public void simpleInitApp() {
    [...]
    // creates an object forest that contains a big node and 30 sub nodes. 
    // Each sub nodes contain about 100 tree meshes (4 iterations of the growth algorithm)
    Forest forest = Forest.placeOnTerrain(30, 4, ForestType.CHERRY_1, t, assetManager);
    scene.attachChild(forest.getNode());

    int j = 0;
    // attaching the custom control I created for managing transparency
    // the control is attached to a node containing the 100 meshes
    for (Node cn : forest.getNode().descendantMatches(Node.class, "Forestnode")){
        cn.addControl(new TreeBillboardControl(SpatialType.FORESTNODE));
        cn.setName(cn.getName()+j);
        j++;
    }

    rootNode.attachChild(scene);

}

Then the control class. The idea is that each tree is a node containing a mesh and a billboard. The control is here to swap the two geometries when needed. When we are far away (distance > transparencySwapDistance), we just show the billboard, when we are very close (distance < fullSwapDistance ) we show only the mesh. When we are between the two distances (transparencySwapDistance < distance < fullSwapDistance) we show both mesh and billboard with an alpha that goes from 0 to 1 so that there is a progressive fade from the mesh to the billboard and vice-versa

public class TreeBillboardControl extends AbstractControl{

/* I used that variable in order to check for when the alpha I’m applying was changing
it keeps the last value that is set to the material of all the trees making up the node */
private float lastAlpha = -1f;

private float fullSwapDistance = 40;
private float transparencySwapDistance = 300;
Geometry mesh, billboard;
/* this is to tell the control what is the current control state, it can be showing the billboard, showing the mesh, or both   */ 
BillboardType billboardType;

public enum BillboardType {
    MESH, MESH_AND_BOARD, BOARD
}

// I use these variables to only update every X ms
float timer = 0f, refreshTime = .5f;

// this is to store the meshes and billboard from the whole node (remember this node contains 100 meshes)
Node meshes, billboards;


public TreeBillboardControl(SpatialType typeOfNode) {
    super();
}

@Override
protected void controlUpdate(float tpf) {
    timer += tpf;
    
}

protected BillboardType getType(float distance){
    if (distance < fullSwapDistance) {
        return BillboardType.MESH;
    } else if (distance < transparencySwapDistance) {
        return BillboardType.MESH_AND_BOARD;
    } else {
        return BillboardType.BOARD;
    }
}

protected void controlRender(RenderManager rm, ViewPort vp) {
    if (node==null) {
        initialize();
    } 

    /* In this part, we have the logic to swap the models (billboard/tree) depending on the distance */
    if (timer > refreshTime){
        Camera cam = vp.getCamera();
        float distance = cam.getLocation().distance(node.getWorldTranslation());
        BillboardType newType = getType(distance);

        if (newType != billboardType) {
            if (billboardType != BillboardType.MESH_AND_BOARD){
                if (newType == BillboardType.BOARD){
                //it was showing the mesh, now needs to switch to board only
                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)billboards.getChild("Billboard"+suffix);
                            mesh = (Geometry)n.getChild("Mesh"+suffix);
                            meshes.attachChild(mesh);
                            n.attachChild(billboard); 
                            updateTransparency(billboardType, newType, distance);
                        }
                    }  
                } else if (newType == BillboardType.MESH){
                    //so it was a board and we switch it to mesh
                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)n.getChild("Billboard"+suffix);
                            mesh = (Geometry)meshes.getChild("Mesh"+suffix);
                            n.attachChild(mesh);
                            billboards.attachChild(billboard); 
                            updateTransparency(billboardType, newType, distance);
                        }
                    }
                } else {
                    // otherwise it means that we were a board or mesh and we move to board/mesh
                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)billboards.getChild("Billboard"+suffix);
                            mesh = (Geometry)meshes.getChild("Mesh"+suffix);
                            if (mesh!=null) {
                                n.attachChild(mesh);
                            } else {
                                mesh = (Geometry)n.getChild("Mesh"+suffix);
                            } 
                            if (billboard!=null) {
                                n.attachChild(billboard);
                            } else {
                                billboard = (Geometry)n.getChild("Billboard"+suffix);
                            }    
                            updateTransparency(billboardType, newType, distance);
                        }
                    }
                }
            } else {
                //it means that we are in board/mesh mode already 
                if (newType == BillboardType.BOARD){

                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)n.getChild("Billboard"+suffix);
                            mesh = (Geometry)n.getChild("Mesh"+suffix);
                            meshes.attachChild(mesh);
                            updateTransparency(billboardType, newType, distance);
                        }
                    }  
                } else if (newType == BillboardType.MESH){
                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)n.getChild("Billboard"+suffix);
                            mesh = (Geometry)n.getChild("Mesh"+suffix);
                            billboards.attachChild(billboard); 
                            updateTransparency(billboardType, newType, distance);
                        }
                    }
                } else {
                    // we were already in board/mesh mode, now we just need to update transparency

                    for (Node n : node.descendantMatches(Node.class)){
                        if (n.getName().substring(0, 8).matches("Treenode")){
                            String suffix = n.getName().substring(8, n.getName().length());
                            billboard = (Geometry)billboards.getChild("Billboard"+suffix);
                            mesh = (Geometry)meshes.getChild("Mesh"+suffix);
                            if (mesh!=null) {
                                n.attachChild(mesh);
                            } else {
                                billboard = (Geometry)n.getChild("Billboard"+suffix);
                            }
                            if (billboard!=null) {
                                n.attachChild(billboard);
                            } else {
                                mesh = (Geometry)n.getChild("Mesh"+suffix);
                            }                                    
                            updateTransparency(billboardType, newType, distance);
                        }
                    }
                }
            }
        } else if (newType == BillboardType.MESH_AND_BOARD) {
            //we are in the same mode as before
            for (Node n : node.descendantMatches(Node.class)){
                if (n.getName().substring(0, 8).matches("Treenode")){
                    String suffix = n.getName().substring(8, n.getName().length());     
                    billboard = (Geometry)n.getChild("Billboard"+suffix);
                    mesh = (Geometry)n.getChild("Mesh"+suffix);
                    updateTransparency(billboardType, newType, distance);
                }
            }
        }
        if (newType == BillboardType.MESH_AND_BOARD){
            lastAlpha = getAlpha(distance);
            System.out.println("New alpha for forest "+node.toString()+" : "+lastAlpha);
        }
        billboardType = newType;
        meshes.updateGeometricState();
        billboards.updateGeometricState();
        node.updateGeometricState();
        timer = 0f;
    }
}

private void initialize(){
    node = (Node)spatial;
    meshes = new Node();
    billboards = new Node();
    int i = 0;
    for (Geometry g : node.descendantMatches(Geometry.class,"Treenode")){
        g.setName(g.getName()+i);
        g.getParent().setName(g.getParent().getName()+i);
        i++;
    }
    i=0;
    for (Geometry g : node.descendantMatches(Geometry.class,"Mesh")){
        g.setName(g.getName()+i);
        g.getParent().setName(g.getParent().getName()+i);
        i++;
    }
    i=0;
    for (Geometry g : node.descendantMatches(Geometry.class,"Billboard")){
        g.setName(g.getName()+i);
        i++;
    }
    billboardType = BillboardType.MESH_AND_BOARD; 
}


private void updateTransparency(BillboardType oldType, BillboardType newType, float distance){
    
    if (newType == BillboardType.MESH_AND_BOARD){

        Material meshMat = mesh.getMaterial();     

        meshMat.setBoolean("UseMaterialColors", true);
        meshMat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
        meshMat.getAdditionalRenderState().setDepthWrite(false);
        mesh.setShadowMode(RenderQueue.ShadowMode.Off);        
        mesh.setQueueBucket(RenderQueue.Bucket.Transparent);
        
        Material billMat = billboard.getMaterial();
        billMat.getAdditionalRenderState().setDepthWrite(false);

        ColorRGBA color = ColorRGBA.White;
        float alpha = getAlpha(distance);
        

        Material boardMat = billboard.getMaterial();
        

        color.a = alpha;
        meshMat.setColor("Diffuse", color);
        boardMat.setFloat("alpha", 1f-alpha);

    
    } else if (newType == BillboardType.BOARD){
        Material boardMat = billboard.getMaterial();
        boardMat.setFloat("alpha", 1f);
    } else if (newType == BillboardType.MESH){
        Material meshMat = mesh.getMaterial();       
        meshMat.setBoolean("UseMaterialColors", false);
        meshMat.getAdditionalRenderState().setBlendMode(BlendMode.Off);
        mesh.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);        
        mesh.setQueueBucket(RenderQueue.Bucket.Opaque);
        meshMat.getAdditionalRenderState().setDepthWrite(true);
    }
}

private float getAlpha(float distance){
    return FastMath.clamp((distance-transparencySwapDistance)/(fullSwapDistance-transparencySwapDistance), 0, 1);
}

Whatever you are doing, if a control needs to reference global spatials then you are probably doing something wrong at the design level.

That being said, approaching trees this way is probably never going to fly in a real game. All of this stuff should ultimately be done with batches/instancing and tricks in the shader.

Blockquote
Whatever you are doing, if a control needs to reference global spatials then you are probably doing something wrong at the design level.

Fine, but then what do you suggest? Rather have the control send commands to the trees themselves? (something like tree.enableBillboard( true / false) Or something else?

Do you think it’s likely to explain the flickering?

I mean, without looking in detail, calling those methods yourself is almost always wrong unless you are some sort of JME expert. The only valid use-case for having to call those manually is managing your own viewport, really.

If you have a control per tree then they don’t need a reference to all of the other trees or you’ve done something very wrong.

If you have a control per tree then they don’t need a reference to all of the other trees or you’ve done something very wrong.

It’s one control for a node that contains 100 trees, not one per tree

You shouldn’t need to call this yourself. If you had to for some reason then that is a likely candidate for the root of your troubles.

Edit: also if you want to rule transparency sorting as the problem you can stop doing transparency and instead make them turn red or something.

Ok, so I removed every occurrence of updateGeometricState and replaced the mesh switching between nodes by a simple setCullHint (the node switching was the reason behind the updateGeom…). However the issue is still there

Also, replacing the transparency thing by applying a red color instead (+ moving to opaque queue and blendmode off), the blinking is still happening

I will take your advice and refactor this class nonetheless (probably use some sort of interface on the forest and trees to make them billboardable)

I tried on other test cases,

  • if I just load 4000 meshes of my train in the main loop and overwrite the “Diffuse” parameter in the simpleupdate method, I don’t have the issue

  • if I overwrite alpha with a fixed number, no issue either. On the other hand, when the value depends on a variable but that variable doesn’t move within the game (by not moving the camera, the distance remain constant)

  • I also note that when the screen is blinking, I also see the number of Uniforms (in the stats) blinking. When the uniform count stops blinking, the screen blinking as well

Yes, there is something very strange you are doing that the rest of us can’t see. You may have to boil it down to a simple test case.

It could be that you are modifying one of the engine “constants” like a ColorRGBA constant or a Vector3f constant. For example, if you were to modify the values of ColorRGBA.White then you might have some interesting issues.

found the issue … wasn’t far from your last comment actually

        ColorRGBA color = ColorRGBA.White; 
        float alpha = getAlpha(distance);
        color.a = alpha;
        meshMat.setColor("Diffuse", color);

By doing this, all materials share the same “Diffuse” parameter, which is the static constant ColorRGBA.White. Even though I overwrite the alpha of at every frame, there could be a frame lag or something like that which makes that there is a concurrent writing into the object

It is solved by doing this

        ColorRGBA color = new ColorRGBA(1f,1f,1f,1f); 
        float alpha = getAlpha(distance);
        color.a = alpha;
        meshMat.setColor("Diffuse", color);
2 Likes

Nice you got it solved!
Note that you should not use the first approach for other classes’ constants like Vector3f or Quaternion too :wink:

Believe me, it will not happen again …

Thanks anyway for all the help and to @pspeed for taking the time to look at the code