Skip to content
217 changes: 157 additions & 60 deletions jme3-core/src/main/java/com/jme3/scene/control/LightControl.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}.
* <p>
* This is particularly useful for attaching lights to animated characters,
* moving vehicles, or dynamically controlled objects.
* </p>
*
* @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;
}
Expand All @@ -104,6 +129,7 @@ public Light getLight() {
}

public void setLight(Light light) {
validateSupportedLightType(light);
this.light = light;
}

Expand All @@ -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();
}
Expand All @@ -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 +
"]";
}
}
}