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: + *

+ */ 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 + +}