11/*
2- * Copyright (c) 2009-2021 jMonkeyEngine
2+ * Copyright (c) 2009-2025 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
4646import com .jme3 .scene .Spatial ;
4747import java .io .IOException ;
4848
49+ /**
50+ * <code>BillboardControl</code> is a special control that makes a spatial always
51+ * face the camera. This is useful for health bars, or other 2D elements
52+ * that should always be oriented towards the viewer in a 3D scene.
53+ * <p>
54+ * The alignment can be customized to different modes:
55+ * <ul>
56+ * <li>Screen: The billboard always faces the screen, keeping its 'up' vector aligned with camera's 'up'.</li>
57+ * <li>Camera: The billboard always faces the camera position directly.</li>
58+ * <li>AxialY: The billboard faces the camera but keeps its local Y-axis fixed.</li>
59+ * <li>AxialZ: The billboard faces the camera but keeps its local Z-axis fixed.</li>
60+ * </ul>
61+ */
4962public class BillboardControl extends AbstractControl {
5063
51- private Matrix3f orient ;
52- private Vector3f look ;
53- private Vector3f left ;
54- private Alignment alignment ;
64+ // Member variables for calculations, reused to avoid constant object allocation.
65+ private final Matrix3f orient = new Matrix3f ();
66+ private final Vector3f look = new Vector3f ();
67+ private final Vector3f left = new Vector3f ();
68+ private final Quaternion tempQuat = new Quaternion ();
69+
70+ /**
71+ * The current alignment mode for the billboard.
72+ */
73+ private Alignment alignment = Alignment .Screen ;
5574
5675 /**
5776 * Determines how the billboard is aligned to the screen/camera.
@@ -61,38 +80,36 @@ public enum Alignment {
6180 * Aligns this Billboard to the screen.
6281 */
6382 Screen ,
64-
6583 /**
6684 * Aligns this Billboard to the camera position.
6785 */
6886 Camera ,
69-
7087 /**
7188 * Aligns this Billboard to the screen, but keeps the Y axis fixed.
7289 */
7390 AxialY ,
74-
7591 /**
7692 * Aligns this Billboard to the screen, but keeps the Z axis fixed.
7793 */
7894 AxialZ ;
7995 }
8096
97+ /**
98+ * Constructs a new `BillboardControl` with the default alignment set to
99+ * {@link Alignment#Screen}.
100+ */
81101 public BillboardControl () {
82- super ();
83- orient = new Matrix3f ();
84- look = new Vector3f ();
85- left = new Vector3f ();
86- alignment = Alignment .Screen ;
87102 }
88103
89- // default implementation from AbstractControl is equivalent
90- //public Control cloneForSpatial(Spatial spatial) {
91- // BillboardControl control = new BillboardControl();
92- // control.alignment = this.alignment;
93- // control.setSpatial(spatial);
94- // return control;
95- //}
104+ /**
105+ * Constructs a new `BillboardControl` with the specified alignment.
106+ *
107+ * @param alignment The desired alignment type for the billboard.
108+ * See {@link Alignment} for available options.
109+ */
110+ public BillboardControl (Alignment alignment ) {
111+ this .alignment = alignment ;
112+ }
96113
97114 @ Override
98115 protected void controlUpdate (float tpf ) {
@@ -102,25 +119,26 @@ protected void controlUpdate(float tpf) {
102119 protected void controlRender (RenderManager rm , ViewPort vp ) {
103120 Camera cam = vp .getCamera ();
104121 rotateBillboard (cam );
122+ updateRefreshFlags ();
105123 }
106124
107- private void fixRefreshFlags () {
125+ private void updateRefreshFlags () {
108126 // force transforms to update below this node
109127 spatial .updateGeometricState ();
110128
111129 // force world bound to update
112130 Spatial rootNode = spatial ;
113- while (rootNode .getParent () != null ){
131+ while (rootNode .getParent () != null ) {
114132 rootNode = rootNode .getParent ();
115133 }
116134 rootNode .getWorldBound ();
117135 }
118136
119137 /**
120- * rotate the billboard based on the type set
138+ * Rotates the billboard based on the alignment type set.
139+ * This method is called every frame during the render phase.
121140 *
122- * @param cam
123- * Camera
141+ * @param cam The current Camera used for rendering.
124142 */
125143 private void rotateBillboard (Camera cam ) {
126144 switch (alignment ) {
@@ -140,10 +158,11 @@ private void rotateBillboard(Camera cam) {
140158 }
141159
142160 /**
143- * Aligns this Billboard so that it points to the camera position.
161+ * Aligns this Billboard so that it points directly to the camera position.
162+ * The billboard's local rotation is set to ensure its positive Z-axis
163+ * points towards the camera's location.
144164 *
145- * @param camera
146- * Camera
165+ * @param camera The current Camera.
147166 */
148167 private void rotateCameraAligned (Camera camera ) {
149168 look .set (camera .getLocation ()).subtractLocal (
@@ -173,40 +192,47 @@ private void rotateCameraAligned(Camera camera) {
173192 orient .set (2 , 1 , xzp .z * -look .y );
174193 orient .set (2 , 2 , xzp .z * cosp );
175194
176- // The billboard must be oriented to face the camera before it is
177- // transformed into the world.
195+ // Set the billboard's local rotation based on the computed orientation matrix.
178196 spatial .setLocalRotation (orient );
179- fixRefreshFlags ();
180197 }
181198
182199 /**
183200 * Rotates the billboard so it points directly opposite the direction the
184- * camera is facing.
201+ * camera is facing (screen-aligned). This means the billboard will always
202+ * be flat against the screen, regardless of its position in 3D space.
203+ * Its Z-axis will point against the camera's direction, and its Y-axis
204+ * will align with the camera's Y-axis.
185205 *
186- * @param camera
187- * Camera
206+ * @param camera The current Camera.
188207 */
189208 private void rotateScreenAligned (Camera camera ) {
190209 // co-opt diff for our in direction:
191210 look .set (camera .getDirection ()).negateLocal ();
192211 // co-opt loc for our left direction:
193212 left .set (camera .getLeft ()).negateLocal ();
194213 orient .fromAxes (left , camera .getUp (), look );
214+
195215 Node parent = spatial .getParent ();
196- Quaternion rot = new Quaternion ().fromRotationMatrix (orient );
216+ tempQuat .fromRotationMatrix (orient );
217+ Quaternion rot = tempQuat ;
218+
197219 if (parent != null ) {
198- rot = parent .getWorldRotation ().inverse ().multLocal (rot );
220+ rot = parent .getWorldRotation ().inverse ().multLocal (rot );
199221 rot .normalizeLocal ();
200222 }
223+
224+ // Apply the calculated local rotation to the spatial.
201225 spatial .setLocalRotation (rot );
202- fixRefreshFlags ();
203226 }
204227
205228 /**
206- * Rotate the billboard towards the camera, but keeping a given axis fixed.
229+ * Rotates the billboard towards the camera, but keeps a given axis fixed.
230+ * This is used for {@link Alignment#AxialY} (fixed Y-axis) or
231+ * {@link Alignment#AxialZ} (fixed Z-axis) alignments. The billboard will
232+ * only rotate around the specified axis.
207233 *
208- * @param camera
209- * Camera
234+ * @param camera The current Camera.
235+ * @param axis The fixed axis (e.g., {@link Vector3f#UNIT_Y} for AxialY).
210236 */
211237 private void rotateAxial (Camera camera , Vector3f axis ) {
212238 // Compute the additional rotation required for the billboard to face
@@ -220,33 +246,51 @@ private void rotateAxial(Camera camera, Vector3f axis) {
220246 left .z *= 1.0f / spatial .getWorldScale ().z ;
221247
222248 // squared length of the camera projection in the xz-plane
223- float lengthSquared = left .x * left .x + left .z * left .z ;
249+ // float lengthSquared = left.x * left.x + left.z * left.z;
250+
251+ // Calculate squared length of the camera projection on the plane perpendicular
252+ // to the fixed axis. This determines the magnitude of the projection used
253+ // for axial rotation.
254+ float lengthSquared ;
255+ if (axis .y == 1 ) { // AxialY: projection on XZ plane
256+ lengthSquared = left .x * left .x + left .z * left .z ;
257+ } else if (axis .z == 1 ) { // AxialZ: projection on XY plane
258+ lengthSquared = left .x * left .x + left .y * left .y ;
259+ } else {
260+ // This case should ideally not be reached with the current Alignment enum,
261+ // but provides robustness for unexpected 'axis' values.
262+ return ;
263+ }
264+
265+ // Check for edge case: camera is directly on the fixed axis relative to the billboard.
266+ // If the projection length is too small, the rotation is undefined.
224267 if (lengthSquared < FastMath .FLT_EPSILON ) {
225- // camera on the billboard axis, rotation not defined
268+ // Rotation is undefined, so no rotation is applied.
226269 return ;
227270 }
228271
229- // unitize the projection
272+ // Unitize the projection to get a normalized direction vector in the plane.
230273 float invLength = FastMath .invSqrt (lengthSquared );
231274 if (axis .y == 1 ) {
232275 left .x *= invLength ;
233- left .y = 0.0f ;
276+ left .y = 0.0f ; // Fix Y-component to 0 as it's axial, forcing rotation only around Y.
234277 left .z *= invLength ;
235278
236279 // compute the local orientation matrix for the billboard
237280 orient .set (0 , 0 , left .z );
238281 orient .set (0 , 1 , 0 );
239282 orient .set (0 , 2 , left .x );
240283 orient .set (1 , 0 , 0 );
241- orient .set (1 , 1 , 1 );
284+ orient .set (1 , 1 , 1 ); // Y-axis remains fixed (no rotation along Y).
242285 orient .set (1 , 2 , 0 );
243286 orient .set (2 , 0 , -left .x );
244287 orient .set (2 , 1 , 0 );
245288 orient .set (2 , 2 , left .z );
289+
246290 } else if (axis .z == 1 ) {
247291 left .x *= invLength ;
248292 left .y *= invLength ;
249- left .z = 0.0f ;
293+ left .z = 0.0f ; // Fix Z-component to 0 as it's axial, forcing rotation only around Z.
250294
251295 // compute the local orientation matrix for the billboard
252296 orient .set (0 , 0 , left .y );
@@ -257,13 +301,11 @@ private void rotateAxial(Camera camera, Vector3f axis) {
257301 orient .set (1 , 2 , 0 );
258302 orient .set (2 , 0 , 0 );
259303 orient .set (2 , 1 , 0 );
260- orient .set (2 , 2 , 1 );
304+ orient .set (2 , 2 , 1 ); // Z-axis remains fixed (no rotation along Z).
261305 }
262306
263- // The billboard must be oriented to face the camera before it is
264- // transformed into the world.
307+ // Apply the calculated local rotation matrix to the spatial.
265308 spatial .setLocalRotation (orient );
266- fixRefreshFlags ();
267309 }
268310
269311 /**
@@ -277,32 +319,26 @@ public Alignment getAlignment() {
277319
278320 /**
279321 * Sets the type of rotation this Billboard will have. The alignment can
280- * be Camera, Screen, AxialY, or AxialZ. Invalid alignments will
281- * assume no billboard rotation .
322+ * be {@link Alignment# Camera}, {@link Alignment# Screen},
323+ * {@link Alignment#AxialY}, or {@link Alignment#AxialZ} .
282324 *
283- * @param alignment the desired alignment (Camera/Screen/AxialY/AxialZ)
325+ * @param alignment The desired {@link Alignment} for the billboard's rotation behavior.
284326 */
285327 public void setAlignment (Alignment alignment ) {
286328 this .alignment = alignment ;
287329 }
288330
289331 @ Override
290- public void write (JmeExporter e ) throws IOException {
291- super .write (e );
292- OutputCapsule capsule = e .getCapsule (this );
293- capsule .write (orient , "orient" , null );
294- capsule .write (look , "look" , null );
295- capsule .write (left , "left" , null );
296- capsule .write (alignment , "alignment" , Alignment .Screen );
332+ public void write (JmeExporter ex ) throws IOException {
333+ super .write (ex );
334+ OutputCapsule oc = ex .getCapsule (this );
335+ oc .write (alignment , "alignment" , Alignment .Screen );
297336 }
298337
299338 @ Override
300- public void read (JmeImporter importer ) throws IOException {
301- super .read (importer );
302- InputCapsule capsule = importer .getCapsule (this );
303- orient = (Matrix3f ) capsule .readSavable ("orient" , null );
304- look = (Vector3f ) capsule .readSavable ("look" , null );
305- left = (Vector3f ) capsule .readSavable ("left" , null );
306- alignment = capsule .readEnum ("alignment" , Alignment .class , Alignment .Screen );
339+ public void read (JmeImporter im ) throws IOException {
340+ super .read (im );
341+ InputCapsule ic = im .getCapsule (this );
342+ alignment = ic .readEnum ("alignment" , Alignment .class , Alignment .Screen );
307343 }
308344}
0 commit comments