Areas
  • J3D FAQ
  • Tutorials
  • Utilities
  • Books
  • News
  • Other Sites
  •  Chapters
  • TOC
  • Getting Started
  • Lighting
  • Textures
  • Behaviours
  • User Feedback
  • Navigation
  • Audio
  • Input Devices
  • No Monitors
  •  Sections
  • Chapter TOC
  • Built-in Behaviours
  • Interpolators
  • Summary
  •   

    Behaviour Fundamentals

    © Justin Couch 2000

    Unlike many other 3D graphics APIs and even most 2D APIs, behaviours form a fundamental part of the Java3D system. As a rough estimate, 80% of all non-static scene graph interactions at the code level will have originated with a behaviour. Typically a behaviour is what triggers your code to start running, which in turn makes modifications to the scene graph. A cascade effect may then start where one behaviour triggers a series of follow-on actions. For example, the behaviour of pressing the up arrow key would move the user forward , this then makes an object become visible causing some animation to start. At the same time, this action could also cause the user to collide with another object forcing the user to move in reaction; and so the chain reaction goes on.

      

    Scene Graph Relationships

    Behaviours form part of the standard scene graph, just like pieces of geometry do. In the same way that a piece of geometry not existing in the renderable scene graph is not drawn, a behaviour not existing in the renderable scene graph will not be executed.

    The Behavior class extends the Leaf class suggesting that it too forms part of a normal scene graph. As you will see shortly, the execution of the behaviour depends on that instance being placed along side the nodes.

    Being a leaf node in the scene graph structure allows you to place behaviours almost anywhere that you can place geometry. At the same time this implies that as you move an object around, its behaviour moves with it. In a sort of OO way, the behaviours and geometry move together. As you remove a part of the scene graph and place it under a different parent, any associated behaviours also move with it.

    Although the behaviour is added as part of the scene graph, it does not form a part of it. Figure 1 illustrates the common representation used of behaviours. As you can see, the behaviour is connected to the scene graph but is kept in another area of the Java3D runtime environment. There is a whole portion devoted purely to behaviour execution: deciding when to run each behaviour, storage and other management issues.

    Scenegraph depiction
    Figure 1: The structure relationship between geometry and behaviours in a scene graph

       Note
      The objects in this picture seem to be the common expressions that have evolved around J3D tutorials that have been presented over the past few years. I don’t know who originated them, but this is the general style in use rather than the more formal and trendy UML syntax.

      

    Behavior Base Class

    All behaviours within Java3D must extend the Behavior base class. This class is used as a signature by the runtime environment to make sure that it gets treated differently. To create a behaviour you must extend this base class and then add it to the scene graph at the appropriate place that you want it to effect. For example, if you want to have a rotating cube then you place the rotation behaviour under the same transform as the cube.

    With the collection of pre-built behaviours available there is little more that you need to do. The environment calls methods in the behaviour to make sure the appropriate actions take place. So what methods are called?

    Most of the methods provided by the base class provide full implementations so you rarely need to add any more. When a behaviour is first added to the scene graph it needs to be put into a known condition. Java3D calls the initialize() method to make sure that your behaviour is in that known state. Then, each time something happens that causes the behaviour to be executed, the processStimulus() method is called. A stimulus is the Java3D term for the notification that is sent to behaviour to tell it that it is time to do something.

    The initialize() method is typically used by the behaviour to register the types of criteria that the behaviour should be executed on. For example, you might only want to execute when the object becomes visible or it may be some complex combination of criteria. Another use for the method is to set the related object into a known condition. Our earlier rotation behaviour might set the cube to the starting position so that you don’t get any strange jumping effects when the real rotation starts. Alternatively the behaviour might read the cube’s starting rotation and progresses from there.

    processStimulus() is called every time that the runtime decides that your behaviour should be run. Apart from the conditions that you set during the initialize method, you have no control over when this method is called. However, once it has been called, you need to take the appropriate action. For the rotating cube it should change the orientation of the cube to the next value. Because this method is called between renders of individual frames, you need to be careful with the amount of work that is done here. The more work, the longer it takes to get back to rendering and results in lowering the frame rate.

    Most of the important methods are covered in more detail in the next couple of sections, but the following are also useful:

    • setEnable() Used to enable and disable the behaviour execution on demand. You may have needs to disable the behaviour because it is inappropriate to perform it at the current time. This could be used to switch in and out various behaviours without needing to make changes to the scene graph during runtime.
    • getView() Used to fetch the current view object for calculation of various items. You might want to know where the viewer currently is relative to your object that the behaviour is effecting.

    Criteria for Running

    Definition of when a behaviour is to execute is fulfilled through the use of criteria classes. These classes define how to control when a behaviour is to be run. There are two parts to deciding how to run a behaviour:
    1. The statement of what sort of condition that a behaviour should be run. This might be because another object has collided with this object or that the user has clicked on the screen. These classes are derived from WakeupCriterion and define a single type of condition.
    2. The combination of individual conditions that will result in the behaviour executing. These classes based on WakeupCondition provide boolean logic combinations of individual criteria. For example you can make sure that a behaviour is only executed after every third frame has been rendered or after an elapsed time of 300 milliseconds.
    Once you have decided on the combination of these two sets of classes you then need to set it all up. The example in the second point above would be coded as:
    public void initialize() {
      WakeupCriterion[] criteria = new WakeupCriterion[2];
      criteria[0] = new WakeupOnElapsedFrames(3);
      criteria[1] = new WakeupOnElapsedTime(300);
    
      WakeupCondition condition = new WakeupOr(criteria);
      wakeupOn(condition);
    }
    
    The last line is a call to the protected method wakeupOn(). This method is used to inform the runtime system that a new set of conditions are now to be used. The default behaviour is to not run the behaviour at all. If you call the method anywhere else except in initialize or processStimulus() an IllegalStateException is generated. There is quite a list of both criteria for operation and combining them together. Although you usually won’t need much more than a simple AND or OR, a good grasp of boolean logic (and De Morgan's Theorem) will help you optimise the combinations to provide minimal overheads. When the processStimulus() method is called, you are given an Enumeration of the conditions that were fulfilled. This returns only the criteria, not the combination of conditions required. In the earlier example we could use this to determine how much time has elapsed, and therefore how much to adjust our output by. If we got frames elapsed conditions then it means the system is lightly loaded, but if the elapsed time criteria has passed, maybe we should look at lightening the calculation load a bit to keep the frame rates up.

    Bounding & Scheduling

    So far the assumption has been that behaviours are always executed assuming that the appropriate criteria are met. In reality, this is far from the case. Because a really complex world may have hundreds of potential behaviours, trying to decide on every frame whether any given behaviour should be run can be very taxing on the system. To relieve this problem Java3D uses two mechanisms: bounds on the behaviour and selective scheduling.

    Scheduling is actually dependent on the bounds setup so we shall look at that first. Bounds are used to define when an object should be added to the list of scheduled objects. That is, bounds are used to control the distance from the target object a behaviour is to be executed. As you saw in the previous chapter with the headlight example, we can create any arbitrary shape and bounding region. A simple spherical radius may be all that is wanted, while others, such as a collidable object may want a bounding box.

    Once you have created the desired region you may then inform the system using the setSchedulingBounds() method. The bounds are centered relative to the parent group that you have placed it in. For our rotating cube example, that means the parent transform that is used to rotate the cube forms the local origin of the bounds.

    Setting a scheduling bounds on the behaviour is only half the solution. How do you know when that bounds has been entered? The answer is that it lies in the combination of two sets of bounds: the bounds of the behaviour and the bounds of the current View. The view part is important because it too has a set of bounds that decides what can be seen close to the user and a maximum distance. The Java3D specification defines that processStimulus() method is called when the two bounds intersect. Figures 2 to 4 illustrate the three conditions that could occur. The camera on the left of the picture is moving right towards the object (plus behaviour) on the right. For illustrative purposes, the object is a clown on a unicycle going around in circles.

    Object scheduling doesn't intersect with view bounds
    Figure 2: The object scheduling bounds completely outside the view bounds.

    At the start, the view is completely separated from the bounds of the behaviour. (Figure 11-2) Because the two bounds don’t intersect, the behaviour is not permitted to operate regardless of the criteria that has been set. At no time will we get to move the clown about the world.

    Intersection of scheduling and view bounds
    Figure 3: The object’s scheduling bounds and view bound intersect.

    As the view moves to the right, the bounds start to intersect (Figure 3). Assuming that the appropriate criteria are satisfied, the behaviour is run. Why might this be the case? Well the object, as a result of the behaviour may start to travel towards the camera. Because the clown’s starting position (relative to the camera) is outside the field of view but his path crosses into the field of view, we really want to see the clown move. The clown will slowly circle now and come into view for part of the time that the behaviour is executed. This is the natural behaviour that we expect.

    Complete overlap of bounds
    Figure 4: The object scheduling bounds is completely inside the view bounds.

    Finally, as we draw closer, the complete scheduling bounds falls inside the view bounds. There is no change from the partial intersection case except for the fact that we now see the clown for the full circle. The behaviour still runs all the time.

      

    [ TOC ] [ Home ] [ FAQ ] [ Books ] [ Tutorials ] [ Utilities ] [ Contact Us ]