BatchNode and transparent Texture

Hey guys,
I am currently working on grass for my game. I use mutlitple Quads with transparent billboard textures and add a large amount of them. That looks good, but then the framerate is too low, so I attach them to a BatchNode and batch them together. This somehow resets the Transparent BUcket. Once I set Bucket.Transparent again on the children of the BatchNode, you can only see the terrain through the transparent parts of the model, not the other grass models. This video demonstrates it:

If I reattach the children to the BatchNode, everything is working fine, but they are not batched and therefore the framerate is low again. Can I somehow fix this?
That’s my code:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResults;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.BatchNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.terrain.geomipmap.TerrainQuad;
import java.util.ArrayList;
import jme3tools.optimize.GeometryBatchFactory;

/**
 * test
 *
 * @author normenhansen
 */
public class Main extends SimpleApplication implements ActionListener {

    private Spatial spatialToAdd;
    private Spatial flower;
    private Spatial terrain;
    private DirectionalLight sun;
    private Node modelRootNode = new Node("Models");
    private BatchNode grassRootNode = new BatchNode("Grass");
    private float radius = 5f;
    private float density = .5f;
    private float flowerDensity = 0f;
    private float height = .1f;
    private TerrainQuad tq;
    private Material grassMat;
    private AmbientLight grassLight;
    private boolean trans = true;

    public static void main(String[] args) {
        Main app = new Main();
        // app.setShowSettings(false);
        app.start();
    }
    private boolean paint;

    @Override
    public void simpleInitApp() {
        /**
         * A white ambient light source.
         */
        grassMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");  //Ein Model durschauen mit mehreren texturen
        grassMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
        grassMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        grassMat.setBoolean("UseAlpha", true);
        grassMat.getAdditionalRenderState().setDepthTest(true);
        grassMat.setTexture("DiffuseMap", assetManager.loadTexture("Textures/billboardGrass.png"));

        grassLight = new AmbientLight();
        grassLight.setColor(ColorRGBA.White.mult(2.5f));

        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White);
        flyCam.setMoveSpeed(100);
        inputManager.setCursorVisible(true);
        flyCam.setDragToRotate(true);
        cam.setLocation(new Vector3f(0, 30, 0));
        modelRootNode.addLight(ambient);
        terrain = assetManager.loadModel("Scenes/newScene.j3o");
        modelRootNode.attachChild(terrain);

        spatialToAdd = getGrassNode();
        spatialToAdd.scale(height);
        flower = getFlowerNode();

        inputManager.addMapping("AddModel", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
        inputManager.addListener(this, "AddModel");
        inputManager.addMapping("opt", new KeyTrigger(KeyInput.KEY_O));
        inputManager.addListener(this, "opt");
        inputManager.addMapping("trans", new KeyTrigger(KeyInput.KEY_T));
        inputManager.addListener(this, "trans");

        /**
         * A white, directional light source
         */
        sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-.5f, -.5f, -.5f));
        sun.setColor(ColorRGBA.White.mult(1.5f));
        modelRootNode.addLight(sun);
        rootNode.attachChild(modelRootNode);
        grassRootNode.addLight(grassLight);
        rootNode.attachChild(grassRootNode);

        tq = (TerrainQuad) ((Node) terrain).getChild("terrain-newScene");
    }

    @Override
    public void simpleUpdate(float tpf) {
        if (paint) {
            Vector2f click2d = inputManager.getCursorPosition();
            Vector3f click3d = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone();
            Vector3f dir = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
            CollisionResults results = new CollisionResults();
            Ray ray = new Ray(click3d, dir);
            terrain.collideWith(ray, results);
            if (results.size() > 0) {
                Vector3f contact = results.getClosestCollision().getContactPoint();
                ArrayList<Vector3f> points = new ArrayList<>();
                for (float x = -radius; x < radius; x++) {
                    for (float z = -radius; z < radius; z++) {
                        float rand = (float) Math.random();
                        if (rand < 1 - density) {
                            continue;
                        }
                        points.add(new Vector3f(contact.x + x, 0, contact.z + z));
                    }
                }

                for (Vector3f point : points) {
                    float yangle = (float) Math.random() * 360;
                    spatialToAdd.rotate(0, yangle * FastMath.DEG_TO_RAD, 0);
                    spatialToAdd.setLocalTranslation(point.x, tq.getHeight(new Vector2f(point.x, point.z)), point.z);
                    Spatial clone = spatialToAdd.clone();
                    clone.setMaterial(grassMat);
                    grassRootNode.attachChild(clone);
                }
            }
        }
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("opt")) {
            if (isPressed) {
                grassRootNode.batch();
                trans = false;
            }
        }
        if (name.equals("trans") && isPressed) {
            for (int i = 0; i < grassRootNode.getQuantity(); i++) {  //da wo schwarz war, wird es transparent, aber man sieht nur dass terrain durch, nicht die anderen mdoels
                Spatial child = grassRootNode.getChild(i);
                child.setQueueBucket(Bucket.Transparent);
            }
        }
        if (name.equals("AddModel")) {
            if (isPressed) {
                paint = true;
            } else {
                paint = false;
            }
        }
    }

    private Node getGrassNode() {
        Quad quad = new Quad(5, 5);
        Node grassNode = new Node();
        Geometry geom = new Geometry("grass", quad);
        geom.setMaterial(grassMat);
        geom.setQueueBucket(Bucket.Transparent);
        geom.setLocalTranslation(0, 0, 1.1f);
        grassNode.attachChild(geom);
        Geometry clone = geom.clone();
        clone.rotate(0, 130 * FastMath.DEG_TO_RAD, 0);
        clone.setLocalTranslation(5, 0, 1);
        grassNode.attachChild(clone);
        Geometry clone2 = geom.clone();
        clone2.rotate(0, 60 * FastMath.DEG_TO_RAD, 0);
        clone2.setLocalTranslation(0, 0, 1);
        grassNode.attachChild(clone2);

        return grassNode;
    }

    private Node getFlowerNode() {
        Quad quad = new Quad(5, 5);
        Node grassNode = new Node();
        Geometry geom = new Geometry("grass", quad);
        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        mat.setTexture("DiffuseMap", assetManager.loadTexture("Textures/billboardGrassFlower.png"));
        geom.setMaterial(mat);
        geom.setQueueBucket(Bucket.Transparent);
        grassNode.attachChild(geom);
        Geometry clone = geom.clone();
        clone.rotate(0, 130 * FastMath.DEG_TO_RAD, 0);
        clone.setLocalTranslation(5, 0, 1);
        grassNode.attachChild(clone);
        Geometry clone2 = geom.clone();
        clone2.rotate(0, 60 * FastMath.DEG_TO_RAD, 0);
        clone2.setLocalTranslation(0, 0, 1);
        grassNode.attachChild(clone2);

        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White.mult(2.5f));
        grassNode.addLight(ambient);

        return grassNode;
    }
}

Welcome to transparency and geometry sorting.

You can try messing with the alpha discard threshold. It will help but there are no perfect solutions.

Okay :frowning: But I still don’t quite get what the problem really is… Why is everything rendered correctly when it is not batched, what difference does it make?

And would you rather suggest using a high or low alpha falloff? It’s the first time I use these values. But setAlphaTest(true) made things already a bit better, thanks.

@mathiasj said: Okay :( But I still don't quite get what the problem really is... Why is everything rendered correctly when it is not batched, what difference does it make?

And would you rather suggest using a high or low alpha falloff? It’s the first time I use these values. But setAlphaTest(true) made things already a bit better, thanks.

If you search the internet for something like “alpha transparency sorting” your browser will probably explode with links.

It works when they aren’t batched because JME sorts them back to front at the object level. When they are batched they are drawn as one geometry and every transparent pixel fills in its distance in the z-buffer thereby excluding anything drawn after it that would be ‘behind’ it.

That’s the shortest explanation possible without including a text-wall of information that 1000 other people have probably already written better.

Okay thanks. I know understand the problem and with setting alphaTest to true it is much less noticeable, especially if I use huge amounts of grass. Thanks!

I’ve got one more question though. Somehow the issue seems to be a lot stronger when I look atthe grass from one side than when I look at it fromt the other side… And that even though I rotate all the grass nodes randomly. What could be the cause for this?

Oh and how would I sort the objects back to front at runtime in JME? Some articles recommmended to do that. I’m not really familiar with the rendering process though…

@mathiasj said: Okay thanks. I know understand the problem and with setting alphaTest to true it is much less noticeable, especially if I use huge amounts of grass. Thanks!

I’ve got one more question though. Somehow the issue seems to be a lot stronger when I look atthe grass from one side than when I look at it fromt the other side… And that even though I rotate all the grass nodes randomly. What could be the cause for this?

Oh and how would I sort the objects back to front at runtime in JME? Some articles recommmended to do that. I’m not really familiar with the rendering process though…

When they are separate objects, they are already being sorted back to front. When they are not, you will have to sort them yourself and make a custom mesh… and you will have to sort them per frame, basically… which kind of defeats the purpose of batching, really.

The reason it looks worse from some angles than others is because from some angles it will be sorted properly.

Okay, I now ended up with a good result. I created multiple grass “chunks”, which are batched grass objects. These are randomly rotated. So when I place these chunks near to each other, I will most likely not see the messed up transparency because it is right on other grass chunks which are rotated.

Thanks @pspeed! You really helped me a lot!

@mathiasj said: What if I created different grass patches (different batched objects) and then sorted these back to front at runtime? Or will they automatically be sorted correctly?

Repeating: When they are separate objects, they are already being sorted back to front.

…but the batches themselves will still exhibit the problem.

There are only two solutions:
-sort the grass and recreate the mesh at runtime (custom meshes are not hard to make)
-set the alpha discard threshold (or the deprecated alpha test as you’ve done and find a nice value for the threshold).

Oh you ninja’d me :slight_smile:
Is the alpha test value just deprecated in the nightly builds? Because I don’t see it as @deprecated int the SDK… When running “Check for updates” it says that there are no updates available.

@mathiasj said: Oh you ninja'd me :) Is the alpha test value just deprecated in the nightly builds? Because I don't see it as @deprecated int the SDK... When running "Check for updates" it says that there are no updates available.

It’s deprecated in OpenGL itself. Alpha test is an OpenGL thing that’s going away someday, I guess. JME’s lighting shader and unshaded shader both support AlphaDiscardThreshold which basically does the same thing.

Ah ok thanks, I could implement that in my shader. Do you have any idea why alpha testing is not marked as @deprecated in the SDK?