From f69f8591e29f4340f5c74d1f19729924584da35d Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Fri, 13 Jun 2025 14:40:06 +0200 Subject: [PATCH 1/8] Update BillboardControl.java --- .../jme3/scene/control/BillboardControl.java | 170 +++++++++++------- 1 file changed, 103 insertions(+), 67 deletions(-) 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..750100a9b2 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 @@ -46,12 +46,31 @@ import com.jme3.scene.Spatial; 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 orient = new Matrix3f(); + private final Vector3f look = new Vector3f(); + private final Vector3f left = new Vector3f(); + private final Quaternion tempQuat = new Quaternion(); + + /** + * The current alignment mode for the billboard. + */ + private Alignment alignment = Alignment.Screen; /** * Determines how the billboard is aligned to the screen/camera. @@ -61,38 +80,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. */ 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 +119,26 @@ protected void controlUpdate(float tpf) { protected void controlRender(RenderManager rm, ViewPort vp) { Camera cam = vp.getCamera(); rotateBillboard(cam); + updateRefreshFlags(); } - private void fixRefreshFlags(){ + private void updateRefreshFlags() { // 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,10 +158,11 @@ 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( @@ -173,18 +192,18 @@ private void rotateCameraAligned(Camera camera) { 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. + // Set the billboard's local rotation based on the computed orientation matrix. spatial.setLocalRotation(orient); - fixRefreshFlags(); } /** * 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) { // co-opt diff for our in direction: @@ -192,21 +211,28 @@ private void rotateScreenAligned(Camera camera) { // co-opt 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); + tempQuat.fromRotationMatrix(orient); + Quaternion rot = tempQuat; + if (parent != null) { - rot = parent.getWorldRotation().inverse().multLocal(rot); + rot = parent.getWorldRotation().inverse().multLocal(rot); rot.normalizeLocal(); } + + // Apply the calculated local rotation to the spatial. spatial.setLocalRotation(rot); - fixRefreshFlags(); } /** - * 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 @@ -220,17 +246,34 @@ private void rotateAxial(Camera camera, Vector3f axis) { 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; +// 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 = left.x * left.x + left.z * left.z; + } else if (axis.z == 1) { // AxialZ: projection on XY plane + lengthSquared = left.x * left.x + left.y * left.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.y = 0.0f; // Fix Y-component to 0 as it's axial, forcing rotation only around Y. left.z *= invLength; // compute the local orientation matrix for the billboard @@ -238,15 +281,16 @@ private void rotateAxial(Camera camera, Vector3f axis) { orient.set(0, 1, 0); orient.set(0, 2, left.x); orient.set(1, 0, 0); - orient.set(1, 1, 1); + orient.set(1, 1, 1); // Y-axis remains fixed (no rotation along Y). 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; + left.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); @@ -257,13 +301,11 @@ private void rotateAxial(Camera camera, Vector3f axis) { orient.set(1, 2, 0); orient.set(2, 0, 0); orient.set(2, 1, 0); - orient.set(2, 2, 1); + orient.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. + // Apply the calculated local rotation matrix to the spatial. spatial.setLocalRotation(orient); - fixRefreshFlags(); } /** @@ -277,32 +319,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); } } From b2c19b1bc35c7babaefc16077d9be4b28824a017 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Fri, 13 Jun 2025 14:41:21 +0200 Subject: [PATCH 2/8] Update TestBillboard.java --- .../jme3test/model/shape/TestBillboard.java | 101 ++++++++---------- 1 file changed, 45 insertions(+), 56 deletions(-) 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 + +} From 837b6238ee2fa2fde196ae61372da6a7d13f0d76 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Fri, 13 Jun 2025 15:00:28 +0200 Subject: [PATCH 3/8] BillboardControl: remove fixRefreshFlags() method --- .../com/jme3/scene/control/BillboardControl.java | 13 ------------- 1 file changed, 13 deletions(-) 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 750100a9b2..ebef77f6d0 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 @@ -119,19 +119,6 @@ protected void controlUpdate(float tpf) { protected void controlRender(RenderManager rm, ViewPort vp) { Camera cam = vp.getCamera(); rotateBillboard(cam); - updateRefreshFlags(); - } - - private void updateRefreshFlags() { - // force transforms to update below this node - spatial.updateGeometricState(); - - // force world bound to update - Spatial rootNode = spatial; - while (rootNode.getParent() != null) { - rootNode = rootNode.getParent(); - } - rootNode.getWorldBound(); } /** From 36a9f2d2035889e20aabdd7d2f79bbd2a5ff3f2a Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 14 Jun 2025 11:30:54 +0200 Subject: [PATCH 4/8] Update BillboardControl.java: remove unused imports --- .../src/main/java/com/jme3/scene/control/BillboardControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ebef77f6d0..6a19ced370 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 @@ -43,7 +43,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Node; -import com.jme3.scene.Spatial; + import java.io.IOException; /** From 5a0948afbebaed6dca1c17b8edb970fa885e7f99 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 16 Jun 2025 19:44:27 +0200 Subject: [PATCH 5/8] BillboardControl: reduce object allocations --- .../jme3/scene/control/BillboardControl.java | 127 ++++++++++-------- 1 file changed, 68 insertions(+), 59 deletions(-) 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 6a19ced370..e0c0f8dd5e 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 @@ -43,6 +43,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Node; +import com.jme3.util.TempVars; import java.io.IOException; @@ -62,10 +63,9 @@ public class BillboardControl extends AbstractControl { // Member variables for calculations, reused to avoid constant object allocation. - private final Matrix3f orient = new Matrix3f(); - private final Vector3f look = new Vector3f(); - private final Vector3f left = new Vector3f(); - private final Quaternion tempQuat = new Quaternion(); + 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. @@ -85,8 +85,8 @@ public enum Alignment { */ 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. @@ -152,35 +152,35 @@ private void rotateBillboard(Camera cam) { * @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); + 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(orient); + spatial.setLocalRotation(tempMat3); } /** @@ -193,23 +193,30 @@ private void rotateCameraAligned(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(); - tempQuat.fromRotationMatrix(orient); - Quaternion rot = tempQuat; + 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); + + vars.release(); } /** @@ -225,12 +232,12 @@ 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( + tempDir.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; + 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; @@ -240,9 +247,9 @@ private void rotateAxial(Camera camera, Vector3f axis) { // for axial rotation. float lengthSquared; if (axis.y == 1) { // AxialY: projection on XZ plane - lengthSquared = left.x * left.x + left.z * left.z; + lengthSquared = tempLeft.x * tempLeft.x + tempLeft.z * tempLeft.z; } else if (axis.z == 1) { // AxialZ: projection on XY plane - lengthSquared = left.x * left.x + left.y * left.y; + 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. @@ -259,40 +266,42 @@ private void rotateAxial(Camera camera, Vector3f axis) { // 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; // Fix Y-component to 0 as it's axial, forcing rotation only around Y. - left.z *= invLength; + System.out.println("y1"); + 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); // Y-axis remains fixed (no rotation along Y). - 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; // Fix Z-component to 0 as it's axial, forcing rotation only around Z. + System.out.println("z1"); + 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); // Z-axis remains fixed (no rotation along Z). + 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). } // Apply the calculated local rotation matrix to the spatial. - spatial.setLocalRotation(orient); + spatial.setLocalRotation(tempMat3); } /** From b3cd1f722cdf7deb0390b2af73a6b6eba99fc4d9 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 16 Jun 2025 19:58:42 +0200 Subject: [PATCH 6/8] Update BillboardControl: remove System.out.println oops --- .../src/main/java/com/jme3/scene/control/BillboardControl.java | 2 -- 1 file changed, 2 deletions(-) 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 e0c0f8dd5e..a17f155de8 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 @@ -266,7 +266,6 @@ private void rotateAxial(Camera camera, Vector3f axis) { // Unitize the projection to get a normalized direction vector in the plane. float invLength = FastMath.invSqrt(lengthSquared); if (axis.y == 1) { - System.out.println("y1"); tempLeft.x *= invLength; tempLeft.y = 0.0f; // Fix Y-component to 0 as it's axial, forcing rotation only around Y. tempLeft.z *= invLength; @@ -283,7 +282,6 @@ private void rotateAxial(Camera camera, Vector3f axis) { tempMat3.set(2, 2, tempLeft.z); } else if (axis.z == 1) { - System.out.println("z1"); tempLeft.x *= invLength; tempLeft.y *= invLength; tempLeft.z = 0.0f; // Fix Z-component to 0 as it's axial, forcing rotation only around Z. From 45e53442fb882d689891b1df550c374d299093a7 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Wed, 25 Jun 2025 20:37:29 +0200 Subject: [PATCH 7/8] fix BillboardControl: updateGeometricState --- .../java/com/jme3/scene/control/BillboardControl.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 a17f155de8..ac9c94abf1 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 @@ -43,6 +43,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Node; +import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import java.io.IOException; @@ -119,6 +120,12 @@ protected void controlUpdate(float tpf) { protected void controlRender(RenderManager rm, ViewPort vp) { Camera cam = vp.getCamera(); rotateBillboard(cam); + fixRefreshFlags(); + } + + private void fixRefreshFlags() { + // force transforms to update below this node + spatial.updateGeometricState(); } /** @@ -232,8 +239,7 @@ 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. - tempDir.set(camera.getLocation()).subtractLocal( - spatial.getWorldTranslation()); + 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; From 9f5c4373028f178ca01717ebb9a7a35bdccfb980 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Wed, 9 Jul 2025 20:25:26 +0200 Subject: [PATCH 8/8] Update BillboardControl: fixRefreshFlags --- .../main/java/com/jme3/scene/control/BillboardControl.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 ac9c94abf1..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 @@ -126,6 +126,13 @@ protected void controlRender(RenderManager rm, ViewPort vp) { private void fixRefreshFlags() { // force transforms to update below this node spatial.updateGeometricState(); + + // force world bound to update + Spatial rootNode = spatial; + while (rootNode.getParent() != null) { + rootNode = rootNode.getParent(); + } + rootNode.getWorldBound(); } /**