diff --git a/jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java b/jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java
index f51e7460b4..e724414288 100644
--- a/jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java
+++ b/jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -44,14 +44,34 @@
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
+import com.jme3.util.TempVars;
+
import java.io.IOException;
+/**
+ * BillboardControl
is a special control that makes a spatial always
+ * face the camera. This is useful for health bars, or other 2D elements
+ * that should always be oriented towards the viewer in a 3D scene.
+ *
+ * The alignment can be customized to different modes:
+ *
+ * - Screen: The billboard always faces the screen, keeping its 'up' vector aligned with camera's 'up'.
+ * - Camera: The billboard always faces the camera position directly.
+ * - AxialY: The billboard faces the camera but keeps its local Y-axis fixed.
+ * - AxialZ: The billboard faces the camera but keeps its local Z-axis fixed.
+ *
+ */
public class BillboardControl extends AbstractControl {
- private Matrix3f orient;
- private Vector3f look;
- private Vector3f left;
- private Alignment alignment;
+ // Member variables for calculations, reused to avoid constant object allocation.
+ private final Matrix3f tempMat3 = new Matrix3f();
+ private final Vector3f tempDir = new Vector3f();
+ private final Vector3f tempLeft = new Vector3f();
+
+ /**
+ * The current alignment mode for the billboard.
+ */
+ private Alignment alignment = Alignment.Screen;
/**
* Determines how the billboard is aligned to the screen/camera.
@@ -61,38 +81,36 @@ public enum Alignment {
* Aligns this Billboard to the screen.
*/
Screen,
-
/**
* Aligns this Billboard to the camera position.
*/
Camera,
-
/**
- * Aligns this Billboard to the screen, but keeps the Y axis fixed.
- */
+ * Aligns this Billboard to the screen, but keeps the Y axis fixed.
+ */
AxialY,
-
/**
* Aligns this Billboard to the screen, but keeps the Z axis fixed.
*/
AxialZ;
}
+ /**
+ * Constructs a new `BillboardControl` with the default alignment set to
+ * {@link Alignment#Screen}.
+ */
public BillboardControl() {
- super();
- orient = new Matrix3f();
- look = new Vector3f();
- left = new Vector3f();
- alignment = Alignment.Screen;
}
- // default implementation from AbstractControl is equivalent
- //public Control cloneForSpatial(Spatial spatial) {
- // BillboardControl control = new BillboardControl();
- // control.alignment = this.alignment;
- // control.setSpatial(spatial);
- // return control;
- //}
+ /**
+ * Constructs a new `BillboardControl` with the specified alignment.
+ *
+ * @param alignment The desired alignment type for the billboard.
+ * See {@link Alignment} for available options.
+ */
+ public BillboardControl(Alignment alignment) {
+ this.alignment = alignment;
+ }
@Override
protected void controlUpdate(float tpf) {
@@ -102,25 +120,26 @@ protected void controlUpdate(float tpf) {
protected void controlRender(RenderManager rm, ViewPort vp) {
Camera cam = vp.getCamera();
rotateBillboard(cam);
+ fixRefreshFlags();
}
- private void fixRefreshFlags(){
+ private void fixRefreshFlags() {
// force transforms to update below this node
spatial.updateGeometricState();
// force world bound to update
Spatial rootNode = spatial;
- while (rootNode.getParent() != null){
+ while (rootNode.getParent() != null) {
rootNode = rootNode.getParent();
}
rootNode.getWorldBound();
}
/**
- * rotate the billboard based on the type set
+ * Rotates the billboard based on the alignment type set.
+ * This method is called every frame during the render phase.
*
- * @param cam
- * Camera
+ * @param cam The current Camera used for rendering.
*/
private void rotateBillboard(Camera cam) {
switch (alignment) {
@@ -140,130 +159,160 @@ private void rotateBillboard(Camera cam) {
}
/**
- * Aligns this Billboard so that it points to the camera position.
+ * Aligns this Billboard so that it points directly to the camera position.
+ * The billboard's local rotation is set to ensure its positive Z-axis
+ * points towards the camera's location.
*
- * @param camera
- * Camera
+ * @param camera The current Camera.
*/
private void rotateCameraAligned(Camera camera) {
- look.set(camera.getLocation()).subtractLocal(
+ tempDir.set(camera.getLocation()).subtractLocal(
spatial.getWorldTranslation());
// co-opt left for our own purposes.
- Vector3f xzp = left;
+ Vector3f xzp = tempLeft;
// The xzp vector is the projection of the look vector on the xz plane
- xzp.set(look.x, 0, look.z);
+ xzp.set(tempDir.x, 0, tempDir.z);
// check for undefined rotation...
if (xzp.equals(Vector3f.ZERO)) {
return;
}
- look.normalizeLocal();
+ tempDir.normalizeLocal();
xzp.normalizeLocal();
- float cosp = look.dot(xzp);
+ float cosp = tempDir.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);
-
- // The billboard must be oriented to face the camera before it is
- // transformed into the world.
- spatial.setLocalRotation(orient);
- fixRefreshFlags();
+ tempMat3.set(0, 0, xzp.z);
+ tempMat3.set(0, 1, xzp.x * -tempDir.y);
+ tempMat3.set(0, 2, xzp.x * cosp);
+ tempMat3.set(1, 0, 0);
+ tempMat3.set(1, 1, cosp);
+ tempMat3.set(1, 2, tempDir.y);
+ tempMat3.set(2, 0, -xzp.x);
+ tempMat3.set(2, 1, xzp.z * -tempDir.y);
+ tempMat3.set(2, 2, xzp.z * cosp);
+
+ // Set the billboard's local rotation based on the computed orientation matrix.
+ spatial.setLocalRotation(tempMat3);
}
/**
* Rotates the billboard so it points directly opposite the direction the
- * camera is facing.
+ * camera is facing (screen-aligned). This means the billboard will always
+ * be flat against the screen, regardless of its position in 3D space.
+ * Its Z-axis will point against the camera's direction, and its Y-axis
+ * will align with the camera's Y-axis.
*
- * @param camera
- * Camera
+ * @param camera The current Camera.
*/
private void rotateScreenAligned(Camera camera) {
+ TempVars vars = TempVars.get();
+
+ Vector3f up = camera.getUp(vars.vect1);
// co-opt diff for our in direction:
- look.set(camera.getDirection()).negateLocal();
+ Vector3f dir = camera.getDirection(vars.vect2).negateLocal();
// co-opt loc for our left direction:
- left.set(camera.getLeft()).negateLocal();
- orient.fromAxes(left, camera.getUp(), look);
+ Vector3f left = camera.getLeft(vars.vect3).negateLocal();
+
+ Matrix3f orient = vars.tempMat3;
+ orient.fromAxes(left, up, dir);
+
Node parent = spatial.getParent();
- Quaternion rot = new Quaternion().fromRotationMatrix(orient);
+ Quaternion rot = vars.quat1.fromRotationMatrix(orient);
+
if (parent != null) {
- rot = parent.getWorldRotation().inverse().multLocal(rot);
+ Quaternion invRot = vars.quat2.set(parent.getWorldRotation()).inverseLocal();
+ rot = invRot.multLocal(rot);
rot.normalizeLocal();
}
+
+ // Apply the calculated local rotation to the spatial.
spatial.setLocalRotation(rot);
- fixRefreshFlags();
+
+ vars.release();
}
/**
- * Rotate the billboard towards the camera, but keeping a given axis fixed.
+ * Rotates the billboard towards the camera, but keeps a given axis fixed.
+ * This is used for {@link Alignment#AxialY} (fixed Y-axis) or
+ * {@link Alignment#AxialZ} (fixed Z-axis) alignments. The billboard will
+ * only rotate around the specified axis.
*
- * @param camera
- * Camera
+ * @param camera The current Camera.
+ * @param axis The fixed axis (e.g., {@link Vector3f#UNIT_Y} for AxialY).
*/
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); // co-opt 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;
+ tempDir.set(camera.getLocation()).subtractLocal(spatial.getWorldTranslation());
+ spatial.getParent().getWorldRotation().mult(tempDir, tempLeft); // co-opt left for our own purposes.
+ tempLeft.x *= 1.0f / spatial.getWorldScale().x;
+ tempLeft.y *= 1.0f / spatial.getWorldScale().y;
+ tempLeft.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;
+// float lengthSquared = left.x * left.x + left.z * left.z;
+
+ // Calculate squared length of the camera projection on the plane perpendicular
+ // to the fixed axis. This determines the magnitude of the projection used
+ // for axial rotation.
+ float lengthSquared;
+ if (axis.y == 1) { // AxialY: projection on XZ plane
+ lengthSquared = tempLeft.x * tempLeft.x + tempLeft.z * tempLeft.z;
+ } else if (axis.z == 1) { // AxialZ: projection on XY plane
+ lengthSquared = tempLeft.x * tempLeft.x + tempLeft.y * tempLeft.y;
+ } else {
+ // This case should ideally not be reached with the current Alignment enum,
+ // but provides robustness for unexpected 'axis' values.
+ return;
+ }
+
+ // Check for edge case: camera is directly on the fixed axis relative to the billboard.
+ // If the projection length is too small, the rotation is undefined.
if (lengthSquared < FastMath.FLT_EPSILON) {
- // camera on the billboard axis, rotation not defined
+ // Rotation is undefined, so no rotation is applied.
return;
}
- // unitize the projection
+ // Unitize the projection to get a normalized direction vector in the plane.
float invLength = FastMath.invSqrt(lengthSquared);
if (axis.y == 1) {
- left.x *= invLength;
- left.y = 0.0f;
- left.z *= invLength;
+ tempLeft.x *= invLength;
+ tempLeft.y = 0.0f; // Fix Y-component to 0 as it's axial, forcing rotation only around Y.
+ tempLeft.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);
+ tempMat3.set(0, 0, tempLeft.z);
+ tempMat3.set(0, 1, 0);
+ tempMat3.set(0, 2, tempLeft.x);
+ tempMat3.set(1, 0, 0);
+ tempMat3.set(1, 1, 1); // Y-axis remains fixed (no rotation along Y).
+ tempMat3.set(1, 2, 0);
+ tempMat3.set(2, 0, -tempLeft.x);
+ tempMat3.set(2, 1, 0);
+ tempMat3.set(2, 2, tempLeft.z);
+
} else if (axis.z == 1) {
- left.x *= invLength;
- left.y *= invLength;
- left.z = 0.0f;
+ tempLeft.x *= invLength;
+ tempLeft.y *= invLength;
+ tempLeft.z = 0.0f; // Fix Z-component to 0 as it's axial, forcing rotation only around Z.
// 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);
+ tempMat3.set(0, 0, tempLeft.y);
+ tempMat3.set(0, 1, tempLeft.x);
+ tempMat3.set(0, 2, 0);
+ tempMat3.set(1, 0, -tempLeft.y);
+ tempMat3.set(1, 1, tempLeft.x);
+ tempMat3.set(1, 2, 0);
+ tempMat3.set(2, 0, 0);
+ tempMat3.set(2, 1, 0);
+ tempMat3.set(2, 2, 1); // Z-axis remains fixed (no rotation along Z).
}
- // The billboard must be oriented to face the camera before it is
- // transformed into the world.
- spatial.setLocalRotation(orient);
- fixRefreshFlags();
+ // Apply the calculated local rotation matrix to the spatial.
+ spatial.setLocalRotation(tempMat3);
}
/**
@@ -277,32 +326,26 @@ public Alignment getAlignment() {
/**
* Sets the type of rotation this Billboard will have. The alignment can
- * be Camera, Screen, AxialY, or AxialZ. Invalid alignments will
- * assume no billboard rotation.
+ * be {@link Alignment#Camera}, {@link Alignment#Screen},
+ * {@link Alignment#AxialY}, or {@link Alignment#AxialZ}.
*
- * @param alignment the desired alignment (Camera/Screen/AxialY/AxialZ)
+ * @param alignment The desired {@link Alignment} for the billboard's rotation behavior.
*/
public void setAlignment(Alignment alignment) {
this.alignment = alignment;
}
@Override
- public void write(JmeExporter e) throws IOException {
- super.write(e);
- OutputCapsule capsule = e.getCapsule(this);
- capsule.write(orient, "orient", null);
- capsule.write(look, "look", null);
- capsule.write(left, "left", null);
- capsule.write(alignment, "alignment", Alignment.Screen);
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(alignment, "alignment", Alignment.Screen);
}
@Override
- public void read(JmeImporter importer) throws IOException {
- super.read(importer);
- InputCapsule capsule = importer.getCapsule(this);
- orient = (Matrix3f) capsule.readSavable("orient", null);
- look = (Vector3f) capsule.readSavable("look", null);
- left = (Vector3f) capsule.readSavable("left", null);
- alignment = capsule.readEnum("alignment", Alignment.class, Alignment.Screen);
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ alignment = ic.readEnum("alignment", Alignment.class, Alignment.Screen);
}
}
diff --git a/jme3-examples/src/main/java/jme3test/model/shape/TestBillboard.java b/jme3-examples/src/main/java/jme3test/model/shape/TestBillboard.java
index 17c44a1430..29a8783440 100644
--- a/jme3-examples/src/main/java/jme3test/model/shape/TestBillboard.java
+++ b/jme3-examples/src/main/java/jme3test/model/shape/TestBillboard.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,7 +29,6 @@
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package jme3test.model.shape;
import com.jme3.app.SimpleApplication;
@@ -37,77 +36,67 @@
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.control.BillboardControl;
-import com.jme3.scene.shape.Box;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.debug.Grid;
import com.jme3.scene.shape.Quad;
/**
- *
- * @author Kirill Vainer
+ * @author capedvon
*/
public class TestBillboard extends SimpleApplication {
+ public static void main(String[] args) {
+ TestBillboard app = new TestBillboard();
+ app.start();
+ }
+
@Override
public void simpleInitApp() {
- flyCam.setMoveSpeed(10);
-
- Quad q = new Quad(2, 2);
- Geometry g = new Geometry("Quad", q);
- Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", ColorRGBA.Blue);
- g.setMaterial(mat);
-
- Quad q2 = new Quad(1, 1);
- Geometry g3 = new Geometry("Quad2", q2);
- Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
- mat2.setColor("Color", ColorRGBA.Yellow);
- g3.setMaterial(mat2);
- g3.setLocalTranslation(.5f, .5f, .01f);
-
- Box b = new Box(.25f, .5f, .25f);
- Geometry g2 = new Geometry("Box", b);
- g2.setLocalTranslation(0, 0, 3);
- g2.setMaterial(mat);
+ flyCam.setMoveSpeed(15f);
+ flyCam.setDragToRotate(true);
- Node bb = new Node("billboard");
+ viewPort.setBackgroundColor(ColorRGBA.DarkGray);
- BillboardControl control=new BillboardControl();
-
- bb.addControl(control);
- bb.attachChild(g);
- bb.attachChild(g3);
-
+ Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray);
+ grid.center().move(0, 0, 0);
+ rootNode.attachChild(grid);
- n=new Node("parent");
- n.attachChild(g2);
- n.attachChild(bb);
- rootNode.attachChild(n);
+ Node node = createBillboard(BillboardControl.Alignment.Screen, ColorRGBA.Red);
+ node.setLocalTranslation(-6f, 0, 0);
+ rootNode.attachChild(node);
- n2=new Node("parentParent");
- n2.setLocalTranslation(Vector3f.UNIT_X.mult(5));
- n2.attachChild(n);
+ node = createBillboard(BillboardControl.Alignment.Camera, ColorRGBA.Green);
+ node.setLocalTranslation(-2f, 0, 0);
+ rootNode.attachChild(node);
- rootNode.attachChild(n2);
+ node = createBillboard(BillboardControl.Alignment.AxialY, ColorRGBA.Blue);
+ node.setLocalTranslation(2f, 0, 0);
+ rootNode.attachChild(node);
-
-// rootNode.attachChild(bb);
-// rootNode.attachChild(g2);
- }
- private Node n;
- private Node n2;
- @Override
- public void simpleUpdate(float tpf) {
- super.simpleUpdate(tpf);
- n.rotate(0, tpf, 0);
- n.move(0.1f*tpf, 0, 0);
- n2.rotate(0, 0, -tpf);
+ node = createBillboard(BillboardControl.Alignment.AxialZ, ColorRGBA.Yellow);
+ node.setLocalTranslation(6f, 0, 0);
+ rootNode.attachChild(node);
}
+ private Node createBillboard(BillboardControl.Alignment alignment, ColorRGBA color) {
+ Node node = new Node("Parent");
+ Quad quad = new Quad(2, 2);
+ Geometry g = makeShape(alignment.name(), quad, color);
+ g.addControl(new BillboardControl(alignment));
+ node.attachChild(g);
+ node.attachChild(makeShape("ZAxis", new Arrow(Vector3f.UNIT_Z), ColorRGBA.Blue));
+ return node;
+ }
-
- public static void main(String[] args) {
- TestBillboard app = new TestBillboard();
- app.start();
+ private Geometry makeShape(String name, Mesh shape, ColorRGBA color) {
+ Geometry geo = new Geometry(name, shape);
+ Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color);
+ geo.setMaterial(mat);
+ return geo;
}
-}
\ No newline at end of file
+
+}