Bugfix to BillboardControl

Hey Guys,
I’ve noticed a bug in billboard-Control: Only one of the Alignments cared about the parents rotation, see this test case:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.BillboardControl;
import com.jme3.scene.shape.Quad;
public class Main extends SimpleApplication {

    Node n;
    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp()
    {
        n = new Node("myNode");
        Node o = new Node("subNode");
        
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        
        Quad q = new Quad(1f, 1f);
        Geometry geom = new Geometry("Box", q);
        geom.setMaterial(mat);
        
        o.attachChild(geom);
        BillboardControl b = new BillboardControl();
        b.setAlignment(BillboardControl.Alignment.Screen);
        o.addControl(b);       
        n.attachChild(o);       
        rootNode.attachChild(n);        
        flyCam.setMoveSpeed(flyCam.getMoveSpeed() * 5f);
    }

    @Override
    public void simpleUpdate(float tpf)
    {
        n.rotate(0f, FastMath.HALF_PI * tpf, 0f);
    }
}

Play around with the different Alignments to see.
After looking into the sourcecode I altered some methods, but Alignment.AXIS does only work when not rotating the parent, I have no clue why. But I could fix Alignment.CAMERA atleast.

See also Attaching Indicators (Healthbars) to rootNode/SceneGraph - #39 by Darkchaos

/**
* Aligns this Billboard so that it points to the camera position.
*
* @param camera
*            Camera
*/
private void rotateCameraAligned(Camera camera) {
    look.set(camera.getLocation()).subtractLocal(
    spatial.getWorldTranslation());
     // coopt left for our own purposes.
    Vector3f xzp = left;
    // The xzp vector is the projection of the look vector on the xz plane
   xzp.set(look.x, 0, look.z);
   
   // check for undefined rotation...
   if (xzp.equals(Vector3f.ZERO)) {
        return;
   }

   look.normalizeLocal();
   xzp.normalizeLocal();
   float cosp = look.dot(xzp);

   // compute the local orientation matrix for the billboard
   orient.set(0, 0, xzp.z);
   orient.set(0, 1, xzp.x * -look.y);
   orient.set(0, 2, xzp.x * cosp);
   orient.set(1, 0, 0);
   orient.set(1, 1, cosp);
   orient.set(1, 2, look.y);
   orient.set(2, 0, -xzp.x);
   orient.set(2, 1, xzp.z * -look.y);
   orient.set(2, 2, xzp.z * cosp);
   Node parent = spatial.getParent();
   

Quaternion rot=new Quaternion().fromRotationMatrix(orient);
        if ( parent != null ) {
            rot =  parent.getWorldRotation().inverse().multLocal(rot);
            rot.normalizeLocal();
        }
        
        
        // The billboard must be oriented to face the camera before it is
        // transformed into the world.
        spatial.setLocalRotation(rot);
        fixRefreshFlags();
    }

    /**
     * Rotate the billboard so it points directly opposite the direction the
     * camera's facing
     *
     * @param camera
     *            Camera
     */
    private void rotateScreenAligned(Camera camera) {
        // coopt diff for our in direction:
        look.set(camera.getDirection()).negateLocal();
        // coopt loc for our left direction:
        left.set(camera.getLeft()).negateLocal();
        orient.fromAxes(left, camera.getUp(), look);
        Node parent = spatial.getParent();
        Quaternion rot=new Quaternion().fromRotationMatrix(orient);
        if ( parent != null ) {
            rot =  parent.getWorldRotation().inverse().multLocal(rot);
            rot.normalizeLocal();
        }
        spatial.setLocalRotation(rot);
        fixRefreshFlags();
    }

    /**
     * Rotate the billboard towards the camera, but keeping a given axis fixed.
     *
     * @param camera
     *            Camera
     */
    private void rotateAxial(Camera camera, Vector3f axis) {
        // Compute the additional rotation required for the billboard to face
        // the camera. To do this, the camera must be inverse-transformed into
        // the model space of the billboard.
        look.set(camera.getLocation()).subtractLocal(
                spatial.getWorldTranslation());   
        spatial.getParent().getWorldRotation().mult(look, left); // coopt left for our own
        // purposes.
        left.x *= 1.0f / spatial.getWorldScale().x;
        left.y *= 1.0f / spatial.getWorldScale().y;
        left.z *= 1.0f / spatial.getWorldScale().z;

        // squared length of the camera projection in the xz-plane
        float lengthSquared = left.x * left.x + left.z * left.z;
        if (lengthSquared < FastMath.FLT_EPSILON) {
            // camera on the billboard axis, rotation not defined
            return;
        }

        // unitize the projection
        float invLength = FastMath.invSqrt(lengthSquared);
        if (axis.y == 1) {
            left.x *= invLength;
            left.y = 0.0f;
            left.z *= invLength;

            // compute the local orientation matrix for the billboard
            orient.set(0, 0, left.z);
            orient.set(0, 1, 0);
            orient.set(0, 2, left.x);
            orient.set(1, 0, 0);
            orient.set(1, 1, 1);
            orient.set(1, 2, 0);
            orient.set(2, 0, -left.x);
            orient.set(2, 1, 0);
            orient.set(2, 2, left.z);
        } else if (axis.z == 1) {
            left.x *= invLength;
            left.y *= invLength;
            left.z = 0.0f;

            // compute the local orientation matrix for the billboard
            orient.set(0, 0, left.y);
            orient.set(0, 1, left.x);
            orient.set(0, 2, 0);
            orient.set(1, 0, -left.y);
            orient.set(1, 1, left.x);
            orient.set(1, 2, 0);
            orient.set(2, 0, 0);
            orient.set(2, 1, 0);
            orient.set(2, 2, 1);
        }

        Node parent = spatial.getParent();
        Quaternion rot=new Quaternion().fromRotationMatrix(orient);
        if ( parent != null ) {
            rot =  parent.getWorldRotation().inverse().multLocal(rot);
            rot.normalizeLocal();
        }
        
        
        // The billboard must be oriented to face the camera before it is
        // transformed into the world.
        spatial.setLocalRotation(rot);
        fixRefreshFlags();
    }

Now it’s nearly working as I said, the Billboard.AXISY works, when moving the camera but only when removing the code from simpleUpdate.

Seems like the axis version is already trying to figure out the local rotation by calculating the camera to spatial vector in local space. So the parent inverse thing shouldn’t be needed for axial.

…but I think the earlier transformation into local space is wrong. I think it actually needs an inverse rotation or something.

Maybe this:
spatial.getParent().getWorldRotation().mult(look, left);

…should be:
spatial.getParent().getWorldRotation().inverse().mult(look, left);

…but I’m kind of just guessing.

The presumption is that one wants to use the local Y axis for rotation and not the world Y axis… which is reasonable to me. Thus the camera location must be moved into parent space… why worldToLocal() wasn’t used I can’t say.

That indeed fixed it.
Now there is only the problem of some sort of perspective distortion when moving around the camera.
Like: It is facing the camera, but not that way, that the quad is still completely in-line (hard to explain), it looks somehow rotated.

I don’t know if this is the expected behavior because we are only aligning along Y or whether this is related to the world vs local space?

(Just so I get it right: local-space means: everything relative to the parent and world-space means: everything absolute to (0, 0, 0))

Well, there is spatial-local space which is local to the spatial and parent-local space which is local to the parent… in both cases, rotation is included in that. Then there is world space which is related to 0,0,0 with no rotation.

Because rotation of the widget is relative to its parent, the camera position needs to be brought into parent-local space to determine which way the widget should face. Though, it’s kind of some weird hybrid space since we want location relative to the spatial but also relative to parent.

Vector3f localCam = parent.worldToLocal(camLoca).subtract(spatial.getLocalTranslation());

…normalized, that becomes your ‘z’ vector. The y vector is UNIT_Y then you calculate X from the cross of y and z (or z and y, I can never remember). Those three vectors can be used to make the quaternion.

Something like that.

I just wanted to bump this thread again as I couldn’t really fix it but maybe some of the core devs have a clue?
If not, how do you feel about a “partial” fix? (Like it seems only the one Axis is wrong)