Advanced Custom Scene Graph Objects
In Custom Scene Graph Objects we have created a simple class as custom scene graph object. The Brick class is derived from Group. It is barely more than a thin wrapper around Group which adds and manages the actual scene graph objects as children of the group. We will continue with the brick example here in order to demonstrate how to
- enable the Brick class to automatically create copies of existing bricks and to
- enable instances of Brick to implement custom interaction logic which works in conjunction with synchronized scenes in ILNumerics Visualization Engine.
If you haven't done yet, we recommend to go to the Custom Scene Graph Objects article and/or understand the concepts described there before continueing with this section.
Copying Custom Scene Graph Objects
One of the big advantages of having a dedicated class for complicated scene graph objects is the capability of reusing the object and the logic it is built upon. Without special care, multiple usages of the same object will lead to the base class (Group) of the custom object being created as the result of the Brick copy operation. Such copies will work (render and display) as expected in most cases. But if the rendering relies on certain programming logic, copies created that way (and downcasted to Group) will miss that logic - hence, rendering might fail.
The efficient creation of scene graph copies is an important part of the ILNumerics Visualization Engine. It is recommended to implement the following methods in order to suport the auto-copy scheme.
In order to support automatic copying of custom scene graph objects, one must implement the Node.Copy() method:
It is recommended to implement the Copy() method in conjunction with a copy constructor. This is a simple and reliable way of ensuring the copies to be of the correct type and have all properties provided by the base class(es):
Since our simple Brick class does not implement any private attributes there is not much to be done in the copy constructor. The only important part is to forward the call to the base class which will automatically create copies of all children in Brick.
What have we gained now? If we attempt to add the same instance of a (potentially preconfigured) scene graph object to a scene multiple times, the objects being created during the copy operations will all be of type Brick instead of Group:
While without the overriden Node.Copy() method the camera node contained all objects of type Group, this time we have created and added objects of type Brick:
Consequently, we are now able to iterate over the genue Brick objects in the scene and use all methods provided by them:
Synchronizing Custom Scene Graph Objects
While the implementation of Node.Copy() is recommended even for simple custom scene graph objects, the following capabilities are only important for certain situations. Their implementation is optional. By leaving them out of your custom class, the custom objects still integrate smoothly into the general scene graph handling of ILNumerics Visualization Engine. One can create and add arbitrary objects to a scene, query the objects and copies thereof by referencing their actual custom type and utilize all properties and methods of the custom object.
ILNumerics Visualization Engine maintains synchronized copies of the global scene and uses such copies for rendering instead of the global scene source. The process of creating and maintaining such synchronized copies exposes some subtle differences to Node.Copy(). Therefore, in order for your custom objects to end up in the final driver eventually, some additional methods needs to be implemented.
By skipping the implementation of the methods described below, the scene graph and all your custom objects will still render correctly (in all but very special cases).
When do I need to support synchronizing copies of my custom object?
One common reason to actually implement these methods is to support interactivity, like custom mouse event handling. Although most event handling can be realized on the common shapes in ILNumerics Visualization Engine, one would be limited to the properties and methods exposed by them. All enhanced information provided by a custom scene graph object would not be available since the events - by default - are only triggered on objects of such common base types.
Another reason to implement synchronizing methods is to be able to interfere with the rendering pipeline. Usually, one can achieve this by overriding protected methods like Node.BeginVisit. Therefore it is necessary to actually create a class derived from Group or Node and override those methods. Implementing below methods will make sure that the object in the synchronized scene used for rendering will be of the actual derived type.
Synchronizing Custom Scene Graph Example
Let's pimp the Brick class a little! We add a double Value as property, backed with a private attribute. The value is used to color the faces of the brick corresponding to a colormap. Also, a label is added displaying the current value of the brick. The label will be repositioned dynamically at runtime: it is always placed at that corner of the brick which is closest to the camera.
We show the complete code first and go through the interesting parts in the following paragraphs. A final version of the complete example can be downloaded at the end of this section.
Guidelines for implementing Custom Properties
Properties exposing a child of your custom object as standard ILNumerics Visualization Engine object are very easily implemented as follows:
Just make sure to identify the child uniquely. Using predefined string tags as static readonly constants is recommended here. The property simply gives away a reference to the child object.
New properties of the custom class should be backed by private instance fields. Common framework rules apply for getter and setter implementation:
Make sure to signal a change to the value by calling OnPropertyChanged("Name") with the name of the property. This will not only give users of your class the chance to react on the change but also tell the synchronized copy of your class to update accordingly. In order to minimize overhead, make sure to enter the change process only if the existing value does really differ from the new value.
Care should be taken when exposing public properties of a type imported from an external library (like ILNumerics.dll). If the lib is strongly typed, .NET may store strongly typed references to the lib if the property is used in conjunction with resource files of UI elements. Issues might arise on updates to the lib. In order to prevent from them, it is recommended to use the DesignerSerializationVisibility attribute with a value of Hidden on the property.
Guidelines on Constructor Implementations
Every full featured custom scene graph object class should implement at least three constructors:
- At least one public standard constructor, intended to be used by users of the class. This constructor offers several (optional) arguements which control the creation of your custom class. You may decide to provide several overloads for a more convenient API.
- A private constructor which is only used in order to create an empty instance of your class. This constructor gets called when the ILNumerics Visualization Engine detects the need to create a synchronized copy of your object to be used in the synchronized scene for rendering. The sole purpose of this constructor is to create a new class of the correct type. All properties / attributes / children etc. are later synchronized (in the Synchronize() method). Update: make this constructor protected if you want to support class derivation.
- A protected copy constructor. This is needed in order to allow copies of the object to be made efficiently and reliably. One example implementation has been given above.
All three constructors from the Brick example:
New Options for Synchronized Custom Objects
By implementing the methods described above we gain one important advantage over a naive implementation: our type now ends up in all scene graph instances - regardless if it has been copied or synchronized. Therefore, we have all methods ready to get used. Furthermore, we can now decide to override any virtual protected base class method in order to get much more control over the way the object is rendered. In fact, we could even implement our own rendering algorithm here, if this is necessary.
Returning to our example, we override the BeginVisit method of the Group base class. This method is called on every frame, just before the brick is rendered. In the method we find the vertex which is closest to the camera and use its coordinates as the position of the brick label. Since the nearest vertex changes with user interaction (rotation, pan etc.) this calculation needs to be done dynamically on every rendering frame. An alternative implemetation would utilize BeginRenderingFrame of the panel and iterate all bricks in the scene. Compare this with the following implementation which is much easier and offers higher flexibility:
Note how the method does all calculations only once for a frame. For correct rendering the scene needs to be walked several times. The count of the walks is provided in the parameter.CurrentPassCount property. We execute the computational expensive parts only on the first tree walk by filtering for value 1.
The actual code is straightforward: we fetch the positions of the brick and transform them into screen coordinates (exercise: only going to clip coordinates would be sufficient here). The nearest z coordinate is found and its index points to the vertex closest to the camera. This vertex is than used as the position for the label.
Note, how the label was included into a sperate group, having a render target of Screen2DFar. This ensures that the label will always be drawn on top of the common scene graph (World3D) nodes.
Executing this example will give a form similar to this. Rotating the form will dynamically reposition the labels for each brick to the corner of the brick which is closest to the camera.
Visit the full example in our examples section.