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);
}
```