Contents

Chapters

Sections

j3d.org

[ Previous ] [ Up ] [ Next ]

Sensors

© Justin Couch 2000

Sensors are the other half of the custom input device handling. A sensor represents a single element on your input device that may produce information. Within a device, the sensors are not required to produce the same amount of information. You will notice that in the above code examples, we neglected to mention how to get information out from the input device to the runtime.

If you look at a standard joystick, there are two degrees of freedom - forwards/backwards and left/right. There are also usually at least two buttons. This corresponds to a single sensor - the combination of one item that can have up to 6DOF and some buttons. On a high end joystick, you may have two or more sensors. The hat on the joystick will usually provide a second sensor as a set of 6DOF input. The hat may be coded to produce pure translation information (slides) where the main stick will produce roll and pitch changes (orientation). You may choose to use these seperately as two sensors, or a single combined sensor, depending on your device driver. Where you choose to associate the buttons is a matter or personal preference. Note that although they are physically the same device input system, one has more sensors than the other.

In our example system, we have only one sensor for that device. When we construct the input device, we also need to construct all of the sensors that it generates. Typically this is done at the construction stage, but may be generated on demand. Each sensor instance is given a reference to the InputDevice providing the data. That is all that needs to be done. The Java 3D runtime environment takes care of the rest.

Typically, the sensors are created during the initialize method call that we use to create the rest of the device. In this case we only have a single sensor so we can hard code the rest of the supporting methods:

private static final int NUM_SENSOR_READS = 5;
private Sensor sensor;

public boolean initialize()
{
    sensor = new Sensor(this, NUM_SENSOR_READS);

    // etc
}

public int getSensorCount()
{
    return 1;
}

public Sensor getSensor(int sensorIndex)
{
    return sensor;
}
Sensor instances for a particular InputDevice should not change with time. Once you have a sensor representing a particular element in the physical device, it should never be replaced with a new instance. This can cause all sorts of strange behaviour within the application and environment. A sensor is fixed, particularly when it comes to reading the information from the sensor to use within your application.

Reading information from the Sensor does not provide the transformation matrix directly. Instead we have a third class called SensorRead that encapsulates everything that needs to be known about that state of the sensor at a given point in time. If this was not provided, you would need to make multiple calls to the Sensor class directly for different information. That short gap even between two consecutive method calls may be enough to provide two wildly different views of the world (button pressed, v's not pressed). At the time that a device is read, it fills in the SensorRead class with all of the available information and passes that to the Sensor. Having said that, both methods of accessing sensor information are available from the Sensor class.

When the sensor is used to track head or hand information, sometimes some smoothing of motions needs to be taken into account. A head tracker may only generate 10 updates a second, but we are generating 30 frames a second. For these cases, the in between values can be reasonably precisely predicted using standard linear interpolation. If you know that the sensor is reading head or hand input information you may wish to set the prediction policy on the sensor. Setting this policy to either HAND_PREDICTOR or head_PREDICTOR allows the runtime environment to make some assumptions about device input and allow for smoothing.

To deal with prediction, the Sensor object keeps track of a number of SensorReads. Depending on the processing method the input device, it may be fetching these ahead of time as well. You will note that when we constructed the Sensor object that we gave it an integer describing the number of Sensor reads to keep. The default is 30, but considering that the config file is probably not likely to contain that many values, we've chopped that number down to a much smaller value of five;

Generating Sensor Information

In the Java 3D view of the world, the sensors don't actually do anything. That is, you don't need to extend the implementation of the Sensor class as everything is provided. Your input device is told to read the latest information and place that in the Sensor object. When the application wishes to use sensor information, then it gets the latest information from that Sensor and applies it in the way that you have coded (for example, to modify a transform of a TransformGroup in the scene graph).

Generating new sensor information should only be done in response to a call to the pollAndProcessInput() method of the InputDevice. This is the runtime instructing your device to load the next lot of information into each of the sensors. This method is always called in accordance with the processing mode that you've set for the device. It is never called by application code. When called, you know exactly what the time is from the system clock and can construct the next SensorRead object, one for each sensor, and place that in the corresponding Sensor.

In our example device, we use the basic parameterized version of a line between two points (for one component)

x(t) = x0 + t(x0 - x1)
A whole bunch of pre-calculation has been done, but to show what happens next, this is the code. x_diff is the last part of the equation in parantheses.

private Transform3D position_tx = new Transform3D();
private Vector3f translation = new Vector3f();

public void pollAndProcessInput()
{
    ...

    translation.x = x0[leg_num] + delta_t * x_diff[leg_num];
    translation.y = y0[leg_num] + delta_t * y_diff[leg_num];
    translation.z = z0[leg_num] + delta_t * z_diff[leg_num];

    position_tx.setTranslation(translation);

    sensor.setNextSensorRead(current_time,
                             position_tx,
                             EMPTY_BUTTONS);
}
To minimize the amount of garbage collection, you should always avoid constructing new objects wherever possible. It is better that you keep the current sensor read instance and set new information than creating a whole collection of new instances. However, you must bear in mind that the Sensor object keeps track of the last n inputs and that may be used by the application, which could cause problems if you always reuse the SensorRead instance. The EMPTY_BUTTONS variable is a zero length array of ints to indicate that there are no buttons for this device.

Reacting to Sensor Input

There are various ways that you might want to react to sensor input. If you know that the application is using a joystick, then you may want to read sensor data every single frame. Other times you may only want something to happen when the sensor enters a particular volume of space. In either case, it requires the use of behaviours to trigger your application code to read information from the sensors and apply it to the scene graph.

Using our example application again, we will use two input devices. One will control the user's position, the other will control an object in the scene. For the smoothest possible movement, we want to act on every frame. This means that we should create a Behavior that uses WakeupOnElapsedFrames as the criteria for wake up. Each time the behavior is called it asks the sensor for the current transform value and uses that to change the TransfromGroup of whatever object it is that we are dealing with. For our example illustration we created the SensorBehavior class. This uses the elapsed frame criteria to wake up. When it is activated it calls the sensor, reads the value and then passes it to the geometry's parent TransformGroup:

public class SensorBehavior extends Behavior
{
    private Sensor sensor;
    private TransformGroup tx_group;
    private Transform3D tx;

    private int elapsed_frame_delay;
    private WakeupCondition criteria;

    public SensorBehavior(Sensor sensor, TransformGroup tg)
    {
        this(sensor, tg, 0);
    }

    public SensorBehavior(Sensor sensor,
                          TransformGroup tg,
                          int elapsedFrames)
    {
        if(!tg.getCapability(TransformGroup.ALLOW_TRANSFORM_WRITE))
            throw new IllegalStateException("Cannot write to transform");

        this.sensor = sensor;
        tx_group = tg;
        elapsed_frame_delay = elapsedFrames;

        tx = new Transform3D();
    }

    public void initialize()
    {
        criteria = new WakeupOnElapsedFrames(elapsed_frame_delay);

        wakeupOn(criteria);
    }

    public void processStimulus(Enumeration crits)
    {
        sensor.getRead(tx);
        tx_group.setTransform(tx);

        wakeupOn(criteria);
    }
}
As we have done with the other examples, we always create a single instance of objects and reuse them wherever possible. Note also that this behaviour is like our mouse behavior where we have to re-register the wake up criteria for each call to processStimulus().

An alternate to this is to only create behaviors that change when the sensor enters an area. Using the WakeupOnSensorEntry criteria you can create behaviors that only fire when your sensors enter the given bounding region. Unfortunately, none of the Java 3D documentation or specification actually define what is meant by a "sensor" entering the area. Based on some simple tests, it seems to relate to only the standard sensors that are used to define non AWT based input like a head mounted display or data glove. Unfortunately the documentation from Sun is very poor explaining exactly what these criteria do.