diff --git a/jme3-core/src/main/java/com/jme3/scene/control/LightControl.java b/jme3-core/src/main/java/com/jme3/scene/control/LightControl.java index 0b8bf40a96..3c0619b912 100644 --- a/jme3-core/src/main/java/com/jme3/scene/control/LightControl.java +++ b/jme3-core/src/main/java/com/jme3/scene/control/LightControl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2023 jMonkeyEngine + * Copyright (c) 2009-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -49,52 +49,77 @@ import java.io.IOException; /** - * This Control maintains a reference to a Light, - * which will be synched with the position (worldTranslation) - * of the current spatial. + * `LightControl` synchronizes the world transformation (position and/or + * direction) of a `Light` with its attached `Spatial`. This control allows + * a light to follow a spatial or vice-versa, depending on the chosen + * {@link ControlDirection}. + *
+ * This is particularly useful for attaching lights to animated characters, + * moving vehicles, or dynamically controlled objects. + *
* - * @author tim + * @author Tim + * @author Markil 3 + * @author capdevon */ public class LightControl extends AbstractControl { - private static final String CONTROL_DIR_NAME = "controlDir"; - private static final String LIGHT_NAME = "light"; - + /** + * Defines the direction of synchronization between the light and the spatial. + */ public enum ControlDirection { - /** - * Means, that the Light's transform is "copied" - * to the Transform of the Spatial. + * The light's transform is copied to the spatial's transform. */ LightToSpatial, /** - * Means, that the Spatial's transform is "copied" - * to the Transform of the light. + * The spatial's transform is copied to the light's transform. */ SpatialToLight } + /** + * Represents the local axis of the spatial (X, Y, or Z) to be used + * for determining the light's direction when `ControlDirection` is + * `SpatialToLight`. + */ + public enum Axis { + X, Y, Z + } + private Light light; private ControlDirection controlDir = ControlDirection.SpatialToLight; + private Axis axisRotation = Axis.Z; + private boolean invertAxisDirection = false; /** - * Constructor used for Serialization. + * For serialization only. Do not use. */ public LightControl() { } /** + * Creates a new `LightControl` that synchronizes the light's transform to the spatial. + * * @param light The light to be synced. + * @throws IllegalArgumentException if the light type is not supported + * (only Point, Directional, and Spot lights are supported). */ public LightControl(Light light) { + validateSupportedLightType(light); this.light = light; } /** + * Creates a new `LightControl` with a specified synchronization direction. + * * @param light The light to be synced. - * @param controlDir SpatialToLight or LightToSpatial + * @param controlDir The direction of synchronization (SpatialToLight or LightToSpatial). + * @throws IllegalArgumentException if the light type is not supported + * (only Point, Directional, and Spot lights are supported). */ public LightControl(Light light, ControlDirection controlDir) { + validateSupportedLightType(light); this.light = light; this.controlDir = controlDir; } @@ -104,6 +129,7 @@ public Light getLight() { } public void setLight(Light light) { + validateSupportedLightType(light); this.light = light; } @@ -115,86 +141,141 @@ public void setControlDir(ControlDirection controlDir) { this.controlDir = controlDir; } - // fields used when inverting ControlDirection: + public Axis getAxisRotation() { + return axisRotation; + } + + public void setAxisRotation(Axis axisRotation) { + this.axisRotation = axisRotation; + } + + public boolean isInvertAxisDirection() { + return invertAxisDirection; + } + + public void setInvertAxisDirection(boolean invertAxisDirection) { + this.invertAxisDirection = invertAxisDirection; + } + + private void validateSupportedLightType(Light light) { + if (light == null) { + return; + } + + switch (light.getType()) { + case Point: + case Directional: + case Spot: + // These types are supported, validation passes. + break; + default: + throw new IllegalArgumentException( + "Unsupported Light type: " + light.getType()); + } + } + @Override protected void controlUpdate(float tpf) { - if (spatial != null && light != null) { - switch (controlDir) { - case SpatialToLight: - spatialToLight(light); - break; - case LightToSpatial: - lightToSpatial(light); - break; - } + if (light == null) { + return; + } + + switch (controlDir) { + case SpatialToLight: + spatialToLight(light); + break; + case LightToSpatial: + lightToSpatial(light); + break; } } /** - * Sets the light to adopt the spatial's world transformations. + * Updates the light's position and/or direction to match the spatial's + * world transformation. * - * @author Markil 3 - * @author pspeed42 + * @param light The light whose properties will be set. */ private void spatialToLight(Light light) { TempVars vars = TempVars.get(); - final Vector3f worldTranslation = vars.vect1; - worldTranslation.set(spatial.getWorldTranslation()); - final Vector3f worldDirection = vars.vect2; - spatial.getWorldRotation().mult(Vector3f.UNIT_Z, worldDirection).negateLocal(); + final Vector3f worldPosition = vars.vect1; + worldPosition.set(spatial.getWorldTranslation()); + + final Vector3f lightDirection = vars.vect2; + spatial.getWorldRotation().getRotationColumn(axisRotation.ordinal(), lightDirection); + if (invertAxisDirection) { + lightDirection.negateLocal(); + } if (light instanceof PointLight) { - ((PointLight) light).setPosition(worldTranslation); + ((PointLight) light).setPosition(worldPosition); + } else if (light instanceof DirectionalLight) { - ((DirectionalLight) light).setDirection(worldDirection); + ((DirectionalLight) light).setDirection(lightDirection); + } else if (light instanceof SpotLight) { - final SpotLight spotLight = (SpotLight) light; - spotLight.setPosition(worldTranslation); - spotLight.setDirection(worldDirection); + SpotLight sl = (SpotLight) light; + sl.setPosition(worldPosition); + sl.setDirection(lightDirection); } vars.release(); } /** - * Sets the spatial to adopt the light's world transformations. + * Updates the spatial's local transformation (position and/or rotation) + * to match the light's world transformation. * - * @author Markil 3 + * @param light The light from which properties will be read. */ private void lightToSpatial(Light light) { TempVars vars = TempVars.get(); - Vector3f translation = vars.vect1; - Vector3f direction = vars.vect2; + Vector3f lightPosition = vars.vect1; + Vector3f lightDirection = vars.vect2; Quaternion rotation = vars.quat1; - boolean rotateSpatial = false, translateSpatial = false; + boolean rotateSpatial = false; + boolean translateSpatial = false; if (light instanceof PointLight) { - PointLight pLight = (PointLight) light; - translation.set(pLight.getPosition()); + PointLight pl = (PointLight) light; + lightPosition.set(pl.getPosition()); translateSpatial = true; + } else if (light instanceof DirectionalLight) { - DirectionalLight dLight = (DirectionalLight) light; - direction.set(dLight.getDirection()).negateLocal(); + DirectionalLight dl = (DirectionalLight) light; + lightDirection.set(dl.getDirection()); + if (invertAxisDirection) { + lightDirection.negateLocal(); + } rotateSpatial = true; + } else if (light instanceof SpotLight) { - SpotLight sLight = (SpotLight) light; - translation.set(sLight.getPosition()); - direction.set(sLight.getDirection()).negateLocal(); - translateSpatial = rotateSpatial = true; + SpotLight sl = (SpotLight) light; + lightPosition.set(sl.getPosition()); + lightDirection.set(sl.getDirection()); + if (invertAxisDirection) { + lightDirection.negateLocal(); + } + translateSpatial = true; + rotateSpatial = true; } + + // Transform light's world properties to spatial's parent's local space if (spatial.getParent() != null) { + // Get inverse of parent's world matrix spatial.getParent().getLocalToWorldMatrix(vars.tempMat4).invertLocal(); - vars.tempMat4.rotateVect(translation); - vars.tempMat4.translateVect(translation); - vars.tempMat4.rotateVect(direction); + vars.tempMat4.rotateVect(lightPosition); + vars.tempMat4.translateVect(lightPosition); + vars.tempMat4.rotateVect(lightDirection); } + // Apply transformed properties to spatial's local transformation if (rotateSpatial) { - rotation.lookAt(direction, Vector3f.UNIT_Y).normalizeLocal(); + rotation.lookAt(lightDirection, Vector3f.UNIT_Y).normalizeLocal(); spatial.setLocalRotation(rotation); } if (translateSpatial) { - spatial.setLocalTranslation(translation); + spatial.setLocalTranslation(lightPosition); } vars.release(); } @@ -214,15 +295,31 @@ public void cloneFields(final Cloner cloner, final Object original) { public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule ic = im.getCapsule(this); - controlDir = ic.readEnum(CONTROL_DIR_NAME, ControlDirection.class, ControlDirection.SpatialToLight); - light = (Light) ic.readSavable(LIGHT_NAME, null); + light = (Light) ic.readSavable("light", null); + controlDir = ic.readEnum("controlDir", ControlDirection.class, ControlDirection.SpatialToLight); + axisRotation = ic.readEnum("axisRotation", Axis.class, Axis.Z); + invertAxisDirection = ic.readBoolean("invertAxisDirection", false); } @Override public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); - oc.write(controlDir, CONTROL_DIR_NAME, ControlDirection.SpatialToLight); - oc.write(light, LIGHT_NAME, null); + oc.write(light, "light", null); + oc.write(controlDir, "controlDir", ControlDirection.SpatialToLight); + oc.write(axisRotation, "axisRotation", Axis.Z); + oc.write(invertAxisDirection, "invertAxisDirection", false); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[light=" + (light != null ? light.getType() : null) + + ", controlDir=" + controlDir + + ", axisRotation=" + axisRotation + + ", invertAxisDirection=" + invertAxisDirection + + ", enabled=" + enabled + + ", spatial=" + spatial + + "]"; } -} \ No newline at end of file +}