11/*
2- * Copyright (c) 2009-2023 jMonkeyEngine
2+ * Copyright (c) 2009-2025 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
4949import java .io .IOException ;
5050
5151/**
52- * This Control maintains a reference to a Light,
53- * which will be synched with the position (worldTranslation)
54- * of the current spatial.
52+ * `LightControl` synchronizes the world transformation (position and/or
53+ * direction) of a `Light` with its attached `Spatial`. This control allows
54+ * a light to follow a spatial or vice-versa, depending on the chosen
55+ * {@link ControlDirection}.
56+ * <p>
57+ * This is particularly useful for attaching lights to animated characters,
58+ * moving vehicles, or dynamically controlled objects.
59+ * </p>
5560 *
56- * @author tim
61+ * @author Tim
62+ * @author Markil 3
63+ * @author capdevon
5764 */
5865public class LightControl extends AbstractControl {
5966
60- private static final String CONTROL_DIR_NAME = "controlDir" ;
61- private static final String LIGHT_NAME = " light" ;
62-
67+ /**
68+ * Defines the direction of synchronization between the light and the spatial.
69+ */
6370 public enum ControlDirection {
64-
6571 /**
66- * Means, that the Light's transform is "copied"
67- * to the Transform of the Spatial.
72+ * The light's transform is copied to the spatial's transform.
6873 */
6974 LightToSpatial ,
7075 /**
71- * Means, that the Spatial's transform is "copied"
72- * to the Transform of the light.
76+ * The spatial's transform is copied to the light's transform.
7377 */
7478 SpatialToLight
7579 }
7680
81+ /**
82+ * Represents the local axis of the spatial (X, Y, or Z) to be used
83+ * for determining the light's direction when `ControlDirection` is
84+ * `SpatialToLight`.
85+ */
86+ public enum Axis {
87+ X , Y , Z
88+ }
89+
7790 private Light light ;
7891 private ControlDirection controlDir = ControlDirection .SpatialToLight ;
92+ private Axis axisRotation = Axis .Z ;
93+ private boolean invertAxisDirection = false ;
7994
8095 /**
81- * Constructor used for Serialization .
96+ * For serialization only. Do not use .
8297 */
8398 public LightControl () {
8499 }
85100
86101 /**
102+ * Creates a new `LightControl` that synchronizes the light's transform to the spatial.
103+ *
87104 * @param light The light to be synced.
105+ * @throws IllegalArgumentException if the light type is not supported
106+ * (only Point, Directional, and Spot lights are supported).
88107 */
89108 public LightControl (Light light ) {
109+ validateSupportedLightType (light );
90110 this .light = light ;
91111 }
92112
93113 /**
114+ * Creates a new `LightControl` with a specified synchronization direction.
115+ *
94116 * @param light The light to be synced.
95- * @param controlDir SpatialToLight or LightToSpatial
117+ * @param controlDir The direction of synchronization (SpatialToLight or LightToSpatial).
118+ * @throws IllegalArgumentException if the light type is not supported
119+ * (only Point, Directional, and Spot lights are supported).
96120 */
97121 public LightControl (Light light , ControlDirection controlDir ) {
122+ validateSupportedLightType (light );
98123 this .light = light ;
99124 this .controlDir = controlDir ;
100125 }
@@ -104,6 +129,7 @@ public Light getLight() {
104129 }
105130
106131 public void setLight (Light light ) {
132+ validateSupportedLightType (light );
107133 this .light = light ;
108134 }
109135
@@ -115,86 +141,141 @@ public void setControlDir(ControlDirection controlDir) {
115141 this .controlDir = controlDir ;
116142 }
117143
118- // fields used when inverting ControlDirection:
144+ public Axis getAxisRotation () {
145+ return axisRotation ;
146+ }
147+
148+ public void setAxisRotation (Axis axisRotation ) {
149+ this .axisRotation = axisRotation ;
150+ }
151+
152+ public boolean isInvertAxisDirection () {
153+ return invertAxisDirection ;
154+ }
155+
156+ public void setInvertAxisDirection (boolean invertAxisDirection ) {
157+ this .invertAxisDirection = invertAxisDirection ;
158+ }
159+
160+ private void validateSupportedLightType (Light light ) {
161+ if (light == null ) {
162+ return ;
163+ }
164+
165+ switch (light .getType ()) {
166+ case Point :
167+ case Directional :
168+ case Spot :
169+ // These types are supported, validation passes.
170+ break ;
171+ default :
172+ throw new IllegalArgumentException (
173+ "Unsupported Light type: " + light .getType ());
174+ }
175+ }
176+
119177 @ Override
120178 protected void controlUpdate (float tpf ) {
121- if (spatial != null && light != null ) {
122- switch (controlDir ) {
123- case SpatialToLight :
124- spatialToLight (light );
125- break ;
126- case LightToSpatial :
127- lightToSpatial (light );
128- break ;
129- }
179+ if (light == null ) {
180+ return ;
181+ }
182+
183+ switch (controlDir ) {
184+ case SpatialToLight :
185+ spatialToLight (light );
186+ break ;
187+ case LightToSpatial :
188+ lightToSpatial (light );
189+ break ;
130190 }
131191 }
132192
133193 /**
134- * Sets the light to adopt the spatial's world transformations.
194+ * Updates the light's position and/or direction to match the spatial's
195+ * world transformation.
135196 *
136- * @author Markil 3
137- * @author pspeed42
197+ * @param light The light whose properties will be set.
138198 */
139199 private void spatialToLight (Light light ) {
140200 TempVars vars = TempVars .get ();
141201
142- final Vector3f worldTranslation = vars .vect1 ;
143- worldTranslation .set (spatial .getWorldTranslation ());
144- final Vector3f worldDirection = vars .vect2 ;
145- spatial .getWorldRotation ().mult (Vector3f .UNIT_Z , worldDirection ).negateLocal ();
202+ final Vector3f worldPosition = vars .vect1 ;
203+ worldPosition .set (spatial .getWorldTranslation ());
204+
205+ final Vector3f lightDirection = vars .vect2 ;
206+ spatial .getWorldRotation ().getRotationColumn (axisRotation .ordinal (), lightDirection );
207+ if (invertAxisDirection ) {
208+ lightDirection .negateLocal ();
209+ }
146210
147211 if (light instanceof PointLight ) {
148- ((PointLight ) light ).setPosition (worldTranslation );
212+ ((PointLight ) light ).setPosition (worldPosition );
213+
149214 } else if (light instanceof DirectionalLight ) {
150- ((DirectionalLight ) light ).setDirection (worldDirection );
215+ ((DirectionalLight ) light ).setDirection (lightDirection );
216+
151217 } else if (light instanceof SpotLight ) {
152- final SpotLight spotLight = (SpotLight ) light ;
153- spotLight .setPosition (worldTranslation );
154- spotLight .setDirection (worldDirection );
218+ SpotLight sl = (SpotLight ) light ;
219+ sl .setPosition (worldPosition );
220+ sl .setDirection (lightDirection );
155221 }
156222 vars .release ();
157223 }
158224
159225 /**
160- * Sets the spatial to adopt the light's world transformations.
226+ * Updates the spatial's local transformation (position and/or rotation)
227+ * to match the light's world transformation.
161228 *
162- * @author Markil 3
229+ * @param light The light from which properties will be read.
163230 */
164231 private void lightToSpatial (Light light ) {
165232 TempVars vars = TempVars .get ();
166- Vector3f translation = vars .vect1 ;
167- Vector3f direction = vars .vect2 ;
233+ Vector3f lightPosition = vars .vect1 ;
234+ Vector3f lightDirection = vars .vect2 ;
168235 Quaternion rotation = vars .quat1 ;
169- boolean rotateSpatial = false , translateSpatial = false ;
236+ boolean rotateSpatial = false ;
237+ boolean translateSpatial = false ;
170238
171239 if (light instanceof PointLight ) {
172- PointLight pLight = (PointLight ) light ;
173- translation .set (pLight .getPosition ());
240+ PointLight pl = (PointLight ) light ;
241+ lightPosition .set (pl .getPosition ());
174242 translateSpatial = true ;
243+
175244 } else if (light instanceof DirectionalLight ) {
176- DirectionalLight dLight = (DirectionalLight ) light ;
177- direction .set (dLight .getDirection ()).negateLocal ();
245+ DirectionalLight dl = (DirectionalLight ) light ;
246+ lightDirection .set (dl .getDirection ());
247+ if (invertAxisDirection ) {
248+ lightDirection .negateLocal ();
249+ }
178250 rotateSpatial = true ;
251+
179252 } else if (light instanceof SpotLight ) {
180- SpotLight sLight = (SpotLight ) light ;
181- translation .set (sLight .getPosition ());
182- direction .set (sLight .getDirection ()).negateLocal ();
183- translateSpatial = rotateSpatial = true ;
253+ SpotLight sl = (SpotLight ) light ;
254+ lightPosition .set (sl .getPosition ());
255+ lightDirection .set (sl .getDirection ());
256+ if (invertAxisDirection ) {
257+ lightDirection .negateLocal ();
258+ }
259+ translateSpatial = true ;
260+ rotateSpatial = true ;
184261 }
262+
263+ // Transform light's world properties to spatial's parent's local space
185264 if (spatial .getParent () != null ) {
265+ // Get inverse of parent's world matrix
186266 spatial .getParent ().getLocalToWorldMatrix (vars .tempMat4 ).invertLocal ();
187- vars .tempMat4 .rotateVect (translation );
188- vars .tempMat4 .translateVect (translation );
189- vars .tempMat4 .rotateVect (direction );
267+ vars .tempMat4 .rotateVect (lightPosition );
268+ vars .tempMat4 .translateVect (lightPosition );
269+ vars .tempMat4 .rotateVect (lightDirection );
190270 }
191271
272+ // Apply transformed properties to spatial's local transformation
192273 if (rotateSpatial ) {
193- rotation .lookAt (direction , Vector3f .UNIT_Y ).normalizeLocal ();
274+ rotation .lookAt (lightDirection , Vector3f .UNIT_Y ).normalizeLocal ();
194275 spatial .setLocalRotation (rotation );
195276 }
196277 if (translateSpatial ) {
197- spatial .setLocalTranslation (translation );
278+ spatial .setLocalTranslation (lightPosition );
198279 }
199280 vars .release ();
200281 }
@@ -214,15 +295,31 @@ public void cloneFields(final Cloner cloner, final Object original) {
214295 public void read (JmeImporter im ) throws IOException {
215296 super .read (im );
216297 InputCapsule ic = im .getCapsule (this );
217- controlDir = ic .readEnum (CONTROL_DIR_NAME , ControlDirection .class , ControlDirection .SpatialToLight );
218- light = (Light ) ic .readSavable (LIGHT_NAME , null );
298+ light = (Light ) ic .readSavable ("light" , null );
299+ controlDir = ic .readEnum ("controlDir" , ControlDirection .class , ControlDirection .SpatialToLight );
300+ axisRotation = ic .readEnum ("axisRotation" , Axis .class , Axis .Z );
301+ invertAxisDirection = ic .readBoolean ("invertAxisDirection" , false );
219302 }
220303
221304 @ Override
222305 public void write (JmeExporter ex ) throws IOException {
223306 super .write (ex );
224307 OutputCapsule oc = ex .getCapsule (this );
225- oc .write (controlDir , CONTROL_DIR_NAME , ControlDirection .SpatialToLight );
226- oc .write (light , LIGHT_NAME , null );
308+ oc .write (light , "light" , null );
309+ oc .write (controlDir , "controlDir" , ControlDirection .SpatialToLight );
310+ oc .write (axisRotation , "axisRotation" , Axis .Z );
311+ oc .write (invertAxisDirection , "invertAxisDirection" , false );
312+ }
313+
314+ @ Override
315+ public String toString () {
316+ return getClass ().getSimpleName () +
317+ "[light=" + (light != null ? light .getType () : null ) +
318+ ", controlDir=" + controlDir +
319+ ", axisRotation=" + axisRotation +
320+ ", invertAxisDirection=" + invertAxisDirection +
321+ ", enabled=" + enabled +
322+ ", spatial=" + spatial +
323+ "]" ;
227324 }
228- }
325+ }
0 commit comments