Skip to content

Commit f69f859

Browse files
authored
Update BillboardControl.java
1 parent 9a10591 commit f69f859

1 file changed

Lines changed: 103 additions & 67 deletions

File tree

jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java

Lines changed: 103 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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
@@ -46,12 +46,31 @@
4646
import com.jme3.scene.Spatial;
4747
import 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+
*/
4962
public 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

Comments
 (0)